diff --git a/Starcraft2Client.py b/Starcraft2Client.py index fb219a69..14e18320 100644 --- a/Starcraft2Client.py +++ b/Starcraft2Client.py @@ -3,9 +3,11 @@ from __future__ import annotations import ModuleUpdate ModuleUpdate.update() -from worlds.sc2.Client import launch +from worlds.sc2.client import launch import Utils +# This is deprecated, replaced with the client hooked from the Launcher +# Will be removed in a following release if __name__ == "__main__": Utils.init_logging("Starcraft2Client", exception_logger="Client") launch() diff --git a/WebHostLib/static/assets/sc2Tracker.js b/WebHostLib/static/assets/sc2Tracker.js index 30d4acd6..19cff21c 100644 --- a/WebHostLib/static/assets/sc2Tracker.js +++ b/WebHostLib/static/assets/sc2Tracker.js @@ -1,49 +1,43 @@ +let updateSection = (sectionName, fakeDOM) => { + document.getElementById(sectionName).innerHTML = fakeDOM.getElementById(sectionName).innerHTML; +} + window.addEventListener('load', () => { - // Reload tracker every 15 seconds - const url = window.location; - setInterval(() => { - const ajax = new XMLHttpRequest(); - ajax.onreadystatechange = () => { - if (ajax.readyState !== 4) { return; } + // Reload tracker every 60 seconds (sync'd) + const url = window.location; + // Note: This synchronization code is adapted from code in trackerCommon.js + const targetSecond = parseInt(document.getElementById('player-tracker').getAttribute('data-second')) + 3; + console.log("Target second of refresh: " + targetSecond); - // Create a fake DOM using the returned HTML - const domParser = new DOMParser(); - const fakeDOM = domParser.parseFromString(ajax.responseText, 'text/html'); - - // Update item tracker - document.getElementById('inventory-table').innerHTML = fakeDOM.getElementById('inventory-table').innerHTML; - // Update only counters in the location-table - let counters = document.getElementsByClassName('counter'); - const fakeCounters = fakeDOM.getElementsByClassName('counter'); - for (let i = 0; i < counters.length; i++) { - counters[i].innerHTML = fakeCounters[i].innerHTML; - } + let getSleepTimeSeconds = () => { + // -40 % 60 is -40, which is absolutely wrong and should burn + var sleepSeconds = (((targetSecond - new Date().getSeconds()) % 60) + 60) % 60; + return sleepSeconds || 60; }; - ajax.open('GET', url); - ajax.send(); - }, 15000) - // Collapsible advancement sections - const categories = document.getElementsByClassName("location-category"); - for (let category of categories) { - let hide_id = category.id.split('_')[0]; - if (hide_id === 'Total') { - continue; - } - 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 orig_text = tab_header.innerHTML; - let new_text; - if (orig_text.includes("▼")) { - new_text = orig_text.replace("▼", "▲"); - } - else { - new_text = orig_text.replace("▲", "▼"); - } - tab_header.innerHTML = new_text; - }); - } + let updateTracker = () => { + const ajax = new XMLHttpRequest(); + ajax.onreadystatechange = () => { + if (ajax.readyState !== 4) { return; } + + // Create a fake DOM using the returned HTML + const domParser = new DOMParser(); + const fakeDOM = domParser.parseFromString(ajax.responseText, 'text/html'); + + // Update dynamic sections + updateSection('player-info', fakeDOM); + updateSection('section-filler', fakeDOM); + updateSection('section-terran', fakeDOM); + updateSection('section-zerg', fakeDOM); + updateSection('section-protoss', fakeDOM); + updateSection('section-nova', fakeDOM); + updateSection('section-kerrigan', fakeDOM); + updateSection('section-keys', fakeDOM); + updateSection('section-locations', fakeDOM); + }; + ajax.open('GET', url); + ajax.send(); + updater = setTimeout(updateTracker, getSleepTimeSeconds() * 1000); + }; + window.updater = setTimeout(updateTracker, getSleepTimeSeconds() * 1000); }); diff --git a/WebHostLib/static/styles/sc2Tracker.css b/WebHostLib/static/styles/sc2Tracker.css index 29a719a1..3048213e 100644 --- a/WebHostLib/static/styles/sc2Tracker.css +++ b/WebHostLib/static/styles/sc2Tracker.css @@ -1,160 +1,279 @@ -#player-tracker-wrapper{ - margin: 0; +*{ + margin: 0; + font-family: "JuraBook", monospace; +} +body{ + --icon-size: 36px; + --item-class-padding: 4px; +} +a{ + color: #1ae; } -#tracker-table td { - vertical-align: top; +/* Section colours */ +#player-info{ + background-color: #37a; +} +.player-tracker{ + max-width: 100%; +} +.tracker-section{ + background-color: grey; +} +#terran-items{ + background-color: #3a7; +} +#zerg-items{ + background-color: #d94; +} +#protoss-items{ + background-color: #37a; +} +#nova-items{ + background-color: #777; +} +#kerrigan-items{ + background-color: #a37; +} +#keys{ + background-color: #aa2; } -.inventory-table-area{ - border: 2px solid #000000; - border-radius: 4px; - padding: 3px 10px 3px 10px; +/* Sections */ +.section-body{ + display: flex; + flex-flow: row wrap; + justify-content: flex-start; + align-items: flex-start; + padding-bottom: 3px; +} +.section-body-2{ + display: flex; + flex-direction: column; +} +.tracker-section:has(input.collapse-section[type=checkbox]:checked) .section-body, +.tracker-section:has(input.collapse-section[type=checkbox]:checked) .section-body-2{ + display: none; +} +.section-title{ + position: relative; + border-bottom: 3px solid black; + /* Prevent text selection */ + user-select: none; + -webkit-user-select: none; + -ms-user-select: none; +} +input[type="checkbox"]{ + position: absolute; + cursor: pointer; + opacity: 0; + z-index: 1; + width: 100%; + height: 100%; +} +.section-title:hover h2{ + text-shadow: 0 0 4px #ddd; +} +.f { + display: flex; + overflow: hidden; } -.inventory-table-area:has(.inventory-table-terran) { - width: 690px; - background-color: #525494; +/* Acquire item filters */ +.tracker-section img{ + height: 100%; + width: var(--icon-size); + height: var(--icon-size); + background-color: black; +} +.unacquired, .lvl-0 .f{ + filter: grayscale(100%) contrast(80%) brightness(42%) blur(0.5px); +} +.spacer{ + width: var(--icon-size); + height: var(--icon-size); } -.inventory-table-area:has(.inventory-table-zerg) { - width: 360px; - background-color: #9d60d2; +/* Item groups */ +.item-class{ + display: flex; + flex-flow: column; + justify-content: center; + padding: var(--item-class-padding); +} +.item-class-header{ + display: flex; + flex-flow: row; +} +.item-class-upgrades{ + /* Note: {display: flex; flex-flow: column wrap} */ + /* just breaks on Firefox (width does not scale to content) */ + display: grid; + grid-template-rows: repeat(4, auto); + grid-auto-flow: column; } -.inventory-table-area:has(.inventory-table-protoss) { - width: 400px; - background-color: #d2b260; +/* Subsections */ +.section-toc{ + display: flex; + flex-direction: row; +} +.toc-box{ + position: relative; + padding-left: 15px; + padding-right: 15px; +} +.toc-box:hover{ + text-shadow: 0 0 7px white; +} +.ss-header{ + position: relative; + text-align: center; + writing-mode: sideways-lr; + user-select: none; + padding-top: 5px; + font-size: 115%; +} +.tracker-section:has(input.ss-1-toggle:checked) .ss-1{ + display: none; +} +.tracker-section:has(input.ss-2-toggle:checked) .ss-2{ + display: none; +} +.tracker-section:has(input.ss-3-toggle:checked) .ss-3{ + display: none; +} +.tracker-section:has(input.ss-4-toggle:checked) .ss-4{ + display: none; +} +.tracker-section:has(input.ss-5-toggle:checked) .ss-5{ + display: none; +} +.tracker-section:has(input.ss-6-toggle:checked) .ss-6{ + display: none; +} +.tracker-section:has(input.ss-7-toggle:checked) .ss-7{ + display: none; +} +.tracker-section:has(input.ss-1-toggle:hover) .ss-1{ + background-color: #fff5; + box-shadow: 0 0 1px 1px white; +} +.tracker-section:has(input.ss-2-toggle:hover) .ss-2{ + background-color: #fff5; + box-shadow: 0 0 1px 1px white; +} +.tracker-section:has(input.ss-3-toggle:hover) .ss-3{ + background-color: #fff5; + box-shadow: 0 0 1px 1px white; +} +.tracker-section:has(input.ss-4-toggle:hover) .ss-4{ + background-color: #fff5; + box-shadow: 0 0 1px 1px white; +} +.tracker-section:has(input.ss-5-toggle:hover) .ss-5{ + background-color: #fff5; + box-shadow: 0 0 1px 1px white; +} +.tracker-section:has(input.ss-6-toggle:hover) .ss-6{ + background-color: #fff5; + box-shadow: 0 0 1px 1px white; +} +.tracker-section:has(input.ss-7-toggle:hover) .ss-7{ + background-color: #fff5; + box-shadow: 0 0 1px 1px white; } -#tracker-table .inventory-table td{ - width: 40px; - height: 40px; - text-align: center; - vertical-align: middle; +/* Progressive items */ +.progressive{ + max-height: var(--icon-size); + display: contents; } -.inventory-table td.title{ - padding-top: 10px; - height: 20px; - font-family: "JuraBook", monospace; - font-size: 16px; - font-weight: bold; +.lvl-0 > :nth-child(2), +.lvl-0 > :nth-child(3), +.lvl-0 > :nth-child(4), +.lvl-0 > :nth-child(5){ + display: none; +} +.lvl-1 > :nth-child(2), +.lvl-1 > :nth-child(3), +.lvl-1 > :nth-child(4), +.lvl-1 > :nth-child(5){ + display: none; +} +.lvl-2 > :nth-child(1), +.lvl-2 > :nth-child(3), +.lvl-2 > :nth-child(4), +.lvl-2 > :nth-child(5){ + display: none; +} +.lvl-3 > :nth-child(1), +.lvl-3 > :nth-child(2), +.lvl-3 > :nth-child(4), +.lvl-3 > :nth-child(5){ + display: none; +} +.lvl-4 > :nth-child(1), +.lvl-4 > :nth-child(2), +.lvl-4 > :nth-child(3), +.lvl-4 > :nth-child(5){ + display: none; +} +.lvl-5 > :nth-child(1), +.lvl-5 > :nth-child(2), +.lvl-5 > :nth-child(3), +.lvl-5 > :nth-child(4){ + display: none; } -.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; +/* Filler item counters */ +.item-counter{ + display: table; + text-align: center; + padding: var(--item-class-padding); +} +.item-count{ + display: table-cell; + vertical-align: middle; + padding-left: 3px; + padding-right: 15px; } -.inventory-table img.acquired{ - filter: none; - background-color: black; +/* Hidden items */ +.hidden-class:not(:has(img.acquired)){ + display: none; +} +.hidden-item:not(.acquired){ + display:none; } -.inventory-table .tint-terran img.acquired { - filter: sepia(100%) saturate(300%) brightness(130%) hue-rotate(120deg) +/* Keys */ +#keys ol, #keys ul{ + columns: 3; + -webkit-columns: 3; + -moz-columns: 3; +} +#keys li{ + padding-right: 15pt; } -.inventory-table .tint-protoss img.acquired { - filter: sepia(100%) saturate(1000%) brightness(110%) hue-rotate(180deg) +/* Locations */ +#section-locations{ + padding-left: 5px; +} +@media only screen and (min-width: 120ch){ + #section-locations ul{ + columns: 2; + -webkit-columns: 2; + -moz-columns: 2; + } +} +#locations li.checked{ + list-style-type: "✔ "; } -.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; -} +/* Allowing scrolling down a little further */ +.bottom-padding{ + min-height: 33vh; +} \ No newline at end of file diff --git a/WebHostLib/static/styles/sc2TrackerAtlas.css b/WebHostLib/static/styles/sc2TrackerAtlas.css new file mode 100644 index 00000000..7fc8746f --- /dev/null +++ b/WebHostLib/static/styles/sc2TrackerAtlas.css @@ -0,0 +1,3965 @@ +.abilityicon_spawnbanelings_square-png{ + clip-path: xywh(0 0.0% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 49.93694829760403%); +} + +.abilityicon_spawnbroodlings_square-png{ + clip-path: xywh(0 0.12610340479192939% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 49.810844892812106%); +} + +.biomassrecovery_coop-png{ + clip-path: xywh(0 0.25220680958385877% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 49.68474148802018%); +} + +.btn-ability-dehaka-airbonusdamage-png{ + clip-path: xywh(0 0.37831021437578816% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 49.558638083228246%); +} + +.btn-ability-hornerhan-fleethyperjump-png{ + clip-path: xywh(0 0.5044136191677175% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 49.43253467843632%); +} + +.btn-ability-hornerhan-raven-analyzetarget-png{ + clip-path: xywh(0 0.6305170239596469% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 49.306431273644385%); +} + +.btn-ability-hornerhan-reaper-flightmode-png{ + clip-path: xywh(0 0.7566204287515763% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 49.18032786885246%); +} + +.btn-ability-hornerhan-salvagebonus-png{ + clip-path: xywh(0 0.8827238335435057% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 49.05422446406053%); +} + +.btn-ability-hornerhan-viking-missileupgrade-png{ + clip-path: xywh(0 1.008827238335435% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 48.9281210592686%); +} + +.btn-ability-hornerhan-viking-piercingattacks-png{ + clip-path: xywh(0 1.1349306431273645% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 48.80201765447667%); +} + +.btn-ability-hornerhan-widowmine-attackrange-png{ + clip-path: xywh(0 1.2610340479192939% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 48.675914249684745%); +} + +.btn-ability-hornerhan-widowmine-deathblossom-png{ + clip-path: xywh(0 1.3871374527112232% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 48.54981084489281%); +} + +.btn-ability-hornerhan-wraith-attackspeed-png{ + clip-path: xywh(0 1.5132408575031526% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 48.423707440100884%); +} + +.btn-ability-kerrigan-abilityefficiency-png{ + clip-path: xywh(0 1.639344262295082% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 48.29760403530895%); +} + +.btn-ability-kerrigan-apocalypse-png{ + clip-path: xywh(0 1.7654476670870114% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 48.17150063051702%); +} + +.btn-ability-kerrigan-automatedextractors-png{ + clip-path: xywh(0 1.8915510718789408% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 48.0453972257251%); +} + +.btn-ability-kerrigan-broodlingnest-png{ + clip-path: xywh(0 2.01765447667087% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 47.91929382093316%); +} + +.btn-ability-kerrigan-droppods-png{ + clip-path: xywh(0 2.1437578814627996% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 47.793190416141236%); +} + +.btn-ability-kerrigan-fury-png{ + clip-path: xywh(0 2.269861286254729% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 47.66708701134931%); +} + +.btn-ability-kerrigan-heroicfortitude-png{ + clip-path: xywh(0 2.3959646910466583% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 47.540983606557376%); +} + +.btn-ability-kerrigan-improvedoverlords-png{ + clip-path: xywh(0 2.5220680958385877% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 47.41488020176545%); +} + +.btn-ability-kerrigan-kineticblast-png{ + clip-path: xywh(0 2.648171500630517% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 47.288776796973515%); +} + +.btn-ability-kerrigan-leapingstrike-png{ + clip-path: xywh(0 2.7742749054224465% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 47.16267339218159%); +} + +.btn-ability-kerrigan-malignantcreep-png{ + clip-path: xywh(0 2.900378310214376% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 47.03656998738966%); +} + +.btn-ability-kerrigan-psychicshift-png{ + clip-path: xywh(0 3.0264817150063053% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 46.91046658259773%); +} + +.btn-ability-kerrigan-revive-png{ + clip-path: xywh(0 3.1525851197982346% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 46.7843631778058%); +} + +.btn-ability-kerrigan-twindrones-png{ + clip-path: xywh(0 3.278688524590164% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 46.658259773013874%); +} + +.btn-ability-kerrigan-vespeneefficiency-png{ + clip-path: xywh(0 3.4047919293820934% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 46.53215636822194%); +} + +.btn-ability-kerrigan-wildmutation-png{ + clip-path: xywh(0 3.530895334174023% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 46.406052963430014%); +} + +.btn-ability-kerrigan-zerglingreconstitution-png{ + clip-path: xywh(0 3.656998738965952% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 46.27994955863808%); +} + +.btn-ability-mengsk-battlecruiser-decksights-png{ + clip-path: xywh(0 3.7831021437578816% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 46.15384615384615%); +} + +.btn-ability-mengsk-ghost-pyrokineticimmolation_orange-png{ + clip-path: xywh(0 3.909205548549811% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 46.02774274905423%); +} + +.btn-ability-mengsk-ghost-staticempblast-png{ + clip-path: xywh(0 4.03530895334174% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 45.90163934426229%); +} + +.btn-ability-mengsk-ghost-tacticalmissilestrike-png{ + clip-path: xywh(0 4.16141235813367% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 45.775535939470366%); +} + +.btn-ability-mengsk-medivac-doublehealbeam-png{ + clip-path: xywh(0 4.287515762925599% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 45.64943253467844%); +} + +.btn-ability-mengsk-medivac-igniteafterburners-png{ + clip-path: xywh(0 4.4136191677175285% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 45.523329129886505%); +} + +.btn-ability-mengsk-siegetank-flyingtankarmament-png{ + clip-path: xywh(0 4.539722572509458% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 45.39722572509458%); +} + +.btn-ability-mengsk-viking-speed-png{ + clip-path: xywh(0 4.665825977301387% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 45.27112232030265%); +} + +.btn-ability-nova-domination-png{ + clip-path: xywh(0 4.791929382093317% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 45.14501891551072%); +} + +.btn-ability-protoss-adept-spiritform-png{ + clip-path: xywh(0 4.918032786885246% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 45.01891551071879%); +} + +.btn-ability-protoss-astralwind-png{ + clip-path: xywh(0 5.044136191677175% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 44.89281210592686%); +} + +.btn-ability-protoss-barrier-upgraded-png{ + clip-path: xywh(0 5.170239596469105% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 44.76670870113493%); +} + +.btn-ability-protoss-blink-color-png{ + clip-path: xywh(0 5.296343001261034% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 44.640605296343004%); +} + +.btn-ability-protoss-blinkshieldrestore-png{ + clip-path: xywh(0 5.422446406052964% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 44.51450189155107%); +} + +.btn-ability-protoss-carrierrepairdrones-png{ + clip-path: xywh(0 5.548549810844893% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 44.388398486759144%); +} + +.btn-ability-protoss-chargedblast-png{ + clip-path: xywh(0 5.674653215636822% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 44.26229508196721%); +} + +.btn-ability-protoss-coronabeam-png{ + clip-path: xywh(0 5.800756620428752% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 44.13619167717528%); +} + +.btn-ability-protoss-disintegration-png{ + clip-path: xywh(0 5.926860025220681% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 44.010088272383356%); +} + +.btn-ability-protoss-disruptionblast-png{ + clip-path: xywh(0 6.0529634300126105% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 43.88398486759142%); +} + +.btn-ability-protoss-doubleshieldrecharge-png{ + clip-path: xywh(0 6.17906683480454% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 43.757881462799496%); +} + +.btn-ability-protoss-dragoonchassis-png{ + clip-path: xywh(0 6.305170239596469% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 43.63177805800757%); +} + +.btn-ability-protoss-dualgravitonbeam-png{ + clip-path: xywh(0 6.431273644388399% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 43.505674653215635%); +} + +.btn-ability-protoss-entomb-png{ + clip-path: xywh(0 6.557377049180328% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 43.37957124842371%); +} + +.btn-ability-protoss-feedback-color-png{ + clip-path: xywh(0 6.683480453972257% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 43.25346784363178%); +} + +.btn-ability-protoss-firebeam-png{ + clip-path: xywh(0 6.809583858764187% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 43.12736443883985%); +} + +.btn-ability-protoss-forcefield-color-png{ + clip-path: xywh(0 6.935687263556116% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 43.00126103404792%); +} + +.btn-ability-protoss-forceofwill-png{ + clip-path: xywh(0 7.061790668348046% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 42.87515762925599%); +} + +.btn-ability-protoss-gravitonbeam-color-png{ + clip-path: xywh(0 7.187894073139975% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 42.74905422446406%); +} + +.btn-ability-protoss-hallucination-color-png{ + clip-path: xywh(0 7.313997477931904% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 42.622950819672134%); +} + +.btn-ability-protoss-lightningdash-png{ + clip-path: xywh(0 7.440100882723834% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 42.4968474148802%); +} + +.btn-ability-protoss-massrecall-png{ + clip-path: xywh(0 7.566204287515763% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 42.37074401008827%); +} + +.btn-ability-protoss-mindblast-png{ + clip-path: xywh(0 7.6923076923076925% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 42.24464060529634%); +} + +.btn-ability-protoss-oracle-stasiscalibration-png{ + clip-path: xywh(0 7.818411097099622% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 42.11853720050441%); +} + +.btn-ability-protoss-oraclepulsarcannonon-png{ + clip-path: xywh(0 7.944514501891551% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 41.992433795712486%); +} + +.btn-ability-protoss-phantomdash-png{ + clip-path: xywh(0 8.07061790668348% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 41.86633039092055%); +} + +.btn-ability-protoss-prismaticrange-png{ + clip-path: xywh(0 8.19672131147541% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 41.740226986128626%); +} + +.btn-ability-protoss-purify-png{ + clip-path: xywh(0 8.32282471626734% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 41.6141235813367%); +} + +.btn-ability-protoss-recallondeath-png{ + clip-path: xywh(0 8.448928121059268% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 41.488020176544765%); +} + +.btn-ability-protoss-reclamation-png{ + clip-path: xywh(0 8.575031525851198% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 41.36191677175284%); +} + +.btn-ability-protoss-shadowdash-png{ + clip-path: xywh(0 8.701134930643127% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 41.23581336696091%); +} + +.btn-ability-protoss-shadowfury-png{ + clip-path: xywh(0 8.827238335435057% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 41.10970996216898%); +} + +.btn-ability-protoss-shieldrecharge-png{ + clip-path: xywh(0 8.953341740226985% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 40.98360655737705%); +} + +.btn-ability-protoss-stasistrap-png{ + clip-path: xywh(0 9.079445145018916% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 40.85750315258512%); +} + +.btn-ability-protoss-supplicant-sacrificeon-png{ + clip-path: xywh(0 9.205548549810844% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 40.73139974779319%); +} + +.btn-ability-protoss-veilofshadowsvorazun-png{ + clip-path: xywh(0 9.331651954602775% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 40.60529634300126%); +} + +.btn-ability-protoss-voidstasis-png{ + clip-path: xywh(0 9.457755359394703% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 40.47919293820933%); +} + +.btn-ability-protoss-vulcanblaster-png{ + clip-path: xywh(0 9.583858764186633% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 40.3530895334174%); +} + +.btn-ability-protoss-warprelocatelvl2-png{ + clip-path: xywh(0 9.709962168978562% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 40.22698612862547%); +} + +.btn-ability-protoss-whirlwind-png{ + clip-path: xywh(0 9.836065573770492% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 40.10088272383354%); +} + +.btn-ability-spearofadun-chronomancy-png{ + clip-path: xywh(0 9.96216897856242% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 39.974779319041616%); +} + +.btn-ability-spearofadun-chronosurge-png{ + clip-path: xywh(0 10.08827238335435% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 39.84867591424968%); +} + +.btn-ability-spearofadun-deploypylon-png{ + clip-path: xywh(0 10.21437578814628% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 39.722572509457756%); +} + +.btn-ability-spearofadun-guardianshell-png{ + clip-path: xywh(0 10.34047919293821% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 39.59646910466583%); +} + +.btn-ability-spearofadun-massrecall-png{ + clip-path: xywh(0 10.466582597730138% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 39.470365699873895%); +} + +.btn-ability-spearofadun-matrixoverload-png{ + clip-path: xywh(0 10.592686002522068% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 39.34426229508197%); +} + +.btn-ability-spearofadun-nexusovercharge-png{ + clip-path: xywh(0 10.718789407313997% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 39.21815889029004%); +} + +.btn-ability-spearofadun-orbitalassimilator-png{ + clip-path: xywh(0 10.844892812105927% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 39.09205548549811%); +} + +.btn-ability-spearofadun-orbitalstrike-png{ + clip-path: xywh(0 10.970996216897856% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 38.96595208070618%); +} + +.btn-ability-spearofadun-purifierbeam-png{ + clip-path: xywh(0 11.097099621689786% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 38.83984867591425%); +} + +.btn-ability-spearofadun-reconstructionbeam-png{ + clip-path: xywh(0 11.223203026481714% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 38.71374527112232%); +} + +.btn-ability-spearofadun-shieldovercharge-png{ + clip-path: xywh(0 11.349306431273645% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 38.58764186633039%); +} + +.btn-ability-spearofadun-solarbombardment-png{ + clip-path: xywh(0 11.475409836065573% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 38.46153846153846%); +} + +.btn-ability-spearofadun-solarlance-png{ + clip-path: xywh(0 11.601513240857503% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 38.33543505674653%); +} + +.btn-ability-spearofadun-temporalfield-png{ + clip-path: xywh(0 11.727616645649432% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 38.2093316519546%); +} + +.btn-ability-spearofadun-timestop-png{ + clip-path: xywh(0 11.853720050441362% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 38.08322824716267%); +} + +.btn-ability-spearofadun-warpharmonization-png{ + clip-path: xywh(0 11.97982345523329% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 37.957124842370746%); +} + +.btn-ability-spearofadun-warpinreinforcements-png{ + clip-path: xywh(0 12.105926860025221% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 37.83102143757881%); +} + +.btn-ability-stetmann-banelingmanashield-png{ + clip-path: xywh(0 12.23203026481715% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 37.704918032786885%); +} + +.btn-ability-stetmann-corruptormissilebarrage-png{ + clip-path: xywh(0 12.35813366960908% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 37.57881462799496%); +} + +.btn-ability-stukov-plaugedmunitions-png{ + clip-path: xywh(0 12.484237074401008% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 37.452711223203025%); +} + +.btn-ability-swarm-kerrigan-chainreaction-png{ + clip-path: xywh(0 12.610340479192939% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 37.3266078184111%); +} + +.btn-ability-swarm-kerrigan-crushinggrip-png{ + clip-path: xywh(0 12.736443883984867% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 37.20050441361917%); +} + +.btn-ability-terran-calldownextrasupplies-color-png{ + clip-path: xywh(0 12.862547288776797% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 37.07440100882724%); +} + +.btn-ability-terran-cloak-color-png{ + clip-path: xywh(0 12.988650693568726% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 36.94829760403531%); +} + +.btn-ability-terran-detectionconedebuff-png{ + clip-path: xywh(0 13.114754098360656% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 36.82219419924338%); +} + +.btn-ability-terran-electricfield-png{ + clip-path: xywh(0 13.240857503152585% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 36.69609079445145%); +} + +.btn-ability-terran-emergencythrusters-png{ + clip-path: xywh(0 13.366960907944515% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 36.56998738965952%); +} + +.btn-ability-terran-emp-color-png{ + clip-path: xywh(0 13.493064312736443% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 36.44388398486759%); +} + +.btn-ability-terran-goliath-jetpack-png{ + clip-path: xywh(0 13.619167717528374% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 36.31778058007566%); +} + +.btn-ability-terran-hercules-tacticaljump-png{ + clip-path: xywh(0 13.745271122320302% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 36.19167717528373%); +} + +.btn-ability-terran-ignorearmor-png{ + clip-path: xywh(0 13.871374527112232% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 36.0655737704918%); +} + +.btn-ability-terran-liftoff-png{ + clip-path: xywh(0 13.997477931904161% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 35.939470365699876%); +} + +.btn-ability-terran-nuclearstrike-color-png{ + clip-path: xywh(0 14.123581336696091% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 35.81336696090794%); +} + +.btn-ability-terran-psidisruption-png{ + clip-path: xywh(0 14.24968474148802% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 35.687263556116015%); +} + +.btn-ability-terran-punishergrenade-color-png{ + clip-path: xywh(0 14.37578814627995% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 35.56116015132409%); +} + +.btn-ability-terran-restorationscbw-png{ + clip-path: xywh(0 14.501891551071878% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 35.435056746532155%); +} + +.btn-ability-terran-scannersweep-color-png{ + clip-path: xywh(0 14.627994955863809% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 35.30895334174023%); +} + +.btn-ability-terran-shreddermissile-color-png{ + clip-path: xywh(0 14.754098360655737% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 35.1828499369483%); +} + +.btn-ability-terran-spidermine-png{ + clip-path: xywh(0 14.880201765447667% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 35.05674653215637%); +} + +.btn-ability-terran-stimpack-color-png{ + clip-path: xywh(0 15.006305170239596% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 34.93064312736444%); +} + +.btn-ability-terran-unloadall-png{ + clip-path: xywh(0 15.132408575031526% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 34.80453972257251%); +} + +.btn-ability-terran-warpjump-png{ + clip-path: xywh(0 15.258511979823455% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 34.67843631778058%); +} + +.btn-ability-terran-widowminehidden-png{ + clip-path: xywh(0 15.384615384615385% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 34.552332912988646%); +} + +.btn-ability-thor-330mm-png{ + clip-path: xywh(0 15.510718789407314% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 34.42622950819672%); +} + +.btn-ability-tychus-herc-heavyimpact-png{ + clip-path: xywh(0 15.636822194199244% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 34.30012610340479%); +} + +.btn-ability-tychus-medivac-png{ + clip-path: xywh(0 15.762925598991172% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 34.17402269861286%); +} + +.btn-ability-zeratul-avatarofform-psionicblast-png{ + clip-path: xywh(0 15.889029003783103% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 34.04791929382093%); +} + +.btn-ability-zeratul-chargedcrystal-psionicwinds-png{ + clip-path: xywh(0 16.01513240857503% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 33.921815889029006%); +} + +.btn-ability-zeratul-darkarchon-maelstrom-png{ + clip-path: xywh(0 16.14123581336696% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 33.79571248423707%); +} + +.btn-ability-zeratul-immortal-forcecannon-png{ + clip-path: xywh(0 16.26733921815889% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 33.669609079445145%); +} + +.btn-ability-zeratul-observer-sensorarray-png{ + clip-path: xywh(0 16.39344262295082% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 33.54350567465322%); +} + +.btn-ability-zeratul-topbar-serdathlegion-png{ + clip-path: xywh(0 16.51954602774275% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 33.417402269861284%); +} + +.btn-ability-zerg-abathur-corrosivebilelarge-png{ + clip-path: xywh(0 16.64564943253468% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 33.29129886506936%); +} + +.btn-ability-zerg-acidspores-png{ + clip-path: xywh(0 16.77175283732661% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 33.16519546027743%); +} + +.btn-ability-zerg-burrow-color-png{ + clip-path: xywh(0 16.897856242118536% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 33.0390920554855%); +} + +.btn-ability-zerg-causticspray-png{ + clip-path: xywh(0 17.023959646910466% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 32.91298865069357%); +} + +.btn-ability-zerg-corruption-color-png{ + clip-path: xywh(0 17.150063051702396% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 32.786885245901644%); +} + +.btn-ability-zerg-creepspread-png{ + clip-path: xywh(0 17.276166456494327% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 32.66078184110971%); +} + +.btn-ability-zerg-creepteleport-png{ + clip-path: xywh(0 17.402269861286253% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 32.534678436317776%); +} + +.btn-ability-zerg-darkswarm-png{ + clip-path: xywh(0 17.528373266078184% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 32.40857503152586%); +} + +.btn-ability-zerg-deeptunnel-png{ + clip-path: xywh(0 17.654476670870114% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 32.28247162673392%); +} + +.btn-ability-zerg-dehaka-essencecollector-png{ + clip-path: xywh(0 17.780580075662044% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 32.15636822194199%); +} + +.btn-ability-zerg-dehaka-guardian-explosivespores-png{ + clip-path: xywh(0 17.90668348045397% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 32.03026481715006%); +} + +.btn-ability-zerg-dehaka-guardian-primordialfury-png{ + clip-path: xywh(0 18.0327868852459% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 31.904161412358135%); +} + +.btn-ability-zerg-dehaka-impaler-tenderize-png{ + clip-path: xywh(0 18.15889029003783% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 31.778058007566205%); +} + +.btn-ability-zerg-dehaka-tyrannozor-barrageofspikes-png{ + clip-path: xywh(0 18.284993694829762% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 31.651954602774275%); +} + +.btn-ability-zerg-dehaka-tyrannozor-tyrantprotection-png{ + clip-path: xywh(0 18.41109709962169% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 31.525851197982345%); +} + +.btn-ability-zerg-dehaka-ultralisk-brutalcharge-png{ + clip-path: xywh(0 18.53720050441362% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 31.399747793190418%); +} + +.btn-ability-zerg-dehaka-ultralisk-healingadaptation-png{ + clip-path: xywh(0 18.66330390920555% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 31.273644388398488%); +} + +.btn-ability-zerg-dehaka-ultralisk-impalingstrike-png{ + clip-path: xywh(0 18.78940731399748% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 31.147540983606557%); +} + +.btn-ability-zerg-fireroach-increasefiredamage-png{ + clip-path: xywh(0 18.915510718789406% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 31.021437578814627%); +} + +.btn-ability-zerg-fungalgrowth-color-png{ + clip-path: xywh(0 19.041614123581336% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 30.8953341740227%); +} + +.btn-ability-zerg-genemutation-thornsaura-png{ + clip-path: xywh(0 19.167717528373267% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 30.76923076923077%); +} + +.btn-ability-zerg-generatecreep-color-png{ + clip-path: xywh(0 19.293820933165197% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 30.64312736443884%); +} + +.btn-ability-zerg-overlord-oversight-off-png{ + clip-path: xywh(0 19.419924337957124% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 30.51702395964691%); +} + +.btn-ability-zerg-parasiticbomb-png{ + clip-path: xywh(0 19.546027742749054% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 30.390920554854983%); +} + +.btn-ability-zerg-rapidregeneration-color-png{ + clip-path: xywh(0 19.672131147540984% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 30.264817150063053%); +} + +.btn-ability-zerg-stukov-ensnare-png{ + clip-path: xywh(0 19.798234552332914% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 30.138713745271122%); +} + +.btn-ability-zerg-stukov-ensnarecdr-png{ + clip-path: xywh(0 19.92433795712484% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 30.012610340479192%); +} + +.btn-ability-zerg-transfusion-color-png{ + clip-path: xywh(0 20.05044136191677% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 29.886506935687265%); +} + +.btn-abilty-terran-lockdownscbw-png{ + clip-path: xywh(0 20.1765447667087% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 29.760403530895335%); +} + +.btn-accelerated-warp-png{ + clip-path: xywh(0 20.302648171500632% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 29.634300126103405%); +} + +.btn-adaptive-medpacks-png{ + clip-path: xywh(0 20.42875157629256% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 29.508196721311474%); +} + +.btn-advanced-construction-png{ + clip-path: xywh(0 20.55485498108449% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 29.382093316519548%); +} + +.btn-advanced-defensive-matrix-png{ + clip-path: xywh(0 20.68095838587642% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 29.255989911727617%); +} + +.btn-advanced-photon-blasters-png{ + clip-path: xywh(0 20.80706179066835% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 29.129886506935687%); +} + +.btn-advanced-targeting-png{ + clip-path: xywh(0 20.933165195460276% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 29.003783102143757%); +} + +.btn-afterburners-valkyrie-png{ + clip-path: xywh(0 21.059268600252206% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 28.87767969735183%); +} + +.btn-all-terrain-treads-png{ + clip-path: xywh(0 21.185372005044137% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 28.7515762925599%); +} + +.btn-amonshardsarmor-png{ + clip-path: xywh(0 21.311475409836067% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 28.62547288776797%); +} + +.btn-anti-surface-countermeasures-png{ + clip-path: xywh(0 21.437578814627994% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 28.49936948297604%); +} + +.btn-apial-sensors-png{ + clip-path: xywh(0 21.563682219419924% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 28.373266078184113%); +} + +.btn-arc-inducers-png{ + clip-path: xywh(0 21.689785624211854% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 28.247162673392182%); +} + +.btn-argus-talisman-png{ + clip-path: xywh(0 21.815889029003785% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 28.121059268600252%); +} + +.btn-armor-metling-blasters-png{ + clip-path: xywh(0 21.94199243379571% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 27.994955863808322%); +} + +.btn-atx-batteries-png{ + clip-path: xywh(0 22.06809583858764% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 27.868852459016395%); +} + +.btn-automated-mitosis-lvl1-png{ + clip-path: xywh(0 22.194199243379572% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 27.742749054224465%); +} + +.btn-banshee-cross-spectrum-dampeners-png{ + clip-path: xywh(0 22.320302648171502% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 27.616645649432535%); +} + +.btn-behemoth-stellarskin-png{ + clip-path: xywh(0 22.44640605296343% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 27.490542244640604%); +} + +.btn-blood-amulet-png{ + clip-path: xywh(0 22.57250945775536% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 27.364438839848678%); +} + +.btn-building-protoss-photoncannon-png{ + clip-path: xywh(0 22.69861286254729% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 27.238335435056747%); +} + +.btn-building-protoss-shieldbattery-png{ + clip-path: xywh(0 22.82471626733922% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 27.112232030264817%); +} + +.btn-building-stukov-infestedbunker-png{ + clip-path: xywh(0 22.950819672131146% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 26.986128625472887%); +} + +.btn-building-stukov-infestedturret-png{ + clip-path: xywh(0 23.076923076923077% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 26.86002522068096%); +} + +.btn-building-terran-autoturret-png{ + clip-path: xywh(0 23.203026481715007% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 26.73392181588903%); +} + +.btn-building-terran-bunker-png{ + clip-path: xywh(0 23.329129886506937% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 26.6078184110971%); +} + +.btn-building-terran-bunkerneosteel-png{ + clip-path: xywh(0 23.455233291298864% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 26.48171500630517%); +} + +.btn-building-terran-hivemindemulator-png{ + clip-path: xywh(0 23.581336696090794% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 26.355611601513242%); +} + +.btn-building-terran-missileturret-png{ + clip-path: xywh(0 23.707440100882724% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 26.229508196721312%); +} + +.btn-building-terran-planetaryfortress-png{ + clip-path: xywh(0 23.833543505674655% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 26.103404791929382%); +} + +.btn-building-terran-refineryautomated-png{ + clip-path: xywh(0 23.95964691046658% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 25.97730138713745%); +} + +.btn-building-terran-sensordome-png{ + clip-path: xywh(0 24.08575031525851% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 25.851197982345525%); +} + +.btn-building-terran-sigmaprojector-png{ + clip-path: xywh(0 24.211853720050442% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 25.725094577553595%); +} + +.btn-building-terran-techreactor-png{ + clip-path: xywh(0 24.337957124842372% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 25.598991172761664%); +} + +.btn-building-zerg-hive-png{ + clip-path: xywh(0 24.4640605296343% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 25.472887767969734%); +} + +.btn-building-zerg-nydusworm-png{ + clip-path: xywh(0 24.59016393442623% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 25.346784363177807%); +} + +.btn-building-zerg-spinecrawler-png{ + clip-path: xywh(0 24.71626733921816% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 25.220680958385877%); +} + +.btn-building-zerg-sporecannon-png{ + clip-path: xywh(0 24.84237074401009% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 25.094577553593947%); +} + +.btn-building-zerg-sporecrawler-png{ + clip-path: xywh(0 24.968474148802017% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 24.968474148802017%); +} + +.btn-caladrius-structure-png{ + clip-path: xywh(0 25.094577553593947% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 24.84237074401009%); +} + +.btn-chronostatic-reinforcement-png{ + clip-path: xywh(0 25.220680958385877% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 24.71626733921816%); +} + +.btn-command-cancel-png{ + clip-path: xywh(0 25.346784363177807% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 24.59016393442623%); +} + +.btn-concentrated-antimatter-png{ + clip-path: xywh(0 25.472887767969734% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 24.4640605296343%); +} + +.btn-disintegrating-particles-png{ + clip-path: xywh(0 25.598991172761664% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 24.337957124842372%); +} + +.btn-disruptor-dispersion-png{ + clip-path: xywh(0 25.725094577553595% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 24.211853720050442%); +} + +.btn-endless-servitude-png{ + clip-path: xywh(0 25.851197982345525% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 24.08575031525851%); +} + +.btn-enhanced-servo-striders-png{ + clip-path: xywh(0 25.97730138713745% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 23.95964691046658%); +} + +.btn-enhanced-shield-generator-png{ + clip-path: xywh(0 26.103404791929382% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 23.833543505674655%); +} + +.btn-eye-of-wrath-png{ + clip-path: xywh(0 26.229508196721312% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 23.707440100882724%); +} + +.btn-fire-suppression-system-lvl2-png{ + clip-path: xywh(0 26.355611601513242% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 23.581336696090794%); +} + +.btn-fleshfused-targeting-optics-png{ + clip-path: xywh(0 26.48171500630517% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 23.455233291298864%); +} + +.btn-forged-chassis-png{ + clip-path: xywh(0 26.6078184110971% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 23.329129886506937%); +} + +.btn-gaping-maw-png{ + clip-path: xywh(0 26.73392181588903% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 23.203026481715007%); +} + +.btn-gravitic-thrusters-png{ + clip-path: xywh(0 26.86002522068096% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 23.076923076923077%); +} + +.btn-high-explosive-munition-png{ + clip-path: xywh(0 26.986128625472887% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 22.950819672131146%); +} + +.btn-high-voltage-capacitors-png{ + clip-path: xywh(0 27.112232030264817% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 22.82471626733922%); +} + +.btn-hostile-environment-adaptation-png{ + clip-path: xywh(0 27.238335435056747% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 22.69861286254729%); +} + +.btn-hull-of-past-glories-png{ + clip-path: xywh(0 27.364438839848678% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 22.57250945775536%); +} + +.btn-hunter-seeker-weapon-png{ + clip-path: xywh(0 27.490542244640604% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 22.44640605296343%); +} + +.btn-iconic-wavelength-flux-png{ + clip-path: xywh(0 27.616645649432535% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 22.320302648171502%); +} + +.btn-improved-osmosis-png{ + clip-path: xywh(0 27.742749054224465% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 22.194199243379572%); +} + +.btn-infested-liberator-ag-png{ + clip-path: xywh(0 27.868852459016395% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 22.06809583858764%); +} + +.btn-integrated-power-png{ + clip-path: xywh(0 27.994955863808322% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 21.94199243379571%); +} + +.btn-jerry-rigged-patchjob-png{ + clip-path: xywh(0 28.121059268600252% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 21.815889029003785%); +} + +.btn-juggernaut-plating-herc-png{ + clip-path: xywh(0 28.247162673392182% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 21.689785624211854%); +} + +.btn-juggernaut-plating-marauder-png{ + clip-path: xywh(0 28.373266078184113% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 21.563682219419924%); +} + +.btn-jump-png{ + clip-path: xywh(0 28.49936948297604% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 21.437578814627994%); +} + +.btn-kryhas-cloak-png{ + clip-path: xywh(0 28.62547288776797% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 21.311475409836067%); +} + +.btn-latticed-shielding-png{ + clip-path: xywh(0 28.7515762925599% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 21.185372005044137%); +} + +.btn-launch-vector-compensator-png{ + clip-path: xywh(0 28.87767969735183% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 21.059268600252206%); +} + +.btn-lesser-shadow-fury-png{ + clip-path: xywh(0 29.003783102143757% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 20.933165195460276%); +} + +.btn-magellan-computation-systems-png{ + clip-path: xywh(0 29.129886506935687% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 20.80706179066835%); +} + +.btn-mobility-protocols-png{ + clip-path: xywh(0 29.255989911727617% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 20.68095838587642%); +} + +.btn-modernized-servos-png{ + clip-path: xywh(0 29.382093316519548% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 20.55485498108449%); +} + +.btn-moirai-impulse-drive-png{ + clip-path: xywh(0 29.508196721311474% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 20.42875157629256%); +} + +.btn-monstrous-resilience-aberration-png{ + clip-path: xywh(0 29.634300126103405% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 20.302648171500632%); +} + +.btn-monstrous-resilience-corruptor-png{ + clip-path: xywh(0 29.760403530895335% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 20.1765447667087%); +} + +.btn-neutron-shields-png{ + clip-path: xywh(0 29.886506935687265% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 20.05044136191677%); +} + +.btn-null-shroud-png{ + clip-path: xywh(0 30.012610340479192% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 19.92433795712484%); +} + +.btn-obliterate-png{ + clip-path: xywh(0 30.138713745271122% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 19.798234552332914%); +} + +.btn-orbital-fortress-png{ + clip-path: xywh(0 30.264817150063053% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 19.672131147540984%); +} + +.btn-pacification-protocols-png{ + clip-path: xywh(0 30.390920554854983% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 19.546027742749054%); +} + +.btn-peer-contempt-png{ + clip-path: xywh(0 30.51702395964691% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 19.419924337957124%); +} + +.btn-permacloak-banshee-png{ + clip-path: xywh(0 30.64312736443884% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 19.293820933165197%); +} + +.btn-permacloak-ghost-png{ + clip-path: xywh(0 30.76923076923077% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 19.167717528373267%); +} + +.btn-permacloak-medivac-png{ + clip-path: xywh(0 30.8953341740227% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 19.041614123581336%); +} + +.btn-permacloak-reaper-png{ + clip-path: xywh(0 31.021437578814627% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 18.915510718789406%); +} + +.btn-permacloak-spectre-png{ + clip-path: xywh(0 31.147540983606557% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 18.78940731399748%); +} + +.btn-permacloak-wraith-png{ + clip-path: xywh(0 31.273644388398488% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 18.66330390920555%); +} + +.btn-phase-blaster-png{ + clip-path: xywh(0 31.399747793190418% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 18.53720050441362%); +} + +.btn-phase-cloak-png{ + clip-path: xywh(0 31.525851197982345% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 18.41109709962169%); +} + +.btn-prescient-spores-png{ + clip-path: xywh(0 31.651954602774275% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 18.284993694829762%); +} + +.btn-progression-hornerhan-6-mirabuildtime-png{ + clip-path: xywh(0 31.778058007566205% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 18.15889029003783%); +} + +.btn-progression-protoss-fenix-1-zealotsuit-png{ + clip-path: xywh(0 31.904161412358135% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 18.0327868852459%); +} + +.btn-progression-protoss-fenix-6-forgeresearch-png{ + clip-path: xywh(0 32.03026481715006% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 17.90668348045397%); +} + +.btn-progression-zerg-dehaka-15-genemutation-png{ + clip-path: xywh(0 32.156368221941996% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 17.780580075662044%); +} + +.btn-progression-zerg-dehaka-7-newdehakaabilities-png{ + clip-path: xywh(0 32.28247162673392% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 17.65447667087011%); +} + +.btn-propellant-sacs-png{ + clip-path: xywh(0 32.40857503152585% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 17.528373266078184%); +} + +.btn-rapid-metamorph-png{ + clip-path: xywh(0 32.53467843631778% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 17.402269861286257%); +} + +.btn-regenerativebiosteel-blue-png{ + clip-path: xywh(0 32.66078184110971% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 17.276166456494323%); +} + +.btn-regenerativebiosteel-green-png{ + clip-path: xywh(0 32.78688524590164% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 17.150063051702396%); +} + +.btn-reintigrated-framework-png{ + clip-path: xywh(0 32.91298865069357% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 17.02395964691047%); +} + +.btn-research-terran-commandcenterreactor-png{ + clip-path: xywh(0 33.0390920554855% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 16.897856242118536%); +} + +.btn-research-terran-microfiltering-png{ + clip-path: xywh(0 33.16519546027743% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 16.77175283732661%); +} + +.btn-research-terran-orbitaldepots-png{ + clip-path: xywh(0 33.29129886506936% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 16.645649432534675%); +} + +.btn-research-terran-orbitalstrikerally-png{ + clip-path: xywh(0 33.417402269861284% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 16.51954602774275%); +} + +.btn-research-terran-ultracapacitors-png{ + clip-path: xywh(0 33.54350567465322% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 16.393442622950822%); +} + +.btn-research-terran-vanadiumplating-png{ + clip-path: xywh(0 33.669609079445145% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 16.267339218158888%); +} + +.btn-research-zerg-cellularreactor-png{ + clip-path: xywh(0 33.79571248423707% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 16.14123581336696%); +} + +.btn-research-zerg-fortifiedbunker-png{ + clip-path: xywh(0 33.921815889029006% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 16.015132408575035%); +} + +.btn-research-zerg-regenerativebio-steel-png{ + clip-path: xywh(0 34.04791929382093% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 15.8890290037831%); +} + +.btn-rogue-forces-png{ + clip-path: xywh(0 34.17402269861286% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 15.762925598991174%); +} + +.btn-royalliberator-png{ + clip-path: xywh(0 34.30012610340479% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 15.63682219419924%); +} + +.btn-scatter-veil-png{ + clip-path: xywh(0 34.42622950819672% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 15.510718789407314%); +} + +.btn-scv-cliffjump-png{ + clip-path: xywh(0 34.55233291298865% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 15.384615384615387%); +} + +.btn-seismic-sonar-png{ + clip-path: xywh(0 34.67843631778058% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 15.258511979823453%); +} + +.btn-shadow-guard-training-png{ + clip-path: xywh(0 34.80453972257251% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 15.132408575031526%); +} + +.btn-shield-capacity-png{ + clip-path: xywh(0 34.93064312736444% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 15.0063051702396%); +} + +.btn-side-missiles-png{ + clip-path: xywh(0 35.05674653215637% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 14.880201765447666%); +} + +.btn-skyward-chronoanomaly-png{ + clip-path: xywh(0 35.182849936948294% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 14.754098360655739%); +} + +.btn-solarite-lens-png{ + clip-path: xywh(0 35.30895334174023% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 14.627994955863805%); +} + +.btn-solarite-payload-png{ + clip-path: xywh(0 35.435056746532155% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 14.501891551071878%); +} + +.btn-stabilized-electrodes-png{ + clip-path: xywh(0 35.56116015132409% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 14.375788146279952%); +} + +.btn-sustaining-disruption-png{ + clip-path: xywh(0 35.687263556116015% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 14.249684741488018%); +} + +.btn-techupgrade-kinetic-foam-png{ + clip-path: xywh(0 35.81336696090794% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 14.123581336696091%); +} + +.btn-techupgrade-terran-cloakdistortionfield-color-png{ + clip-path: xywh(0 35.939470365699876% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 13.997477931904164%); +} + +.btn-techupgrade-terran-combatshield-color-png{ + clip-path: xywh(0 36.0655737704918% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 13.87137452711223%); +} + +.btn-techupgrade-terran-hellstormbatteries-color-png{ + clip-path: xywh(0 36.19167717528373% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 13.745271122320304%); +} + +.btn-techupgrade-terran-immortalityprotocol-color-png{ + clip-path: xywh(0 36.31778058007566% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 13.61916771752837%); +} + +.btn-techupgrade-terran-impalerrounds-color-png{ + clip-path: xywh(0 36.44388398486759% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 13.493064312736443%); +} + +.btn-techupgrade-terran-missilepods-color-level1-png{ + clip-path: xywh(0 36.569987389659524% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 13.366960907944517%); +} + +.btn-techupgrade-terran-ocularimplants-png{ + clip-path: xywh(0 36.69609079445145% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 13.240857503152583%); +} + +.btn-techupgrade-terran-psioniclash-color-png{ + clip-path: xywh(0 36.82219419924338% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 13.114754098360656%); +} + +.btn-techupgrade-terran-rapiddeployment-color-png{ + clip-path: xywh(0 36.94829760403531% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 12.98865069356873%); +} + +.btn-techupgrade-terran-shapedblast-color-png{ + clip-path: xywh(0 37.07440100882724% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 12.862547288776796%); +} + +.btn-techupgrade-terran-shapedhull-colored-png{ + clip-path: xywh(0 37.200504413619164% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 12.736443883984869%); +} + +.btn-techupgrade-terran-titaniumhousing-color-png{ + clip-path: xywh(0 37.3266078184111% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 12.610340479192935%); +} + +.btn-techupgrade-terran-tomahawkpowercell-color-png{ + clip-path: xywh(0 37.452711223203025% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 12.484237074401008%); +} + +.btn-techupgrade-terran-u238rounds-color-png{ + clip-path: xywh(0 37.57881462799496% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 12.358133669609082%); +} + +.btn-tips-armory-png{ + clip-path: xywh(0 37.704918032786885% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 12.232030264817148%); +} + +.btn-tips-flamingbetty-png{ + clip-path: xywh(0 37.83102143757881% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 12.105926860025221%); +} + +.btn-tips-laserdrillantiair-png{ + clip-path: xywh(0 37.957124842370746% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 11.979823455233294%); +} + +.btn-tips-terran-energynova-png{ + clip-path: xywh(0 38.08322824716267% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 11.85372005044136%); +} + +.btn-twilight-chassis-png{ + clip-path: xywh(0 38.2093316519546% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 11.727616645649434%); +} + +.btn-ued-rocketry-technology-png{ + clip-path: xywh(0 38.33543505674653% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 11.6015132408575%); +} + +.btn-ultrasonic-pulse-color-png{ + clip-path: xywh(0 38.46153846153846% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 11.475409836065573%); +} + +.btn-unit-biomechanicaldrone-png{ + clip-path: xywh(0 38.587641866330394% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 11.349306431273646%); +} + +.btn-unit-collection-primal-roachupgrade-png{ + clip-path: xywh(0 38.71374527112232% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 11.223203026481713%); +} + +.btn-unit-collection-primal-tyrannozor-png{ + clip-path: xywh(0 38.83984867591425% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 11.097099621689786%); +} + +.btn-unit-collection-probe-remastered-png{ + clip-path: xywh(0 38.96595208070618% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 10.97099621689786%); +} + +.btn-unit-collection-purifier-carrier-png{ + clip-path: xywh(0 39.09205548549811% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 10.844892812105925%); +} + +.btn-unit-collection-purifier-disruptor-png{ + clip-path: xywh(0 39.218158890290034% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 10.718789407313999%); +} + +.btn-unit-collection-purifier-immortal-png{ + clip-path: xywh(0 39.34426229508197% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 10.592686002522065%); +} + +.btn-unit-collection-taldarim-carrier-png{ + clip-path: xywh(0 39.470365699873895% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 10.466582597730138%); +} + +.btn-unit-collection-taldarim-phoenix-png{ + clip-path: xywh(0 39.59646910466583% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 10.340479192938211%); +} + +.btn-unit-collection-vikingfighter-covertops-png{ + clip-path: xywh(0 39.722572509457756% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 10.214375788146278%); +} + +.btn-unit-collection-wraith-junker-png{ + clip-path: xywh(0 39.84867591424968% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 10.08827238335435%); +} + +.btn-unit-hunterling-png{ + clip-path: xywh(0 39.974779319041616% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 9.962168978562424%); +} + +.btn-unit-infested-infestedmedic-png{ + clip-path: xywh(0 40.10088272383354% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 9.83606557377049%); +} + +.btn-unit-protoss-adept-purifier-png{ + clip-path: xywh(0 40.22698612862547% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 9.709962168978564%); +} + +.btn-unit-protoss-alarak-taldarim-supplicant-png{ + clip-path: xywh(0 40.3530895334174% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 9.58385876418663%); +} + +.btn-unit-protoss-arbiter-png{ + clip-path: xywh(0 40.47919293820933% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 9.457755359394703%); +} + +.btn-unit-protoss-archon-upgraded-png{ + clip-path: xywh(0 40.605296343001264% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 9.331651954602776%); +} + +.btn-unit-protoss-archon-png{ + clip-path: xywh(0 40.73139974779319% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 9.205548549810842%); +} + +.btn-unit-protoss-carrier-png{ + clip-path: xywh(0 40.85750315258512% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 9.079445145018916%); +} + +.btn-unit-protoss-colossus-taldarim-png{ + clip-path: xywh(0 40.98360655737705% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 8.953341740226989%); +} + +.btn-unit-protoss-colossus-png{ + clip-path: xywh(0 41.10970996216898% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 8.827238335435055%); +} + +.btn-unit-protoss-corsair-png{ + clip-path: xywh(0 41.235813366960905% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 8.701134930643128%); +} + +.btn-unit-protoss-darktemplar-aiur-png{ + clip-path: xywh(0 41.36191677175284% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 8.575031525851195%); +} + +.btn-unit-protoss-darktemplar-taldarim-png{ + clip-path: xywh(0 41.488020176544765% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 8.448928121059268%); +} + +.btn-unit-protoss-darktemplar-png{ + clip-path: xywh(0 41.6141235813367% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 8.322824716267341%); +} + +.btn-unit-protoss-dragoon-void-png{ + clip-path: xywh(0 41.740226986128626% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 8.196721311475407%); +} + +.btn-unit-protoss-fenix-png{ + clip-path: xywh(0 41.86633039092055% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 8.07061790668348%); +} + +.btn-unit-protoss-hightemplar-nerazim-png{ + clip-path: xywh(0 41.992433795712486% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 7.944514501891554%); +} + +.btn-unit-protoss-hightemplar-taldarim-png{ + clip-path: xywh(0 42.11853720050441% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 7.81841109709962%); +} + +.btn-unit-protoss-hightemplar-png{ + clip-path: xywh(0 42.24464060529634% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 7.692307692307693%); +} + +.btn-unit-protoss-immortal-nerazim-png{ + clip-path: xywh(0 42.37074401008827% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 7.56620428751576%); +} + +.btn-unit-protoss-immortal-taldarim-png{ + clip-path: xywh(0 42.4968474148802% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 7.440100882723833%); +} + +.btn-unit-protoss-immortal-png{ + clip-path: xywh(0 42.622950819672134% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 7.313997477931906%); +} + +.btn-unit-protoss-khaydarinmonolith-png{ + clip-path: xywh(0 42.74905422446406% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 7.187894073139972%); +} + +.btn-unit-protoss-mothership-taldarim-png{ + clip-path: xywh(0 42.87515762925599% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 7.061790668348046%); +} + +.btn-unit-protoss-observer-png{ + clip-path: xywh(0 43.00126103404792% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 6.935687263556119%); +} + +.btn-unit-protoss-oracle-png{ + clip-path: xywh(0 43.12736443883985% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 6.809583858764185%); +} + +.btn-unit-protoss-phoenix-purifier-png{ + clip-path: xywh(0 43.253467843631775% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 6.683480453972258%); +} + +.btn-unit-protoss-phoenix-png{ + clip-path: xywh(0 43.37957124842371% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 6.5573770491803245%); +} + +.btn-unit-protoss-probe-warpin-png{ + clip-path: xywh(0 43.505674653215635% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 6.431273644388398%); +} + +.btn-unit-protoss-probe-png{ + clip-path: xywh(0 43.63177805800757% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 6.305170239596471%); +} + +.btn-unit-protoss-reaver-png{ + clip-path: xywh(0 43.757881462799496% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 6.179066834804537%); +} + +.btn-unit-protoss-scout-png{ + clip-path: xywh(0 43.88398486759142% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 6.0529634300126105%); +} + +.btn-unit-protoss-scoutnerazim-png{ + clip-path: xywh(0 44.010088272383356% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 5.926860025220684%); +} + +.btn-unit-protoss-scoutpurifier-png{ + clip-path: xywh(0 44.13619167717528% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 5.80075662042875%); +} + +.btn-unit-protoss-scouttaldarim-png{ + clip-path: xywh(0 44.26229508196721% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 5.674653215636823%); +} + +.btn-unit-protoss-sentry-purifier-png{ + clip-path: xywh(0 44.388398486759144% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 5.548549810844889%); +} + +.btn-unit-protoss-sentry-taldarim-png{ + clip-path: xywh(0 44.51450189155107% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 5.422446406052963%); +} + +.btn-unit-protoss-sentry-png{ + clip-path: xywh(0 44.640605296343004% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 5.296343001261036%); +} + +.btn-unit-protoss-stalker-purifier-png{ + clip-path: xywh(0 44.76670870113493% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 5.170239596469102%); +} + +.btn-unit-protoss-stalker-taldarim-collection-ds-png{ + clip-path: xywh(0 44.89281210592686% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 5.044136191677175%); +} + +.btn-unit-protoss-stalker-png{ + clip-path: xywh(0 45.01891551071879% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 4.918032786885249%); +} + +.btn-unit-protoss-tempest-purifier-png{ + clip-path: xywh(0 45.14501891551072% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 4.791929382093315%); +} + +.btn-unit-protoss-voidray-purifier-png{ + clip-path: xywh(0 45.271122320302645% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 4.665825977301388%); +} + +.btn-unit-protoss-voidray-taldarim-png{ + clip-path: xywh(0 45.39722572509458% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 4.539722572509454%); +} + +.btn-unit-protoss-warpprism-png{ + clip-path: xywh(0 45.523329129886505% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 4.413619167717528%); +} + +.btn-unit-protoss-warpray-png{ + clip-path: xywh(0 45.64943253467844% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 4.287515762925601%); +} + +.btn-unit-protoss-zealot-nerazim-png{ + clip-path: xywh(0 45.775535939470366% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 4.161412358133667%); +} + +.btn-unit-protoss-zealot-purifier-png{ + clip-path: xywh(0 45.90163934426229% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 4.03530895334174%); +} + +.btn-unit-protoss-zealot-png{ + clip-path: xywh(0 46.02774274905423% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 3.9092055485498136%); +} + +.btn-unit-terran-autoturretblackops-png{ + clip-path: xywh(0 46.15384615384615% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 3.78310214375788%); +} + +.btn-unit-terran-banshee-mengsk-png{ + clip-path: xywh(0 46.27994955863808% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 3.656998738965953%); +} + +.btn-unit-terran-banshee-png{ + clip-path: xywh(0 46.406052963430014% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 3.5308953341740192%); +} + +.btn-unit-terran-bansheemercenary-png{ + clip-path: xywh(0 46.53215636822194% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 3.4047919293820925%); +} + +.btn-unit-terran-battlecruiser-png{ + clip-path: xywh(0 46.658259773013874% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 3.278688524590166%); +} + +.btn-unit-terran-battlecruiserloki-png{ + clip-path: xywh(0 46.7843631778058% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 3.152585119798232%); +} + +.btn-unit-terran-battlecruisermengsk-png{ + clip-path: xywh(0 46.91046658259773% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 3.0264817150063053%); +} + +.btn-unit-terran-cobra-png{ + clip-path: xywh(0 47.03656998738966% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 2.9003783102143785%); +} + +.btn-unit-terran-cyclone-png{ + clip-path: xywh(0 47.16267339218159% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 2.7742749054224447%); +} + +.btn-unit-terran-deathhead-png{ + clip-path: xywh(0 47.288776796973515% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 2.648171500630518%); +} + +.btn-unit-terran-firebat-png{ + clip-path: xywh(0 47.41488020176545% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 2.522068095838584%); +} + +.btn-unit-terran-firebatmercenary-png{ + clip-path: xywh(0 47.540983606557376% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 2.3959646910466574%); +} + +.btn-unit-terran-ghost-png{ + clip-path: xywh(0 47.66708701134931% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 2.2698612862547307%); +} + +.btn-unit-terran-ghostmengsk-png{ + clip-path: xywh(0 47.793190416141236% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 2.143757881462797%); +} + +.btn-unit-terran-goliath-mengsk-png{ + clip-path: xywh(0 47.91929382093316% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 2.01765447667087%); +} + +.btn-unit-terran-goliath-png{ + clip-path: xywh(0 48.0453972257251% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 1.8915510718789434%); +} + +.btn-unit-terran-goliathmercenary-png{ + clip-path: xywh(0 48.17150063051702% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 1.7654476670870096%); +} + +.btn-unit-terran-hellion-png{ + clip-path: xywh(0 48.29760403530895% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 1.639344262295083%); +} + +.btn-unit-terran-hellionbattlemode-png{ + clip-path: xywh(0 48.423707440100884% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 1.513240857503149%); +} + +.btn-unit-terran-herc-png{ + clip-path: xywh(0 48.54981084489281% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 1.3871374527112224%); +} + +.btn-unit-terran-hercules-png{ + clip-path: xywh(0 48.675914249684745% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 1.2610340479192956%); +} + +.btn-unit-terran-liberator-png{ + clip-path: xywh(0 48.80201765447667% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 1.1349306431273618%); +} + +.btn-unit-terran-liberatorblackops-png{ + clip-path: xywh(0 48.9281210592686% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 1.008827238335435%); +} + +.btn-unit-terran-marauder-png{ + clip-path: xywh(0 49.05422446406053% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 0.8827238335435084%); +} + +.btn-unit-terran-maraudermengsk-png{ + clip-path: xywh(0 49.18032786885246% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 0.7566204287515745%); +} + +.btn-unit-terran-maraudermercenary-png{ + clip-path: xywh(0 49.306431273644385% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 0.6305170239596478%); +} + +.btn-unit-terran-marine-mengsk-png{ + clip-path: xywh(0 49.43253467843632% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 0.504413619167714%); +} + +.btn-unit-terran-marine-png{ + clip-path: xywh(0 49.558638083228246% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 0.37831021437578727%); +} + +.btn-unit-terran-marinemercenary-png{ + clip-path: xywh(0 49.68474148802018% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 0.25220680958386055%); +} + +.btn-unit-terran-medic-mengsk-png{ + clip-path: xywh(0 49.810844892812106% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 0.12610340479192672%); +} + +.btn-unit-terran-medic-png{ + clip-path: xywh(0 49.93694829760403% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, 0.0%); +} + +.btn-unit-terran-medicelite-png{ + clip-path: xywh(0 50.06305170239597% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -0.12610340479192672%); +} + +.btn-unit-terran-medivac-png{ + clip-path: xywh(0 50.189155107187894% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -0.25220680958386055%); +} + +.btn-unit-terran-merc-thor-png{ + clip-path: xywh(0 50.31525851197982% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -0.37831021437578727%); +} + +.btn-unit-terran-mule-png{ + clip-path: xywh(0 50.441361916771754% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -0.504413619167714%); +} + +.btn-unit-terran-perditionturret-png{ + clip-path: xywh(0 50.56746532156368% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -0.6305170239596478%); +} + +.btn-unit-terran-predator-png{ + clip-path: xywh(0 50.693568726355615% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -0.7566204287515745%); +} + +.btn-unit-terran-raven-png{ + clip-path: xywh(0 50.81967213114754% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -0.8827238335435084%); +} + +.btn-unit-terran-reaper-png{ + clip-path: xywh(0 50.94577553593947% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -1.008827238335435%); +} + +.btn-unit-terran-sciencevessel-png{ + clip-path: xywh(0 51.0718789407314% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -1.1349306431273618%); +} + +.btn-unit-terran-siegetank-png{ + clip-path: xywh(0 51.19798234552333% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -1.2610340479192956%); +} + +.btn-unit-terran-siegetankmengsk-png{ + clip-path: xywh(0 51.324085750315255% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -1.3871374527112224%); +} + +.btn-unit-terran-siegetankmercenary-tank-png{ + clip-path: xywh(0 51.45018915510719% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -1.513240857503149%); +} + +.btn-unit-terran-spectre-png{ + clip-path: xywh(0 51.576292559899116% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -1.639344262295083%); +} + +.btn-unit-terran-thor-png{ + clip-path: xywh(0 51.70239596469105% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -1.7654476670870096%); +} + +.btn-unit-terran-thormengsk-png{ + clip-path: xywh(0 51.82849936948298% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -1.8915510718789434%); +} + +.btn-unit-terran-thorsiegemode-png{ + clip-path: xywh(0 51.9546027742749% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -2.01765447667087%); +} + +.btn-unit-terran-troopermengsk-png{ + clip-path: xywh(0 52.08070617906684% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -2.143757881462797%); +} + +.btn-unit-terran-valkyriescbw-png{ + clip-path: xywh(0 52.206809583858764% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -2.2698612862547307%); +} + +.btn-unit-terran-vikingfighter-png{ + clip-path: xywh(0 52.33291298865069% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -2.3959646910466574%); +} + +.btn-unit-terran-vikingmengskfighter-png{ + clip-path: xywh(0 52.459016393442624% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -2.522068095838584%); +} + +.btn-unit-terran-vikingmercenary-fighter-png{ + clip-path: xywh(0 52.58511979823455% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -2.648171500630518%); +} + +.btn-unit-terran-vulture-png{ + clip-path: xywh(0 52.711223203026485% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -2.7742749054224447%); +} + +.btn-unit-terran-warhound-png{ + clip-path: xywh(0 52.83732660781841% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -2.9003783102143785%); +} + +.btn-unit-terran-widowmine-png{ + clip-path: xywh(0 52.96343001261034% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -3.0264817150063053%); +} + +.btn-unit-terran-wraith-mengsk-png{ + clip-path: xywh(0 53.08953341740227% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -3.152585119798232%); +} + +.btn-unit-terran-wraith-png{ + clip-path: xywh(0 53.2156368221942% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -3.278688524590166%); +} + +.btn-unit-voidray-aiur-png{ + clip-path: xywh(0 53.341740226986126% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -3.4047919293820925%); +} + +.btn-unit-zerg-aberration-png{ + clip-path: xywh(0 53.46784363177806% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -3.5308953341740192%); +} + +.btn-unit-zerg-baneling-hunter-png{ + clip-path: xywh(0 53.593947036569986% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -3.656998738965953%); +} + +.btn-unit-zerg-baneling-png{ + clip-path: xywh(0 53.72005044136192% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -3.78310214375788%); +} + +.btn-unit-zerg-broodlord-png{ + clip-path: xywh(0 53.84615384615385% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -3.9092055485498136%); +} + +.btn-unit-zerg-broodqueen-png{ + clip-path: xywh(0 53.97225725094577% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -4.03530895334174%); +} + +.btn-unit-zerg-bullfrog-png{ + clip-path: xywh(0 54.09836065573771% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -4.161412358133667%); +} + +.btn-unit-zerg-classicqueen-png{ + clip-path: xywh(0 54.224464060529634% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -4.287515762925601%); +} + +.btn-unit-zerg-corruptor-png{ + clip-path: xywh(0 54.35056746532156% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -4.413619167717528%); +} + +.btn-unit-zerg-defilerscbw-png{ + clip-path: xywh(0 54.476670870113495% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -4.539722572509454%); +} + +.btn-unit-zerg-devourerex3-png{ + clip-path: xywh(0 54.60277427490542% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -4.665825977301388%); +} + +.btn-unit-zerg-hydralisk-remastered-png{ + clip-path: xywh(0 54.728877679697355% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -4.791929382093315%); +} + +.btn-unit-zerg-hydralisk-png{ + clip-path: xywh(0 54.85498108448928% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -4.918032786885249%); +} + +.btn-unit-zerg-impaler-png{ + clip-path: xywh(0 54.98108448928121% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -5.044136191677175%); +} + +.btn-unit-zerg-infestedbanshee-png{ + clip-path: xywh(0 55.10718789407314% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -5.170239596469102%); +} + +.btn-unit-zerg-infesteddiamondback-png{ + clip-path: xywh(0 55.23329129886507% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -5.296343001261036%); +} + +.btn-unit-zerg-infestedliberator-png{ + clip-path: xywh(0 55.359394703656996% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -5.422446406052963%); +} + +.btn-unit-zerg-infestedmarine-png{ + clip-path: xywh(0 55.48549810844893% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -5.548549810844889%); +} + +.btn-unit-zerg-infestedsiegetank-png{ + clip-path: xywh(0 55.611601513240856% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -5.674653215636823%); +} + +.btn-unit-zerg-infestor-png{ + clip-path: xywh(0 55.73770491803279% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -5.80075662042875%); +} + +.btn-unit-zerg-kerriganascended-png{ + clip-path: xywh(0 55.86380832282472% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -5.926860025220684%); +} + +.btn-unit-zerg-kerriganghost-png{ + clip-path: xywh(0 55.989911727616644% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -6.0529634300126105%); +} + +.btn-unit-zerg-kerriganinfested-png{ + clip-path: xywh(0 56.11601513240858% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -6.179066834804537%); +} + +.btn-unit-zerg-larva-png{ + clip-path: xywh(0 56.242118537200504% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -6.305170239596471%); +} + +.btn-unit-zerg-leviathan-png{ + clip-path: xywh(0 56.36822194199243% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -6.431273644388398%); +} + +.btn-unit-zerg-lurker-png{ + clip-path: xywh(0 56.494325346784365% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -6.5573770491803245%); +} + +.btn-unit-zerg-mutalisk-png{ + clip-path: xywh(0 56.62042875157629% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -6.683480453972258%); +} + +.btn-unit-zerg-nydusdragon-png{ + clip-path: xywh(0 56.746532156368225% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -6.809583858764185%); +} + +.btn-unit-zerg-overlordscbw-png{ + clip-path: xywh(0 56.87263556116015% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -6.935687263556119%); +} + +.btn-unit-zerg-overseer-png{ + clip-path: xywh(0 56.99873896595208% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -7.061790668348046%); +} + +.btn-unit-zerg-primalguardian-png{ + clip-path: xywh(0 57.12484237074401% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -7.187894073139972%); +} + +.btn-unit-zerg-ravager-png{ + clip-path: xywh(0 57.25094577553594% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -7.313997477931906%); +} + +.btn-unit-zerg-roach-corpser-png{ + clip-path: xywh(0 57.377049180327866% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -7.440100882723833%); +} + +.btn-unit-zerg-roach-vile-png{ + clip-path: xywh(0 57.5031525851198% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -7.56620428751576%); +} + +.btn-unit-zerg-roach-png{ + clip-path: xywh(0 57.62925598991173% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -7.692307692307693%); +} + +.btn-unit-zerg-roach_collection-png{ + clip-path: xywh(0 57.75535939470366% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -7.81841109709962%); +} + +.btn-unit-zerg-scourge-png{ + clip-path: xywh(0 57.88146279949559% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -7.944514501891554%); +} + +.btn-unit-zerg-swarmhost-carrion-png{ + clip-path: xywh(0 58.007566204287514% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -8.07061790668348%); +} + +.btn-unit-zerg-swarmhost-creeper-png{ + clip-path: xywh(0 58.13366960907945% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -8.196721311475407%); +} + +.btn-unit-zerg-swarmhost-png{ + clip-path: xywh(0 58.259773013871374% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -8.322824716267341%); +} + +.btn-unit-zerg-ultralisk-noxious-png{ + clip-path: xywh(0 58.3858764186633% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -8.448928121059268%); +} + +.btn-unit-zerg-ultralisk-rcz-png{ + clip-path: xywh(0 58.511979823455235% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -8.575031525851195%); +} + +.btn-unit-zerg-ultralisk-remastered-png{ + clip-path: xywh(0 58.63808322824716% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -8.701134930643128%); +} + +.btn-unit-zerg-ultralisk-torrasque-png{ + clip-path: xywh(0 58.764186633039095% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -8.827238335435055%); +} + +.btn-unit-zerg-ultralisk-png{ + clip-path: xywh(0 58.89029003783102% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -8.953341740226989%); +} + +.btn-unit-zerg-viper-png{ + clip-path: xywh(0 59.01639344262295% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -9.079445145018916%); +} + +.btn-unit-zerg-zergling-raptor-png{ + clip-path: xywh(0 59.14249684741488% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -9.205548549810842%); +} + +.btn-unit-zerg-zergling-scr-png{ + clip-path: xywh(0 59.26860025220681% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -9.331651954602776%); +} + +.btn-unit-zerg-zergling-swarmling-png{ + clip-path: xywh(0 59.394703656998736% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -9.457755359394703%); +} + +.btn-unit-zerg-zergling-png{ + clip-path: xywh(0 59.52080706179067% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -9.58385876418663%); +} + +.btn-unshackled-psionic-storm-png{ + clip-path: xywh(0 59.6469104665826% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -9.709962168978564%); +} + +.btn-upgrade-afaidofthedark-png{ + clip-path: xywh(0 59.77301387137453% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -9.83606557377049%); +} + +.btn-upgrade-artanis-healingpsionicstorm-png{ + clip-path: xywh(0 59.89911727616646% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -9.962168978562424%); +} + +.btn-upgrade-artanis-scarabsplashradius-png{ + clip-path: xywh(0 60.025220680958384% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -10.08827238335435%); +} + +.btn-upgrade-artanis-singularitycharge-png{ + clip-path: xywh(0 60.15132408575032% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -10.214375788146278%); +} + +.btn-upgrade-custom-triple-scourge-png{ + clip-path: xywh(0 60.277427490542244% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -10.340479192938211%); +} + +.btn-upgrade-increasedupgraderesearchspeed-png{ + clip-path: xywh(0 60.40353089533417% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -10.466582597730138%); +} + +.btn-upgrade-karax-energyregen200-png{ + clip-path: xywh(0 60.529634300126105% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -10.592686002522065%); +} + +.btn-upgrade-karax-pylonwarpininstantly-png{ + clip-path: xywh(0 60.65573770491803% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -10.718789407313999%); +} + +.btn-upgrade-karax-turretattackspeed-png{ + clip-path: xywh(0 60.781841109709966% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -10.844892812105925%); +} + +.btn-upgrade-karax-turretrange-png{ + clip-path: xywh(0 60.90794451450189% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -10.97099621689786%); +} + +.btn-upgrade-kerrigan-assimilationaura-png{ + clip-path: xywh(0 61.03404791929382% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -11.097099621689786%); +} + +.btn-upgrade-kerrigan-broodlordspeed-png{ + clip-path: xywh(0 61.16015132408575% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -11.223203026481713%); +} + +.btn-upgrade-kerrigan-crushinggripwave-png{ + clip-path: xywh(0 61.28625472887768% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -11.349306431273646%); +} + +.btn-upgrade-kerrigan-seismicspines-png{ + clip-path: xywh(0 61.412358133669606% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -11.475409836065573%); +} + +.btn-upgrade-mengsk-engineeringbay-dominionarmorlevel2-png{ + clip-path: xywh(0 61.53846153846154% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -11.6015132408575%); +} + +.btn-upgrade-mengsk-engineeringbay-dominionweaponslevel0-png{ + clip-path: xywh(0 61.66456494325347% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -11.727616645649434%); +} + +.btn-upgrade-mengsk-engineeringbay-neosteelfortifiedarmor-png{ + clip-path: xywh(0 61.7906683480454% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -11.85372005044136%); +} + +.btn-upgrade-mengsk-engineeringbay-orbitaldrop-png{ + clip-path: xywh(0 61.91677175283733% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -11.979823455233294%); +} + +.btn-upgrade-mengsk-ghostacademy-guidedtacticalstrike-png{ + clip-path: xywh(0 62.042875157629254% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -12.105926860025221%); +} + +.btn-upgrade-mengsk-trooper-flamethrower-png{ + clip-path: xywh(0 62.16897856242119% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -12.232030264817148%); +} + +.btn-upgrade-mengsk-trooper-missilelauncher-png{ + clip-path: xywh(0 62.295081967213115% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -12.358133669609082%); +} + +.btn-upgrade-mengsk-trooper-plasmarifle-png{ + clip-path: xywh(0 62.42118537200504% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -12.484237074401008%); +} + +.btn-upgrade-nova-blink-png{ + clip-path: xywh(0 62.547288776796975% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -12.610340479192935%); +} + +.btn-upgrade-nova-btn-upgrade-nova-flashgrenade-png{ + clip-path: xywh(0 62.6733921815889% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -12.736443883984869%); +} + +.btn-upgrade-nova-btn-upgrade-nova-pulsegrenade-png{ + clip-path: xywh(0 62.799495586380836% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -12.862547288776796%); +} + +.btn-upgrade-nova-equipment-apolloinfantrysuit-png{ + clip-path: xywh(0 62.92559899117276% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -12.98865069356873%); +} + +.btn-upgrade-nova-equipment-blinksuit-png{ + clip-path: xywh(0 63.05170239596469% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -13.114754098360656%); +} + +.btn-upgrade-nova-equipment-canisterrifle-png{ + clip-path: xywh(0 63.17780580075662% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -13.240857503152583%); +} + +.btn-upgrade-nova-equipment-ghostvisor-png{ + clip-path: xywh(0 63.30390920554855% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -13.366960907944517%); +} + +.btn-upgrade-nova-equipment-gunblade_sword-png{ + clip-path: xywh(0 63.430012610340476% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -13.493064312736443%); +} + +.btn-upgrade-nova-equipment-monomolecularblade-png{ + clip-path: xywh(0 63.55611601513241% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -13.61916771752837%); +} + +.btn-upgrade-nova-equipment-plasmagun-png{ + clip-path: xywh(0 63.68221941992434% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -13.745271122320304%); +} + +.btn-upgrade-nova-equipment-rangefinderoculus-png{ + clip-path: xywh(0 63.80832282471627% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -13.87137452711223%); +} + +.btn-upgrade-nova-equipment-shotgun-png{ + clip-path: xywh(0 63.9344262295082% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -13.997477931904164%); +} + +.btn-upgrade-nova-equipment-stealthsuit-png{ + clip-path: xywh(0 64.06052963430012% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -14.123581336696091%); +} + +.btn-upgrade-nova-holographicdecoy-png{ + clip-path: xywh(0 64.18663303909206% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -14.249684741488025%); +} + +.btn-upgrade-nova-jetpack-png{ + clip-path: xywh(0 64.31273644388399% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -14.375788146279945%); +} + +.btn-upgrade-nova-tacticalstealthsuit-png{ + clip-path: xywh(0 64.43883984867591% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -14.501891551071878%); +} + +.btn-upgrade-protoss-adeptshieldupgrade-png{ + clip-path: xywh(0 64.56494325346785% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -14.627994955863812%); +} + +.btn-upgrade-protoss-airarmorlevel1-png{ + clip-path: xywh(0 64.69104665825978% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -14.754098360655732%); +} + +.btn-upgrade-protoss-airarmorlevel2-png{ + clip-path: xywh(0 64.8171500630517% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -14.880201765447666%); +} + +.btn-upgrade-protoss-airarmorlevel3-png{ + clip-path: xywh(0 64.94325346784363% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -15.0063051702396%); +} + +.btn-upgrade-protoss-airarmorlevel4-png{ + clip-path: xywh(0 65.06935687263557% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -15.13240857503152%); +} + +.btn-upgrade-protoss-airarmorlevel5-png{ + clip-path: xywh(0 65.19546027742749% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -15.258511979823453%); +} + +.btn-upgrade-protoss-airweaponslevel1-png{ + clip-path: xywh(0 65.32156368221942% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -15.384615384615387%); +} + +.btn-upgrade-protoss-airweaponslevel2-png{ + clip-path: xywh(0 65.44766708701135% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -15.51071878940732%); +} + +.btn-upgrade-protoss-airweaponslevel3-png{ + clip-path: xywh(0 65.57377049180327% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -15.63682219419924%); +} + +.btn-upgrade-protoss-airweaponslevel4-png{ + clip-path: xywh(0 65.69987389659521% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -15.762925598991174%); +} + +.btn-upgrade-protoss-airweaponslevel5-png{ + clip-path: xywh(0 65.82597730138714% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -15.889029003783108%); +} + +.btn-upgrade-protoss-alarak-ascendantspsiorbtravelsfurther-png{ + clip-path: xywh(0 65.95208070617906% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -16.015132408575028%); +} + +.btn-upgrade-protoss-alarak-ascendantspermanentlybetter-png{ + clip-path: xywh(0 66.078184110971% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -16.14123581336696%); +} + +.btn-upgrade-protoss-alarak-graviticdrive-png{ + clip-path: xywh(0 66.20428751576293% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -16.267339218158895%); +} + +.btn-upgrade-protoss-alarak-havoctargetlockbuffed-png{ + clip-path: xywh(0 66.33039092055486% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -16.393442622950815%); +} + +.btn-upgrade-protoss-alarak-melleeweapon-png{ + clip-path: xywh(0 66.45649432534678% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -16.51954602774275%); +} + +.btn-upgrade-protoss-alarak-permanentcloak-png{ + clip-path: xywh(0 66.58259773013872% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -16.645649432534682%); +} + +.btn-upgrade-protoss-alarak-rangeincrease-png{ + clip-path: xywh(0 66.70870113493065% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -16.771752837326602%); +} + +.btn-upgrade-protoss-alarak-rangeweapon-png{ + clip-path: xywh(0 66.83480453972257% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -16.897856242118536%); +} + +.btn-upgrade-protoss-alarak-supplicantarmor-png{ + clip-path: xywh(0 66.9609079445145% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -17.02395964691047%); +} + +.btn-upgrade-protoss-alarak-supplicantextrashields-png{ + clip-path: xywh(0 67.08701134930644% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -17.15006305170239%); +} + +.btn-upgrade-protoss-fenix-adept-recochetglaiveupgraded-png{ + clip-path: xywh(0 67.21311475409836% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -17.276166456494323%); +} + +.btn-upgrade-protoss-fenix-adeptchampionbounceattack-png{ + clip-path: xywh(0 67.33921815889029% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -17.402269861286257%); +} + +.btn-upgrade-protoss-fenix-carrier-solarbeam-png{ + clip-path: xywh(0 67.46532156368222% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -17.52837326607819%); +} + +.btn-upgrade-protoss-fenix-disruptorpermanentcloak-png{ + clip-path: xywh(0 67.59142496847414% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -17.65447667087011%); +} + +.btn-upgrade-protoss-fenix-dragoonsolariteflare-png{ + clip-path: xywh(0 67.71752837326608% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -17.780580075662044%); +} + +.btn-upgrade-protoss-fenix-scoutchampionrange-png{ + clip-path: xywh(0 67.84363177805801% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -17.906683480453978%); +} + +.btn-upgrade-protoss-fenix-stasisfield-png{ + clip-path: xywh(0 67.96973518284993% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -18.032786885245898%); +} + +.btn-upgrade-protoss-fenix-zealotsuit-armorplate-png{ + clip-path: xywh(0 68.09583858764186% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -18.15889029003783%); +} + +.btn-upgrade-protoss-fluxvanes-png{ + clip-path: xywh(0 68.2219419924338% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -18.284993694829765%); +} + +.btn-upgrade-protoss-graviticbooster-png{ + clip-path: xywh(0 68.34804539722572% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -18.411097099621685%); +} + +.btn-upgrade-protoss-graviticdrive-png{ + clip-path: xywh(0 68.47414880201765% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -18.53720050441362%); +} + +.btn-upgrade-protoss-gravitoncatapult-png{ + clip-path: xywh(0 68.60025220680959% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -18.663303909205553%); +} + +.btn-upgrade-protoss-groundarmorlevel1-png{ + clip-path: xywh(0 68.72635561160152% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -18.789407313997472%); +} + +.btn-upgrade-protoss-groundarmorlevel2-png{ + clip-path: xywh(0 68.85245901639344% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -18.915510718789406%); +} + +.btn-upgrade-protoss-groundarmorlevel3-png{ + clip-path: xywh(0 68.97856242118537% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -19.04161412358134%); +} + +.btn-upgrade-protoss-groundarmorlevel4-png{ + clip-path: xywh(0 69.1046658259773% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -19.16771752837326%); +} + +.btn-upgrade-protoss-groundarmorlevel5-png{ + clip-path: xywh(0 69.23076923076923% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -19.293820933165193%); +} + +.btn-upgrade-protoss-groundweaponslevel1-png{ + clip-path: xywh(0 69.35687263556116% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -19.419924337957127%); +} + +.btn-upgrade-protoss-groundweaponslevel2-png{ + clip-path: xywh(0 69.4829760403531% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -19.54602774274906%); +} + +.btn-upgrade-protoss-groundweaponslevel3-png{ + clip-path: xywh(0 69.60907944514501% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -19.67213114754098%); +} + +.btn-upgrade-protoss-groundweaponslevel4-png{ + clip-path: xywh(0 69.73518284993695% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -19.798234552332914%); +} + +.btn-upgrade-protoss-groundweaponslevel5-png{ + clip-path: xywh(0 69.86128625472888% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -19.92433795712485%); +} + +.btn-upgrade-protoss-increasedscarabcapacityscbw-png{ + clip-path: xywh(0 69.9873896595208% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -20.050441361916768%); +} + +.btn-upgrade-protoss-khaydarinamulet-png{ + clip-path: xywh(0 70.11349306431273% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -20.1765447667087%); +} + +.btn-upgrade-protoss-phoenixrange-png{ + clip-path: xywh(0 70.23959646910467% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -20.302648171500635%); +} + +.btn-upgrade-protoss-researchbosoniccore-png{ + clip-path: xywh(0 70.36569987389659% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -20.428751576292555%); +} + +.btn-upgrade-protoss-researchgravitysling-png{ + clip-path: xywh(0 70.49180327868852% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -20.55485498108449%); +} + +.btn-upgrade-protoss-resonatingglaives-png{ + clip-path: xywh(0 70.61790668348046% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -20.680958385876423%); +} + +.btn-upgrade-protoss-shieldslevel1-png{ + clip-path: xywh(0 70.74401008827239% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -20.807061790668342%); +} + +.btn-upgrade-protoss-shieldslevel2-png{ + clip-path: xywh(0 70.87011349306431% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -20.933165195460276%); +} + +.btn-upgrade-protoss-shieldslevel3-png{ + clip-path: xywh(0 70.99621689785624% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -21.05926860025221%); +} + +.btn-upgrade-protoss-shieldslevel4-png{ + clip-path: xywh(0 71.12232030264818% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -21.18537200504413%); +} + +.btn-upgrade-protoss-shieldslevel5-png{ + clip-path: xywh(0 71.2484237074401% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -21.311475409836063%); +} + +.btn-upgrade-protoss-stalkerpurifier-reconstruction-png{ + clip-path: xywh(0 71.37452711223203% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -21.437578814627997%); +} + +.btn-upgrade-protoss-tectonicdisruptors-png{ + clip-path: xywh(0 71.50063051702396% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -21.56368221941993%); +} + +.btn-upgrade-protoss-vanguard-aoeradiusincreased-png{ + clip-path: xywh(0 71.62673392181588% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -21.68978562421185%); +} + +.btn-upgrade-protoss-vanguard-increasedarmordamage-png{ + clip-path: xywh(0 71.75283732660782% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -21.815889029003785%); +} + +.btn-upgrade-protoss-wrathwalker-cantargetairunits-png{ + clip-path: xywh(0 71.87894073139975% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -21.94199243379572%); +} + +.btn-upgrade-protoss-wrathwalker-chargetimeimproved-png{ + clip-path: xywh(0 72.00504413619167% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -22.068095838587638%); +} + +.btn-upgrade-psi-indoctrinator-png{ + clip-path: xywh(0 72.1311475409836% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -22.194199243379572%); +} + +.btn-upgrade-raynor-cerberusmines-png{ + clip-path: xywh(0 72.25725094577554% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -22.320302648171506%); +} + +.btn-upgrade-raynor-improvedsiegemode-png{ + clip-path: xywh(0 72.38335435056746% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -22.446406052963425%); +} + +.btn-upgrade-raynor-incineratorgauntlets-png{ + clip-path: xywh(0 72.50945775535939% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -22.57250945775536%); +} + +.btn-upgrade-raynor-juggernautplating-png{ + clip-path: xywh(0 72.63556116015133% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -22.698612862547293%); +} + +.btn-upgrade-raynor-maelstromrounds-png{ + clip-path: xywh(0 72.76166456494326% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -22.824716267339213%); +} + +.btn-upgrade-raynor-phobosclassweaponssystem-png{ + clip-path: xywh(0 72.88776796973518% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -22.950819672131146%); +} + +.btn-upgrade-raynor-replenishablemagazine-png{ + clip-path: xywh(0 73.01387137452711% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -23.07692307692308%); +} + +.btn-upgrade-raynor-ripwavemissiles-png{ + clip-path: xywh(0 73.13997477931905% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -23.203026481715%); +} + +.btn-upgrade-raynor-shockwavemissilebattery-png{ + clip-path: xywh(0 73.26607818411097% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -23.329129886506934%); +} + +.btn-upgrade-raynor-stabilizermedpacks-png{ + clip-path: xywh(0 73.3921815889029% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -23.455233291298867%); +} + +.btn-upgrade-reducedupgraderesearchcost-png{ + clip-path: xywh(0 73.51828499369483% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -23.5813366960908%); +} + +.btn-upgrade-siegetank-spidermines-png{ + clip-path: xywh(0 73.64438839848675% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -23.70744010088272%); +} + +.btn-upgrade-stetmann-banelingmanashieldefficiency-png{ + clip-path: xywh(0 73.77049180327869% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -23.833543505674655%); +} + +.btn-upgrade-stetmann-mechachitinousplating-png{ + clip-path: xywh(0 73.89659520807062% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -23.95964691046659%); +} + +.btn-upgrade-stetmann-zerglinghardenedshield-png{ + clip-path: xywh(0 74.02269861286254% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -24.085750315258508%); +} + +.btn-upgrade-swann-aresclasstargetingsystem-png{ + clip-path: xywh(0 74.14880201765448% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -24.211853720050442%); +} + +.btn-upgrade-swann-defensivematrix-png{ + clip-path: xywh(0 74.27490542244641% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -24.337957124842376%); +} + +.btn-upgrade-swann-displacementfield-png{ + clip-path: xywh(0 74.40100882723833% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -24.464060529634295%); +} + +.btn-upgrade-swann-firesuppressionsystem-png{ + clip-path: xywh(0 74.52711223203026% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -24.59016393442623%); +} + +.btn-upgrade-swann-hellarmor-png{ + clip-path: xywh(0 74.6532156368222% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -24.716267339218163%); +} + +.btn-upgrade-swann-improvedburstlaser-png{ + clip-path: xywh(0 74.77931904161413% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -24.842370744010083%); +} + +.btn-upgrade-swann-improvednanorepair-png{ + clip-path: xywh(0 74.90542244640605% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -24.968474148802017%); +} + +.btn-upgrade-swann-improvedturretattackspeed-png{ + clip-path: xywh(0 75.03152585119798% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -25.09457755359395%); +} + +.btn-upgrade-swann-multilockweaponsystem-png{ + clip-path: xywh(0 75.15762925598992% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -25.22068095838587%); +} + +.btn-upgrade-swann-scvdoublerepair-png{ + clip-path: xywh(0 75.28373266078184% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -25.346784363177804%); +} + +.btn-upgrade-swann-targetingoptics-png{ + clip-path: xywh(0 75.40983606557377% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -25.472887767969738%); +} + +.btn-upgrade-swann-vehiclerangeincrease-png{ + clip-path: xywh(0 75.5359394703657% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -25.59899117276167%); +} + +.btn-upgrade-terran-advanceballistics-png{ + clip-path: xywh(0 75.66204287515762% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -25.72509457755359%); +} + +.btn-upgrade-terran-behemothreactor-png{ + clip-path: xywh(0 75.78814627994956% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -25.851197982345525%); +} + +.btn-upgrade-terran-buildingarmor-png{ + clip-path: xywh(0 75.91424968474149% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -25.97730138713746%); +} + +.btn-upgrade-terran-cyclonerangeupgrade-png{ + clip-path: xywh(0 76.04035308953341% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -26.10340479192938%); +} + +.btn-upgrade-terran-durablematerials-png{ + clip-path: xywh(0 76.16645649432535% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -26.229508196721312%); +} + +.btn-upgrade-terran-highcapacityfueltanks-png{ + clip-path: xywh(0 76.29255989911728% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -26.355611601513246%); +} + +.btn-upgrade-terran-hisecautotracking-png{ + clip-path: xywh(0 76.4186633039092% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -26.481715006305166%); +} + +.btn-upgrade-terran-hyperflightrotors-png{ + clip-path: xywh(0 76.54476670870113% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -26.6078184110971%); +} + +.btn-upgrade-terran-infantryarmorlevel1-png{ + clip-path: xywh(0 76.67087011349307% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -26.733921815889033%); +} + +.btn-upgrade-terran-infantryarmorlevel2-png{ + clip-path: xywh(0 76.796973518285% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -26.860025220680953%); +} + +.btn-upgrade-terran-infantryarmorlevel3-png{ + clip-path: xywh(0 76.92307692307692% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -26.986128625472887%); +} + +.btn-upgrade-terran-infantryarmorlevel4-png{ + clip-path: xywh(0 77.04918032786885% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -27.11223203026482%); +} + +.btn-upgrade-terran-infantryarmorlevel5-png{ + clip-path: xywh(0 77.17528373266079% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -27.23833543505674%); +} + +.btn-upgrade-terran-infantryweaponslevel1-png{ + clip-path: xywh(0 77.3013871374527% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -27.364438839848674%); +} + +.btn-upgrade-terran-infantryweaponslevel2-png{ + clip-path: xywh(0 77.42749054224464% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -27.490542244640608%); +} + +.btn-upgrade-terran-infantryweaponslevel3-png{ + clip-path: xywh(0 77.55359394703657% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -27.61664564943254%); +} + +.btn-upgrade-terran-infantryweaponslevel4-png{ + clip-path: xywh(0 77.6796973518285% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -27.74274905422446%); +} + +.btn-upgrade-terran-infantryweaponslevel5-png{ + clip-path: xywh(0 77.80580075662043% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -27.868852459016395%); +} + +.btn-upgrade-terran-infernalpreigniter-png{ + clip-path: xywh(0 77.93190416141236% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -27.99495586380833%); +} + +.btn-upgrade-terran-interferencematrix-png{ + clip-path: xywh(0 78.05800756620428% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -28.12105926860025%); +} + +.btn-upgrade-terran-internalizedtechmodule-png{ + clip-path: xywh(0 78.18411097099622% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -28.247162673392182%); +} + +.btn-upgrade-terran-jumpjets-png{ + clip-path: xywh(0 78.31021437578815% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -28.373266078184116%); +} + +.btn-upgrade-terran-kd8chargeex3-png{ + clip-path: xywh(0 78.43631778058007% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -28.499369482976036%); +} + +.btn-upgrade-terran-lazertargetingsystem-png{ + clip-path: xywh(0 78.562421185372% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -28.62547288776797%); +} + +.btn-upgrade-terran-magfieldaccelerator-png{ + clip-path: xywh(0 78.68852459016394% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -28.751576292559903%); +} + +.btn-upgrade-terran-magrailmunitions-png{ + clip-path: xywh(0 78.81462799495587% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -28.877679697351823%); +} + +.btn-upgrade-terran-medivacemergencythrusters-png{ + clip-path: xywh(0 78.94073139974779% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -29.003783102143757%); +} + +.btn-upgrade-terran-neosteelframe-png{ + clip-path: xywh(0 79.06683480453972% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -29.12988650693569%); +} + +.btn-upgrade-terran-nova-bansheemissilestrik-png{ + clip-path: xywh(0 79.19293820933166% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -29.25598991172761%); +} + +.btn-upgrade-terran-nova-hellfiremissiles-png{ + clip-path: xywh(0 79.31904161412358% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -29.382093316519544%); +} + +.btn-upgrade-terran-nova-personaldefensivematrix-png{ + clip-path: xywh(0 79.44514501891551% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -29.508196721311478%); +} + +.btn-upgrade-terran-nova-siegetankrange-png{ + clip-path: xywh(0 79.57124842370744% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -29.634300126103412%); +} + +.btn-upgrade-terran-nova-specialordance-png{ + clip-path: xywh(0 79.69735182849936% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -29.76040353089533%); +} + +.btn-upgrade-terran-nova-terrandefendermodestructureattack-png{ + clip-path: xywh(0 79.8234552332913% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -29.886506935687265%); +} + +.btn-upgrade-terran-optimizedlogistics-png{ + clip-path: xywh(0 79.94955863808323% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -30.0126103404792%); +} + +.btn-upgrade-terran-reapercombatdrugs-png{ + clip-path: xywh(0 80.07566204287515% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -30.13871374527112%); +} + +.btn-upgrade-terran-replenishablemagazinelvl2-png{ + clip-path: xywh(0 80.20176544766709% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -30.264817150063053%); +} + +.btn-upgrade-terran-researchdrillingclaws-png{ + clip-path: xywh(0 80.32786885245902% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -30.390920554854986%); +} + +.btn-upgrade-terran-shipplatinglevel1-png{ + clip-path: xywh(0 80.45397225725094% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -30.517023959646906%); +} + +.btn-upgrade-terran-shipplatinglevel2-png{ + clip-path: xywh(0 80.58007566204287% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -30.64312736443884%); +} + +.btn-upgrade-terran-shipplatinglevel3-png{ + clip-path: xywh(0 80.7061790668348% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -30.769230769230774%); +} + +.btn-upgrade-terran-shipplatinglevel4-png{ + clip-path: xywh(0 80.83228247162674% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -30.895334174022693%); +} + +.btn-upgrade-terran-shipplatinglevel5-png{ + clip-path: xywh(0 80.95838587641866% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -31.021437578814627%); +} + +.btn-upgrade-terran-shipweaponslevel1-png{ + clip-path: xywh(0 81.0844892812106% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -31.14754098360656%); +} + +.btn-upgrade-terran-shipweaponslevel2-png{ + clip-path: xywh(0 81.21059268600253% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -31.27364438839848%); +} + +.btn-upgrade-terran-shipweaponslevel3-png{ + clip-path: xywh(0 81.33669609079445% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -31.399747793190414%); +} + +.btn-upgrade-terran-shipweaponslevel4-png{ + clip-path: xywh(0 81.46279949558638% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -31.525851197982348%); +} + +.btn-upgrade-terran-shipweaponslevel5-png{ + clip-path: xywh(0 81.58890290037832% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -31.651954602774282%); +} + +.btn-upgrade-terran-superstimppack-png{ + clip-path: xywh(0 81.71500630517023% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -31.7780580075662%); +} + +.btn-upgrade-terran-transformationservos-png{ + clip-path: xywh(0 81.84110970996217% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -31.904161412358135%); +} + +.btn-upgrade-terran-trilithium-power-cell-png{ + clip-path: xywh(0 81.9672131147541% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -32.03026481715007%); +} + +.btn-upgrade-terran-tungsten-spikes-png{ + clip-path: xywh(0 82.09331651954602% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -32.15636822194199%); +} + +.btn-upgrade-terran-twin-linkedflamethrower-color-png{ + clip-path: xywh(0 82.21941992433796% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -32.28247162673392%); +} + +.btn-upgrade-terran-vehicleplatinglevel1-png{ + clip-path: xywh(0 82.34552332912989% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -32.40857503152586%); +} + +.btn-upgrade-terran-vehicleplatinglevel2-png{ + clip-path: xywh(0 82.47162673392181% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -32.534678436317776%); +} + +.btn-upgrade-terran-vehicleplatinglevel3-png{ + clip-path: xywh(0 82.59773013871374% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -32.66078184110971%); +} + +.btn-upgrade-terran-vehicleplatinglevel4-png{ + clip-path: xywh(0 82.72383354350568% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -32.786885245901644%); +} + +.btn-upgrade-terran-vehicleplatinglevel5-png{ + clip-path: xywh(0 82.84993694829761% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -32.91298865069356%); +} + +.btn-upgrade-terran-vehicleweaponslevel1-png{ + clip-path: xywh(0 82.97604035308953% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -33.0390920554855%); +} + +.btn-upgrade-terran-vehicleweaponslevel2-png{ + clip-path: xywh(0 83.10214375788146% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -33.16519546027743%); +} + +.btn-upgrade-terran-vehicleweaponslevel3-png{ + clip-path: xywh(0 83.2282471626734% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -33.29129886506935%); +} + +.btn-upgrade-terran-vehicleweaponslevel4-png{ + clip-path: xywh(0 83.35435056746532% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -33.417402269861284%); +} + +.btn-upgrade-terran-vehicleweaponslevel5-png{ + clip-path: xywh(0 83.48045397225725% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -33.54350567465322%); +} + +.btn-upgrade-vorazun-corsairpermanentlycloaked-png{ + clip-path: xywh(0 83.60655737704919% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -33.66960907944514%); +} + +.btn-upgrade-vorazun-oraclepermanentlycloaked-png{ + clip-path: xywh(0 83.7326607818411% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -33.79571248423707%); +} + +.btn-upgrade-zagara-aberrationarmorcover-png{ + clip-path: xywh(0 83.85876418663304% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -33.921815889029006%); +} + +.btn-upgrade-zagara-increasebilelauncherrange-png{ + clip-path: xywh(0 83.98486759142497% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -34.04791929382094%); +} + +.btn-upgrade-zagara-scourgesplashdamage-png{ + clip-path: xywh(0 84.11097099621689% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -34.17402269861286%); +} + +.btn-upgrade-zerg-abathur-abduct-png{ + clip-path: xywh(0 84.23707440100883% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -34.30012610340479%); +} + +.btn-upgrade-zerg-abathur-biomass-png{ + clip-path: xywh(0 84.36317780580076% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -34.42622950819673%); +} + +.btn-upgrade-zerg-abathur-biomechanicaltransfusion-png{ + clip-path: xywh(0 84.48928121059268% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -34.552332912988646%); +} + +.btn-upgrade-zerg-abathur-castrange-png{ + clip-path: xywh(0 84.61538461538461% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -34.67843631778058%); +} + +.btn-upgrade-zerg-abathur-devourer-corrosivespray-png{ + clip-path: xywh(0 84.74148802017655% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -34.804539722572514%); +} + +.btn-upgrade-zerg-abathur-improvedmend-png{ + clip-path: xywh(0 84.86759142496848% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -34.930643127364434%); +} + +.btn-upgrade-zerg-abathur-incubationchamber-png{ + clip-path: xywh(0 84.9936948297604% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -35.05674653215637%); +} + +.btn-upgrade-zerg-abathur-prolongeddispersion-png{ + clip-path: xywh(0 85.11979823455233% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -35.1828499369483%); +} + +.btn-upgrade-zerg-adaptivecarapace-png{ + clip-path: xywh(0 85.24590163934427% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -35.30895334174022%); +} + +.btn-upgrade-zerg-adaptivetalons-png{ + clip-path: xywh(0 85.37200504413619% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -35.435056746532155%); +} + +.btn-upgrade-zerg-adrenaloverload-png{ + clip-path: xywh(0 85.49810844892812% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -35.56116015132409%); +} + +.btn-upgrade-zerg-airattacks-level1-png{ + clip-path: xywh(0 85.62421185372006% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -35.68726355611601%); +} + +.btn-upgrade-zerg-airattacks-level2-png{ + clip-path: xywh(0 85.75031525851198% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -35.81336696090794%); +} + +.btn-upgrade-zerg-airattacks-level3-png{ + clip-path: xywh(0 85.87641866330391% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -35.939470365699876%); +} + +.btn-upgrade-zerg-airattacks-level4-png{ + clip-path: xywh(0 86.00252206809584% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -36.06557377049181%); +} + +.btn-upgrade-zerg-airattacks-level5-png{ + clip-path: xywh(0 86.12862547288776% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -36.19167717528373%); +} + +.btn-upgrade-zerg-anabolicsynthesis-png{ + clip-path: xywh(0 86.2547288776797% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -36.31778058007566%); +} + +.btn-upgrade-zerg-ancillaryarmor-png{ + clip-path: xywh(0 86.38083228247163% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -36.4438839848676%); +} + +.btn-upgrade-zerg-buildingarmor-png{ + clip-path: xywh(0 86.50693568726355% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -36.56998738965952%); +} + +.btn-upgrade-zerg-burrowcharge-png{ + clip-path: xywh(0 86.63303909205548% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -36.69609079445145%); +} + +.btn-upgrade-zerg-burrowmove-png{ + clip-path: xywh(0 86.75914249684742% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -36.822194199243384%); +} + +.btn-upgrade-zerg-celldivisionon-png{ + clip-path: xywh(0 86.88524590163935% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -36.948297604035304%); +} + +.btn-upgrade-zerg-centrifugalhooks-png{ + clip-path: xywh(0 87.01134930643127% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -37.07440100882724%); +} + +.btn-upgrade-zerg-chitinousplating-png{ + clip-path: xywh(0 87.1374527112232% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -37.20050441361917%); +} + +.btn-upgrade-zerg-concentrated-spew-png{ + clip-path: xywh(0 87.26355611601514% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -37.32660781841109%); +} + +.btn-upgrade-zerg-corrosiveacid-png{ + clip-path: xywh(0 87.38965952080706% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -37.452711223203025%); +} + +.btn-upgrade-zerg-dehaka-tenderize-png{ + clip-path: xywh(0 87.51576292559899% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -37.57881462799496%); +} + +.btn-upgrade-zerg-demolition-png{ + clip-path: xywh(0 87.64186633039093% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -37.70491803278688%); +} + +.btn-upgrade-zerg-enduringcorruption-png{ + clip-path: xywh(0 87.76796973518285% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -37.83102143757881%); +} + +.btn-upgrade-zerg-evolveincreasedlocustlifetime-png{ + clip-path: xywh(0 87.89407313997478% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -37.957124842370746%); +} + +.btn-upgrade-zerg-evolvemuscularaugments-png{ + clip-path: xywh(0 88.02017654476671% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -38.08322824716268%); +} + +.btn-upgrade-zerg-explosiveglaive-png{ + clip-path: xywh(0 88.14627994955863% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -38.2093316519546%); +} + +.btn-upgrade-zerg-flyercarapace-level1-png{ + clip-path: xywh(0 88.27238335435057% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -38.33543505674653%); +} + +.btn-upgrade-zerg-flyercarapace-level2-png{ + clip-path: xywh(0 88.3984867591425% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -38.46153846153847%); +} + +.btn-upgrade-zerg-flyercarapace-level3-png{ + clip-path: xywh(0 88.52459016393442% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -38.58764186633039%); +} + +.btn-upgrade-zerg-flyercarapace-level4-png{ + clip-path: xywh(0 88.65069356872635% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -38.71374527112232%); +} + +.btn-upgrade-zerg-flyercarapace-level5-png{ + clip-path: xywh(0 88.77679697351829% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -38.839848675914254%); +} + +.btn-upgrade-zerg-frenzy-png{ + clip-path: xywh(0 88.90290037831022% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -38.965952080706174%); +} + +.btn-upgrade-zerg-glialreconstitution-png{ + clip-path: xywh(0 89.02900378310214% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -39.09205548549811%); +} + +.btn-upgrade-zerg-groovedspines-png{ + clip-path: xywh(0 89.15510718789407% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -39.21815889029004%); +} + +.btn-upgrade-zerg-groundcarapace-level1-png{ + clip-path: xywh(0 89.28121059268601% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -39.34426229508196%); +} + +.btn-upgrade-zerg-groundcarapace-level2-png{ + clip-path: xywh(0 89.40731399747793% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -39.470365699873895%); +} + +.btn-upgrade-zerg-groundcarapace-level3-png{ + clip-path: xywh(0 89.53341740226986% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -39.59646910466583%); +} + +.btn-upgrade-zerg-groundcarapace-level4-png{ + clip-path: xywh(0 89.6595208070618% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -39.72257250945775%); +} + +.btn-upgrade-zerg-groundcarapace-level5-png{ + clip-path: xywh(0 89.78562421185372% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -39.84867591424968%); +} + +.btn-upgrade-zerg-hardenedcarapace-png{ + clip-path: xywh(0 89.91172761664565% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -39.974779319041616%); +} + +.btn-upgrade-zerg-hotsgroovedspines-png{ + clip-path: xywh(0 90.03783102143758% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -40.10088272383355%); +} + +.btn-upgrade-zerg-hotsmetabolicboost-png{ + clip-path: xywh(0 90.1639344262295% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -40.22698612862547%); +} + +.btn-upgrade-zerg-hotstunnelingclaws-png{ + clip-path: xywh(0 90.29003783102144% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -40.3530895334174%); +} + +.btn-upgrade-zerg-hydriaticacid-png{ + clip-path: xywh(0 90.41614123581337% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -40.47919293820934%); +} + +.btn-upgrade-zerg-meleeattacks-level1-png{ + clip-path: xywh(0 90.54224464060529% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -40.60529634300126%); +} + +.btn-upgrade-zerg-meleeattacks-level2-png{ + clip-path: xywh(0 90.66834804539722% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -40.73139974779319%); +} + +.btn-upgrade-zerg-meleeattacks-level3-png{ + clip-path: xywh(0 90.79445145018916% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -40.857503152585124%); +} + +.btn-upgrade-zerg-meleeattacks-level4-png{ + clip-path: xywh(0 90.92055485498109% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -40.983606557377044%); +} + +.btn-upgrade-zerg-meleeattacks-level5-png{ + clip-path: xywh(0 91.04665825977301% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -41.10970996216898%); +} + +.btn-upgrade-zerg-missileattacks-level1-png{ + clip-path: xywh(0 91.17276166456494% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -41.23581336696091%); +} + +.btn-upgrade-zerg-missileattacks-level2-png{ + clip-path: xywh(0 91.29886506935688% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -41.36191677175283%); +} + +.btn-upgrade-zerg-missileattacks-level3-png{ + clip-path: xywh(0 91.4249684741488% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -41.488020176544765%); +} + +.btn-upgrade-zerg-missileattacks-level4-png{ + clip-path: xywh(0 91.55107187894073% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -41.6141235813367%); +} + +.btn-upgrade-zerg-missileattacks-level5-png{ + clip-path: xywh(0 91.67717528373267% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -41.74022698612862%); +} + +.btn-upgrade-zerg-monarchblades-png{ + clip-path: xywh(0 91.80327868852459% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -41.86633039092055%); +} + +.btn-upgrade-zerg-organiccarapace-png{ + clip-path: xywh(0 91.92938209331652% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -41.992433795712486%); +} + +.btn-upgrade-zerg-pneumatizedcarapace-png{ + clip-path: xywh(0 92.05548549810845% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -42.11853720050442%); +} + +.btn-upgrade-zerg-pressurizedglands-png{ + clip-path: xywh(0 92.18158890290037% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -42.24464060529634%); +} + +.btn-upgrade-zerg-rapidincubation-png{ + clip-path: xywh(0 92.3076923076923% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -42.37074401008827%); +} + +.btn-upgrade-zerg-rapidregeneration-png{ + clip-path: xywh(0 92.43379571248424% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -42.49684741488021%); +} + +.btn-upgrade-zerg-regenerativebile-png{ + clip-path: xywh(0 92.55989911727616% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -42.62295081967213%); +} + +.btn-upgrade-zerg-rupture-png{ + clip-path: xywh(0 92.6860025220681% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -42.74905422446406%); +} + +.btn-upgrade-zerg-stukov-bansheeburrowregeneration-png{ + clip-path: xywh(0 92.81210592686003% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -42.875157629255995%); +} + +.btn-upgrade-zerg-stukov-bansheemorelife-png{ + clip-path: xywh(0 92.93820933165196% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -43.001261034047914%); +} + +.btn-upgrade-zerg-stukov-bunkerformliferegenupgraded-png{ + clip-path: xywh(0 93.06431273644388% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -43.12736443883985%); +} + +.btn-upgrade-zerg-stukov-bunkerresearchbundle_05-png{ + clip-path: xywh(0 93.19041614123581% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -43.25346784363178%); +} + +.btn-upgrade-zerg-stukov-bunkerupgradeii_14-png{ + clip-path: xywh(0 93.31651954602775% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -43.3795712484237%); +} + +.btn-upgrade-zerg-stukov-diamondbacksnailtrail-png{ + clip-path: xywh(0 93.44262295081967% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -43.505674653215635%); +} + +.btn-upgrade-zerg-stukov-infestedbunkermorelife-png{ + clip-path: xywh(0 93.5687263556116% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -43.63177805800757%); +} + +.btn-upgrade-zerg-stukov-infestedliberatoraoe-png{ + clip-path: xywh(0 93.69482976040354% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -43.75788146279949%); +} + +.btn-upgrade-zerg-stukov-infestedliberatorswarmcloud-png{ + clip-path: xywh(0 93.82093316519546% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -43.88398486759142%); +} + +.btn-upgrade-zerg-stukov-infestedmarinerangeupgrade-png{ + clip-path: xywh(0 93.94703656998739% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -44.010088272383356%); +} + +.btn-upgrade-zerg-stukov-infestedspawnbroodling-png{ + clip-path: xywh(0 94.07313997477932% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -44.13619167717529%); +} + +.btn-upgrade-zerg-stukov-queenenergyregen-png{ + clip-path: xywh(0 94.19924337957124% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -44.26229508196721%); +} + +.btn-upgrade-zerg-stukov-researchqueenfungalgrowth-png{ + clip-path: xywh(0 94.32534678436318% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -44.388398486759144%); +} + +.btn-upgrade-zerg-stukov-siegetankammoregen-png{ + clip-path: xywh(0 94.45145018915511% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -44.51450189155108%); +} + +.btn-upgrade-zerg-stukov-siegetankbonusdamage-png{ + clip-path: xywh(0 94.57755359394703% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -44.640605296343%); +} + +.btn-upgrade-zerg-swarmfrenzy-png{ + clip-path: xywh(0 94.70365699873896% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -44.76670870113493%); +} + +.btn-upgrade-zerg-tissueassimilation-png{ + clip-path: xywh(0 94.8297604035309% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -44.892812105926865%); +} + +.btn-upgrade-zerg-tunnelingjaws-png{ + clip-path: xywh(0 94.95586380832283% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -45.018915510718784%); +} + +.btn-upgrade-zerg-ventralsacs-png{ + clip-path: xywh(0 95.08196721311475% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -45.14501891551072%); +} + +.btn-upgrade-zerg-viciousglaive-png{ + clip-path: xywh(0 95.20807061790669% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -45.27112232030265%); +} + +.btn-upgrade-zergling-armorshredding-png{ + clip-path: xywh(0 95.33417402269862% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -45.39722572509457%); +} + +.btn-veil-of-the-judicator-png{ + clip-path: xywh(0 95.46027742749054% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -45.523329129886505%); +} + +.btn-warp-refraction-png{ + clip-path: xywh(0 95.58638083228247% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -45.64943253467844%); +} + +.evolution_coop-png{ + clip-path: xywh(0 95.7124842370744% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -45.77553593947036%); +} + +.icon-bargain-bin-prices-png{ + clip-path: xywh(0 95.83858764186633% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -45.90163934426229%); +} + +.icon-gas-terran-nobg-png{ + clip-path: xywh(0 95.96469104665826% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -46.02774274905423%); +} + +.icon-health-nobg-png{ + clip-path: xywh(0 96.0907944514502% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -46.15384615384616%); +} + +.icon-mineral-nobg-png{ + clip-path: xywh(0 96.21689785624211% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -46.27994955863808%); +} + +.icon-shields-png{ + clip-path: xywh(0 96.34300126103405% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -46.406052963430014%); +} + +.icon-supply-protoss_nobg-png{ + clip-path: xywh(0 96.46910466582598% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -46.53215636822195%); +} + +.icon-supply-terran_nobg-png{ + clip-path: xywh(0 96.5952080706179% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -46.65825977301387%); +} + +.icon-supply-zerg_nobg-png{ + clip-path: xywh(0 96.72131147540983% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -46.7843631778058%); +} + +.icon-time-protoss-png{ + clip-path: xywh(0 96.84741488020177% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -46.910466582597735%); +} + +.potentbile_coop-png{ + clip-path: xywh(0 96.9735182849937% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -47.036569987389655%); +} + +.predatorcharge-png{ + clip-path: xywh(0 97.09962168978562% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -47.16267339218159%); +} + +.predatorvespene-png{ + clip-path: xywh(0 97.22572509457756% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -47.28877679697352%); +} + +.talent-artanis-level03-warpgatecharges-png{ + clip-path: xywh(0 97.35182849936949% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -47.41488020176544%); +} + +.talent-artanis-level14-startingmaxsupply-png{ + clip-path: xywh(0 97.47793190416141% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -47.540983606557376%); +} + +.talent-raynor-level03-firebatmedicrange-png{ + clip-path: xywh(0 97.60403530895334% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -47.66708701134931%); +} + +.talent-raynor-level08-orbitaldroppods-png{ + clip-path: xywh(0 97.73013871374528% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -47.79319041614123%); +} + +.talent-raynor-level14-infantryattackspeed-png{ + clip-path: xywh(0 97.8562421185372% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -47.91929382093316%); +} + +.talent-swann-level12-immortalityprotocol-png{ + clip-path: xywh(0 97.98234552332913% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -48.0453972257251%); +} + +.talent-swann-level14-vehiclehealthincrease-png{ + clip-path: xywh(0 98.10844892812106% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -48.17150063051703%); +} + +.talent-tychus-level02-additionaloutlaw-png{ + clip-path: xywh(0 98.23455233291298% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -48.29760403530895%); +} + +.talent-tychus-level07-firstdiscount-png{ + clip-path: xywh(0 98.36065573770492% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -48.423707440100884%); +} + +.talent-vorazun-level01-shadowstalk-png{ + clip-path: xywh(0 98.48675914249685% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -48.54981084489282%); +} + +.talent-vorazun-level05-unlockdarkarchon-png{ + clip-path: xywh(0 98.61286254728877% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -48.67591424968474%); +} + +.talent-zagara-level12-unlockswarmling-png{ + clip-path: xywh(0 98.7389659520807% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -48.80201765447667%); +} + +.talent-zagara-level14-unlocksplitterling-png{ + clip-path: xywh(0 98.86506935687264% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -48.928121059268605%); +} + +.tip_terrazinefog-png{ + clip-path: xywh(0 98.99117276166457% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -49.054224464060525%); +} + +.ui_aicommand_build_open_aggressivepush-png{ + clip-path: xywh(0 99.11727616645649% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -49.18032786885246%); +} + +.ui_btn_generic_exclemation_red-png{ + clip-path: xywh(0 99.24337957124843% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -49.30643127364439%); +} + +.ui_glues_help_armyicon_protoss-png{ + clip-path: xywh(0 99.36948297604036% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -49.43253467843631%); +} + +.ui_glues_help_armyicon_terran-png{ + clip-path: xywh(0 99.49558638083228% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -49.558638083228246%); +} + +.ui_glues_help_armyicon_zerg-png{ + clip-path: xywh(0 99.62168978562421% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -49.68474148802018%); +} + +.ui_tipicon_evolution_hydralisk-waves-png{ + clip-path: xywh(0 99.74779319041615% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -49.8108448928121%); +} + +.vultureautolaunchers-png{ + clip-path: xywh(0 99.87389659520807% 100% 0.12610340479192939%); + transform: scale(1, 793) translate(0, -49.93694829760403%); +} + diff --git a/WebHostLib/templates/tracker__Starcraft2.html b/WebHostLib/templates/tracker__Starcraft2.html index d365d126..932f2150 100644 --- a/WebHostLib/templates/tracker__Starcraft2.html +++ b/WebHostLib/templates/tracker__Starcraft2.html @@ -1,1092 +1,2254 @@ +{# Most of this file is generated using code from the ap-sc2-tracker-proto repo. #} -{% macro sc2_icon(name) -%} - -{% endmacro -%} -{% macro sc2_progressive_icon(name, url, level) -%} - -{% endmacro -%} -{% macro sc2_progressive_icon_with_custom_name(item_name, url, title) -%} - -{% endmacro -%} -{%+ macro sc2_tint_level(level) %} - tint-level-{{ level }} -{%+ endmacro %} -{% macro sc2_render_area(area) %} - - {{ area }} {{'▼' if area != 'Total'}} - {{ checks_done[area] }} / {{ checks_in_area[area] }} - - - {% for location in location_info[area] %} - - {{ location }} - {{ '✔' if location_info[area][location] else '' }} - - {% endfor %} - -{% endmacro -%} -{% macro sc2_loop_areas(column_index, column_count) %} - {% for area in checks_in_area if checks_in_area[area] > 0 and area != 'Total' %} - {% if loop.index0 < (loop.length / column_count) * (column_index + 1) - and loop.index0 >= (loop.length / column_count) * (column_index) %} - {{ sc2_render_area(area) }} - {% endif %} - {% endfor %} -{% endmacro -%} - {{ player_name }}'s Tracker - - - + {{ player_name }}'s Tracker + + + + - - - {# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #} -
- Switch To Generic Tracker + +
+ Switch To Generic Tracker +
+
+
+

{{ player_name }}'s Starcraft 2 Tracker{{' - Finished' if game_finished}}

- -
- - - - - - - - - - - - - - -
- - - - - - - - - - - - -
-

{{ player_name }}'s Starcraft 2 Tracker

- Starting Resources -
+{{ minerals_count }}
+{{ vespene_count }}
+{{ supply_count }}
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Terran -
- Weapon & Armor Upgrades -
{{ sc2_progressive_icon('Progressive Terran Infantry Weapon', terran_infantry_weapon_url, terran_infantry_weapon_level) }}{{ sc2_progressive_icon('Progressive Terran Infantry Armor', terran_infantry_armor_url, terran_infantry_armor_level) }}{{ sc2_progressive_icon('Progressive Terran Vehicle Weapon', terran_vehicle_weapon_url, terran_vehicle_weapon_level) }}{{ sc2_progressive_icon('Progressive Terran Vehicle Armor', terran_vehicle_armor_url, terran_vehicle_armor_level) }}{{ sc2_progressive_icon('Progressive Terran Ship Weapon', terran_ship_weapon_url, terran_ship_weapon_level) }}{{ sc2_progressive_icon('Progressive Terran Ship Armor', terran_ship_armor_url, terran_ship_armor_level) }}{{ sc2_icon('Ultra-Capacitors') }}{{ sc2_icon('Vanadium Plating') }}
- Base -
{{ sc2_icon('Bunker') }}{{ sc2_icon('Projectile Accelerator (Bunker)') }}{{ sc2_icon('Neosteel Bunker (Bunker)') }}{{ sc2_icon('Shrike Turret (Bunker)') }}{{ sc2_icon('Fortified Bunker (Bunker)') }}{{ sc2_icon('Missile Turret') }}{{ sc2_icon('Titanium Housing (Missile Turret)') }}{{ sc2_icon('Hellstorm Batteries (Missile Turret)') }}{{ sc2_icon('Tech Reactor') }}{{ sc2_icon('Orbital Depots') }}
{{ sc2_icon('Command Center Reactor') }}{{ sc2_progressive_icon_with_custom_name('Progressive Orbital Command', orbital_command_url, orbital_command_name) }}{{ sc2_icon('Planetary Fortress') }}{{ sc2_progressive_icon_with_custom_name('Progressive Augmented Thrusters (Planetary Fortress)', augmented_thrusters_planetary_fortress_url, augmented_thrusters_planetary_fortress_name) }}{{ sc2_icon('Advanced Targeting (Planetary Fortress)') }}{{ sc2_icon('Micro-Filtering') }}{{ sc2_icon('Automated Refinery') }}{{ sc2_icon('Advanced Construction (SCV)') }}{{ sc2_icon('Dual-Fusion Welders (SCV)') }}{{ sc2_icon('Hostile Environment Adaptation (SCV)') }}
{{ sc2_icon('Sensor Tower') }}{{ sc2_icon('Perdition Turret') }}{{ sc2_icon('Hive Mind Emulator') }}{{ sc2_icon('Psi Disrupter') }}
- Infantry - - Vehicles -
{{ sc2_icon('Marine') }}{{ sc2_progressive_icon_with_custom_name('Progressive Stimpack (Marine)', stimpack_marine_url, stimpack_marine_name) }}{{ sc2_icon('Combat Shield (Marine)') }}{{ sc2_icon('Laser Targeting System (Marine)') }}{{ sc2_icon('Magrail Munitions (Marine)') }}{{ sc2_icon('Optimized Logistics (Marine)') }}{{ sc2_icon('Hellion') }}{{ sc2_icon('Twin-Linked Flamethrower (Hellion)') }}{{ sc2_icon('Thermite Filaments (Hellion)') }}{{ sc2_icon('Hellbat Aspect (Hellion)') }}{{ sc2_icon('Smart Servos (Hellion)') }}{{ sc2_icon('Optimized Logistics (Hellion)') }}{{ sc2_icon('Jump Jets (Hellion)') }}
{{ sc2_icon('Medic') }}{{ sc2_icon('Advanced Medic Facilities (Medic)') }}{{ sc2_icon('Stabilizer Medpacks (Medic)') }}{{ sc2_icon('Restoration (Medic)') }}{{ sc2_icon('Optical Flare (Medic)') }}{{ sc2_icon('Resource Efficiency (Medic)') }}{{ sc2_icon('Adaptive Medpacks (Medic)') }}{{ sc2_progressive_icon_with_custom_name('Progressive Stimpack (Hellion)', stimpack_hellion_url, stimpack_hellion_name) }}{{ sc2_icon('Infernal Plating (Hellion)') }}
{{ sc2_icon('Nano Projector (Medic)') }}{{ sc2_icon('Vulture') }}{{ sc2_progressive_icon_with_custom_name('Progressive Replenishable Magazine (Vulture)', replenishable_magazine_vulture_url, replenishable_magazine_vulture_name) }}{{ sc2_icon('Ion Thrusters (Vulture)') }}{{ sc2_icon('Auto Launchers (Vulture)') }}{{ sc2_icon('Auto-Repair (Vulture)') }}
{{ sc2_icon('Firebat') }}{{ sc2_icon('Incinerator Gauntlets (Firebat)') }}{{ sc2_icon('Juggernaut Plating (Firebat)') }}{{ sc2_progressive_icon_with_custom_name('Progressive Stimpack (Firebat)', stimpack_firebat_url, stimpack_firebat_name) }}{{ sc2_icon('Resource Efficiency (Firebat)') }}{{ sc2_icon('Infernal Pre-Igniter (Firebat)') }}{{ sc2_icon('Kinetic Foam (Firebat)') }}{{ sc2_icon('Cerberus Mine (Spider Mine)') }}{{ sc2_icon('High Explosive Munition (Spider Mine)') }}
{{ sc2_icon('Nano Projectors (Firebat)') }}{{ sc2_icon('Goliath') }}{{ sc2_icon('Multi-Lock Weapons System (Goliath)') }}{{ sc2_icon('Ares-Class Targeting System (Goliath)') }}{{ sc2_icon('Jump Jets (Goliath)') }}{{ sc2_icon('Shaped Hull (Goliath)') }}{{ sc2_icon('Optimized Logistics (Goliath)') }}{{ sc2_icon('Resource Efficiency (Goliath)') }}
{{ sc2_icon('Marauder') }}{{ sc2_icon('Concussive Shells (Marauder)') }}{{ sc2_icon('Kinetic Foam (Marauder)') }}{{ sc2_progressive_icon_with_custom_name('Progressive Stimpack (Marauder)', stimpack_marauder_url, stimpack_marauder_name) }}{{ sc2_icon('Laser Targeting System (Marauder)') }}{{ sc2_icon('Magrail Munitions (Marauder)') }}{{ sc2_icon('Internal Tech Module (Marauder)') }}{{ sc2_icon('Internal Tech Module (Goliath)') }}
{{ sc2_icon('Juggernaut Plating (Marauder)') }}{{ sc2_icon('Diamondback') }}{{ sc2_progressive_icon_with_custom_name('Progressive Tri-Lithium Power Cell (Diamondback)', trilithium_power_cell_diamondback_url, trilithium_power_cell_diamondback_name) }}{{ sc2_icon('Shaped Hull (Diamondback)') }}{{ sc2_icon('Hyperfluxor (Diamondback)') }}{{ sc2_icon('Burst Capacitors (Diamondback)') }}{{ sc2_icon('Ion Thrusters (Diamondback)') }}{{ sc2_icon('Resource Efficiency (Diamondback)') }}
{{ sc2_icon('Reaper') }}{{ sc2_icon('U-238 Rounds (Reaper)') }}{{ sc2_icon('G-4 Clusterbomb (Reaper)') }}{{ sc2_progressive_icon_with_custom_name('Progressive Stimpack (Reaper)', stimpack_reaper_url, stimpack_reaper_name) }}{{ sc2_icon('Laser Targeting System (Reaper)') }}{{ sc2_icon('Advanced Cloaking Field (Reaper)') }}{{ sc2_icon('Spider Mines (Reaper)') }}{{ sc2_icon('Siege Tank') }}{{ sc2_icon('Maelstrom Rounds (Siege Tank)') }}{{ sc2_icon('Shaped Blast (Siege Tank)') }}{{ sc2_icon('Jump Jets (Siege Tank)') }}{{ sc2_icon('Spider Mines (Siege Tank)') }}{{ sc2_icon('Smart Servos (Siege Tank)') }}{{ sc2_icon('Graduating Range (Siege Tank)') }}
{{ sc2_icon('Combat Drugs (Reaper)') }}{{ sc2_icon('Jet Pack Overdrive (Reaper)') }}{{ sc2_icon('Laser Targeting System (Siege Tank)') }}{{ sc2_icon('Advanced Siege Tech (Siege Tank)') }}{{ sc2_icon('Internal Tech Module (Siege Tank)') }}{{ sc2_icon('Shaped Hull (Siege Tank)') }}{{ sc2_icon('Resource Efficiency (Siege Tank)') }}
{{ sc2_icon('Ghost') }}{{ sc2_icon('Ocular Implants (Ghost)') }}{{ sc2_icon('Crius Suit (Ghost)') }}{{ sc2_icon('EMP Rounds (Ghost)') }}{{ sc2_icon('Lockdown (Ghost)') }}{{ sc2_icon('Resource Efficiency (Ghost)') }}{{ sc2_icon('Thor') }}{{ sc2_icon('330mm Barrage Cannon (Thor)') }}{{ sc2_progressive_icon_with_custom_name('Progressive Immortality Protocol (Thor)', immortality_protocol_thor_url, immortality_protocol_thor_name) }}{{ sc2_progressive_icon_with_custom_name('Progressive High Impact Payload (Thor)', high_impact_payload_thor_url, high_impact_payload_thor_name) }}{{ sc2_icon('Button With a Skull on It (Thor)') }}{{ sc2_icon('Laser Targeting System (Thor)') }}{{ sc2_icon('Large Scale Field Construction (Thor)') }}
{{ sc2_icon('Spectre') }}{{ sc2_icon('Psionic Lash (Spectre)') }}{{ sc2_icon('Nyx-Class Cloaking Module (Spectre)') }}{{ sc2_icon('Impaler Rounds (Spectre)') }}{{ sc2_icon('Resource Efficiency (Spectre)') }}{{ sc2_icon('Predator') }}{{ sc2_icon('Resource Efficiency (Predator)') }}{{ sc2_icon('Cloak (Predator)') }}{{ sc2_icon('Charge (Predator)') }}{{ sc2_icon('Predator\'s Fury (Predator)') }}
{{ sc2_icon('HERC') }}{{ sc2_icon('Juggernaut Plating (HERC)') }}{{ sc2_icon('Kinetic Foam (HERC)') }}{{ sc2_icon('Resource Efficiency (HERC)') }}{{ sc2_icon('Widow Mine') }}{{ sc2_icon('Drilling Claws (Widow Mine)') }}{{ sc2_icon('Concealment (Widow Mine)') }}{{ sc2_icon('Black Market Launchers (Widow Mine)') }}{{ sc2_icon('Executioner Missiles (Widow Mine)') }}
{{ sc2_icon('Cyclone') }}{{ sc2_icon('Mag-Field Accelerators (Cyclone)') }}{{ sc2_icon('Mag-Field Launchers (Cyclone)') }}{{ sc2_icon('Targeting Optics (Cyclone)') }}{{ sc2_icon('Rapid Fire Launchers (Cyclone)') }}{{ sc2_icon('Resource Efficiency (Cyclone)') }}{{ sc2_icon('Internal Tech Module (Cyclone)') }}
{{ sc2_icon('Warhound') }}{{ sc2_icon('Resource Efficiency (Warhound)') }}{{ sc2_icon('Reinforced Plating (Warhound)') }}
- Starships -
{{ sc2_icon('Medivac') }}{{ sc2_icon('Rapid Deployment Tube (Medivac)') }}{{ sc2_icon('Advanced Healing AI (Medivac)') }}{{ sc2_icon('Expanded Hull (Medivac)') }}{{ sc2_icon('Afterburners (Medivac)') }}{{ sc2_icon('Scatter Veil (Medivac)') }}{{ sc2_icon('Advanced Cloaking Field (Medivac)') }}{{ sc2_icon('Raven') }}{{ sc2_icon('Bio Mechanical Repair Drone (Raven)') }}{{ sc2_icon('Spider Mines (Raven)') }}{{ sc2_icon('Railgun Turret (Raven)') }}{{ sc2_icon('Hunter-Seeker Weapon (Raven)') }}{{ sc2_icon('Interference Matrix (Raven)') }}{{ sc2_icon('Anti-Armor Missile (Raven)') }}
{{ sc2_icon('Wraith') }}{{ sc2_progressive_icon_with_custom_name('Progressive Tomahawk Power Cells (Wraith)', tomahawk_power_cells_wraith_url, tomahawk_power_cells_wraith_name) }}{{ sc2_icon('Displacement Field (Wraith)') }}{{ sc2_icon('Advanced Laser Technology (Wraith)') }}{{ sc2_icon('Trigger Override (Wraith)') }}{{ sc2_icon('Internal Tech Module (Wraith)') }}{{ sc2_icon('Resource Efficiency (Wraith)') }}{{ sc2_icon('Internal Tech Module (Raven)') }}{{ sc2_icon('Resource Efficiency (Raven)') }}{{ sc2_icon('Durable Materials (Raven)') }}
{{ sc2_icon('Viking') }}{{ sc2_icon('Ripwave Missiles (Viking)') }}{{ sc2_icon('Phobos-Class Weapons System (Viking)') }}{{ sc2_icon('Smart Servos (Viking)') }}{{ sc2_icon('Anti-Mechanical Munition (Viking)') }}{{ sc2_icon('Shredder Rounds (Viking)') }}{{ sc2_icon('W.I.L.D. Missiles (Viking)') }}{{ sc2_icon('Science Vessel') }}{{ sc2_icon('EMP Shockwave (Science Vessel)') }}{{ sc2_icon('Defensive Matrix (Science Vessel)') }}{{ sc2_icon('Improved Nano-Repair (Science Vessel)') }}{{ sc2_icon('Advanced AI Systems (Science Vessel)') }}
{{ sc2_icon('Banshee') }}{{ sc2_progressive_icon_with_custom_name('Progressive Cross-Spectrum Dampeners (Banshee)', crossspectrum_dampeners_banshee_url, crossspectrum_dampeners_banshee_name) }}{{ sc2_icon('Shockwave Missile Battery (Banshee)') }}{{ sc2_icon('Hyperflight Rotors (Banshee)') }}{{ sc2_icon('Laser Targeting System (Banshee)') }}{{ sc2_icon('Internal Tech Module (Banshee)') }}{{ sc2_icon('Shaped Hull (Banshee)') }}{{ sc2_icon('Hercules') }}{{ sc2_icon('Internal Fusion Module (Hercules)') }}{{ sc2_icon('Tactical Jump (Hercules)') }}
{{ sc2_icon('Advanced Targeting Optics (Banshee)') }}{{ sc2_icon('Distortion Blasters (Banshee)') }}{{ sc2_icon('Rocket Barrage (Banshee)') }}{{ sc2_icon('Liberator') }}{{ sc2_icon('Advanced Ballistics (Liberator)') }}{{ sc2_icon('Raid Artillery (Liberator)') }}{{ sc2_icon('Cloak (Liberator)') }}{{ sc2_icon('Laser Targeting System (Liberator)') }}{{ sc2_icon('Optimized Logistics (Liberator)') }}{{ sc2_icon('Smart Servos (Liberator)') }}
{{ sc2_icon('Battlecruiser') }}{{ sc2_progressive_icon('Progressive Missile Pods (Battlecruiser)', missile_pods_battlecruiser_url, missile_pods_battlecruiser_level) }}{{ sc2_progressive_icon_with_custom_name('Progressive Defensive Matrix (Battlecruiser)', defensive_matrix_battlecruiser_url, defensive_matrix_battlecruiser_name) }}{{ sc2_icon('Tactical Jump (Battlecruiser)') }}{{ sc2_icon('Cloak (Battlecruiser)') }}{{ sc2_icon('ATX Laser Battery (Battlecruiser)') }}{{ sc2_icon('Optimized Logistics (Battlecruiser)') }}{{ sc2_icon('Resource Efficiency (Liberator)') }}
{{ sc2_icon('Internal Tech Module (Battlecruiser)') }}{{ sc2_icon('Behemoth Plating (Battlecruiser)') }}{{ sc2_icon('Covert Ops Engines (Battlecruiser)') }}{{ sc2_icon('Valkyrie') }}{{ sc2_icon('Enhanced Cluster Launchers (Valkyrie)') }}{{ sc2_icon('Shaped Hull (Valkyrie)') }}{{ sc2_icon('Flechette Missiles (Valkyrie)') }}{{ sc2_icon('Afterburners (Valkyrie)') }}{{ sc2_icon('Launching Vector Compensator (Valkyrie)') }}{{ sc2_icon('Resource Efficiency (Valkyrie)') }}
- Mercenaries -
{{ sc2_icon('War Pigs') }}{{ sc2_icon('Devil Dogs') }}{{ sc2_icon('Hammer Securities') }}{{ sc2_icon('Spartan Company') }}{{ sc2_icon('Siege Breakers') }}{{ sc2_icon('Hel\'s Angels') }}{{ sc2_icon('Dusk Wings') }}{{ sc2_icon('Jackson\'s Revenge') }}{{ sc2_icon('Skibi\'s Angels') }}{{ sc2_icon('Death Heads') }}{{ sc2_icon('Winged Nightmares') }}{{ sc2_icon('Midnight Riders') }}{{ sc2_icon('Brynhilds') }}{{ sc2_icon('Jotun') }}
- General Upgrades -
{{ sc2_progressive_icon('Progressive Fire-Suppression System', firesuppression_system_url, firesuppression_system_level) }}{{ sc2_icon('Orbital Strike') }}{{ sc2_icon('Cellular Reactor') }}{{ sc2_progressive_icon('Progressive Regenerative Bio-Steel', regenerative_biosteel_url, regenerative_biosteel_level) }}{{ sc2_icon('Structure Armor') }}{{ sc2_icon('Hi-Sec Auto Tracking') }}{{ sc2_icon('Advanced Optics') }}{{ sc2_icon('Rogue Forces') }}
- Nova Equipment -
{{ sc2_icon('C20A Canister Rifle (Nova Weapon)') }}{{ sc2_icon('Hellfire Shotgun (Nova Weapon)') }}{{ sc2_icon('Plasma Rifle (Nova Weapon)') }}{{ sc2_icon('Monomolecular Blade (Nova Weapon)') }}{{ sc2_icon('Blazefire Gunblade (Nova Weapon)') }}{{ sc2_icon('Stim Infusion (Nova Gadget)') }}{{ sc2_icon('Pulse Grenades (Nova Gadget)') }}{{ sc2_icon('Flashbang Grenades (Nova Gadget)') }}{{ sc2_icon('Ionic Force Field (Nova Gadget)') }}{{ sc2_icon('Holo Decoy (Nova Gadget)') }}
{{ sc2_progressive_icon_with_custom_name('Progressive Stealth Suit Module (Nova Suit Module)', stealth_suit_module_nova_suit_module_url, stealth_suit_module_nova_suit_module_name) }}{{ sc2_icon('Energy Suit Module (Nova Suit Module)') }}{{ sc2_icon('Armored Suit Module (Nova Suit Module)') }}{{ sc2_icon('Jump Suit Module (Nova Suit Module)') }}{{ sc2_icon('Ghost Visor (Nova Equipment)') }}{{ sc2_icon('Rangefinder Oculus (Nova Equipment)') }}{{ sc2_icon('Domination (Nova Ability)') }}{{ sc2_icon('Blink (Nova Ability)') }}{{ sc2_icon('Tac Nuke Strike (Nova Ability)') }}
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Zerg -
- Weapon & Armor Upgrades -
{{ sc2_progressive_icon('Progressive Zerg Melee Attack', zerg_melee_attack_url, zerg_melee_attack_level) }}{{ sc2_progressive_icon('Progressive Zerg Missile Attack', zerg_missile_attack_url, zerg_missile_attack_level) }}{{ sc2_progressive_icon('Progressive Zerg Ground Carapace', zerg_ground_carapace_url, zerg_ground_carapace_level) }}{{ sc2_progressive_icon('Progressive Zerg Flyer Attack', zerg_flyer_attack_url, zerg_flyer_attack_level) }}{{ sc2_progressive_icon('Progressive Zerg Flyer Carapace', zerg_flyer_carapace_url, zerg_flyer_carapace_level) }}
- Base -
{{ sc2_icon('Automated Extractors (Kerrigan Tier 3)') }}{{ sc2_icon('Vespene Efficiency (Kerrigan Tier 5)') }}{{ sc2_icon('Twin Drones (Kerrigan Tier 5)') }}{{ sc2_icon('Improved Overlords (Kerrigan Tier 3)') }}{{ sc2_icon('Ventral Sacs (Overlord)') }}
{{ sc2_icon('Malignant Creep (Kerrigan Tier 5)') }}{{ sc2_icon('Spine Crawler') }}{{ sc2_icon('Spore Crawler') }}
- Units -
{{ sc2_icon('Zergling') }}{{ sc2_icon('Raptor Strain (Zergling)') }}{{ sc2_icon('Swarmling Strain (Zergling)') }}{{ sc2_icon('Hardened Carapace (Zergling)') }}{{ sc2_icon('Adrenal Overload (Zergling)') }}{{ sc2_icon('Metabolic Boost (Zergling)') }}{{ sc2_icon('Shredding Claws (Zergling)') }}{{ sc2_icon('Zergling Reconstitution (Kerrigan Tier 3)') }}
{{ sc2_icon('Baneling Aspect (Zergling)') }}{{ sc2_icon('Splitter Strain (Baneling)') }}{{ sc2_icon('Hunter Strain (Baneling)') }}{{ sc2_icon('Corrosive Acid (Baneling)') }}{{ sc2_icon('Rupture (Baneling)') }}{{ sc2_icon('Regenerative Acid (Baneling)') }}{{ sc2_icon('Centrifugal Hooks (Baneling)') }}
{{ sc2_icon('Tunneling Jaws (Baneling)') }}{{ sc2_icon('Rapid Metamorph (Baneling)') }}
{{ sc2_icon('Swarm Queen') }}{{ sc2_icon('Spawn Larvae (Swarm Queen)') }}{{ sc2_icon('Deep Tunnel (Swarm Queen)') }}{{ sc2_icon('Organic Carapace (Swarm Queen)') }}{{ sc2_icon('Bio-Mechanical Transfusion (Swarm Queen)') }}{{ sc2_icon('Resource Efficiency (Swarm Queen)') }}{{ sc2_icon('Incubator Chamber (Swarm Queen)') }}
{{ sc2_icon('Roach') }}{{ sc2_icon('Vile Strain (Roach)') }}{{ sc2_icon('Corpser Strain (Roach)') }}{{ sc2_icon('Hydriodic Bile (Roach)') }}{{ sc2_icon('Adaptive Plating (Roach)') }}{{ sc2_icon('Tunneling Claws (Roach)') }}{{ sc2_icon('Glial Reconstitution (Roach)') }}{{ sc2_icon('Organic Carapace (Roach)') }}
{{ sc2_icon('Ravager Aspect (Roach)') }}{{ sc2_icon('Potent Bile (Ravager)') }}{{ sc2_icon('Bloated Bile Ducts (Ravager)') }}{{ sc2_icon('Deep Tunnel (Ravager)') }}
{{ sc2_icon('Hydralisk') }}{{ sc2_icon('Frenzy (Hydralisk)') }}{{ sc2_icon('Ancillary Carapace (Hydralisk)') }}{{ sc2_icon('Grooved Spines (Hydralisk)') }}{{ sc2_icon('Muscular Augments (Hydralisk)') }}{{ sc2_icon('Resource Efficiency (Hydralisk)') }}
{{ sc2_icon('Impaler Aspect (Hydralisk)') }}{{ sc2_icon('Adaptive Talons (Impaler)') }}{{ sc2_icon('Secretion Glands (Impaler)') }}{{ sc2_icon('Hardened Tentacle Spines (Impaler)') }}
{{ sc2_icon('Lurker Aspect (Hydralisk)') }}{{ sc2_icon('Seismic Spines (Lurker)') }}{{ sc2_icon('Adapted Spines (Lurker)') }}
{{ sc2_icon('Aberration') }}
{{ sc2_icon('Swarm Host') }}{{ sc2_icon('Carrion Strain (Swarm Host)') }}{{ sc2_icon('Creeper Strain (Swarm Host)') }}{{ sc2_icon('Burrow (Swarm Host)') }}{{ sc2_icon('Rapid Incubation (Swarm Host)') }}{{ sc2_icon('Pressurized Glands (Swarm Host)') }}{{ sc2_icon('Locust Metabolic Boost (Swarm Host)') }}{{ sc2_icon('Enduring Locusts (Swarm Host)') }}
{{ sc2_icon('Organic Carapace (Swarm Host)') }}{{ sc2_icon('Resource Efficiency (Swarm Host)') }}
{{ sc2_icon('Infestor') }}{{ sc2_icon('Infested Terran (Infestor)') }}{{ sc2_icon('Microbial Shroud (Infestor)') }}
{{ sc2_icon('Defiler') }}
{{ sc2_icon('Ultralisk') }}{{ sc2_icon('Noxious Strain (Ultralisk)') }}{{ sc2_icon('Torrasque Strain (Ultralisk)') }}{{ sc2_icon('Burrow Charge (Ultralisk)') }}{{ sc2_icon('Tissue Assimilation (Ultralisk)') }}{{ sc2_icon('Monarch Blades (Ultralisk)') }}{{ sc2_icon('Anabolic Synthesis (Ultralisk)') }}{{ sc2_icon('Chitinous Plating (Ultralisk)') }}
{{ sc2_icon('Organic Carapace (Ultralisk)') }}{{ sc2_icon('Resource Efficiency (Ultralisk)') }}
{{ sc2_icon('Mutalisk') }}{{ sc2_icon('Rapid Regeneration (Mutalisk)') }}{{ sc2_icon('Sundering Glaive (Mutalisk)') }}{{ sc2_icon('Vicious Glaive (Mutalisk)') }}{{ sc2_icon('Severing Glaive (Mutalisk)') }}{{ sc2_icon('Aerodynamic Glaive Shape (Mutalisk)') }}
{{ sc2_icon('Corruptor') }}{{ sc2_icon('Corruption (Corruptor)') }}{{ sc2_icon('Caustic Spray (Corruptor)') }}
{{ sc2_icon('Brood Lord Aspect (Mutalisk/Corruptor)') }}{{ sc2_icon('Porous Cartilage (Brood Lord)') }}{{ sc2_icon('Evolved Carapace (Brood Lord)') }}{{ sc2_icon('Splitter Mitosis (Brood Lord)') }}{{ sc2_icon('Resource Efficiency (Brood Lord)') }}
{{ sc2_icon('Viper Aspect (Mutalisk/Corruptor)') }}{{ sc2_icon('Parasitic Bomb (Viper)') }}{{ sc2_icon('Paralytic Barbs (Viper)') }}{{ sc2_icon('Virulent Microbes (Viper)') }}
{{ sc2_icon('Guardian Aspect (Mutalisk/Corruptor)') }}{{ sc2_icon('Prolonged Dispersion (Guardian)') }}{{ sc2_icon('Primal Adaptation (Guardian)') }}{{ sc2_icon('Soronan Acid (Guardian)') }}
{{ sc2_icon('Devourer Aspect (Mutalisk/Corruptor)') }}{{ sc2_icon('Corrosive Spray (Devourer)') }}{{ sc2_icon('Gaping Maw (Devourer)') }}{{ sc2_icon('Improved Osmosis (Devourer)') }}{{ sc2_icon('Prescient Spores (Devourer)') }}
{{ sc2_icon('Brood Queen') }}{{ sc2_icon('Fungal Growth (Brood Queen)') }}{{ sc2_icon('Ensnare (Brood Queen)') }}{{ sc2_icon('Enhanced Mitochondria (Brood Queen)') }}
{{ sc2_icon('Scourge') }}{{ sc2_icon('Virulent Spores (Scourge)') }}{{ sc2_icon('Resource Efficiency (Scourge)') }}{{ sc2_icon('Swarm Scourge (Scourge)') }}
- Mercenaries -
{{ sc2_icon('Infested Medics') }}{{ sc2_icon('Infested Siege Tanks') }}{{ sc2_icon('Infested Banshees') }}
- Kerrigan -
Level: {{ kerrigan_level }}
{{ sc2_icon('Primal Form (Kerrigan)') }}
{{ sc2_icon('Kinetic Blast (Kerrigan Tier 1)') }}{{ sc2_icon('Heroic Fortitude (Kerrigan Tier 1)') }}{{ sc2_icon('Leaping Strike (Kerrigan Tier 1)') }}{{ sc2_icon('Crushing Grip (Kerrigan Tier 2)') }}{{ sc2_icon('Chain Reaction (Kerrigan Tier 2)') }}{{ sc2_icon('Psionic Shift (Kerrigan Tier 2)') }}
{{ sc2_icon('Wild Mutation (Kerrigan Tier 4)') }}{{ sc2_icon('Spawn Banelings (Kerrigan Tier 4)') }}{{ sc2_icon('Mend (Kerrigan Tier 4)') }}{{ sc2_icon('Infest Broodlings (Kerrigan Tier 6)') }}{{ sc2_icon('Fury (Kerrigan Tier 6)') }}{{ sc2_icon('Ability Efficiency (Kerrigan Tier 6)') }}
{{ sc2_icon('Apocalypse (Kerrigan Tier 7)') }}{{ sc2_icon('Spawn Leviathan (Kerrigan Tier 7)') }}{{ sc2_icon('Drop-Pods (Kerrigan Tier 7)') }}
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Protoss -
- Weapon & Armor Upgrades -
{{ sc2_progressive_icon('Progressive Protoss Ground Weapon', protoss_ground_weapon_url, protoss_ground_weapon_level) }}{{ sc2_progressive_icon('Progressive Protoss Ground Armor', protoss_ground_armor_url, protoss_ground_armor_level) }}{{ sc2_progressive_icon('Progressive Protoss Air Weapon', protoss_air_weapon_url, protoss_air_weapon_level) }}{{ sc2_progressive_icon('Progressive Protoss Air Armor', protoss_air_armor_url, protoss_air_armor_level) }}{{ sc2_progressive_icon('Progressive Protoss Shields', protoss_shields_url, protoss_shields_level) }}{{ sc2_icon('Quatro') }}
- Base -
{{ sc2_icon('Photon Cannon') }}{{ sc2_icon('Khaydarin Monolith') }}{{ sc2_icon('Shield Battery') }}{{ sc2_icon('Enhanced Targeting') }}{{ sc2_icon('Optimized Ordnance') }}{{ sc2_icon('Khalai Ingenuity') }}{{ sc2_icon('Orbital Assimilators') }}{{ sc2_icon('Amplified Assimilators') }}
{{ sc2_icon('Warp Harmonization') }}{{ sc2_icon('Superior Warp Gates') }}{{ sc2_icon('Nexus Overcharge') }}
- Gateway -
{{ sc2_icon('Zealot') }}{{ sc2_icon('Centurion') }}{{ sc2_icon('Sentinel') }}{{ sc2_icon('Leg Enhancements (Zealot/Sentinel/Centurion)') }}{{ sc2_icon('Shield Capacity (Zealot/Sentinel/Centurion)') }}
{{ sc2_icon('Supplicant') }}{{ sc2_icon('Blood Shield (Supplicant)') }}{{ sc2_icon('Soul Augmentation (Supplicant)') }}{{ sc2_icon('Shield Regeneration (Supplicant)') }}
{{ sc2_icon('Sentry') }}{{ sc2_icon('Force Field (Sentry)') }}{{ sc2_icon('Hallucination (Sentry)') }}
{{ sc2_icon('Energizer') }}{{ sc2_icon('Reclamation (Energizer)') }}{{ sc2_icon('Forged Chassis (Energizer)') }}{{ sc2_icon('Cloaking Module (Sentry/Energizer/Havoc)') }}{{ sc2_icon('Rapid Recharging (Sentry/Energizer/Havoc/Shield Battery)') }}
{{ sc2_icon('Havoc') }}{{ sc2_icon('Detect Weakness (Havoc)') }}{{ sc2_icon('Bloodshard Resonance (Havoc)') }}
{{ sc2_icon('Stalker') }}{{ sc2_icon('Instigator') }}{{ sc2_icon('Slayer') }}{{ sc2_icon('Disintegrating Particles (Stalker/Instigator/Slayer)') }}{{ sc2_icon('Particle Reflection (Stalker/Instigator/Slayer)') }}
{{ sc2_icon('Dragoon') }}{{ sc2_icon('High Impact Phase Disruptor (Dragoon)') }}{{ sc2_icon('Trillic Compression System (Dragoon)') }}{{ sc2_icon('Singularity Charge (Dragoon)') }}{{ sc2_icon('Enhanced Strider Servos (Dragoon)') }}
{{ sc2_icon('Adept') }}{{ sc2_icon('Shockwave (Adept)') }}{{ sc2_icon('Resonating Glaives (Adept)') }}{{ sc2_icon('Phase Bulwark (Adept)') }}
{{ sc2_icon('High Templar') }}{{ sc2_icon('Signifier') }}{{ sc2_icon('Unshackled Psionic Storm (High Templar/Signifier)') }}{{ sc2_icon('Hallucination (High Templar/Signifier)') }}{{ sc2_icon('Khaydarin Amulet (High Templar/Signifier)') }}{{ sc2_icon('High Archon (Archon)') }}
{{ sc2_icon('Ascendant') }}{{ sc2_icon('Power Overwhelming (Ascendant)') }}{{ sc2_icon('Chaotic Attunement (Ascendant)') }}{{ sc2_icon('Blood Amulet (Ascendant)') }}
{{ sc2_icon('Dark Archon') }}{{ sc2_icon('Feedback (Dark Archon)') }}{{ sc2_icon('Maelstrom (Dark Archon)') }}{{ sc2_icon('Argus Talisman (Dark Archon)') }}
{{ sc2_icon('Dark Templar') }}{{ sc2_icon('Dark Archon Meld (Dark Templar)') }}
{{ sc2_icon('Avenger') }}{{ sc2_icon('Blood Hunter') }}{{ sc2_icon('Shroud of Adun (Dark Templar/Avenger/Blood Hunter)') }}{{ sc2_icon('Shadow Guard Training (Dark Templar/Avenger/Blood Hunter)') }}{{ sc2_icon('Blink (Dark Templar/Avenger/Blood Hunter)') }}{{ sc2_icon('Resource Efficiency (Dark Templar/Avenger/Blood Hunter)') }}
- Robotics Facility -
{{ sc2_icon('Warp Prism') }}{{ sc2_icon('Gravitic Drive (Warp Prism)') }}{{ sc2_icon('Phase Blaster (Warp Prism)') }}{{ sc2_icon('War Configuration (Warp Prism)') }}
{{ sc2_icon('Immortal') }}{{ sc2_icon('Annihilator') }}{{ sc2_icon('Singularity Charge (Immortal/Annihilator)') }}{{ sc2_icon('Advanced Targeting Mechanics (Immortal/Annihilator)') }}
{{ sc2_icon('Vanguard') }}{{ sc2_icon('Agony Launchers (Vanguard)') }}{{ sc2_icon('Matter Dispersion (Vanguard)') }}
{{ sc2_icon('Colossus') }}{{ sc2_icon('Pacification Protocol (Colossus)') }}
{{ sc2_icon('Wrathwalker') }}{{ sc2_icon('Rapid Power Cycling (Wrathwalker)') }}{{ sc2_icon('Eye of Wrath (Wrathwalker)') }}
{{ sc2_icon('Observer') }}{{ sc2_icon('Gravitic Boosters (Observer)') }}{{ sc2_icon('Sensor Array (Observer)') }}
{{ sc2_icon('Reaver') }}{{ sc2_icon('Scarab Damage (Reaver)') }}{{ sc2_icon('Solarite Payload (Reaver)') }}{{ sc2_icon('Reaver Capacity (Reaver)') }}{{ sc2_icon('Resource Efficiency (Reaver)') }}
{{ sc2_icon('Disruptor') }}
- Stargate -
{{ sc2_icon('Phoenix') }}{{ sc2_icon('Mirage') }}{{ sc2_icon('Ionic Wavelength Flux (Phoenix/Mirage)') }}{{ sc2_icon('Anion Pulse-Crystals (Phoenix/Mirage)') }}
{{ sc2_icon('Corsair') }}{{ sc2_icon('Stealth Drive (Corsair)') }}{{ sc2_icon('Argus Jewel (Corsair)') }}{{ sc2_icon('Sustaining Disruption (Corsair)') }}{{ sc2_icon('Neutron Shields (Corsair)') }}
{{ sc2_icon('Destroyer') }}{{ sc2_icon('Reforged Bloodshard Core (Destroyer)') }}
{{ sc2_icon('Void Ray') }}{{ sc2_icon('Flux Vanes (Void Ray/Destroyer)') }}
{{ sc2_icon('Carrier') }}{{ sc2_icon('Graviton Catapult (Carrier)') }}{{ sc2_icon('Hull of Past Glories (Carrier)') }}
{{ sc2_icon('Scout') }}{{ sc2_icon('Combat Sensor Array (Scout)') }}{{ sc2_icon('Apial Sensors (Scout)') }}{{ sc2_icon('Gravitic Thrusters (Scout)') }}{{ sc2_icon('Advanced Photon Blasters (Scout)') }}
{{ sc2_icon('Tempest') }}{{ sc2_icon('Tectonic Destabilizers (Tempest)') }}{{ sc2_icon('Quantic Reactor (Tempest)') }}{{ sc2_icon('Gravity Sling (Tempest)') }}
{{ sc2_icon('Mothership') }}
{{ sc2_icon('Arbiter') }}{{ sc2_icon('Chronostatic Reinforcement (Arbiter)') }}{{ sc2_icon('Khaydarin Core (Arbiter)') }}{{ sc2_icon('Spacetime Anchor (Arbiter)') }}{{ sc2_icon('Resource Efficiency (Arbiter)') }}{{ sc2_icon('Enhanced Cloak Field (Arbiter)') }}
{{ sc2_icon('Oracle') }}{{ sc2_icon('Stealth Drive (Oracle)') }}{{ sc2_icon('Stasis Calibration (Oracle)') }}{{ sc2_icon('Temporal Acceleration Beam (Oracle)') }}
- General Upgrades -
{{ sc2_icon('Matrix Overload') }}{{ sc2_icon('Guardian Shell') }}
- Spear of Adun -
{{ sc2_icon('Chrono Surge (Spear of Adun Calldown)') }}{{ sc2_progressive_icon_with_custom_name('Progressive Proxy Pylon (Spear of Adun Calldown)', proxy_pylon_spear_of_adun_calldown_url, proxy_pylon_spear_of_adun_calldown_name) }}{{ sc2_icon('Pylon Overcharge (Spear of Adun Calldown)') }}{{ sc2_icon('Mass Recall (Spear of Adun Calldown)') }}{{ sc2_icon('Shield Overcharge (Spear of Adun Calldown)') }}{{ sc2_icon('Deploy Fenix (Spear of Adun Calldown)') }}{{ sc2_icon('Reconstruction Beam (Spear of Adun Auto-Cast)') }}
{{ sc2_icon('Orbital Strike (Spear of Adun Calldown)') }}{{ sc2_icon('Temporal Field (Spear of Adun Calldown)') }}{{ sc2_icon('Solar Lance (Spear of Adun Calldown)') }}{{ sc2_icon('Purifier Beam (Spear of Adun Calldown)') }}{{ sc2_icon('Time Stop (Spear of Adun Calldown)') }}{{ sc2_icon('Solar Bombardment (Spear of Adun Calldown)') }}{{ sc2_icon('Overwatch (Spear of Adun Auto-Cast)') }}
-
- - - - - - -
- - {{ sc2_loop_areas(0, 3) }} -
-
- - {{ sc2_loop_areas(1, 3) }} -
-
- - {{ sc2_loop_areas(2, 3) }} - - {{ sc2_render_area('Total') }} -
 
-
-
+
+
+ +

Filler Items

+
+
+
+
+ +
+ +{{minerals_count}} +
+
+
+ +
+ +{{vespene_count}} +
+
+
+ +
+ +{{supply_count}} +
+
+
+ +
+ +{{max_supply_count}} +
+
+
+ +
+ -{{reduced_supply_count}} +
+
+
+ +
+ {{construction_speed_count}} +
+
+
+ +
+ {{shield_regen_count}} +
+
+
+ +
+ {{upgrade_speed_count}} +
+
+
+ +
+ {{research_cost_count}} +
+
- - +
+
+ +

Terran Items

+
+
+
+
+ + Barracks +
+
+ + Factory +
+
+ + Starport +
+
+ + Buildings +
+
+ + Mercenaries +
+
+ + Miscellaneous +
+
+
+
+ — Barracks — +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ — Factory — +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ — Starport — +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ — Buildings — +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ — Mercenaries — +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ — Miscellaneous — +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +

Zerg Items

+
+
+
+
+ + Ground +
+
+ + Flyers +
+
+ + Morphs +
+
+ + Infested +
+
+ + Buildings +
+
+ + Mercenaries +
+
+ + Miscellaneous +
+
+
+
+ — Ground — +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ — Flyers — +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ — Morphs — +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ — Infested — +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ — Buildings — +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ — Mercenaries — +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ — Miscellaneous — +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +

Protoss Items

+
+
+
+
+ + Gateway +
+
+ + Robotics Facility +
+
+ + Stargate +
+
+ + Buildings +
+
+ + Miscellaneous +
+
+
+
+ — Gateway — +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ — Robotics Facility — +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ — Stargate — +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ — Buildings — +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ — Miscellaneous — +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +

Nova Items

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +

Kerrigan Items

+
+
+
+ + {{kerrigan_level}} +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +

Keys

+
+
+
    + {% for key_name, key_amount in keys.items() %} +
  • {{key_name}}{{ ' (' + (key_amount | string) + ')' if key_amount > 1}}
  • + {% endfor %} +
+
+
+
+
+ +

Locations

+
+ {{checked_locations | length}} / {{locations | length}} = {{((checked_locations | length) / (locations | length) * 100) | round(3)}}% +
+
    + {% for mission_name, location_info in missions.items() %} +
  1. {{mission_name}}
      + {% for location_name, collected in location_info %} +
    • {{location_name}}
    • + {% endfor %} +
    +
  2. + {% endfor %} +
+
+
+
+
+ \ No newline at end of file diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 4b92f4b4..18145dae 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -1247,1114 +1247,225 @@ if "ChecksFinder" in network_data_package["games"]: if "Starcraft 2" in network_data_package["games"]: def render_Starcraft2_tracker(tracker_data: TrackerData, team: int, player: int) -> str: - SC2WOL_LOC_ID_OFFSET = 1000 - SC2HOTS_LOC_ID_OFFSET = 20000000 # Avoid clashes with The Legend of Zelda - SC2LOTV_LOC_ID_OFFSET = SC2HOTS_LOC_ID_OFFSET + 2000 - SC2NCO_LOC_ID_OFFSET = SC2LOTV_LOC_ID_OFFSET + 2500 - SC2WOL_ITEM_ID_OFFSET = 1000 - SC2HOTS_ITEM_ID_OFFSET = SC2WOL_ITEM_ID_OFFSET + 1000 - SC2LOTV_ITEM_ID_OFFSET = SC2HOTS_ITEM_ID_OFFSET + 1000 + SC2HOTS_ITEM_ID_OFFSET = 2000 + SC2LOTV_ITEM_ID_OFFSET = 2000 + SC2_KEY_ITEM_ID_OFFSET = 4000 + NCO_LOCATION_ID_LOW = 20004500 + NCO_LOCATION_ID_HIGH = NCO_LOCATION_ID_LOW + 1000 + STARTING_MINERALS_ITEM_ID = 1800 + STARTING_VESPENE_ITEM_ID = 1801 + STARTING_SUPPLY_ITEM_ID = 1802 + # NOTHING_ITEM_ID = 1803 + MAX_SUPPLY_ITEM_ID = 1804 + SHIELD_REGENERATION_ITEM_ID = 1805 + BUILDING_CONSTRUCTION_SPEED_ITEM_ID = 1806 + UPGRADE_RESEARCH_SPEED_ITEM_ID = 1807 + UPGRADE_RESEARCH_COST_ITEM_ID = 1808 + REDUCED_MAX_SUPPLY_ITEM_ID = 1850 slot_data = tracker_data.get_slot_data(team, player) - minerals_per_item = slot_data.get("minerals_per_item", 15) - vespene_per_item = slot_data.get("vespene_per_item", 15) - starting_supply_per_item = slot_data.get("starting_supply_per_item", 2) - - github_icon_base_url = "https://matthewmarinets.github.io/ap_sc2_icons/icons/" - organics_icon_base_url = "https://0rganics.org/archipelago/sc2wol/" - - icons = { - "Starting Minerals": github_icon_base_url + "blizzard/icon-mineral-nobg.png", - "Starting Vespene": github_icon_base_url + "blizzard/icon-gas-terran-nobg.png", - "Starting Supply": github_icon_base_url + "blizzard/icon-supply-terran_nobg.png", - - "Terran Infantry Weapons Level 1": github_icon_base_url + "blizzard/btn-upgrade-terran-infantryweaponslevel1.png", - "Terran Infantry Weapons Level 2": github_icon_base_url + "blizzard/btn-upgrade-terran-infantryweaponslevel2.png", - "Terran Infantry Weapons Level 3": github_icon_base_url + "blizzard/btn-upgrade-terran-infantryweaponslevel3.png", - "Terran Infantry Armor Level 1": github_icon_base_url + "blizzard/btn-upgrade-terran-infantryarmorlevel1.png", - "Terran Infantry Armor Level 2": github_icon_base_url + "blizzard/btn-upgrade-terran-infantryarmorlevel2.png", - "Terran Infantry Armor Level 3": github_icon_base_url + "blizzard/btn-upgrade-terran-infantryarmorlevel3.png", - "Terran Vehicle Weapons Level 1": github_icon_base_url + "blizzard/btn-upgrade-terran-vehicleweaponslevel1.png", - "Terran Vehicle Weapons Level 2": github_icon_base_url + "blizzard/btn-upgrade-terran-vehicleweaponslevel2.png", - "Terran Vehicle Weapons Level 3": github_icon_base_url + "blizzard/btn-upgrade-terran-vehicleweaponslevel3.png", - "Terran Vehicle Armor Level 1": github_icon_base_url + "blizzard/btn-upgrade-terran-vehicleplatinglevel1.png", - "Terran Vehicle Armor Level 2": github_icon_base_url + "blizzard/btn-upgrade-terran-vehicleplatinglevel2.png", - "Terran Vehicle Armor Level 3": github_icon_base_url + "blizzard/btn-upgrade-terran-vehicleplatinglevel3.png", - "Terran Ship Weapons Level 1": github_icon_base_url + "blizzard/btn-upgrade-terran-shipweaponslevel1.png", - "Terran Ship Weapons Level 2": github_icon_base_url + "blizzard/btn-upgrade-terran-shipweaponslevel2.png", - "Terran Ship Weapons Level 3": github_icon_base_url + "blizzard/btn-upgrade-terran-shipweaponslevel3.png", - "Terran Ship Armor Level 1": github_icon_base_url + "blizzard/btn-upgrade-terran-shipplatinglevel1.png", - "Terran Ship Armor Level 2": github_icon_base_url + "blizzard/btn-upgrade-terran-shipplatinglevel2.png", - "Terran Ship Armor Level 3": github_icon_base_url + "blizzard/btn-upgrade-terran-shipplatinglevel3.png", - - "Bunker": "https://static.wikia.nocookie.net/starcraft/images/c/c5/Bunker_SC2_Icon1.jpg", - "Missile Turret": "https://static.wikia.nocookie.net/starcraft/images/5/5f/MissileTurret_SC2_Icon1.jpg", - "Sensor Tower": "https://static.wikia.nocookie.net/starcraft/images/d/d2/SensorTower_SC2_Icon1.jpg", - - "Projectile Accelerator (Bunker)": github_icon_base_url + "blizzard/btn-upgrade-zerg-stukov-bunkerresearchbundle_05.png", - "Neosteel Bunker (Bunker)": organics_icon_base_url + "NeosteelBunker.png", - "Titanium Housing (Missile Turret)": organics_icon_base_url + "TitaniumHousing.png", - "Hellstorm Batteries (Missile Turret)": github_icon_base_url + "blizzard/btn-ability-stetmann-corruptormissilebarrage.png", - "Advanced Construction (SCV)": github_icon_base_url + "blizzard/btn-ability-mengsk-trooper-advancedconstruction.png", - "Dual-Fusion Welders (SCV)": github_icon_base_url + "blizzard/btn-upgrade-swann-scvdoublerepair.png", - "Hostile Environment Adaptation (SCV)": github_icon_base_url + "blizzard/btn-upgrade-swann-hellarmor.png", - "Fire-Suppression System Level 1": organics_icon_base_url + "Fire-SuppressionSystem.png", - "Fire-Suppression System Level 2": github_icon_base_url + "blizzard/btn-upgrade-swann-firesuppressionsystem.png", - - "Orbital Command": organics_icon_base_url + "OrbitalCommandCampaign.png", - "Planetary Command Module": github_icon_base_url + "original/btn-orbital-fortress.png", - "Lift Off (Planetary Fortress)": github_icon_base_url + "blizzard/btn-ability-terran-liftoff.png", - "Armament Stabilizers (Planetary Fortress)": github_icon_base_url + "blizzard/btn-ability-mengsk-siegetank-flyingtankarmament.png", - "Advanced Targeting (Planetary Fortress)": github_icon_base_url + "blizzard/btn-ability-terran-detectionconedebuff.png", - - "Marine": "https://static.wikia.nocookie.net/starcraft/images/4/47/Marine_SC2_Icon1.jpg", - "Medic": github_icon_base_url + "blizzard/btn-unit-terran-medic.png", - "Firebat": github_icon_base_url + "blizzard/btn-unit-terran-firebat.png", - "Marauder": "https://static.wikia.nocookie.net/starcraft/images/b/ba/Marauder_SC2_Icon1.jpg", - "Reaper": "https://static.wikia.nocookie.net/starcraft/images/7/7d/Reaper_SC2_Icon1.jpg", - "Ghost": "https://static.wikia.nocookie.net/starcraft/images/6/6e/Ghost_SC2_Icon1.jpg", - "Spectre": github_icon_base_url + "original/btn-unit-terran-spectre.png", - "HERC": github_icon_base_url + "blizzard/btn-unit-terran-herc.png", - - "Stimpack (Marine)": github_icon_base_url + "blizzard/btn-ability-terran-stimpack-color.png", - "Super Stimpack (Marine)": github_icon_base_url + "blizzard/btn-upgrade-terran-superstimppack.png", - "Combat Shield (Marine)": github_icon_base_url + "blizzard/btn-techupgrade-terran-combatshield-color.png", - "Laser Targeting System (Marine)": github_icon_base_url + "blizzard/btn-upgrade-terran-lazertargetingsystem.png", - "Magrail Munitions (Marine)": github_icon_base_url + "blizzard/btn-upgrade-terran-magrailmunitions.png", - "Optimized Logistics (Marine)": github_icon_base_url + "blizzard/btn-upgrade-terran-optimizedlogistics.png", - "Advanced Medic Facilities (Medic)": organics_icon_base_url + "AdvancedMedicFacilities.png", - "Stabilizer Medpacks (Medic)": github_icon_base_url + "blizzard/btn-upgrade-raynor-stabilizermedpacks.png", - "Restoration (Medic)": github_icon_base_url + "original/btn-ability-terran-restoration@scbw.png", - "Optical Flare (Medic)": github_icon_base_url + "blizzard/btn-upgrade-protoss-fenix-dragoonsolariteflare.png", - "Resource Efficiency (Medic)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png", - "Adaptive Medpacks (Medic)": github_icon_base_url + "blizzard/btn-ability-terran-heal-color.png", - "Nano Projector (Medic)": github_icon_base_url + "blizzard/talent-raynor-level03-firebatmedicrange.png", - "Incinerator Gauntlets (Firebat)": github_icon_base_url + "blizzard/btn-upgrade-raynor-incineratorgauntlets.png", - "Juggernaut Plating (Firebat)": github_icon_base_url + "blizzard/btn-upgrade-raynor-juggernautplating.png", - "Stimpack (Firebat)": github_icon_base_url + "blizzard/btn-ability-terran-stimpack-color.png", - "Super Stimpack (Firebat)": github_icon_base_url + "blizzard/btn-upgrade-terran-superstimppack.png", - "Resource Efficiency (Firebat)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png", - "Infernal Pre-Igniter (Firebat)": github_icon_base_url + "blizzard/btn-upgrade-terran-infernalpreigniter.png", - "Kinetic Foam (Firebat)": organics_icon_base_url + "KineticFoam.png", - "Nano Projectors (Firebat)": github_icon_base_url + "blizzard/talent-raynor-level03-firebatmedicrange.png", - "Concussive Shells (Marauder)": github_icon_base_url + "blizzard/btn-ability-terran-punishergrenade-color.png", - "Kinetic Foam (Marauder)": organics_icon_base_url + "KineticFoam.png", - "Stimpack (Marauder)": github_icon_base_url + "blizzard/btn-ability-terran-stimpack-color.png", - "Super Stimpack (Marauder)": github_icon_base_url + "blizzard/btn-upgrade-terran-superstimppack.png", - "Laser Targeting System (Marauder)": github_icon_base_url + "blizzard/btn-upgrade-terran-lazertargetingsystem.png", - "Magrail Munitions (Marauder)": github_icon_base_url + "blizzard/btn-upgrade-terran-magrailmunitions.png", - "Internal Tech Module (Marauder)": github_icon_base_url + "blizzard/btn-upgrade-terran-internalizedtechmodule.png", - "Juggernaut Plating (Marauder)": organics_icon_base_url + "JuggernautPlating.png", - "U-238 Rounds (Reaper)": organics_icon_base_url + "U-238Rounds.png", - "G-4 Clusterbomb (Reaper)": github_icon_base_url + "blizzard/btn-upgrade-terran-kd8chargeex3.png", - "Stimpack (Reaper)": github_icon_base_url + "blizzard/btn-ability-terran-stimpack-color.png", - "Super Stimpack (Reaper)": github_icon_base_url + "blizzard/btn-upgrade-terran-superstimppack.png", - "Laser Targeting System (Reaper)": github_icon_base_url + "blizzard/btn-upgrade-terran-lazertargetingsystem.png", - "Advanced Cloaking Field (Reaper)": github_icon_base_url + "original/btn-permacloak-reaper.png", - "Spider Mines (Reaper)": github_icon_base_url + "original/btn-ability-terran-spidermine.png", - "Combat Drugs (Reaper)": github_icon_base_url + "blizzard/btn-upgrade-terran-reapercombatdrugs.png", - "Jet Pack Overdrive (Reaper)": github_icon_base_url + "blizzard/btn-ability-hornerhan-reaper-flightmode.png", - "Ocular Implants (Ghost)": organics_icon_base_url + "OcularImplants.png", - "Crius Suit (Ghost)": github_icon_base_url + "original/btn-permacloak-ghost.png", - "EMP Rounds (Ghost)": github_icon_base_url + "blizzard/btn-ability-terran-emp-color.png", - "Lockdown (Ghost)": github_icon_base_url + "original/btn-abilty-terran-lockdown@scbw.png", - "Resource Efficiency (Ghost)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png", - "Psionic Lash (Spectre)": organics_icon_base_url + "PsionicLash.png", - "Nyx-Class Cloaking Module (Spectre)": github_icon_base_url + "original/btn-permacloak-spectre.png", - "Impaler Rounds (Spectre)": github_icon_base_url + "blizzard/btn-techupgrade-terran-impalerrounds.png", - "Resource Efficiency (Spectre)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png", - "Juggernaut Plating (HERC)": organics_icon_base_url + "JuggernautPlating.png", - "Kinetic Foam (HERC)": organics_icon_base_url + "KineticFoam.png", - "Resource Efficiency (HERC)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png", - - "Hellion": "https://static.wikia.nocookie.net/starcraft/images/5/56/Hellion_SC2_Icon1.jpg", - "Vulture": github_icon_base_url + "blizzard/btn-unit-terran-vulture.png", - "Goliath": github_icon_base_url + "blizzard/btn-unit-terran-goliath.png", - "Diamondback": github_icon_base_url + "blizzard/btn-unit-terran-cobra.png", - "Siege Tank": "https://static.wikia.nocookie.net/starcraft/images/5/57/SiegeTank_SC2_Icon1.jpg", - "Thor": "https://static.wikia.nocookie.net/starcraft/images/e/ef/Thor_SC2_Icon1.jpg", - "Predator": github_icon_base_url + "original/btn-unit-terran-predator.png", - "Widow Mine": github_icon_base_url + "blizzard/btn-unit-terran-widowmine.png", - "Cyclone": github_icon_base_url + "blizzard/btn-unit-terran-cyclone.png", - "Warhound": github_icon_base_url + "blizzard/btn-unit-terran-warhound.png", - - "Twin-Linked Flamethrower (Hellion)": github_icon_base_url + "blizzard/btn-upgrade-mengsk-trooper-flamethrower.png", - "Thermite Filaments (Hellion)": github_icon_base_url + "blizzard/btn-upgrade-terran-infernalpreigniter.png", - "Hellbat Aspect (Hellion)": github_icon_base_url + "blizzard/btn-unit-terran-hellionbattlemode.png", - "Smart Servos (Hellion)": github_icon_base_url + "blizzard/btn-upgrade-terran-transformationservos.png", - "Optimized Logistics (Hellion)": github_icon_base_url + "blizzard/btn-upgrade-terran-optimizedlogistics.png", - "Jump Jets (Hellion)": github_icon_base_url + "blizzard/btn-upgrade-terran-jumpjets.png", - "Stimpack (Hellion)": github_icon_base_url + "blizzard/btn-ability-terran-stimpack-color.png", - "Super Stimpack (Hellion)": github_icon_base_url + "blizzard/btn-upgrade-terran-superstimppack.png", - "Infernal Plating (Hellion)": github_icon_base_url + "blizzard/btn-upgrade-swann-hellarmor.png", - "Cerberus Mine (Spider Mine)": github_icon_base_url + "blizzard/btn-upgrade-raynor-cerberusmines.png", - "High Explosive Munition (Spider Mine)": github_icon_base_url + "original/btn-ability-terran-spidermine.png", - "Replenishable Magazine (Vulture)": github_icon_base_url + "blizzard/btn-upgrade-raynor-replenishablemagazine.png", - "Replenishable Magazine (Free) (Vulture)": github_icon_base_url + "blizzard/btn-upgrade-raynor-replenishablemagazine.png", - "Ion Thrusters (Vulture)": github_icon_base_url + "blizzard/btn-ability-terran-emergencythrusters.png", - "Auto Launchers (Vulture)": github_icon_base_url + "blizzard/btn-upgrade-terran-jotunboosters.png", - "Auto-Repair (Vulture)": github_icon_base_url + "blizzard/ui_tipicon_campaign_space01-repair.png", - "Multi-Lock Weapons System (Goliath)": github_icon_base_url + "blizzard/btn-upgrade-swann-multilockweaponsystem.png", - "Ares-Class Targeting System (Goliath)": github_icon_base_url + "blizzard/btn-upgrade-swann-aresclasstargetingsystem.png", - "Jump Jets (Goliath)": github_icon_base_url + "blizzard/btn-upgrade-terran-jumpjets.png", - "Optimized Logistics (Goliath)": github_icon_base_url + "blizzard/btn-upgrade-terran-optimizedlogistics.png", - "Shaped Hull (Goliath)": organics_icon_base_url + "ShapedHull.png", - "Resource Efficiency (Goliath)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png", - "Internal Tech Module (Goliath)": github_icon_base_url + "blizzard/btn-upgrade-terran-internalizedtechmodule.png", - "Tri-Lithium Power Cell (Diamondback)": github_icon_base_url + "original/btn-upgrade-terran-trilithium-power-cell.png", - "Tungsten Spikes (Diamondback)": github_icon_base_url + "original/btn-upgrade-terran-tungsten-spikes.png", - "Shaped Hull (Diamondback)": organics_icon_base_url + "ShapedHull.png", - "Hyperfluxor (Diamondback)": github_icon_base_url + "blizzard/btn-upgrade-mengsk-engineeringbay-orbitaldrop.png", - "Burst Capacitors (Diamondback)": github_icon_base_url + "blizzard/btn-ability-terran-electricfield.png", - "Ion Thrusters (Diamondback)": github_icon_base_url + "blizzard/btn-ability-terran-emergencythrusters.png", - "Resource Efficiency (Diamondback)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png", - "Maelstrom Rounds (Siege Tank)": github_icon_base_url + "blizzard/btn-upgrade-raynor-maelstromrounds.png", - "Shaped Blast (Siege Tank)": organics_icon_base_url + "ShapedBlast.png", - "Jump Jets (Siege Tank)": github_icon_base_url + "blizzard/btn-upgrade-terran-jumpjets.png", - "Spider Mines (Siege Tank)": github_icon_base_url + "blizzard/btn-upgrade-siegetank-spidermines.png", - "Smart Servos (Siege Tank)": github_icon_base_url + "blizzard/btn-upgrade-terran-transformationservos.png", - "Graduating Range (Siege Tank)": github_icon_base_url + "blizzard/btn-upgrade-terran-nova-siegetankrange.png", - "Laser Targeting System (Siege Tank)": github_icon_base_url + "blizzard/btn-upgrade-terran-lazertargetingsystem.png", - "Advanced Siege Tech (Siege Tank)": github_icon_base_url + "blizzard/btn-upgrade-raynor-improvedsiegemode.png", - "Internal Tech Module (Siege Tank)": github_icon_base_url + "blizzard/btn-upgrade-terran-internalizedtechmodule.png", - "Shaped Hull (Siege Tank)": organics_icon_base_url + "ShapedHull.png", - "Resource Efficiency (Siege Tank)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png", - "330mm Barrage Cannon (Thor)": github_icon_base_url + "original/btn-ability-thor-330mm.png", - "Immortality Protocol (Thor)": github_icon_base_url + "blizzard/btn-techupgrade-terran-immortalityprotocol.png", - "Immortality Protocol (Free) (Thor)": github_icon_base_url + "blizzard/btn-techupgrade-terran-immortalityprotocol.png", - "High Impact Payload (Thor)": github_icon_base_url + "blizzard/btn-unit-terran-thorsiegemode.png", - "Smart Servos (Thor)": github_icon_base_url + "blizzard/btn-upgrade-terran-transformationservos.png", - "Button With a Skull on It (Thor)": github_icon_base_url + "blizzard/btn-ability-terran-nuclearstrike-color.png", - "Laser Targeting System (Thor)": github_icon_base_url + "blizzard/btn-upgrade-terran-lazertargetingsystem.png", - "Large Scale Field Construction (Thor)": github_icon_base_url + "blizzard/talent-swann-level12-immortalityprotocol.png", - "Resource Efficiency (Predator)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png", - "Cloak (Predator)": github_icon_base_url + "blizzard/btn-ability-terran-cloak-color.png", - "Charge (Predator)": github_icon_base_url + "blizzard/btn-ability-protoss-charge-color.png", - "Predator's Fury (Predator)": github_icon_base_url + "blizzard/btn-ability-protoss-shadowfury.png", - "Drilling Claws (Widow Mine)": github_icon_base_url + "blizzard/btn-upgrade-terran-researchdrillingclaws.png", - "Concealment (Widow Mine)": github_icon_base_url + "blizzard/btn-ability-terran-widowminehidden.png", - "Black Market Launchers (Widow Mine)": github_icon_base_url + "blizzard/btn-ability-hornerhan-widowmine-attackrange.png", - "Executioner Missiles (Widow Mine)": github_icon_base_url + "blizzard/btn-ability-hornerhan-widowmine-deathblossom.png", - "Mag-Field Accelerators (Cyclone)": github_icon_base_url + "blizzard/btn-upgrade-terran-magfieldaccelerator.png", - "Mag-Field Launchers (Cyclone)": github_icon_base_url + "blizzard/btn-upgrade-terran-cyclonerangeupgrade.png", - "Targeting Optics (Cyclone)": github_icon_base_url + "blizzard/btn-upgrade-swann-targetingoptics.png", - "Rapid Fire Launchers (Cyclone)": github_icon_base_url + "blizzard/btn-upgrade-raynor-ripwavemissiles.png", - "Resource Efficiency (Cyclone)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png", - "Internal Tech Module (Cyclone)": github_icon_base_url + "blizzard/btn-upgrade-terran-internalizedtechmodule.png", - "Resource Efficiency (Warhound)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png", - "Reinforced Plating (Warhound)": github_icon_base_url + "original/btn-research-zerg-fortifiedbunker.png", - - "Medivac": "https://static.wikia.nocookie.net/starcraft/images/d/db/Medivac_SC2_Icon1.jpg", - "Wraith": github_icon_base_url + "blizzard/btn-unit-terran-wraith.png", - "Viking": "https://static.wikia.nocookie.net/starcraft/images/2/2a/Viking_SC2_Icon1.jpg", - "Banshee": "https://static.wikia.nocookie.net/starcraft/images/3/32/Banshee_SC2_Icon1.jpg", - "Battlecruiser": "https://static.wikia.nocookie.net/starcraft/images/f/f5/Battlecruiser_SC2_Icon1.jpg", - "Raven": "https://static.wikia.nocookie.net/starcraft/images/1/19/SC2_Lab_Raven_Icon.png", - "Science Vessel": "https://static.wikia.nocookie.net/starcraft/images/c/c3/SC2_Lab_SciVes_Icon.png", - "Hercules": "https://static.wikia.nocookie.net/starcraft/images/4/40/SC2_Lab_Hercules_Icon.png", - "Liberator": github_icon_base_url + "blizzard/btn-unit-terran-liberator.png", - "Valkyrie": github_icon_base_url + "original/btn-unit-terran-valkyrie@scbw.png", - - "Rapid Deployment Tube (Medivac)": organics_icon_base_url + "RapidDeploymentTube.png", - "Advanced Healing AI (Medivac)": github_icon_base_url + "blizzard/btn-ability-mengsk-medivac-doublehealbeam.png", - "Expanded Hull (Medivac)": github_icon_base_url + "blizzard/btn-upgrade-mengsk-engineeringbay-neosteelfortifiedarmor.png", - "Afterburners (Medivac)": github_icon_base_url + "blizzard/btn-upgrade-terran-medivacemergencythrusters.png", - "Scatter Veil (Medivac)": github_icon_base_url + "blizzard/btn-upgrade-swann-defensivematrix.png", - "Advanced Cloaking Field (Medivac)": github_icon_base_url + "original/btn-permacloak-medivac.png", - "Tomahawk Power Cells (Wraith)": organics_icon_base_url + "TomahawkPowerCells.png", - "Unregistered Cloaking Module (Wraith)": github_icon_base_url + "original/btn-permacloak-wraith.png", - "Trigger Override (Wraith)": github_icon_base_url + "blizzard/btn-ability-hornerhan-wraith-attackspeed.png", - "Internal Tech Module (Wraith)": github_icon_base_url + "blizzard/btn-upgrade-terran-internalizedtechmodule.png", - "Resource Efficiency (Wraith)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png", - "Displacement Field (Wraith)": github_icon_base_url + "blizzard/btn-upgrade-swann-displacementfield.png", - "Advanced Laser Technology (Wraith)": github_icon_base_url + "blizzard/btn-upgrade-swann-improvedburstlaser.png", - "Ripwave Missiles (Viking)": github_icon_base_url + "blizzard/btn-upgrade-raynor-ripwavemissiles.png", - "Phobos-Class Weapons System (Viking)": github_icon_base_url + "blizzard/btn-upgrade-raynor-phobosclassweaponssystem.png", - "Smart Servos (Viking)": github_icon_base_url + "blizzard/btn-upgrade-terran-transformationservos.png", - "Anti-Mechanical Munition (Viking)": github_icon_base_url + "blizzard/btn-ability-terran-ignorearmor.png", - "Shredder Rounds (Viking)": github_icon_base_url + "blizzard/btn-ability-hornerhan-viking-piercingattacks.png", - "W.I.L.D. Missiles (Viking)": github_icon_base_url + "blizzard/btn-ability-hornerhan-viking-missileupgrade.png", - "Cross-Spectrum Dampeners (Banshee)": github_icon_base_url + "original/btn-banshee-cross-spectrum-dampeners.png", - "Advanced Cross-Spectrum Dampeners (Banshee)": github_icon_base_url + "original/btn-permacloak-banshee.png", - "Shockwave Missile Battery (Banshee)": github_icon_base_url + "blizzard/btn-upgrade-raynor-shockwavemissilebattery.png", - "Hyperflight Rotors (Banshee)": github_icon_base_url + "blizzard/btn-upgrade-terran-hyperflightrotors.png", - "Laser Targeting System (Banshee)": github_icon_base_url + "blizzard/btn-upgrade-terran-lazertargetingsystem.png", - "Internal Tech Module (Banshee)": github_icon_base_url + "blizzard/btn-upgrade-terran-internalizedtechmodule.png", - "Shaped Hull (Banshee)": organics_icon_base_url + "ShapedHull.png", - "Advanced Targeting Optics (Banshee)": github_icon_base_url + "blizzard/btn-ability-terran-detectionconedebuff.png", - "Distortion Blasters (Banshee)": github_icon_base_url + "blizzard/btn-techupgrade-terran-cloakdistortionfield.png", - "Rocket Barrage (Banshee)": github_icon_base_url + "blizzard/btn-upgrade-terran-nova-bansheemissilestrik.png", - "Missile Pods (Battlecruiser) Level 1": organics_icon_base_url + "MissilePods.png", - "Missile Pods (Battlecruiser) Level 2": github_icon_base_url + "blizzard/btn-upgrade-terran-nova-bansheemissilestrik.png", - "Defensive Matrix (Battlecruiser)": github_icon_base_url + "blizzard/btn-upgrade-swann-defensivematrix.png", - "Advanced Defensive Matrix (Battlecruiser)": github_icon_base_url + "blizzard/btn-upgrade-swann-defensivematrix.png", - "Tactical Jump (Battlecruiser)": github_icon_base_url + "blizzard/btn-ability-terran-warpjump.png", - "Cloak (Battlecruiser)": github_icon_base_url + "blizzard/btn-ability-terran-cloak-color.png", - "ATX Laser Battery (Battlecruiser)": github_icon_base_url + "blizzard/btn-upgrade-terran-nova-specialordance.png", - "Optimized Logistics (Battlecruiser)": github_icon_base_url + "blizzard/btn-upgrade-terran-optimizedlogistics.png", - "Internal Tech Module (Battlecruiser)": github_icon_base_url + "blizzard/btn-upgrade-terran-internalizedtechmodule.png", - "Behemoth Plating (Battlecruiser)": github_icon_base_url + "original/btn-research-zerg-fortifiedbunker.png", - "Covert Ops Engines (Battlecruiser)": github_icon_base_url + "blizzard/btn-ability-terran-emergencythrusters.png", - "Bio Mechanical Repair Drone (Raven)": github_icon_base_url + "blizzard/btn-unit-biomechanicaldrone.png", - "Spider Mines (Raven)": github_icon_base_url + "blizzard/btn-upgrade-siegetank-spidermines.png", - "Railgun Turret (Raven)": github_icon_base_url + "blizzard/btn-unit-terran-autoturretblackops.png", - "Hunter-Seeker Weapon (Raven)": github_icon_base_url + "blizzard/btn-upgrade-terran-nova-specialordance.png", - "Interference Matrix (Raven)": github_icon_base_url + "blizzard/btn-upgrade-terran-interferencematrix.png", - "Anti-Armor Missile (Raven)": github_icon_base_url + "blizzard/btn-ability-terran-shreddermissile-color.png", - "Internal Tech Module (Raven)": github_icon_base_url + "blizzard/btn-upgrade-terran-internalizedtechmodule.png", - "Resource Efficiency (Raven)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png", - "Durable Materials (Raven)": github_icon_base_url + "blizzard/btn-upgrade-terran-durablematerials.png", - "EMP Shockwave (Science Vessel)": github_icon_base_url + "blizzard/btn-ability-mengsk-ghost-staticempblast.png", - "Defensive Matrix (Science Vessel)": github_icon_base_url + "blizzard/btn-upgrade-swann-defensivematrix.png", - "Improved Nano-Repair (Science Vessel)": github_icon_base_url + "blizzard/btn-upgrade-swann-improvednanorepair.png", - "Advanced AI Systems (Science Vessel)": github_icon_base_url + "blizzard/btn-ability-mengsk-medivac-doublehealbeam.png", - "Internal Fusion Module (Hercules)": github_icon_base_url + "blizzard/btn-upgrade-terran-internalizedtechmodule.png", - "Tactical Jump (Hercules)": github_icon_base_url + "blizzard/btn-ability-terran-hercules-tacticaljump.png", - "Advanced Ballistics (Liberator)": github_icon_base_url + "blizzard/btn-upgrade-terran-advanceballistics.png", - "Raid Artillery (Liberator)": github_icon_base_url + "blizzard/btn-upgrade-terran-nova-terrandefendermodestructureattack.png", - "Cloak (Liberator)": github_icon_base_url + "blizzard/btn-ability-terran-cloak-color.png", - "Laser Targeting System (Liberator)": github_icon_base_url + "blizzard/btn-upgrade-terran-lazertargetingsystem.png", - "Optimized Logistics (Liberator)": github_icon_base_url + "blizzard/btn-upgrade-terran-optimizedlogistics.png", - "Smart Servos (Liberator)": github_icon_base_url + "blizzard/btn-upgrade-terran-transformationservos.png", - "Resource Efficiency (Liberator)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png", - "Enhanced Cluster Launchers (Valkyrie)": github_icon_base_url + "blizzard/btn-ability-stetmann-corruptormissilebarrage.png", - "Shaped Hull (Valkyrie)": organics_icon_base_url + "ShapedHull.png", - "Flechette Missiles (Valkyrie)": github_icon_base_url + "blizzard/btn-ability-hornerhan-viking-missileupgrade.png", - "Afterburners (Valkyrie)": github_icon_base_url + "blizzard/btn-upgrade-terran-medivacemergencythrusters.png", - "Launching Vector Compensator (Valkyrie)": github_icon_base_url + "blizzard/btn-ability-terran-emergencythrusters.png", - "Resource Efficiency (Valkyrie)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png", - - "War Pigs": "https://static.wikia.nocookie.net/starcraft/images/e/ed/WarPigs_SC2_Icon1.jpg", - "Devil Dogs": "https://static.wikia.nocookie.net/starcraft/images/3/33/DevilDogs_SC2_Icon1.jpg", - "Hammer Securities": "https://static.wikia.nocookie.net/starcraft/images/3/3b/HammerSecurity_SC2_Icon1.jpg", - "Spartan Company": "https://static.wikia.nocookie.net/starcraft/images/b/be/SpartanCompany_SC2_Icon1.jpg", - "Siege Breakers": "https://static.wikia.nocookie.net/starcraft/images/3/31/SiegeBreakers_SC2_Icon1.jpg", - "Hel's Angels": "https://static.wikia.nocookie.net/starcraft/images/6/63/HelsAngels_SC2_Icon1.jpg", - "Dusk Wings": "https://static.wikia.nocookie.net/starcraft/images/5/52/DuskWings_SC2_Icon1.jpg", - "Jackson's Revenge": "https://static.wikia.nocookie.net/starcraft/images/9/95/JacksonsRevenge_SC2_Icon1.jpg", - "Skibi's Angels": github_icon_base_url + "blizzard/btn-unit-terran-medicelite.png", - "Death Heads": github_icon_base_url + "blizzard/btn-unit-terran-deathhead.png", - "Winged Nightmares": github_icon_base_url + "blizzard/btn-unit-collection-wraith-junker.png", - "Midnight Riders": github_icon_base_url + "blizzard/btn-unit-terran-liberatorblackops.png", - "Brynhilds": github_icon_base_url + "blizzard/btn-unit-collection-vikingfighter-covertops.png", - "Jotun": github_icon_base_url + "blizzard/btn-unit-terran-thormengsk.png", - - "Ultra-Capacitors": "https://static.wikia.nocookie.net/starcraft/images/2/23/SC2_Lab_Ultra_Capacitors_Icon.png", - "Vanadium Plating": "https://static.wikia.nocookie.net/starcraft/images/6/67/SC2_Lab_VanPlating_Icon.png", - "Orbital Depots": "https://static.wikia.nocookie.net/starcraft/images/0/01/SC2_Lab_Orbital_Depot_Icon.png", - "Micro-Filtering": "https://static.wikia.nocookie.net/starcraft/images/2/20/SC2_Lab_MicroFilter_Icon.png", - "Automated Refinery": "https://static.wikia.nocookie.net/starcraft/images/7/71/SC2_Lab_Auto_Refinery_Icon.png", - "Command Center Reactor": "https://static.wikia.nocookie.net/starcraft/images/e/ef/SC2_Lab_CC_Reactor_Icon.png", - "Tech Reactor": "https://static.wikia.nocookie.net/starcraft/images/c/c5/SC2_Lab_Tech_Reactor_Icon.png", - "Orbital Strike": "https://static.wikia.nocookie.net/starcraft/images/d/df/SC2_Lab_Orb_Strike_Icon.png", - - "Shrike Turret (Bunker)": "https://static.wikia.nocookie.net/starcraft/images/4/44/SC2_Lab_Shrike_Turret_Icon.png", - "Fortified Bunker (Bunker)": "https://static.wikia.nocookie.net/starcraft/images/4/4f/SC2_Lab_FortBunker_Icon.png", - "Planetary Fortress": "https://static.wikia.nocookie.net/starcraft/images/0/0b/SC2_Lab_PlanetFortress_Icon.png", - "Perdition Turret": "https://static.wikia.nocookie.net/starcraft/images/a/af/SC2_Lab_PerdTurret_Icon.png", - "Cellular Reactor": "https://static.wikia.nocookie.net/starcraft/images/d/d8/SC2_Lab_CellReactor_Icon.png", - "Regenerative Bio-Steel Level 1": github_icon_base_url + "original/btn-regenerativebiosteel-green.png", - "Regenerative Bio-Steel Level 2": github_icon_base_url + "original/btn-regenerativebiosteel-blue.png", - "Regenerative Bio-Steel Level 3": github_icon_base_url + "blizzard/btn-research-zerg-regenerativebio-steel.png", - "Hive Mind Emulator": "https://static.wikia.nocookie.net/starcraft/images/b/bc/SC2_Lab_Hive_Emulator_Icon.png", - "Psi Disrupter": "https://static.wikia.nocookie.net/starcraft/images/c/cf/SC2_Lab_Psi_Disruptor_Icon.png", - - "Structure Armor": github_icon_base_url + "blizzard/btn-upgrade-terran-buildingarmor.png", - "Hi-Sec Auto Tracking": github_icon_base_url + "blizzard/btn-upgrade-terran-hisecautotracking.png", - "Advanced Optics": github_icon_base_url + "blizzard/btn-upgrade-swann-vehiclerangeincrease.png", - "Rogue Forces": github_icon_base_url + "blizzard/btn-unit-terran-tosh.png", - - "Ghost Visor (Nova Equipment)": github_icon_base_url + "blizzard/btn-upgrade-nova-equipment-ghostvisor.png", - "Rangefinder Oculus (Nova Equipment)": github_icon_base_url + "blizzard/btn-upgrade-nova-equipment-rangefinderoculus.png", - "Domination (Nova Ability)": github_icon_base_url + "blizzard/btn-ability-nova-domination.png", - "Blink (Nova Ability)": github_icon_base_url + "blizzard/btn-upgrade-nova-blink.png", - "Stealth Suit Module (Nova Suit Module)": github_icon_base_url + "blizzard/btn-upgrade-nova-equipment-stealthsuit.png", - "Cloak (Nova Suit Module)": github_icon_base_url + "blizzard/btn-ability-terran-cloak-color.png", - "Permanently Cloaked (Nova Suit Module)": github_icon_base_url + "blizzard/btn-upgrade-nova-tacticalstealthsuit.png", - "Energy Suit Module (Nova Suit Module)": github_icon_base_url + "blizzard/btn-upgrade-nova-equipment-apolloinfantrysuit.png", - "Armored Suit Module (Nova Suit Module)": github_icon_base_url + "blizzard/btn-upgrade-nova-equipment-blinksuit.png", - "Jump Suit Module (Nova Suit Module)": github_icon_base_url + "blizzard/btn-upgrade-nova-jetpack.png", - "C20A Canister Rifle (Nova Weapon)": github_icon_base_url + "blizzard/btn-upgrade-nova-equipment-canisterrifle.png", - "Hellfire Shotgun (Nova Weapon)": github_icon_base_url + "blizzard/btn-upgrade-nova-equipment-shotgun.png", - "Plasma Rifle (Nova Weapon)": github_icon_base_url + "blizzard/btn-upgrade-nova-equipment-plasmagun.png", - "Monomolecular Blade (Nova Weapon)": github_icon_base_url + "blizzard/btn-upgrade-nova-equipment-monomolecularblade.png", - "Blazefire Gunblade (Nova Weapon)": github_icon_base_url + "blizzard/btn-upgrade-nova-equipment-gunblade_sword.png", - "Stim Infusion (Nova Gadget)": github_icon_base_url + "blizzard/btn-upgrade-terran-superstimppack.png", - "Pulse Grenades (Nova Gadget)": github_icon_base_url + "blizzard/btn-upgrade-nova-btn-upgrade-nova-pulsegrenade.png", - "Flashbang Grenades (Nova Gadget)": github_icon_base_url + "blizzard/btn-upgrade-nova-btn-upgrade-nova-flashgrenade.png", - "Ionic Force Field (Nova Gadget)": github_icon_base_url + "blizzard/btn-upgrade-terran-nova-personaldefensivematrix.png", - "Holo Decoy (Nova Gadget)": github_icon_base_url + "blizzard/btn-upgrade-nova-holographicdecoy.png", - "Tac Nuke Strike (Nova Ability)": github_icon_base_url + "blizzard/btn-ability-terran-nuclearstrike-color.png", - - "Zerg Melee Attack Level 1": github_icon_base_url + "blizzard/btn-upgrade-zerg-meleeattacks-level1.png", - "Zerg Melee Attack Level 2": github_icon_base_url + "blizzard/btn-upgrade-zerg-meleeattacks-level2.png", - "Zerg Melee Attack Level 3": github_icon_base_url + "blizzard/btn-upgrade-zerg-meleeattacks-level3.png", - "Zerg Missile Attack Level 1": github_icon_base_url + "blizzard/btn-upgrade-zerg-missileattacks-level1.png", - "Zerg Missile Attack Level 2": github_icon_base_url + "blizzard/btn-upgrade-zerg-missileattacks-level2.png", - "Zerg Missile Attack Level 3": github_icon_base_url + "blizzard/btn-upgrade-zerg-missileattacks-level3.png", - "Zerg Ground Carapace Level 1": github_icon_base_url + "blizzard/btn-upgrade-zerg-groundcarapace-level1.png", - "Zerg Ground Carapace Level 2": github_icon_base_url + "blizzard/btn-upgrade-zerg-groundcarapace-level2.png", - "Zerg Ground Carapace Level 3": github_icon_base_url + "blizzard/btn-upgrade-zerg-groundcarapace-level3.png", - "Zerg Flyer Attack Level 1": github_icon_base_url + "blizzard/btn-upgrade-zerg-airattacks-level1.png", - "Zerg Flyer Attack Level 2": github_icon_base_url + "blizzard/btn-upgrade-zerg-airattacks-level2.png", - "Zerg Flyer Attack Level 3": github_icon_base_url + "blizzard/btn-upgrade-zerg-airattacks-level3.png", - "Zerg Flyer Carapace Level 1": github_icon_base_url + "blizzard/btn-upgrade-zerg-flyercarapace-level1.png", - "Zerg Flyer Carapace Level 2": github_icon_base_url + "blizzard/btn-upgrade-zerg-flyercarapace-level2.png", - "Zerg Flyer Carapace Level 3": github_icon_base_url + "blizzard/btn-upgrade-zerg-flyercarapace-level3.png", - - "Automated Extractors (Kerrigan Tier 3)": github_icon_base_url + "blizzard/btn-ability-kerrigan-automatedextractors.png", - "Vespene Efficiency (Kerrigan Tier 5)": github_icon_base_url + "blizzard/btn-ability-kerrigan-vespeneefficiency.png", - "Twin Drones (Kerrigan Tier 5)": github_icon_base_url + "blizzard/btn-ability-kerrigan-twindrones.png", - "Improved Overlords (Kerrigan Tier 3)": github_icon_base_url + "blizzard/btn-ability-kerrigan-improvedoverlords.png", - "Ventral Sacs (Overlord)": github_icon_base_url + "blizzard/btn-upgrade-zerg-ventralsacs.png", - "Malignant Creep (Kerrigan Tier 5)": github_icon_base_url + "blizzard/btn-ability-kerrigan-malignantcreep.png", - - "Spine Crawler": github_icon_base_url + "blizzard/btn-building-zerg-spinecrawler.png", - "Spore Crawler": github_icon_base_url + "blizzard/btn-building-zerg-sporecrawler.png", - - "Zergling": github_icon_base_url + "blizzard/btn-unit-zerg-zergling.png", - "Swarm Queen": github_icon_base_url + "blizzard/btn-unit-zerg-broodqueen.png", - "Roach": github_icon_base_url + "blizzard/btn-unit-zerg-roach.png", - "Hydralisk": github_icon_base_url + "blizzard/btn-unit-zerg-hydralisk.png", - "Aberration": github_icon_base_url + "blizzard/btn-unit-zerg-aberration.png", - "Mutalisk": github_icon_base_url + "blizzard/btn-unit-zerg-mutalisk.png", - "Corruptor": github_icon_base_url + "blizzard/btn-unit-zerg-corruptor.png", - "Swarm Host": github_icon_base_url + "blizzard/btn-unit-zerg-swarmhost.png", - "Infestor": github_icon_base_url + "blizzard/btn-unit-zerg-infestor.png", - "Defiler": github_icon_base_url + "original/btn-unit-zerg-defiler@scbw.png", - "Ultralisk": github_icon_base_url + "blizzard/btn-unit-zerg-ultralisk.png", - "Brood Queen": github_icon_base_url + "blizzard/btn-unit-zerg-classicqueen.png", - "Scourge": github_icon_base_url + "blizzard/btn-unit-zerg-scourge.png", - - "Baneling Aspect (Zergling)": github_icon_base_url + "blizzard/btn-unit-zerg-baneling.png", - "Ravager Aspect (Roach)": github_icon_base_url + "blizzard/btn-unit-zerg-ravager.png", - "Impaler Aspect (Hydralisk)": github_icon_base_url + "blizzard/btn-unit-zerg-impaler.png", - "Lurker Aspect (Hydralisk)": github_icon_base_url + "blizzard/btn-unit-zerg-lurker.png", - "Brood Lord Aspect (Mutalisk/Corruptor)": github_icon_base_url + "blizzard/btn-unit-zerg-broodlord.png", - "Viper Aspect (Mutalisk/Corruptor)": github_icon_base_url + "blizzard/btn-unit-zerg-viper.png", - "Guardian Aspect (Mutalisk/Corruptor)": github_icon_base_url + "blizzard/btn-unit-zerg-primalguardian.png", - "Devourer Aspect (Mutalisk/Corruptor)": github_icon_base_url + "blizzard/btn-unit-zerg-devourerex3.png", - - "Raptor Strain (Zergling)": github_icon_base_url + "blizzard/btn-unit-zerg-zergling-raptor.png", - "Swarmling Strain (Zergling)": github_icon_base_url + "blizzard/btn-unit-zerg-zergling-swarmling.png", - "Hardened Carapace (Zergling)": github_icon_base_url + "blizzard/btn-upgrade-zerg-hardenedcarapace.png", - "Adrenal Overload (Zergling)": github_icon_base_url + "blizzard/btn-upgrade-zerg-adrenaloverload.png", - "Metabolic Boost (Zergling)": github_icon_base_url + "blizzard/btn-upgrade-zerg-hotsmetabolicboost.png", - "Shredding Claws (Zergling)": github_icon_base_url + "blizzard/btn-upgrade-zergling-armorshredding.png", - "Zergling Reconstitution (Kerrigan Tier 3)": github_icon_base_url + "blizzard/btn-ability-kerrigan-zerglingreconstitution.png", - "Splitter Strain (Baneling)": github_icon_base_url + "blizzard/talent-zagara-level14-unlocksplitterling.png", - "Hunter Strain (Baneling)": github_icon_base_url + "blizzard/btn-ability-zerg-cliffjump-baneling.png", - "Corrosive Acid (Baneling)": github_icon_base_url + "blizzard/btn-upgrade-zerg-corrosiveacid.png", - "Rupture (Baneling)": github_icon_base_url + "blizzard/btn-upgrade-zerg-rupture.png", - "Regenerative Acid (Baneling)": github_icon_base_url + "blizzard/btn-upgrade-zerg-regenerativebile.png", - "Centrifugal Hooks (Baneling)": github_icon_base_url + "blizzard/btn-upgrade-zerg-centrifugalhooks.png", - "Tunneling Jaws (Baneling)": github_icon_base_url + "blizzard/btn-upgrade-zerg-tunnelingjaws.png", - "Rapid Metamorph (Baneling)": github_icon_base_url + "blizzard/btn-upgrade-terran-optimizedlogistics.png", - "Spawn Larvae (Swarm Queen)": github_icon_base_url + "blizzard/btn-unit-zerg-larva.png", - "Deep Tunnel (Swarm Queen)": github_icon_base_url + "blizzard/btn-ability-zerg-deeptunnel.png", - "Organic Carapace (Swarm Queen)": github_icon_base_url + "blizzard/btn-upgrade-zerg-organiccarapace.png", - "Bio-Mechanical Transfusion (Swarm Queen)": github_icon_base_url + "blizzard/btn-upgrade-zerg-abathur-biomechanicaltransfusion.png", - "Resource Efficiency (Swarm Queen)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png", - "Incubator Chamber (Swarm Queen)": github_icon_base_url + "blizzard/btn-upgrade-zerg-abathur-incubationchamber.png", - "Vile Strain (Roach)": github_icon_base_url + "blizzard/btn-unit-zerg-roach-vile.png", - "Corpser Strain (Roach)": github_icon_base_url + "blizzard/btn-unit-zerg-roach-corpser.png", - "Hydriodic Bile (Roach)": github_icon_base_url + "blizzard/btn-upgrade-zerg-hydriaticacid.png", - "Adaptive Plating (Roach)": github_icon_base_url + "blizzard/btn-upgrade-zerg-adaptivecarapace.png", - "Tunneling Claws (Roach)": github_icon_base_url + "blizzard/btn-upgrade-zerg-hotstunnelingclaws.png", - "Glial Reconstitution (Roach)": github_icon_base_url + "blizzard/btn-upgrade-zerg-glialreconstitution.png", - "Organic Carapace (Roach)": github_icon_base_url + "blizzard/btn-upgrade-zerg-organiccarapace.png", - "Potent Bile (Ravager)": github_icon_base_url + "blizzard/potentbile_coop.png", - "Bloated Bile Ducts (Ravager)": github_icon_base_url + "blizzard/btn-ability-zerg-abathur-corrosivebilelarge.png", - "Deep Tunnel (Ravager)": github_icon_base_url + "blizzard/btn-ability-zerg-deeptunnel.png", - "Frenzy (Hydralisk)": github_icon_base_url + "blizzard/btn-upgrade-zerg-frenzy.png", - "Ancillary Carapace (Hydralisk)": github_icon_base_url + "blizzard/btn-upgrade-zerg-ancillaryarmor.png", - "Grooved Spines (Hydralisk)": github_icon_base_url + "blizzard/btn-upgrade-zerg-hotsgroovedspines.png", - "Muscular Augments (Hydralisk)": github_icon_base_url + "blizzard/btn-upgrade-zerg-evolvemuscularaugments.png", - "Resource Efficiency (Hydralisk)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png", - "Adaptive Talons (Impaler)": github_icon_base_url + "blizzard/btn-upgrade-zerg-adaptivetalons.png", - "Secretion Glands (Impaler)": github_icon_base_url + "blizzard/btn-ability-zerg-creepspread.png", - "Hardened Tentacle Spines (Impaler)": github_icon_base_url + "blizzard/btn-ability-zerg-dehaka-impaler-tenderize.png", - "Seismic Spines (Lurker)": github_icon_base_url + "blizzard/btn-upgrade-kerrigan-seismicspines.png", - "Adapted Spines (Lurker)": github_icon_base_url + "blizzard/btn-upgrade-zerg-groovedspines.png", - "Vicious Glaive (Mutalisk)": github_icon_base_url + "blizzard/btn-upgrade-zerg-viciousglaive.png", - "Rapid Regeneration (Mutalisk)": github_icon_base_url + "blizzard/btn-upgrade-zerg-rapidregeneration.png", - "Sundering Glaive (Mutalisk)": github_icon_base_url + "blizzard/btn-upgrade-zerg-explosiveglaive.png", - "Severing Glaive (Mutalisk)": github_icon_base_url + "blizzard/btn-upgrade-zerg-explosiveglaive.png", - "Aerodynamic Glaive Shape (Mutalisk)": github_icon_base_url + "blizzard/btn-ability-dehaka-airbonusdamage.png", - "Corruption (Corruptor)": github_icon_base_url + "blizzard/btn-ability-zerg-causticspray.png", - "Caustic Spray (Corruptor)": github_icon_base_url + "blizzard/btn-ability-zerg-corruption-color.png", - "Porous Cartilage (Brood Lord)": github_icon_base_url + "blizzard/btn-upgrade-kerrigan-broodlordspeed.png", - "Evolved Carapace (Brood Lord)": github_icon_base_url + "blizzard/btn-upgrade-zerg-chitinousplating.png", - "Splitter Mitosis (Brood Lord)": github_icon_base_url + "blizzard/abilityicon_spawnbroodlings_square.png", - "Resource Efficiency (Brood Lord)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png", - "Parasitic Bomb (Viper)": github_icon_base_url + "blizzard/btn-ability-zerg-parasiticbomb.png", - "Paralytic Barbs (Viper)": github_icon_base_url + "blizzard/btn-upgrade-zerg-abathur-abduct.png", - "Virulent Microbes (Viper)": github_icon_base_url + "blizzard/btn-upgrade-zerg-abathur-castrange.png", - "Prolonged Dispersion (Guardian)": github_icon_base_url + "blizzard/btn-upgrade-zerg-abathur-prolongeddispersion.png", - "Primal Adaptation (Guardian)": github_icon_base_url + "blizzard/biomassrecovery_coop.png", - "Soronan Acid (Guardian)": github_icon_base_url + "blizzard/btn-upgrade-zerg-abathur-biomass.png", - "Corrosive Spray (Devourer)": github_icon_base_url + "blizzard/btn-upgrade-zerg-abathur-devourer-corrosivespray.png", - "Gaping Maw (Devourer)": github_icon_base_url + "blizzard/btn-ability-zerg-explode-color.png", - "Improved Osmosis (Devourer)": github_icon_base_url + "blizzard/btn-upgrade-zerg-pneumatizedcarapace.png", - "Prescient Spores (Devourer)": github_icon_base_url + "blizzard/btn-upgrade-zerg-airattacks-level2.png", - "Carrion Strain (Swarm Host)": github_icon_base_url + "blizzard/btn-unit-zerg-swarmhost-carrion.png", - "Creeper Strain (Swarm Host)": github_icon_base_url + "blizzard/btn-unit-zerg-swarmhost-creeper.png", - "Burrow (Swarm Host)": github_icon_base_url + "blizzard/btn-ability-zerg-burrow-color.png", - "Rapid Incubation (Swarm Host)": github_icon_base_url + "blizzard/btn-upgrade-zerg-rapidincubation.png", - "Pressurized Glands (Swarm Host)": github_icon_base_url + "blizzard/btn-upgrade-zerg-pressurizedglands.png", - "Locust Metabolic Boost (Swarm Host)": github_icon_base_url + "blizzard/btn-upgrade-zerg-glialreconstitution.png", - "Enduring Locusts (Swarm Host)": github_icon_base_url + "blizzard/btn-upgrade-zerg-evolveincreasedlocustlifetime.png", - "Organic Carapace (Swarm Host)": github_icon_base_url + "blizzard/btn-upgrade-zerg-organiccarapace.png", - "Resource Efficiency (Swarm Host)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png", - "Infested Terran (Infestor)": github_icon_base_url + "blizzard/btn-unit-zerg-infestedmarine.png", - "Microbial Shroud (Infestor)": github_icon_base_url + "blizzard/btn-ability-zerg-darkswarm.png", - "Noxious Strain (Ultralisk)": github_icon_base_url + "blizzard/btn-unit-zerg-ultralisk-noxious.png", - "Torrasque Strain (Ultralisk)": github_icon_base_url + "blizzard/btn-unit-zerg-ultralisk-torrasque.png", - "Burrow Charge (Ultralisk)": github_icon_base_url + "blizzard/btn-upgrade-zerg-burrowcharge.png", - "Tissue Assimilation (Ultralisk)": github_icon_base_url + "blizzard/btn-upgrade-zerg-tissueassimilation.png", - "Monarch Blades (Ultralisk)": github_icon_base_url + "blizzard/btn-upgrade-zerg-monarchblades.png", - "Anabolic Synthesis (Ultralisk)": github_icon_base_url + "blizzard/btn-upgrade-zerg-anabolicsynthesis.png", - "Chitinous Plating (Ultralisk)": github_icon_base_url + "blizzard/btn-upgrade-zerg-chitinousplating.png", - "Organic Carapace (Ultralisk)": github_icon_base_url + "blizzard/btn-upgrade-zerg-organiccarapace.png", - "Resource Efficiency (Ultralisk)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png", - "Fungal Growth (Brood Queen)": github_icon_base_url + "blizzard/btn-upgrade-zerg-stukov-researchqueenfungalgrowth.png", - "Ensnare (Brood Queen)": github_icon_base_url + "blizzard/btn-ability-zerg-fungalgrowth-color.png", - "Enhanced Mitochondria (Brood Queen)": github_icon_base_url + "blizzard/btn-upgrade-zerg-stukov-queenenergyregen.png", - "Virulent Spores (Scourge)": github_icon_base_url + "blizzard/btn-upgrade-zagara-scourgesplashdamage.png", - "Resource Efficiency (Scourge)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png", - "Swarm Scourge (Scourge)": github_icon_base_url + "original/btn-upgrade-custom-triple-scourge.png", - - "Infested Medics": github_icon_base_url + "blizzard/btn-unit-terran-medicelite.png", - "Infested Siege Tanks": github_icon_base_url + "original/btn-unit-terran-siegetankmercenary-tank.png", - "Infested Banshees": github_icon_base_url + "original/btn-unit-terran-bansheemercenary.png", - - "Primal Form (Kerrigan)": github_icon_base_url + "blizzard/btn-unit-zerg-kerriganinfested.png", - "Kinetic Blast (Kerrigan Tier 1)": github_icon_base_url + "blizzard/btn-ability-kerrigan-kineticblast.png", - "Heroic Fortitude (Kerrigan Tier 1)": github_icon_base_url + "blizzard/btn-ability-kerrigan-heroicfortitude.png", - "Leaping Strike (Kerrigan Tier 1)": github_icon_base_url + "blizzard/btn-ability-kerrigan-leapingstrike.png", - "Crushing Grip (Kerrigan Tier 2)": github_icon_base_url + "blizzard/btn-ability-swarm-kerrigan-crushinggrip.png", - "Chain Reaction (Kerrigan Tier 2)": github_icon_base_url + "blizzard/btn-ability-swarm-kerrigan-chainreaction.png", - "Psionic Shift (Kerrigan Tier 2)": github_icon_base_url + "blizzard/btn-ability-kerrigan-psychicshift.png", - "Wild Mutation (Kerrigan Tier 4)": github_icon_base_url + "blizzard/btn-ability-kerrigan-wildmutation.png", - "Spawn Banelings (Kerrigan Tier 4)": github_icon_base_url + "blizzard/abilityicon_spawnbanelings_square.png", - "Mend (Kerrigan Tier 4)": github_icon_base_url + "blizzard/btn-ability-zerg-transfusion-color.png", - "Infest Broodlings (Kerrigan Tier 6)": github_icon_base_url + "blizzard/abilityicon_spawnbroodlings_square.png", - "Fury (Kerrigan Tier 6)": github_icon_base_url + "blizzard/btn-ability-kerrigan-fury.png", - "Ability Efficiency (Kerrigan Tier 6)": github_icon_base_url + "blizzard/btn-ability-kerrigan-abilityefficiency.png", - "Apocalypse (Kerrigan Tier 7)": github_icon_base_url + "blizzard/btn-ability-kerrigan-apocalypse.png", - "Spawn Leviathan (Kerrigan Tier 7)": github_icon_base_url + "blizzard/btn-unit-zerg-leviathan.png", - "Drop-Pods (Kerrigan Tier 7)": github_icon_base_url + "blizzard/btn-ability-kerrigan-droppods.png", - - "Protoss Ground Weapon Level 1": github_icon_base_url + "blizzard/btn-upgrade-protoss-groundweaponslevel1.png", - "Protoss Ground Weapon Level 2": github_icon_base_url + "blizzard/btn-upgrade-protoss-groundweaponslevel2.png", - "Protoss Ground Weapon Level 3": github_icon_base_url + "blizzard/btn-upgrade-protoss-groundweaponslevel3.png", - "Protoss Ground Armor Level 1": github_icon_base_url + "blizzard/btn-upgrade-protoss-groundarmorlevel1.png", - "Protoss Ground Armor Level 2": github_icon_base_url + "blizzard/btn-upgrade-protoss-groundarmorlevel2.png", - "Protoss Ground Armor Level 3": github_icon_base_url + "blizzard/btn-upgrade-protoss-groundarmorlevel3.png", - "Protoss Shields Level 1": github_icon_base_url + "blizzard/btn-upgrade-protoss-shieldslevel1.png", - "Protoss Shields Level 2": github_icon_base_url + "blizzard/btn-upgrade-protoss-shieldslevel2.png", - "Protoss Shields Level 3": github_icon_base_url + "blizzard/btn-upgrade-protoss-shieldslevel3.png", - "Protoss Air Weapon Level 1": github_icon_base_url + "blizzard/btn-upgrade-protoss-airweaponslevel1.png", - "Protoss Air Weapon Level 2": github_icon_base_url + "blizzard/btn-upgrade-protoss-airweaponslevel2.png", - "Protoss Air Weapon Level 3": github_icon_base_url + "blizzard/btn-upgrade-protoss-airweaponslevel3.png", - "Protoss Air Armor Level 1": github_icon_base_url + "blizzard/btn-upgrade-protoss-airarmorlevel1.png", - "Protoss Air Armor Level 2": github_icon_base_url + "blizzard/btn-upgrade-protoss-airarmorlevel2.png", - "Protoss Air Armor Level 3": github_icon_base_url + "blizzard/btn-upgrade-protoss-airarmorlevel3.png", - - "Quatro": github_icon_base_url + "blizzard/btn-progression-protoss-fenix-6-forgeresearch.png", - - "Photon Cannon": github_icon_base_url + "blizzard/btn-building-protoss-photoncannon.png", - "Khaydarin Monolith": github_icon_base_url + "blizzard/btn-unit-protoss-khaydarinmonolith.png", - "Shield Battery": github_icon_base_url + "blizzard/btn-building-protoss-shieldbattery.png", - - "Enhanced Targeting": github_icon_base_url + "blizzard/btn-upgrade-karax-turretrange.png", - "Optimized Ordnance": github_icon_base_url + "blizzard/btn-upgrade-karax-turretattackspeed.png", - "Khalai Ingenuity": github_icon_base_url + "blizzard/btn-upgrade-karax-pylonwarpininstantly.png", - "Orbital Assimilators": github_icon_base_url + "blizzard/btn-ability-spearofadun-orbitalassimilator.png", - "Amplified Assimilators": github_icon_base_url + "original/btn-research-terran-microfiltering.png", - "Warp Harmonization": github_icon_base_url + "blizzard/btn-ability-spearofadun-warpharmonization.png", - "Superior Warp Gates": github_icon_base_url + "blizzard/talent-artanis-level03-warpgatecharges.png", - "Nexus Overcharge": github_icon_base_url + "blizzard/btn-ability-spearofadun-nexusovercharge.png", - - "Zealot": github_icon_base_url + "blizzard/btn-unit-protoss-zealot-aiur.png", - "Centurion": github_icon_base_url + "blizzard/btn-unit-protoss-zealot-nerazim.png", - "Sentinel": github_icon_base_url + "blizzard/btn-unit-protoss-zealot-purifier.png", - "Supplicant": github_icon_base_url + "blizzard/btn-unit-protoss-alarak-taldarim-supplicant.png", - "Sentry": github_icon_base_url + "blizzard/btn-unit-protoss-sentry.png", - "Energizer": github_icon_base_url + "blizzard/btn-unit-protoss-sentry-purifier.png", - "Havoc": github_icon_base_url + "blizzard/btn-unit-protoss-sentry-taldarim.png", - "Stalker": "https://static.wikia.nocookie.net/starcraft/images/0/0d/Icon_Protoss_Stalker.jpg", - "Instigator": github_icon_base_url + "blizzard/btn-unit-protoss-stalker-purifier.png", - "Slayer": github_icon_base_url + "blizzard/btn-unit-protoss-alarak-taldarim-stalker.png", - "Dragoon": github_icon_base_url + "blizzard/btn-unit-protoss-dragoon-void.png", - "Adept": github_icon_base_url + "blizzard/btn-unit-protoss-adept-purifier.png", - "High Templar": "https://static.wikia.nocookie.net/starcraft/images/a/a0/Icon_Protoss_High_Templar.jpg", - "Signifier": github_icon_base_url + "original/btn-unit-protoss-hightemplar-nerazim.png", - "Ascendant": github_icon_base_url + "blizzard/btn-unit-protoss-hightemplar-taldarim.png", - "Dark Archon": github_icon_base_url + "blizzard/talent-vorazun-level05-unlockdarkarchon.png", - "Dark Templar": "https://static.wikia.nocookie.net/starcraft/images/9/90/Icon_Protoss_Dark_Templar.jpg", - "Avenger": github_icon_base_url + "blizzard/btn-unit-protoss-darktemplar-aiur.png", - "Blood Hunter": github_icon_base_url + "blizzard/btn-unit-protoss-darktemplar-taldarim.png", - - "Leg Enhancements (Zealot/Sentinel/Centurion)": github_icon_base_url + "blizzard/btn-ability-protoss-charge-color.png", - "Shield Capacity (Zealot/Sentinel/Centurion)": github_icon_base_url + "blizzard/btn-upgrade-protoss-shieldslevel1.png", - "Blood Shield (Supplicant)": github_icon_base_url + "blizzard/btn-upgrade-protoss-alarak-supplicantarmor.png", - "Soul Augmentation (Supplicant)": github_icon_base_url + "blizzard/btn-upgrade-protoss-alarak-supplicantextrashields.png", - "Shield Regeneration (Supplicant)": github_icon_base_url + "blizzard/btn-ability-protoss-voidarmor.png", - "Force Field (Sentry)": github_icon_base_url + "blizzard/btn-ability-protoss-forcefield-color.png", - "Hallucination (Sentry)": github_icon_base_url + "blizzard/btn-ability-protoss-hallucination-color.png", - "Reclamation (Energizer)": github_icon_base_url + "blizzard/btn-ability-protoss-reclamation.png", - "Forged Chassis (Energizer)": github_icon_base_url + "blizzard/btn-upgrade-protoss-groundarmorlevel0.png", - "Detect Weakness (Havoc)": github_icon_base_url + "blizzard/btn-upgrade-protoss-alarak-havoctargetlockbuffed.png", - "Bloodshard Resonance (Havoc)": github_icon_base_url + "blizzard/btn-upgrade-protoss-alarak-rangeincrease.png", - "Cloaking Module (Sentry/Energizer/Havoc)": github_icon_base_url + "blizzard/btn-upgrade-protoss-alarak-permanentcloak.png", - "Rapid Recharging (Sentry/Energizer/Havoc/Shield Battery)": github_icon_base_url + "blizzard/btn-upgrade-karax-energyregen200.png", - "Disintegrating Particles (Stalker/Instigator/Slayer)": github_icon_base_url + "blizzard/btn-ability-protoss-phasedisruptor.png", - "Particle Reflection (Stalker/Instigator/Slayer)": github_icon_base_url + "blizzard/btn-upgrade-protoss-fenix-adeptchampionbounceattack.png", - "High Impact Phase Disruptor (Dragoon)": github_icon_base_url + "blizzard/btn-ability-protoss-phasedisruptor.png", - "Trillic Compression System (Dragoon)": github_icon_base_url + "blizzard/btn-ability-protoss-dragoonchassis.png", - "Singularity Charge (Dragoon)": github_icon_base_url + "blizzard/btn-upgrade-artanis-singularitycharge.png", - "Enhanced Strider Servos (Dragoon)": github_icon_base_url + "blizzard/btn-upgrade-terran-transformationservos.png", - "Shockwave (Adept)": github_icon_base_url + "blizzard/btn-upgrade-protoss-fenix-adept-recochetglaiveupgraded.png", - "Resonating Glaives (Adept)": github_icon_base_url + "blizzard/btn-upgrade-protoss-resonatingglaives.png", - "Phase Bulwark (Adept)": github_icon_base_url + "blizzard/btn-upgrade-protoss-adeptshieldupgrade.png", - "Unshackled Psionic Storm (High Templar/Signifier)": github_icon_base_url + "blizzard/btn-ability-protoss-psistorm.png", - "Hallucination (High Templar/Signifier)": github_icon_base_url + "blizzard/btn-ability-protoss-hallucination-color.png", - "Khaydarin Amulet (High Templar/Signifier)": github_icon_base_url + "blizzard/btn-upgrade-protoss-khaydarinamulet.png", - "High Archon (Archon)": github_icon_base_url + "blizzard/btn-upgrade-artanis-healingpsionicstorm.png", - "Power Overwhelming (Ascendant)": github_icon_base_url + "blizzard/btn-upgrade-protoss-alarak-ascendantspermanentlybetter.png", - "Chaotic Attunement (Ascendant)": github_icon_base_url + "blizzard/btn-upgrade-protoss-alarak-ascendant'spsiorbtravelsfurther.png", - "Blood Amulet (Ascendant)": github_icon_base_url + "blizzard/btn-upgrade-protoss-wrathwalker-chargetimeimproved.png", - "Feedback (Dark Archon)": github_icon_base_url + "blizzard/btn-ability-protoss-feedback-color.png", - "Maelstrom (Dark Archon)": github_icon_base_url + "blizzard/btn-ability-protoss-voidstasis.png", - "Argus Talisman (Dark Archon)": github_icon_base_url + "original/btn-upgrade-protoss-argustalisman@scbw.png", - "Dark Archon Meld (Dark Templar)": github_icon_base_url + "blizzard/talent-vorazun-level05-unlockdarkarchon.png", - "Shroud of Adun (Dark Templar/Avenger/Blood Hunter)": github_icon_base_url + "blizzard/talent-vorazun-level01-shadowstalk.png", - "Shadow Guard Training (Dark Templar/Avenger/Blood Hunter)": github_icon_base_url + "blizzard/btn-ability-terran-heal-color.png", - "Blink (Dark Templar/Avenger/Blood Hunter)": github_icon_base_url + "blizzard/btn-ability-protoss-shadowdash.png", - "Resource Efficiency (Dark Templar/Avenger/Blood Hunter)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png", - - "Warp Prism": github_icon_base_url + "blizzard/btn-unit-protoss-warpprism.png", - "Immortal": "https://static.wikia.nocookie.net/starcraft/images/c/c1/Icon_Protoss_Immortal.jpg", - "Annihilator": github_icon_base_url + "blizzard/btn-unit-protoss-immortal-nerazim.png", - "Vanguard": github_icon_base_url + "blizzard/btn-unit-protoss-immortal-taldarim.png", - "Colossus": github_icon_base_url + "blizzard/btn-unit-protoss-colossus-purifier.png", - "Wrathwalker": github_icon_base_url + "blizzard/btn-unit-protoss-colossus-taldarim.png", - "Observer": github_icon_base_url + "blizzard/btn-unit-protoss-observer.png", - "Reaver": github_icon_base_url + "blizzard/btn-unit-protoss-reaver.png", - "Disruptor": github_icon_base_url + "blizzard/btn-unit-protoss-disruptor.png", - - "Gravitic Drive (Warp Prism)": github_icon_base_url + "blizzard/btn-upgrade-protoss-graviticdrive.png", - "Phase Blaster (Warp Prism)": github_icon_base_url + "blizzard/btn-upgrade-protoss-airweaponslevel0.png", - "War Configuration (Warp Prism)": github_icon_base_url + "blizzard/btn-upgrade-protoss-alarak-graviticdrive.png", - "Singularity Charge (Immortal/Annihilator)": github_icon_base_url + "blizzard/btn-upgrade-artanis-singularitycharge.png", - "Advanced Targeting Mechanics (Immortal/Annihilator)": github_icon_base_url + "blizzard/btn-ability-terran-detectionconedebuff.png", - "Agony Launchers (Vanguard)": github_icon_base_url + "blizzard/btn-upgrade-protoss-vanguard-aoeradiusincreased.png", - "Matter Dispersion (Vanguard)": github_icon_base_url + "blizzard/btn-ability-terran-detectionconedebuff.png", - "Pacification Protocol (Colossus)": github_icon_base_url + "blizzard/btn-ability-protoss-chargedblast.png", - "Rapid Power Cycling (Wrathwalker)": github_icon_base_url + "blizzard/btn-upgrade-protoss-wrathwalker-chargetimeimproved.png", - "Eye of Wrath (Wrathwalker)": github_icon_base_url + "blizzard/btn-upgrade-protoss-extendedthermallance.png", - "Gravitic Boosters (Observer)": github_icon_base_url + "blizzard/btn-upgrade-protoss-graviticbooster.png", - "Sensor Array (Observer)": github_icon_base_url + "blizzard/btn-ability-zeratul-observer-sensorarray.png", - "Scarab Damage (Reaver)": github_icon_base_url + "blizzard/btn-ability-protoss-scarabshot.png", - "Solarite Payload (Reaver)": github_icon_base_url + "blizzard/btn-upgrade-artanis-scarabsplashradius.png", - "Reaver Capacity (Reaver)": github_icon_base_url + "original/btn-upgrade-protoss-increasedscarabcapacity@scbw.png", - "Resource Efficiency (Reaver)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png", - - "Phoenix": "https://static.wikia.nocookie.net/starcraft/images/b/b1/Icon_Protoss_Phoenix.jpg", - "Mirage": github_icon_base_url + "blizzard/btn-unit-protoss-phoenix-purifier.png", - "Corsair": github_icon_base_url + "blizzard/btn-unit-protoss-corsair.png", - "Destroyer": github_icon_base_url + "blizzard/btn-unit-protoss-voidray-taldarim.png", - "Void Ray": github_icon_base_url + "blizzard/btn-unit-protoss-voidray-nerazim.png", - "Carrier": "https://static.wikia.nocookie.net/starcraft/images/2/2c/Icon_Protoss_Carrier.jpg", - "Scout": github_icon_base_url + "original/btn-unit-protoss-scout.png", - "Tempest": github_icon_base_url + "blizzard/btn-unit-protoss-tempest-purifier.png", - "Mothership": github_icon_base_url + "blizzard/btn-unit-protoss-mothership-taldarim.png", - "Arbiter": github_icon_base_url + "blizzard/btn-unit-protoss-arbiter.png", - "Oracle": github_icon_base_url + "blizzard/btn-unit-protoss-oracle.png", - - "Ionic Wavelength Flux (Phoenix/Mirage)": github_icon_base_url + "blizzard/btn-upgrade-protoss-airweaponslevel0.png", - "Anion Pulse-Crystals (Phoenix/Mirage)": github_icon_base_url + "blizzard/btn-upgrade-protoss-phoenixrange.png", - "Stealth Drive (Corsair)": github_icon_base_url + "blizzard/btn-upgrade-vorazun-corsairpermanentlycloaked.png", - "Argus Jewel (Corsair)": github_icon_base_url + "blizzard/btn-ability-protoss-stasistrap.png", - "Sustaining Disruption (Corsair)": github_icon_base_url + "blizzard/btn-ability-protoss-disruptionweb.png", - "Neutron Shields (Corsair)": github_icon_base_url + "blizzard/btn-upgrade-protoss-shieldslevel1.png", - "Reforged Bloodshard Core (Destroyer)": github_icon_base_url + "blizzard/btn-amonshardsarmor.png", - "Flux Vanes (Void Ray/Destroyer)": github_icon_base_url + "blizzard/btn-upgrade-protoss-fluxvanes.png", - "Graviton Catapult (Carrier)": github_icon_base_url + "blizzard/btn-upgrade-protoss-gravitoncatapult.png", - "Hull of Past Glories (Carrier)": github_icon_base_url + "blizzard/btn-progression-protoss-fenix-14-colossusandcarrierchampionsresearch.png", - "Combat Sensor Array (Scout)": github_icon_base_url + "blizzard/btn-upgrade-protoss-fenix-scoutchampionrange.png", - "Apial Sensors (Scout)": github_icon_base_url + "blizzard/btn-upgrade-tychus-detection.png", - "Gravitic Thrusters (Scout)": github_icon_base_url + "blizzard/btn-upgrade-protoss-graviticbooster.png", - "Advanced Photon Blasters (Scout)": github_icon_base_url + "blizzard/btn-upgrade-protoss-airweaponslevel3.png", - "Tectonic Destabilizers (Tempest)": github_icon_base_url + "blizzard/btn-ability-protoss-disruptionblast.png", - "Quantic Reactor (Tempest)": github_icon_base_url + "blizzard/btn-upgrade-protoss-researchgravitysling.png", - "Gravity Sling (Tempest)": github_icon_base_url + "blizzard/btn-upgrade-protoss-tectonicdisruptors.png", - "Chronostatic Reinforcement (Arbiter)": github_icon_base_url + "blizzard/btn-upgrade-protoss-airarmorlevel2.png", - "Khaydarin Core (Arbiter)": github_icon_base_url + "blizzard/btn-upgrade-protoss-adeptshieldupgrade.png", - "Spacetime Anchor (Arbiter)": github_icon_base_url + "blizzard/btn-ability-protoss-stasisfield.png", - "Resource Efficiency (Arbiter)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png", - "Enhanced Cloak Field (Arbiter)": github_icon_base_url + "blizzard/btn-ability-stetmann-stetzonegenerator-speed.png", - "Stealth Drive (Oracle)": github_icon_base_url + "blizzard/btn-upgrade-vorazun-oraclepermanentlycloaked.png", - "Stasis Calibration (Oracle)": github_icon_base_url + "blizzard/btn-ability-protoss-oracle-stasiscalibration.png", - "Temporal Acceleration Beam (Oracle)": github_icon_base_url + "blizzard/btn-ability-protoss-oraclepulsarcannonon.png", - - "Matrix Overload": github_icon_base_url + "blizzard/btn-ability-spearofadun-matrixoverload.png", - "Guardian Shell": github_icon_base_url + "blizzard/btn-ability-spearofadun-guardianshell.png", - - "Chrono Surge (Spear of Adun Calldown)": github_icon_base_url + "blizzard/btn-ability-spearofadun-chronosurge.png", - "Proxy Pylon (Spear of Adun Calldown)": github_icon_base_url + "blizzard/btn-ability-spearofadun-deploypylon.png", - "Warp In Reinforcements (Spear of Adun Calldown)": github_icon_base_url + "blizzard/btn-ability-spearofadun-warpinreinforcements.png", - "Pylon Overcharge (Spear of Adun Calldown)": github_icon_base_url + "blizzard/btn-ability-protoss-purify.png", - "Orbital Strike (Spear of Adun Calldown)": github_icon_base_url + "blizzard/btn-ability-spearofadun-orbitalstrike.png", - "Temporal Field (Spear of Adun Calldown)": github_icon_base_url + "blizzard/btn-ability-spearofadun-temporalfield.png", - "Solar Lance (Spear of Adun Calldown)": github_icon_base_url + "blizzard/btn-ability-spearofadun-solarlance.png", - "Mass Recall (Spear of Adun Calldown)": github_icon_base_url + "blizzard/btn-ability-spearofadun-massrecall.png", - "Shield Overcharge (Spear of Adun Calldown)": github_icon_base_url + "blizzard/btn-ability-spearofadun-shieldovercharge.png", - "Deploy Fenix (Spear of Adun Calldown)": github_icon_base_url + "blizzard/btn-unit-protoss-fenix.png", - "Purifier Beam (Spear of Adun Calldown)": github_icon_base_url + "blizzard/btn-ability-spearofadun-purifierbeam.png", - "Time Stop (Spear of Adun Calldown)": github_icon_base_url + "blizzard/btn-ability-spearofadun-timestop.png", - "Solar Bombardment (Spear of Adun Calldown)": github_icon_base_url + "blizzard/btn-ability-spearofadun-solarbombardment.png", - - "Reconstruction Beam (Spear of Adun Auto-Cast)": github_icon_base_url + "blizzard/btn-ability-spearofadun-reconstructionbeam.png", - "Overwatch (Spear of Adun Auto-Cast)": github_icon_base_url + "blizzard/btn-ability-zeratul-chargedcrystal-psionicwinds.png", - - "Nothing": "", - } - sc2wol_location_ids = { - "Liberation Day": range(SC2WOL_LOC_ID_OFFSET + 100, SC2WOL_LOC_ID_OFFSET + 200), - "The Outlaws": range(SC2WOL_LOC_ID_OFFSET + 200, SC2WOL_LOC_ID_OFFSET + 300), - "Zero Hour": range(SC2WOL_LOC_ID_OFFSET + 300, SC2WOL_LOC_ID_OFFSET + 400), - "Evacuation": range(SC2WOL_LOC_ID_OFFSET + 400, SC2WOL_LOC_ID_OFFSET + 500), - "Outbreak": range(SC2WOL_LOC_ID_OFFSET + 500, SC2WOL_LOC_ID_OFFSET + 600), - "Safe Haven": range(SC2WOL_LOC_ID_OFFSET + 600, SC2WOL_LOC_ID_OFFSET + 700), - "Haven's Fall": range(SC2WOL_LOC_ID_OFFSET + 700, SC2WOL_LOC_ID_OFFSET + 800), - "Smash and Grab": range(SC2WOL_LOC_ID_OFFSET + 800, SC2WOL_LOC_ID_OFFSET + 900), - "The Dig": range(SC2WOL_LOC_ID_OFFSET + 900, SC2WOL_LOC_ID_OFFSET + 1000), - "The Moebius Factor": range(SC2WOL_LOC_ID_OFFSET + 1000, SC2WOL_LOC_ID_OFFSET + 1100), - "Supernova": range(SC2WOL_LOC_ID_OFFSET + 1100, SC2WOL_LOC_ID_OFFSET + 1200), - "Maw of the Void": range(SC2WOL_LOC_ID_OFFSET + 1200, SC2WOL_LOC_ID_OFFSET + 1300), - "Devil's Playground": range(SC2WOL_LOC_ID_OFFSET + 1300, SC2WOL_LOC_ID_OFFSET + 1400), - "Welcome to the Jungle": range(SC2WOL_LOC_ID_OFFSET + 1400, SC2WOL_LOC_ID_OFFSET + 1500), - "Breakout": range(SC2WOL_LOC_ID_OFFSET + 1500, SC2WOL_LOC_ID_OFFSET + 1600), - "Ghost of a Chance": range(SC2WOL_LOC_ID_OFFSET + 1600, SC2WOL_LOC_ID_OFFSET + 1700), - "The Great Train Robbery": range(SC2WOL_LOC_ID_OFFSET + 1700, SC2WOL_LOC_ID_OFFSET + 1800), - "Cutthroat": range(SC2WOL_LOC_ID_OFFSET + 1800, SC2WOL_LOC_ID_OFFSET + 1900), - "Engine of Destruction": range(SC2WOL_LOC_ID_OFFSET + 1900, SC2WOL_LOC_ID_OFFSET + 2000), - "Media Blitz": range(SC2WOL_LOC_ID_OFFSET + 2000, SC2WOL_LOC_ID_OFFSET + 2100), - "Piercing the Shroud": range(SC2WOL_LOC_ID_OFFSET + 2100, SC2WOL_LOC_ID_OFFSET + 2200), - "Whispers of Doom": range(SC2WOL_LOC_ID_OFFSET + 2200, SC2WOL_LOC_ID_OFFSET + 2300), - "A Sinister Turn": range(SC2WOL_LOC_ID_OFFSET + 2300, SC2WOL_LOC_ID_OFFSET + 2400), - "Echoes of the Future": range(SC2WOL_LOC_ID_OFFSET + 2400, SC2WOL_LOC_ID_OFFSET + 2500), - "In Utter Darkness": range(SC2WOL_LOC_ID_OFFSET + 2500, SC2WOL_LOC_ID_OFFSET + 2600), - "Gates of Hell": range(SC2WOL_LOC_ID_OFFSET + 2600, SC2WOL_LOC_ID_OFFSET + 2700), - "Belly of the Beast": range(SC2WOL_LOC_ID_OFFSET + 2700, SC2WOL_LOC_ID_OFFSET + 2800), - "Shatter the Sky": range(SC2WOL_LOC_ID_OFFSET + 2800, SC2WOL_LOC_ID_OFFSET + 2900), - "All-In": range(SC2WOL_LOC_ID_OFFSET + 2900, SC2WOL_LOC_ID_OFFSET + 3000), - - "Lab Rat": range(SC2HOTS_LOC_ID_OFFSET + 100, SC2HOTS_LOC_ID_OFFSET + 200), - "Back in the Saddle": range(SC2HOTS_LOC_ID_OFFSET + 200, SC2HOTS_LOC_ID_OFFSET + 300), - "Rendezvous": range(SC2HOTS_LOC_ID_OFFSET + 300, SC2HOTS_LOC_ID_OFFSET + 400), - "Harvest of Screams": range(SC2HOTS_LOC_ID_OFFSET + 400, SC2HOTS_LOC_ID_OFFSET + 500), - "Shoot the Messenger": range(SC2HOTS_LOC_ID_OFFSET + 500, SC2HOTS_LOC_ID_OFFSET + 600), - "Enemy Within": range(SC2HOTS_LOC_ID_OFFSET + 600, SC2HOTS_LOC_ID_OFFSET + 700), - "Domination": range(SC2HOTS_LOC_ID_OFFSET + 700, SC2HOTS_LOC_ID_OFFSET + 800), - "Fire in the Sky": range(SC2HOTS_LOC_ID_OFFSET + 800, SC2HOTS_LOC_ID_OFFSET + 900), - "Old Soldiers": range(SC2HOTS_LOC_ID_OFFSET + 900, SC2HOTS_LOC_ID_OFFSET + 1000), - "Waking the Ancient": range(SC2HOTS_LOC_ID_OFFSET + 1000, SC2HOTS_LOC_ID_OFFSET + 1100), - "The Crucible": range(SC2HOTS_LOC_ID_OFFSET + 1100, SC2HOTS_LOC_ID_OFFSET + 1200), - "Supreme": range(SC2HOTS_LOC_ID_OFFSET + 1200, SC2HOTS_LOC_ID_OFFSET + 1300), - "Infested": range(SC2HOTS_LOC_ID_OFFSET + 1300, SC2HOTS_LOC_ID_OFFSET + 1400), - "Hand of Darkness": range(SC2HOTS_LOC_ID_OFFSET + 1400, SC2HOTS_LOC_ID_OFFSET + 1500), - "Phantoms of the Void": range(SC2HOTS_LOC_ID_OFFSET + 1500, SC2HOTS_LOC_ID_OFFSET + 1600), - "With Friends Like These": range(SC2HOTS_LOC_ID_OFFSET + 1600, SC2HOTS_LOC_ID_OFFSET + 1700), - "Conviction": range(SC2HOTS_LOC_ID_OFFSET + 1700, SC2HOTS_LOC_ID_OFFSET + 1800), - "Planetfall": range(SC2HOTS_LOC_ID_OFFSET + 1800, SC2HOTS_LOC_ID_OFFSET + 1900), - "Death From Above": range(SC2HOTS_LOC_ID_OFFSET + 1900, SC2HOTS_LOC_ID_OFFSET + 2000), - "The Reckoning": range(SC2HOTS_LOC_ID_OFFSET + 2000, SC2HOTS_LOC_ID_OFFSET + 2100), - - "Dark Whispers": range(SC2LOTV_LOC_ID_OFFSET + 100, SC2LOTV_LOC_ID_OFFSET + 200), - "Ghosts in the Fog": range(SC2LOTV_LOC_ID_OFFSET + 200, SC2LOTV_LOC_ID_OFFSET + 300), - "Evil Awoken": range(SC2LOTV_LOC_ID_OFFSET + 300, SC2LOTV_LOC_ID_OFFSET + 400), - - "For Aiur!": range(SC2LOTV_LOC_ID_OFFSET + 400, SC2LOTV_LOC_ID_OFFSET + 500), - "The Growing Shadow": range(SC2LOTV_LOC_ID_OFFSET + 500, SC2LOTV_LOC_ID_OFFSET + 600), - "The Spear of Adun": range(SC2LOTV_LOC_ID_OFFSET + 600, SC2LOTV_LOC_ID_OFFSET + 700), - "Sky Shield": range(SC2LOTV_LOC_ID_OFFSET + 700, SC2LOTV_LOC_ID_OFFSET + 800), - "Brothers in Arms": range(SC2LOTV_LOC_ID_OFFSET + 800, SC2LOTV_LOC_ID_OFFSET + 900), - "Amon's Reach": range(SC2LOTV_LOC_ID_OFFSET + 900, SC2LOTV_LOC_ID_OFFSET + 1000), - "Last Stand": range(SC2LOTV_LOC_ID_OFFSET + 1000, SC2LOTV_LOC_ID_OFFSET + 1100), - "Forbidden Weapon": range(SC2LOTV_LOC_ID_OFFSET + 1100, SC2LOTV_LOC_ID_OFFSET + 1200), - "Temple of Unification": range(SC2LOTV_LOC_ID_OFFSET + 1200, SC2LOTV_LOC_ID_OFFSET + 1300), - "The Infinite Cycle": range(SC2LOTV_LOC_ID_OFFSET + 1300, SC2LOTV_LOC_ID_OFFSET + 1400), - "Harbinger of Oblivion": range(SC2LOTV_LOC_ID_OFFSET + 1400, SC2LOTV_LOC_ID_OFFSET + 1500), - "Unsealing the Past": range(SC2LOTV_LOC_ID_OFFSET + 1500, SC2LOTV_LOC_ID_OFFSET + 1600), - "Purification": range(SC2LOTV_LOC_ID_OFFSET + 1600, SC2LOTV_LOC_ID_OFFSET + 1700), - "Steps of the Rite": range(SC2LOTV_LOC_ID_OFFSET + 1700, SC2LOTV_LOC_ID_OFFSET + 1800), - "Rak'Shir": range(SC2LOTV_LOC_ID_OFFSET + 1800, SC2LOTV_LOC_ID_OFFSET + 1900), - "Templar's Charge": range(SC2LOTV_LOC_ID_OFFSET + 1900, SC2LOTV_LOC_ID_OFFSET + 2000), - "Templar's Return": range(SC2LOTV_LOC_ID_OFFSET + 2000, SC2LOTV_LOC_ID_OFFSET + 2100), - "The Host": range(SC2LOTV_LOC_ID_OFFSET + 2100, SC2LOTV_LOC_ID_OFFSET + 2200), - "Salvation": range(SC2LOTV_LOC_ID_OFFSET + 2200, SC2LOTV_LOC_ID_OFFSET + 2300), - - "Into the Void": range(SC2LOTV_LOC_ID_OFFSET + 2300, SC2LOTV_LOC_ID_OFFSET + 2400), - "The Essence of Eternity": range(SC2LOTV_LOC_ID_OFFSET + 2400, SC2LOTV_LOC_ID_OFFSET + 2500), - "Amon's Fall": range(SC2LOTV_LOC_ID_OFFSET + 2500, SC2LOTV_LOC_ID_OFFSET + 2600), - - "The Escape": range(SC2NCO_LOC_ID_OFFSET + 100, SC2NCO_LOC_ID_OFFSET + 200), - "Sudden Strike": range(SC2NCO_LOC_ID_OFFSET + 200, SC2NCO_LOC_ID_OFFSET + 300), - "Enemy Intelligence": range(SC2NCO_LOC_ID_OFFSET + 300, SC2NCO_LOC_ID_OFFSET + 400), - "Trouble In Paradise": range(SC2NCO_LOC_ID_OFFSET + 400, SC2NCO_LOC_ID_OFFSET + 500), - "Night Terrors": range(SC2NCO_LOC_ID_OFFSET + 500, SC2NCO_LOC_ID_OFFSET + 600), - "Flashpoint": range(SC2NCO_LOC_ID_OFFSET + 600, SC2NCO_LOC_ID_OFFSET + 700), - "In the Enemy's Shadow": range(SC2NCO_LOC_ID_OFFSET + 700, SC2NCO_LOC_ID_OFFSET + 800), - "Dark Skies": range(SC2NCO_LOC_ID_OFFSET + 800, SC2NCO_LOC_ID_OFFSET + 900), - "End Game": range(SC2NCO_LOC_ID_OFFSET + 900, SC2NCO_LOC_ID_OFFSET + 1000), - } + inventory: collections.Counter[int] = tracker_data.get_player_inventory_counts(team, player) + item_id_to_name = tracker_data.item_id_to_name["Starcraft 2"] + location_id_to_name = tracker_data.location_id_to_name["Starcraft 2"] + # Filler item counters display_data = {} - - # Grouped Items - grouped_item_ids = { - "Progressive Terran Weapon Upgrade": 107 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Terran Armor Upgrade": 108 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Terran Infantry Upgrade": 109 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Terran Vehicle Upgrade": 110 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Terran Ship Upgrade": 111 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Terran Weapon/Armor Upgrade": 112 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Zerg Weapon Upgrade": 105 + SC2HOTS_ITEM_ID_OFFSET, - "Progressive Zerg Armor Upgrade": 106 + SC2HOTS_ITEM_ID_OFFSET, - "Progressive Zerg Ground Upgrade": 107 + SC2HOTS_ITEM_ID_OFFSET, - "Progressive Zerg Flyer Upgrade": 108 + SC2HOTS_ITEM_ID_OFFSET, - "Progressive Zerg Weapon/Armor Upgrade": 109 + SC2HOTS_ITEM_ID_OFFSET, - "Progressive Protoss Weapon Upgrade": 105 + SC2LOTV_ITEM_ID_OFFSET, - "Progressive Protoss Armor Upgrade": 106 + SC2LOTV_ITEM_ID_OFFSET, - "Progressive Protoss Ground Upgrade": 107 + SC2LOTV_ITEM_ID_OFFSET, - "Progressive Protoss Air Upgrade": 108 + SC2LOTV_ITEM_ID_OFFSET, - "Progressive Protoss Weapon/Armor Upgrade": 109 + SC2LOTV_ITEM_ID_OFFSET, - } - grouped_item_replacements = { - "Progressive Terran Weapon Upgrade": ["Progressive Terran Infantry Weapon", - "Progressive Terran Vehicle Weapon", - "Progressive Terran Ship Weapon"], - "Progressive Terran Armor Upgrade": ["Progressive Terran Infantry Armor", - "Progressive Terran Vehicle Armor", - "Progressive Terran Ship Armor"], - "Progressive Terran Infantry Upgrade": ["Progressive Terran Infantry Weapon", - "Progressive Terran Infantry Armor"], - "Progressive Terran Vehicle Upgrade": ["Progressive Terran Vehicle Weapon", - "Progressive Terran Vehicle Armor"], - "Progressive Terran Ship Upgrade": ["Progressive Terran Ship Weapon", "Progressive Terran Ship Armor"], - "Progressive Zerg Weapon Upgrade": ["Progressive Zerg Melee Attack", "Progressive Zerg Missile Attack", - "Progressive Zerg Flyer Attack"], - "Progressive Zerg Armor Upgrade": ["Progressive Zerg Ground Carapace", - "Progressive Zerg Flyer Carapace"], - "Progressive Zerg Ground Upgrade": ["Progressive Zerg Melee Attack", "Progressive Zerg Missile Attack", - "Progressive Zerg Ground Carapace"], - "Progressive Zerg Flyer Upgrade": ["Progressive Zerg Flyer Attack", "Progressive Zerg Flyer Carapace"], - "Progressive Protoss Weapon Upgrade": ["Progressive Protoss Ground Weapon", - "Progressive Protoss Air Weapon"], - "Progressive Protoss Armor Upgrade": ["Progressive Protoss Ground Armor", "Progressive Protoss Shields", - "Progressive Protoss Air Armor"], - "Progressive Protoss Ground Upgrade": ["Progressive Protoss Ground Weapon", - "Progressive Protoss Ground Armor", - "Progressive Protoss Shields"], - "Progressive Protoss Air Upgrade": ["Progressive Protoss Air Weapon", "Progressive Protoss Air Armor", - "Progressive Protoss Shields"] - } - grouped_item_replacements["Progressive Terran Weapon/Armor Upgrade"] = \ - grouped_item_replacements["Progressive Terran Weapon Upgrade"] \ - + grouped_item_replacements["Progressive Terran Armor Upgrade"] - grouped_item_replacements["Progressive Zerg Weapon/Armor Upgrade"] = \ - grouped_item_replacements["Progressive Zerg Weapon Upgrade"] \ - + grouped_item_replacements["Progressive Zerg Armor Upgrade"] - grouped_item_replacements["Progressive Protoss Weapon/Armor Upgrade"] = \ - grouped_item_replacements["Progressive Protoss Weapon Upgrade"] \ - + grouped_item_replacements["Progressive Protoss Armor Upgrade"] - replacement_item_ids = { - "Progressive Terran Infantry Weapon": 100 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Terran Infantry Armor": 102 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Terran Vehicle Weapon": 103 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Terran Vehicle Armor": 104 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Terran Ship Weapon": 105 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Terran Ship Armor": 106 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Zerg Melee Attack": 100 + SC2HOTS_ITEM_ID_OFFSET, - "Progressive Zerg Missile Attack": 101 + SC2HOTS_ITEM_ID_OFFSET, - "Progressive Zerg Ground Carapace": 102 + SC2HOTS_ITEM_ID_OFFSET, - "Progressive Zerg Flyer Attack": 103 + SC2HOTS_ITEM_ID_OFFSET, - "Progressive Zerg Flyer Carapace": 104 + SC2HOTS_ITEM_ID_OFFSET, - "Progressive Protoss Ground Weapon": 100 + SC2LOTV_ITEM_ID_OFFSET, - "Progressive Protoss Ground Armor": 101 + SC2LOTV_ITEM_ID_OFFSET, - "Progressive Protoss Shields": 102 + SC2LOTV_ITEM_ID_OFFSET, - "Progressive Protoss Air Weapon": 103 + SC2LOTV_ITEM_ID_OFFSET, - "Progressive Protoss Air Armor": 104 + SC2LOTV_ITEM_ID_OFFSET, - } - - inventory: collections.Counter = tracker_data.get_player_inventory_counts(team, player) - for grouped_item_name, grouped_item_id in grouped_item_ids.items(): - count: int = inventory[grouped_item_id] - if count > 0: - for replacement_item in grouped_item_replacements[grouped_item_name]: - replacement_id: int = replacement_item_ids[replacement_item] - if replacement_id not in inventory or count > inventory[replacement_id]: - # If two groups provide the same individual item, maximum is used - # (this behavior is used for Protoss Shields) - inventory[replacement_id] = count - - # Determine display for progressive items - progressive_items = { - "Progressive Terran Infantry Weapon": 100 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Terran Infantry Armor": 102 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Terran Vehicle Weapon": 103 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Terran Vehicle Armor": 104 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Terran Ship Weapon": 105 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Terran Ship Armor": 106 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Fire-Suppression System": 206 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Orbital Command": 207 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Stimpack (Marine)": 208 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Stimpack (Firebat)": 226 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Stimpack (Marauder)": 228 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Stimpack (Reaper)": 250 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Stimpack (Hellion)": 259 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Replenishable Magazine (Vulture)": 303 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Tri-Lithium Power Cell (Diamondback)": 306 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Tomahawk Power Cells (Wraith)": 312 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Cross-Spectrum Dampeners (Banshee)": 316 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Missile Pods (Battlecruiser)": 318 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Defensive Matrix (Battlecruiser)": 319 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Immortality Protocol (Thor)": 325 + SC2WOL_ITEM_ID_OFFSET, - "Progressive High Impact Payload (Thor)": 361 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Augmented Thrusters (Planetary Fortress)": 388 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Regenerative Bio-Steel": 617 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Stealth Suit Module (Nova Suit Module)": 904 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Zerg Melee Attack": 100 + SC2HOTS_ITEM_ID_OFFSET, - "Progressive Zerg Missile Attack": 101 + SC2HOTS_ITEM_ID_OFFSET, - "Progressive Zerg Ground Carapace": 102 + SC2HOTS_ITEM_ID_OFFSET, - "Progressive Zerg Flyer Attack": 103 + SC2HOTS_ITEM_ID_OFFSET, - "Progressive Zerg Flyer Carapace": 104 + SC2HOTS_ITEM_ID_OFFSET, - "Progressive Protoss Ground Weapon": 100 + SC2LOTV_ITEM_ID_OFFSET, - "Progressive Protoss Ground Armor": 101 + SC2LOTV_ITEM_ID_OFFSET, - "Progressive Protoss Shields": 102 + SC2LOTV_ITEM_ID_OFFSET, - "Progressive Protoss Air Weapon": 103 + SC2LOTV_ITEM_ID_OFFSET, - "Progressive Protoss Air Armor": 104 + SC2LOTV_ITEM_ID_OFFSET, - "Progressive Proxy Pylon (Spear of Adun Calldown)": 701 + SC2LOTV_ITEM_ID_OFFSET, - } - # Format: L0, L1, L2, L3 - progressive_names = { - "Progressive Terran Infantry Weapon": ["Terran Infantry Weapons Level 1", - "Terran Infantry Weapons Level 1", - "Terran Infantry Weapons Level 2", - "Terran Infantry Weapons Level 3"], - "Progressive Terran Infantry Armor": ["Terran Infantry Armor Level 1", - "Terran Infantry Armor Level 1", - "Terran Infantry Armor Level 2", - "Terran Infantry Armor Level 3"], - "Progressive Terran Vehicle Weapon": ["Terran Vehicle Weapons Level 1", - "Terran Vehicle Weapons Level 1", - "Terran Vehicle Weapons Level 2", - "Terran Vehicle Weapons Level 3"], - "Progressive Terran Vehicle Armor": ["Terran Vehicle Armor Level 1", - "Terran Vehicle Armor Level 1", - "Terran Vehicle Armor Level 2", - "Terran Vehicle Armor Level 3"], - "Progressive Terran Ship Weapon": ["Terran Ship Weapons Level 1", - "Terran Ship Weapons Level 1", - "Terran Ship Weapons Level 2", - "Terran Ship Weapons Level 3"], - "Progressive Terran Ship Armor": ["Terran Ship Armor Level 1", - "Terran Ship Armor Level 1", - "Terran Ship Armor Level 2", - "Terran Ship Armor Level 3"], - "Progressive Fire-Suppression System": ["Fire-Suppression System Level 1", - "Fire-Suppression System Level 1", - "Fire-Suppression System Level 2"], - "Progressive Orbital Command": ["Orbital Command", "Orbital Command", - "Planetary Command Module"], - "Progressive Stimpack (Marine)": ["Stimpack (Marine)", "Stimpack (Marine)", - "Super Stimpack (Marine)"], - "Progressive Stimpack (Firebat)": ["Stimpack (Firebat)", "Stimpack (Firebat)", - "Super Stimpack (Firebat)"], - "Progressive Stimpack (Marauder)": ["Stimpack (Marauder)", "Stimpack (Marauder)", - "Super Stimpack (Marauder)"], - "Progressive Stimpack (Reaper)": ["Stimpack (Reaper)", "Stimpack (Reaper)", - "Super Stimpack (Reaper)"], - "Progressive Stimpack (Hellion)": ["Stimpack (Hellion)", "Stimpack (Hellion)", - "Super Stimpack (Hellion)"], - "Progressive Replenishable Magazine (Vulture)": ["Replenishable Magazine (Vulture)", - "Replenishable Magazine (Vulture)", - "Replenishable Magazine (Free) (Vulture)"], - "Progressive Tri-Lithium Power Cell (Diamondback)": ["Tri-Lithium Power Cell (Diamondback)", - "Tri-Lithium Power Cell (Diamondback)", - "Tungsten Spikes (Diamondback)"], - "Progressive Tomahawk Power Cells (Wraith)": ["Tomahawk Power Cells (Wraith)", - "Tomahawk Power Cells (Wraith)", - "Unregistered Cloaking Module (Wraith)"], - "Progressive Cross-Spectrum Dampeners (Banshee)": ["Cross-Spectrum Dampeners (Banshee)", - "Cross-Spectrum Dampeners (Banshee)", - "Advanced Cross-Spectrum Dampeners (Banshee)"], - "Progressive Missile Pods (Battlecruiser)": ["Missile Pods (Battlecruiser) Level 1", - "Missile Pods (Battlecruiser) Level 1", - "Missile Pods (Battlecruiser) Level 2"], - "Progressive Defensive Matrix (Battlecruiser)": ["Defensive Matrix (Battlecruiser)", - "Defensive Matrix (Battlecruiser)", - "Advanced Defensive Matrix (Battlecruiser)"], - "Progressive Immortality Protocol (Thor)": ["Immortality Protocol (Thor)", - "Immortality Protocol (Thor)", - "Immortality Protocol (Free) (Thor)"], - "Progressive High Impact Payload (Thor)": ["High Impact Payload (Thor)", - "High Impact Payload (Thor)", "Smart Servos (Thor)"], - "Progressive Augmented Thrusters (Planetary Fortress)": ["Lift Off (Planetary Fortress)", - "Lift Off (Planetary Fortress)", - "Armament Stabilizers (Planetary Fortress)"], - "Progressive Regenerative Bio-Steel": ["Regenerative Bio-Steel Level 1", - "Regenerative Bio-Steel Level 1", - "Regenerative Bio-Steel Level 2", - "Regenerative Bio-Steel Level 3"], - "Progressive Stealth Suit Module (Nova Suit Module)": ["Stealth Suit Module (Nova Suit Module)", - "Cloak (Nova Suit Module)", - "Permanently Cloaked (Nova Suit Module)"], - "Progressive Zerg Melee Attack": ["Zerg Melee Attack Level 1", - "Zerg Melee Attack Level 1", - "Zerg Melee Attack Level 2", - "Zerg Melee Attack Level 3"], - "Progressive Zerg Missile Attack": ["Zerg Missile Attack Level 1", - "Zerg Missile Attack Level 1", - "Zerg Missile Attack Level 2", - "Zerg Missile Attack Level 3"], - "Progressive Zerg Ground Carapace": ["Zerg Ground Carapace Level 1", - "Zerg Ground Carapace Level 1", - "Zerg Ground Carapace Level 2", - "Zerg Ground Carapace Level 3"], - "Progressive Zerg Flyer Attack": ["Zerg Flyer Attack Level 1", - "Zerg Flyer Attack Level 1", - "Zerg Flyer Attack Level 2", - "Zerg Flyer Attack Level 3"], - "Progressive Zerg Flyer Carapace": ["Zerg Flyer Carapace Level 1", - "Zerg Flyer Carapace Level 1", - "Zerg Flyer Carapace Level 2", - "Zerg Flyer Carapace Level 3"], - "Progressive Protoss Ground Weapon": ["Protoss Ground Weapon Level 1", - "Protoss Ground Weapon Level 1", - "Protoss Ground Weapon Level 2", - "Protoss Ground Weapon Level 3"], - "Progressive Protoss Ground Armor": ["Protoss Ground Armor Level 1", - "Protoss Ground Armor Level 1", - "Protoss Ground Armor Level 2", - "Protoss Ground Armor Level 3"], - "Progressive Protoss Shields": ["Protoss Shields Level 1", "Protoss Shields Level 1", - "Protoss Shields Level 2", "Protoss Shields Level 3"], - "Progressive Protoss Air Weapon": ["Protoss Air Weapon Level 1", - "Protoss Air Weapon Level 1", - "Protoss Air Weapon Level 2", - "Protoss Air Weapon Level 3"], - "Progressive Protoss Air Armor": ["Protoss Air Armor Level 1", - "Protoss Air Armor Level 1", - "Protoss Air Armor Level 2", - "Protoss Air Armor Level 3"], - "Progressive Proxy Pylon (Spear of Adun Calldown)": ["Proxy Pylon (Spear of Adun Calldown)", - "Proxy Pylon (Spear of Adun Calldown)", - "Warp In Reinforcements (Spear of Adun Calldown)"] - } - for item_name, item_id in progressive_items.items(): - level = min(inventory[item_id], len(progressive_names[item_name]) - 1) - display_name = progressive_names[item_name][level] - base_name = (item_name.split(maxsplit=1)[1].lower() - .replace(' ', '_') - .replace("-", "") - .replace("(", "") - .replace(")", "")) - display_data[base_name + "_level"] = level - display_data[base_name + "_url"] = icons[display_name] if display_name in icons else "FIXME" - display_data[base_name + "_name"] = display_name - - # Multi-items - multi_items = { - "Additional Starting Minerals": 800 + SC2WOL_ITEM_ID_OFFSET, - "Additional Starting Vespene": 801 + SC2WOL_ITEM_ID_OFFSET, - "Additional Starting Supply": 802 + SC2WOL_ITEM_ID_OFFSET - } - for item_name, item_id in multi_items.items(): - base_name = item_name.split()[-1].lower() - count = inventory[item_id] - if base_name == "supply": - count = count * starting_supply_per_item - elif base_name == "minerals": - count = count * minerals_per_item - elif base_name == "vespene": - count = count * vespene_per_item - display_data[base_name + "_count"] = count - # Kerrigan level - level_items = { - "1 Kerrigan Level": 509 + SC2HOTS_ITEM_ID_OFFSET, - "2 Kerrigan Levels": 508 + SC2HOTS_ITEM_ID_OFFSET, - "3 Kerrigan Levels": 507 + SC2HOTS_ITEM_ID_OFFSET, - "4 Kerrigan Levels": 506 + SC2HOTS_ITEM_ID_OFFSET, - "5 Kerrigan Levels": 505 + SC2HOTS_ITEM_ID_OFFSET, - "6 Kerrigan Levels": 504 + SC2HOTS_ITEM_ID_OFFSET, - "7 Kerrigan Levels": 503 + SC2HOTS_ITEM_ID_OFFSET, - "8 Kerrigan Levels": 502 + SC2HOTS_ITEM_ID_OFFSET, - "9 Kerrigan Levels": 501 + SC2HOTS_ITEM_ID_OFFSET, - "10 Kerrigan Levels": 500 + SC2HOTS_ITEM_ID_OFFSET, - "14 Kerrigan Levels": 510 + SC2HOTS_ITEM_ID_OFFSET, - "35 Kerrigan Levels": 511 + SC2HOTS_ITEM_ID_OFFSET, - "70 Kerrigan Levels": 512 + SC2HOTS_ITEM_ID_OFFSET, - } - level_amounts = { - "1 Kerrigan Level": 1, - "2 Kerrigan Levels": 2, - "3 Kerrigan Levels": 3, - "4 Kerrigan Levels": 4, - "5 Kerrigan Levels": 5, - "6 Kerrigan Levels": 6, - "7 Kerrigan Levels": 7, - "8 Kerrigan Levels": 8, - "9 Kerrigan Levels": 9, - "10 Kerrigan Levels": 10, - "14 Kerrigan Levels": 14, - "35 Kerrigan Levels": 35, - "70 Kerrigan Levels": 70, - } - kerrigan_level = 0 - for item_name, item_id in level_items.items(): - count = inventory[item_id] - amount = level_amounts[item_name] - kerrigan_level += count * amount - display_data["kerrigan_level"] = kerrigan_level - - # Victory condition - game_state = tracker_data.get_player_client_status(team, player) - display_data["game_finished"] = game_state == 30 - - # Turn location IDs into mission objective counts + display_data["minerals_count"] = slot_data.get("minerals_per_item", 15) * inventory.get(STARTING_MINERALS_ITEM_ID, 0) + display_data["vespene_count"] = slot_data.get("vespene_per_item", 15) * inventory.get(STARTING_VESPENE_ITEM_ID, 0) + display_data["supply_count"] = slot_data.get("starting_supply_per_item", 2) * inventory.get(STARTING_SUPPLY_ITEM_ID, 0) + display_data["max_supply_count"] = slot_data.get("maximum_supply_per_item", 1) * inventory.get(MAX_SUPPLY_ITEM_ID, 0) + display_data["reduced_supply_count"] = slot_data.get("maximum_supply_reduction_per_item", 1) * inventory.get(REDUCED_MAX_SUPPLY_ITEM_ID, 0) + display_data["construction_speed_count"] = inventory.get(BUILDING_CONSTRUCTION_SPEED_ITEM_ID, 0) + display_data["shield_regen_count"] = inventory.get(SHIELD_REGENERATION_ITEM_ID, 0) + display_data["upgrade_speed_count"] = inventory.get(UPGRADE_RESEARCH_SPEED_ITEM_ID, 0) + display_data["research_cost_count"] = inventory.get(UPGRADE_RESEARCH_COST_ITEM_ID, 0) + + # Locations + have_nco_locations = False locations = tracker_data.get_player_locations(team, player) checked_locations = tracker_data.get_player_checked_locations(team, player) - lookup_name = lambda id: tracker_data.location_id_to_name["Starcraft 2"][id] - location_info = {mission_name: {lookup_name(id): (id in checked_locations) for id in mission_locations if - id in set(locations)} for mission_name, mission_locations in - sc2wol_location_ids.items()} - checks_done = {mission_name: len( - [id for id in mission_locations if id in checked_locations and id in set(locations)]) for - mission_name, mission_locations in sc2wol_location_ids.items()} - checks_done['Total'] = len(checked_locations) - checks_in_area = {mission_name: len([id for id in mission_locations if id in set(locations)]) for - mission_name, mission_locations in sc2wol_location_ids.items()} - checks_in_area['Total'] = sum(checks_in_area.values()) + missions: dict[str, list[tuple[str, bool]]] = {} + for location_id in locations: + location_name = location_id_to_name.get(location_id, "") + if ":" not in location_name: + continue + mission_name = location_name.split(":", 1)[0] + missions.setdefault(mission_name, []).append((location_name, location_id in checked_locations)) + if location_id >= NCO_LOCATION_ID_LOW and location_id < NCO_LOCATION_ID_HIGH: + have_nco_locations = True + missions = {mission: missions[mission] for mission in sorted(missions)} + + # Kerrigan level + level_item_id_to_amount = ( + (509 + SC2HOTS_ITEM_ID_OFFSET, 1,), + (508 + SC2HOTS_ITEM_ID_OFFSET, 2,), + (507 + SC2HOTS_ITEM_ID_OFFSET, 3,), + (506 + SC2HOTS_ITEM_ID_OFFSET, 4,), + (505 + SC2HOTS_ITEM_ID_OFFSET, 5,), + (504 + SC2HOTS_ITEM_ID_OFFSET, 6,), + (503 + SC2HOTS_ITEM_ID_OFFSET, 7,), + (502 + SC2HOTS_ITEM_ID_OFFSET, 8,), + (501 + SC2HOTS_ITEM_ID_OFFSET, 9,), + (500 + SC2HOTS_ITEM_ID_OFFSET, 10,), + (510 + SC2HOTS_ITEM_ID_OFFSET, 14,), + (511 + SC2HOTS_ITEM_ID_OFFSET, 35,), + (512 + SC2HOTS_ITEM_ID_OFFSET, 70,), + ) + kerrigan_level = 0 + for item_id, levels_per_item in level_item_id_to_amount: + kerrigan_level += levels_per_item * inventory[item_id] + display_data["kerrigan_level"] = kerrigan_level + + # Hero presence + display_data["kerrigan_present"] = slot_data.get("kerrigan_presence", 0) == 0 + display_data["nova_present"] = have_nco_locations + + # Upgrades + TERRAN_INFANTRY_WEAPON_ID = 100 + SC2WOL_ITEM_ID_OFFSET + TERRAN_INFANTRY_ARMOR_ID = 102 + SC2WOL_ITEM_ID_OFFSET + TERRAN_VEHICLE_WEAPON_ID = 103 + SC2WOL_ITEM_ID_OFFSET + TERRAN_VEHICLE_ARMOR_ID = 104 + SC2WOL_ITEM_ID_OFFSET + TERRAN_SHIP_WEAPON_ID = 105 + SC2WOL_ITEM_ID_OFFSET + TERRAN_SHIP_ARMOR_ID = 106 + SC2WOL_ITEM_ID_OFFSET + ZERG_MELEE_ATTACK_ID = 100 + SC2HOTS_ITEM_ID_OFFSET + ZERG_MISSILE_ATTACK_ID = 101 + SC2HOTS_ITEM_ID_OFFSET + ZERG_GROUND_CARAPACE_ID = 102 + SC2HOTS_ITEM_ID_OFFSET + ZERG_FLYER_ATTACK_ID = 103 + SC2HOTS_ITEM_ID_OFFSET + ZERG_FLYER_CARAPACE_ID = 104 + SC2HOTS_ITEM_ID_OFFSET + PROTOSS_GROUND_WEAPON_ID = 100 + SC2LOTV_ITEM_ID_OFFSET + PROTOSS_GROUND_ARMOR_ID = 101 + SC2LOTV_ITEM_ID_OFFSET + PROTOSS_SHIELDS_ID = 102 + SC2LOTV_ITEM_ID_OFFSET + PROTOSS_AIR_WEAPON_ID = 103 + SC2LOTV_ITEM_ID_OFFSET + PROTOSS_AIR_ARMOR_ID = 104 + SC2LOTV_ITEM_ID_OFFSET + + # Bundles + TERRAN_WEAPON_UPGRADE_ID = 107 + SC2WOL_ITEM_ID_OFFSET + TERRAN_ARMOR_UPGRADE_ID = 108 + SC2WOL_ITEM_ID_OFFSET + TERRAN_INFANTRY_UPGRADE_ID = 109 + SC2WOL_ITEM_ID_OFFSET + TERRAN_VEHICLE_UPGRADE_ID = 110 + SC2WOL_ITEM_ID_OFFSET + TERRAN_SHIP_UPGRADE_ID = 111 + SC2WOL_ITEM_ID_OFFSET + TERRAN_WEAPON_ARMOR_UPGRADE_ID = 112 + SC2WOL_ITEM_ID_OFFSET + ZERG_WEAPON_UPGRADE_ID = 105 + SC2HOTS_ITEM_ID_OFFSET + ZERG_ARMOR_UPGRADE_ID = 106 + SC2HOTS_ITEM_ID_OFFSET + ZERG_GROUND_UPGRADE_ID = 107 + SC2HOTS_ITEM_ID_OFFSET + ZERG_FLYER_UPGRADE_ID = 108 + SC2HOTS_ITEM_ID_OFFSET + ZERG_WEAPON_ARMOR_UPGRADE_ID = 109 + SC2HOTS_ITEM_ID_OFFSET + PROTOSS_WEAPON_UPGRADE_ID = 105 + SC2LOTV_ITEM_ID_OFFSET + PROTOSS_ARMOR_UPGRADE_ID = 106 + SC2LOTV_ITEM_ID_OFFSET + PROTOSS_GROUND_UPGRADE_ID = 107 + SC2LOTV_ITEM_ID_OFFSET + PROTOSS_AIR_UPGRADE_ID = 108 + SC2LOTV_ITEM_ID_OFFSET + PROTOSS_WEAPON_ARMOR_UPGRADE_ID = 109 + SC2LOTV_ITEM_ID_OFFSET + grouped_item_replacements = { + TERRAN_WEAPON_UPGRADE_ID: [ + TERRAN_INFANTRY_WEAPON_ID, + TERRAN_VEHICLE_WEAPON_ID, + TERRAN_SHIP_WEAPON_ID, + ], + TERRAN_ARMOR_UPGRADE_ID: [ + TERRAN_INFANTRY_ARMOR_ID, + TERRAN_VEHICLE_ARMOR_ID, + TERRAN_SHIP_ARMOR_ID, + ], + TERRAN_INFANTRY_UPGRADE_ID: [ + TERRAN_INFANTRY_WEAPON_ID, + TERRAN_INFANTRY_ARMOR_ID, + ], + TERRAN_VEHICLE_UPGRADE_ID: [ + TERRAN_VEHICLE_WEAPON_ID, + TERRAN_VEHICLE_ARMOR_ID, + ], + TERRAN_SHIP_UPGRADE_ID: [ + TERRAN_SHIP_WEAPON_ID, + TERRAN_SHIP_ARMOR_ID + ], + ZERG_WEAPON_UPGRADE_ID: [ + ZERG_MELEE_ATTACK_ID, + ZERG_MISSILE_ATTACK_ID, + ZERG_FLYER_ATTACK_ID, + ], + ZERG_ARMOR_UPGRADE_ID: [ + ZERG_GROUND_CARAPACE_ID, + ZERG_FLYER_CARAPACE_ID, + ], + ZERG_GROUND_UPGRADE_ID: [ + ZERG_MELEE_ATTACK_ID, + ZERG_MISSILE_ATTACK_ID, + ZERG_GROUND_CARAPACE_ID, + ], + ZERG_FLYER_UPGRADE_ID: [ + ZERG_FLYER_ATTACK_ID, + ZERG_FLYER_CARAPACE_ID, + ], + PROTOSS_WEAPON_UPGRADE_ID: [ + PROTOSS_GROUND_WEAPON_ID, + PROTOSS_AIR_WEAPON_ID, + ], + PROTOSS_ARMOR_UPGRADE_ID: [ + PROTOSS_GROUND_ARMOR_ID, + PROTOSS_SHIELDS_ID, + PROTOSS_AIR_ARMOR_ID, + ], + PROTOSS_GROUND_UPGRADE_ID: [ + PROTOSS_GROUND_WEAPON_ID, + PROTOSS_GROUND_ARMOR_ID, + PROTOSS_SHIELDS_ID, + ], + PROTOSS_AIR_UPGRADE_ID: [ + PROTOSS_AIR_WEAPON_ID, + PROTOSS_AIR_ARMOR_ID, + PROTOSS_SHIELDS_ID, + ] + } + grouped_item_replacements[TERRAN_WEAPON_ARMOR_UPGRADE_ID] = ( + grouped_item_replacements[TERRAN_WEAPON_UPGRADE_ID] + + grouped_item_replacements[TERRAN_ARMOR_UPGRADE_ID] + ) + grouped_item_replacements[ZERG_WEAPON_ARMOR_UPGRADE_ID] = ( + grouped_item_replacements[ZERG_WEAPON_UPGRADE_ID] + + grouped_item_replacements[ZERG_ARMOR_UPGRADE_ID] + ) + grouped_item_replacements[PROTOSS_WEAPON_ARMOR_UPGRADE_ID] = ( + grouped_item_replacements[PROTOSS_WEAPON_UPGRADE_ID] + + grouped_item_replacements[PROTOSS_ARMOR_UPGRADE_ID] + ) + for bundle_id, upgrade_ids in grouped_item_replacements.items(): + bundle_amount = inventory[bundle_id] + for upgrade_id in upgrade_ids: + if bundle_amount > inventory[upgrade_id]: + # Only assign, don't add. + # This behaviour mimics protoss shields, where the output is + # the maximum bundle contribution, not the sum + inventory[upgrade_id] = bundle_amount + + + # Victory condition + game_state = tracker_data.get_player_client_status(team, player) + display_data["game_finished"] = game_state == ClientStatus.CLIENT_GOAL + + # Keys + keys: dict[str, int] = {} + for item_id, item_count in inventory.items(): + if item_id < SC2_KEY_ITEM_ID_OFFSET: + continue + keys[item_id_to_name[item_id]] = item_count - lookup_any_item_id_to_name = tracker_data.item_id_to_name["Starcraft 2"] return render_template( "tracker__Starcraft2.html", inventory=inventory, - icons=icons, - acquired_items={lookup_any_item_id_to_name[id] for id, count in inventory.items() if count > 0}, player=player, team=team, room=tracker_data.room, player_name=tracker_data.get_player_name(team, player), - checks_done=checks_done, - checks_in_area=checks_in_area, - location_info=location_info, + missions=missions, + locations=locations, + checked_locations=checked_locations, + location_id_to_name=location_id_to_name, + item_id_to_name=item_id_to_name, + keys=keys, + saving_second=tracker_data.get_room_saving_second(), **display_data, ) + _player_trackers["Starcraft 2"] = render_Starcraft2_tracker diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py index 06c77ab0..7bd47d0b 100644 --- a/worlds/LauncherComponents.py +++ b/worlds/LauncherComponents.py @@ -229,8 +229,6 @@ components: List[Component] = [ Component('Zelda 1 Client', 'Zelda1Client', file_identifier=SuffixIdentifier('.aptloz')), # ChecksFinder Component('ChecksFinder Client', 'ChecksFinderClient'), - # Starcraft 2 - Component('Starcraft 2 Client', 'Starcraft2Client'), # Zillion Component('Zillion Client', 'ZillionClient', file_identifier=SuffixIdentifier('.apzl')), diff --git a/worlds/_sc2common/bot/game_data.py b/worlds/_sc2common/bot/game_data.py index 50f10bd6..ed0edf0b 100644 --- a/worlds/_sc2common/bot/game_data.py +++ b/worlds/_sc2common/bot/game_data.py @@ -19,7 +19,7 @@ class GameData: """ :param data: """ - self.abilities: Dict[int, AbilityData] = {} + self.abilities: Dict[int, AbilityData] = {a.ability_id: AbilityData(self, a) for a in data.abilities if a.available} self.units: Dict[int, UnitTypeData] = {u.unit_id: UnitTypeData(self, u) for u in data.units if u.available} self.upgrades: Dict[int, UpgradeData] = {u.upgrade_id: UpgradeData(self, u) for u in data.upgrades} # Cached UnitTypeIds so that conversion does not take long. This needs to be moved elsewhere if a new GameData object is created multiple times per game @@ -40,7 +40,7 @@ class AbilityData: self._proto = proto # What happens if we comment this out? Should this not be commented out? What is its purpose? - assert self.id != 0 + # assert self.id != 0 # let the world burn def __repr__(self) -> str: return f"AbilityData(name={self._proto.button_name})" diff --git a/worlds/sc2/Client.py b/worlds/sc2/Client.py deleted file mode 100644 index 77b13a5a..00000000 --- a/worlds/sc2/Client.py +++ /dev/null @@ -1,1630 +0,0 @@ -from __future__ import annotations - -import asyncio -import copy -import ctypes -import enum -import inspect -import logging -import multiprocessing -import os.path -import re -import sys -import tempfile -import typing -import queue -import zipfile -import io -import random -import concurrent.futures -from pathlib import Path - -# CommonClient import first to trigger ModuleUpdater -from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser -from Utils import init_logging, is_windows, async_start -from . import ItemNames, Options -from .ItemGroups import item_name_groups -from .Options import ( - MissionOrder, KerriganPrimalStatus, kerrigan_unit_available, KerriganPresence, - GameSpeed, GenericUpgradeItems, GenericUpgradeResearch, ColorChoice, GenericUpgradeMissions, - LocationInclusion, ExtraLocations, MasteryLocations, ChallengeLocations, VanillaLocations, - DisableForcedCamera, SkipCutscenes, GrantStoryTech, GrantStoryLevels, TakeOverAIAllies, RequiredTactics, - SpearOfAdunPresence, SpearOfAdunPresentInNoBuild, SpearOfAdunAutonomouslyCastAbilityPresence, - SpearOfAdunAutonomouslyCastPresentInNoBuild -) - - -if __name__ == "__main__": - init_logging("SC2Client", exception_logger="Client") - -logger = logging.getLogger("Client") -sc2_logger = logging.getLogger("Starcraft2") - -import nest_asyncio -from worlds._sc2common import bot -from worlds._sc2common.bot.data import Race -from worlds._sc2common.bot.main import run_game -from worlds._sc2common.bot.player import Bot -from .Items import (lookup_id_to_name, get_full_item_list, ItemData, type_flaggroups, upgrade_numbers, - upgrade_numbers_all) -from .Locations import SC2WOL_LOC_ID_OFFSET, LocationType, SC2HOTS_LOC_ID_OFFSET -from .MissionTables import (lookup_id_to_mission, SC2Campaign, lookup_name_to_mission, - lookup_id_to_campaign, MissionConnection, SC2Mission, campaign_mission_table, SC2Race) -from .Regions import MissionInfo - -import colorama -from Options import Option -from NetUtils import ClientStatus, NetworkItem, JSONtoTextParser, JSONMessagePart, add_json_item, add_json_location, add_json_text, JSONTypes -from MultiServer import mark_raw - -pool = concurrent.futures.ThreadPoolExecutor(1) -loop = asyncio.get_event_loop_policy().new_event_loop() -nest_asyncio.apply(loop) -MAX_BONUS: int = 28 -VICTORY_MODULO: int = 100 - -# GitHub repo where the Map/mod data is hosted for /download_data command -DATA_REPO_OWNER = "Ziktofel" -DATA_REPO_NAME = "Archipelago-SC2-data" -DATA_API_VERSION = "API3" - -# Bot controller -CONTROLLER_HEALTH: int = 38281 -CONTROLLER2_HEALTH: int = 38282 - -# Games -STARCRAFT2 = "Starcraft 2" -STARCRAFT2_WOL = "Starcraft 2 Wings of Liberty" - - -# Data version file path. -# This file is used to tell if the downloaded data are outdated -# Associated with /download_data command -def get_metadata_file() -> str: - return os.environ["SC2PATH"] + os.sep + "ArchipelagoSC2Metadata.txt" - - -class ConfigurableOptionType(enum.Enum): - INTEGER = enum.auto() - ENUM = enum.auto() - -class ConfigurableOptionInfo(typing.NamedTuple): - name: str - variable_name: str - option_class: typing.Type[Option] - option_type: ConfigurableOptionType = ConfigurableOptionType.ENUM - can_break_logic: bool = False - - -class ColouredMessage: - def __init__(self, text: str = '', *, keep_markup: bool = False) -> None: - self.parts: typing.List[dict] = [] - if text: - self(text, keep_markup=keep_markup) - def __call__(self, text: str, *, keep_markup: bool = False) -> 'ColouredMessage': - add_json_text(self.parts, text, keep_markup=keep_markup) - return self - def coloured(self, text: str, colour: str) -> 'ColouredMessage': - add_json_text(self.parts, text, type="color", color=colour) - return self - def location(self, location_id: int, player_id: int) -> 'ColouredMessage': - add_json_location(self.parts, location_id, player_id) - return self - def item(self, item_id: int, player_id: int, flags: int = 0) -> 'ColouredMessage': - add_json_item(self.parts, item_id, player_id, flags) - return self - def player(self, player_id: int) -> 'ColouredMessage': - add_json_text(self.parts, str(player_id), type=JSONTypes.player_id) - return self - def send(self, ctx: SC2Context) -> None: - ctx.on_print_json({"data": self.parts, "cmd": "PrintJSON"}) - - -class StarcraftClientProcessor(ClientCommandProcessor): - ctx: SC2Context - - def formatted_print(self, text: str) -> None: - """Prints with kivy formatting to the GUI, and also prints to command-line and to all logs""" - # Note(mm): Bold/underline can help readability, but unfortunately the CommonClient does not filter bold tags from command-line output. - # Regardless, using `on_print_json` to get formatted text in the GUI and output in the command-line and in the logs, - # without having to branch code from CommonClient - self.ctx.on_print_json({"data": [{"text": text, "keep_markup": True}]}) - - def _cmd_difficulty(self, difficulty: str = "") -> bool: - """Overrides the current difficulty set for the world. Takes the argument casual, normal, hard, or brutal""" - options = difficulty.split() - num_options = len(options) - - if num_options > 0: - difficulty_choice = options[0].lower() - if difficulty_choice == "casual": - self.ctx.difficulty_override = 0 - elif difficulty_choice == "normal": - self.ctx.difficulty_override = 1 - elif difficulty_choice == "hard": - self.ctx.difficulty_override = 2 - elif difficulty_choice == "brutal": - self.ctx.difficulty_override = 3 - else: - self.output("Unable to parse difficulty '" + options[0] + "'") - return False - - self.output("Difficulty set to " + options[0]) - return True - - else: - if self.ctx.difficulty == -1: - self.output("Please connect to a seed before checking difficulty.") - else: - current_difficulty = self.ctx.difficulty - if self.ctx.difficulty_override >= 0: - current_difficulty = self.ctx.difficulty_override - self.output("Current difficulty: " + ["Casual", "Normal", "Hard", "Brutal"][current_difficulty]) - self.output("To change the difficulty, add the name of the difficulty after the command.") - return False - - - def _cmd_game_speed(self, game_speed: str = "") -> bool: - """Overrides the current game speed for the world. - Takes the arguments default, slower, slow, normal, fast, faster""" - options = game_speed.split() - num_options = len(options) - - if num_options > 0: - speed_choice = options[0].lower() - if speed_choice == "default": - self.ctx.game_speed_override = 0 - elif speed_choice == "slower": - self.ctx.game_speed_override = 1 - elif speed_choice == "slow": - self.ctx.game_speed_override = 2 - elif speed_choice == "normal": - self.ctx.game_speed_override = 3 - elif speed_choice == "fast": - self.ctx.game_speed_override = 4 - elif speed_choice == "faster": - self.ctx.game_speed_override = 5 - else: - self.output("Unable to parse game speed '" + options[0] + "'") - return False - - self.output("Game speed set to " + options[0]) - return True - - else: - if self.ctx.game_speed == -1: - self.output("Please connect to a seed before checking game speed.") - else: - current_speed = self.ctx.game_speed - if self.ctx.game_speed_override >= 0: - current_speed = self.ctx.game_speed_override - self.output("Current game speed: " - + ["Default", "Slower", "Slow", "Normal", "Fast", "Faster"][current_speed]) - self.output("To change the game speed, add the name of the speed after the command," - " or Default to select based on difficulty.") - return False - - @mark_raw - def _cmd_received(self, filter_search: str = "") -> bool: - """List received items. - Pass in a parameter to filter the search by partial item name or exact item group.""" - # Groups must be matched case-sensitively, so we properly capitalize the search term - # eg. "Spear of Adun" over "Spear Of Adun" or "spear of adun" - # This fails a lot of item name matches, but those should be found by partial name match - formatted_filter_search = " ".join([(part.lower() if len(part) <= 3 else part.lower().capitalize()) for part in filter_search.split()]) - - def item_matches_filter(item_name: str) -> bool: - # The filter can be an exact group name or a partial item name - # Partial item name can be matched case-insensitively - if filter_search.lower() in item_name.lower(): - return True - # The search term should already be formatted as a group name - if formatted_filter_search in item_name_groups and item_name in item_name_groups[formatted_filter_search]: - return True - return False - - items = get_full_item_list() - categorized_items: typing.Dict[SC2Race, typing.List[int]] = {} - parent_to_child: typing.Dict[int, typing.List[int]] = {} - items_received: typing.Dict[int, typing.List[NetworkItem]] = {} - filter_match_count = 0 - for item in self.ctx.items_received: - items_received.setdefault(item.item, []).append(item) - items_received_set = set(items_received) - for item_data in items.values(): - if item_data.parent_item: - parent_to_child.setdefault(items[item_data.parent_item].code, []).append(item_data.code) - else: - categorized_items.setdefault(item_data.race, []).append(item_data.code) - for faction in SC2Race: - has_printed_faction_title = False - def print_faction_title(): - if not has_printed_faction_title: - self.formatted_print(f" [u]{faction.name}[/u] ") - - for item_id in categorized_items[faction]: - item_name = self.ctx.item_names.lookup_in_game(item_id) - received_child_items = items_received_set.intersection(parent_to_child.get(item_id, [])) - matching_children = [child for child in received_child_items - if item_matches_filter(self.ctx.item_names.lookup_in_game(child))] - received_items_of_this_type = items_received.get(item_id, []) - item_is_match = item_matches_filter(item_name) - if item_is_match or len(matching_children) > 0: - # Print found item if it or its children match the filter - if item_is_match: - filter_match_count += len(received_items_of_this_type) - for item in received_items_of_this_type: - print_faction_title() - has_printed_faction_title = True - (ColouredMessage('* ').item(item.item, self.ctx.slot, flags=item.flags) - (" from ").location(item.location, item.player) - (" by ").player(item.player) - ).send(self.ctx) - - if received_child_items: - # We have this item's children - if len(matching_children) == 0: - # ...but none of them match the filter - continue - - if not received_items_of_this_type: - # We didn't receive the item itself - print_faction_title() - has_printed_faction_title = True - ColouredMessage("- ").coloured(item_name, "black")(" - not obtained").send(self.ctx) - - for child_item in matching_children: - received_items_of_this_type = items_received.get(child_item, []) - for item in received_items_of_this_type: - filter_match_count += len(received_items_of_this_type) - (ColouredMessage(' * ').item(item.item, self.ctx.slot, flags=item.flags) - (" from ").location(item.location, item.player) - (" by ").player(item.player) - ).send(self.ctx) - - non_matching_children = len(received_child_items) - len(matching_children) - if non_matching_children > 0: - self.formatted_print(f" + {non_matching_children} child items that don't match the filter") - if filter_search == "": - self.formatted_print(f"[b]Obtained: {len(self.ctx.items_received)} items[/b]") - else: - self.formatted_print(f"[b]Filter \"{filter_search}\" found {filter_match_count} out of {len(self.ctx.items_received)} obtained items[/b]") - return True - - def _cmd_option(self, option_name: str = "", option_value: str = "") -> None: - """Sets a Starcraft game option that can be changed after generation. Use "/option list" to see all options.""" - - LOGIC_WARNING = f" *Note changing this may result in logically unbeatable games*\n" - - options = ( - ConfigurableOptionInfo('kerrigan_presence', 'kerrigan_presence', Options.KerriganPresence, can_break_logic=True), - ConfigurableOptionInfo('soa_presence', 'spear_of_adun_presence', Options.SpearOfAdunPresence, can_break_logic=True), - ConfigurableOptionInfo('soa_in_nobuilds', 'spear_of_adun_present_in_no_build', Options.SpearOfAdunPresentInNoBuild, can_break_logic=True), - ConfigurableOptionInfo('control_ally', 'take_over_ai_allies', Options.TakeOverAIAllies, can_break_logic=True), - ConfigurableOptionInfo('minerals_per_item', 'minerals_per_item', Options.MineralsPerItem, ConfigurableOptionType.INTEGER), - ConfigurableOptionInfo('gas_per_item', 'vespene_per_item', Options.VespenePerItem, ConfigurableOptionType.INTEGER), - ConfigurableOptionInfo('supply_per_item', 'starting_supply_per_item', Options.StartingSupplyPerItem, ConfigurableOptionType.INTEGER), - ConfigurableOptionInfo('no_forced_camera', 'disable_forced_camera', Options.DisableForcedCamera), - ConfigurableOptionInfo('skip_cutscenes', 'skip_cutscenes', Options.SkipCutscenes), - ) - - WARNING_COLOUR = "salmon" - CMD_COLOUR = "slateblue" - boolean_option_map = { - 'y': 'true', 'yes': 'true', 'n': 'false', 'no': 'false', - } - - help_message = ColouredMessage(inspect.cleandoc(""" - Options - -------------------- - """))('\n') - for option in options: - option_help_text = inspect.cleandoc(option.option_class.__doc__ or "No description provided.").split('\n', 1)[0] - help_message.coloured(option.name, CMD_COLOUR)(": " + " | ".join(option.option_class.options) - + f" -- {option_help_text}\n") - if option.can_break_logic: - help_message.coloured(LOGIC_WARNING, WARNING_COLOUR) - help_message("--------------------\nEnter an option without arguments to see its current value.\n") - - if not option_name or option_name == 'list' or option_name == 'help': - help_message.send(self.ctx) - return - for option in options: - if option_name == option.name: - option_value = boolean_option_map.get(option_value, option_value) - if not option_value: - pass - elif option.option_type == ConfigurableOptionType.ENUM and option_value in option.option_class.options: - self.ctx.__dict__[option.variable_name] = option.option_class.options[option_value] - elif option.option_type == ConfigurableOptionType.INTEGER: - try: - self.ctx.__dict__[option.variable_name] = int(option_value, base=0) - except: - self.output(f"{option_value} is not a valid integer") - else: - self.output(f"Unknown option value '{option_value}'") - ColouredMessage(f"{option.name} is '{option.option_class.get_option_name(self.ctx.__dict__[option.variable_name])}'").send(self.ctx) - break - else: - self.output(f"Unknown option '{option_name}'") - help_message.send(self.ctx) - - def _cmd_color(self, faction: str = "", color: str = "") -> None: - """Changes the player color for a given faction.""" - player_colors = [ - "White", "Red", "Blue", "Teal", - "Purple", "Yellow", "Orange", "Green", - "LightPink", "Violet", "LightGrey", "DarkGreen", - "Brown", "LightGreen", "DarkGrey", "Pink", - "Rainbow", "Random", "Default" - ] - var_names = { - 'raynor': 'player_color_raynor', - 'kerrigan': 'player_color_zerg', - 'primal': 'player_color_zerg_primal', - 'protoss': 'player_color_protoss', - 'nova': 'player_color_nova', - } - faction = faction.lower() - if not faction: - for faction_name, key in var_names.items(): - self.output(f"Current player color for {faction_name}: {player_colors[self.ctx.__dict__[key]]}") - self.output("To change your color, add the faction name and color after the command.") - self.output("Available factions: " + ', '.join(var_names)) - self.output("Available colors: " + ', '.join(player_colors)) - return - elif faction not in var_names: - self.output(f"Unknown faction '{faction}'.") - self.output("Available factions: " + ', '.join(var_names)) - return - match_colors = [player_color.lower() for player_color in player_colors] - if not color: - self.output(f"Current player color for {faction}: {player_colors[self.ctx.__dict__[var_names[faction]]]}") - self.output("To change this faction's colors, add the name of the color after the command.") - self.output("Available colors: " + ', '.join(player_colors)) - else: - if color.lower() not in match_colors: - self.output(color + " is not a valid color. Available colors: " + ', '.join(player_colors)) - return - if color.lower() == "random": - color = random.choice(player_colors[:16]) - self.ctx.__dict__[var_names[faction]] = match_colors.index(color.lower()) - self.ctx.pending_color_update = True - self.output(f"Color for {faction} set to " + player_colors[self.ctx.__dict__[var_names[faction]]]) - - def _cmd_disable_mission_check(self) -> bool: - """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.""" - self.ctx.missions_unlocked = True - sc2_logger.info("Mission check has been disabled") - return True - - def _cmd_play(self, mission_id: str = "") -> bool: - """Start a Starcraft 2 mission""" - - options = mission_id.split() - num_options = len(options) - - if num_options > 0: - mission_number = int(options[0]) - - self.ctx.play_mission(mission_number) - - else: - sc2_logger.info( - "Mission ID needs to be specified. Use /unfinished or /available to view ids for available missions.") - return False - - return True - - def _cmd_available(self) -> bool: - """Get what missions are currently available to play""" - - request_available_missions(self.ctx) - return True - - def _cmd_unfinished(self) -> bool: - """Get what missions are currently available to play and have not had all locations checked""" - - request_unfinished_missions(self.ctx) - return True - - @mark_raw - def _cmd_set_path(self, path: str = '') -> bool: - """Manually set the SC2 install directory (if the automatic detection fails).""" - if path: - os.environ["SC2PATH"] = path - is_mod_installed_correctly() - return True - else: - sc2_logger.warning("When using set_path, you must type the path to your SC2 install directory.") - return False - - def _cmd_download_data(self) -> bool: - """Download the most recent release of the necessary files for playing SC2 with - Archipelago. Will overwrite existing files.""" - pool.submit(self._download_data) - return True - - @staticmethod - def _download_data() -> bool: - if "SC2PATH" not in os.environ: - check_game_install_path() - - if os.path.exists(get_metadata_file()): - with open(get_metadata_file(), "r") as f: - metadata = f.read() - else: - metadata = None - - tempzip, metadata = download_latest_release_zip( - DATA_REPO_OWNER, DATA_REPO_NAME, DATA_API_VERSION, metadata=metadata, force_download=True) - - if tempzip: - try: - zipfile.ZipFile(tempzip).extractall(path=os.environ["SC2PATH"]) - sc2_logger.info(f"Download complete. Package installed.") - if metadata is not None: - with open(get_metadata_file(), "w") as f: - f.write(metadata) - finally: - os.remove(tempzip) - else: - sc2_logger.warning("Download aborted/failed. Read the log for more information.") - return False - return True - - -class SC2JSONtoTextParser(JSONtoTextParser): - def __init__(self, ctx) -> None: - self.handlers = { - "ItemSend": self._handle_color, - "ItemCheat": self._handle_color, - "Hint": self._handle_color, - } - super().__init__(ctx) - - def _handle_color(self, node: JSONMessagePart) -> str: - codes = node["color"].split(";") - buffer = "".join(self.color_code(code) for code in codes if code in self.color_codes) - return buffer + self._handle_text(node) + '' - - def color_code(self, code: str) -> str: - return '' - - -class SC2Context(CommonContext): - command_processor = StarcraftClientProcessor - game = STARCRAFT2 - items_handling = 0b111 - - def __init__(self, *args, **kwargs) -> None: - super(SC2Context, self).__init__(*args, **kwargs) - self.raw_text_parser = SC2JSONtoTextParser(self) - - self.difficulty = -1 - self.game_speed = -1 - self.disable_forced_camera = 0 - self.skip_cutscenes = 0 - self.all_in_choice = 0 - self.mission_order = 0 - self.player_color_raynor = ColorChoice.option_blue - self.player_color_zerg = ColorChoice.option_orange - self.player_color_zerg_primal = ColorChoice.option_purple - self.player_color_protoss = ColorChoice.option_blue - self.player_color_nova = ColorChoice.option_dark_grey - self.pending_color_update = False - self.kerrigan_presence = 0 - self.kerrigan_primal_status = 0 - self.levels_per_check = 0 - self.checks_per_level = 1 - self.mission_req_table: typing.Dict[SC2Campaign, typing.Dict[str, MissionInfo]] = {} - self.final_mission: int = 29 - self.announcements: queue.Queue = queue.Queue() - self.sc2_run_task: typing.Optional[asyncio.Task] = None - self.missions_unlocked: bool = False # allow launching missions ignoring requirements - self.generic_upgrade_missions = 0 - self.generic_upgrade_research = 0 - self.generic_upgrade_items = 0 - self.location_inclusions: typing.Dict[LocationType, int] = {} - self.plando_locations: typing.List[str] = [] - self.current_tooltip = None - self.last_loc_list = None - self.difficulty_override = -1 - self.game_speed_override = -1 - self.mission_id_to_location_ids: typing.Dict[int, typing.List[int]] = {} - self.last_bot: typing.Optional[ArchipelagoBot] = None - self.slot_data_version = 2 - self.grant_story_tech = 0 - self.required_tactics = RequiredTactics.option_standard - self.take_over_ai_allies = TakeOverAIAllies.option_false - self.spear_of_adun_presence = SpearOfAdunPresence.option_not_present - self.spear_of_adun_present_in_no_build = SpearOfAdunPresentInNoBuild.option_false - self.spear_of_adun_autonomously_cast_ability_presence = SpearOfAdunAutonomouslyCastAbilityPresence.option_not_present - self.spear_of_adun_autonomously_cast_present_in_no_build = SpearOfAdunAutonomouslyCastPresentInNoBuild.option_false - self.minerals_per_item = 15 - self.vespene_per_item = 15 - self.starting_supply_per_item = 2 - self.nova_covert_ops_only = False - self.kerrigan_levels_per_mission_completed = 0 - - async def server_auth(self, password_requested: bool = False) -> None: - self.game = STARCRAFT2 - if password_requested and not self.password: - await super(SC2Context, self).server_auth(password_requested) - await self.get_username() - await self.send_connect() - if self.ui: - self.ui.first_check = True - - def is_legacy_game(self): - return self.game == STARCRAFT2_WOL - - def event_invalid_game(self): - if self.is_legacy_game(): - self.game = STARCRAFT2 - super().event_invalid_game() - else: - self.game = STARCRAFT2_WOL - async_start(self.send_connect()) - - def on_package(self, cmd: str, args: dict) -> None: - if cmd == "Connected": - self.difficulty = args["slot_data"]["game_difficulty"] - self.game_speed = args["slot_data"].get("game_speed", GameSpeed.option_default) - self.disable_forced_camera = args["slot_data"].get("disable_forced_camera", DisableForcedCamera.default) - self.skip_cutscenes = args["slot_data"].get("skip_cutscenes", SkipCutscenes.default) - self.all_in_choice = args["slot_data"]["all_in_map"] - self.slot_data_version = args["slot_data"].get("version", 2) - slot_req_table: dict = args["slot_data"]["mission_req"] - - first_item = list(slot_req_table.keys())[0] - # Maintaining backwards compatibility with older slot data - if first_item in [str(campaign.id) for campaign in SC2Campaign]: - # Multi-campaign - self.mission_req_table = {} - for campaign_id in slot_req_table: - campaign = lookup_id_to_campaign[int(campaign_id)] - self.mission_req_table[campaign] = { - mission: self.parse_mission_info(mission_info) - for mission, mission_info in slot_req_table[campaign_id].items() - } - else: - # Old format - self.mission_req_table = {SC2Campaign.GLOBAL: { - mission: self.parse_mission_info(mission_info) - for mission, mission_info in slot_req_table.items() - } - } - - self.mission_order = args["slot_data"].get("mission_order", MissionOrder.option_vanilla) - self.final_mission = args["slot_data"].get("final_mission", SC2Mission.ALL_IN.id) - self.player_color_raynor = args["slot_data"].get("player_color_terran_raynor", ColorChoice.option_blue) - self.player_color_zerg = args["slot_data"].get("player_color_zerg", ColorChoice.option_orange) - self.player_color_zerg_primal = args["slot_data"].get("player_color_zerg_primal", ColorChoice.option_purple) - self.player_color_protoss = args["slot_data"].get("player_color_protoss", ColorChoice.option_blue) - self.player_color_nova = args["slot_data"].get("player_color_nova", ColorChoice.option_dark_grey) - self.generic_upgrade_missions = args["slot_data"].get("generic_upgrade_missions", GenericUpgradeMissions.default) - self.generic_upgrade_items = args["slot_data"].get("generic_upgrade_items", GenericUpgradeItems.option_individual_items) - self.generic_upgrade_research = args["slot_data"].get("generic_upgrade_research", GenericUpgradeResearch.option_vanilla) - self.kerrigan_presence = args["slot_data"].get("kerrigan_presence", KerriganPresence.option_vanilla) - self.kerrigan_primal_status = args["slot_data"].get("kerrigan_primal_status", KerriganPrimalStatus.option_vanilla) - self.kerrigan_levels_per_mission_completed = args["slot_data"].get("kerrigan_levels_per_mission_completed", 0) - self.kerrigan_levels_per_mission_completed_cap = args["slot_data"].get("kerrigan_levels_per_mission_completed_cap", -1) - self.kerrigan_total_level_cap = args["slot_data"].get("kerrigan_total_level_cap", -1) - self.grant_story_tech = args["slot_data"].get("grant_story_tech", GrantStoryTech.option_false) - self.grant_story_levels = args["slot_data"].get("grant_story_levels", GrantStoryLevels.option_additive) - self.required_tactics = args["slot_data"].get("required_tactics", RequiredTactics.option_standard) - self.take_over_ai_allies = args["slot_data"].get("take_over_ai_allies", TakeOverAIAllies.option_false) - self.spear_of_adun_presence = args["slot_data"].get("spear_of_adun_presence", SpearOfAdunPresence.option_not_present) - self.spear_of_adun_present_in_no_build = args["slot_data"].get("spear_of_adun_present_in_no_build", SpearOfAdunPresentInNoBuild.option_false) - self.spear_of_adun_autonomously_cast_ability_presence = args["slot_data"].get("spear_of_adun_autonomously_cast_ability_presence", SpearOfAdunAutonomouslyCastAbilityPresence.option_not_present) - self.spear_of_adun_autonomously_cast_present_in_no_build = args["slot_data"].get("spear_of_adun_autonomously_cast_present_in_no_build", SpearOfAdunAutonomouslyCastPresentInNoBuild.option_false) - self.minerals_per_item = args["slot_data"].get("minerals_per_item", 15) - self.vespene_per_item = args["slot_data"].get("vespene_per_item", 15) - self.starting_supply_per_item = args["slot_data"].get("starting_supply_per_item", 2) - self.nova_covert_ops_only = args["slot_data"].get("nova_covert_ops_only", False) - - if self.required_tactics == RequiredTactics.option_no_logic: - # Locking Grant Story Tech/Levels if no logic - self.grant_story_tech = GrantStoryTech.option_true - self.grant_story_levels = GrantStoryLevels.option_minimum - - self.location_inclusions = { - LocationType.VICTORY: LocationInclusion.option_enabled, # Victory checks are always enabled - LocationType.VANILLA: args["slot_data"].get("vanilla_locations", VanillaLocations.default), - LocationType.EXTRA: args["slot_data"].get("extra_locations", ExtraLocations.default), - LocationType.CHALLENGE: args["slot_data"].get("challenge_locations", ChallengeLocations.default), - LocationType.MASTERY: args["slot_data"].get("mastery_locations", MasteryLocations.default), - } - self.plando_locations = args["slot_data"].get("plando_locations", []) - - self.build_location_to_mission_mapping() - - # Looks for the required maps and mods for SC2. Runs check_game_install_path. - maps_present = is_mod_installed_correctly() - if os.path.exists(get_metadata_file()): - with open(get_metadata_file(), "r") as f: - current_ver = f.read() - sc2_logger.debug(f"Current version: {current_ver}") - if is_mod_update_available(DATA_REPO_OWNER, DATA_REPO_NAME, DATA_API_VERSION, current_ver): - sc2_logger.info("NOTICE: Update for required files found. Run /download_data to install.") - elif maps_present: - sc2_logger.warning("NOTICE: Your map files may be outdated (version number not found). " - "Run /download_data to update them.") - - @staticmethod - def parse_mission_info(mission_info: dict[str, typing.Any]) -> MissionInfo: - if mission_info.get("id") is not None: - mission_info["mission"] = lookup_id_to_mission[mission_info["id"]] - elif isinstance(mission_info["mission"], int): - mission_info["mission"] = lookup_id_to_mission[mission_info["mission"]] - - return MissionInfo( - **{field: value for field, value in mission_info.items() if field in MissionInfo._fields} - ) - - def find_campaign(self, mission_name: str) -> SC2Campaign: - data = self.mission_req_table - for campaign in data.keys(): - if mission_name in data[campaign].keys(): - return campaign - sc2_logger.info(f"Attempted to find campaign of unknown mission '{mission_name}'; defaulting to GLOBAL") - return SC2Campaign.GLOBAL - - - - def on_print_json(self, args: dict) -> None: - # goes to this world - if "receiving" in args and self.slot_concerns_self(args["receiving"]): - relevant = True - # found in this world - elif "item" in args and self.slot_concerns_self(args["item"].player): - relevant = True - # not related - else: - relevant = False - - if relevant: - self.announcements.put(self.raw_text_parser(copy.deepcopy(args["data"]))) - - super(SC2Context, self).on_print_json(args) - - def run_gui(self) -> None: - from .ClientGui import start_gui - start_gui(self) - - - async def shutdown(self) -> None: - await super(SC2Context, self).shutdown() - if self.last_bot: - self.last_bot.want_close = True - if self.sc2_run_task: - self.sc2_run_task.cancel() - - def play_mission(self, mission_id: int) -> bool: - if self.missions_unlocked or is_mission_available(self, mission_id): - if self.sc2_run_task: - if not self.sc2_run_task.done(): - sc2_logger.warning("Starcraft 2 Client is still running!") - self.sc2_run_task.cancel() # doesn't actually close the game, just stops the python task - if self.slot is None: - sc2_logger.warning("Launching Mission without Archipelago authentication, " - "checks will not be registered to server.") - self.sc2_run_task = asyncio.create_task(starcraft_launch(self, mission_id), - name="Starcraft 2 Launch") - return True - else: - sc2_logger.info( - f"{lookup_id_to_mission[mission_id].mission_name} is not currently unlocked. " - f"Use /unfinished or /available to see what is available.") - return False - - def build_location_to_mission_mapping(self) -> None: - mission_id_to_location_ids: typing.Dict[int, typing.Set[int]] = { - mission_info.mission.id: set() for campaign_mission in self.mission_req_table.values() for mission_info in campaign_mission.values() - } - - for loc in self.server_locations: - offset = SC2WOL_LOC_ID_OFFSET if loc < SC2HOTS_LOC_ID_OFFSET \ - else (SC2HOTS_LOC_ID_OFFSET - SC2Mission.ALL_IN.id * VICTORY_MODULO) - mission_id, objective = divmod(loc - offset, VICTORY_MODULO) - mission_id_to_location_ids[mission_id].add(objective) - self.mission_id_to_location_ids = {mission_id: sorted(objectives) for mission_id, objectives in - mission_id_to_location_ids.items()} - - def locations_for_mission(self, mission_name: str): - mission = lookup_name_to_mission[mission_name] - mission_id: int = mission.id - objectives = self.mission_id_to_location_ids[mission_id] - for objective in objectives: - yield get_location_offset(mission_id) + mission_id * VICTORY_MODULO + objective - - -class CompatItemHolder(typing.NamedTuple): - name: str - quantity: int = 1 - - -async def main(): - multiprocessing.freeze_support() - parser = get_base_parser() - parser.add_argument('--name', default=None, help="Slot Name to connect as.") - args = parser.parse_args() - - ctx = SC2Context(args.connect, args.password) - ctx.auth = args.name - if ctx.server_task is None: - ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") - - if gui_enabled: - ctx.run_gui() - ctx.run_cli() - - await ctx.exit_event.wait() - - await ctx.shutdown() - -# These items must be given to the player if the game is generated on version 2 -API2_TO_API3_COMPAT_ITEMS: typing.Set[CompatItemHolder] = { - CompatItemHolder(ItemNames.PHOTON_CANNON), - CompatItemHolder(ItemNames.OBSERVER), - CompatItemHolder(ItemNames.WARP_HARMONIZATION), - CompatItemHolder(ItemNames.PROGRESSIVE_PROTOSS_GROUND_WEAPON, 3), - CompatItemHolder(ItemNames.PROGRESSIVE_PROTOSS_GROUND_ARMOR, 3), - CompatItemHolder(ItemNames.PROGRESSIVE_PROTOSS_SHIELDS, 3), - CompatItemHolder(ItemNames.PROGRESSIVE_PROTOSS_AIR_WEAPON, 3), - CompatItemHolder(ItemNames.PROGRESSIVE_PROTOSS_AIR_ARMOR, 3), - CompatItemHolder(ItemNames.PROGRESSIVE_PROTOSS_WEAPON_ARMOR_UPGRADE, 3) -} - - -def compat_item_to_network_items(compat_item: CompatItemHolder) -> typing.List[NetworkItem]: - item_id = get_full_item_list()[compat_item.name].code - network_item = NetworkItem(item_id, 0, 0, 0) - return compat_item.quantity * [network_item] - - -def calculate_items(ctx: SC2Context) -> typing.Dict[SC2Race, typing.List[int]]: - items = ctx.items_received.copy() - # Items unlocked in API2 by default (Prophecy default items) - if ctx.slot_data_version < 3: - for compat_item in API2_TO_API3_COMPAT_ITEMS: - items.extend(compat_item_to_network_items(compat_item)) - - network_item: NetworkItem - accumulators: typing.Dict[SC2Race, typing.List[int]] = {race: [0 for _ in type_flaggroups[race]] for race in SC2Race} - - # Protoss Shield grouped item specific logic - shields_from_ground_upgrade: int = 0 - shields_from_air_upgrade: int = 0 - - item_list = get_full_item_list() - for network_item in items: - name: str = lookup_id_to_name[network_item.item] - item_data: ItemData = item_list[name] - - # exists exactly once - if item_data.quantity == 1: - accumulators[item_data.race][type_flaggroups[item_data.race][item_data.type]] |= 1 << item_data.number - - # exists multiple times - elif item_data.type in ["Upgrade", "Progressive Upgrade","Progressive Upgrade 2"]: - flaggroup = type_flaggroups[item_data.race][item_data.type] - - # Generic upgrades apply only to Weapon / Armor upgrades - if item_data.type != "Upgrade" or ctx.generic_upgrade_items == 0: - accumulators[item_data.race][flaggroup] += 1 << item_data.number - else: - if name == ItemNames.PROGRESSIVE_PROTOSS_GROUND_UPGRADE: - shields_from_ground_upgrade += 1 - if name == ItemNames.PROGRESSIVE_PROTOSS_AIR_UPGRADE: - shields_from_air_upgrade += 1 - for bundled_number in upgrade_numbers[item_data.number]: - accumulators[item_data.race][flaggroup] += 1 << bundled_number - - # Regen bio-steel nerf with API3 - undo for older games - if ctx.slot_data_version < 3 and name == ItemNames.PROGRESSIVE_REGENERATIVE_BIO_STEEL: - current_level = (accumulators[item_data.race][flaggroup] >> item_data.number) % 4 - if current_level == 2: - # Switch from level 2 to level 3 for compatibility - accumulators[item_data.race][flaggroup] += 1 << item_data.number - # sum - else: - if name == ItemNames.STARTING_MINERALS: - accumulators[item_data.race][type_flaggroups[item_data.race][item_data.type]] += ctx.minerals_per_item - elif name == ItemNames.STARTING_VESPENE: - accumulators[item_data.race][type_flaggroups[item_data.race][item_data.type]] += ctx.vespene_per_item - elif name == ItemNames.STARTING_SUPPLY: - accumulators[item_data.race][type_flaggroups[item_data.race][item_data.type]] += ctx.starting_supply_per_item - else: - accumulators[item_data.race][type_flaggroups[item_data.race][item_data.type]] += item_data.number - - # Fix Shields from generic upgrades by unit class (Maximum of ground/air upgrades) - if shields_from_ground_upgrade > 0 or shields_from_air_upgrade > 0: - shield_upgrade_level = max(shields_from_ground_upgrade, shields_from_air_upgrade) - shield_upgrade_item = item_list[ItemNames.PROGRESSIVE_PROTOSS_SHIELDS] - for _ in range(0, shield_upgrade_level): - accumulators[shield_upgrade_item.race][type_flaggroups[shield_upgrade_item.race][shield_upgrade_item.type]] += 1 << shield_upgrade_item.number - - # Kerrigan levels per check - accumulators[SC2Race.ZERG][type_flaggroups[SC2Race.ZERG]["Level"]] += (len(ctx.checked_locations) // ctx.checks_per_level) * ctx.levels_per_check - - # Upgrades from completed missions - if ctx.generic_upgrade_missions > 0: - total_missions = sum(len(ctx.mission_req_table[campaign]) for campaign in ctx.mission_req_table) - for race in SC2Race: - if "Upgrade" not in type_flaggroups[race]: - continue - upgrade_flaggroup = type_flaggroups[race]["Upgrade"] - num_missions = ctx.generic_upgrade_missions * total_missions - amounts = [ - num_missions // 100, - 2 * num_missions // 100, - 3 * num_missions // 100 - ] - upgrade_count = 0 - completed = len([id for id in ctx.mission_id_to_location_ids if get_location_offset(id) + VICTORY_MODULO * id in ctx.checked_locations]) - for amount in amounts: - if completed >= amount: - upgrade_count += 1 - # Equivalent to "Progressive Weapon/Armor Upgrade" item - for bundled_number in upgrade_numbers[upgrade_numbers_all[race]]: - accumulators[race][upgrade_flaggroup] += upgrade_count << bundled_number - - return accumulators - - -def calc_difficulty(difficulty: int): - if difficulty == 0: - return 'C' - elif difficulty == 1: - return 'N' - elif difficulty == 2: - return 'H' - elif difficulty == 3: - return 'B' - - return 'X' - - -def get_kerrigan_level(ctx: SC2Context, items: typing.Dict[SC2Race, typing.List[int]], missions_beaten: int) -> int: - item_value = items[SC2Race.ZERG][type_flaggroups[SC2Race.ZERG]["Level"]] - mission_value = missions_beaten * ctx.kerrigan_levels_per_mission_completed - if ctx.kerrigan_levels_per_mission_completed_cap != -1: - mission_value = min(mission_value, ctx.kerrigan_levels_per_mission_completed_cap) - total_value = item_value + mission_value - if ctx.kerrigan_total_level_cap != -1: - total_value = min(total_value, ctx.kerrigan_total_level_cap) - return total_value - - -def calculate_kerrigan_options(ctx: SC2Context) -> int: - options = 0 - - # Bits 0, 1 - # Kerrigan unit available - if ctx.kerrigan_presence in kerrigan_unit_available: - options |= 1 << 0 - - # Bit 2 - # Kerrigan primal status by map - if ctx.kerrigan_primal_status == KerriganPrimalStatus.option_vanilla: - options |= 1 << 2 - - return options - - -def caclulate_soa_options(ctx: SC2Context) -> int: - options = 0 - - # Bits 0, 1 - # SoA Calldowns available - soa_presence_value = 0 - if ctx.spear_of_adun_presence == SpearOfAdunPresence.option_not_present: - soa_presence_value = 0 - elif ctx.spear_of_adun_presence == SpearOfAdunPresence.option_lotv_protoss: - soa_presence_value = 1 - elif ctx.spear_of_adun_presence == SpearOfAdunPresence.option_protoss: - soa_presence_value = 2 - elif ctx.spear_of_adun_presence == SpearOfAdunPresence.option_everywhere: - soa_presence_value = 3 - options |= soa_presence_value << 0 - - # Bit 2 - # SoA Calldowns for no-builds - if ctx.spear_of_adun_present_in_no_build == SpearOfAdunPresentInNoBuild.option_true: - options |= 1 << 2 - - # Bits 3,4 - # Autocasts - soa_autocasts_presence_value = 0 - if ctx.spear_of_adun_autonomously_cast_ability_presence == SpearOfAdunAutonomouslyCastAbilityPresence.option_not_present: - soa_autocasts_presence_value = 0 - elif ctx.spear_of_adun_autonomously_cast_ability_presence == SpearOfAdunAutonomouslyCastAbilityPresence.option_lotv_protoss: - soa_autocasts_presence_value = 1 - elif ctx.spear_of_adun_autonomously_cast_ability_presence == SpearOfAdunAutonomouslyCastAbilityPresence.option_protoss: - soa_autocasts_presence_value = 2 - elif ctx.spear_of_adun_autonomously_cast_ability_presence == SpearOfAdunAutonomouslyCastAbilityPresence.option_everywhere: - soa_autocasts_presence_value = 3 - options |= soa_autocasts_presence_value << 3 - - # Bit 5 - # Autocasts in no-builds - if ctx.spear_of_adun_autonomously_cast_present_in_no_build == SpearOfAdunAutonomouslyCastPresentInNoBuild.option_true: - options |= 1 << 5 - - return options - -def kerrigan_primal(ctx: SC2Context, kerrigan_level: int) -> bool: - if ctx.kerrigan_primal_status == KerriganPrimalStatus.option_always_zerg: - return True - elif ctx.kerrigan_primal_status == KerriganPrimalStatus.option_always_human: - return False - elif ctx.kerrigan_primal_status == KerriganPrimalStatus.option_level_35: - return kerrigan_level >= 35 - elif ctx.kerrigan_primal_status == KerriganPrimalStatus.option_half_completion: - total_missions = len(ctx.mission_id_to_location_ids) - completed = sum((mission_id * VICTORY_MODULO + get_location_offset(mission_id)) in ctx.checked_locations - for mission_id in ctx.mission_id_to_location_ids) - return completed >= (total_missions / 2) - elif ctx.kerrigan_primal_status == KerriganPrimalStatus.option_item: - codes = [item.item for item in ctx.items_received] - return get_full_item_list()[ItemNames.KERRIGAN_PRIMAL_FORM].code in codes - return False - -async def starcraft_launch(ctx: SC2Context, mission_id: int): - sc2_logger.info(f"Launching {lookup_id_to_mission[mission_id].mission_name}. If game does not launch check log file for errors.") - - with DllDirectory(None): - run_game(bot.maps.get(lookup_id_to_mission[mission_id].map_file), [Bot(Race.Terran, ArchipelagoBot(ctx, mission_id), - name="Archipelago", fullscreen=True)], realtime=True) - - -class ArchipelagoBot(bot.bot_ai.BotAI): - __slots__ = [ - 'game_running', - 'mission_completed', - 'boni', - 'setup_done', - 'ctx', - 'mission_id', - 'want_close', - 'can_read_game', - 'last_received_update', - ] - - def __init__(self, ctx: SC2Context, mission_id: int): - self.game_running = False - self.mission_completed = False - self.want_close = False - self.can_read_game = False - self.last_received_update: int = 0 - self.setup_done = False - self.ctx = ctx - self.ctx.last_bot = self - self.mission_id = mission_id - self.boni = [False for _ in range(MAX_BONUS)] - - super(ArchipelagoBot, self).__init__() - - async def on_step(self, iteration: int): - if self.want_close: - self.want_close = False - await self._client.leave() - return - game_state = 0 - if not self.setup_done: - self.setup_done = True - start_items = calculate_items(self.ctx) - missions_beaten = self.missions_beaten_count() - kerrigan_level = get_kerrigan_level(self.ctx, start_items, missions_beaten) - kerrigan_options = calculate_kerrigan_options(self.ctx) - soa_options = caclulate_soa_options(self.ctx) - if self.ctx.difficulty_override >= 0: - difficulty = calc_difficulty(self.ctx.difficulty_override) - else: - difficulty = calc_difficulty(self.ctx.difficulty) - if self.ctx.game_speed_override >= 0: - game_speed = self.ctx.game_speed_override - else: - game_speed = self.ctx.game_speed - await self.chat_send("?SetOptions {} {} {} {} {} {} {} {} {} {} {} {} {}".format( - difficulty, - self.ctx.generic_upgrade_research, - self.ctx.all_in_choice, - game_speed, - self.ctx.disable_forced_camera, - self.ctx.skip_cutscenes, - kerrigan_options, - self.ctx.grant_story_tech, - self.ctx.take_over_ai_allies, - soa_options, - self.ctx.mission_order, - 1 if self.ctx.nova_covert_ops_only else 0, - self.ctx.grant_story_levels - )) - await self.chat_send("?GiveResources {} {} {}".format( - start_items[SC2Race.ANY][0], - start_items[SC2Race.ANY][1], - start_items[SC2Race.ANY][2] - )) - await self.updateTerranTech(start_items) - await self.updateZergTech(start_items, kerrigan_level) - await self.updateProtossTech(start_items) - await self.updateColors() - await self.chat_send("?LoadFinished") - self.last_received_update = len(self.ctx.items_received) - - else: - if self.ctx.pending_color_update: - await self.updateColors() - - if not self.ctx.announcements.empty(): - message = self.ctx.announcements.get(timeout=1) - await self.chat_send("?SendMessage " + message) - self.ctx.announcements.task_done() - - # Archipelago reads the health - controller1_state = 0 - controller2_state = 0 - for unit in self.all_own_units(): - if unit.health_max == CONTROLLER_HEALTH: - controller1_state = int(CONTROLLER_HEALTH - unit.health) - self.can_read_game = True - elif unit.health_max == CONTROLLER2_HEALTH: - controller2_state = int(CONTROLLER2_HEALTH - unit.health) - self.can_read_game = True - game_state = controller1_state + (controller2_state << 15) - - if iteration == 160 and not game_state & 1: - await self.chat_send("?SendMessage Warning: Archipelago unable to connect or has lost connection to " + - "Starcraft 2 (This is likely a map issue)") - - if self.last_received_update < len(self.ctx.items_received): - current_items = calculate_items(self.ctx) - missions_beaten = self.missions_beaten_count() - kerrigan_level = get_kerrigan_level(self.ctx, current_items, missions_beaten) - await self.updateTerranTech(current_items) - await self.updateZergTech(current_items, kerrigan_level) - await self.updateProtossTech(current_items) - self.last_received_update = len(self.ctx.items_received) - - if game_state & 1: - if not self.game_running: - print("Archipelago Connected") - self.game_running = True - - if self.can_read_game: - if game_state & (1 << 1) and not self.mission_completed: - if self.mission_id != self.ctx.final_mission: - print("Mission Completed") - await self.ctx.send_msgs( - [{"cmd": 'LocationChecks', - "locations": [get_location_offset(self.mission_id) + VICTORY_MODULO * self.mission_id]}]) - self.mission_completed = True - else: - print("Game Complete") - await self.ctx.send_msgs([{"cmd": 'StatusUpdate', "status": ClientStatus.CLIENT_GOAL}]) - self.mission_completed = True - - for x, completed in enumerate(self.boni): - if not completed and game_state & (1 << (x + 2)): - await self.ctx.send_msgs( - [{"cmd": 'LocationChecks', - "locations": [get_location_offset(self.mission_id) + VICTORY_MODULO * self.mission_id + x + 1]}]) - self.boni[x] = True - else: - await self.chat_send("?SendMessage LostConnection - Lost connection to game.") - - def missions_beaten_count(self): - return len([location for location in self.ctx.checked_locations if location % VICTORY_MODULO == 0]) - - async def updateColors(self): - await self.chat_send("?SetColor rr " + str(self.ctx.player_color_raynor)) - await self.chat_send("?SetColor ks " + str(self.ctx.player_color_zerg)) - await self.chat_send("?SetColor pz " + str(self.ctx.player_color_zerg_primal)) - await self.chat_send("?SetColor da " + str(self.ctx.player_color_protoss)) - await self.chat_send("?SetColor nova " + str(self.ctx.player_color_nova)) - self.ctx.pending_color_update = False - - async def updateTerranTech(self, current_items): - terran_items = current_items[SC2Race.TERRAN] - await self.chat_send("?GiveTerranTech {} {} {} {} {} {} {} {} {} {} {} {} {} {}".format( - terran_items[0], terran_items[1], terran_items[2], terran_items[3], terran_items[4], - terran_items[5], terran_items[6], terran_items[7], terran_items[8], terran_items[9], terran_items[10], - terran_items[11], terran_items[12], terran_items[13])) - - async def updateZergTech(self, current_items, kerrigan_level): - zerg_items = current_items[SC2Race.ZERG] - kerrigan_primal_by_items = kerrigan_primal(self.ctx, kerrigan_level) - kerrigan_primal_bot_value = 1 if kerrigan_primal_by_items else 0 - await self.chat_send("?GiveZergTech {} {} {} {} {} {} {} {} {} {} {} {}".format( - kerrigan_level, kerrigan_primal_bot_value, zerg_items[0], zerg_items[1], zerg_items[2], - zerg_items[3], zerg_items[4], zerg_items[5], zerg_items[6], zerg_items[9], zerg_items[10], zerg_items[11] - )) - - async def updateProtossTech(self, current_items): - protoss_items = current_items[SC2Race.PROTOSS] - await self.chat_send("?GiveProtossTech {} {} {} {} {} {} {} {} {} {}".format( - protoss_items[0], protoss_items[1], protoss_items[2], protoss_items[3], protoss_items[4], - protoss_items[5], protoss_items[6], protoss_items[7], protoss_items[8], protoss_items[9] - )) - - -def request_unfinished_missions(ctx: SC2Context) -> None: - if ctx.mission_req_table: - message = "Unfinished Missions: " - unlocks = initialize_blank_mission_dict(ctx.mission_req_table) - unfinished_locations: typing.Dict[SC2Mission, typing.List[str]] = {} - - _, unfinished_missions = calc_unfinished_missions(ctx, unlocks=unlocks) - - for mission in unfinished_missions: - objectives = set(ctx.locations_for_mission(mission)) - if objectives: - remaining_objectives = objectives.difference(ctx.checked_locations) - unfinished_locations[mission] = [ctx.location_names.lookup_in_game(location_id) for location_id in remaining_objectives] - else: - unfinished_locations[mission] = [] - - # Removing All-In from location pool - final_mission = lookup_id_to_mission[ctx.final_mission] - if final_mission in unfinished_missions.keys(): - message = f"Final Mission Available: {final_mission}[{ctx.final_mission}]\n" + message - if unfinished_missions[final_mission] == -1: - unfinished_missions.pop(final_mission) - - message += ", ".join(f"{mark_up_mission_name(ctx, mission, unlocks)}[{ctx.mission_req_table[ctx.find_campaign(mission)][mission].mission.id}] " + - mark_up_objectives( - f"[{len(unfinished_missions[mission])}/" - f"{sum(1 for _ in ctx.locations_for_mission(mission))}]", - ctx, unfinished_locations, mission) - for mission in unfinished_missions) - - if ctx.ui: - ctx.ui.log_panels['All'].on_message_markup(message) - ctx.ui.log_panels['Starcraft2'].on_message_markup(message) - else: - sc2_logger.info(message) - else: - sc2_logger.warning("No mission table found, you are likely not connected to a server.") - - -def calc_unfinished_missions(ctx: SC2Context, unlocks: typing.Optional[typing.Dict] = None): - unfinished_missions: typing.List[str] = [] - locations_completed: typing.List[typing.Union[typing.Set[int], typing.Literal[-1]]] = [] - - if not unlocks: - unlocks = initialize_blank_mission_dict(ctx.mission_req_table) - - available_missions = calc_available_missions(ctx, unlocks) - - for name in available_missions: - objectives = set(ctx.locations_for_mission(name)) - if objectives: - objectives_completed = ctx.checked_locations & objectives - if len(objectives_completed) < len(objectives): - unfinished_missions.append(name) - locations_completed.append(objectives_completed) - - else: # infer that this is the final mission as it has no objectives - unfinished_missions.append(name) - locations_completed.append(-1) - - return available_missions, dict(zip(unfinished_missions, locations_completed)) - - -def is_mission_available(ctx: SC2Context, mission_id_to_check: int) -> bool: - unfinished_missions = calc_available_missions(ctx) - - return any(mission_id_to_check == ctx.mission_req_table[ctx.find_campaign(mission)][mission].mission.id for mission in unfinished_missions) - - -def mark_up_mission_name(ctx: SC2Context, mission_name: str, unlock_table: typing.Dict) -> str: - """Checks if the mission is required for game completion and adds '*' to the name to mark that.""" - - campaign = ctx.find_campaign(mission_name) - mission_info = ctx.mission_req_table[campaign][mission_name] - if mission_info.completion_critical: - if ctx.ui: - message = "[color=AF99EF]" + mission_name + "[/color]" - else: - message = "*" + mission_name + "*" - else: - message = mission_name - - if ctx.ui: - campaign_missions = list(ctx.mission_req_table[campaign].keys()) - unlocks: typing.List[str] - index = campaign_missions.index(mission_name) - if index in unlock_table[campaign]: - unlocks = unlock_table[campaign][index] - else: - unlocks = [] - - if len(unlocks) > 0: - pre_message = f"[ref={mission_info.mission.id}|Unlocks: " - pre_message += ", ".join(f"{unlock}({ctx.mission_req_table[ctx.find_campaign(unlock)][unlock].mission.id})" for unlock in unlocks) - pre_message += f"]" - message = pre_message + message + "[/ref]" - - return message - - -def mark_up_objectives(message, ctx, unfinished_locations, mission): - formatted_message = message - - if ctx.ui: - locations = unfinished_locations[mission] - campaign = ctx.find_campaign(mission) - - pre_message = f"[ref={list(ctx.mission_req_table[campaign]).index(mission) + 30}|" - pre_message += "
".join(location for location in locations) - pre_message += f"]" - formatted_message = pre_message + message + "[/ref]" - - return formatted_message - - -def request_available_missions(ctx: SC2Context): - if ctx.mission_req_table: - message = "Available Missions: " - - # Initialize mission unlock table - unlocks = initialize_blank_mission_dict(ctx.mission_req_table) - - missions = calc_available_missions(ctx, unlocks) - message += \ - ", ".join(f"{mark_up_mission_name(ctx, mission, unlocks)}" - f"[{ctx.mission_req_table[ctx.find_campaign(mission)][mission].mission.id}]" - for mission in missions) - - if ctx.ui: - ctx.ui.log_panels['All'].on_message_markup(message) - ctx.ui.log_panels['Starcraft2'].on_message_markup(message) - else: - sc2_logger.info(message) - else: - sc2_logger.warning("No mission table found, you are likely not connected to a server.") - - -def calc_available_missions(ctx: SC2Context, unlocks: typing.Optional[dict] = None) -> typing.List[str]: - available_missions: typing.List[str] = [] - missions_complete = 0 - - # Get number of missions completed - for loc in ctx.checked_locations: - if loc % VICTORY_MODULO == 0: - missions_complete += 1 - - for campaign in ctx.mission_req_table: - # Go through the required missions for each mission and fill up unlock table used later for hover-over tooltips - for mission_name in ctx.mission_req_table[campaign]: - if unlocks: - for unlock in ctx.mission_req_table[campaign][mission_name].required_world: - parsed_unlock = parse_unlock(unlock) - # TODO prophecy-only wants to connect to WoL here - index = parsed_unlock.connect_to - 1 - unlock_mission = list(ctx.mission_req_table[parsed_unlock.campaign])[index] - unlock_campaign = ctx.find_campaign(unlock_mission) - if unlock_campaign in unlocks: - if index not in unlocks[unlock_campaign]: - unlocks[unlock_campaign][index] = list() - unlocks[unlock_campaign][index].append(mission_name) - - if mission_reqs_completed(ctx, mission_name, missions_complete): - available_missions.append(mission_name) - - return available_missions - - -def parse_unlock(unlock: typing.Union[typing.Dict[typing.Literal["connect_to", "campaign"], int], MissionConnection, int]) -> MissionConnection: - if isinstance(unlock, int): - # Legacy - return MissionConnection(unlock) - elif isinstance(unlock, MissionConnection): - return unlock - else: - # Multi-campaign - return MissionConnection(unlock["connect_to"], lookup_id_to_campaign[unlock["campaign"]]) - - -def mission_reqs_completed(ctx: SC2Context, mission_name: str, missions_complete: int) -> bool: - """Returns a bool signifying if the mission has all requirements complete and can be done - - Arguments: - ctx -- instance of SC2Context - locations_to_check -- the mission string name to check - missions_complete -- an int of how many missions have been completed - mission_path -- a list of missions that have already been checked - """ - campaign = ctx.find_campaign(mission_name) - - if len(ctx.mission_req_table[campaign][mission_name].required_world) >= 1: - # A check for when the requirements are being or'd - or_success = False - - # Loop through required missions - for req_mission in ctx.mission_req_table[campaign][mission_name].required_world: - req_success = True - parsed_req_mission = parse_unlock(req_mission) - - # Check if required mission has been completed - mission_id = ctx.mission_req_table[parsed_req_mission.campaign][ - list(ctx.mission_req_table[parsed_req_mission.campaign])[parsed_req_mission.connect_to - 1]].mission.id - if not (mission_id * VICTORY_MODULO + get_location_offset(mission_id)) in ctx.checked_locations: - if not ctx.mission_req_table[campaign][mission_name].or_requirements: - return False - else: - req_success = False - - # Grid-specific logic (to avoid long path checks and infinite recursion) - if ctx.mission_order in (MissionOrder.option_grid, MissionOrder.option_mini_grid, MissionOrder.option_medium_grid): - if req_success: - return True - else: - if parsed_req_mission == ctx.mission_req_table[campaign][mission_name].required_world[-1]: - return False - else: - continue - - # Recursively check required mission to see if it's requirements are met, in case !collect has been done - # Skipping recursive check on Grid settings to speed up checks and avoid infinite recursion - if not mission_reqs_completed(ctx, list(ctx.mission_req_table[parsed_req_mission.campaign])[parsed_req_mission.connect_to - 1], missions_complete): - if not ctx.mission_req_table[campaign][mission_name].or_requirements: - return False - else: - req_success = False - - # If requirement check succeeded mark or as satisfied - if ctx.mission_req_table[campaign][mission_name].or_requirements and req_success: - or_success = True - - if ctx.mission_req_table[campaign][mission_name].or_requirements: - # Return false if or requirements not met - if not or_success: - return False - - # Check number of missions - if missions_complete >= ctx.mission_req_table[campaign][mission_name].number: - return True - else: - return False - else: - return True - - -def initialize_blank_mission_dict(location_table: typing.Dict[SC2Campaign, typing.Dict[str, MissionInfo]]): - unlocks: typing.Dict[SC2Campaign, typing.Dict] = {} - - for mission in list(location_table): - unlocks[mission] = {} - - return unlocks - - -def check_game_install_path() -> bool: - # First thing: go to the default location for ExecuteInfo. - # An exception for Windows is included because it's very difficult to find ~\Documents if the user moved it. - if is_windows: - # The next five lines of utterly inscrutable code are brought to you by copy-paste from Stack Overflow. - # https://stackoverflow.com/questions/6227590/finding-the-users-my-documents-path/30924555# - import ctypes.wintypes - CSIDL_PERSONAL = 5 # My Documents - SHGFP_TYPE_CURRENT = 0 # Get current, not default value - - buf = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH) - ctypes.windll.shell32.SHGetFolderPathW(None, CSIDL_PERSONAL, None, SHGFP_TYPE_CURRENT, buf) - documentspath: str = buf.value - einfo = str(documentspath / Path("StarCraft II\\ExecuteInfo.txt")) - else: - einfo = str(bot.paths.get_home() / Path(bot.paths.USERPATH[bot.paths.PF])) - - # Check if the file exists. - if os.path.isfile(einfo): - - # Open the file and read it, picking out the latest executable's path. - with open(einfo) as f: - content = f.read() - if content: - search_result = re.search(r" = (.*)Versions", content) - if not search_result: - sc2_logger.warning(f"Found {einfo}, but it was empty. Run SC2 through the Blizzard launcher, " - "then try again.") - return False - base = search_result.group(1) - - if os.path.exists(base): - executable = bot.paths.latest_executeble(Path(base).expanduser() / "Versions") - - # Finally, check the path for an actual executable. - # If we find one, great. Set up the SC2PATH. - if os.path.isfile(executable): - sc2_logger.info(f"Found an SC2 install at {base}!") - sc2_logger.debug(f"Latest executable at {executable}.") - os.environ["SC2PATH"] = base - sc2_logger.debug(f"SC2PATH set to {base}.") - return True - else: - sc2_logger.warning(f"We may have found an SC2 install at {base}, but couldn't find {executable}.") - else: - sc2_logger.warning(f"{einfo} pointed to {base}, but we could not find an SC2 install there.") - else: - sc2_logger.warning(f"Couldn't find {einfo}. Run SC2 through the Blizzard launcher, then try again. " - f"If that fails, please run /set_path with your SC2 install directory.") - return False - - -def is_mod_installed_correctly() -> bool: - """Searches for all required files.""" - if "SC2PATH" not in os.environ: - check_game_install_path() - sc2_path: str = os.environ["SC2PATH"] - mapdir = sc2_path / Path('Maps/ArchipelagoCampaign') - mods = ["ArchipelagoCore", "ArchipelagoPlayer", "ArchipelagoPlayerSuper", "ArchipelagoPatches", - "ArchipelagoTriggers", "ArchipelagoPlayerWoL", "ArchipelagoPlayerHotS", - "ArchipelagoPlayerLotV", "ArchipelagoPlayerLotVPrologue", "ArchipelagoPlayerNCO"] - modfiles = [sc2_path / Path("Mods/" + mod + ".SC2Mod") for mod in mods] - wol_required_maps: typing.List[str] = ["WoL" + os.sep + mission.map_file + ".SC2Map" for mission in SC2Mission - if mission.campaign in (SC2Campaign.WOL, SC2Campaign.PROPHECY)] - hots_required_maps: typing.List[str] = ["HotS" + os.sep + mission.map_file + ".SC2Map" for mission in campaign_mission_table[SC2Campaign.HOTS]] - lotv_required_maps: typing.List[str] = ["LotV" + os.sep + mission.map_file + ".SC2Map" for mission in SC2Mission - if mission.campaign in (SC2Campaign.LOTV, SC2Campaign.PROLOGUE, SC2Campaign.EPILOGUE)] - nco_required_maps: typing.List[str] = ["NCO" + os.sep + mission.map_file + ".SC2Map" for mission in campaign_mission_table[SC2Campaign.NCO]] - required_maps = wol_required_maps + hots_required_maps + lotv_required_maps + nco_required_maps - needs_files = False - - # Check for maps. - missing_maps: typing.List[str] = [] - for mapfile in required_maps: - if not os.path.isfile(mapdir / mapfile): - missing_maps.append(mapfile) - if len(missing_maps) >= 19: - sc2_logger.warning(f"All map files missing from {mapdir}.") - needs_files = True - elif len(missing_maps) > 0: - for map in missing_maps: - sc2_logger.debug(f"Missing {map} from {mapdir}.") - sc2_logger.warning(f"Missing {len(missing_maps)} map files.") - needs_files = True - else: # Must be no maps missing - sc2_logger.info(f"All maps found in {mapdir}.") - - # Check for mods. - for modfile in modfiles: - if os.path.isfile(modfile) or os.path.isdir(modfile): - sc2_logger.info(f"Archipelago mod found at {modfile}.") - else: - sc2_logger.warning(f"Archipelago mod could not be found at {modfile}.") - needs_files = True - - # Final verdict. - if needs_files: - sc2_logger.warning(f"Required files are missing. Run /download_data to acquire them.") - return False - else: - sc2_logger.debug(f"All map/mod files are properly installed.") - return True - - -class DllDirectory: - # Credit to Black Sliver for this code. - # More info: https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setdlldirectoryw - _old: typing.Optional[str] = None - _new: typing.Optional[str] = None - - def __init__(self, new: typing.Optional[str]): - self._new = new - - def __enter__(self): - old = self.get() - if self.set(self._new): - self._old = old - - def __exit__(self, *args): - if self._old is not None: - self.set(self._old) - - @staticmethod - def get() -> typing.Optional[str]: - if sys.platform == "win32": - n = ctypes.windll.kernel32.GetDllDirectoryW(0, None) - buf = ctypes.create_unicode_buffer(n) - ctypes.windll.kernel32.GetDllDirectoryW(n, buf) - return buf.value - # NOTE: other OS may support os.environ["LD_LIBRARY_PATH"], but this fix is windows-specific - return None - - @staticmethod - def set(s: typing.Optional[str]) -> bool: - if sys.platform == "win32": - return ctypes.windll.kernel32.SetDllDirectoryW(s) != 0 - # NOTE: other OS may support os.environ["LD_LIBRARY_PATH"], but this fix is windows-specific - return False - - -def download_latest_release_zip( - owner: str, - repo: str, - api_version: str, - metadata: typing.Optional[str] = None, - force_download=False -) -> typing.Tuple[str, typing.Optional[str]]: - """Downloads the latest release of a GitHub repo to the current directory as a .zip file.""" - import requests - - headers = {"Accept": 'application/vnd.github.v3+json'} - url = f"https://api.github.com/repos/{owner}/{repo}/releases/tags/{api_version}" - - r1 = requests.get(url, headers=headers) - if r1.status_code == 200: - latest_metadata = r1.json() - cleanup_downloaded_metadata(latest_metadata) - latest_metadata = str(latest_metadata) - # sc2_logger.info(f"Latest version: {latest_metadata}.") - else: - sc2_logger.warning(f"Status code: {r1.status_code}") - sc2_logger.warning(f"Failed to reach GitHub. Could not find download link.") - sc2_logger.warning(f"text: {r1.text}") - return "", metadata - - if (force_download is False) and (metadata == latest_metadata): - sc2_logger.info("Latest version already installed.") - return "", metadata - - sc2_logger.info(f"Attempting to download latest version of API version {api_version} of {repo}.") - download_url = r1.json()["assets"][0]["browser_download_url"] - - r2 = requests.get(download_url, headers=headers) - if r2.status_code == 200 and zipfile.is_zipfile(io.BytesIO(r2.content)): - tempdir = tempfile.gettempdir() - file = tempdir + os.sep + f"{repo}.zip" - with open(file, "wb") as fh: - fh.write(r2.content) - sc2_logger.info(f"Successfully downloaded {repo}.zip.") - return file, latest_metadata - else: - sc2_logger.warning(f"Status code: {r2.status_code}") - sc2_logger.warning("Download failed.") - sc2_logger.warning(f"text: {r2.text}") - return "", metadata - - -def cleanup_downloaded_metadata(medatada_json: dict) -> None: - for asset in medatada_json['assets']: - del asset['download_count'] - - -def is_mod_update_available(owner: str, repo: str, api_version: str, metadata: str) -> bool: - import requests - - headers = {"Accept": 'application/vnd.github.v3+json'} - url = f"https://api.github.com/repos/{owner}/{repo}/releases/tags/{api_version}" - - r1 = requests.get(url, headers=headers) - if r1.status_code == 200: - latest_metadata = r1.json() - cleanup_downloaded_metadata(latest_metadata) - latest_metadata = str(latest_metadata) - if metadata != latest_metadata: - return True - else: - return False - - else: - sc2_logger.warning(f"Failed to reach GitHub while checking for updates.") - sc2_logger.warning(f"Status code: {r1.status_code}") - sc2_logger.warning(f"text: {r1.text}") - return False - - -def get_location_offset(mission_id): - return SC2WOL_LOC_ID_OFFSET if mission_id <= SC2Mission.ALL_IN.id \ - else (SC2HOTS_LOC_ID_OFFSET - SC2Mission.ALL_IN.id * VICTORY_MODULO) - - -def launch(): - colorama.just_fix_windows_console() - asyncio.run(main()) - colorama.deinit() diff --git a/worlds/sc2/ClientGui.py b/worlds/sc2/ClientGui.py deleted file mode 100644 index d16acad8..00000000 --- a/worlds/sc2/ClientGui.py +++ /dev/null @@ -1,306 +0,0 @@ -from typing import * -import asyncio - -from NetUtils import JSONMessagePart -from kvui import GameManager, HoverBehavior, ServerToolTip, KivyJSONtoTextParser -from kivy.app import App -from kivy.clock import Clock -from kivy.uix.gridlayout import GridLayout -from kivy.lang import Builder -from kivy.uix.label import Label -from kivy.uix.button import Button -from kivymd.uix.tooltip import MDTooltip -from kivy.uix.scrollview import ScrollView -from kivy.properties import StringProperty - -from .Client import SC2Context, calc_unfinished_missions, parse_unlock -from .MissionTables import (lookup_id_to_mission, lookup_name_to_mission, campaign_race_exceptions, SC2Mission, SC2Race, - SC2Campaign) -from .Locations import LocationType, lookup_location_id_to_type -from .Options import LocationInclusion -from . import SC2World, get_first_mission - - -class HoverableButton(HoverBehavior, Button): - pass - - -class MissionButton(HoverableButton, MDTooltip): - tooltip_text = StringProperty("Test") - - def __init__(self, *args, **kwargs): - super(HoverableButton, self).__init__(**kwargs) - self._tooltip = ServerToolTip(text=self.text, markup=True) - self._tooltip.padding = [5, 2, 5, 2] - - def on_enter(self): - self._tooltip.text = self.tooltip_text - - if self.tooltip_text != "": - self.display_tooltip() - - def on_leave(self): - self.remove_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 SC2JSONtoKivyParser(KivyJSONtoTextParser): - def _handle_text(self, node: JSONMessagePart): - if node.get("keep_markup", False): - for ref in node.get("refs", []): - node["text"] = f"[ref={self.ref_count}|{ref}]{node['text']}[/ref]" - self.ref_count += 1 - return super(KivyJSONtoTextParser, self)._handle_text(node) - else: - return super()._handle_text(node) - - -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) - self.json_to_kivy_parser = SC2JSONtoKivyParser(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 = self.add_client_tab("Starcraft 2 Launcher", CampaignScroll()) - self.campaign_panel = MultiCampaignLayout() - panel.content.add_widget(self.campaign_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.lookup_in_game(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) diff --git a/worlds/sc2/ItemGroups.py b/worlds/sc2/ItemGroups.py deleted file mode 100644 index 3a373304..00000000 --- a/worlds/sc2/ItemGroups.py +++ /dev/null @@ -1,100 +0,0 @@ -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, -] \ No newline at end of file diff --git a/worlds/sc2/ItemNames.py b/worlds/sc2/ItemNames.py deleted file mode 100644 index 10c71391..00000000 --- a/worlds/sc2/ItemNames.py +++ /dev/null @@ -1,661 +0,0 @@ -""" -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" diff --git a/worlds/sc2/Items.py b/worlds/sc2/Items.py deleted file mode 100644 index ee1f34d7..00000000 --- a/worlds/sc2/Items.py +++ /dev/null @@ -1,2554 +0,0 @@ -import inspect -from pydoc import describe - -from BaseClasses import Item, ItemClassification, MultiWorld -import typing - -from .Options import get_option_value, RequiredTactics -from .MissionTables import SC2Mission, SC2Race, SC2Campaign, campaign_mission_table -from . import ItemNames -from worlds.AutoWorld import World - - -class ItemData(typing.NamedTuple): - code: int - type: str - number: int # Important for bot commands to send the item into the game - race: SC2Race - classification: ItemClassification = ItemClassification.useful - quantity: int = 1 - parent_item: typing.Optional[str] = None - origin: typing.Set[str] = {"wol"} - description: typing.Optional[str] = None - important_for_filtering: bool = False - - def is_important_for_filtering(self): - return self.important_for_filtering \ - or self.classification == ItemClassification.progression \ - or self.classification == ItemClassification.progression_skip_balancing - - -class StarcraftItem(Item): - game: str = "Starcraft 2" - - -def get_full_item_list(): - return item_table - - -SC2WOL_ITEM_ID_OFFSET = 1000 -SC2HOTS_ITEM_ID_OFFSET = SC2WOL_ITEM_ID_OFFSET + 1000 -SC2LOTV_ITEM_ID_OFFSET = SC2HOTS_ITEM_ID_OFFSET + 1000 - -# Descriptions -WEAPON_ARMOR_UPGRADE_NOTE = inspect.cleandoc(""" - Must be researched during the mission if the mission type isn't set to auto-unlock generic upgrades. -""") -LASER_TARGETING_SYSTEMS_DESCRIPTION = "Increases vision by 2 and weapon range by 1." -STIMPACK_SMALL_COST = 10 -STIMPACK_SMALL_HEAL = 30 -STIMPACK_LARGE_COST = 20 -STIMPACK_LARGE_HEAL = 60 -STIMPACK_TEMPLATE = inspect.cleandoc(""" - Level 1: Stimpack: Increases unit movement and attack speed for 15 seconds. Injures the unit for {} life. - Level 2: Super Stimpack: Instead of injuring the unit, heals the unit for {} life instead. -""") -STIMPACK_SMALL_DESCRIPTION = STIMPACK_TEMPLATE.format(STIMPACK_SMALL_COST, STIMPACK_SMALL_HEAL) -STIMPACK_LARGE_DESCRIPTION = STIMPACK_TEMPLATE.format(STIMPACK_LARGE_COST, STIMPACK_LARGE_HEAL) -SMART_SERVOS_DESCRIPTION = "Increases transformation speed between modes." -INTERNAL_TECH_MODULE_DESCRIPTION_TEMPLATE = "{} can be trained from a {} without an attached Tech Lab." -RESOURCE_EFFICIENCY_DESCRIPTION_TEMPLATE = "Reduces {} resource and supply cost." -RESOURCE_EFFICIENCY_NO_SUPPLY_DESCRIPTION_TEMPLATE = "Reduces {} resource cost." -CLOAK_DESCRIPTION_TEMPLATE = "Allows {} to use the Cloak ability." - - -# The items are sorted by their IDs. The IDs shall be kept for compatibility with older games. -item_table = { - # WoL - ItemNames.MARINE: - ItemData(0 + SC2WOL_ITEM_ID_OFFSET, "Unit", 0, SC2Race.TERRAN, - classification=ItemClassification.progression, - description="General-purpose infantry."), - ItemNames.MEDIC: - ItemData(1 + SC2WOL_ITEM_ID_OFFSET, "Unit", 1, SC2Race.TERRAN, - classification=ItemClassification.progression, - description="Support trooper. Heals nearby biological units."), - ItemNames.FIREBAT: - ItemData(2 + SC2WOL_ITEM_ID_OFFSET, "Unit", 2, SC2Race.TERRAN, - classification=ItemClassification.progression, - description="Specialized anti-infantry attacker."), - ItemNames.MARAUDER: - ItemData(3 + SC2WOL_ITEM_ID_OFFSET, "Unit", 3, SC2Race.TERRAN, - classification=ItemClassification.progression, - description="Heavy assault infantry."), - ItemNames.REAPER: - ItemData(4 + SC2WOL_ITEM_ID_OFFSET, "Unit", 4, SC2Race.TERRAN, - classification=ItemClassification.progression, - description="Raider. Capable of jumping up and down cliffs. Throws explosive mines."), - ItemNames.HELLION: - ItemData(5 + SC2WOL_ITEM_ID_OFFSET, "Unit", 5, SC2Race.TERRAN, - classification=ItemClassification.progression, - description="Fast scout. Has a flame attack that damages all enemy units in its line of fire."), - ItemNames.VULTURE: - ItemData(6 + SC2WOL_ITEM_ID_OFFSET, "Unit", 6, SC2Race.TERRAN, - classification=ItemClassification.progression, - description="Fast skirmish unit. Can use the Spider Mine ability."), - ItemNames.GOLIATH: - ItemData(7 + SC2WOL_ITEM_ID_OFFSET, "Unit", 7, SC2Race.TERRAN, - classification=ItemClassification.progression, - description="Heavy-fire support unit."), - ItemNames.DIAMONDBACK: - ItemData(8 + SC2WOL_ITEM_ID_OFFSET, "Unit", 8, SC2Race.TERRAN, - classification=ItemClassification.progression, - description="Fast, high-damage hovertank. Rail Gun can fire while the Diamondback is moving."), - ItemNames.SIEGE_TANK: - ItemData(9 + SC2WOL_ITEM_ID_OFFSET, "Unit", 9, SC2Race.TERRAN, - classification=ItemClassification.progression, - description="Heavy tank. Long-range artillery in Siege Mode."), - ItemNames.MEDIVAC: - ItemData(10 + SC2WOL_ITEM_ID_OFFSET, "Unit", 10, SC2Race.TERRAN, - classification=ItemClassification.progression, - description="Air transport. Heals nearby biological units."), - ItemNames.WRAITH: - ItemData(11 + SC2WOL_ITEM_ID_OFFSET, "Unit", 11, SC2Race.TERRAN, - classification=ItemClassification.progression, - description="Highly mobile flying unit. Excellent at surgical strikes."), - ItemNames.VIKING: - ItemData(12 + SC2WOL_ITEM_ID_OFFSET, "Unit", 12, SC2Race.TERRAN, - classification=ItemClassification.progression, - description=inspect.cleandoc( - """ - Durable support flyer. Loaded with strong anti-capital air missiles. - Can switch into Assault Mode to attack ground units. - """ - )), - ItemNames.BANSHEE: - ItemData(13 + SC2WOL_ITEM_ID_OFFSET, "Unit", 13, SC2Race.TERRAN, - classification=ItemClassification.progression, - description="Tactical-strike aircraft."), - ItemNames.BATTLECRUISER: - ItemData(14 + SC2WOL_ITEM_ID_OFFSET, "Unit", 14, SC2Race.TERRAN, - classification=ItemClassification.progression, - description="Powerful warship."), - ItemNames.GHOST: - ItemData(15 + SC2WOL_ITEM_ID_OFFSET, "Unit", 15, SC2Race.TERRAN, - classification=ItemClassification.progression, - description=inspect.cleandoc( - """ - Infiltration unit. Can use Snipe and Cloak abilities. Can also call down Tactical Nukes. - """ - )), - ItemNames.SPECTRE: - ItemData(16 + SC2WOL_ITEM_ID_OFFSET, "Unit", 16, SC2Race.TERRAN, - classification=ItemClassification.progression, - description=inspect.cleandoc( - """ - Infiltration unit. Can use Ultrasonic Pulse, Psionic Lash, and Cloak. - Can also call down Tactical Nukes. - """ - )), - ItemNames.THOR: - ItemData(17 + SC2WOL_ITEM_ID_OFFSET, "Unit", 17, SC2Race.TERRAN, - classification=ItemClassification.progression, - description="Heavy assault mech."), - # EE units - ItemNames.LIBERATOR: - ItemData(18 + SC2WOL_ITEM_ID_OFFSET, "Unit", 18, SC2Race.TERRAN, - classification=ItemClassification.progression, origin={"nco", "ext"}, - description=inspect.cleandoc( - """ - Artillery fighter. Loaded with missiles that deal area damage to enemy air targets. - Can switch into Defender Mode to provide siege support. - """ - )), - ItemNames.VALKYRIE: - ItemData(19 + SC2WOL_ITEM_ID_OFFSET, "Unit", 19, SC2Race.TERRAN, - classification=ItemClassification.progression, origin={"bw"}, - description=inspect.cleandoc( - """ - Advanced anti-aircraft fighter. - Able to use cluster missiles that deal area damage to air targets. - """ - )), - ItemNames.WIDOW_MINE: - ItemData(20 + SC2WOL_ITEM_ID_OFFSET, "Unit", 20, SC2Race.TERRAN, - classification=ItemClassification.progression, origin={"ext"}, - description=inspect.cleandoc( - """ - Robotic mine. Launches missiles at nearby enemy units while burrowed. - Attacks deal splash damage in a small area around the target. - Widow Mine is revealed when Sentinel Missile is on cooldown. - """ - )), - ItemNames.CYCLONE: - ItemData(21 + SC2WOL_ITEM_ID_OFFSET, "Unit", 21, SC2Race.TERRAN, - classification=ItemClassification.progression, origin={"ext"}, - description=inspect.cleandoc( - """ - Mobile assault vehicle. Can use Lock On to quickly fire while moving. - """ - )), - ItemNames.HERC: - ItemData(22 + SC2WOL_ITEM_ID_OFFSET, "Unit", 26, SC2Race.TERRAN, - classification=ItemClassification.progression, origin={"ext"}, - description=inspect.cleandoc( - """ - Front-line infantry. Can use Grapple. - """ - )), - ItemNames.WARHOUND: - ItemData(23 + SC2WOL_ITEM_ID_OFFSET, "Unit", 27, SC2Race.TERRAN, - classification=ItemClassification.progression, origin={"ext"}, - description=inspect.cleandoc( - """ - Anti-vehicle mech. Haywire missiles do bonus damage to mechanical units. - """ - )), - - # Some other items are moved to Upgrade group because of the way how the bot message is parsed - ItemNames.PROGRESSIVE_TERRAN_INFANTRY_WEAPON: - ItemData(100 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 0, SC2Race.TERRAN, - quantity=3, - description=inspect.cleandoc( - f""" - Increases damage of Terran infantry units. - {WEAPON_ARMOR_UPGRADE_NOTE} - """ - )), - ItemNames.PROGRESSIVE_TERRAN_INFANTRY_ARMOR: - ItemData(102 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 2, SC2Race.TERRAN, - quantity=3, - description=inspect.cleandoc( - f""" - Increases armor of Terran infantry units. - {WEAPON_ARMOR_UPGRADE_NOTE} - """ - )), - ItemNames.PROGRESSIVE_TERRAN_VEHICLE_WEAPON: - ItemData(103 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 4, SC2Race.TERRAN, - quantity=3, - description=inspect.cleandoc( - f""" - Increases damage of Terran vehicle units. - {WEAPON_ARMOR_UPGRADE_NOTE} - """ - )), - ItemNames.PROGRESSIVE_TERRAN_VEHICLE_ARMOR: - ItemData(104 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 6, SC2Race.TERRAN, - quantity=3, - description=inspect.cleandoc( - f""" - Increases armor of Terran vehicle units. - {WEAPON_ARMOR_UPGRADE_NOTE} - """ - )), - ItemNames.PROGRESSIVE_TERRAN_SHIP_WEAPON: - ItemData(105 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 8, SC2Race.TERRAN, - quantity=3, - description=inspect.cleandoc( - f""" - Increases damage of Terran starship units. - {WEAPON_ARMOR_UPGRADE_NOTE} - """ - )), - ItemNames.PROGRESSIVE_TERRAN_SHIP_ARMOR: - ItemData(106 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 10, SC2Race.TERRAN, - quantity=3, - description=inspect.cleandoc( - f""" - Increases armor of Terran starship units. - {WEAPON_ARMOR_UPGRADE_NOTE} - """ - )), - # Upgrade bundle 'number' values are used as indices to get affected 'number's - ItemNames.PROGRESSIVE_TERRAN_WEAPON_UPGRADE: ItemData(107 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 0, SC2Race.TERRAN, quantity=3), - ItemNames.PROGRESSIVE_TERRAN_ARMOR_UPGRADE: ItemData(108 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 1, SC2Race.TERRAN, quantity=3), - ItemNames.PROGRESSIVE_TERRAN_INFANTRY_UPGRADE: ItemData(109 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 2, SC2Race.TERRAN, quantity=3), - ItemNames.PROGRESSIVE_TERRAN_VEHICLE_UPGRADE: ItemData(110 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 3, SC2Race.TERRAN, quantity=3), - ItemNames.PROGRESSIVE_TERRAN_SHIP_UPGRADE: ItemData(111 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 4, SC2Race.TERRAN, quantity=3), - ItemNames.PROGRESSIVE_TERRAN_WEAPON_ARMOR_UPGRADE: ItemData(112 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 5, SC2Race.TERRAN, quantity=3), - - # Unit and structure upgrades - ItemNames.BUNKER_PROJECTILE_ACCELERATOR: - ItemData(200 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 0, SC2Race.TERRAN, - parent_item=ItemNames.BUNKER, - description="Increases range of all units in the Bunker by 1."), - ItemNames.BUNKER_NEOSTEEL_BUNKER: - ItemData(201 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 1, SC2Race.TERRAN, - parent_item=ItemNames.BUNKER, - description="Increases the number of Bunker slots by 2."), - ItemNames.MISSILE_TURRET_TITANIUM_HOUSING: - ItemData(202 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 2, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.MISSILE_TURRET, - description="Increases Missile Turret life by 75."), - ItemNames.MISSILE_TURRET_HELLSTORM_BATTERIES: - ItemData(203 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 3, SC2Race.TERRAN, - parent_item=ItemNames.MISSILE_TURRET, - description="The Missile Turret unleashes an additional flurry of missiles with each attack."), - ItemNames.SCV_ADVANCED_CONSTRUCTION: - ItemData(204 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 4, SC2Race.TERRAN, - description="Multiple SCVs can construct a structure, reducing its construction time."), - ItemNames.SCV_DUAL_FUSION_WELDERS: - ItemData(205 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 5, SC2Race.TERRAN, - description="SCVs repair twice as fast."), - ItemNames.PROGRESSIVE_FIRE_SUPPRESSION_SYSTEM: - ItemData(206 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 24, SC2Race.TERRAN, - quantity=2, - description=inspect.cleandoc( - """ - Level 1: While on low health, Terran structures are repaired to half health instead of burning down. - Level 2: Terran structures are repaired to full health instead of half health - """ - )), - ItemNames.PROGRESSIVE_ORBITAL_COMMAND: - ItemData(207 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 26, SC2Race.TERRAN, - quantity=2, classification=ItemClassification.progression, - description=inspect.cleandoc( - """ - Level 1: Allows Command Centers to use Scanner Sweep and Calldown: MULE abilities. - Level 2: Orbital Command abilities work even in Planetary Fortress mode. - """ - )), - ItemNames.MARINE_PROGRESSIVE_STIMPACK: - ItemData(208 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 0, SC2Race.TERRAN, - classification=ItemClassification.progression, parent_item=ItemNames.MARINE, quantity=2, - description=STIMPACK_SMALL_DESCRIPTION), - ItemNames.MARINE_COMBAT_SHIELD: - ItemData(209 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 9, SC2Race.TERRAN, - classification=ItemClassification.progression, parent_item=ItemNames.MARINE, - description="Increases Marine life by 10."), - ItemNames.MEDIC_ADVANCED_MEDIC_FACILITIES: - ItemData(210 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 10, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.MEDIC, - description=INTERNAL_TECH_MODULE_DESCRIPTION_TEMPLATE.format("Medics", "Barracks")), - ItemNames.MEDIC_STABILIZER_MEDPACKS: - ItemData(211 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 11, SC2Race.TERRAN, - classification=ItemClassification.progression, parent_item=ItemNames.MEDIC, - description="Increases Medic heal speed. Reduces the amount of energy required for each heal."), - ItemNames.FIREBAT_INCINERATOR_GAUNTLETS: - ItemData(212 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 12, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.FIREBAT, - description="Increases Firebat's damage radius by 40%"), - ItemNames.FIREBAT_JUGGERNAUT_PLATING: - ItemData(213 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 13, SC2Race.TERRAN, - parent_item=ItemNames.FIREBAT, - description="Increases Firebat's armor by 2."), - ItemNames.MARAUDER_CONCUSSIVE_SHELLS: - ItemData(214 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 14, SC2Race.TERRAN, - parent_item=ItemNames.MARAUDER, - description="Marauder attack temporarily slows all units in target area."), - ItemNames.MARAUDER_KINETIC_FOAM: - ItemData(215 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 15, SC2Race.TERRAN, - parent_item=ItemNames.MARAUDER, - description="Increases Marauder life by 25."), - ItemNames.REAPER_U238_ROUNDS: - ItemData(216 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 16, SC2Race.TERRAN, - parent_item=ItemNames.REAPER, - description=inspect.cleandoc( - """ - Increases Reaper pistol attack range by 1. - Reaper pistols do additional 3 damage to Light Armor. - """ - )), - ItemNames.REAPER_G4_CLUSTERBOMB: - ItemData(217 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 17, SC2Race.TERRAN, - classification=ItemClassification.progression, parent_item=ItemNames.REAPER, - description="Timed explosive that does heavy area damage."), - ItemNames.CYCLONE_MAG_FIELD_ACCELERATORS: - ItemData(218 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 18, SC2Race.TERRAN, - parent_item=ItemNames.CYCLONE, origin={"ext"}, - description="Increases Cyclone Lock On damage"), - ItemNames.CYCLONE_MAG_FIELD_LAUNCHERS: - ItemData(219 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 19, SC2Race.TERRAN, - parent_item=ItemNames.CYCLONE, origin={"ext"}, - description="Increases Cyclone attack range by 2."), - ItemNames.MARINE_LASER_TARGETING_SYSTEM: - ItemData(220 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 8, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.MARINE, origin={"nco"}, - description=LASER_TARGETING_SYSTEMS_DESCRIPTION), - ItemNames.MARINE_MAGRAIL_MUNITIONS: - ItemData(221 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 20, SC2Race.TERRAN, - classification=ItemClassification.progression, parent_item=ItemNames.MARINE, origin={"nco"}, - description="Deals 20 damage to target unit. Autocast on attack with a cooldown."), - ItemNames.MARINE_OPTIMIZED_LOGISTICS: - ItemData(222 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 21, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.MARINE, origin={"nco"}, - description="Increases Marine training speed."), - ItemNames.MEDIC_RESTORATION: - ItemData(223 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 22, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.MEDIC, origin={"bw"}, - description="Removes negative status effects from target allied unit."), - ItemNames.MEDIC_OPTICAL_FLARE: - ItemData(224 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 23, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.MEDIC, origin={"bw"}, - description="Reduces vision range of target enemy unit. Disables detection."), - ItemNames.MEDIC_RESOURCE_EFFICIENCY: - ItemData(225 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 24, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.MEDIC, origin={"bw"}, - description=RESOURCE_EFFICIENCY_DESCRIPTION_TEMPLATE.format("Medic")), - ItemNames.FIREBAT_PROGRESSIVE_STIMPACK: - ItemData(226 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 6, SC2Race.TERRAN, - parent_item=ItemNames.FIREBAT, quantity=2, origin={"bw"}, - description=STIMPACK_LARGE_DESCRIPTION), - ItemNames.FIREBAT_RESOURCE_EFFICIENCY: - ItemData(227 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 25, SC2Race.TERRAN, - parent_item=ItemNames.FIREBAT, origin={"bw"}, - description=RESOURCE_EFFICIENCY_DESCRIPTION_TEMPLATE.format("Firebat")), - ItemNames.MARAUDER_PROGRESSIVE_STIMPACK: - ItemData(228 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 8, SC2Race.TERRAN, - parent_item=ItemNames.MARAUDER, quantity=2, origin={"nco"}, - description=STIMPACK_LARGE_DESCRIPTION), - ItemNames.MARAUDER_LASER_TARGETING_SYSTEM: - ItemData(229 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 26, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.MARAUDER, origin={"nco"}, - description=LASER_TARGETING_SYSTEMS_DESCRIPTION), - ItemNames.MARAUDER_MAGRAIL_MUNITIONS: - ItemData(230 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 27, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.MARAUDER, origin={"nco"}, - description="Deals 20 damage to target unit. Autocast on attack with a cooldown."), - ItemNames.MARAUDER_INTERNAL_TECH_MODULE: - ItemData(231 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 28, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.MARAUDER, origin={"nco"}, - description=INTERNAL_TECH_MODULE_DESCRIPTION_TEMPLATE.format("Marauders", "Barracks")), - ItemNames.SCV_HOSTILE_ENVIRONMENT_ADAPTATION: - ItemData(232 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 29, SC2Race.TERRAN, - classification=ItemClassification.filler, origin={"bw"}, - description="Increases SCV life by 15 and attack speed slightly."), - ItemNames.MEDIC_ADAPTIVE_MEDPACKS: - ItemData(233 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 0, SC2Race.TERRAN, - classification=ItemClassification.progression, parent_item=ItemNames.MEDIC, origin={"ext"}, - description="Allows Medics to heal mechanical and air units."), - ItemNames.MEDIC_NANO_PROJECTOR: - ItemData(234 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 1, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.MEDIC, origin={"ext"}, - description="Increases Medic heal range by 2."), - ItemNames.FIREBAT_INFERNAL_PRE_IGNITER: - ItemData(235 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 2, SC2Race.TERRAN, - parent_item=ItemNames.FIREBAT, origin={"bw"}, - description="Firebats do an additional 4 damage to Light Armor."), - ItemNames.FIREBAT_KINETIC_FOAM: - ItemData(236 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 3, SC2Race.TERRAN, - parent_item=ItemNames.FIREBAT, origin={"ext"}, - description="Increases Firebat life by 100."), - ItemNames.FIREBAT_NANO_PROJECTORS: - ItemData(237 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 4, SC2Race.TERRAN, - parent_item=ItemNames.FIREBAT, origin={"ext"}, - description="Increases Firebat attack range by 2"), - ItemNames.MARAUDER_JUGGERNAUT_PLATING: - ItemData(238 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 5, SC2Race.TERRAN, - parent_item=ItemNames.MARAUDER, origin={"ext"}, - description="Increases Marauder's armor by 2."), - ItemNames.REAPER_JET_PACK_OVERDRIVE: - ItemData(239 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 6, SC2Race.TERRAN, - parent_item=ItemNames.REAPER, origin={"ext"}, - description=inspect.cleandoc( - """ - Allows the Reaper to fly for 10 seconds. - While flying, the Reaper can attack air units. - """ - )), - ItemNames.HELLION_INFERNAL_PLATING: - ItemData(240 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 7, SC2Race.TERRAN, - parent_item=ItemNames.HELLION, origin={"ext"}, - description="Increases Hellion and Hellbat armor by 2."), - ItemNames.VULTURE_AUTO_REPAIR: - ItemData(241 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 8, SC2Race.TERRAN, - parent_item=ItemNames.VULTURE, origin={"ext"}, - description="Vultures regenerate life."), - ItemNames.GOLIATH_SHAPED_HULL: - ItemData(242 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 9, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.GOLIATH, origin={"nco", "ext"}, - description="Increases Goliath life by 25."), - ItemNames.GOLIATH_RESOURCE_EFFICIENCY: - ItemData(243 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 10, SC2Race.TERRAN, - parent_item=ItemNames.GOLIATH, origin={"nco", "bw"}, - description=RESOURCE_EFFICIENCY_DESCRIPTION_TEMPLATE.format("Goliath")), - ItemNames.GOLIATH_INTERNAL_TECH_MODULE: - ItemData(244 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 11, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.GOLIATH, origin={"nco", "bw"}, - description=INTERNAL_TECH_MODULE_DESCRIPTION_TEMPLATE.format("Goliaths", "Factory")), - ItemNames.SIEGE_TANK_SHAPED_HULL: - ItemData(245 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 12, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.SIEGE_TANK, origin={"nco", "ext"}, - description="Increases Siege Tank life by 25."), - ItemNames.SIEGE_TANK_RESOURCE_EFFICIENCY: - ItemData(246 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 13, SC2Race.TERRAN, - parent_item=ItemNames.SIEGE_TANK, origin={"bw"}, - description=RESOURCE_EFFICIENCY_DESCRIPTION_TEMPLATE.format("Siege Tank")), - ItemNames.PREDATOR_CLOAK: - ItemData(247 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 14, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.PREDATOR, origin={"ext"}, - description=CLOAK_DESCRIPTION_TEMPLATE.format("Predators")), - ItemNames.PREDATOR_CHARGE: - ItemData(248 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 15, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.PREDATOR, origin={"ext"}, - description="Allows Predators to intercept enemy ground units."), - ItemNames.MEDIVAC_SCATTER_VEIL: - ItemData(249 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 16, SC2Race.TERRAN, - parent_item=ItemNames.MEDIVAC, origin={"ext"}, - description="Medivacs get 100 shields."), - ItemNames.REAPER_PROGRESSIVE_STIMPACK: - ItemData(250 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 10, SC2Race.TERRAN, - parent_item=ItemNames.REAPER, quantity=2, origin={"nco"}, - description=STIMPACK_SMALL_DESCRIPTION), - ItemNames.REAPER_LASER_TARGETING_SYSTEM: - ItemData(251 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 17, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.REAPER, origin={"nco"}, - description=LASER_TARGETING_SYSTEMS_DESCRIPTION), - ItemNames.REAPER_ADVANCED_CLOAKING_FIELD: - ItemData(252 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 18, SC2Race.TERRAN, - parent_item=ItemNames.REAPER, origin={"nco"}, - description="Reapers are permanently cloaked."), - ItemNames.REAPER_SPIDER_MINES: - ItemData(253 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 19, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.REAPER, origin={"nco"}, - important_for_filtering=True, - description="Allows Reapers to lay Spider Mines. 3 charges per Reaper."), - ItemNames.REAPER_COMBAT_DRUGS: - ItemData(254 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 20, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.REAPER, origin={"ext"}, - description="Reapers regenerate life while out of combat."), - ItemNames.HELLION_HELLBAT_ASPECT: - ItemData(255 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 21, SC2Race.TERRAN, - classification=ItemClassification.progression, parent_item=ItemNames.HELLION, origin={"nco"}, - description="Allows Hellions to transform into Hellbats."), - ItemNames.HELLION_SMART_SERVOS: - ItemData(256 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 22, SC2Race.TERRAN, - parent_item=ItemNames.HELLION, origin={"nco"}, - description="Transforms faster between modes. Hellions can attack while moving."), - ItemNames.HELLION_OPTIMIZED_LOGISTICS: - ItemData(257 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 23, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.HELLION, origin={"nco"}, - description="Increases Hellion training speed."), - ItemNames.HELLION_JUMP_JETS: - ItemData(258 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 24, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.HELLION, origin={"nco"}, - description=inspect.cleandoc( - """ - Increases movement speed in Hellion mode. - In Hellbat mode, launches the Hellbat toward enemy ground units and briefly stuns them. - """ - )), - ItemNames.HELLION_PROGRESSIVE_STIMPACK: - ItemData(259 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 12, SC2Race.TERRAN, - parent_item=ItemNames.HELLION, quantity=2, origin={"nco"}, - description=STIMPACK_LARGE_DESCRIPTION), - ItemNames.VULTURE_ION_THRUSTERS: - ItemData(260 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 25, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.VULTURE, origin={"bw"}, - description="Increases Vulture movement speed."), - ItemNames.VULTURE_AUTO_LAUNCHERS: - ItemData(261 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 26, SC2Race.TERRAN, - parent_item=ItemNames.VULTURE, origin={"bw"}, - description="Allows Vultures to attack while moving."), - ItemNames.SPIDER_MINE_HIGH_EXPLOSIVE_MUNITION: - ItemData(262 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 27, SC2Race.TERRAN, - origin={"bw"}, - description="Increases Spider mine damage."), - ItemNames.GOLIATH_JUMP_JETS: - ItemData(263 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 28, SC2Race.TERRAN, - classification=ItemClassification.progression, parent_item=ItemNames.GOLIATH, origin={"nco"}, - description="Allows Goliaths to jump up and down cliffs."), - ItemNames.GOLIATH_OPTIMIZED_LOGISTICS: - ItemData(264 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 29, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.GOLIATH, origin={"nco"}, - description="Increases Goliath training speed."), - ItemNames.DIAMONDBACK_HYPERFLUXOR: - ItemData(265 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 0, SC2Race.TERRAN, - parent_item=ItemNames.DIAMONDBACK, origin={"ext"}, - description="Increases Diamondback attack speed."), - ItemNames.DIAMONDBACK_BURST_CAPACITORS: - ItemData(266 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 1, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.DIAMONDBACK, origin={"ext"}, - description=inspect.cleandoc( - """ - While not attacking, the Diamondback charges its weapon. - The next attack does 10 additional damage. - """ - )), - ItemNames.DIAMONDBACK_RESOURCE_EFFICIENCY: - ItemData(267 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 2, SC2Race.TERRAN, - parent_item=ItemNames.DIAMONDBACK, origin={"ext"}, - description=RESOURCE_EFFICIENCY_DESCRIPTION_TEMPLATE.format("Diamondback")), - ItemNames.SIEGE_TANK_JUMP_JETS: - ItemData(268 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 3, SC2Race.TERRAN, - classification=ItemClassification.progression, parent_item=ItemNames.SIEGE_TANK, origin={"nco"}, - description=inspect.cleandoc( - """ - Repositions Siege Tank to a target location. - Can be used in either mode and to jump up and down cliffs. - """ - )), - ItemNames.SIEGE_TANK_SPIDER_MINES: - ItemData(269 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 4, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.SIEGE_TANK, origin={"nco"}, - important_for_filtering=True, - description=inspect.cleandoc( - """ - Allows Siege Tanks to lay Spider Mines. - Lays 3 Spider Mines at once. 3 charges - """ - )), - ItemNames.SIEGE_TANK_SMART_SERVOS: - ItemData(270 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 5, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.SIEGE_TANK, origin={"nco"}, - description=SMART_SERVOS_DESCRIPTION), - ItemNames.SIEGE_TANK_GRADUATING_RANGE: - ItemData(271 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 6, SC2Race.TERRAN, - classification=ItemClassification.progression, parent_item=ItemNames.SIEGE_TANK, origin={"ext"}, - description=inspect.cleandoc( - """ - Increases the Siege Tank's attack range by 1 every 3 seconds while in Siege Mode, - up to a maximum of 5 additional range. - """ - )), - ItemNames.SIEGE_TANK_LASER_TARGETING_SYSTEM: - ItemData(272 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 7, SC2Race.TERRAN, - parent_item=ItemNames.SIEGE_TANK, origin={"nco"}, - description=LASER_TARGETING_SYSTEMS_DESCRIPTION), - ItemNames.SIEGE_TANK_ADVANCED_SIEGE_TECH: - ItemData(273 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 8, SC2Race.TERRAN, - parent_item=ItemNames.SIEGE_TANK, origin={"ext"}, - description="Siege Tanks gain +3 armor in Siege Mode."), - ItemNames.SIEGE_TANK_INTERNAL_TECH_MODULE: - ItemData(274 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 9, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.SIEGE_TANK, origin={"nco"}, - description=INTERNAL_TECH_MODULE_DESCRIPTION_TEMPLATE.format("Siege Tanks", "Factory")), - ItemNames.PREDATOR_RESOURCE_EFFICIENCY: - ItemData(275 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 10, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.PREDATOR, origin={"ext"}, - description="Decreases Predator resource and supply cost."), - ItemNames.MEDIVAC_EXPANDED_HULL: - ItemData(276 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 11, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.MEDIVAC, origin={"ext"}, - description="Increases Medivac cargo space by 4."), - ItemNames.MEDIVAC_AFTERBURNERS: - ItemData(277 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 12, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.MEDIVAC, origin={"ext"}, - description="Ability. Temporarily increases the Medivac's movement speed by 70%."), - ItemNames.WRAITH_ADVANCED_LASER_TECHNOLOGY: - ItemData(278 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 13, SC2Race.TERRAN, - classification=ItemClassification.progression, parent_item=ItemNames.WRAITH, origin={"ext"}, - description=inspect.cleandoc( - """ - Burst Lasers do more damage and can hit both ground and air targets. - Replaces Gemini Missiles weapon. - """ - )), - ItemNames.VIKING_SMART_SERVOS: - ItemData(279 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 14, SC2Race.TERRAN, - parent_item=ItemNames.VIKING, origin={"ext"}, - description=SMART_SERVOS_DESCRIPTION), - ItemNames.VIKING_ANTI_MECHANICAL_MUNITION: - ItemData(280 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 15, SC2Race.TERRAN, - parent_item=ItemNames.VIKING, origin={"ext"}, - description="Increases Viking damage to mechanical units while in Assault Mode."), - ItemNames.DIAMONDBACK_ION_THRUSTERS: - ItemData(281 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 21, SC2Race.TERRAN, - parent_item=ItemNames.DIAMONDBACK, origin={"ext"}, - description="Increases Diamondback movement speed."), - ItemNames.WARHOUND_RESOURCE_EFFICIENCY: - ItemData(282 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 13, SC2Race.TERRAN, - parent_item=ItemNames.WARHOUND, origin={"ext"}, - description=RESOURCE_EFFICIENCY_NO_SUPPLY_DESCRIPTION_TEMPLATE.format("Warhound")), - ItemNames.WARHOUND_REINFORCED_PLATING: - ItemData(283 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 14, SC2Race.TERRAN, - parent_item=ItemNames.WARHOUND, origin={"ext"}, - description="Increases Warhound armor by 2."), - ItemNames.HERC_RESOURCE_EFFICIENCY: - ItemData(284 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 15, SC2Race.TERRAN, - parent_item=ItemNames.HERC, origin={"ext"}, - description=RESOURCE_EFFICIENCY_DESCRIPTION_TEMPLATE.format("HERC")), - ItemNames.HERC_JUGGERNAUT_PLATING: - ItemData(285 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 16, SC2Race.TERRAN, - parent_item=ItemNames.HERC, origin={"ext"}, - description="Increases HERC armor by 2."), - ItemNames.HERC_KINETIC_FOAM: - ItemData(286 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 17, SC2Race.TERRAN, - parent_item=ItemNames.HERC, origin={"ext"}, - description="Increases HERC life by 50."), - - ItemNames.HELLION_TWIN_LINKED_FLAMETHROWER: - ItemData(300 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 16, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.HELLION, - description="Doubles the width of the Hellion's flame attack."), - ItemNames.HELLION_THERMITE_FILAMENTS: - ItemData(301 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 17, SC2Race.TERRAN, - parent_item=ItemNames.HELLION, - description="Hellions do an additional 10 damage to Light Armor."), - ItemNames.SPIDER_MINE_CERBERUS_MINE: - ItemData(302 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 18, SC2Race.TERRAN, - classification=ItemClassification.filler, - description="Increases trigger and blast radius of Spider Mines."), - ItemNames.VULTURE_PROGRESSIVE_REPLENISHABLE_MAGAZINE: - ItemData(303 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 16, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.VULTURE, quantity=2, - description=inspect.cleandoc( - """ - Level 1: Allows Vultures to replace used Spider Mines. Costs 15 minerals. - Level 2: Replacing used Spider Mines no longer costs minerals. - """ - )), - ItemNames.GOLIATH_MULTI_LOCK_WEAPONS_SYSTEM: - ItemData(304 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 19, SC2Race.TERRAN, - parent_item=ItemNames.GOLIATH, - description="Goliaths can attack both ground and air targets simultaneously."), - ItemNames.GOLIATH_ARES_CLASS_TARGETING_SYSTEM: - ItemData(305 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 20, SC2Race.TERRAN, - parent_item=ItemNames.GOLIATH, - description="Increases Goliath ground attack range by 1 and air by 3."), - ItemNames.DIAMONDBACK_PROGRESSIVE_TRI_LITHIUM_POWER_CELL: - ItemData(306 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade 2", 4, SC2Race.TERRAN, - parent_item=ItemNames.DIAMONDBACK, quantity=2, - description=inspect.cleandoc( - """ - Level 1: Tri-Lithium Power Cell: Increases Diamondback attack range by 1. - Level 2: Tungsten Spikes: Increases Diamondback attack range by 3. - """ - )), - ItemNames.DIAMONDBACK_SHAPED_HULL: - ItemData(307 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 22, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.DIAMONDBACK, - description="Increases Diamondback life by 50."), - ItemNames.SIEGE_TANK_MAELSTROM_ROUNDS: - ItemData(308 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 23, SC2Race.TERRAN, - classification=ItemClassification.progression, parent_item=ItemNames.SIEGE_TANK, - description="Siege Tanks do an additional 40 damage to the primary target in Siege Mode."), - ItemNames.SIEGE_TANK_SHAPED_BLAST: - ItemData(309 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 24, SC2Race.TERRAN, - parent_item=ItemNames.SIEGE_TANK, - description="Reduces splash damage to friendly targets while in Siege Mode by 75%."), - ItemNames.MEDIVAC_RAPID_DEPLOYMENT_TUBE: - ItemData(310 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 25, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.MEDIVAC, - description="Medivacs deploy loaded troops almost instantly."), - ItemNames.MEDIVAC_ADVANCED_HEALING_AI: - ItemData(311 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 26, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.MEDIVAC, - description="Medivacs can heal two targets at once."), - ItemNames.WRAITH_PROGRESSIVE_TOMAHAWK_POWER_CELLS: - ItemData(312 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 18, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.WRAITH, quantity=2, - description=inspect.cleandoc( - """ - Level 1: Tomahawk Power Cells: Increases Wraith starting energy by 100. - Level 2: Unregistered Cloaking Module: Wraiths do not require energy to cloak and remain cloaked. - """ - )), - ItemNames.WRAITH_DISPLACEMENT_FIELD: - ItemData(313 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 27, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.WRAITH, - description="Wraiths evade 20% of incoming attacks while cloaked."), - ItemNames.VIKING_RIPWAVE_MISSILES: - ItemData(314 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 28, SC2Race.TERRAN, - parent_item=ItemNames.VIKING, - description="Vikings do area damage while in Fighter Mode"), - ItemNames.VIKING_PHOBOS_CLASS_WEAPONS_SYSTEM: - ItemData(315 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 29, SC2Race.TERRAN, - parent_item=ItemNames.VIKING, - description="Increases Viking attack range by 1 in Assault mode and 2 in Fighter mode."), - ItemNames.BANSHEE_PROGRESSIVE_CROSS_SPECTRUM_DAMPENERS: - ItemData(316 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 2, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.BANSHEE, quantity=2, - description=inspect.cleandoc( - """ - Level 1: Banshees can remain cloaked twice as long. - Level 2: Banshees do not require energy to cloak and remain cloaked. - """ - )), - ItemNames.BANSHEE_SHOCKWAVE_MISSILE_BATTERY: - ItemData(317 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 0, SC2Race.TERRAN, - classification=ItemClassification.progression, parent_item=ItemNames.BANSHEE, - description="Banshees do area damage in a straight line."), - ItemNames.BATTLECRUISER_PROGRESSIVE_MISSILE_PODS: - ItemData(318 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade 2", 2, SC2Race.TERRAN, - parent_item=ItemNames.BATTLECRUISER, quantity=2, - description="Spell. Missile Pods do damage to air targets in a target area."), - ItemNames.BATTLECRUISER_PROGRESSIVE_DEFENSIVE_MATRIX: - ItemData(319 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 20, SC2Race.TERRAN, - parent_item=ItemNames.BATTLECRUISER, quantity=2, - description=inspect.cleandoc( - """ - Level 1: Spell. For 20 seconds the Battlecruiser gains a shield that can absorb up to 200 damage. - Level 2: Passive. Battlecruiser gets 200 shields. - """ - )), - ItemNames.GHOST_OCULAR_IMPLANTS: - ItemData(320 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 2, SC2Race.TERRAN, - parent_item=ItemNames.GHOST, - description="Increases Ghost sight range by 3 and attack range by 2."), - ItemNames.GHOST_CRIUS_SUIT: - ItemData(321 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 3, SC2Race.TERRAN, - parent_item=ItemNames.GHOST, - description="Cloak no longer requires energy to activate or maintain."), - ItemNames.SPECTRE_PSIONIC_LASH: - ItemData(322 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 4, SC2Race.TERRAN, - classification=ItemClassification.progression, parent_item=ItemNames.SPECTRE, - description="Spell. Deals 200 damage to a single target."), - ItemNames.SPECTRE_NYX_CLASS_CLOAKING_MODULE: - ItemData(323 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 5, SC2Race.TERRAN, - parent_item=ItemNames.SPECTRE, - description="Cloak no longer requires energy to activate or maintain."), - ItemNames.THOR_330MM_BARRAGE_CANNON: - ItemData(324 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 6, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.THOR, - description=inspect.cleandoc( - """ - Improves 250mm Strike Cannons ability to deal area damage and stun units in a small area. - Can be also freely aimed on ground. - """ - )), - ItemNames.THOR_PROGRESSIVE_IMMORTALITY_PROTOCOL: - ItemData(325 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 22, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.THOR, quantity=2, - description=inspect.cleandoc(""" - Level 1: Allows destroyed Thors to be reconstructed on the field. Costs Vespene Gas. - Level 2: Thors are automatically reconstructed after falling for free. - """ - )), - ItemNames.LIBERATOR_ADVANCED_BALLISTICS: - ItemData(326 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 7, SC2Race.TERRAN, - parent_item=ItemNames.LIBERATOR, origin={"ext"}, - description="Increases Liberator range by 3 in Defender Mode."), - ItemNames.LIBERATOR_RAID_ARTILLERY: - ItemData(327 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 8, SC2Race.TERRAN, - classification=ItemClassification.progression, parent_item=ItemNames.LIBERATOR, origin={"nco"}, - description="Allows Liberators to attack structures while in Defender Mode."), - ItemNames.WIDOW_MINE_DRILLING_CLAWS: - ItemData(328 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 9, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.WIDOW_MINE, origin={"ext"}, - description="Allows Widow Mines to burrow and unburrow faster."), - ItemNames.WIDOW_MINE_CONCEALMENT: - ItemData(329 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 10, SC2Race.TERRAN, - classification=ItemClassification.progression, parent_item=ItemNames.WIDOW_MINE, origin={"ext"}, - description="Burrowed Widow Mines are no longer revealed when the Sentinel Missile is on cooldown."), - ItemNames.MEDIVAC_ADVANCED_CLOAKING_FIELD: - ItemData(330 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 11, SC2Race.TERRAN, - parent_item=ItemNames.MEDIVAC, origin={"ext"}, - description="Medivacs are permanently cloaked."), - ItemNames.WRAITH_TRIGGER_OVERRIDE: - ItemData(331 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 12, SC2Race.TERRAN, - parent_item=ItemNames.WRAITH, origin={"ext"}, - description="Wraith attack speed increases by 10% with each attack, up to a maximum of 100%."), - ItemNames.WRAITH_INTERNAL_TECH_MODULE: - ItemData(332 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 13, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.WRAITH, origin={"bw"}, - description=INTERNAL_TECH_MODULE_DESCRIPTION_TEMPLATE.format("Wraiths", "Starport")), - ItemNames.WRAITH_RESOURCE_EFFICIENCY: - ItemData(333 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 14, SC2Race.TERRAN, - parent_item=ItemNames.WRAITH, origin={"bw"}, - description=RESOURCE_EFFICIENCY_NO_SUPPLY_DESCRIPTION_TEMPLATE.format("Wraith")), - ItemNames.VIKING_SHREDDER_ROUNDS: - ItemData(334 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 15, SC2Race.TERRAN, - classification=ItemClassification.progression, parent_item=ItemNames.VIKING, origin={"ext"}, - description="Attacks in Assault mode do line splash damage."), - ItemNames.VIKING_WILD_MISSILES: - ItemData(335 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 16, SC2Race.TERRAN, - parent_item=ItemNames.VIKING, origin={"ext"}, - description="Launches 5 rockets at the target unit. Each rocket does 25 (40 vs armored) damage."), - ItemNames.BANSHEE_SHAPED_HULL: - ItemData(336 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 17, SC2Race.TERRAN, - parent_item=ItemNames.BANSHEE, origin={"ext"}, - description="Increases Banshee life by 100."), - ItemNames.BANSHEE_ADVANCED_TARGETING_OPTICS: - ItemData(337 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 18, SC2Race.TERRAN, - classification=ItemClassification.progression, parent_item=ItemNames.BANSHEE, origin={"ext"}, - description="Increases Banshee attack range by 2 while cloaked."), - ItemNames.BANSHEE_DISTORTION_BLASTERS: - ItemData(338 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 19, SC2Race.TERRAN, - parent_item=ItemNames.BANSHEE, origin={"ext"}, - description="Increases Banshee attack damage by 25% while cloaked."), - ItemNames.BANSHEE_ROCKET_BARRAGE: - ItemData(339 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 20, SC2Race.TERRAN, - parent_item=ItemNames.BANSHEE, origin={"ext"}, - description="Deals 75 damage to enemy ground units in the target area."), - ItemNames.GHOST_RESOURCE_EFFICIENCY: - ItemData(340 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 21, SC2Race.TERRAN, - parent_item=ItemNames.GHOST, origin={"bw"}, - description=RESOURCE_EFFICIENCY_DESCRIPTION_TEMPLATE.format("Ghost")), - ItemNames.SPECTRE_RESOURCE_EFFICIENCY: - ItemData(341 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 22, SC2Race.TERRAN, - parent_item=ItemNames.SPECTRE, origin={"ext"}, - description=RESOURCE_EFFICIENCY_DESCRIPTION_TEMPLATE.format("Spectre")), - ItemNames.THOR_BUTTON_WITH_A_SKULL_ON_IT: - ItemData(342 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 23, SC2Race.TERRAN, - classification=ItemClassification.progression, parent_item=ItemNames.THOR, origin={"ext"}, - description="Allows Thors to launch nukes."), - ItemNames.THOR_LASER_TARGETING_SYSTEM: - ItemData(343 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 24, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.THOR, origin={"ext"}, - description=LASER_TARGETING_SYSTEMS_DESCRIPTION), - ItemNames.THOR_LARGE_SCALE_FIELD_CONSTRUCTION: - ItemData(344 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 25, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.THOR, origin={"ext"}, - description="Allows Thors to be built by SCVs like a structure."), - ItemNames.RAVEN_RESOURCE_EFFICIENCY: - ItemData(345 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 26, SC2Race.TERRAN, - parent_item=ItemNames.RAVEN, origin={"ext"}, - description=RESOURCE_EFFICIENCY_NO_SUPPLY_DESCRIPTION_TEMPLATE.format("Raven")), - ItemNames.RAVEN_DURABLE_MATERIALS: - ItemData(346 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 27, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.RAVEN, origin={"ext"}, - description="Extends timed life duration of Raven's summoned objects."), - ItemNames.SCIENCE_VESSEL_IMPROVED_NANO_REPAIR: - ItemData(347 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 28, SC2Race.TERRAN, - parent_item=ItemNames.SCIENCE_VESSEL, origin={"ext"}, - description="Nano-Repair no longer requires energy to use."), - ItemNames.SCIENCE_VESSEL_ADVANCED_AI_SYSTEMS: - ItemData(348 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 29, SC2Race.TERRAN, - parent_item=ItemNames.SCIENCE_VESSEL, origin={"ext"}, - description="Science Vessel can use Nano-Repair at two targets at once."), - ItemNames.CYCLONE_RESOURCE_EFFICIENCY: - ItemData(349 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 0, SC2Race.TERRAN, - parent_item=ItemNames.CYCLONE, origin={"ext"}, - description=RESOURCE_EFFICIENCY_DESCRIPTION_TEMPLATE.format("Cyclone")), - ItemNames.BANSHEE_HYPERFLIGHT_ROTORS: - ItemData(350 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 1, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.BANSHEE, origin={"ext"}, - description="Increases Banshee movement speed."), - ItemNames.BANSHEE_LASER_TARGETING_SYSTEM: - ItemData(351 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 2, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.BANSHEE, origin={"nco"}, - description=LASER_TARGETING_SYSTEMS_DESCRIPTION), - ItemNames.BANSHEE_INTERNAL_TECH_MODULE: - ItemData(352 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 3, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.BANSHEE, origin={"nco"}, - description=INTERNAL_TECH_MODULE_DESCRIPTION_TEMPLATE.format("Banshees", "Starport")), - ItemNames.BATTLECRUISER_TACTICAL_JUMP: - ItemData(353 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 4, SC2Race.TERRAN, - parent_item=ItemNames.BATTLECRUISER, origin={"nco", "ext"}, - description=inspect.cleandoc( - """ - Allows Battlecruisers to warp to a target location anywhere on the map. - """ - )), - ItemNames.BATTLECRUISER_CLOAK: - ItemData(354 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 5, SC2Race.TERRAN, - parent_item=ItemNames.BATTLECRUISER, origin={"nco"}, - description=CLOAK_DESCRIPTION_TEMPLATE.format("Battlecruisers")), - ItemNames.BATTLECRUISER_ATX_LASER_BATTERY: - ItemData(355 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 6, SC2Race.TERRAN, - classification=ItemClassification.progression, parent_item=ItemNames.BATTLECRUISER, origin={"nco"}, - description=inspect.cleandoc( - """ - Battlecruisers can attack while moving, - do the same damage to both ground and air targets, and fire faster. - """ - )), - ItemNames.BATTLECRUISER_OPTIMIZED_LOGISTICS: - ItemData(356 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 7, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.BATTLECRUISER, origin={"ext"}, - description="Increases Battlecruiser training speed."), - ItemNames.BATTLECRUISER_INTERNAL_TECH_MODULE: - ItemData(357 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 8, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.BATTLECRUISER, origin={"nco"}, - description=INTERNAL_TECH_MODULE_DESCRIPTION_TEMPLATE.format("Battlecruisers", "Starport")), - ItemNames.GHOST_EMP_ROUNDS: - ItemData(358 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 9, SC2Race.TERRAN, - parent_item=ItemNames.GHOST, origin={"ext"}, - description=inspect.cleandoc( - """ - Spell. Does 100 damage to shields and drains all energy from units in the targeted area. - Cloaked units hit by EMP are revealed for a short time. - """ - )), - ItemNames.GHOST_LOCKDOWN: - ItemData(359 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 10, SC2Race.TERRAN, - parent_item=ItemNames.GHOST, origin={"bw"}, - description="Spell. Stuns a target mechanical unit for a long time."), - ItemNames.SPECTRE_IMPALER_ROUNDS: - ItemData(360 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 11, SC2Race.TERRAN, - parent_item=ItemNames.SPECTRE, origin={"ext"}, - description="Spectres do additional damage to armored targets."), - ItemNames.THOR_PROGRESSIVE_HIGH_IMPACT_PAYLOAD: - ItemData(361 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 14, SC2Race.TERRAN, - parent_item=ItemNames.THOR, quantity=2, origin={"ext"}, - description=inspect.cleandoc( - f""" - Level 1: Allows Thors to transform in order to use an alternative air attack. - Level 2: {SMART_SERVOS_DESCRIPTION} - """ - )), - ItemNames.RAVEN_BIO_MECHANICAL_REPAIR_DRONE: - ItemData(363 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 12, SC2Race.TERRAN, - classification=ItemClassification.progression, parent_item=ItemNames.RAVEN, origin={"nco"}, - description="Spell. Deploys a drone that can heal biological or mechanical units."), - ItemNames.RAVEN_SPIDER_MINES: - ItemData(364 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 13, SC2Race.TERRAN, - parent_item=ItemNames.RAVEN, origin={"nco"}, important_for_filtering=True, - description="Spell. Deploys 3 Spider Mines to a target location."), - ItemNames.RAVEN_RAILGUN_TURRET: - ItemData(365 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 14, SC2Race.TERRAN, - parent_item=ItemNames.RAVEN, origin={"nco"}, - description=inspect.cleandoc( - """ - Spell. Allows Ravens to deploy an advanced Auto-Turret, - that can attack enemy ground units in a straight line. - """ - )), - ItemNames.RAVEN_HUNTER_SEEKER_WEAPON: - ItemData(366 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 15, SC2Race.TERRAN, - classification=ItemClassification.progression, parent_item=ItemNames.RAVEN, origin={"nco"}, - description="Allows Ravens to attack with a Hunter-Seeker weapon."), - ItemNames.RAVEN_INTERFERENCE_MATRIX: - ItemData(367 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 16, SC2Race.TERRAN, - parent_item=ItemNames.RAVEN, origin={"ext"}, - description=inspect.cleandoc( - """ - Spell. Target enemy Mechanical or Psionic unit can't attack or use abilities for a short duration. - """ - )), - ItemNames.RAVEN_ANTI_ARMOR_MISSILE: - ItemData(368 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 17, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.RAVEN, origin={"ext"}, - description="Spell. Decreases target and nearby enemy units armor by 2."), - ItemNames.RAVEN_INTERNAL_TECH_MODULE: - ItemData(369 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 18, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.RAVEN, origin={"nco"}, - description=INTERNAL_TECH_MODULE_DESCRIPTION_TEMPLATE.format("Ravens", "Starport")), - ItemNames.SCIENCE_VESSEL_EMP_SHOCKWAVE: - ItemData(370 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 19, SC2Race.TERRAN, - parent_item=ItemNames.SCIENCE_VESSEL, origin={"bw"}, - description="Spell. Depletes all energy and shields of all units in a target area."), - ItemNames.SCIENCE_VESSEL_DEFENSIVE_MATRIX: - ItemData(371 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 20, SC2Race.TERRAN, - parent_item=ItemNames.SCIENCE_VESSEL, origin={"bw"}, - description=inspect.cleandoc( - """ - Spell. Provides a target unit with a defensive barrier that can absorb up to 250 damage - """ - )), - ItemNames.CYCLONE_TARGETING_OPTICS: - ItemData(372 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 21, SC2Race.TERRAN, - parent_item=ItemNames.CYCLONE, origin={"ext"}, - description="Increases Cyclone Lock On casting range and the range while Locked On."), - ItemNames.CYCLONE_RAPID_FIRE_LAUNCHERS: - ItemData(373 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 22, SC2Race.TERRAN, - parent_item=ItemNames.CYCLONE, origin={"ext"}, - description="The first 12 shots of Lock On are fired more quickly."), - ItemNames.LIBERATOR_CLOAK: - ItemData(374 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 23, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.LIBERATOR, origin={"nco"}, - description=CLOAK_DESCRIPTION_TEMPLATE.format("Liberators")), - ItemNames.LIBERATOR_LASER_TARGETING_SYSTEM: - ItemData(375 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 24, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.LIBERATOR, origin={"ext"}, - description=LASER_TARGETING_SYSTEMS_DESCRIPTION), - ItemNames.LIBERATOR_OPTIMIZED_LOGISTICS: - ItemData(376 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 25, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.LIBERATOR, origin={"nco"}, - description="Increases Liberator training speed."), - ItemNames.WIDOW_MINE_BLACK_MARKET_LAUNCHERS: - ItemData(377 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 26, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.WIDOW_MINE, origin={"ext"}, - description="Increases Widow Mine Sentinel Missile range."), - ItemNames.WIDOW_MINE_EXECUTIONER_MISSILES: - ItemData(378 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 27, SC2Race.TERRAN, - parent_item=ItemNames.WIDOW_MINE, origin={"ext"}, - description=inspect.cleandoc( - """ - Reduces Sentinel Missile cooldown. - When killed, Widow Mines will launch several missiles at random enemy targets. - """ - )), - ItemNames.VALKYRIE_ENHANCED_CLUSTER_LAUNCHERS: - ItemData(379 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 28, - SC2Race.TERRAN, parent_item=ItemNames.VALKYRIE, origin={"ext"}, - description="Valkyries fire 2 additional rockets each volley."), - ItemNames.VALKYRIE_SHAPED_HULL: - ItemData(380 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 29, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.VALKYRIE, origin={"ext"}, - description="Increases Valkyrie life by 50."), - ItemNames.VALKYRIE_FLECHETTE_MISSILES: - ItemData(381 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 0, SC2Race.TERRAN, - parent_item=ItemNames.VALKYRIE, origin={"ext"}, - description="Equips Valkyries with Air-to-Surface missiles to attack ground units."), - ItemNames.VALKYRIE_AFTERBURNERS: - ItemData(382 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 1, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.VALKYRIE, origin={"ext"}, - description="Ability. Temporarily increases the Valkyries's movement speed by 70%."), - ItemNames.CYCLONE_INTERNAL_TECH_MODULE: - ItemData(383 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 2, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.CYCLONE, origin={"ext"}, - description=INTERNAL_TECH_MODULE_DESCRIPTION_TEMPLATE.format("Cyclones", "Factory")), - ItemNames.LIBERATOR_SMART_SERVOS: - ItemData(384 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 3, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.LIBERATOR, origin={"nco"}, - description=SMART_SERVOS_DESCRIPTION), - ItemNames.LIBERATOR_RESOURCE_EFFICIENCY: - ItemData(385 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 4, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.LIBERATOR, origin={"ext"}, - description=RESOURCE_EFFICIENCY_NO_SUPPLY_DESCRIPTION_TEMPLATE.format("Liberator")), - ItemNames.HERCULES_INTERNAL_FUSION_MODULE: - ItemData(386 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 5, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.HERCULES, origin={"ext"}, - description="Hercules can be trained from a Starport without having a Fusion Core."), - ItemNames.HERCULES_TACTICAL_JUMP: - ItemData(387 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 6, SC2Race.TERRAN, - parent_item=ItemNames.HERCULES, origin={"ext"}, - description=inspect.cleandoc( - """ - Allows Hercules to warp to a target location anywhere on the map. - """ - )), - ItemNames.PLANETARY_FORTRESS_PROGRESSIVE_AUGMENTED_THRUSTERS: - ItemData(388 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 28, SC2Race.TERRAN, - parent_item=ItemNames.PLANETARY_FORTRESS, origin={"ext"}, quantity=2, - description=inspect.cleandoc( - """ - Level 1: Lift Off - Planetary Fortress can lift off. - Level 2: Armament Stabilizers - Planetary Fortress can attack while lifted off. - """ - )), - ItemNames.PLANETARY_FORTRESS_ADVANCED_TARGETING: - ItemData(389 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 7, SC2Race.TERRAN, - parent_item=ItemNames.PLANETARY_FORTRESS, origin={"ext"}, - description="Planetary Fortress can attack air units."), - ItemNames.VALKYRIE_LAUNCHING_VECTOR_COMPENSATOR: - ItemData(390 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 8, SC2Race.TERRAN, - classification=ItemClassification.filler, parent_item=ItemNames.VALKYRIE, origin={"ext"}, - description="Allows Valkyries to shoot air while moving."), - ItemNames.VALKYRIE_RESOURCE_EFFICIENCY: - ItemData(391 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 9, SC2Race.TERRAN, - parent_item=ItemNames.VALKYRIE, origin={"ext"}, - description=RESOURCE_EFFICIENCY_DESCRIPTION_TEMPLATE.format("Valkyrie")), - ItemNames.PREDATOR_PREDATOR_S_FURY: - ItemData(392 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 10, SC2Race.TERRAN, - parent_item=ItemNames.PREDATOR, origin={"ext"}, - description="Predators can use an attack that jumps between targets."), - ItemNames.BATTLECRUISER_BEHEMOTH_PLATING: - ItemData(393 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 11, SC2Race.TERRAN, - parent_item=ItemNames.BATTLECRUISER, origin={"ext"}, - description="Increases Battlecruiser armor by 2."), - ItemNames.BATTLECRUISER_COVERT_OPS_ENGINES: - ItemData(394 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 12, SC2Race.TERRAN, - parent_item=ItemNames.BATTLECRUISER, origin={"nco"}, - description="Increases Battlecruiser movement speed."), - - #Buildings - ItemNames.BUNKER: - ItemData(400 + SC2WOL_ITEM_ID_OFFSET, "Building", 0, SC2Race.TERRAN, - classification=ItemClassification.progression, - description="Defensive structure. Able to load infantry units, giving them +1 range to their attacks."), - ItemNames.MISSILE_TURRET: - ItemData(401 + SC2WOL_ITEM_ID_OFFSET, "Building", 1, SC2Race.TERRAN, - classification=ItemClassification.progression, - description="Anti-air defensive structure."), - ItemNames.SENSOR_TOWER: - ItemData(402 + SC2WOL_ITEM_ID_OFFSET, "Building", 2, SC2Race.TERRAN, - description="Reveals locations of enemy units at long range."), - - ItemNames.WAR_PIGS: - ItemData(500 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 0, SC2Race.TERRAN, - classification=ItemClassification.progression, - description="Mercenary Marines"), - ItemNames.DEVIL_DOGS: - ItemData(501 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 1, SC2Race.TERRAN, - classification=ItemClassification.filler, - description="Mercenary Firebats"), - ItemNames.HAMMER_SECURITIES: - ItemData(502 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 2, SC2Race.TERRAN, - description="Mercenary Marauders"), - ItemNames.SPARTAN_COMPANY: - ItemData(503 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 3, SC2Race.TERRAN, - classification=ItemClassification.progression, - description="Mercenary Goliaths"), - ItemNames.SIEGE_BREAKERS: - ItemData(504 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 4, SC2Race.TERRAN, - description="Mercenary Siege Tanks"), - ItemNames.HELS_ANGELS: - ItemData(505 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 5, SC2Race.TERRAN, - classification=ItemClassification.progression, - description="Mercenary Vikings"), - ItemNames.DUSK_WINGS: - ItemData(506 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 6, SC2Race.TERRAN, - description="Mercenary Banshees"), - ItemNames.JACKSONS_REVENGE: - ItemData(507 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 7, SC2Race.TERRAN, - description="Mercenary Battlecruiser"), - ItemNames.SKIBIS_ANGELS: - ItemData(508 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 8, SC2Race.TERRAN, - origin={"ext"}, - description="Mercenary Medics"), - ItemNames.DEATH_HEADS: - ItemData(509 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 9, SC2Race.TERRAN, - origin={"ext"}, - description="Mercenary Reapers"), - ItemNames.WINGED_NIGHTMARES: - ItemData(510 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 10, SC2Race.TERRAN, - classification=ItemClassification.progression, origin={"ext"}, - description="Mercenary Wraiths"), - ItemNames.MIDNIGHT_RIDERS: - ItemData(511 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 11, SC2Race.TERRAN, - origin={"ext"}, - description="Mercenary Liberators"), - ItemNames.BRYNHILDS: - ItemData(512 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 12, SC2Race.TERRAN, - classification=ItemClassification.progression, origin={"ext"}, - description="Mercenary Valkyries"), - ItemNames.JOTUN: - ItemData(513 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 13, SC2Race.TERRAN, - origin={"ext"}, - description="Mercenary Thor"), - - ItemNames.ULTRA_CAPACITORS: - ItemData(600 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 0, SC2Race.TERRAN, - description="Increases attack speed of units by 5% per weapon upgrade."), - ItemNames.VANADIUM_PLATING: - ItemData(601 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 1, SC2Race.TERRAN, - description="Increases the life of units by 5% per armor upgrade."), - ItemNames.ORBITAL_DEPOTS: - ItemData(602 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 2, SC2Race.TERRAN, - description="Supply depots are built instantly."), - ItemNames.MICRO_FILTERING: - ItemData(603 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 3, SC2Race.TERRAN, - description="Refineries produce Vespene gas 25% faster."), - ItemNames.AUTOMATED_REFINERY: - ItemData(604 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 4, SC2Race.TERRAN, - description="Eliminates the need for SCVs in vespene gas production."), - ItemNames.COMMAND_CENTER_REACTOR: - ItemData(605 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 5, SC2Race.TERRAN, - description="Command Centers can train two SCVs at once."), - ItemNames.RAVEN: - ItemData(606 + SC2WOL_ITEM_ID_OFFSET, "Unit", 22, SC2Race.TERRAN, - classification=ItemClassification.progression, - description="Aerial Caster unit."), - ItemNames.SCIENCE_VESSEL: - ItemData(607 + SC2WOL_ITEM_ID_OFFSET, "Unit", 23, SC2Race.TERRAN, - classification=ItemClassification.progression, - description="Aerial Caster unit. Can repair mechanical units."), - ItemNames.TECH_REACTOR: - ItemData(608 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 6, SC2Race.TERRAN, - description="Merges Tech Labs and Reactors into one add on structure to provide both functions."), - ItemNames.ORBITAL_STRIKE: - ItemData(609 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 7, SC2Race.TERRAN, - description="Trained units from Barracks are instantly deployed on rally point."), - ItemNames.BUNKER_SHRIKE_TURRET: - ItemData(610 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 6, SC2Race.TERRAN, - parent_item=ItemNames.BUNKER, - description="Adds an automated turret to Bunkers."), - ItemNames.BUNKER_FORTIFIED_BUNKER: - ItemData(611 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 7, SC2Race.TERRAN, - parent_item=ItemNames.BUNKER, - description="Bunkers have more life."), - ItemNames.PLANETARY_FORTRESS: - ItemData(612 + SC2WOL_ITEM_ID_OFFSET, "Building", 3, SC2Race.TERRAN, - classification=ItemClassification.progression, - description=inspect.cleandoc( - """ - Allows Command Centers to upgrade into a defensive structure with a turret and additional armor. - Planetary Fortresses cannot Lift Off, or cast Orbital Command spells. - """ - )), - ItemNames.PERDITION_TURRET: - ItemData(613 + SC2WOL_ITEM_ID_OFFSET, "Building", 4, SC2Race.TERRAN, - classification=ItemClassification.progression, - description="Automated defensive turret. Burrows down while no enemies are nearby."), - ItemNames.PREDATOR: - ItemData(614 + SC2WOL_ITEM_ID_OFFSET, "Unit", 24, SC2Race.TERRAN, - classification=ItemClassification.filler, - description="Anti-infantry specialist that deals area damage with each attack."), - ItemNames.HERCULES: - ItemData(615 + SC2WOL_ITEM_ID_OFFSET, "Unit", 25, SC2Race.TERRAN, - classification=ItemClassification.progression, - description="Massive transport ship."), - ItemNames.CELLULAR_REACTOR: - ItemData(616 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 8, SC2Race.TERRAN, - description="All Terran spellcasters get +100 starting and maximum energy."), - ItemNames.PROGRESSIVE_REGENERATIVE_BIO_STEEL: - ItemData(617 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 4, SC2Race.TERRAN, quantity=3, - classification= ItemClassification.progression, - description=inspect.cleandoc( - """ - Allows Terran mechanical units to regenerate health while not in combat. - Each level increases life regeneration speed. - """ - )), - ItemNames.HIVE_MIND_EMULATOR: - ItemData(618 + SC2WOL_ITEM_ID_OFFSET, "Building", 5, SC2Race.TERRAN, - ItemClassification.progression, - description="Defensive structure. Can permanently Mind Control Zerg units."), - ItemNames.PSI_DISRUPTER: - ItemData(619 + SC2WOL_ITEM_ID_OFFSET, "Building", 6, SC2Race.TERRAN, - classification=ItemClassification.progression, - description="Defensive structure. Slows the attack and movement speeds of all nearby Zerg units."), - ItemNames.STRUCTURE_ARMOR: - ItemData(620 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 9, SC2Race.TERRAN, - description="Increases armor of all Terran structures by 2.", origin={"ext"}), - ItemNames.HI_SEC_AUTO_TRACKING: - ItemData(621 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 10, SC2Race.TERRAN, - description="Increases attack range of all Terran structures by 1.", origin={"ext"}), - ItemNames.ADVANCED_OPTICS: - ItemData(622 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 11, SC2Race.TERRAN, - description="Increases attack range of all Terran mechanical units by 1.", origin={"ext"}), - ItemNames.ROGUE_FORCES: - ItemData(623 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 12, SC2Race.TERRAN, - description="Mercenary calldowns are no longer limited by charges.", origin={"ext"}), - - ItemNames.ZEALOT: - ItemData(700 + SC2WOL_ITEM_ID_OFFSET, "Unit", 0, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"wol", "lotv"}, - description="Powerful melee warrior. Can use the charge ability."), - ItemNames.STALKER: - ItemData(701 + SC2WOL_ITEM_ID_OFFSET, "Unit", 1, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"wol", "lotv"}, - description="Ranged attack strider. Can use the Blink ability."), - ItemNames.HIGH_TEMPLAR: - ItemData(702 + SC2WOL_ITEM_ID_OFFSET, "Unit", 2, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"wol", "lotv"}, - description="Potent psionic master. Can use the Feedback and Psionic Storm abilities. Can merge into an Archon."), - ItemNames.DARK_TEMPLAR: - ItemData(703 + SC2WOL_ITEM_ID_OFFSET, "Unit", 3, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"wol", "lotv"}, - description="Deadly warrior-assassin. Permanently cloaked. Can use the Shadow Fury ability."), - ItemNames.IMMORTAL: - ItemData(704 + SC2WOL_ITEM_ID_OFFSET, "Unit", 4, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"wol", "lotv"}, - description="Assault strider. Can use Barrier to absorb damage."), - ItemNames.COLOSSUS: - ItemData(705 + SC2WOL_ITEM_ID_OFFSET, "Unit", 5, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"wol", "lotv"}, - description="Battle strider with a powerful area attack. Can walk up and down cliffs. Attacks set fire to the ground, dealing extra damage to enemies over time."), - ItemNames.PHOENIX: - ItemData(706 + SC2WOL_ITEM_ID_OFFSET, "Unit", 6, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"wol", "lotv"}, - description="Air superiority starfighter. Can use Graviton Beam and Phasing Armor abilities."), - ItemNames.VOID_RAY: - ItemData(707 + SC2WOL_ITEM_ID_OFFSET, "Unit", 7, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"wol", "lotv"}, - description="Surgical strike craft. Has the Prismatic Alignment and Prismatic Range abilities."), - ItemNames.CARRIER: - ItemData(708 + SC2WOL_ITEM_ID_OFFSET, "Unit", 8, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"wol", "lotv"}, - description="Capital ship. Builds and launches Interceptors that attack enemy targets. Repair Drones heal nearby mechanical units."), - - # Filler items to fill remaining spots - ItemNames.STARTING_MINERALS: - ItemData(800 + SC2WOL_ITEM_ID_OFFSET, "Minerals", 15, SC2Race.ANY, quantity=0, - classification=ItemClassification.filler, - description="Increases the starting minerals for all missions."), - ItemNames.STARTING_VESPENE: - ItemData(801 + SC2WOL_ITEM_ID_OFFSET, "Vespene", 15, SC2Race.ANY, quantity=0, - classification=ItemClassification.filler, - description="Increases the starting vespene for all missions."), - ItemNames.STARTING_SUPPLY: - ItemData(802 + SC2WOL_ITEM_ID_OFFSET, "Supply", 2, SC2Race.ANY, quantity=0, - classification=ItemClassification.filler, - description="Increases the starting supply for all missions."), - # This item is used to "remove" location from the game. Never placed unless plando'd - ItemNames.NOTHING: - ItemData(803 + SC2WOL_ITEM_ID_OFFSET, "Nothing Group", 2, SC2Race.ANY, quantity=0, - classification=ItemClassification.trap, - description="Does nothing. Used to remove a location from the game."), - - # Nova gear - ItemNames.NOVA_GHOST_VISOR: - ItemData(900 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 0, SC2Race.TERRAN, origin={"nco"}, - description="Reveals the locations of enemy units in the fog of war around Nova. Can detect cloaked units."), - ItemNames.NOVA_RANGEFINDER_OCULUS: - ItemData(901 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 1, SC2Race.TERRAN, origin={"nco"}, - description="Increaases Nova's vision range and non-melee weapon attack range by 2. Also increases range of melee weapons by 1."), - ItemNames.NOVA_DOMINATION: - ItemData(902 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 2, SC2Race.TERRAN, origin={"nco"}, - classification=ItemClassification.progression, - description="Gives Nova the ability to mind-control a target enemy unit."), - ItemNames.NOVA_BLINK: - ItemData(903 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 3, SC2Race.TERRAN, origin={"nco"}, - classification=ItemClassification.progression, - description="Gives Nova the ability to teleport a short distance and cloak for 10s."), - ItemNames.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE: - ItemData(904 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade 2", 0, SC2Race.TERRAN, quantity=2, origin={"nco"}, - classification=ItemClassification.progression, - description=inspect.cleandoc( - """ - Level 1: Gives Nova the ability to cloak. - Level 2: Nova is permanently cloaked. - """ - )), - ItemNames.NOVA_ENERGY_SUIT_MODULE: - ItemData(905 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 4, SC2Race.TERRAN, origin={"nco"}, - description="Increases Nova's maximum energy and energy regeneration rate."), - ItemNames.NOVA_ARMORED_SUIT_MODULE: - ItemData(906 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 5, SC2Race.TERRAN, origin={"nco"}, - classification=ItemClassification.progression, - description="Increases Nova's health by 100 and armour by 1. Nova also regenerates life quickly out of combat."), - ItemNames.NOVA_JUMP_SUIT_MODULE: - ItemData(907 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 6, SC2Race.TERRAN, origin={"nco"}, - classification=ItemClassification.progression, - description="Increases Nova's movement speed and allows her to jump up and down cliffs."), - ItemNames.NOVA_C20A_CANISTER_RIFLE: - ItemData(908 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 7, SC2Race.TERRAN, origin={"nco"}, - classification=ItemClassification.progression, - description="Allows Nova to equip the C20A Canister Rifle, which has a ranged attack and allows Nova to cast Snipe."), - ItemNames.NOVA_HELLFIRE_SHOTGUN: - ItemData(909 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 8, SC2Race.TERRAN, origin={"nco"}, - classification=ItemClassification.progression, - description="Allows Nova to equip the Hellfire Shotgun, which has a short-range area attack in a cone and allows Nova to cast Penetrating Blast."), - ItemNames.NOVA_PLASMA_RIFLE: - ItemData(910 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 9, SC2Race.TERRAN, origin={"nco"}, - classification=ItemClassification.progression, - description="Allows Nova to equip the Plasma Rifle, which has a rapidfire ranged attack and allows Nova to cast Plasma Shot."), - ItemNames.NOVA_MONOMOLECULAR_BLADE: - ItemData(911 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 10, SC2Race.TERRAN, origin={"nco"}, - classification=ItemClassification.progression, - description="Allows Nova to equip the Monomolecular Blade, which has a melee attack and allows Nova to cast Dash Attack."), - ItemNames.NOVA_BLAZEFIRE_GUNBLADE: - ItemData(912 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 11, SC2Race.TERRAN, origin={"nco"}, - classification=ItemClassification.progression, - description="Allows Nova to equip the Blazefire Gunblade, which has a melee attack and allows Nova to cast Fury of One."), - ItemNames.NOVA_STIM_INFUSION: - ItemData(913 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 12, SC2Race.TERRAN, origin={"nco"}, - classification=ItemClassification.progression, - description="Gives Nova the ability to heal herself and temporarily increase her movement and attack speeds."), - ItemNames.NOVA_PULSE_GRENADES: - ItemData(914 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 13, SC2Race.TERRAN, origin={"nco"}, - classification=ItemClassification.progression, - description="Gives Nova the ability to throw a grenade dealing large damage in an area."), - ItemNames.NOVA_FLASHBANG_GRENADES: - ItemData(915 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 14, SC2Race.TERRAN, origin={"nco"}, - classification=ItemClassification.progression, - description="Gives Nova the ability to throw a grenade to stun enemies and disable detection in a large area."), - ItemNames.NOVA_IONIC_FORCE_FIELD: - ItemData(916 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 15, SC2Race.TERRAN, origin={"nco"}, - classification=ItemClassification.progression, - description="Gives Nova the ability to shield herself temporarily."), - ItemNames.NOVA_HOLO_DECOY: - ItemData(917 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 16, SC2Race.TERRAN, origin={"nco"}, - classification=ItemClassification.progression, - description="Gives Nova the ability to summon a decoy unit which enemies will prefer to target and takes reduced damage."), - ItemNames.NOVA_NUKE: - ItemData(918 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 17, SC2Race.TERRAN, origin={"nco"}, - classification=ItemClassification.progression, - description="Gives Nova the ability to launch tactical nukes built from the Shadow Ops."), - - # HotS - ItemNames.ZERGLING: - ItemData(0 + SC2HOTS_ITEM_ID_OFFSET, "Unit", 0, SC2Race.ZERG, - classification=ItemClassification.progression, origin={"hots"}, - description="Fast inexpensive melee attacker. Hatches in pairs from a single larva. Can morph into a Baneling."), - ItemNames.SWARM_QUEEN: - ItemData(1 + SC2HOTS_ITEM_ID_OFFSET, "Unit", 1, SC2Race.ZERG, - classification=ItemClassification.progression, origin={"hots"}, - description="Ranged support caster. Can use the Spawn Creep Tumor and Rapid Transfusion abilities."), - ItemNames.ROACH: - ItemData(2 + SC2HOTS_ITEM_ID_OFFSET, "Unit", 2, SC2Race.ZERG, - classification=ItemClassification.progression, origin={"hots"}, - description="Durable short ranged attacker. Regenerates life quickly when burrowed."), - ItemNames.HYDRALISK: - ItemData(3 + SC2HOTS_ITEM_ID_OFFSET, "Unit", 3, SC2Race.ZERG, - classification=ItemClassification.progression, origin={"hots"}, - description="High-damage generalist ranged attacker."), - ItemNames.ZERGLING_BANELING_ASPECT: - ItemData(4 + SC2HOTS_ITEM_ID_OFFSET, "Morph", 5, SC2Race.ZERG, - classification=ItemClassification.progression, origin={"hots"}, - description="Anti-ground suicide unit. Does damage over a small area on death."), - ItemNames.ABERRATION: - ItemData(5 + SC2HOTS_ITEM_ID_OFFSET, "Unit", 5, SC2Race.ZERG, - classification=ItemClassification.progression, origin={"hots"}, - description="Durable melee attacker that deals heavy damage and can walk over other units."), - ItemNames.MUTALISK: - ItemData(6 + SC2HOTS_ITEM_ID_OFFSET, "Unit", 6, SC2Race.ZERG, - classification=ItemClassification.progression, origin={"hots"}, - description="Fragile flying attacker. Attacks bounce between targets."), - ItemNames.SWARM_HOST: - ItemData(7 + SC2HOTS_ITEM_ID_OFFSET, "Unit", 7, SC2Race.ZERG, - classification=ItemClassification.progression, origin={"hots"}, - description="Siege unit that attacks by rooting in place and continually spawning Locusts."), - ItemNames.INFESTOR: - ItemData(8 + SC2HOTS_ITEM_ID_OFFSET, "Unit", 8, SC2Race.ZERG, - classification=ItemClassification.progression, origin={"hots"}, - description="Support caster that can move while burrowed. Can use the Fungal Growth, Parasitic Domination, and Consumption abilities."), - ItemNames.ULTRALISK: - ItemData(9 + SC2HOTS_ITEM_ID_OFFSET, "Unit", 9, SC2Race.ZERG, - classification=ItemClassification.progression, origin={"hots"}, - description="Massive melee attacker. Has an area-damage cleave attack."), - ItemNames.SPORE_CRAWLER: - ItemData(10 + SC2HOTS_ITEM_ID_OFFSET, "Unit", 10, SC2Race.ZERG, - classification=ItemClassification.progression, origin={"hots"}, - description="Anti-air defensive structure that can detect cloaked units."), - ItemNames.SPINE_CRAWLER: - ItemData(11 + SC2HOTS_ITEM_ID_OFFSET, "Unit", 11, SC2Race.ZERG, - classification=ItemClassification.progression, origin={"hots"}, - description="Anti-ground defensive structure."), - ItemNames.CORRUPTOR: - ItemData(12 + SC2HOTS_ITEM_ID_OFFSET, "Unit", 12, SC2Race.ZERG, - classification=ItemClassification.progression, origin={"ext"}, - description="Anti-air flying attacker specializing in taking down enemy capital ships."), - ItemNames.SCOURGE: - ItemData(13 + SC2HOTS_ITEM_ID_OFFSET, "Unit", 13, SC2Race.ZERG, - classification=ItemClassification.progression, origin={"bw", "ext"}, - description="Flying anti-air suicide unit. Hatches in pairs from a single larva."), - ItemNames.BROOD_QUEEN: - ItemData(14 + SC2HOTS_ITEM_ID_OFFSET, "Unit", 4, SC2Race.ZERG, - classification=ItemClassification.progression, origin={"bw", "ext"}, - description="Flying support caster. Can cast the Ocular Symbiote and Spawn Broodlings abilities."), - ItemNames.DEFILER: - ItemData(15 + SC2HOTS_ITEM_ID_OFFSET, "Unit", 14, SC2Race.ZERG, - classification=ItemClassification.progression, origin={"bw"}, - description="Support caster. Can use the Dark Swarm, Consume, and Plague abilities."), - - ItemNames.PROGRESSIVE_ZERG_MELEE_ATTACK: ItemData(100 + SC2HOTS_ITEM_ID_OFFSET, "Upgrade", 0, SC2Race.ZERG, quantity=3, origin={"hots"}), - ItemNames.PROGRESSIVE_ZERG_MISSILE_ATTACK: ItemData(101 + SC2HOTS_ITEM_ID_OFFSET, "Upgrade", 2, SC2Race.ZERG, quantity=3, origin={"hots"}), - ItemNames.PROGRESSIVE_ZERG_GROUND_CARAPACE: ItemData(102 + SC2HOTS_ITEM_ID_OFFSET, "Upgrade", 4, SC2Race.ZERG, quantity=3, origin={"hots"}), - ItemNames.PROGRESSIVE_ZERG_FLYER_ATTACK: ItemData(103 + SC2HOTS_ITEM_ID_OFFSET, "Upgrade", 6, SC2Race.ZERG, quantity=3, origin={"hots"}), - ItemNames.PROGRESSIVE_ZERG_FLYER_CARAPACE: ItemData(104 + SC2HOTS_ITEM_ID_OFFSET, "Upgrade", 8, SC2Race.ZERG, quantity=3, origin={"hots"}), - # Upgrade bundle 'number' values are used as indices to get affected 'number's - ItemNames.PROGRESSIVE_ZERG_WEAPON_UPGRADE: ItemData(105 + SC2HOTS_ITEM_ID_OFFSET, "Upgrade", 6, SC2Race.ZERG, quantity=3, origin={"hots"}), - ItemNames.PROGRESSIVE_ZERG_ARMOR_UPGRADE: ItemData(106 + SC2HOTS_ITEM_ID_OFFSET, "Upgrade", 7, SC2Race.ZERG, quantity=3, origin={"hots"}), - ItemNames.PROGRESSIVE_ZERG_GROUND_UPGRADE: ItemData(107 + SC2HOTS_ITEM_ID_OFFSET, "Upgrade", 8, SC2Race.ZERG, quantity=3, origin={"hots"}), - ItemNames.PROGRESSIVE_ZERG_FLYER_UPGRADE: ItemData(108 + SC2HOTS_ITEM_ID_OFFSET, "Upgrade", 9, SC2Race.ZERG, quantity=3, origin={"hots"}), - ItemNames.PROGRESSIVE_ZERG_WEAPON_ARMOR_UPGRADE: ItemData(109 + SC2HOTS_ITEM_ID_OFFSET, "Upgrade", 10, SC2Race.ZERG, quantity=3, origin={"hots"}), - - ItemNames.ZERGLING_HARDENED_CARAPACE: - ItemData(200 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 0, SC2Race.ZERG, parent_item=ItemNames.ZERGLING, - origin={"hots"}, description="Increases Zergling health by +10."), - ItemNames.ZERGLING_ADRENAL_OVERLOAD: - ItemData(201 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 1, SC2Race.ZERG, parent_item=ItemNames.ZERGLING, - origin={"hots"}, description="Increases Zergling attack speed."), - ItemNames.ZERGLING_METABOLIC_BOOST: - ItemData(202 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 2, SC2Race.ZERG, parent_item=ItemNames.ZERGLING, - origin={"hots"}, classification=ItemClassification.filler, - description="Increases Zergling movement speed."), - ItemNames.ROACH_HYDRIODIC_BILE: - ItemData(203 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 3, SC2Race.ZERG, parent_item=ItemNames.ROACH, - origin={"hots"}, description="Roaches deal +8 damage to light targets."), - ItemNames.ROACH_ADAPTIVE_PLATING: - ItemData(204 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 4, SC2Race.ZERG, parent_item=ItemNames.ROACH, - origin={"hots"}, description="Roaches gain +3 armour when their life is below 50%."), - ItemNames.ROACH_TUNNELING_CLAWS: - ItemData(205 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 5, SC2Race.ZERG, parent_item=ItemNames.ROACH, - origin={"hots"}, classification=ItemClassification.filler, - description="Allows Roaches to move while burrowed."), - ItemNames.HYDRALISK_FRENZY: - ItemData(206 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 6, SC2Race.ZERG, parent_item=ItemNames.HYDRALISK, - origin={"hots"}, - description="Allows Hydralisks to use the Frenzy ability, which increases their attack speed by 50%."), - ItemNames.HYDRALISK_ANCILLARY_CARAPACE: - ItemData(207 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 7, SC2Race.ZERG, parent_item=ItemNames.HYDRALISK, - origin={"hots"}, classification=ItemClassification.filler, description="Hydralisks gain +20 health."), - ItemNames.HYDRALISK_GROOVED_SPINES: - ItemData(208 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 8, SC2Race.ZERG, parent_item=ItemNames.HYDRALISK, - origin={"hots"}, description="Hydralisks gain +1 range."), - ItemNames.BANELING_CORROSIVE_ACID: - ItemData(209 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 9, SC2Race.ZERG, - parent_item=ItemNames.ZERGLING_BANELING_ASPECT, origin={"hots"}, - description="Increases the damage banelings deal to their primary target. Splash damage remains the same."), - ItemNames.BANELING_RUPTURE: - ItemData(210 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 10, SC2Race.ZERG, - parent_item=ItemNames.ZERGLING_BANELING_ASPECT, origin={"hots"}, - classification=ItemClassification.filler, - description="Increases the splash radius of baneling attacks."), - ItemNames.BANELING_REGENERATIVE_ACID: - ItemData(211 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 11, SC2Race.ZERG, - parent_item=ItemNames.ZERGLING_BANELING_ASPECT, origin={"hots"}, - classification=ItemClassification.filler, - description="Banelings will heal nearby friendly units when they explode."), - ItemNames.MUTALISK_VICIOUS_GLAIVE: - ItemData(212 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 12, SC2Race.ZERG, parent_item=ItemNames.MUTALISK, - origin={"hots"}, description="Mutalisks attacks will bounce an additional 3 times."), - ItemNames.MUTALISK_RAPID_REGENERATION: - ItemData(213 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 13, SC2Race.ZERG, parent_item=ItemNames.MUTALISK, - origin={"hots"}, description="Mutalisks will regenerate quickly when out of combat."), - ItemNames.MUTALISK_SUNDERING_GLAIVE: - ItemData(214 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 14, SC2Race.ZERG, parent_item=ItemNames.MUTALISK, - origin={"hots"}, description="Mutalisks deal increased damage to their primary target."), - ItemNames.SWARM_HOST_BURROW: - ItemData(215 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 15, SC2Race.ZERG, parent_item=ItemNames.SWARM_HOST, - origin={"hots"}, classification=ItemClassification.filler, - description="Allows Swarm Hosts to burrow instead of root to spawn locusts."), - ItemNames.SWARM_HOST_RAPID_INCUBATION: - ItemData(216 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 16, SC2Race.ZERG, parent_item=ItemNames.SWARM_HOST, - origin={"hots"}, description="Swarm Hosts will spawn locusts 20% faster."), - ItemNames.SWARM_HOST_PRESSURIZED_GLANDS: - ItemData(217 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 17, SC2Race.ZERG, parent_item=ItemNames.SWARM_HOST, - origin={"hots"}, classification=ItemClassification.progression, - description="Allows Swarm Host Locusts to attack air targets."), - ItemNames.ULTRALISK_BURROW_CHARGE: - ItemData(218 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 18, SC2Race.ZERG, parent_item=ItemNames.ULTRALISK, - origin={"hots"}, - description="Allows Ultralisks to burrow and charge at enemy units, knocking back and stunning units when it emerges."), - ItemNames.ULTRALISK_TISSUE_ASSIMILATION: - ItemData(219 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 19, SC2Race.ZERG, parent_item=ItemNames.ULTRALISK, - origin={"hots"}, description="Ultralisks recover health when they deal damage."), - ItemNames.ULTRALISK_MONARCH_BLADES: - ItemData(220 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 20, SC2Race.ZERG, parent_item=ItemNames.ULTRALISK, - origin={"hots"}, description="Ultralisks gain increased splash damage."), - ItemNames.CORRUPTOR_CAUSTIC_SPRAY: - ItemData(221 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 21, SC2Race.ZERG, parent_item=ItemNames.CORRUPTOR, - origin={"ext"}, - description="Allows Corruptors to use the Caustic Spray ability, which deals ramping damage to buildings over time."), - ItemNames.CORRUPTOR_CORRUPTION: - ItemData(222 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 22, SC2Race.ZERG, parent_item=ItemNames.CORRUPTOR, - origin={"ext"}, - description="Allows Corruptors to use the Corruption ability, which causes a target enemy unit to take increased damage."), - ItemNames.SCOURGE_VIRULENT_SPORES: - ItemData(223 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 23, SC2Race.ZERG, parent_item=ItemNames.SCOURGE, - origin={"ext"}, description="Scourge will deal splash damage."), - ItemNames.SCOURGE_RESOURCE_EFFICIENCY: - ItemData(224 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 24, SC2Race.ZERG, parent_item=ItemNames.SCOURGE, - origin={"ext"}, classification=ItemClassification.progression, - description="Reduces the cost of Scourge by 50 gas per egg."), - ItemNames.SCOURGE_SWARM_SCOURGE: - ItemData(225 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 25, SC2Race.ZERG, parent_item=ItemNames.SCOURGE, - origin={"ext"}, description="An extra Scourge will be built from each egg at no additional cost."), - ItemNames.ZERGLING_SHREDDING_CLAWS: - ItemData(226 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 26, SC2Race.ZERG, parent_item=ItemNames.ZERGLING, - origin={"ext"}, description="Zergling attacks will temporarily reduce their target's armour to 0."), - ItemNames.ROACH_GLIAL_RECONSTITUTION: - ItemData(227 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 27, SC2Race.ZERG, parent_item=ItemNames.ROACH, - origin={"ext"}, description="Increases Roach movement speed."), - ItemNames.ROACH_ORGANIC_CARAPACE: - ItemData(228 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 28, SC2Race.ZERG, parent_item=ItemNames.ROACH, - origin={"ext"}, description="Increases Roach health by +25."), - ItemNames.HYDRALISK_MUSCULAR_AUGMENTS: - ItemData(229 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 29, SC2Race.ZERG, parent_item=ItemNames.HYDRALISK, - origin={"bw"}, description="Increases Hydralisk movement speed."), - ItemNames.HYDRALISK_RESOURCE_EFFICIENCY: - ItemData(230 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 0, SC2Race.ZERG, parent_item=ItemNames.HYDRALISK, - origin={"bw"}, description="Reduces Hydralisk resource cost by 25/25 and supply cost by 1."), - ItemNames.BANELING_CENTRIFUGAL_HOOKS: - ItemData(231 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 1, SC2Race.ZERG, - parent_item=ItemNames.ZERGLING_BANELING_ASPECT, origin={"ext"}, - description="Increases the movement speed of Banelings."), - ItemNames.BANELING_TUNNELING_JAWS: - ItemData(232 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 2, SC2Race.ZERG, - parent_item=ItemNames.ZERGLING_BANELING_ASPECT, origin={"ext"}, - description="Allows Banelings to move while burrowed."), - ItemNames.BANELING_RAPID_METAMORPH: - ItemData(233 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 3, SC2Race.ZERG, - parent_item=ItemNames.ZERGLING_BANELING_ASPECT, origin={"ext"}, description="Banelings morph faster."), - ItemNames.MUTALISK_SEVERING_GLAIVE: - ItemData(234 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 4, SC2Race.ZERG, parent_item=ItemNames.MUTALISK, - origin={"ext"}, description="Mutalisk bounce attacks will deal full damage."), - ItemNames.MUTALISK_AERODYNAMIC_GLAIVE_SHAPE: - ItemData(235 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 5, SC2Race.ZERG, parent_item=ItemNames.MUTALISK, - origin={"ext"}, description="Increases the attack range of Mutalisks by 2."), - ItemNames.SWARM_HOST_LOCUST_METABOLIC_BOOST: - ItemData(236 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 6, SC2Race.ZERG, parent_item=ItemNames.SWARM_HOST, - origin={"ext"}, classification=ItemClassification.filler, - description="Increases Locust movement speed."), - ItemNames.SWARM_HOST_ENDURING_LOCUSTS: - ItemData(237 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 7, SC2Race.ZERG, parent_item=ItemNames.SWARM_HOST, - origin={"ext"}, description="Increases the duration of Swarm Hosts' Locusts by 10s."), - ItemNames.SWARM_HOST_ORGANIC_CARAPACE: - ItemData(238 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 8, SC2Race.ZERG, parent_item=ItemNames.SWARM_HOST, - origin={"ext"}, description="Increases Swarm Host health by +40."), - ItemNames.SWARM_HOST_RESOURCE_EFFICIENCY: - ItemData(239 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 9, SC2Race.ZERG, parent_item=ItemNames.SWARM_HOST, - origin={"ext"}, description="Reduces Swarm Host resource cost by 100/25."), - ItemNames.ULTRALISK_ANABOLIC_SYNTHESIS: - ItemData(240 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 10, SC2Race.ZERG, parent_item=ItemNames.ULTRALISK, - origin={"bw"}, classification=ItemClassification.filler), - ItemNames.ULTRALISK_CHITINOUS_PLATING: - ItemData(241 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 11, SC2Race.ZERG, parent_item=ItemNames.ULTRALISK, - origin={"bw"}), - ItemNames.ULTRALISK_ORGANIC_CARAPACE: - ItemData(242 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 12, SC2Race.ZERG, parent_item=ItemNames.ULTRALISK, - origin={"ext"}), - ItemNames.ULTRALISK_RESOURCE_EFFICIENCY: - ItemData(243 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 13, SC2Race.ZERG, parent_item=ItemNames.ULTRALISK, - origin={"bw"}), - ItemNames.DEVOURER_CORROSIVE_SPRAY: - ItemData(244 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 14, SC2Race.ZERG, - parent_item=ItemNames.MUTALISK_CORRUPTOR_DEVOURER_ASPECT, origin={"ext"}), - ItemNames.DEVOURER_GAPING_MAW: - ItemData(245 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 15, SC2Race.ZERG, - parent_item=ItemNames.MUTALISK_CORRUPTOR_DEVOURER_ASPECT, origin={"ext"}), - ItemNames.DEVOURER_IMPROVED_OSMOSIS: - ItemData(246 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 16, SC2Race.ZERG, - parent_item=ItemNames.MUTALISK_CORRUPTOR_DEVOURER_ASPECT, origin={"ext"}, - classification=ItemClassification.filler), - ItemNames.DEVOURER_PRESCIENT_SPORES: - ItemData(247 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 17, SC2Race.ZERG, - parent_item=ItemNames.MUTALISK_CORRUPTOR_DEVOURER_ASPECT, origin={"ext"}), - ItemNames.GUARDIAN_PROLONGED_DISPERSION: - ItemData(248 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 18, SC2Race.ZERG, - parent_item=ItemNames.MUTALISK_CORRUPTOR_GUARDIAN_ASPECT, origin={"ext"}), - ItemNames.GUARDIAN_PRIMAL_ADAPTATION: - ItemData(249 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 19, SC2Race.ZERG, - parent_item=ItemNames.MUTALISK_CORRUPTOR_GUARDIAN_ASPECT, origin={"ext"}), - ItemNames.GUARDIAN_SORONAN_ACID: - ItemData(250 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 20, SC2Race.ZERG, - parent_item=ItemNames.MUTALISK_CORRUPTOR_GUARDIAN_ASPECT, origin={"ext"}), - ItemNames.IMPALER_ADAPTIVE_TALONS: - ItemData(251 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 21, SC2Race.ZERG, - parent_item=ItemNames.HYDRALISK_IMPALER_ASPECT, origin={"ext"}, - classification=ItemClassification.filler), - ItemNames.IMPALER_SECRETION_GLANDS: - ItemData(252 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 22, SC2Race.ZERG, - parent_item=ItemNames.HYDRALISK_IMPALER_ASPECT, origin={"ext"}), - ItemNames.IMPALER_HARDENED_TENTACLE_SPINES: - ItemData(253 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 23, SC2Race.ZERG, - parent_item=ItemNames.HYDRALISK_IMPALER_ASPECT, origin={"ext"}), - ItemNames.LURKER_SEISMIC_SPINES: - ItemData(254 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 24, SC2Race.ZERG, - parent_item=ItemNames.HYDRALISK_LURKER_ASPECT, origin={"ext"}), - ItemNames.LURKER_ADAPTED_SPINES: - ItemData(255 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 25, SC2Race.ZERG, - parent_item=ItemNames.HYDRALISK_LURKER_ASPECT, origin={"ext"}), - ItemNames.RAVAGER_POTENT_BILE: - ItemData(256 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 26, SC2Race.ZERG, - parent_item=ItemNames.ROACH_RAVAGER_ASPECT, origin={"ext"}), - ItemNames.RAVAGER_BLOATED_BILE_DUCTS: - ItemData(257 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 27, SC2Race.ZERG, - parent_item=ItemNames.ROACH_RAVAGER_ASPECT, origin={"ext"}), - ItemNames.RAVAGER_DEEP_TUNNEL: - ItemData(258 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 28, SC2Race.ZERG, - parent_item=ItemNames.ROACH_RAVAGER_ASPECT, origin={"ext"}), - ItemNames.VIPER_PARASITIC_BOMB: - ItemData(259 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 29, SC2Race.ZERG, - parent_item=ItemNames.MUTALISK_CORRUPTOR_VIPER_ASPECT, origin={"ext"}), - ItemNames.VIPER_PARALYTIC_BARBS: - ItemData(260 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 3", 0, SC2Race.ZERG, - parent_item=ItemNames.MUTALISK_CORRUPTOR_VIPER_ASPECT, origin={"ext"}), - ItemNames.VIPER_VIRULENT_MICROBES: - ItemData(261 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 3", 1, SC2Race.ZERG, - parent_item=ItemNames.MUTALISK_CORRUPTOR_VIPER_ASPECT, origin={"ext"}), - ItemNames.BROOD_LORD_POROUS_CARTILAGE: - ItemData(262 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 3", 2, SC2Race.ZERG, - parent_item=ItemNames.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT, origin={"ext"}), - ItemNames.BROOD_LORD_EVOLVED_CARAPACE: - ItemData(263 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 3", 3, SC2Race.ZERG, - parent_item=ItemNames.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT, origin={"ext"}), - ItemNames.BROOD_LORD_SPLITTER_MITOSIS: - ItemData(264 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 3", 4, SC2Race.ZERG, - parent_item=ItemNames.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT, origin={"ext"}), - ItemNames.BROOD_LORD_RESOURCE_EFFICIENCY: - ItemData(265 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 3", 5, SC2Race.ZERG, - parent_item=ItemNames.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT, origin={"ext"}), - ItemNames.INFESTOR_INFESTED_TERRAN: - ItemData(266 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 3", 6, SC2Race.ZERG, parent_item=ItemNames.INFESTOR, - origin={"ext"}), - ItemNames.INFESTOR_MICROBIAL_SHROUD: - ItemData(267 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 3", 7, SC2Race.ZERG, parent_item=ItemNames.INFESTOR, - origin={"ext"}), - ItemNames.SWARM_QUEEN_SPAWN_LARVAE: - ItemData(268 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 3", 8, SC2Race.ZERG, parent_item=ItemNames.SWARM_QUEEN, - origin={"ext"}), - ItemNames.SWARM_QUEEN_DEEP_TUNNEL: - ItemData(269 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 3", 9, SC2Race.ZERG, parent_item=ItemNames.SWARM_QUEEN, - origin={"ext"}), - ItemNames.SWARM_QUEEN_ORGANIC_CARAPACE: - ItemData(270 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 3", 10, SC2Race.ZERG, parent_item=ItemNames.SWARM_QUEEN, - origin={"ext"}, classification=ItemClassification.filler), - ItemNames.SWARM_QUEEN_BIO_MECHANICAL_TRANSFUSION: - ItemData(271 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 3", 11, SC2Race.ZERG, parent_item=ItemNames.SWARM_QUEEN, - origin={"ext"}), - ItemNames.SWARM_QUEEN_RESOURCE_EFFICIENCY: - ItemData(272 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 3", 12, SC2Race.ZERG, parent_item=ItemNames.SWARM_QUEEN, - origin={"ext"}), - ItemNames.SWARM_QUEEN_INCUBATOR_CHAMBER: - ItemData(273 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 3", 13, SC2Race.ZERG, parent_item=ItemNames.SWARM_QUEEN, - origin={"ext"}), - ItemNames.BROOD_QUEEN_FUNGAL_GROWTH: - ItemData(274 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 3", 14, SC2Race.ZERG, parent_item=ItemNames.BROOD_QUEEN, - origin={"ext"}), - ItemNames.BROOD_QUEEN_ENSNARE: - ItemData(275 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 3", 15, SC2Race.ZERG, parent_item=ItemNames.BROOD_QUEEN, - origin={"ext"}), - ItemNames.BROOD_QUEEN_ENHANCED_MITOCHONDRIA: - ItemData(276 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 3", 16, SC2Race.ZERG, parent_item=ItemNames.BROOD_QUEEN, - origin={"ext"}), - - ItemNames.ZERGLING_RAPTOR_STRAIN: - ItemData(300 + SC2HOTS_ITEM_ID_OFFSET, "Strain", 0, SC2Race.ZERG, parent_item=ItemNames.ZERGLING, - origin={"hots"}, - description="Allows Zerglings to jump up and down cliffs and leap onto enemies. Also increases Zergling attack damage by 2."), - ItemNames.ZERGLING_SWARMLING_STRAIN: - ItemData(301 + SC2HOTS_ITEM_ID_OFFSET, "Strain", 1, SC2Race.ZERG, parent_item=ItemNames.ZERGLING, - origin={"hots"}, - description="Zerglings will spawn instantly and with an extra Zergling per egg at no additional cost."), - ItemNames.ROACH_VILE_STRAIN: - ItemData(302 + SC2HOTS_ITEM_ID_OFFSET, "Strain", 2, SC2Race.ZERG, parent_item=ItemNames.ROACH, origin={"hots"}, - description="Roach attacks will slow the movement and attack speed of enemies."), - ItemNames.ROACH_CORPSER_STRAIN: - ItemData(303 + SC2HOTS_ITEM_ID_OFFSET, "Strain", 3, SC2Race.ZERG, parent_item=ItemNames.ROACH, origin={"hots"}, - description="Units killed after being attacked by Roaches will spawn 2 Roachlings."), - ItemNames.HYDRALISK_IMPALER_ASPECT: - ItemData(304 + SC2HOTS_ITEM_ID_OFFSET, "Morph", 0, SC2Race.ZERG, origin={"hots"}, - classification=ItemClassification.progression, - description="Allows Hydralisks to morph into Impalers."), - ItemNames.HYDRALISK_LURKER_ASPECT: - ItemData(305 + SC2HOTS_ITEM_ID_OFFSET, "Morph", 1, SC2Race.ZERG, origin={"hots"}, - classification=ItemClassification.progression, description="Allows Hydralisks to morph into Lurkers."), - ItemNames.BANELING_SPLITTER_STRAIN: - ItemData(306 + SC2HOTS_ITEM_ID_OFFSET, "Strain", 6, SC2Race.ZERG, - parent_item=ItemNames.ZERGLING_BANELING_ASPECT, origin={"hots"}, - description="Banelings will split into two smaller Splitterlings on exploding."), - ItemNames.BANELING_HUNTER_STRAIN: - ItemData(307 + SC2HOTS_ITEM_ID_OFFSET, "Strain", 7, SC2Race.ZERG, - parent_item=ItemNames.ZERGLING_BANELING_ASPECT, origin={"hots"}, - description="Allows Banelings to jump up and down cliffs and leap onto enemies."), - ItemNames.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT: - ItemData(308 + SC2HOTS_ITEM_ID_OFFSET, "Morph", 2, SC2Race.ZERG, origin={"hots"}, - classification=ItemClassification.progression, - description="Allows Mutalisks and Corruptors to morph into Brood Lords."), - ItemNames.MUTALISK_CORRUPTOR_VIPER_ASPECT: - ItemData(309 + SC2HOTS_ITEM_ID_OFFSET, "Morph", 3, SC2Race.ZERG, origin={"hots"}, - classification=ItemClassification.progression, - description="Allows Mutalisks and Corruptors to morph into Vipers."), - ItemNames.SWARM_HOST_CARRION_STRAIN: - ItemData(310 + SC2HOTS_ITEM_ID_OFFSET, "Strain", 10, SC2Race.ZERG, parent_item=ItemNames.SWARM_HOST, - origin={"hots"}, description="Swarm Hosts will spawn Flying Locusts."), - ItemNames.SWARM_HOST_CREEPER_STRAIN: - ItemData(311 + SC2HOTS_ITEM_ID_OFFSET, "Strain", 11, SC2Race.ZERG, parent_item=ItemNames.SWARM_HOST, - origin={"hots"}, classification=ItemClassification.filler, - description="Allows Swarm Hosts to teleport to any creep on the map in vision. Swarm Hosts will spread creep around them when rooted or burrowed."), - ItemNames.ULTRALISK_NOXIOUS_STRAIN: - ItemData(312 + SC2HOTS_ITEM_ID_OFFSET, "Strain", 12, SC2Race.ZERG, parent_item=ItemNames.ULTRALISK, - origin={"hots"}, classification=ItemClassification.filler, - description="Ultralisks will periodically spread poison, damaging nearby biological enemies."), - ItemNames.ULTRALISK_TORRASQUE_STRAIN: - ItemData(313 + SC2HOTS_ITEM_ID_OFFSET, "Strain", 13, SC2Race.ZERG, parent_item=ItemNames.ULTRALISK, - origin={"hots"}, description="Ultralisks will revive after being killed."), - - ItemNames.KERRIGAN_KINETIC_BLAST: ItemData(400 + SC2HOTS_ITEM_ID_OFFSET, "Ability", 0, SC2Race.ZERG, origin={"hots"}, classification=ItemClassification.progression), - ItemNames.KERRIGAN_HEROIC_FORTITUDE: ItemData(401 + SC2HOTS_ITEM_ID_OFFSET, "Ability", 1, SC2Race.ZERG, origin={"hots"}, classification=ItemClassification.progression), - ItemNames.KERRIGAN_LEAPING_STRIKE: ItemData(402 + SC2HOTS_ITEM_ID_OFFSET, "Ability", 2, SC2Race.ZERG, origin={"hots"}, classification=ItemClassification.progression), - ItemNames.KERRIGAN_CRUSHING_GRIP: ItemData(403 + SC2HOTS_ITEM_ID_OFFSET, "Ability", 3, SC2Race.ZERG, origin={"hots"}, classification=ItemClassification.progression), - ItemNames.KERRIGAN_CHAIN_REACTION: ItemData(404 + SC2HOTS_ITEM_ID_OFFSET, "Ability", 4, SC2Race.ZERG, origin={"hots"}, classification=ItemClassification.progression), - ItemNames.KERRIGAN_PSIONIC_SHIFT: ItemData(405 + SC2HOTS_ITEM_ID_OFFSET, "Ability", 5, SC2Race.ZERG, origin={"hots"}, classification=ItemClassification.progression), - ItemNames.KERRIGAN_ZERGLING_RECONSTITUTION: ItemData(406 + SC2HOTS_ITEM_ID_OFFSET, "Evolution Pit", 0, SC2Race.ZERG, origin={"hots"}, classification=ItemClassification.filler), - ItemNames.KERRIGAN_IMPROVED_OVERLORDS: ItemData(407 + SC2HOTS_ITEM_ID_OFFSET, "Evolution Pit", 1, SC2Race.ZERG, origin={"hots"}), - ItemNames.KERRIGAN_AUTOMATED_EXTRACTORS: ItemData(408 + SC2HOTS_ITEM_ID_OFFSET, "Evolution Pit", 2, SC2Race.ZERG, origin={"hots"}), - ItemNames.KERRIGAN_WILD_MUTATION: ItemData(409 + SC2HOTS_ITEM_ID_OFFSET, "Ability", 6, SC2Race.ZERG, origin={"hots"}, classification=ItemClassification.progression), - ItemNames.KERRIGAN_SPAWN_BANELINGS: ItemData(410 + SC2HOTS_ITEM_ID_OFFSET, "Ability", 7, SC2Race.ZERG, origin={"hots"}, classification=ItemClassification.progression), - ItemNames.KERRIGAN_MEND: ItemData(411 + SC2HOTS_ITEM_ID_OFFSET, "Ability", 8, SC2Race.ZERG, origin={"hots"}, classification=ItemClassification.progression), - ItemNames.KERRIGAN_TWIN_DRONES: ItemData(412 + SC2HOTS_ITEM_ID_OFFSET, "Evolution Pit", 3, SC2Race.ZERG, origin={"hots"}), - ItemNames.KERRIGAN_MALIGNANT_CREEP: ItemData(413 + SC2HOTS_ITEM_ID_OFFSET, "Evolution Pit", 4, SC2Race.ZERG, origin={"hots"}), - ItemNames.KERRIGAN_VESPENE_EFFICIENCY: ItemData(414 + SC2HOTS_ITEM_ID_OFFSET, "Evolution Pit", 5, SC2Race.ZERG, origin={"hots"}), - ItemNames.KERRIGAN_INFEST_BROODLINGS: ItemData(415 + SC2HOTS_ITEM_ID_OFFSET, "Ability", 9, SC2Race.ZERG, origin={"hots"}, classification=ItemClassification.progression), - ItemNames.KERRIGAN_FURY: ItemData(416 + SC2HOTS_ITEM_ID_OFFSET, "Ability", 10, SC2Race.ZERG, origin={"hots"}, classification=ItemClassification.progression), - ItemNames.KERRIGAN_ABILITY_EFFICIENCY: ItemData(417 + SC2HOTS_ITEM_ID_OFFSET, "Ability", 11, SC2Race.ZERG, origin={"hots"}), - ItemNames.KERRIGAN_APOCALYPSE: ItemData(418 + SC2HOTS_ITEM_ID_OFFSET, "Ability", 12, SC2Race.ZERG, origin={"hots"}, classification=ItemClassification.progression), - ItemNames.KERRIGAN_SPAWN_LEVIATHAN: ItemData(419 + SC2HOTS_ITEM_ID_OFFSET, "Ability", 13, SC2Race.ZERG, origin={"hots"}, classification=ItemClassification.progression), - ItemNames.KERRIGAN_DROP_PODS: ItemData(420 + SC2HOTS_ITEM_ID_OFFSET, "Ability", 14, SC2Race.ZERG, origin={"hots"}, classification=ItemClassification.progression), - # Handled separately from other abilities - ItemNames.KERRIGAN_PRIMAL_FORM: ItemData(421 + SC2HOTS_ITEM_ID_OFFSET, "Primal Form", 0, SC2Race.ZERG, origin={"hots"}), - - ItemNames.KERRIGAN_LEVELS_10: ItemData(500 + SC2HOTS_ITEM_ID_OFFSET, "Level", 10, SC2Race.ZERG, origin={"hots"}, quantity=0, classification=ItemClassification.progression), - ItemNames.KERRIGAN_LEVELS_9: ItemData(501 + SC2HOTS_ITEM_ID_OFFSET, "Level", 9, SC2Race.ZERG, origin={"hots"}, quantity=0, classification=ItemClassification.progression), - ItemNames.KERRIGAN_LEVELS_8: ItemData(502 + SC2HOTS_ITEM_ID_OFFSET, "Level", 8, SC2Race.ZERG, origin={"hots"}, quantity=0, classification=ItemClassification.progression), - ItemNames.KERRIGAN_LEVELS_7: ItemData(503 + SC2HOTS_ITEM_ID_OFFSET, "Level", 7, SC2Race.ZERG, origin={"hots"}, quantity=0, classification=ItemClassification.progression), - ItemNames.KERRIGAN_LEVELS_6: ItemData(504 + SC2HOTS_ITEM_ID_OFFSET, "Level", 6, SC2Race.ZERG, origin={"hots"}, quantity=0, classification=ItemClassification.progression), - ItemNames.KERRIGAN_LEVELS_5: ItemData(505 + SC2HOTS_ITEM_ID_OFFSET, "Level", 5, SC2Race.ZERG, origin={"hots"}, quantity=0, classification=ItemClassification.progression), - ItemNames.KERRIGAN_LEVELS_4: ItemData(506 + SC2HOTS_ITEM_ID_OFFSET, "Level", 4, SC2Race.ZERG, origin={"hots"}, quantity=0, classification=ItemClassification.progression_skip_balancing), - ItemNames.KERRIGAN_LEVELS_3: ItemData(507 + SC2HOTS_ITEM_ID_OFFSET, "Level", 3, SC2Race.ZERG, origin={"hots"}, quantity=0, classification=ItemClassification.progression_skip_balancing), - ItemNames.KERRIGAN_LEVELS_2: ItemData(508 + SC2HOTS_ITEM_ID_OFFSET, "Level", 2, SC2Race.ZERG, origin={"hots"}, quantity=0, classification=ItemClassification.progression_skip_balancing), - ItemNames.KERRIGAN_LEVELS_1: ItemData(509 + SC2HOTS_ITEM_ID_OFFSET, "Level", 1, SC2Race.ZERG, origin={"hots"}, quantity=0, classification=ItemClassification.progression_skip_balancing), - ItemNames.KERRIGAN_LEVELS_14: ItemData(510 + SC2HOTS_ITEM_ID_OFFSET, "Level", 14, SC2Race.ZERG, origin={"hots"}, quantity=0, classification=ItemClassification.progression), - ItemNames.KERRIGAN_LEVELS_35: ItemData(511 + SC2HOTS_ITEM_ID_OFFSET, "Level", 35, SC2Race.ZERG, origin={"hots"}, quantity=0, classification=ItemClassification.progression), - ItemNames.KERRIGAN_LEVELS_70: ItemData(512 + SC2HOTS_ITEM_ID_OFFSET, "Level", 70, SC2Race.ZERG, origin={"hots"}, quantity=0, classification=ItemClassification.progression), - - # Zerg Mercs - ItemNames.INFESTED_MEDICS: ItemData(600 + SC2HOTS_ITEM_ID_OFFSET, "Mercenary", 0, SC2Race.ZERG, origin={"ext"}), - ItemNames.INFESTED_SIEGE_TANKS: ItemData(601 + SC2HOTS_ITEM_ID_OFFSET, "Mercenary", 1, SC2Race.ZERG, origin={"ext"}), - ItemNames.INFESTED_BANSHEES: ItemData(602 + SC2HOTS_ITEM_ID_OFFSET, "Mercenary", 2, SC2Race.ZERG, origin={"ext"}), - - # Misc Upgrades - ItemNames.OVERLORD_VENTRAL_SACS: ItemData(700 + SC2HOTS_ITEM_ID_OFFSET, "Evolution Pit", 6, SC2Race.ZERG, origin={"bw"}), - - # Morphs - ItemNames.MUTALISK_CORRUPTOR_GUARDIAN_ASPECT: ItemData(800 + SC2HOTS_ITEM_ID_OFFSET, "Morph", 6, SC2Race.ZERG, origin={"bw"}), - ItemNames.MUTALISK_CORRUPTOR_DEVOURER_ASPECT: ItemData(801 + SC2HOTS_ITEM_ID_OFFSET, "Morph", 7, SC2Race.ZERG, origin={"bw"}), - ItemNames.ROACH_RAVAGER_ASPECT: ItemData(802 + SC2HOTS_ITEM_ID_OFFSET, "Morph", 8, SC2Race.ZERG, origin={"ext"}), - - - # Protoss Units (those that aren't as items in WoL (Prophecy)) - ItemNames.OBSERVER: ItemData(0 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 9, SC2Race.PROTOSS, - classification=ItemClassification.filler, origin={"wol"}, - description="Flying spy. Cloak renders the unit invisible to enemies without detection."), - ItemNames.CENTURION: ItemData(1 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 10, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"lotv"}, - description="Powerful melee warrior. Has the Shadow Charge and Darkcoil abilities."), - ItemNames.SENTINEL: ItemData(2 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 11, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"lotv"}, - description="Powerful melee warrior. Has the Charge and Reconstruction abilities."), - ItemNames.SUPPLICANT: ItemData(3 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 12, SC2Race.PROTOSS, - classification=ItemClassification.filler, important_for_filtering=True, origin={"ext"}, - description="Powerful melee warrior. Has powerful damage resistant shields."), - ItemNames.INSTIGATOR: ItemData(4 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 13, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"ext"}, - description="Ranged support strider. Can store multiple Blink charges."), - ItemNames.SLAYER: ItemData(5 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 14, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"ext"}, - description="Ranged attack strider. Can use the Phase Blink and Phasing Armor abilities."), - ItemNames.SENTRY: ItemData(6 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 15, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"lotv"}, - description="Robotic support unit can use the Guardian Shield ability and restore the shields of nearby Protoss units."), - ItemNames.ENERGIZER: ItemData(7 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 16, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"lotv"}, - description="Robotic support unit. Can use the Chrono Beam ability and become stationary to power nearby structures."), - ItemNames.HAVOC: ItemData(8 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 17, SC2Race.PROTOSS, - origin={"lotv"}, important_for_filtering=True, - description="Robotic support unit. Can use the Target Lock and Force Field abilities and increase the range of nearby Protoss units."), - ItemNames.SIGNIFIER: ItemData(9 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 18, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"ext"}, - description="Potent permanently cloaked psionic master. Can use the Feedback and Crippling Psionic Storm abilities. Can merge into an Archon."), - ItemNames.ASCENDANT: ItemData(10 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 19, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"lotv"}, - description="Potent psionic master. Can use the Psionic Orb, Mind Blast, and Sacrifice abilities."), - ItemNames.AVENGER: ItemData(11 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 20, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"lotv"}, - description="Deadly warrior-assassin. Permanently cloaked. Recalls to the nearest Dark Shrine upon death."), - ItemNames.BLOOD_HUNTER: ItemData(12 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 21, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"lotv"}, - description="Deadly warrior-assassin. Permanently cloaked. Can use the Void Stasis ability."), - ItemNames.DRAGOON: ItemData(13 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 22, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"lotv"}, - description="Ranged assault strider. Has enhanced health and damage."), - ItemNames.DARK_ARCHON: ItemData(14 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 23, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"lotv"}, - description="Potent psionic master. Can use the Confuse and Mind Control abilities."), - ItemNames.ADEPT: ItemData(15 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 24, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"lotv"}, - description="Ranged specialist. Can use the Psionic Transfer ability."), - ItemNames.WARP_PRISM: ItemData(16 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 25, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"ext"}, - description="Flying transport. Can carry units and become stationary to deploy a power field."), - ItemNames.ANNIHILATOR: ItemData(17 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 26, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"lotv"}, - description="Assault Strider. Can use the Shadow Cannon ability to damage air and ground units."), - ItemNames.VANGUARD: ItemData(18 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 27, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"lotv"}, - description="Assault Strider. Deals splash damage around the primary target."), - ItemNames.WRATHWALKER: ItemData(19 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 28, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"lotv"}, - description="Battle strider with a powerful single target attack. Can walk up and down cliffs."), - ItemNames.REAVER: ItemData(20 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 29, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"lotv"}, - description="Area damage siege unit. Builds and launches explosive Scarabs for high burst damage."), - ItemNames.DISRUPTOR: ItemData(21 + SC2LOTV_ITEM_ID_OFFSET, "Unit 2", 0, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"ext"}, - description="Robotic disruption unit. Can use the Purification Nova ability to deal heavy area damage."), - ItemNames.MIRAGE: ItemData(22 + SC2LOTV_ITEM_ID_OFFSET, "Unit 2", 1, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"lotv"}, - description="Air superiority starfighter. Can use Graviton Beam and Phasing Armor abilities."), - ItemNames.CORSAIR: ItemData(23 + SC2LOTV_ITEM_ID_OFFSET, "Unit 2", 2, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"lotv"}, - description="Air superiority starfighter. Can use the Disruption Web ability."), - ItemNames.DESTROYER: ItemData(24 + SC2LOTV_ITEM_ID_OFFSET, "Unit 2", 3, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"lotv"}, - description="Area assault craft. Can use the Destruction Beam ability to attack multiple units at once."), - ItemNames.SCOUT: ItemData(25 + SC2LOTV_ITEM_ID_OFFSET, "Unit 2", 4, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"ext"}, - description="Versatile high-speed fighter."), - ItemNames.TEMPEST: ItemData(26 + SC2LOTV_ITEM_ID_OFFSET, "Unit 2", 5, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"lotv"}, - description="Siege artillery craft. Attacks from long range. Can use the Disintegration ability."), - ItemNames.MOTHERSHIP: ItemData(27 + SC2LOTV_ITEM_ID_OFFSET, "Unit 2", 6, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"lotv"}, - description="Ultimate Protoss vessel, Can use the Vortex and Mass Recall abilities. Cloaks nearby units and structures."), - ItemNames.ARBITER: ItemData(28 + SC2LOTV_ITEM_ID_OFFSET, "Unit 2", 7, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"lotv"}, - description="Army support craft. Has the Stasis Field and Recall abilities. Cloaks nearby units."), - ItemNames.ORACLE: ItemData(29 + SC2LOTV_ITEM_ID_OFFSET, "Unit 2", 8, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"ext"}, - description="Flying caster. Can use the Revelation and Stasis Ward abilities."), - - # Protoss Upgrades - ItemNames.PROGRESSIVE_PROTOSS_GROUND_WEAPON: ItemData(100 + SC2LOTV_ITEM_ID_OFFSET, "Upgrade", 0, SC2Race.PROTOSS, quantity=3, origin={"wol", "lotv"}), - ItemNames.PROGRESSIVE_PROTOSS_GROUND_ARMOR: ItemData(101 + SC2LOTV_ITEM_ID_OFFSET, "Upgrade", 2, SC2Race.PROTOSS, quantity=3, origin={"wol", "lotv"}), - ItemNames.PROGRESSIVE_PROTOSS_SHIELDS: ItemData(102 + SC2LOTV_ITEM_ID_OFFSET, "Upgrade", 4, SC2Race.PROTOSS, quantity=3, origin={"wol", "lotv"}), - ItemNames.PROGRESSIVE_PROTOSS_AIR_WEAPON: ItemData(103 + SC2LOTV_ITEM_ID_OFFSET, "Upgrade", 6, SC2Race.PROTOSS, quantity=3, origin={"wol", "lotv"}), - ItemNames.PROGRESSIVE_PROTOSS_AIR_ARMOR: ItemData(104 + SC2LOTV_ITEM_ID_OFFSET, "Upgrade", 8, SC2Race.PROTOSS, quantity=3, origin={"wol", "lotv"}), - # Upgrade bundle 'number' values are used as indices to get affected 'number's - ItemNames.PROGRESSIVE_PROTOSS_WEAPON_UPGRADE: ItemData(105 + SC2LOTV_ITEM_ID_OFFSET, "Upgrade", 11, SC2Race.PROTOSS, quantity=3, origin={"wol", "lotv"}), - ItemNames.PROGRESSIVE_PROTOSS_ARMOR_UPGRADE: ItemData(106 + SC2LOTV_ITEM_ID_OFFSET, "Upgrade", 12, SC2Race.PROTOSS, quantity=3, origin={"wol", "lotv"}), - ItemNames.PROGRESSIVE_PROTOSS_GROUND_UPGRADE: ItemData(107 + SC2LOTV_ITEM_ID_OFFSET, "Upgrade", 13, SC2Race.PROTOSS, quantity=3, origin={"wol", "lotv"}), - ItemNames.PROGRESSIVE_PROTOSS_AIR_UPGRADE: ItemData(108 + SC2LOTV_ITEM_ID_OFFSET, "Upgrade", 14, SC2Race.PROTOSS, quantity=3, origin={"wol", "lotv"}), - ItemNames.PROGRESSIVE_PROTOSS_WEAPON_ARMOR_UPGRADE: ItemData(109 + SC2LOTV_ITEM_ID_OFFSET, "Upgrade", 15, SC2Race.PROTOSS, quantity=3, origin={"wol", "lotv"}), - - # Protoss Buildings - ItemNames.PHOTON_CANNON: ItemData(200 + SC2LOTV_ITEM_ID_OFFSET, "Building", 0, SC2Race.PROTOSS, classification=ItemClassification.progression, origin={"wol", "lotv"}), - ItemNames.KHAYDARIN_MONOLITH: ItemData(201 + SC2LOTV_ITEM_ID_OFFSET, "Building", 1, SC2Race.PROTOSS, classification=ItemClassification.progression, origin={"lotv"}), - ItemNames.SHIELD_BATTERY: ItemData(202 + SC2LOTV_ITEM_ID_OFFSET, "Building", 2, SC2Race.PROTOSS, classification=ItemClassification.progression, origin={"lotv"}), - - # Protoss Unit Upgrades - ItemNames.SUPPLICANT_BLOOD_SHIELD: ItemData(300 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 0, SC2Race.PROTOSS, classification=ItemClassification.filler, origin={"ext"}, parent_item=ItemNames.SUPPLICANT), - ItemNames.SUPPLICANT_SOUL_AUGMENTATION: ItemData(301 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 1, SC2Race.PROTOSS, classification=ItemClassification.filler, origin={"ext"}, parent_item=ItemNames.SUPPLICANT), - ItemNames.SUPPLICANT_SHIELD_REGENERATION: ItemData(302 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 2, SC2Race.PROTOSS, classification=ItemClassification.filler, origin={"ext"}, parent_item=ItemNames.SUPPLICANT), - ItemNames.ADEPT_SHOCKWAVE: ItemData(303 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 3, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.ADEPT), - ItemNames.ADEPT_RESONATING_GLAIVES: ItemData(304 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 4, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.ADEPT), - ItemNames.ADEPT_PHASE_BULWARK: ItemData(305 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 5, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.ADEPT), - ItemNames.STALKER_INSTIGATOR_SLAYER_DISINTEGRATING_PARTICLES: ItemData(306 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 6, SC2Race.PROTOSS, origin={"ext"}, classification=ItemClassification.progression), - ItemNames.STALKER_INSTIGATOR_SLAYER_PARTICLE_REFLECTION: ItemData(307 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 7, SC2Race.PROTOSS, origin={"ext"}, classification=ItemClassification.progression), - ItemNames.DRAGOON_HIGH_IMPACT_PHASE_DISRUPTORS: ItemData(308 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 8, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.DRAGOON), - ItemNames.DRAGOON_TRILLIC_COMPRESSION_SYSTEM: ItemData(309 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 9, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.DRAGOON), - ItemNames.DRAGOON_SINGULARITY_CHARGE: ItemData(310 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 10, SC2Race.PROTOSS, origin={"bw"}, parent_item=ItemNames.DRAGOON), - ItemNames.DRAGOON_ENHANCED_STRIDER_SERVOS: ItemData(311 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 11, SC2Race.PROTOSS, classification=ItemClassification.filler, origin={"bw"}, parent_item=ItemNames.DRAGOON), - ItemNames.SCOUT_COMBAT_SENSOR_ARRAY: ItemData(312 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 12, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.SCOUT), - ItemNames.SCOUT_APIAL_SENSORS: ItemData(313 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 13, SC2Race.PROTOSS, classification=ItemClassification.filler, origin={"bw"}, parent_item=ItemNames.SCOUT), - ItemNames.SCOUT_GRAVITIC_THRUSTERS: ItemData(314 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 14, SC2Race.PROTOSS, classification=ItemClassification.filler, origin={"bw"}, parent_item=ItemNames.SCOUT), - ItemNames.SCOUT_ADVANCED_PHOTON_BLASTERS: ItemData(315 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 15, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.SCOUT), - ItemNames.TEMPEST_TECTONIC_DESTABILIZERS: ItemData(316 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 16, SC2Race.PROTOSS, classification=ItemClassification.filler, origin={"ext"}, parent_item=ItemNames.TEMPEST), - ItemNames.TEMPEST_QUANTIC_REACTOR: ItemData(317 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 17, SC2Race.PROTOSS, classification=ItemClassification.filler, origin={"ext"}, parent_item=ItemNames.TEMPEST), - ItemNames.TEMPEST_GRAVITY_SLING: ItemData(318 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 18, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.TEMPEST), - ItemNames.PHOENIX_MIRAGE_IONIC_WAVELENGTH_FLUX: ItemData(319 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 19, SC2Race.PROTOSS, origin={"ext"}), - ItemNames.PHOENIX_MIRAGE_ANION_PULSE_CRYSTALS: ItemData(320 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 20, SC2Race.PROTOSS, origin={"ext"}), - ItemNames.CORSAIR_STEALTH_DRIVE: ItemData(321 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 21, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.CORSAIR), - ItemNames.CORSAIR_ARGUS_JEWEL: ItemData(322 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 22, SC2Race.PROTOSS, origin={"bw"}, parent_item=ItemNames.CORSAIR), - ItemNames.CORSAIR_SUSTAINING_DISRUPTION: ItemData(323 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 23, SC2Race.PROTOSS, origin={"bw"}, parent_item=ItemNames.CORSAIR), - ItemNames.CORSAIR_NEUTRON_SHIELDS: ItemData(324 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 24, SC2Race.PROTOSS, classification=ItemClassification.filler, origin={"bw"}, parent_item=ItemNames.CORSAIR), - ItemNames.ORACLE_STEALTH_DRIVE: ItemData(325 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 25, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.ORACLE), - ItemNames.ORACLE_STASIS_CALIBRATION: ItemData(326 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 26, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.ORACLE), - ItemNames.ORACLE_TEMPORAL_ACCELERATION_BEAM: ItemData(327 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 27, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.ORACLE), - ItemNames.ARBITER_CHRONOSTATIC_REINFORCEMENT: ItemData(328 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 28, SC2Race.PROTOSS, origin={"bw"}, parent_item=ItemNames.ARBITER), - ItemNames.ARBITER_KHAYDARIN_CORE: ItemData(329 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 29, SC2Race.PROTOSS, origin={"bw"}, parent_item=ItemNames.ARBITER), - ItemNames.ARBITER_SPACETIME_ANCHOR: ItemData(330 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 0, SC2Race.PROTOSS, origin={"bw"}, parent_item=ItemNames.ARBITER), - ItemNames.ARBITER_RESOURCE_EFFICIENCY: ItemData(331 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 1, SC2Race.PROTOSS, classification=ItemClassification.filler, origin={"bw"}, parent_item=ItemNames.ARBITER), - ItemNames.ARBITER_ENHANCED_CLOAK_FIELD: ItemData(332 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 2, SC2Race.PROTOSS, classification=ItemClassification.filler, origin={"bw"}, parent_item=ItemNames.ARBITER), - ItemNames.CARRIER_GRAVITON_CATAPULT: - ItemData(333 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 3, SC2Race.PROTOSS, origin={"wol"}, - parent_item=ItemNames.CARRIER, - description="Carriers can launch Interceptors more quickly."), - ItemNames.CARRIER_HULL_OF_PAST_GLORIES: - ItemData(334 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 4, SC2Race.PROTOSS, origin={"bw"}, - parent_item=ItemNames.CARRIER, - description="Carriers gain +2 armour."), - ItemNames.VOID_RAY_DESTROYER_FLUX_VANES: - ItemData(335 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 5, SC2Race.PROTOSS, classification=ItemClassification.filler, - origin={"ext"}, - description="Increases Void Ray and Destroyer movement speed."), - ItemNames.DESTROYER_REFORGED_BLOODSHARD_CORE: - ItemData(336 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 6, SC2Race.PROTOSS, origin={"ext"}, - parent_item=ItemNames.DESTROYER, - description="When fully charged, the Destroyer's Destruction Beam weapon does full damage to secondary targets."), - ItemNames.WARP_PRISM_GRAVITIC_DRIVE: - ItemData(337 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 7, SC2Race.PROTOSS, classification=ItemClassification.filler, - origin={"ext"}, parent_item=ItemNames.WARP_PRISM, - description="Increases the movement speed of Warp Prisms."), - ItemNames.WARP_PRISM_PHASE_BLASTER: - ItemData(338 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 8, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"ext"}, parent_item=ItemNames.WARP_PRISM, - description="Equips Warp Prisms with an auto-attack that can hit ground and air targets."), - ItemNames.WARP_PRISM_WAR_CONFIGURATION: ItemData(339 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 9, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.WARP_PRISM), - ItemNames.OBSERVER_GRAVITIC_BOOSTERS: ItemData(340 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 10, SC2Race.PROTOSS, classification=ItemClassification.filler, origin={"bw"}, parent_item=ItemNames.OBSERVER), - ItemNames.OBSERVER_SENSOR_ARRAY: ItemData(341 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 11, SC2Race.PROTOSS, classification=ItemClassification.filler, origin={"bw"}, parent_item=ItemNames.OBSERVER), - ItemNames.REAVER_SCARAB_DAMAGE: ItemData(342 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 12, SC2Race.PROTOSS, origin={"bw"}, parent_item=ItemNames.REAVER), - ItemNames.REAVER_SOLARITE_PAYLOAD: ItemData(343 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 13, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.REAVER), - ItemNames.REAVER_REAVER_CAPACITY: ItemData(344 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 14, SC2Race.PROTOSS, classification=ItemClassification.filler, origin={"bw"}, parent_item=ItemNames.REAVER), - ItemNames.REAVER_RESOURCE_EFFICIENCY: ItemData(345 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 15, SC2Race.PROTOSS, origin={"bw"}, parent_item=ItemNames.REAVER), - ItemNames.VANGUARD_AGONY_LAUNCHERS: ItemData(346 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 16, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.VANGUARD), - ItemNames.VANGUARD_MATTER_DISPERSION: ItemData(347 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 17, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.VANGUARD), - ItemNames.IMMORTAL_ANNIHILATOR_SINGULARITY_CHARGE: ItemData(348 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 18, SC2Race.PROTOSS, origin={"ext"}), - ItemNames.IMMORTAL_ANNIHILATOR_ADVANCED_TARGETING_MECHANICS: ItemData(349 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 19, SC2Race.PROTOSS, classification=ItemClassification.progression, origin={"ext"}), - ItemNames.COLOSSUS_PACIFICATION_PROTOCOL: ItemData(350 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 20, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.COLOSSUS), - ItemNames.WRATHWALKER_RAPID_POWER_CYCLING: ItemData(351 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 21, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.WRATHWALKER), - ItemNames.WRATHWALKER_EYE_OF_WRATH: ItemData(352 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 22, SC2Race.PROTOSS, classification=ItemClassification.filler, origin={"ext"}, parent_item=ItemNames.WRATHWALKER), - ItemNames.DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_SHROUD_OF_ADUN: ItemData(353 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 23, SC2Race.PROTOSS, origin={"ext"}), - ItemNames.DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_SHADOW_GUARD_TRAINING: ItemData(354 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 24, SC2Race.PROTOSS, origin={"bw"}), - ItemNames.DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_BLINK: ItemData(355 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 25, SC2Race.PROTOSS, classification=ItemClassification.progression, origin={"ext"}), - ItemNames.DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_RESOURCE_EFFICIENCY: ItemData(356 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 26, SC2Race.PROTOSS, origin={"ext"}), - ItemNames.DARK_TEMPLAR_DARK_ARCHON_MELD: ItemData(357 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 27, SC2Race.PROTOSS, origin={"bw"}, important_for_filtering=True ,parent_item=ItemNames.DARK_TEMPLAR), - ItemNames.HIGH_TEMPLAR_SIGNIFIER_UNSHACKLED_PSIONIC_STORM: ItemData(358 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 28, SC2Race.PROTOSS, origin={"bw"}), - ItemNames.HIGH_TEMPLAR_SIGNIFIER_HALLUCINATION: ItemData(359 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 29, SC2Race.PROTOSS, classification=ItemClassification.filler, origin={"bw"}), - ItemNames.HIGH_TEMPLAR_SIGNIFIER_KHAYDARIN_AMULET: ItemData(360 + SC2LOTV_ITEM_ID_OFFSET, "Forge 3", 0, SC2Race.PROTOSS, origin={"bw"}), - ItemNames.ARCHON_HIGH_ARCHON: ItemData(361 + SC2LOTV_ITEM_ID_OFFSET, "Forge 3", 1, SC2Race.PROTOSS, origin={"ext"}, important_for_filtering=True), - ItemNames.DARK_ARCHON_FEEDBACK: ItemData(362 + SC2LOTV_ITEM_ID_OFFSET, "Forge 3", 2, SC2Race.PROTOSS, origin={"bw"}), - ItemNames.DARK_ARCHON_MAELSTROM: ItemData(363 + SC2LOTV_ITEM_ID_OFFSET, "Forge 3", 3, SC2Race.PROTOSS, origin={"bw"}), - ItemNames.DARK_ARCHON_ARGUS_TALISMAN: ItemData(364 + SC2LOTV_ITEM_ID_OFFSET, "Forge 3", 4, SC2Race.PROTOSS, origin={"bw"}), - ItemNames.ASCENDANT_POWER_OVERWHELMING: ItemData(365 + SC2LOTV_ITEM_ID_OFFSET, "Forge 3", 5, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.ASCENDANT), - ItemNames.ASCENDANT_CHAOTIC_ATTUNEMENT: ItemData(366 + SC2LOTV_ITEM_ID_OFFSET, "Forge 3", 6, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.ASCENDANT), - ItemNames.ASCENDANT_BLOOD_AMULET: ItemData(367 + SC2LOTV_ITEM_ID_OFFSET, "Forge 3", 7, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.ASCENDANT), - ItemNames.SENTRY_ENERGIZER_HAVOC_CLOAKING_MODULE: ItemData(368 + SC2LOTV_ITEM_ID_OFFSET, "Forge 3", 8, SC2Race.PROTOSS, origin={"ext"}), - ItemNames.SENTRY_ENERGIZER_HAVOC_SHIELD_BATTERY_RAPID_RECHARGING: ItemData(369 + SC2LOTV_ITEM_ID_OFFSET, "Forge 3", 9, SC2Race.PROTOSS, origin={"ext"}), - ItemNames.SENTRY_FORCE_FIELD: ItemData(370 + SC2LOTV_ITEM_ID_OFFSET, "Forge 3", 10, SC2Race.PROTOSS, classification=ItemClassification.filler, origin={"ext"}, parent_item=ItemNames.SENTRY), - ItemNames.SENTRY_HALLUCINATION: ItemData(371 + SC2LOTV_ITEM_ID_OFFSET, "Forge 3", 11, SC2Race.PROTOSS, classification=ItemClassification.filler, origin={"ext"}, parent_item=ItemNames.SENTRY), - ItemNames.ENERGIZER_RECLAMATION: ItemData(372 + SC2LOTV_ITEM_ID_OFFSET, "Forge 3", 12, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.ENERGIZER), - ItemNames.ENERGIZER_FORGED_CHASSIS: ItemData(373 + SC2LOTV_ITEM_ID_OFFSET, "Forge 3", 13, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.ENERGIZER), - ItemNames.HAVOC_DETECT_WEAKNESS: ItemData(374 + SC2LOTV_ITEM_ID_OFFSET, "Forge 3", 14, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.HAVOC), - ItemNames.HAVOC_BLOODSHARD_RESONANCE: ItemData(375 + SC2LOTV_ITEM_ID_OFFSET, "Forge 3", 15, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.HAVOC), - ItemNames.ZEALOT_SENTINEL_CENTURION_LEG_ENHANCEMENTS: ItemData(376 + SC2LOTV_ITEM_ID_OFFSET, "Forge 3", 16, SC2Race.PROTOSS, origin={"bw"}), - ItemNames.ZEALOT_SENTINEL_CENTURION_SHIELD_CAPACITY: ItemData(377 + SC2LOTV_ITEM_ID_OFFSET, "Forge 3", 17, SC2Race.PROTOSS, origin={"bw"}), - - # SoA Calldown powers - ItemNames.SOA_CHRONO_SURGE: ItemData(700 + SC2LOTV_ITEM_ID_OFFSET, "Spear of Adun", 0, SC2Race.PROTOSS, origin={"lotv"}), - ItemNames.SOA_PROGRESSIVE_PROXY_PYLON: ItemData(701 + SC2LOTV_ITEM_ID_OFFSET, "Progressive Upgrade", 0, SC2Race.PROTOSS, origin={"lotv"}, quantity=2), - ItemNames.SOA_PYLON_OVERCHARGE: ItemData(702 + SC2LOTV_ITEM_ID_OFFSET, "Spear of Adun", 1, SC2Race.PROTOSS, origin={"ext"}), - ItemNames.SOA_ORBITAL_STRIKE: ItemData(703 + SC2LOTV_ITEM_ID_OFFSET, "Spear of Adun", 2, SC2Race.PROTOSS, origin={"lotv"}), - ItemNames.SOA_TEMPORAL_FIELD: ItemData(704 + SC2LOTV_ITEM_ID_OFFSET, "Spear of Adun", 3, SC2Race.PROTOSS, origin={"lotv"}), - ItemNames.SOA_SOLAR_LANCE: ItemData(705 + SC2LOTV_ITEM_ID_OFFSET, "Spear of Adun", 4, SC2Race.PROTOSS, classification=ItemClassification.progression, origin={"lotv"}), - ItemNames.SOA_MASS_RECALL: ItemData(706 + SC2LOTV_ITEM_ID_OFFSET, "Spear of Adun", 5, SC2Race.PROTOSS, origin={"lotv"}), - ItemNames.SOA_SHIELD_OVERCHARGE: ItemData(707 + SC2LOTV_ITEM_ID_OFFSET, "Spear of Adun", 6, SC2Race.PROTOSS, origin={"lotv"}), - ItemNames.SOA_DEPLOY_FENIX: ItemData(708 + SC2LOTV_ITEM_ID_OFFSET, "Spear of Adun", 7, SC2Race.PROTOSS, classification=ItemClassification.progression, origin={"lotv"}), - ItemNames.SOA_PURIFIER_BEAM: ItemData(709 + SC2LOTV_ITEM_ID_OFFSET, "Spear of Adun", 8, SC2Race.PROTOSS, origin={"lotv"}), - ItemNames.SOA_TIME_STOP: ItemData(710 + SC2LOTV_ITEM_ID_OFFSET, "Spear of Adun", 9, SC2Race.PROTOSS, classification=ItemClassification.progression, origin={"lotv"}), - ItemNames.SOA_SOLAR_BOMBARDMENT: ItemData(711 + SC2LOTV_ITEM_ID_OFFSET, "Spear of Adun", 10, SC2Race.PROTOSS, origin={"lotv"}), - - # Generic Protoss Upgrades - ItemNames.MATRIX_OVERLOAD: - ItemData(800 + SC2LOTV_ITEM_ID_OFFSET, "Solarite Core", 0, SC2Race.PROTOSS, origin={"lotv"}, - description=r"All friendly units gain 25% movement speed and 15% attack speed within a Pylon's power field and for 15 seconds after leaving it."), - ItemNames.QUATRO: - ItemData(801 + SC2LOTV_ITEM_ID_OFFSET, "Solarite Core", 1, SC2Race.PROTOSS, origin={"ext"}, - description="All friendly Protoss units gain the equivalent of their +1 armour, attack, and shield upgrades."), - ItemNames.NEXUS_OVERCHARGE: - ItemData(802 + SC2LOTV_ITEM_ID_OFFSET, "Solarite Core", 2, SC2Race.PROTOSS, origin={"lotv"}, - important_for_filtering=True, description="The Protoss Nexus gains a long-range auto-attack."), - ItemNames.ORBITAL_ASSIMILATORS: - ItemData(803 + SC2LOTV_ITEM_ID_OFFSET, "Solarite Core", 3, SC2Race.PROTOSS, origin={"lotv"}, - description="Assimilators automatically harvest Vespene Gas without the need for Probes."), - ItemNames.WARP_HARMONIZATION: - ItemData(804 + SC2LOTV_ITEM_ID_OFFSET, "Solarite Core", 4, SC2Race.PROTOSS, origin={"lotv"}, - description=r"Stargates and Robotics Facilities can transform to utilize Warp In technology. Warp In cooldowns are 20% faster than original build times."), - ItemNames.GUARDIAN_SHELL: - ItemData(805 + SC2LOTV_ITEM_ID_OFFSET, "Solarite Core", 5, SC2Race.PROTOSS, origin={"lotv"}, - description="The Spear of Adun passively shields friendly Protoss units before death, making them invulnerable for 5 seconds. Each unit can only be shielded once every 60 seconds."), - ItemNames.RECONSTRUCTION_BEAM: - ItemData(806 + SC2LOTV_ITEM_ID_OFFSET, "Solarite Core", 6, SC2Race.PROTOSS, - classification=ItemClassification.progression, origin={"lotv"}, - description="The Spear of Adun will passively heal mechanical units for 5 and non-biological structures for 10 life per second. Up to 3 targets can be repaired at once."), - ItemNames.OVERWATCH: - ItemData(807 + SC2LOTV_ITEM_ID_OFFSET, "Solarite Core", 7, SC2Race.PROTOSS, origin={"ext"}, - description="Once per second, the Spear of Adun will last-hit a damaged enemy unit that is below 50 health."), - ItemNames.SUPERIOR_WARP_GATES: - ItemData(808 + SC2LOTV_ITEM_ID_OFFSET, "Solarite Core", 8, SC2Race.PROTOSS, origin={"ext"}, - description="Protoss Warp Gates can hold up to 3 charges of unit warp-ins."), - ItemNames.ENHANCED_TARGETING: - ItemData(809 + SC2LOTV_ITEM_ID_OFFSET, "Solarite Core", 9, SC2Race.PROTOSS, origin={"ext"}, - description="Protoss defensive structures gain +2 range."), - ItemNames.OPTIMIZED_ORDNANCE: - ItemData(810 + SC2LOTV_ITEM_ID_OFFSET, "Solarite Core", 10, SC2Race.PROTOSS, origin={"ext"}, - description="Increases the attack speed of Protoss defensive structures by 25%."), - ItemNames.KHALAI_INGENUITY: - ItemData(811 + SC2LOTV_ITEM_ID_OFFSET, "Solarite Core", 11, SC2Race.PROTOSS, origin={"ext"}, - description="Pylons, Photon Cannons, Monoliths, and Shield Batteries warp in near-instantly."), - ItemNames.AMPLIFIED_ASSIMILATORS: - ItemData(812 + SC2LOTV_ITEM_ID_OFFSET, "Solarite Core", 12, SC2Race.PROTOSS, origin={"ext"}, - description=r"Assimilators produce Vespene gas 25% faster."), -} - - -def get_item_table(): - return item_table - - -basic_units = { - SC2Race.TERRAN: { - ItemNames.MARINE, - ItemNames.MARAUDER, - ItemNames.GOLIATH, - ItemNames.HELLION, - ItemNames.VULTURE, - ItemNames.WARHOUND, - }, - SC2Race.ZERG: { - ItemNames.ZERGLING, - ItemNames.SWARM_QUEEN, - ItemNames.ROACH, - ItemNames.HYDRALISK, - }, - SC2Race.PROTOSS: { - ItemNames.ZEALOT, - ItemNames.CENTURION, - ItemNames.SENTINEL, - ItemNames.STALKER, - ItemNames.INSTIGATOR, - ItemNames.SLAYER, - ItemNames.DRAGOON, - ItemNames.ADEPT, - } -} - -advanced_basic_units = { - SC2Race.TERRAN: basic_units[SC2Race.TERRAN].union({ - ItemNames.REAPER, - ItemNames.DIAMONDBACK, - ItemNames.VIKING, - ItemNames.SIEGE_TANK, - ItemNames.BANSHEE, - ItemNames.THOR, - ItemNames.BATTLECRUISER, - ItemNames.CYCLONE - }), - SC2Race.ZERG: basic_units[SC2Race.ZERG].union({ - ItemNames.INFESTOR, - ItemNames.ABERRATION, - }), - SC2Race.PROTOSS: basic_units[SC2Race.PROTOSS].union({ - ItemNames.DARK_TEMPLAR, - ItemNames.BLOOD_HUNTER, - ItemNames.AVENGER, - ItemNames.IMMORTAL, - ItemNames.ANNIHILATOR, - ItemNames.VANGUARD, - }) -} - -no_logic_starting_units = { - SC2Race.TERRAN: advanced_basic_units[SC2Race.TERRAN].union({ - ItemNames.FIREBAT, - ItemNames.GHOST, - ItemNames.SPECTRE, - ItemNames.WRAITH, - ItemNames.RAVEN, - ItemNames.PREDATOR, - ItemNames.LIBERATOR, - ItemNames.HERC, - }), - SC2Race.ZERG: advanced_basic_units[SC2Race.ZERG].union({ - ItemNames.ULTRALISK, - ItemNames.SWARM_HOST - }), - SC2Race.PROTOSS: advanced_basic_units[SC2Race.PROTOSS].union({ - ItemNames.CARRIER, - ItemNames.TEMPEST, - ItemNames.VOID_RAY, - ItemNames.DESTROYER, - ItemNames.COLOSSUS, - ItemNames.WRATHWALKER, - ItemNames.SCOUT, - ItemNames.HIGH_TEMPLAR, - ItemNames.SIGNIFIER, - ItemNames.ASCENDANT, - ItemNames.DARK_ARCHON, - ItemNames.SUPPLICANT, - }) -} - -not_balanced_starting_units = { - ItemNames.SIEGE_TANK, - ItemNames.THOR, - ItemNames.BANSHEE, - ItemNames.BATTLECRUISER, - ItemNames.ULTRALISK, - ItemNames.CARRIER, - ItemNames.TEMPEST, -} - - -def get_basic_units(world: World, race: SC2Race) -> typing.Set[str]: - logic_level = get_option_value(world, 'required_tactics') - if logic_level == RequiredTactics.option_no_logic: - return no_logic_starting_units[race] - elif logic_level == RequiredTactics.option_advanced: - return advanced_basic_units[race] - else: - return basic_units[race] - - -# Items that can be placed before resources if not already in -# General upgrades and Mercs -second_pass_placeable_items: typing.Tuple[str, ...] = ( - # Global weapon/armor upgrades - ItemNames.PROGRESSIVE_TERRAN_ARMOR_UPGRADE, - ItemNames.PROGRESSIVE_TERRAN_WEAPON_UPGRADE, - ItemNames.PROGRESSIVE_TERRAN_WEAPON_ARMOR_UPGRADE, - ItemNames.PROGRESSIVE_ZERG_ARMOR_UPGRADE, - ItemNames.PROGRESSIVE_ZERG_WEAPON_UPGRADE, - ItemNames.PROGRESSIVE_ZERG_WEAPON_ARMOR_UPGRADE, - ItemNames.PROGRESSIVE_PROTOSS_ARMOR_UPGRADE, - ItemNames.PROGRESSIVE_PROTOSS_WEAPON_UPGRADE, - ItemNames.PROGRESSIVE_PROTOSS_WEAPON_ARMOR_UPGRADE, - ItemNames.PROGRESSIVE_PROTOSS_SHIELDS, - # Terran Buildings without upgrades - ItemNames.SENSOR_TOWER, - ItemNames.HIVE_MIND_EMULATOR, - ItemNames.PSI_DISRUPTER, - ItemNames.PERDITION_TURRET, - # Terran units without upgrades - ItemNames.HERC, - ItemNames.WARHOUND, - # General Terran upgrades without any dependencies - ItemNames.SCV_ADVANCED_CONSTRUCTION, - ItemNames.SCV_DUAL_FUSION_WELDERS, - ItemNames.PROGRESSIVE_FIRE_SUPPRESSION_SYSTEM, - ItemNames.PROGRESSIVE_ORBITAL_COMMAND, - ItemNames.ULTRA_CAPACITORS, - ItemNames.VANADIUM_PLATING, - ItemNames.ORBITAL_DEPOTS, - ItemNames.MICRO_FILTERING, - ItemNames.AUTOMATED_REFINERY, - ItemNames.COMMAND_CENTER_REACTOR, - ItemNames.TECH_REACTOR, - ItemNames.CELLULAR_REACTOR, - ItemNames.PROGRESSIVE_REGENERATIVE_BIO_STEEL, # Place only L1 - ItemNames.STRUCTURE_ARMOR, - ItemNames.HI_SEC_AUTO_TRACKING, - ItemNames.ADVANCED_OPTICS, - ItemNames.ROGUE_FORCES, - # Mercenaries (All races) - *[item_name for item_name, item_data in get_full_item_list().items() - if item_data.type == "Mercenary"], - # Kerrigan and Nova levels, abilities and generally useful stuff - *[item_name for item_name, item_data in get_full_item_list().items() - if item_data.type in ("Level", "Ability", "Evolution Pit", "Nova Gear")], - ItemNames.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE, - # Zerg static defenses - ItemNames.SPORE_CRAWLER, - ItemNames.SPINE_CRAWLER, - # Defiler, Aberration (no upgrades) - ItemNames.DEFILER, - ItemNames.ABERRATION, - # Spear of Adun Abilities - ItemNames.SOA_CHRONO_SURGE, - ItemNames.SOA_PROGRESSIVE_PROXY_PYLON, - ItemNames.SOA_PYLON_OVERCHARGE, - ItemNames.SOA_ORBITAL_STRIKE, - ItemNames.SOA_TEMPORAL_FIELD, - ItemNames.SOA_SOLAR_LANCE, - ItemNames.SOA_MASS_RECALL, - ItemNames.SOA_SHIELD_OVERCHARGE, - ItemNames.SOA_DEPLOY_FENIX, - ItemNames.SOA_PURIFIER_BEAM, - ItemNames.SOA_TIME_STOP, - ItemNames.SOA_SOLAR_BOMBARDMENT, - # Protoss generic upgrades - ItemNames.MATRIX_OVERLOAD, - ItemNames.QUATRO, - ItemNames.NEXUS_OVERCHARGE, - ItemNames.ORBITAL_ASSIMILATORS, - ItemNames.WARP_HARMONIZATION, - ItemNames.GUARDIAN_SHELL, - ItemNames.RECONSTRUCTION_BEAM, - ItemNames.OVERWATCH, - ItemNames.SUPERIOR_WARP_GATES, - ItemNames.KHALAI_INGENUITY, - ItemNames.AMPLIFIED_ASSIMILATORS, - # Protoss static defenses - ItemNames.PHOTON_CANNON, - ItemNames.KHAYDARIN_MONOLITH, - ItemNames.SHIELD_BATTERY -) - - -filler_items: typing.Tuple[str, ...] = ( - ItemNames.STARTING_MINERALS, - ItemNames.STARTING_VESPENE, - ItemNames.STARTING_SUPPLY, -) - -# Defense rating table -# Commented defense ratings are handled in LogicMixin -defense_ratings = { - ItemNames.SIEGE_TANK: 5, - # "Maelstrom Rounds": 2, - ItemNames.PLANETARY_FORTRESS: 3, - # Bunker w/ Marine/Marauder: 3, - ItemNames.PERDITION_TURRET: 2, - ItemNames.VULTURE: 1, - ItemNames.BANSHEE: 1, - ItemNames.BATTLECRUISER: 1, - ItemNames.LIBERATOR: 4, - ItemNames.WIDOW_MINE: 1, - # "Concealment (Widow Mine)": 1 -} -zerg_defense_ratings = { - ItemNames.PERDITION_TURRET: 2, - # Bunker w/ Firebat: 2, - ItemNames.LIBERATOR: -2, - ItemNames.HIVE_MIND_EMULATOR: 3, - ItemNames.PSI_DISRUPTER: 3, -} -air_defense_ratings = { - ItemNames.MISSILE_TURRET: 2, -} - -kerrigan_levels = [item_name for item_name, item_data in get_full_item_list().items() - if item_data.type == "Level" and item_data.race == SC2Race.ZERG] - -spider_mine_sources = { - ItemNames.VULTURE, - ItemNames.REAPER_SPIDER_MINES, - ItemNames.SIEGE_TANK_SPIDER_MINES, - ItemNames.RAVEN_SPIDER_MINES, -} - -progressive_if_nco = { - ItemNames.MARINE_PROGRESSIVE_STIMPACK, - ItemNames.FIREBAT_PROGRESSIVE_STIMPACK, - ItemNames.BANSHEE_PROGRESSIVE_CROSS_SPECTRUM_DAMPENERS, - ItemNames.PROGRESSIVE_REGENERATIVE_BIO_STEEL, -} - -progressive_if_ext = { - ItemNames.VULTURE_PROGRESSIVE_REPLENISHABLE_MAGAZINE, - ItemNames.WRAITH_PROGRESSIVE_TOMAHAWK_POWER_CELLS, - ItemNames.BATTLECRUISER_PROGRESSIVE_DEFENSIVE_MATRIX, - ItemNames.BATTLECRUISER_PROGRESSIVE_MISSILE_PODS, - ItemNames.THOR_PROGRESSIVE_IMMORTALITY_PROTOCOL, - ItemNames.PROGRESSIVE_FIRE_SUPPRESSION_SYSTEM, - ItemNames.DIAMONDBACK_PROGRESSIVE_TRI_LITHIUM_POWER_CELL, - ItemNames.PROGRESSIVE_ORBITAL_COMMAND -} - -kerrigan_actives: typing.List[typing.Set[str]] = [ - {ItemNames.KERRIGAN_KINETIC_BLAST, ItemNames.KERRIGAN_LEAPING_STRIKE}, - {ItemNames.KERRIGAN_CRUSHING_GRIP, ItemNames.KERRIGAN_PSIONIC_SHIFT}, - set(), - {ItemNames.KERRIGAN_WILD_MUTATION, ItemNames.KERRIGAN_SPAWN_BANELINGS, ItemNames.KERRIGAN_MEND}, - set(), - set(), - {ItemNames.KERRIGAN_APOCALYPSE, ItemNames.KERRIGAN_SPAWN_LEVIATHAN, ItemNames.KERRIGAN_DROP_PODS}, -] - -kerrigan_passives: typing.List[typing.Set[str]] = [ - {ItemNames.KERRIGAN_HEROIC_FORTITUDE}, - {ItemNames.KERRIGAN_CHAIN_REACTION}, - {ItemNames.KERRIGAN_ZERGLING_RECONSTITUTION, ItemNames.KERRIGAN_IMPROVED_OVERLORDS, ItemNames.KERRIGAN_AUTOMATED_EXTRACTORS}, - set(), - {ItemNames.KERRIGAN_TWIN_DRONES, ItemNames.KERRIGAN_MALIGNANT_CREEP, ItemNames.KERRIGAN_VESPENE_EFFICIENCY}, - {ItemNames.KERRIGAN_INFEST_BROODLINGS, ItemNames.KERRIGAN_FURY, ItemNames.KERRIGAN_ABILITY_EFFICIENCY}, - set(), -] - -kerrigan_only_passives = { - ItemNames.KERRIGAN_HEROIC_FORTITUDE, ItemNames.KERRIGAN_CHAIN_REACTION, - ItemNames.KERRIGAN_INFEST_BROODLINGS, ItemNames.KERRIGAN_FURY, ItemNames.KERRIGAN_ABILITY_EFFICIENCY, -} - -spear_of_adun_calldowns = { - ItemNames.SOA_CHRONO_SURGE, - ItemNames.SOA_PROGRESSIVE_PROXY_PYLON, - ItemNames.SOA_PYLON_OVERCHARGE, - ItemNames.SOA_ORBITAL_STRIKE, - ItemNames.SOA_TEMPORAL_FIELD, - ItemNames.SOA_SOLAR_LANCE, - ItemNames.SOA_MASS_RECALL, - ItemNames.SOA_SHIELD_OVERCHARGE, - ItemNames.SOA_DEPLOY_FENIX, - ItemNames.SOA_PURIFIER_BEAM, - ItemNames.SOA_TIME_STOP, - ItemNames.SOA_SOLAR_BOMBARDMENT -} - -spear_of_adun_castable_passives = { - ItemNames.RECONSTRUCTION_BEAM, - ItemNames.OVERWATCH, -} - -nova_equipment = { - *[item_name for item_name, item_data in get_full_item_list().items() - if item_data.type == "Nova Gear"], - ItemNames.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE -} - -# 'number' values of upgrades for upgrade bundle items -upgrade_numbers = [ - # Terran - {0, 4, 8}, # Weapon - {2, 6, 10}, # Armor - {0, 2}, # Infantry - {4, 6}, # Vehicle - {8, 10}, # Starship - {0, 2, 4, 6, 8, 10}, # All - # Zerg - {0, 2, 6}, # Weapon - {4, 8}, # Armor - {0, 2, 4}, # Ground - {6, 8}, # Flyer - {0, 2, 4, 6, 8}, # All - # Protoss - {0, 6}, # Weapon - {2, 4, 8}, # Armor - {0, 2}, # Ground, Shields are handled specially - {6, 8}, # Air, Shields are handled specially - {0, 2, 4, 6, 8}, # All -] -# 'upgrade_numbers' indices for all upgrades -upgrade_numbers_all = { - SC2Race.TERRAN: 5, - SC2Race.ZERG: 10, - SC2Race.PROTOSS: 15, -} - -# Names of upgrades to be included for different options -upgrade_included_names = [ - { # Individual Items - ItemNames.PROGRESSIVE_TERRAN_INFANTRY_WEAPON, - ItemNames.PROGRESSIVE_TERRAN_INFANTRY_ARMOR, - ItemNames.PROGRESSIVE_TERRAN_VEHICLE_WEAPON, - ItemNames.PROGRESSIVE_TERRAN_VEHICLE_ARMOR, - ItemNames.PROGRESSIVE_TERRAN_SHIP_WEAPON, - ItemNames.PROGRESSIVE_TERRAN_SHIP_ARMOR, - ItemNames.PROGRESSIVE_ZERG_MELEE_ATTACK, - ItemNames.PROGRESSIVE_ZERG_MISSILE_ATTACK, - ItemNames.PROGRESSIVE_ZERG_GROUND_CARAPACE, - ItemNames.PROGRESSIVE_ZERG_FLYER_ATTACK, - ItemNames.PROGRESSIVE_ZERG_FLYER_CARAPACE, - ItemNames.PROGRESSIVE_PROTOSS_GROUND_WEAPON, - ItemNames.PROGRESSIVE_PROTOSS_GROUND_ARMOR, - ItemNames.PROGRESSIVE_PROTOSS_SHIELDS, - ItemNames.PROGRESSIVE_PROTOSS_AIR_WEAPON, - ItemNames.PROGRESSIVE_PROTOSS_AIR_ARMOR, - }, - { # Bundle Weapon And Armor - ItemNames.PROGRESSIVE_TERRAN_WEAPON_UPGRADE, - ItemNames.PROGRESSIVE_TERRAN_ARMOR_UPGRADE, - ItemNames.PROGRESSIVE_ZERG_WEAPON_UPGRADE, - ItemNames.PROGRESSIVE_ZERG_ARMOR_UPGRADE, - ItemNames.PROGRESSIVE_PROTOSS_WEAPON_UPGRADE, - ItemNames.PROGRESSIVE_PROTOSS_ARMOR_UPGRADE, - }, - { # Bundle Unit Class - ItemNames.PROGRESSIVE_TERRAN_INFANTRY_UPGRADE, - ItemNames.PROGRESSIVE_TERRAN_VEHICLE_UPGRADE, - ItemNames.PROGRESSIVE_TERRAN_SHIP_UPGRADE, - ItemNames.PROGRESSIVE_ZERG_GROUND_UPGRADE, - ItemNames.PROGRESSIVE_ZERG_FLYER_UPGRADE, - ItemNames.PROGRESSIVE_PROTOSS_GROUND_UPGRADE, - ItemNames.PROGRESSIVE_PROTOSS_AIR_UPGRADE, - }, - { # Bundle All - ItemNames.PROGRESSIVE_TERRAN_WEAPON_ARMOR_UPGRADE, - ItemNames.PROGRESSIVE_ZERG_WEAPON_ARMOR_UPGRADE, - ItemNames.PROGRESSIVE_PROTOSS_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[SC2Race, typing.Dict[str, int]] = { - SC2Race.ANY: { - "Minerals": 0, - "Vespene": 1, - "Supply": 2, - "Goal": 3, - "Nothing Group": 4, - }, - SC2Race.TERRAN: { - "Armory 1": 0, - "Armory 2": 1, - "Armory 3": 2, - "Armory 4": 3, - "Armory 5": 4, - "Armory 6": 5, - "Progressive Upgrade": 6, # Unit upgrades that exist multiple times (Stimpack / Super Stimpack) - "Laboratory": 7, - "Upgrade": 8, # Weapon / Armor upgrades - "Unit": 9, - "Building": 10, - "Mercenary": 11, - "Nova Gear": 12, - "Progressive Upgrade 2": 13, - }, - SC2Race.ZERG: { - "Ability": 0, - "Mutation 1": 1, - "Strain": 2, - "Morph": 3, - "Upgrade": 4, - "Mercenary": 5, - "Unit": 6, - "Level": 7, - "Primal Form": 8, - "Evolution Pit": 9, - "Mutation 2": 10, - "Mutation 3": 11 - }, - SC2Race.PROTOSS: { - "Unit": 0, - "Unit 2": 1, - "Upgrade": 2, # Weapon / Armor upgrades - "Building": 3, - "Progressive Upgrade": 4, - "Spear of Adun": 5, - "Solarite Core": 6, - "Forge 1": 7, - "Forge 2": 8, - "Forge 3": 9, - } -} diff --git a/worlds/sc2/Locations.py b/worlds/sc2/Locations.py deleted file mode 100644 index 42b1dd4d..00000000 --- a/worlds/sc2/Locations.py +++ /dev/null @@ -1,1635 +0,0 @@ -from enum import IntEnum -from typing import List, Tuple, Optional, Callable, NamedTuple, Set, Any -from BaseClasses import MultiWorld -from . import ItemNames -from .Options import get_option_value, kerrigan_unit_available, RequiredTactics, GrantStoryTech, LocationInclusion, \ - EnableHotsMissions -from .Rules import SC2Logic - -from BaseClasses import Location -from worlds.AutoWorld import World - -SC2WOL_LOC_ID_OFFSET = 1000 -SC2HOTS_LOC_ID_OFFSET = 20000000 # Avoid clashes with The Legend of Zelda -SC2LOTV_LOC_ID_OFFSET = SC2HOTS_LOC_ID_OFFSET + 2000 -SC2NCO_LOC_ID_OFFSET = SC2LOTV_LOC_ID_OFFSET + 2500 - - -class SC2Location(Location): - game: str = "Starcraft2" - - -class LocationType(IntEnum): - VICTORY = 0 # Winning a mission - VANILLA = 1 # Objectives that provided metaprogression in the original campaign, along with a few other locations for a balanced experience - EXTRA = 2 # Additional locations based on mission progression, collecting in-mission rewards, etc. that do not significantly increase the challenge. - CHALLENGE = 3 # Challenging objectives, often harder than just completing a mission, and often associated with Achievements - MASTERY = 4 # Extremely challenging objectives often associated with Masteries and Feats of Strength in the original campaign - - -class LocationData(NamedTuple): - region: str - name: str - code: Optional[int] - type: LocationType - rule: Optional[Callable[[Any], bool]] = Location.access_rule - - -def get_location_types(world: World, inclusion_type: LocationInclusion) -> Set[LocationType]: - """ - - :param multiworld: - :param player: - :param inclusion_type: Level of inclusion to check for - :return: A list of location types that match the inclusion type - """ - exclusion_options = [ - ("vanilla_locations", LocationType.VANILLA), - ("extra_locations", LocationType.EXTRA), - ("challenge_locations", LocationType.CHALLENGE), - ("mastery_locations", LocationType.MASTERY) - ] - excluded_location_types = set() - for option_name, location_type in exclusion_options: - if get_option_value(world, option_name) is inclusion_type: - excluded_location_types.add(location_type) - return excluded_location_types - - -def get_plando_locations(world: World) -> List[str]: - """ - - :param multiworld: - :param player: - :return: A list of locations affected by a plando in a world - """ - if world is None: - return [] - plando_locations = [] - for plando_setting in world.options.plando_items: - plando_locations += plando_setting.locations - - return plando_locations - - -def get_locations(world: Optional[World]) -> 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(world, 'required_tactics') - adv_tactics = logic_level != RequiredTactics.option_standard - kerriganless = get_option_value(world, 'kerrigan_presence') not in kerrigan_unit_available \ - or get_option_value(world, "enable_hots_missions") == EnableHotsMissions.option_false - story_tech_granted = get_option_value(world, "grant_story_tech") == GrantStoryTech.option_true - logic = SC2Logic(world) - player = None if world is None else world.player - location_table: List[LocationData] = [ - # WoL - 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.VANILLA), - LocationData("Liberation Day", "Liberation Day: Second Statue", SC2WOL_LOC_ID_OFFSET + 102, LocationType.VANILLA), - LocationData("Liberation Day", "Liberation Day: Third Statue", SC2WOL_LOC_ID_OFFSET + 103, LocationType.VANILLA), - LocationData("Liberation Day", "Liberation Day: Fourth Statue", SC2WOL_LOC_ID_OFFSET + 104, LocationType.VANILLA), - LocationData("Liberation Day", "Liberation Day: Fifth Statue", SC2WOL_LOC_ID_OFFSET + 105, LocationType.VANILLA), - LocationData("Liberation Day", "Liberation Day: Sixth Statue", SC2WOL_LOC_ID_OFFSET + 106, LocationType.VANILLA), - LocationData("Liberation Day", "Liberation Day: Special Delivery", SC2WOL_LOC_ID_OFFSET + 107, LocationType.EXTRA), - LocationData("Liberation Day", "Liberation Day: Transport", SC2WOL_LOC_ID_OFFSET + 108, LocationType.EXTRA), - LocationData("The Outlaws", "The Outlaws: Victory", SC2WOL_LOC_ID_OFFSET + 200, LocationType.VICTORY, - lambda state: logic.terran_early_tech(state)), - LocationData("The Outlaws", "The Outlaws: Rebel Base", SC2WOL_LOC_ID_OFFSET + 201, LocationType.VANILLA, - lambda state: logic.terran_early_tech(state)), - LocationData("The Outlaws", "The Outlaws: North Resource Pickups", SC2WOL_LOC_ID_OFFSET + 202, LocationType.EXTRA, - lambda state: logic.terran_early_tech(state)), - LocationData("The Outlaws", "The Outlaws: Bunker", SC2WOL_LOC_ID_OFFSET + 203, LocationType.VANILLA, - lambda state: logic.terran_early_tech(state)), - LocationData("The Outlaws", "The Outlaws: Close Resource Pickups", SC2WOL_LOC_ID_OFFSET + 204, LocationType.EXTRA), - LocationData("Zero Hour", "Zero Hour: Victory", SC2WOL_LOC_ID_OFFSET + 300, LocationType.VICTORY, - lambda state: logic.terran_common_unit(state) and - logic.terran_defense_rating(state, True) >= 2 and - (adv_tactics or logic.terran_basic_anti_air(state))), - LocationData("Zero Hour", "Zero Hour: First Group Rescued", SC2WOL_LOC_ID_OFFSET + 301, LocationType.VANILLA), - LocationData("Zero Hour", "Zero Hour: Second Group Rescued", SC2WOL_LOC_ID_OFFSET + 302, LocationType.VANILLA, - lambda state: logic.terran_common_unit(state)), - LocationData("Zero Hour", "Zero Hour: Third Group Rescued", SC2WOL_LOC_ID_OFFSET + 303, LocationType.VANILLA, - lambda state: logic.terran_common_unit(state) and - logic.terran_defense_rating(state, True) >= 2), - LocationData("Zero Hour", "Zero Hour: First Hatchery", SC2WOL_LOC_ID_OFFSET + 304, LocationType.CHALLENGE, - lambda state: logic.terran_competent_comp(state)), - LocationData("Zero Hour", "Zero Hour: Second Hatchery", SC2WOL_LOC_ID_OFFSET + 305, LocationType.CHALLENGE, - lambda state: logic.terran_competent_comp(state)), - LocationData("Zero Hour", "Zero Hour: Third Hatchery", SC2WOL_LOC_ID_OFFSET + 306, LocationType.CHALLENGE, - lambda state: logic.terran_competent_comp(state)), - LocationData("Zero Hour", "Zero Hour: Fourth Hatchery", SC2WOL_LOC_ID_OFFSET + 307, LocationType.CHALLENGE, - lambda state: logic.terran_competent_comp(state)), - LocationData("Zero Hour", "Zero Hour: Ride's on its Way", SC2WOL_LOC_ID_OFFSET + 308, LocationType.EXTRA, - lambda state: logic.terran_common_unit(state)), - LocationData("Zero Hour", "Zero Hour: Hold Just a Little Longer", SC2WOL_LOC_ID_OFFSET + 309, LocationType.EXTRA, - lambda state: logic.terran_common_unit(state) and - logic.terran_defense_rating(state, True) >= 2), - LocationData("Zero Hour", "Zero Hour: Cavalry's on the Way", SC2WOL_LOC_ID_OFFSET + 310, LocationType.EXTRA, - lambda state: logic.terran_common_unit(state) and - logic.terran_defense_rating(state, True) >= 2), - LocationData("Evacuation", "Evacuation: Victory", SC2WOL_LOC_ID_OFFSET + 400, LocationType.VICTORY, - lambda state: logic.terran_early_tech(state) and - (adv_tactics and logic.terran_basic_anti_air(state) - or logic.terran_competent_anti_air(state))), - LocationData("Evacuation", "Evacuation: North Chrysalis", SC2WOL_LOC_ID_OFFSET + 401, LocationType.VANILLA), - LocationData("Evacuation", "Evacuation: West Chrysalis", SC2WOL_LOC_ID_OFFSET + 402, LocationType.VANILLA, - lambda state: logic.terran_early_tech(state)), - LocationData("Evacuation", "Evacuation: East Chrysalis", SC2WOL_LOC_ID_OFFSET + 403, LocationType.VANILLA, - lambda state: logic.terran_early_tech(state)), - LocationData("Evacuation", "Evacuation: Reach Hanson", SC2WOL_LOC_ID_OFFSET + 404, LocationType.EXTRA), - LocationData("Evacuation", "Evacuation: Secret Resource Stash", SC2WOL_LOC_ID_OFFSET + 405, LocationType.EXTRA), - LocationData("Evacuation", "Evacuation: Flawless", SC2WOL_LOC_ID_OFFSET + 406, LocationType.CHALLENGE, - lambda state: logic.terran_early_tech(state) and - logic.terran_defense_rating(state, True, False) >= 2 and - (adv_tactics and logic.terran_basic_anti_air(state) - or logic.terran_competent_anti_air(state))), - LocationData("Outbreak", "Outbreak: Victory", SC2WOL_LOC_ID_OFFSET + 500, LocationType.VICTORY, - lambda state: logic.terran_defense_rating(state, True, False) >= 4 and - (logic.terran_common_unit(state) or state.has(ItemNames.REAPER, player))), - LocationData("Outbreak", "Outbreak: Left Infestor", SC2WOL_LOC_ID_OFFSET + 501, LocationType.VANILLA, - lambda state: logic.terran_defense_rating(state, True, False) >= 2 and - (logic.terran_common_unit(state) or state.has(ItemNames.REAPER, player))), - LocationData("Outbreak", "Outbreak: Right Infestor", SC2WOL_LOC_ID_OFFSET + 502, LocationType.VANILLA, - lambda state: logic.terran_defense_rating(state, True, False) >= 2 and - (logic.terran_common_unit(state) or state.has(ItemNames.REAPER, player))), - LocationData("Outbreak", "Outbreak: North Infested Command Center", SC2WOL_LOC_ID_OFFSET + 503, LocationType.EXTRA, - lambda state: logic.terran_defense_rating(state, True, False) >= 2 and - (logic.terran_common_unit(state) or state.has(ItemNames.REAPER, player))), - LocationData("Outbreak", "Outbreak: South Infested Command Center", SC2WOL_LOC_ID_OFFSET + 504, LocationType.EXTRA, - lambda state: logic.terran_defense_rating(state, True, False) >= 2 and - (logic.terran_common_unit(state) or state.has(ItemNames.REAPER, player))), - LocationData("Outbreak", "Outbreak: Northwest Bar", SC2WOL_LOC_ID_OFFSET + 505, LocationType.EXTRA, - lambda state: logic.terran_defense_rating(state, True, False) >= 2 and - (logic.terran_common_unit(state) or state.has(ItemNames.REAPER, player))), - LocationData("Outbreak", "Outbreak: North Bar", SC2WOL_LOC_ID_OFFSET + 506, LocationType.EXTRA, - lambda state: logic.terran_defense_rating(state, True, False) >= 2 and - (logic.terran_common_unit(state) or state.has(ItemNames.REAPER, player))), - LocationData("Outbreak", "Outbreak: South Bar", SC2WOL_LOC_ID_OFFSET + 507, LocationType.EXTRA, - lambda state: logic.terran_defense_rating(state, True, False) >= 2 and - (logic.terran_common_unit(state) or state.has(ItemNames.REAPER, player))), - LocationData("Safe Haven", "Safe Haven: Victory", SC2WOL_LOC_ID_OFFSET + 600, LocationType.VICTORY, - lambda state: logic.terran_common_unit(state) and - logic.terran_competent_anti_air(state)), - LocationData("Safe Haven", "Safe Haven: North Nexus", SC2WOL_LOC_ID_OFFSET + 601, LocationType.EXTRA, - lambda state: logic.terran_common_unit(state) and - logic.terran_competent_anti_air(state)), - LocationData("Safe Haven", "Safe Haven: East Nexus", SC2WOL_LOC_ID_OFFSET + 602, LocationType.EXTRA, - lambda state: logic.terran_common_unit(state) and - logic.terran_competent_anti_air(state)), - LocationData("Safe Haven", "Safe Haven: South Nexus", SC2WOL_LOC_ID_OFFSET + 603, LocationType.EXTRA, - lambda state: logic.terran_common_unit(state) and - logic.terran_competent_anti_air(state)), - LocationData("Safe Haven", "Safe Haven: First Terror Fleet", SC2WOL_LOC_ID_OFFSET + 604, LocationType.VANILLA, - lambda state: logic.terran_common_unit(state) and - logic.terran_competent_anti_air(state)), - LocationData("Safe Haven", "Safe Haven: Second Terror Fleet", SC2WOL_LOC_ID_OFFSET + 605, LocationType.VANILLA, - lambda state: logic.terran_common_unit(state) and - logic.terran_competent_anti_air(state)), - LocationData("Safe Haven", "Safe Haven: Third Terror Fleet", SC2WOL_LOC_ID_OFFSET + 606, LocationType.VANILLA, - lambda state: logic.terran_common_unit(state) and - logic.terran_competent_anti_air(state)), - LocationData("Haven's Fall", "Haven's Fall: Victory", SC2WOL_LOC_ID_OFFSET + 700, LocationType.VICTORY, - lambda state: logic.terran_common_unit(state) and - logic.terran_competent_anti_air(state) and - logic.terran_defense_rating(state, True) >= 3), - LocationData("Haven's Fall", "Haven's Fall: North Hive", SC2WOL_LOC_ID_OFFSET + 701, LocationType.VANILLA, - lambda state: logic.terran_common_unit(state) and - logic.terran_competent_anti_air(state) and - logic.terran_defense_rating(state, True) >= 3), - LocationData("Haven's Fall", "Haven's Fall: East Hive", SC2WOL_LOC_ID_OFFSET + 702, LocationType.VANILLA, - lambda state: logic.terran_common_unit(state) and - logic.terran_competent_anti_air(state) and - logic.terran_defense_rating(state, True) >= 3), - LocationData("Haven's Fall", "Haven's Fall: South Hive", SC2WOL_LOC_ID_OFFSET + 703, LocationType.VANILLA, - lambda state: logic.terran_common_unit(state) and - logic.terran_competent_anti_air(state) and - logic.terran_defense_rating(state, True) >= 3), - LocationData("Haven's Fall", "Haven's Fall: Northeast Colony Base", SC2WOL_LOC_ID_OFFSET + 704, LocationType.CHALLENGE, - lambda state: logic.terran_respond_to_colony_infestations(state)), - LocationData("Haven's Fall", "Haven's Fall: East Colony Base", SC2WOL_LOC_ID_OFFSET + 705, LocationType.CHALLENGE, - lambda state: logic.terran_respond_to_colony_infestations(state)), - LocationData("Haven's Fall", "Haven's Fall: Middle Colony Base", SC2WOL_LOC_ID_OFFSET + 706, LocationType.CHALLENGE, - lambda state: logic.terran_respond_to_colony_infestations(state)), - LocationData("Haven's Fall", "Haven's Fall: Southeast Colony Base", SC2WOL_LOC_ID_OFFSET + 707, LocationType.CHALLENGE, - lambda state: logic.terran_respond_to_colony_infestations(state)), - LocationData("Haven's Fall", "Haven's Fall: Southwest Colony Base", SC2WOL_LOC_ID_OFFSET + 708, LocationType.CHALLENGE, - lambda state: logic.terran_respond_to_colony_infestations(state)), - LocationData("Haven's Fall", "Haven's Fall: Southwest Gas Pickups", SC2WOL_LOC_ID_OFFSET + 709, LocationType.EXTRA, - lambda state: logic.terran_common_unit(state) and - logic.terran_competent_anti_air(state) and - logic.terran_defense_rating(state, True) >= 3), - LocationData("Haven's Fall", "Haven's Fall: East Gas Pickups", SC2WOL_LOC_ID_OFFSET + 710, LocationType.EXTRA, - lambda state: logic.terran_common_unit(state) and - logic.terran_competent_anti_air(state) and - logic.terran_defense_rating(state, True) >= 3), - LocationData("Haven's Fall", "Haven's Fall: Southeast Gas Pickups", SC2WOL_LOC_ID_OFFSET + 711, LocationType.EXTRA, - lambda state: logic.terran_common_unit(state) and - logic.terran_competent_anti_air(state) and - logic.terran_defense_rating(state, True) >= 3), - LocationData("Smash and Grab", "Smash and Grab: Victory", SC2WOL_LOC_ID_OFFSET + 800, LocationType.VICTORY, - lambda state: logic.terran_common_unit(state) and - (adv_tactics and logic.terran_basic_anti_air(state) - or logic.terran_competent_anti_air(state))), - LocationData("Smash and Grab", "Smash and Grab: First Relic", SC2WOL_LOC_ID_OFFSET + 801, LocationType.VANILLA), - LocationData("Smash and Grab", "Smash and Grab: Second Relic", SC2WOL_LOC_ID_OFFSET + 802, LocationType.VANILLA), - LocationData("Smash and Grab", "Smash and Grab: Third Relic", SC2WOL_LOC_ID_OFFSET + 803, LocationType.VANILLA, - lambda state: logic.terran_common_unit(state) and - (adv_tactics and logic.terran_basic_anti_air(state) - or logic.terran_competent_anti_air(state))), - LocationData("Smash and Grab", "Smash and Grab: Fourth Relic", SC2WOL_LOC_ID_OFFSET + 804, LocationType.VANILLA, - lambda state: logic.terran_common_unit(state) and - (adv_tactics and logic.terran_basic_anti_air(state) - or logic.terran_competent_anti_air(state))), - LocationData("Smash and Grab", "Smash and Grab: First Forcefield Area Busted", SC2WOL_LOC_ID_OFFSET + 805, LocationType.EXTRA, - lambda state: logic.terran_common_unit(state) and - (adv_tactics and logic.terran_basic_anti_air(state) - or logic.terran_competent_anti_air(state))), - LocationData("Smash and Grab", "Smash and Grab: Second Forcefield Area Busted", SC2WOL_LOC_ID_OFFSET + 806, LocationType.EXTRA, - lambda state: logic.terran_common_unit(state) and - (adv_tactics and logic.terran_basic_anti_air(state) - or logic.terran_competent_anti_air(state))), - LocationData("The Dig", "The Dig: Victory", SC2WOL_LOC_ID_OFFSET + 900, LocationType.VICTORY, - lambda state: logic.terran_basic_anti_air(state) - and logic.terran_defense_rating(state, False, True) >= 8 - and logic.terran_defense_rating(state, False, False) >= 6 - and logic.terran_common_unit(state) - and (logic.marine_medic_upgrade(state) or adv_tactics)), - LocationData("The Dig", "The Dig: Left Relic", SC2WOL_LOC_ID_OFFSET + 901, LocationType.VANILLA, - lambda state: logic.terran_defense_rating(state, False, False) >= 6 - and logic.terran_common_unit(state) - and (logic.marine_medic_upgrade(state) or adv_tactics)), - LocationData("The Dig", "The Dig: Right Ground Relic", SC2WOL_LOC_ID_OFFSET + 902, LocationType.VANILLA, - lambda state: logic.terran_defense_rating(state, False, False) >= 6 - and logic.terran_common_unit(state) - and (logic.marine_medic_upgrade(state) or adv_tactics)), - LocationData("The Dig", "The Dig: Right Cliff Relic", SC2WOL_LOC_ID_OFFSET + 903, LocationType.VANILLA, - lambda state: logic.terran_defense_rating(state, False, False) >= 6 - and logic.terran_common_unit(state) - and (logic.marine_medic_upgrade(state) or adv_tactics)), - LocationData("The Dig", "The Dig: Moebius Base", SC2WOL_LOC_ID_OFFSET + 904, LocationType.EXTRA, - lambda state: logic.marine_medic_upgrade(state) or adv_tactics), - LocationData("The Dig", "The Dig: Door Outer Layer", SC2WOL_LOC_ID_OFFSET + 905, LocationType.EXTRA, - lambda state: logic.terran_defense_rating(state, False, False) >= 6 - and logic.terran_common_unit(state) - and (logic.marine_medic_upgrade(state) or adv_tactics)), - LocationData("The Dig", "The Dig: Door Thermal Barrier", SC2WOL_LOC_ID_OFFSET + 906, LocationType.EXTRA, - lambda state: logic.terran_basic_anti_air(state) - and logic.terran_defense_rating(state, False, True) >= 8 - and logic.terran_defense_rating(state, False, False) >= 6 - and logic.terran_common_unit(state) - and (logic.marine_medic_upgrade(state) or adv_tactics)), - LocationData("The Dig", "The Dig: Cutting Through the Core", SC2WOL_LOC_ID_OFFSET + 907, LocationType.EXTRA, - lambda state: logic.terran_basic_anti_air(state) - and logic.terran_defense_rating(state, False, True) >= 8 - and logic.terran_defense_rating(state, False, False) >= 6 - and logic.terran_common_unit(state) - and (logic.marine_medic_upgrade(state) or adv_tactics)), - LocationData("The Dig", "The Dig: Structure Access Imminent", SC2WOL_LOC_ID_OFFSET + 908, LocationType.EXTRA, - lambda state: logic.terran_basic_anti_air(state) - and logic.terran_defense_rating(state, False, True) >= 8 - and logic.terran_defense_rating(state, False, False) >= 6 - and logic.terran_common_unit(state) - and (logic.marine_medic_upgrade(state) or adv_tactics)), - LocationData("The Moebius Factor", "The Moebius Factor: Victory", SC2WOL_LOC_ID_OFFSET + 1000, LocationType.VICTORY, - lambda state: logic.terran_basic_anti_air(state) and - (logic.terran_air(state) - or state.has_any({ItemNames.MEDIVAC, ItemNames.HERCULES}, player) - and logic.terran_common_unit(state))), - LocationData("The Moebius Factor", "The Moebius Factor: 1st Data Core", SC2WOL_LOC_ID_OFFSET + 1001, LocationType.VANILLA), - LocationData("The Moebius Factor", "The Moebius Factor: 2nd Data Core", SC2WOL_LOC_ID_OFFSET + 1002, LocationType.VANILLA, - lambda state: (logic.terran_air(state) - or state.has_any({ItemNames.MEDIVAC, ItemNames.HERCULES}, player) - and logic.terran_common_unit(state))), - LocationData("The Moebius Factor", "The Moebius Factor: South Rescue", SC2WOL_LOC_ID_OFFSET + 1003, LocationType.EXTRA, - lambda state: logic.terran_can_rescue(state)), - LocationData("The Moebius Factor", "The Moebius Factor: Wall Rescue", SC2WOL_LOC_ID_OFFSET + 1004, LocationType.EXTRA, - lambda state: logic.terran_can_rescue(state)), - LocationData("The Moebius Factor", "The Moebius Factor: Mid Rescue", SC2WOL_LOC_ID_OFFSET + 1005, LocationType.EXTRA, - lambda state: logic.terran_can_rescue(state)), - LocationData("The Moebius Factor", "The Moebius Factor: Nydus Roof Rescue", SC2WOL_LOC_ID_OFFSET + 1006, LocationType.EXTRA, - lambda state: logic.terran_can_rescue(state)), - LocationData("The Moebius Factor", "The Moebius Factor: Alive Inside Rescue", SC2WOL_LOC_ID_OFFSET + 1007, LocationType.EXTRA, - lambda state: logic.terran_can_rescue(state)), - LocationData("The Moebius Factor", "The Moebius Factor: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1008, LocationType.VANILLA, - lambda state: logic.terran_basic_anti_air(state) and - (logic.terran_air(state) - or state.has_any({ItemNames.MEDIVAC, ItemNames.HERCULES}, player) - and logic.terran_common_unit(state))), - LocationData("The Moebius Factor", "The Moebius Factor: 3rd Data Core", SC2WOL_LOC_ID_OFFSET + 1009, LocationType.VANILLA, - lambda state: logic.terran_basic_anti_air(state) and - (logic.terran_air(state) - or state.has_any({ItemNames.MEDIVAC, ItemNames.HERCULES}, player) - and logic.terran_common_unit(state))), - LocationData("Supernova", "Supernova: Victory", SC2WOL_LOC_ID_OFFSET + 1100, LocationType.VICTORY, - lambda state: logic.terran_beats_protoss_deathball(state)), - LocationData("Supernova", "Supernova: West Relic", SC2WOL_LOC_ID_OFFSET + 1101, LocationType.VANILLA), - LocationData("Supernova", "Supernova: North Relic", SC2WOL_LOC_ID_OFFSET + 1102, LocationType.VANILLA), - LocationData("Supernova", "Supernova: South Relic", SC2WOL_LOC_ID_OFFSET + 1103, LocationType.VANILLA, - lambda state: logic.terran_beats_protoss_deathball(state)), - LocationData("Supernova", "Supernova: East Relic", SC2WOL_LOC_ID_OFFSET + 1104, LocationType.VANILLA, - lambda state: logic.terran_beats_protoss_deathball(state)), - LocationData("Supernova", "Supernova: Landing Zone Cleared", SC2WOL_LOC_ID_OFFSET + 1105, LocationType.EXTRA), - LocationData("Supernova", "Supernova: Middle Base", SC2WOL_LOC_ID_OFFSET + 1106, LocationType.EXTRA, - lambda state: logic.terran_beats_protoss_deathball(state)), - LocationData("Supernova", "Supernova: Southeast Base", SC2WOL_LOC_ID_OFFSET + 1107, LocationType.EXTRA, - lambda state: logic.terran_beats_protoss_deathball(state)), - LocationData("Maw of the Void", "Maw of the Void: Victory", SC2WOL_LOC_ID_OFFSET + 1200, LocationType.VICTORY, - lambda state: logic.terran_survives_rip_field(state)), - LocationData("Maw of the Void", "Maw of the Void: Landing Zone Cleared", SC2WOL_LOC_ID_OFFSET + 1201, LocationType.EXTRA), - LocationData("Maw of the Void", "Maw of the Void: Expansion Prisoners", SC2WOL_LOC_ID_OFFSET + 1202, LocationType.VANILLA, - lambda state: adv_tactics or logic.terran_survives_rip_field(state)), - LocationData("Maw of the Void", "Maw of the Void: South Close Prisoners", SC2WOL_LOC_ID_OFFSET + 1203, LocationType.VANILLA, - lambda state: adv_tactics or logic.terran_survives_rip_field(state)), - LocationData("Maw of the Void", "Maw of the Void: South Far Prisoners", SC2WOL_LOC_ID_OFFSET + 1204, LocationType.VANILLA, - lambda state: logic.terran_survives_rip_field(state)), - LocationData("Maw of the Void", "Maw of the Void: North Prisoners", SC2WOL_LOC_ID_OFFSET + 1205, LocationType.VANILLA, - lambda state: logic.terran_survives_rip_field(state)), - LocationData("Maw of the Void", "Maw of the Void: Mothership", SC2WOL_LOC_ID_OFFSET + 1206, LocationType.EXTRA, - lambda state: logic.terran_survives_rip_field(state)), - LocationData("Maw of the Void", "Maw of the Void: Expansion Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1207, LocationType.EXTRA, - lambda state: adv_tactics or logic.terran_survives_rip_field(state)), - LocationData("Maw of the Void", "Maw of the Void: Middle Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1208, LocationType.EXTRA, - lambda state: logic.terran_survives_rip_field(state)), - LocationData("Maw of the Void", "Maw of the Void: Southeast Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1209, LocationType.EXTRA, - lambda state: logic.terran_survives_rip_field(state)), - LocationData("Maw of the Void", "Maw of the Void: Stargate Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1210, LocationType.EXTRA, - lambda state: logic.terran_survives_rip_field(state)), - LocationData("Maw of the Void", "Maw of the Void: Northwest Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1211, LocationType.CHALLENGE, - lambda state: logic.terran_survives_rip_field(state)), - LocationData("Maw of the Void", "Maw of the Void: West Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1212, LocationType.CHALLENGE, - lambda state: logic.terran_survives_rip_field(state)), - LocationData("Maw of the Void", "Maw of the Void: Southwest Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1213, LocationType.CHALLENGE, - lambda state: logic.terran_survives_rip_field(state)), - LocationData("Devil's Playground", "Devil's Playground: Victory", SC2WOL_LOC_ID_OFFSET + 1300, LocationType.VICTORY, - lambda state: adv_tactics or - logic.terran_basic_anti_air(state) and ( - logic.terran_common_unit(state) or state.has(ItemNames.REAPER, player))), - LocationData("Devil's Playground", "Devil's Playground: Tosh's Miners", SC2WOL_LOC_ID_OFFSET + 1301, LocationType.VANILLA), - LocationData("Devil's Playground", "Devil's Playground: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1302, LocationType.VANILLA, - lambda state: adv_tactics or logic.terran_common_unit(state) or state.has(ItemNames.REAPER, player)), - LocationData("Devil's Playground", "Devil's Playground: North Reapers", SC2WOL_LOC_ID_OFFSET + 1303, LocationType.EXTRA), - LocationData("Devil's Playground", "Devil's Playground: Middle Reapers", SC2WOL_LOC_ID_OFFSET + 1304, LocationType.EXTRA, - lambda state: adv_tactics or logic.terran_common_unit(state) or state.has(ItemNames.REAPER, player)), - LocationData("Devil's Playground", "Devil's Playground: Southwest Reapers", SC2WOL_LOC_ID_OFFSET + 1305, LocationType.EXTRA, - lambda state: adv_tactics or logic.terran_common_unit(state) or state.has(ItemNames.REAPER, player)), - LocationData("Devil's Playground", "Devil's Playground: Southeast Reapers", SC2WOL_LOC_ID_OFFSET + 1306, LocationType.EXTRA, - lambda state: adv_tactics or - logic.terran_basic_anti_air(state) and ( - logic.terran_common_unit(state) or state.has(ItemNames.REAPER, player))), - LocationData("Devil's Playground", "Devil's Playground: East Reapers", SC2WOL_LOC_ID_OFFSET + 1307, LocationType.CHALLENGE, - lambda state: logic.terran_basic_anti_air(state) and - (adv_tactics or - logic.terran_common_unit(state) or state.has(ItemNames.REAPER, player))), - LocationData("Devil's Playground", "Devil's Playground: Zerg Cleared", SC2WOL_LOC_ID_OFFSET + 1308, LocationType.CHALLENGE, - lambda state: logic.terran_competent_anti_air(state) and ( - logic.terran_common_unit(state) or state.has(ItemNames.REAPER, player))), - LocationData("Welcome to the Jungle", "Welcome to the Jungle: Victory", SC2WOL_LOC_ID_OFFSET + 1400, LocationType.VICTORY, - lambda state: logic.welcome_to_the_jungle_requirement(state)), - LocationData("Welcome to the Jungle", "Welcome to the Jungle: Close Relic", SC2WOL_LOC_ID_OFFSET + 1401, LocationType.VANILLA), - LocationData("Welcome to the Jungle", "Welcome to the Jungle: West Relic", SC2WOL_LOC_ID_OFFSET + 1402, LocationType.VANILLA, - lambda state: logic.welcome_to_the_jungle_requirement(state)), - LocationData("Welcome to the Jungle", "Welcome to the Jungle: North-East Relic", SC2WOL_LOC_ID_OFFSET + 1403, LocationType.VANILLA, - lambda state: logic.welcome_to_the_jungle_requirement(state)), - LocationData("Welcome to the Jungle", "Welcome to the Jungle: Middle Base", SC2WOL_LOC_ID_OFFSET + 1404, LocationType.EXTRA, - lambda state: logic.welcome_to_the_jungle_requirement(state)), - LocationData("Welcome to the Jungle", "Welcome to the Jungle: Main Base", SC2WOL_LOC_ID_OFFSET + 1405, - LocationType.MASTERY, - lambda state: logic.welcome_to_the_jungle_requirement(state) - and logic.terran_beats_protoss_deathball(state) - and logic.terran_base_trasher(state)), - LocationData("Welcome to the Jungle", "Welcome to the Jungle: No Terrazine Nodes Sealed", SC2WOL_LOC_ID_OFFSET + 1406, LocationType.CHALLENGE, - lambda state: logic.welcome_to_the_jungle_requirement(state) - and logic.terran_competent_ground_to_air(state) - and logic.terran_beats_protoss_deathball(state)), - LocationData("Welcome to the Jungle", "Welcome to the Jungle: Up to 1 Terrazine Node Sealed", SC2WOL_LOC_ID_OFFSET + 1407, LocationType.CHALLENGE, - lambda state: logic.welcome_to_the_jungle_requirement(state) - and logic.terran_competent_ground_to_air(state) - and logic.terran_beats_protoss_deathball(state)), - LocationData("Welcome to the Jungle", "Welcome to the Jungle: Up to 2 Terrazine Nodes Sealed", SC2WOL_LOC_ID_OFFSET + 1408, LocationType.CHALLENGE, - lambda state: logic.welcome_to_the_jungle_requirement(state) - and logic.terran_beats_protoss_deathball(state)), - LocationData("Welcome to the Jungle", "Welcome to the Jungle: Up to 3 Terrazine Nodes Sealed", SC2WOL_LOC_ID_OFFSET + 1409, LocationType.CHALLENGE, - lambda state: logic.welcome_to_the_jungle_requirement(state) - and logic.terran_competent_comp(state)), - LocationData("Welcome to the Jungle", "Welcome to the Jungle: Up to 4 Terrazine Nodes Sealed", SC2WOL_LOC_ID_OFFSET + 1410, LocationType.EXTRA, - lambda state: logic.welcome_to_the_jungle_requirement(state)), - LocationData("Welcome to the Jungle", "Welcome to the Jungle: Up to 5 Terrazine Nodes Sealed", SC2WOL_LOC_ID_OFFSET + 1411, LocationType.EXTRA, - lambda state: logic.welcome_to_the_jungle_requirement(state)), - LocationData("Breakout", "Breakout: Victory", SC2WOL_LOC_ID_OFFSET + 1500, LocationType.VICTORY), - LocationData("Breakout", "Breakout: Diamondback Prison", SC2WOL_LOC_ID_OFFSET + 1501, LocationType.VANILLA), - LocationData("Breakout", "Breakout: Siege Tank Prison", SC2WOL_LOC_ID_OFFSET + 1502, LocationType.VANILLA), - LocationData("Breakout", "Breakout: First Checkpoint", SC2WOL_LOC_ID_OFFSET + 1503, LocationType.EXTRA), - LocationData("Breakout", "Breakout: Second Checkpoint", SC2WOL_LOC_ID_OFFSET + 1504, LocationType.EXTRA), - 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.EXTRA), - LocationData("Ghost of a Chance", "Ghost of a Chance: Jorium Stockpile", SC2WOL_LOC_ID_OFFSET + 1602, LocationType.EXTRA), - LocationData("Ghost of a Chance", "Ghost of a Chance: First Island Spectres", SC2WOL_LOC_ID_OFFSET + 1603, LocationType.VANILLA), - LocationData("Ghost of a Chance", "Ghost of a Chance: Second Island Spectres", SC2WOL_LOC_ID_OFFSET + 1604, LocationType.VANILLA), - LocationData("Ghost of a Chance", "Ghost of a Chance: Third Island Spectres", SC2WOL_LOC_ID_OFFSET + 1605, LocationType.VANILLA), - LocationData("The Great Train Robbery", "The Great Train Robbery: Victory", SC2WOL_LOC_ID_OFFSET + 1700, LocationType.VICTORY, - lambda state: logic.great_train_robbery_train_stopper(state) and - logic.terran_basic_anti_air(state)), - LocationData("The Great Train Robbery", "The Great Train Robbery: North Defiler", SC2WOL_LOC_ID_OFFSET + 1701, LocationType.VANILLA), - LocationData("The Great Train Robbery", "The Great Train Robbery: Mid Defiler", SC2WOL_LOC_ID_OFFSET + 1702, LocationType.VANILLA), - LocationData("The Great Train Robbery", "The Great Train Robbery: South Defiler", SC2WOL_LOC_ID_OFFSET + 1703, LocationType.VANILLA), - LocationData("The Great Train Robbery", "The Great Train Robbery: Close Diamondback", SC2WOL_LOC_ID_OFFSET + 1704, LocationType.EXTRA), - LocationData("The Great Train Robbery", "The Great Train Robbery: Northwest Diamondback", SC2WOL_LOC_ID_OFFSET + 1705, LocationType.EXTRA), - LocationData("The Great Train Robbery", "The Great Train Robbery: North Diamondback", SC2WOL_LOC_ID_OFFSET + 1706, LocationType.EXTRA), - LocationData("The Great Train Robbery", "The Great Train Robbery: Northeast Diamondback", SC2WOL_LOC_ID_OFFSET + 1707, LocationType.EXTRA), - LocationData("The Great Train Robbery", "The Great Train Robbery: Southwest Diamondback", SC2WOL_LOC_ID_OFFSET + 1708, LocationType.EXTRA), - LocationData("The Great Train Robbery", "The Great Train Robbery: Southeast Diamondback", SC2WOL_LOC_ID_OFFSET + 1709, LocationType.EXTRA), - LocationData("The Great Train Robbery", "The Great Train Robbery: Kill Team", SC2WOL_LOC_ID_OFFSET + 1710, LocationType.CHALLENGE, - lambda state: (adv_tactics or logic.terran_common_unit(state)) and - logic.great_train_robbery_train_stopper(state) and - logic.terran_basic_anti_air(state)), - LocationData("The Great Train Robbery", "The Great Train Robbery: Flawless", SC2WOL_LOC_ID_OFFSET + 1711, LocationType.CHALLENGE, - lambda state: logic.great_train_robbery_train_stopper(state) and - logic.terran_basic_anti_air(state)), - LocationData("The Great Train Robbery", "The Great Train Robbery: 2 Trains Destroyed", SC2WOL_LOC_ID_OFFSET + 1712, LocationType.EXTRA, - lambda state: logic.great_train_robbery_train_stopper(state)), - LocationData("The Great Train Robbery", "The Great Train Robbery: 4 Trains Destroyed", SC2WOL_LOC_ID_OFFSET + 1713, LocationType.EXTRA, - lambda state: logic.great_train_robbery_train_stopper(state) and - logic.terran_basic_anti_air(state)), - LocationData("The Great Train Robbery", "The Great Train Robbery: 6 Trains Destroyed", SC2WOL_LOC_ID_OFFSET + 1714, LocationType.EXTRA, - lambda state: logic.great_train_robbery_train_stopper(state) and - logic.terran_basic_anti_air(state)), - LocationData("Cutthroat", "Cutthroat: Victory", SC2WOL_LOC_ID_OFFSET + 1800, LocationType.VICTORY, - lambda state: logic.terran_common_unit(state) and - (adv_tactics or logic.terran_basic_anti_air)), - LocationData("Cutthroat", "Cutthroat: Mira Han", SC2WOL_LOC_ID_OFFSET + 1801, LocationType.EXTRA, - lambda state: logic.terran_common_unit(state)), - LocationData("Cutthroat", "Cutthroat: North Relic", SC2WOL_LOC_ID_OFFSET + 1802, LocationType.VANILLA, - lambda state: logic.terran_common_unit(state)), - LocationData("Cutthroat", "Cutthroat: Mid Relic", SC2WOL_LOC_ID_OFFSET + 1803, LocationType.VANILLA), - LocationData("Cutthroat", "Cutthroat: Southwest Relic", SC2WOL_LOC_ID_OFFSET + 1804, LocationType.VANILLA, - lambda state: logic.terran_common_unit(state)), - LocationData("Cutthroat", "Cutthroat: North Command Center", SC2WOL_LOC_ID_OFFSET + 1805, LocationType.EXTRA, - lambda state: logic.terran_common_unit(state)), - LocationData("Cutthroat", "Cutthroat: South Command Center", SC2WOL_LOC_ID_OFFSET + 1806, LocationType.EXTRA, - lambda state: logic.terran_common_unit(state)), - LocationData("Cutthroat", "Cutthroat: West Command Center", SC2WOL_LOC_ID_OFFSET + 1807, LocationType.EXTRA, - lambda state: logic.terran_common_unit(state)), - LocationData("Engine of Destruction", "Engine of Destruction: Victory", SC2WOL_LOC_ID_OFFSET + 1900, LocationType.VICTORY, - lambda state: logic.engine_of_destruction_requirement(state)), - LocationData("Engine of Destruction", "Engine of Destruction: Odin", SC2WOL_LOC_ID_OFFSET + 1901, LocationType.EXTRA, - lambda state: logic.marine_medic_upgrade(state)), - LocationData("Engine of Destruction", "Engine of Destruction: Loki", SC2WOL_LOC_ID_OFFSET + 1902, - LocationType.CHALLENGE, - lambda state: logic.engine_of_destruction_requirement(state)), - LocationData("Engine of Destruction", "Engine of Destruction: Lab Devourer", SC2WOL_LOC_ID_OFFSET + 1903, LocationType.VANILLA, - lambda state: logic.marine_medic_upgrade(state)), - LocationData("Engine of Destruction", "Engine of Destruction: North Devourer", SC2WOL_LOC_ID_OFFSET + 1904, LocationType.VANILLA, - lambda state: logic.engine_of_destruction_requirement(state)), - LocationData("Engine of Destruction", "Engine of Destruction: Southeast Devourer", SC2WOL_LOC_ID_OFFSET + 1905, LocationType.VANILLA, - lambda state: logic.engine_of_destruction_requirement(state)), - LocationData("Engine of Destruction", "Engine of Destruction: West Base", SC2WOL_LOC_ID_OFFSET + 1906, LocationType.EXTRA, - lambda state: logic.engine_of_destruction_requirement(state)), - LocationData("Engine of Destruction", "Engine of Destruction: Northwest Base", SC2WOL_LOC_ID_OFFSET + 1907, LocationType.EXTRA, - lambda state: logic.engine_of_destruction_requirement(state)), - LocationData("Engine of Destruction", "Engine of Destruction: Northeast Base", SC2WOL_LOC_ID_OFFSET + 1908, LocationType.EXTRA, - lambda state: logic.engine_of_destruction_requirement(state)), - LocationData("Engine of Destruction", "Engine of Destruction: Southeast Base", SC2WOL_LOC_ID_OFFSET + 1909, LocationType.EXTRA, - lambda state: logic.engine_of_destruction_requirement(state)), - LocationData("Media Blitz", "Media Blitz: Victory", SC2WOL_LOC_ID_OFFSET + 2000, LocationType.VICTORY, - lambda state: logic.terran_competent_comp(state)), - LocationData("Media Blitz", "Media Blitz: Tower 1", SC2WOL_LOC_ID_OFFSET + 2001, LocationType.VANILLA, - lambda state: logic.terran_competent_comp(state)), - LocationData("Media Blitz", "Media Blitz: Tower 2", SC2WOL_LOC_ID_OFFSET + 2002, LocationType.VANILLA, - lambda state: logic.terran_competent_comp(state)), - LocationData("Media Blitz", "Media Blitz: Tower 3", SC2WOL_LOC_ID_OFFSET + 2003, LocationType.VANILLA, - lambda state: logic.terran_competent_comp(state)), - LocationData("Media Blitz", "Media Blitz: Science Facility", SC2WOL_LOC_ID_OFFSET + 2004, LocationType.VANILLA), - LocationData("Media Blitz", "Media Blitz: All Barracks", SC2WOL_LOC_ID_OFFSET + 2005, LocationType.EXTRA, - lambda state: logic.terran_competent_comp(state)), - LocationData("Media Blitz", "Media Blitz: All Factories", SC2WOL_LOC_ID_OFFSET + 2006, LocationType.EXTRA, - lambda state: logic.terran_competent_comp(state)), - LocationData("Media Blitz", "Media Blitz: All Starports", SC2WOL_LOC_ID_OFFSET + 2007, LocationType.EXTRA, - lambda state: adv_tactics or logic.terran_competent_comp(state)), - LocationData("Media Blitz", "Media Blitz: Odin Not Trashed", SC2WOL_LOC_ID_OFFSET + 2008, LocationType.CHALLENGE, - lambda state: logic.terran_competent_comp(state)), - LocationData("Media Blitz", "Media Blitz: Surprise Attack Ends", SC2WOL_LOC_ID_OFFSET + 2009, LocationType.EXTRA), - LocationData("Piercing the Shroud", "Piercing the Shroud: Victory", SC2WOL_LOC_ID_OFFSET + 2100, LocationType.VICTORY, - lambda state: logic.marine_medic_upgrade(state)), - LocationData("Piercing the Shroud", "Piercing the Shroud: Holding Cell Relic", SC2WOL_LOC_ID_OFFSET + 2101, LocationType.VANILLA), - LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk Relic", SC2WOL_LOC_ID_OFFSET + 2102, LocationType.VANILLA, - lambda state: logic.marine_medic_upgrade(state)), - LocationData("Piercing the Shroud", "Piercing the Shroud: First Escape Relic", SC2WOL_LOC_ID_OFFSET + 2103, LocationType.VANILLA, - lambda state: logic.marine_medic_upgrade(state)), - LocationData("Piercing the Shroud", "Piercing the Shroud: Second Escape Relic", SC2WOL_LOC_ID_OFFSET + 2104, LocationType.VANILLA, - lambda state: logic.marine_medic_upgrade(state)), - LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk", SC2WOL_LOC_ID_OFFSET + 2105, LocationType.VANILLA, - lambda state: logic.marine_medic_upgrade(state)), - LocationData("Piercing the Shroud", "Piercing the Shroud: Fusion Reactor", SC2WOL_LOC_ID_OFFSET + 2106, LocationType.EXTRA, - lambda state: logic.marine_medic_upgrade(state)), - LocationData("Piercing the Shroud", "Piercing the Shroud: Entrance Holding Pen", SC2WOL_LOC_ID_OFFSET + 2107, LocationType.EXTRA), - LocationData("Piercing the Shroud", "Piercing the Shroud: Cargo Bay Warbot", SC2WOL_LOC_ID_OFFSET + 2108, LocationType.EXTRA), - LocationData("Piercing the Shroud", "Piercing the Shroud: Escape Warbot", SC2WOL_LOC_ID_OFFSET + 2109, LocationType.EXTRA, - lambda state: logic.marine_medic_upgrade(state)), - 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.VANILLA), - LocationData("Whispers of Doom", "Whispers of Doom: Second Hatchery", SC2WOL_LOC_ID_OFFSET + 2202, LocationType.VANILLA), - LocationData("Whispers of Doom", "Whispers of Doom: Third Hatchery", SC2WOL_LOC_ID_OFFSET + 2203, LocationType.VANILLA), - LocationData("Whispers of Doom", "Whispers of Doom: First Prophecy Fragment", SC2WOL_LOC_ID_OFFSET + 2204, LocationType.EXTRA), - LocationData("Whispers of Doom", "Whispers of Doom: Second Prophecy Fragment", SC2WOL_LOC_ID_OFFSET + 2205, LocationType.EXTRA), - LocationData("Whispers of Doom", "Whispers of Doom: Third Prophecy Fragment", SC2WOL_LOC_ID_OFFSET + 2206, LocationType.EXTRA), - LocationData("A Sinister Turn", "A Sinister Turn: Victory", SC2WOL_LOC_ID_OFFSET + 2300, LocationType.VICTORY, - lambda state: logic.protoss_common_unit(state) and logic.protoss_competent_anti_air(state)), - LocationData("A Sinister Turn", "A Sinister Turn: Robotics Facility", SC2WOL_LOC_ID_OFFSET + 2301, LocationType.VANILLA, - lambda state: adv_tactics or logic.protoss_common_unit(state)), - LocationData("A Sinister Turn", "A Sinister Turn: Dark Shrine", SC2WOL_LOC_ID_OFFSET + 2302, LocationType.VANILLA, - lambda state: adv_tactics or logic.protoss_common_unit(state)), - LocationData("A Sinister Turn", "A Sinister Turn: Templar Archives", SC2WOL_LOC_ID_OFFSET + 2303, LocationType.VANILLA, - lambda state: logic.protoss_common_unit(state) and logic.protoss_competent_anti_air(state)), - LocationData("A Sinister Turn", "A Sinister Turn: Northeast Base", SC2WOL_LOC_ID_OFFSET + 2304, LocationType.EXTRA, - lambda state: logic.protoss_common_unit(state) and logic.protoss_competent_anti_air(state)), - LocationData("A Sinister Turn", "A Sinister Turn: Southwest Base", SC2WOL_LOC_ID_OFFSET + 2305, LocationType.CHALLENGE, - lambda state: logic.protoss_common_unit(state) and logic.protoss_competent_anti_air(state)), - LocationData("A Sinister Turn", "A Sinister Turn: Maar", SC2WOL_LOC_ID_OFFSET + 2306, LocationType.EXTRA, - lambda state: logic.protoss_common_unit(state)), - LocationData("A Sinister Turn", "A Sinister Turn: Northwest Preserver", SC2WOL_LOC_ID_OFFSET + 2307, LocationType.EXTRA, - lambda state: logic.protoss_common_unit(state) and logic.protoss_competent_anti_air(state)), - LocationData("A Sinister Turn", "A Sinister Turn: Southwest Preserver", SC2WOL_LOC_ID_OFFSET + 2308, LocationType.EXTRA, - lambda state: logic.protoss_common_unit(state) and logic.protoss_competent_anti_air(state)), - LocationData("A Sinister Turn", "A Sinister Turn: East Preserver", SC2WOL_LOC_ID_OFFSET + 2309, LocationType.EXTRA, - lambda state: logic.protoss_common_unit(state) and logic.protoss_competent_anti_air(state)), - LocationData("Echoes of the Future", "Echoes of the Future: Victory", SC2WOL_LOC_ID_OFFSET + 2400, LocationType.VICTORY, - lambda state: adv_tactics and logic.protoss_static_defense(state) or logic.protoss_common_unit(state) and logic.protoss_competent_anti_air(state)), - LocationData("Echoes of the Future", "Echoes of the Future: Close Obelisk", SC2WOL_LOC_ID_OFFSET + 2401, LocationType.VANILLA), - LocationData("Echoes of the Future", "Echoes of the Future: West Obelisk", SC2WOL_LOC_ID_OFFSET + 2402, LocationType.VANILLA, - lambda state: adv_tactics and logic.protoss_static_defense(state) or logic.protoss_common_unit(state)), - LocationData("Echoes of the Future", "Echoes of the Future: Base", SC2WOL_LOC_ID_OFFSET + 2403, LocationType.EXTRA), - LocationData("Echoes of the Future", "Echoes of the Future: Southwest Tendril", SC2WOL_LOC_ID_OFFSET + 2404, LocationType.EXTRA), - LocationData("Echoes of the Future", "Echoes of the Future: Southeast Tendril", SC2WOL_LOC_ID_OFFSET + 2405, LocationType.EXTRA, - lambda state: adv_tactics and logic.protoss_static_defense(state) or logic.protoss_common_unit(state)), - LocationData("Echoes of the Future", "Echoes of the Future: Northeast Tendril", SC2WOL_LOC_ID_OFFSET + 2406, LocationType.EXTRA, - lambda state: adv_tactics and logic.protoss_static_defense(state) or logic.protoss_common_unit(state)), - LocationData("Echoes of the Future", "Echoes of the Future: Northwest Tendril", SC2WOL_LOC_ID_OFFSET + 2407, LocationType.EXTRA, - lambda state: adv_tactics and logic.protoss_static_defense(state) or logic.protoss_common_unit(state)), - 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.VANILLA, - lambda state: logic.last_stand_requirement(state)), - LocationData("In Utter Darkness", "In Utter Darkness: Kills", SC2WOL_LOC_ID_OFFSET + 2502, LocationType.VANILLA, - lambda state: logic.last_stand_requirement(state)), - LocationData("In Utter Darkness", "In Utter Darkness: Urun", SC2WOL_LOC_ID_OFFSET + 2503, LocationType.EXTRA), - LocationData("In Utter Darkness", "In Utter Darkness: Mohandar", SC2WOL_LOC_ID_OFFSET + 2504, LocationType.EXTRA, - lambda state: logic.last_stand_requirement(state)), - LocationData("In Utter Darkness", "In Utter Darkness: Selendis", SC2WOL_LOC_ID_OFFSET + 2505, LocationType.EXTRA, - lambda state: logic.last_stand_requirement(state)), - LocationData("In Utter Darkness", "In Utter Darkness: Artanis", SC2WOL_LOC_ID_OFFSET + 2506, LocationType.EXTRA, - lambda state: logic.last_stand_requirement(state)), - LocationData("Gates of Hell", "Gates of Hell: Victory", SC2WOL_LOC_ID_OFFSET + 2600, LocationType.VICTORY, - lambda state: logic.terran_competent_comp(state) and - logic.terran_defense_rating(state, True) > 6), - LocationData("Gates of Hell", "Gates of Hell: Large Army", SC2WOL_LOC_ID_OFFSET + 2601, LocationType.VANILLA, - lambda state: logic.terran_competent_comp(state) and - logic.terran_defense_rating(state, True) > 6), - LocationData("Gates of Hell", "Gates of Hell: 2 Drop Pods", SC2WOL_LOC_ID_OFFSET + 2602, LocationType.VANILLA, - lambda state: logic.terran_competent_comp(state) and - logic.terran_defense_rating(state, True) > 6), - LocationData("Gates of Hell", "Gates of Hell: 4 Drop Pods", SC2WOL_LOC_ID_OFFSET + 2603, LocationType.VANILLA, - lambda state: logic.terran_competent_comp(state) and - logic.terran_defense_rating(state, True) > 6), - LocationData("Gates of Hell", "Gates of Hell: 6 Drop Pods", SC2WOL_LOC_ID_OFFSET + 2604, LocationType.EXTRA, - lambda state: logic.terran_competent_comp(state) and - logic.terran_defense_rating(state, True) > 6), - LocationData("Gates of Hell", "Gates of Hell: 8 Drop Pods", SC2WOL_LOC_ID_OFFSET + 2605, LocationType.CHALLENGE, - lambda state: logic.terran_competent_comp(state) and - logic.terran_defense_rating(state, True) > 6), - LocationData("Gates of Hell", "Gates of Hell: Southwest Spore Cannon", SC2WOL_LOC_ID_OFFSET + 2606, LocationType.EXTRA, - lambda state: logic.terran_competent_comp(state) and - logic.terran_defense_rating(state, True) > 6), - LocationData("Gates of Hell", "Gates of Hell: Northwest Spore Cannon", SC2WOL_LOC_ID_OFFSET + 2607, LocationType.EXTRA, - lambda state: logic.terran_competent_comp(state) and - logic.terran_defense_rating(state, True) > 6), - LocationData("Gates of Hell", "Gates of Hell: Northeast Spore Cannon", SC2WOL_LOC_ID_OFFSET + 2608, LocationType.EXTRA, - lambda state: logic.terran_competent_comp(state) and - logic.terran_defense_rating(state, True) > 6), - LocationData("Gates of Hell", "Gates of Hell: East Spore Cannon", SC2WOL_LOC_ID_OFFSET + 2609, LocationType.EXTRA, - lambda state: logic.terran_competent_comp(state) and - logic.terran_defense_rating(state, True) > 6), - LocationData("Gates of Hell", "Gates of Hell: Southeast Spore Cannon", SC2WOL_LOC_ID_OFFSET + 2610, LocationType.EXTRA, - lambda state: logic.terran_competent_comp(state) and - logic.terran_defense_rating(state, True) > 6), - LocationData("Gates of Hell", "Gates of Hell: Expansion Spore Cannon", SC2WOL_LOC_ID_OFFSET + 2611, LocationType.EXTRA, - lambda state: logic.terran_competent_comp(state) and - logic.terran_defense_rating(state, 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.EXTRA), - LocationData("Belly of the Beast", "Belly of the Beast: Second Charge", SC2WOL_LOC_ID_OFFSET + 2702, LocationType.EXTRA), - LocationData("Belly of the Beast", "Belly of the Beast: Third Charge", SC2WOL_LOC_ID_OFFSET + 2703, LocationType.EXTRA), - LocationData("Belly of the Beast", "Belly of the Beast: First Group Rescued", SC2WOL_LOC_ID_OFFSET + 2704, LocationType.VANILLA), - LocationData("Belly of the Beast", "Belly of the Beast: Second Group Rescued", SC2WOL_LOC_ID_OFFSET + 2705, LocationType.VANILLA), - LocationData("Belly of the Beast", "Belly of the Beast: Third Group Rescued", SC2WOL_LOC_ID_OFFSET + 2706, LocationType.VANILLA), - LocationData("Shatter the Sky", "Shatter the Sky: Victory", SC2WOL_LOC_ID_OFFSET + 2800, LocationType.VICTORY, - lambda state: logic.terran_competent_comp(state)), - LocationData("Shatter the Sky", "Shatter the Sky: Close Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2801, LocationType.VANILLA, - lambda state: logic.terran_competent_comp(state)), - LocationData("Shatter the Sky", "Shatter the Sky: Northwest Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2802, LocationType.VANILLA, - lambda state: logic.terran_competent_comp(state)), - LocationData("Shatter the Sky", "Shatter the Sky: Southeast Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2803, LocationType.VANILLA, - lambda state: logic.terran_competent_comp(state)), - LocationData("Shatter the Sky", "Shatter the Sky: Southwest Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2804, LocationType.VANILLA, - lambda state: logic.terran_competent_comp(state)), - LocationData("Shatter the Sky", "Shatter the Sky: Leviathan", SC2WOL_LOC_ID_OFFSET + 2805, LocationType.VANILLA, - lambda state: logic.terran_competent_comp(state)), - LocationData("Shatter the Sky", "Shatter the Sky: East Hatchery", SC2WOL_LOC_ID_OFFSET + 2806, LocationType.EXTRA, - lambda state: logic.terran_competent_comp(state)), - LocationData("Shatter the Sky", "Shatter the Sky: North Hatchery", SC2WOL_LOC_ID_OFFSET + 2807, LocationType.EXTRA, - lambda state: logic.terran_competent_comp(state)), - LocationData("Shatter the Sky", "Shatter the Sky: Mid Hatchery", SC2WOL_LOC_ID_OFFSET + 2808, LocationType.EXTRA, - lambda state: logic.terran_competent_comp(state)), - LocationData("All-In", "All-In: Victory", SC2WOL_LOC_ID_OFFSET + 2900, LocationType.VICTORY, - lambda state: logic.all_in_requirement(state)), - LocationData("All-In", "All-In: First Kerrigan Attack", SC2WOL_LOC_ID_OFFSET + 2901, LocationType.EXTRA, - lambda state: logic.all_in_requirement(state)), - LocationData("All-In", "All-In: Second Kerrigan Attack", SC2WOL_LOC_ID_OFFSET + 2902, LocationType.EXTRA, - lambda state: logic.all_in_requirement(state)), - LocationData("All-In", "All-In: Third Kerrigan Attack", SC2WOL_LOC_ID_OFFSET + 2903, LocationType.EXTRA, - lambda state: logic.all_in_requirement(state)), - LocationData("All-In", "All-In: Fourth Kerrigan Attack", SC2WOL_LOC_ID_OFFSET + 2904, LocationType.EXTRA, - lambda state: logic.all_in_requirement(state)), - LocationData("All-In", "All-In: Fifth Kerrigan Attack", SC2WOL_LOC_ID_OFFSET + 2905, LocationType.EXTRA, - lambda state: logic.all_in_requirement(state)), - - # HotS - LocationData("Lab Rat", "Lab Rat: Victory", SC2HOTS_LOC_ID_OFFSET + 100, LocationType.VICTORY, - lambda state: logic.zerg_common_unit(state)), - LocationData("Lab Rat", "Lab Rat: Gather Minerals", SC2HOTS_LOC_ID_OFFSET + 101, LocationType.VANILLA), - LocationData("Lab Rat", "Lab Rat: South Zergling Group", SC2HOTS_LOC_ID_OFFSET + 102, LocationType.VANILLA, - lambda state: adv_tactics or logic.zerg_common_unit(state)), - LocationData("Lab Rat", "Lab Rat: East Zergling Group", SC2HOTS_LOC_ID_OFFSET + 103, LocationType.VANILLA, - lambda state: adv_tactics or logic.zerg_common_unit(state)), - LocationData("Lab Rat", "Lab Rat: West Zergling Group", SC2HOTS_LOC_ID_OFFSET + 104, LocationType.VANILLA, - lambda state: adv_tactics or logic.zerg_common_unit(state)), - LocationData("Lab Rat", "Lab Rat: Hatchery", SC2HOTS_LOC_ID_OFFSET + 105, LocationType.EXTRA), - LocationData("Lab Rat", "Lab Rat: Overlord", SC2HOTS_LOC_ID_OFFSET + 106, LocationType.EXTRA), - LocationData("Lab Rat", "Lab Rat: Gas Turrets", SC2HOTS_LOC_ID_OFFSET + 107, LocationType.EXTRA, - lambda state: adv_tactics or logic.zerg_common_unit(state)), - LocationData("Back in the Saddle", "Back in the Saddle: Victory", SC2HOTS_LOC_ID_OFFSET + 200, LocationType.VICTORY, - lambda state: logic.basic_kerrigan(state) or kerriganless or logic.story_tech_granted), - LocationData("Back in the Saddle", "Back in the Saddle: Defend the Tram", SC2HOTS_LOC_ID_OFFSET + 201, LocationType.EXTRA, - lambda state: logic.basic_kerrigan(state) or kerriganless or logic.story_tech_granted), - LocationData("Back in the Saddle", "Back in the Saddle: Kinetic Blast", SC2HOTS_LOC_ID_OFFSET + 202, LocationType.VANILLA), - LocationData("Back in the Saddle", "Back in the Saddle: Crushing Grip", SC2HOTS_LOC_ID_OFFSET + 203, LocationType.VANILLA), - LocationData("Back in the Saddle", "Back in the Saddle: Reach the Sublevel", SC2HOTS_LOC_ID_OFFSET + 204, LocationType.EXTRA), - LocationData("Back in the Saddle", "Back in the Saddle: Door Section Cleared", SC2HOTS_LOC_ID_OFFSET + 205, LocationType.EXTRA, - lambda state: logic.basic_kerrigan(state) or kerriganless or logic.story_tech_granted), - LocationData("Rendezvous", "Rendezvous: Victory", SC2HOTS_LOC_ID_OFFSET + 300, LocationType.VICTORY, - lambda state: logic.zerg_common_unit(state) and - logic.zerg_basic_anti_air(state)), - LocationData("Rendezvous", "Rendezvous: Right Queen", SC2HOTS_LOC_ID_OFFSET + 301, LocationType.VANILLA, - lambda state: logic.zerg_common_unit(state) and - logic.zerg_basic_anti_air(state)), - LocationData("Rendezvous", "Rendezvous: Center Queen", SC2HOTS_LOC_ID_OFFSET + 302, LocationType.VANILLA, - lambda state: logic.zerg_common_unit(state) and - logic.zerg_basic_anti_air(state)), - LocationData("Rendezvous", "Rendezvous: Left Queen", SC2HOTS_LOC_ID_OFFSET + 303, LocationType.VANILLA, - lambda state: logic.zerg_common_unit(state) and - logic.zerg_basic_anti_air(state)), - LocationData("Rendezvous", "Rendezvous: Hold Out Finished", SC2HOTS_LOC_ID_OFFSET + 304, LocationType.EXTRA, - lambda state: logic.zerg_common_unit(state) and - logic.zerg_basic_anti_air(state)), - LocationData("Harvest of Screams", "Harvest of Screams: Victory", SC2HOTS_LOC_ID_OFFSET + 400, LocationType.VICTORY, - lambda state: logic.zerg_common_unit(state) - and logic.zerg_basic_anti_air(state)), - LocationData("Harvest of Screams", "Harvest of Screams: First Ursadon Matriarch", SC2HOTS_LOC_ID_OFFSET + 401, LocationType.VANILLA), - LocationData("Harvest of Screams", "Harvest of Screams: North Ursadon Matriarch", SC2HOTS_LOC_ID_OFFSET + 402, LocationType.VANILLA, - lambda state: logic.zerg_common_unit(state)), - LocationData("Harvest of Screams", "Harvest of Screams: West Ursadon Matriarch", SC2HOTS_LOC_ID_OFFSET + 403, LocationType.VANILLA, - lambda state: logic.zerg_common_unit(state)), - LocationData("Harvest of Screams", "Harvest of Screams: Lost Brood", SC2HOTS_LOC_ID_OFFSET + 404, LocationType.EXTRA), - LocationData("Harvest of Screams", "Harvest of Screams: Northeast Psi-link Spire", SC2HOTS_LOC_ID_OFFSET + 405, LocationType.EXTRA, - lambda state: logic.zerg_common_unit(state)), - LocationData("Harvest of Screams", "Harvest of Screams: Northwest Psi-link Spire", SC2HOTS_LOC_ID_OFFSET + 406, LocationType.EXTRA, - lambda state: logic.zerg_common_unit(state) - and logic.zerg_basic_anti_air(state)), - LocationData("Harvest of Screams", "Harvest of Screams: Southwest Psi-link Spire", SC2HOTS_LOC_ID_OFFSET + 407, LocationType.EXTRA, - lambda state: logic.zerg_common_unit(state) - and logic.zerg_basic_anti_air(state)), - LocationData("Harvest of Screams", "Harvest of Screams: Nafash", SC2HOTS_LOC_ID_OFFSET + 408, LocationType.EXTRA, - lambda state: logic.zerg_common_unit(state) - and logic.zerg_basic_anti_air(state)), - LocationData("Shoot the Messenger", "Shoot the Messenger: Victory", SC2HOTS_LOC_ID_OFFSET + 500, LocationType.VICTORY, - lambda state: logic.zerg_common_unit(state) and - logic.zerg_competent_anti_air(state)), - LocationData("Shoot the Messenger", "Shoot the Messenger: East Stasis Chamber", SC2HOTS_LOC_ID_OFFSET + 501, LocationType.VANILLA, - lambda state: logic.zerg_common_unit(state) and logic.zerg_basic_anti_air(state)), - LocationData("Shoot the Messenger", "Shoot the Messenger: Center Stasis Chamber", SC2HOTS_LOC_ID_OFFSET + 502, LocationType.VANILLA, - lambda state: logic.zerg_common_unit(state) or adv_tactics), - LocationData("Shoot the Messenger", "Shoot the Messenger: West Stasis Chamber", SC2HOTS_LOC_ID_OFFSET + 503, LocationType.VANILLA, - lambda state: logic.zerg_common_unit(state) and logic.zerg_basic_anti_air(state)), - LocationData("Shoot the Messenger", "Shoot the Messenger: Destroy 4 Shuttles", SC2HOTS_LOC_ID_OFFSET + 504, LocationType.EXTRA, - lambda state: logic.zerg_common_unit(state) and logic.zerg_basic_anti_air(state)), - LocationData("Shoot the Messenger", "Shoot the Messenger: Frozen Expansion", SC2HOTS_LOC_ID_OFFSET + 505, LocationType.EXTRA, - lambda state: logic.zerg_common_unit(state)), - LocationData("Shoot the Messenger", "Shoot the Messenger: Southwest Frozen Zerg", SC2HOTS_LOC_ID_OFFSET + 506, LocationType.EXTRA), - LocationData("Shoot the Messenger", "Shoot the Messenger: Southeast Frozen Zerg", SC2HOTS_LOC_ID_OFFSET + 507, LocationType.EXTRA, - lambda state: logic.zerg_common_unit(state) or adv_tactics), - LocationData("Shoot the Messenger", "Shoot the Messenger: West Frozen Zerg", SC2HOTS_LOC_ID_OFFSET + 508, LocationType.EXTRA, - lambda state: logic.zerg_common_unit(state) and logic.zerg_basic_anti_air(state)), - LocationData("Shoot the Messenger", "Shoot the Messenger: East Frozen Zerg", SC2HOTS_LOC_ID_OFFSET + 509, LocationType.EXTRA, - lambda state: logic.zerg_common_unit(state) and logic.zerg_competent_anti_air(state)), - LocationData("Enemy Within", "Enemy Within: Victory", SC2HOTS_LOC_ID_OFFSET + 600, LocationType.VICTORY, - lambda state: logic.zerg_pass_vents(state) - and (logic.story_tech_granted - or state.has_any({ItemNames.ZERGLING_RAPTOR_STRAIN, ItemNames.ROACH, - ItemNames.HYDRALISK, ItemNames.INFESTOR}, player)) - ), - LocationData("Enemy Within", "Enemy Within: Infest Giant Ursadon", SC2HOTS_LOC_ID_OFFSET + 601, LocationType.VANILLA, - lambda state: logic.zerg_pass_vents(state)), - LocationData("Enemy Within", "Enemy Within: First Niadra Evolution", SC2HOTS_LOC_ID_OFFSET + 602, LocationType.VANILLA, - lambda state: logic.zerg_pass_vents(state)), - LocationData("Enemy Within", "Enemy Within: Second Niadra Evolution", SC2HOTS_LOC_ID_OFFSET + 603, LocationType.VANILLA, - lambda state: logic.zerg_pass_vents(state)), - LocationData("Enemy Within", "Enemy Within: Third Niadra Evolution", SC2HOTS_LOC_ID_OFFSET + 604, LocationType.VANILLA, - lambda state: logic.zerg_pass_vents(state)), - LocationData("Enemy Within", "Enemy Within: Warp Drive", SC2HOTS_LOC_ID_OFFSET + 605, LocationType.EXTRA, - lambda state: logic.zerg_pass_vents(state)), - LocationData("Enemy Within", "Enemy Within: Stasis Quadrant", SC2HOTS_LOC_ID_OFFSET + 606, LocationType.EXTRA, - lambda state: logic.zerg_pass_vents(state)), - LocationData("Domination", "Domination: Victory", SC2HOTS_LOC_ID_OFFSET + 700, LocationType.VICTORY, - lambda state: logic.zerg_common_unit(state) and logic.zerg_basic_anti_air(state)), - LocationData("Domination", "Domination: Center Infested Command Center", SC2HOTS_LOC_ID_OFFSET + 701, LocationType.VANILLA, - lambda state: logic.zerg_common_unit(state)), - LocationData("Domination", "Domination: North Infested Command Center", SC2HOTS_LOC_ID_OFFSET + 702, LocationType.VANILLA, - lambda state: logic.zerg_common_unit(state)), - LocationData("Domination", "Domination: Repel Zagara", SC2HOTS_LOC_ID_OFFSET + 703, LocationType.EXTRA), - LocationData("Domination", "Domination: Close Baneling Nest", SC2HOTS_LOC_ID_OFFSET + 704, LocationType.EXTRA), - LocationData("Domination", "Domination: South Baneling Nest", SC2HOTS_LOC_ID_OFFSET + 705, LocationType.EXTRA, - lambda state: adv_tactics or logic.zerg_common_unit(state)), - LocationData("Domination", "Domination: Southwest Baneling Nest", SC2HOTS_LOC_ID_OFFSET + 706, LocationType.EXTRA, - lambda state: logic.zerg_common_unit(state)), - LocationData("Domination", "Domination: Southeast Baneling Nest", SC2HOTS_LOC_ID_OFFSET + 707, LocationType.EXTRA, - lambda state: logic.zerg_common_unit(state) and logic.zerg_basic_anti_air(state)), - LocationData("Domination", "Domination: North Baneling Nest", SC2HOTS_LOC_ID_OFFSET + 708, LocationType.EXTRA, - lambda state: logic.zerg_common_unit(state)), - LocationData("Domination", "Domination: Northeast Baneling Nest", SC2HOTS_LOC_ID_OFFSET + 709, LocationType.EXTRA, - lambda state: logic.zerg_common_unit(state)), - LocationData("Fire in the Sky", "Fire in the Sky: Victory", SC2HOTS_LOC_ID_OFFSET + 800, LocationType.VICTORY, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_basic_anti_air(state) and - logic.spread_creep(state)), - LocationData("Fire in the Sky", "Fire in the Sky: West Biomass", SC2HOTS_LOC_ID_OFFSET + 801, LocationType.VANILLA), - LocationData("Fire in the Sky", "Fire in the Sky: North Biomass", SC2HOTS_LOC_ID_OFFSET + 802, LocationType.VANILLA, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_basic_anti_air(state) and - logic.spread_creep(state)), - LocationData("Fire in the Sky", "Fire in the Sky: South Biomass", SC2HOTS_LOC_ID_OFFSET + 803, LocationType.VANILLA, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_basic_anti_air(state) and - logic.spread_creep(state)), - LocationData("Fire in the Sky", "Fire in the Sky: Destroy 3 Gorgons", SC2HOTS_LOC_ID_OFFSET + 804, LocationType.EXTRA, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_basic_anti_air(state) and - logic.spread_creep(state)), - LocationData("Fire in the Sky", "Fire in the Sky: Close Zerg Rescue", SC2HOTS_LOC_ID_OFFSET + 805, LocationType.EXTRA), - LocationData("Fire in the Sky", "Fire in the Sky: South Zerg Rescue", SC2HOTS_LOC_ID_OFFSET + 806, LocationType.EXTRA, - lambda state: logic.zerg_common_unit(state)), - LocationData("Fire in the Sky", "Fire in the Sky: North Zerg Rescue", SC2HOTS_LOC_ID_OFFSET + 807, LocationType.EXTRA, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_basic_anti_air(state) and - logic.spread_creep(state)), - LocationData("Fire in the Sky", "Fire in the Sky: West Queen Rescue", SC2HOTS_LOC_ID_OFFSET + 808, LocationType.EXTRA, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_basic_anti_air(state) and - logic.spread_creep(state)), - LocationData("Fire in the Sky", "Fire in the Sky: East Queen Rescue", SC2HOTS_LOC_ID_OFFSET + 809, LocationType.EXTRA, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_basic_anti_air(state) and - logic.spread_creep(state)), - LocationData("Old Soldiers", "Old Soldiers: Victory", SC2HOTS_LOC_ID_OFFSET + 900, LocationType.VICTORY, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_basic_anti_air(state)), - LocationData("Old Soldiers", "Old Soldiers: East Science Lab", SC2HOTS_LOC_ID_OFFSET + 901, LocationType.VANILLA, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_basic_anti_air(state)), - LocationData("Old Soldiers", "Old Soldiers: North Science Lab", SC2HOTS_LOC_ID_OFFSET + 902, LocationType.VANILLA, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_basic_anti_air(state)), - LocationData("Old Soldiers", "Old Soldiers: Get Nuked", SC2HOTS_LOC_ID_OFFSET + 903, LocationType.EXTRA), - LocationData("Old Soldiers", "Old Soldiers: Entrance Gate", SC2HOTS_LOC_ID_OFFSET + 904, LocationType.EXTRA), - LocationData("Old Soldiers", "Old Soldiers: Citadel Gate", SC2HOTS_LOC_ID_OFFSET + 905, LocationType.EXTRA, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_basic_anti_air(state)), - LocationData("Old Soldiers", "Old Soldiers: South Expansion", SC2HOTS_LOC_ID_OFFSET + 906, LocationType.EXTRA), - LocationData("Old Soldiers", "Old Soldiers: Rich Mineral Expansion", SC2HOTS_LOC_ID_OFFSET + 907, LocationType.EXTRA, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_basic_anti_air(state)), - LocationData("Waking the Ancient", "Waking the Ancient: Victory", SC2HOTS_LOC_ID_OFFSET + 1000, LocationType.VICTORY, - lambda state: logic.zerg_common_unit(state) and - logic.zerg_competent_anti_air(state)), - LocationData("Waking the Ancient", "Waking the Ancient: Center Essence Pool", SC2HOTS_LOC_ID_OFFSET + 1001, LocationType.VANILLA), - LocationData("Waking the Ancient", "Waking the Ancient: East Essence Pool", SC2HOTS_LOC_ID_OFFSET + 1002, LocationType.VANILLA, - lambda state: logic.zerg_common_unit(state) and - (adv_tactics and logic.zerg_basic_anti_air(state) - or logic.zerg_competent_anti_air(state))), - LocationData("Waking the Ancient", "Waking the Ancient: South Essence Pool", SC2HOTS_LOC_ID_OFFSET + 1003, LocationType.VANILLA, - lambda state: logic.zerg_common_unit(state) and - (adv_tactics and logic.zerg_basic_anti_air(state) - or logic.zerg_competent_anti_air(state))), - LocationData("Waking the Ancient", "Waking the Ancient: Finish Feeding", SC2HOTS_LOC_ID_OFFSET + 1004, LocationType.EXTRA, - lambda state: logic.zerg_common_unit(state) and - logic.zerg_competent_anti_air(state)), - LocationData("Waking the Ancient", "Waking the Ancient: South Proxy Primal Hive", SC2HOTS_LOC_ID_OFFSET + 1005, LocationType.CHALLENGE, - lambda state: logic.zerg_common_unit(state) and - logic.zerg_competent_anti_air(state)), - LocationData("Waking the Ancient", "Waking the Ancient: East Proxy Primal Hive", SC2HOTS_LOC_ID_OFFSET + 1006, LocationType.CHALLENGE, - lambda state: logic.zerg_common_unit(state) and - logic.zerg_competent_anti_air(state)), - LocationData("Waking the Ancient", "Waking the Ancient: South Main Primal Hive", SC2HOTS_LOC_ID_OFFSET + 1007, LocationType.CHALLENGE, - lambda state: logic.zerg_common_unit(state) and - logic.zerg_competent_anti_air(state)), - LocationData("Waking the Ancient", "Waking the Ancient: East Main Primal Hive", SC2HOTS_LOC_ID_OFFSET + 1008, LocationType.CHALLENGE, - lambda state: logic.zerg_common_unit(state) and - logic.zerg_competent_anti_air(state)), - LocationData("The Crucible", "The Crucible: Victory", SC2HOTS_LOC_ID_OFFSET + 1100, LocationType.VICTORY, - lambda state: logic.zerg_competent_defense(state) and - logic.zerg_competent_anti_air(state)), - LocationData("The Crucible", "The Crucible: Tyrannozor", SC2HOTS_LOC_ID_OFFSET + 1101, LocationType.VANILLA, - lambda state: logic.zerg_competent_defense(state) and - logic.zerg_competent_anti_air(state)), - LocationData("The Crucible", "The Crucible: Reach the Pool", SC2HOTS_LOC_ID_OFFSET + 1102, LocationType.VANILLA), - LocationData("The Crucible", "The Crucible: 15 Minutes Remaining", SC2HOTS_LOC_ID_OFFSET + 1103, LocationType.EXTRA, - lambda state: logic.zerg_competent_defense(state) and - logic.zerg_competent_anti_air(state)), - LocationData("The Crucible", "The Crucible: 5 Minutes Remaining", SC2HOTS_LOC_ID_OFFSET + 1104, LocationType.EXTRA, - lambda state: logic.zerg_competent_defense(state) and - logic.zerg_competent_anti_air(state)), - LocationData("The Crucible", "The Crucible: Pincer Attack", SC2HOTS_LOC_ID_OFFSET + 1105, LocationType.EXTRA, - lambda state: logic.zerg_competent_defense(state) and - logic.zerg_competent_anti_air(state)), - LocationData("The Crucible", "The Crucible: Yagdra Claims Brakk's Pack", SC2HOTS_LOC_ID_OFFSET + 1106, LocationType.EXTRA, - lambda state: logic.zerg_competent_defense(state) and - logic.zerg_competent_anti_air(state)), - LocationData("Supreme", "Supreme: Victory", SC2HOTS_LOC_ID_OFFSET + 1200, LocationType.VICTORY, - lambda state: logic.supreme_requirement(state)), - LocationData("Supreme", "Supreme: First Relic", SC2HOTS_LOC_ID_OFFSET + 1201, LocationType.VANILLA, - lambda state: logic.supreme_requirement(state)), - LocationData("Supreme", "Supreme: Second Relic", SC2HOTS_LOC_ID_OFFSET + 1202, LocationType.VANILLA, - lambda state: logic.supreme_requirement(state)), - LocationData("Supreme", "Supreme: Third Relic", SC2HOTS_LOC_ID_OFFSET + 1203, LocationType.VANILLA, - lambda state: logic.supreme_requirement(state)), - LocationData("Supreme", "Supreme: Fourth Relic", SC2HOTS_LOC_ID_OFFSET + 1204, LocationType.VANILLA, - lambda state: logic.supreme_requirement(state)), - LocationData("Supreme", "Supreme: Yagdra", SC2HOTS_LOC_ID_OFFSET + 1205, LocationType.EXTRA, - lambda state: logic.supreme_requirement(state)), - LocationData("Supreme", "Supreme: Kraith", SC2HOTS_LOC_ID_OFFSET + 1206, LocationType.EXTRA, - lambda state: logic.supreme_requirement(state)), - LocationData("Supreme", "Supreme: Slivan", SC2HOTS_LOC_ID_OFFSET + 1207, LocationType.EXTRA, - lambda state: logic.supreme_requirement(state)), - LocationData("Infested", "Infested: Victory", SC2HOTS_LOC_ID_OFFSET + 1300, LocationType.VICTORY, - lambda state: logic.zerg_common_unit(state) and - ((logic.zerg_competent_anti_air(state) and state.has(ItemNames.INFESTOR, player)) or - (adv_tactics and logic.zerg_basic_anti_air(state)))), - LocationData("Infested", "Infested: East Science Facility", SC2HOTS_LOC_ID_OFFSET + 1301, LocationType.VANILLA, - lambda state: logic.zerg_common_unit(state) and - logic.zerg_basic_anti_air(state) and - logic.spread_creep(state)), - LocationData("Infested", "Infested: Center Science Facility", SC2HOTS_LOC_ID_OFFSET + 1302, LocationType.VANILLA, - lambda state: logic.zerg_common_unit(state) and - logic.zerg_basic_anti_air(state) and - logic.spread_creep(state)), - LocationData("Infested", "Infested: West Science Facility", SC2HOTS_LOC_ID_OFFSET + 1303, LocationType.VANILLA, - lambda state: logic.zerg_common_unit(state) and - logic.zerg_basic_anti_air(state) and - logic.spread_creep(state)), - LocationData("Infested", "Infested: First Intro Garrison", SC2HOTS_LOC_ID_OFFSET + 1304, LocationType.EXTRA), - LocationData("Infested", "Infested: Second Intro Garrison", SC2HOTS_LOC_ID_OFFSET + 1305, LocationType.EXTRA), - LocationData("Infested", "Infested: Base Garrison", SC2HOTS_LOC_ID_OFFSET + 1306, LocationType.EXTRA), - LocationData("Infested", "Infested: East Garrison", SC2HOTS_LOC_ID_OFFSET + 1307, LocationType.EXTRA, - lambda state: logic.zerg_common_unit(state) - and logic.zerg_basic_anti_air(state) - and (adv_tactics or state.has(ItemNames.INFESTOR, player))), - LocationData("Infested", "Infested: Mid Garrison", SC2HOTS_LOC_ID_OFFSET + 1308, LocationType.EXTRA, - lambda state: logic.zerg_common_unit(state) - and logic.zerg_basic_anti_air(state) - and (adv_tactics or state.has(ItemNames.INFESTOR, player))), - LocationData("Infested", "Infested: North Garrison", SC2HOTS_LOC_ID_OFFSET + 1309, LocationType.EXTRA, - lambda state: logic.zerg_common_unit(state) - and logic.zerg_basic_anti_air(state) - and (adv_tactics or state.has(ItemNames.INFESTOR, player))), - LocationData("Infested", "Infested: Close Southwest Garrison", SC2HOTS_LOC_ID_OFFSET + 1310, LocationType.EXTRA, - lambda state: logic.zerg_common_unit(state) - and logic.zerg_basic_anti_air(state) - and (adv_tactics or state.has(ItemNames.INFESTOR, player))), - LocationData("Infested", "Infested: Far Southwest Garrison", SC2HOTS_LOC_ID_OFFSET + 1311, LocationType.EXTRA, - lambda state: logic.zerg_common_unit(state) - and logic.zerg_basic_anti_air(state) - and (adv_tactics or state.has(ItemNames.INFESTOR, player))), - LocationData("Hand of Darkness", "Hand of Darkness: Victory", SC2HOTS_LOC_ID_OFFSET + 1400, LocationType.VICTORY, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_basic_anti_air(state)), - LocationData("Hand of Darkness", "Hand of Darkness: North Brutalisk", SC2HOTS_LOC_ID_OFFSET + 1401, LocationType.VANILLA, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_basic_anti_air(state)), - LocationData("Hand of Darkness", "Hand of Darkness: South Brutalisk", SC2HOTS_LOC_ID_OFFSET + 1402, LocationType.VANILLA, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_basic_anti_air(state)), - LocationData("Hand of Darkness", "Hand of Darkness: Kill 1 Hybrid", SC2HOTS_LOC_ID_OFFSET + 1403, LocationType.EXTRA, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_basic_anti_air(state)), - LocationData("Hand of Darkness", "Hand of Darkness: Kill 2 Hybrid", SC2HOTS_LOC_ID_OFFSET + 1404, LocationType.EXTRA, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_basic_anti_air(state)), - LocationData("Hand of Darkness", "Hand of Darkness: Kill 3 Hybrid", SC2HOTS_LOC_ID_OFFSET + 1405, LocationType.EXTRA, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_basic_anti_air(state)), - LocationData("Hand of Darkness", "Hand of Darkness: Kill 4 Hybrid", SC2HOTS_LOC_ID_OFFSET + 1406, LocationType.EXTRA, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_basic_anti_air(state)), - LocationData("Hand of Darkness", "Hand of Darkness: Kill 5 Hybrid", SC2HOTS_LOC_ID_OFFSET + 1407, LocationType.EXTRA, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_basic_anti_air(state)), - LocationData("Hand of Darkness", "Hand of Darkness: Kill 6 Hybrid", SC2HOTS_LOC_ID_OFFSET + 1408, LocationType.EXTRA, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_basic_anti_air(state)), - LocationData("Hand of Darkness", "Hand of Darkness: Kill 7 Hybrid", SC2HOTS_LOC_ID_OFFSET + 1409, LocationType.EXTRA, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_basic_anti_air(state)), - LocationData("Phantoms of the Void", "Phantoms of the Void: Victory", SC2HOTS_LOC_ID_OFFSET + 1500, LocationType.VICTORY, - lambda state: logic.zerg_competent_comp(state) and - (logic.zerg_competent_anti_air(state) or adv_tactics)), - LocationData("Phantoms of the Void", "Phantoms of the Void: Northwest Crystal", SC2HOTS_LOC_ID_OFFSET + 1501, LocationType.VANILLA, - lambda state: logic.zerg_competent_comp(state) and - (logic.zerg_competent_anti_air(state) or adv_tactics)), - LocationData("Phantoms of the Void", "Phantoms of the Void: Northeast Crystal", SC2HOTS_LOC_ID_OFFSET + 1502, LocationType.VANILLA, - lambda state: logic.zerg_competent_comp(state) and - (logic.zerg_competent_anti_air(state) or adv_tactics)), - LocationData("Phantoms of the Void", "Phantoms of the Void: South Crystal", SC2HOTS_LOC_ID_OFFSET + 1503, LocationType.VANILLA), - LocationData("Phantoms of the Void", "Phantoms of the Void: Base Established", SC2HOTS_LOC_ID_OFFSET + 1504, LocationType.EXTRA), - LocationData("Phantoms of the Void", "Phantoms of the Void: Close Temple", SC2HOTS_LOC_ID_OFFSET + 1505, LocationType.EXTRA, - lambda state: logic.zerg_competent_comp(state) and - (logic.zerg_competent_anti_air(state) or adv_tactics)), - LocationData("Phantoms of the Void", "Phantoms of the Void: Mid Temple", SC2HOTS_LOC_ID_OFFSET + 1506, LocationType.EXTRA, - lambda state: logic.zerg_competent_comp(state) and - (logic.zerg_competent_anti_air(state) or adv_tactics)), - LocationData("Phantoms of the Void", "Phantoms of the Void: Southeast Temple", SC2HOTS_LOC_ID_OFFSET + 1507, LocationType.EXTRA, - lambda state: logic.zerg_competent_comp(state) and - (logic.zerg_competent_anti_air(state) or adv_tactics)), - LocationData("Phantoms of the Void", "Phantoms of the Void: Northeast Temple", SC2HOTS_LOC_ID_OFFSET + 1508, LocationType.EXTRA, - lambda state: logic.zerg_competent_comp(state) and - (logic.zerg_competent_anti_air(state) or adv_tactics)), - LocationData("Phantoms of the Void", "Phantoms of the Void: Northwest Temple", SC2HOTS_LOC_ID_OFFSET + 1509, LocationType.EXTRA, - lambda state: logic.zerg_competent_comp(state) and - (logic.zerg_competent_anti_air(state) or adv_tactics)), - LocationData("With Friends Like These", "With Friends Like These: Victory", SC2HOTS_LOC_ID_OFFSET + 1600, LocationType.VICTORY), - LocationData("With Friends Like These", "With Friends Like These: Pirate Capital Ship", SC2HOTS_LOC_ID_OFFSET + 1601, LocationType.VANILLA), - LocationData("With Friends Like These", "With Friends Like These: First Mineral Patch", SC2HOTS_LOC_ID_OFFSET + 1602, LocationType.VANILLA), - LocationData("With Friends Like These", "With Friends Like These: Second Mineral Patch", SC2HOTS_LOC_ID_OFFSET + 1603, LocationType.VANILLA), - LocationData("With Friends Like These", "With Friends Like These: Third Mineral Patch", SC2HOTS_LOC_ID_OFFSET + 1604, LocationType.VANILLA), - LocationData("Conviction", "Conviction: Victory", SC2HOTS_LOC_ID_OFFSET + 1700, LocationType.VICTORY, - lambda state: logic.two_kerrigan_actives(state) and - (logic.basic_kerrigan(state) or logic.story_tech_granted) or kerriganless), - LocationData("Conviction", "Conviction: First Secret Documents", SC2HOTS_LOC_ID_OFFSET + 1701, LocationType.VANILLA, - lambda state: logic.two_kerrigan_actives(state) or kerriganless), - LocationData("Conviction", "Conviction: Second Secret Documents", SC2HOTS_LOC_ID_OFFSET + 1702, LocationType.VANILLA, - lambda state: logic.two_kerrigan_actives(state) and - (logic.basic_kerrigan(state) or logic.story_tech_granted) or kerriganless), - LocationData("Conviction", "Conviction: Power Coupling", SC2HOTS_LOC_ID_OFFSET + 1703, LocationType.EXTRA, - lambda state: logic.two_kerrigan_actives(state) or kerriganless), - LocationData("Conviction", "Conviction: Door Blasted", SC2HOTS_LOC_ID_OFFSET + 1704, LocationType.EXTRA, - lambda state: logic.two_kerrigan_actives(state) or kerriganless), - LocationData("Planetfall", "Planetfall: Victory", SC2HOTS_LOC_ID_OFFSET + 1800, LocationType.VICTORY, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_competent_anti_air(state)), - LocationData("Planetfall", "Planetfall: East Gate", SC2HOTS_LOC_ID_OFFSET + 1801, LocationType.VANILLA, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_competent_anti_air(state)), - LocationData("Planetfall", "Planetfall: Northwest Gate", SC2HOTS_LOC_ID_OFFSET + 1802, LocationType.VANILLA, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_competent_anti_air(state)), - LocationData("Planetfall", "Planetfall: North Gate", SC2HOTS_LOC_ID_OFFSET + 1803, LocationType.VANILLA, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_competent_anti_air(state)), - LocationData("Planetfall", "Planetfall: 1 Bile Launcher Deployed", SC2HOTS_LOC_ID_OFFSET + 1804, LocationType.EXTRA, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_competent_anti_air(state)), - LocationData("Planetfall", "Planetfall: 2 Bile Launchers Deployed", SC2HOTS_LOC_ID_OFFSET + 1805, LocationType.EXTRA, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_competent_anti_air(state)), - LocationData("Planetfall", "Planetfall: 3 Bile Launchers Deployed", SC2HOTS_LOC_ID_OFFSET + 1806, LocationType.EXTRA, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_competent_anti_air(state)), - LocationData("Planetfall", "Planetfall: 4 Bile Launchers Deployed", SC2HOTS_LOC_ID_OFFSET + 1807, LocationType.EXTRA, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_competent_anti_air(state)), - LocationData("Planetfall", "Planetfall: 5 Bile Launchers Deployed", SC2HOTS_LOC_ID_OFFSET + 1808, LocationType.EXTRA, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_competent_anti_air(state)), - LocationData("Planetfall", "Planetfall: Sons of Korhal", SC2HOTS_LOC_ID_OFFSET + 1809, LocationType.EXTRA, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_competent_anti_air(state)), - LocationData("Planetfall", "Planetfall: Night Wolves", SC2HOTS_LOC_ID_OFFSET + 1810, LocationType.EXTRA, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_competent_anti_air(state)), - LocationData("Planetfall", "Planetfall: West Expansion", SC2HOTS_LOC_ID_OFFSET + 1811, LocationType.EXTRA, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_competent_anti_air(state)), - LocationData("Planetfall", "Planetfall: Mid Expansion", SC2HOTS_LOC_ID_OFFSET + 1812, LocationType.EXTRA, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_competent_anti_air(state)), - LocationData("Death From Above", "Death From Above: Victory", SC2HOTS_LOC_ID_OFFSET + 1900, LocationType.VICTORY, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_competent_anti_air(state)), - LocationData("Death From Above", "Death From Above: First Power Link", SC2HOTS_LOC_ID_OFFSET + 1901, LocationType.VANILLA), - LocationData("Death From Above", "Death From Above: Second Power Link", SC2HOTS_LOC_ID_OFFSET + 1902, LocationType.VANILLA, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_competent_anti_air(state)), - LocationData("Death From Above", "Death From Above: Third Power Link", SC2HOTS_LOC_ID_OFFSET + 1903, LocationType.VANILLA, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_competent_anti_air(state)), - LocationData("Death From Above", "Death From Above: Expansion Command Center", SC2HOTS_LOC_ID_OFFSET + 1904, LocationType.EXTRA, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_competent_anti_air(state)), - LocationData("Death From Above", "Death From Above: Main Path Command Center", SC2HOTS_LOC_ID_OFFSET + 1905, LocationType.EXTRA, - lambda state: logic.zerg_competent_comp(state) and - logic.zerg_competent_anti_air(state)), - LocationData("The Reckoning", "The Reckoning: Victory", SC2HOTS_LOC_ID_OFFSET + 2000, LocationType.VICTORY, - lambda state: logic.the_reckoning_requirement(state)), - LocationData("The Reckoning", "The Reckoning: South Lane", SC2HOTS_LOC_ID_OFFSET + 2001, LocationType.VANILLA, - lambda state: logic.the_reckoning_requirement(state)), - LocationData("The Reckoning", "The Reckoning: North Lane", SC2HOTS_LOC_ID_OFFSET + 2002, LocationType.VANILLA, - lambda state: logic.the_reckoning_requirement(state)), - LocationData("The Reckoning", "The Reckoning: East Lane", SC2HOTS_LOC_ID_OFFSET + 2003, LocationType.VANILLA, - lambda state: logic.the_reckoning_requirement(state)), - LocationData("The Reckoning", "The Reckoning: Odin", SC2HOTS_LOC_ID_OFFSET + 2004, LocationType.EXTRA, - lambda state: logic.the_reckoning_requirement(state)), - - # LotV Prologue - LocationData("Dark Whispers", "Dark Whispers: Victory", SC2LOTV_LOC_ID_OFFSET + 100, LocationType.VICTORY, - lambda state: logic.protoss_common_unit(state) \ - and logic.protoss_basic_anti_air(state)), - LocationData("Dark Whispers", "Dark Whispers: First Prisoner Group", SC2LOTV_LOC_ID_OFFSET + 101, LocationType.VANILLA, - lambda state: logic.protoss_common_unit(state) \ - and logic.protoss_basic_anti_air(state)), - LocationData("Dark Whispers", "Dark Whispers: Second Prisoner Group", SC2LOTV_LOC_ID_OFFSET + 102, LocationType.VANILLA, - lambda state: logic.protoss_common_unit(state) \ - and logic.protoss_basic_anti_air(state)), - LocationData("Dark Whispers", "Dark Whispers: First Pylon", SC2LOTV_LOC_ID_OFFSET + 103, LocationType.VANILLA, - lambda state: logic.protoss_common_unit(state) \ - and logic.protoss_basic_anti_air(state)), - LocationData("Dark Whispers", "Dark Whispers: Second Pylon", SC2LOTV_LOC_ID_OFFSET + 104, LocationType.VANILLA, - lambda state: logic.protoss_common_unit(state) \ - and logic.protoss_basic_anti_air(state)), - LocationData("Ghosts in the Fog", "Ghosts in the Fog: Victory", SC2LOTV_LOC_ID_OFFSET + 200, LocationType.VICTORY, - lambda state: logic.protoss_common_unit(state) \ - and logic.protoss_anti_armor_anti_air(state)), - LocationData("Ghosts in the Fog", "Ghosts in the Fog: South Rock Formation", SC2LOTV_LOC_ID_OFFSET + 201, LocationType.VANILLA, - lambda state: logic.protoss_common_unit(state) \ - and logic.protoss_anti_armor_anti_air(state)), - LocationData("Ghosts in the Fog", "Ghosts in the Fog: West Rock Formation", SC2LOTV_LOC_ID_OFFSET + 202, LocationType.VANILLA, - lambda state: logic.protoss_common_unit(state) \ - and logic.protoss_anti_armor_anti_air(state)), - LocationData("Ghosts in the Fog", "Ghosts in the Fog: East Rock Formation", SC2LOTV_LOC_ID_OFFSET + 203, LocationType.VANILLA, - lambda state: logic.protoss_common_unit(state) \ - and logic.protoss_anti_armor_anti_air(state) \ - and logic.protoss_can_attack_behind_chasm(state)), - LocationData("Evil Awoken", "Evil Awoken: Victory", SC2LOTV_LOC_ID_OFFSET + 300, LocationType.VICTORY, - lambda state: adv_tactics or logic.protoss_stalker_upgrade(state)), - LocationData("Evil Awoken", "Evil Awoken: Temple Investigated", SC2LOTV_LOC_ID_OFFSET + 301, LocationType.EXTRA), - LocationData("Evil Awoken", "Evil Awoken: Void Catalyst", SC2LOTV_LOC_ID_OFFSET + 302, LocationType.EXTRA), - LocationData("Evil Awoken", "Evil Awoken: First Particle Cannon", SC2LOTV_LOC_ID_OFFSET + 303, LocationType.VANILLA), - LocationData("Evil Awoken", "Evil Awoken: Second Particle Cannon", SC2LOTV_LOC_ID_OFFSET + 304, LocationType.VANILLA), - LocationData("Evil Awoken", "Evil Awoken: Third Particle Cannon", SC2LOTV_LOC_ID_OFFSET + 305, LocationType.VANILLA), - - - # LotV - LocationData("For Aiur!", "For Aiur!: Victory", SC2LOTV_LOC_ID_OFFSET + 400, LocationType.VICTORY), - LocationData("For Aiur!", "For Aiur!: Southwest Hive", SC2LOTV_LOC_ID_OFFSET + 401, LocationType.VANILLA), - LocationData("For Aiur!", "For Aiur!: Northwest Hive", SC2LOTV_LOC_ID_OFFSET + 402, LocationType.VANILLA), - LocationData("For Aiur!", "For Aiur!: Northeast Hive", SC2LOTV_LOC_ID_OFFSET + 403, LocationType.VANILLA), - LocationData("For Aiur!", "For Aiur!: East Hive", SC2LOTV_LOC_ID_OFFSET + 404, LocationType.VANILLA), - LocationData("For Aiur!", "For Aiur!: West Conduit", SC2LOTV_LOC_ID_OFFSET + 405, LocationType.EXTRA), - LocationData("For Aiur!", "For Aiur!: Middle Conduit", SC2LOTV_LOC_ID_OFFSET + 406, LocationType.EXTRA), - LocationData("For Aiur!", "For Aiur!: Northeast Conduit", SC2LOTV_LOC_ID_OFFSET + 407, LocationType.EXTRA), - LocationData("The Growing Shadow", "The Growing Shadow: Victory", SC2LOTV_LOC_ID_OFFSET + 500, LocationType.VICTORY, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_basic_anti_air(state)), - LocationData("The Growing Shadow", "The Growing Shadow: Close Pylon", SC2LOTV_LOC_ID_OFFSET + 501, LocationType.VANILLA), - LocationData("The Growing Shadow", "The Growing Shadow: East Pylon", SC2LOTV_LOC_ID_OFFSET + 502, LocationType.VANILLA, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_basic_anti_air(state)), - LocationData("The Growing Shadow", "The Growing Shadow: West Pylon", SC2LOTV_LOC_ID_OFFSET + 503, LocationType.VANILLA, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_basic_anti_air(state)), - LocationData("The Growing Shadow", "The Growing Shadow: Nexus", SC2LOTV_LOC_ID_OFFSET + 504, LocationType.EXTRA), - LocationData("The Growing Shadow", "The Growing Shadow: Templar Base", SC2LOTV_LOC_ID_OFFSET + 505, LocationType.EXTRA, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_basic_anti_air(state)), - LocationData("The Spear of Adun", "The Spear of Adun: Victory", SC2LOTV_LOC_ID_OFFSET + 600, LocationType.VICTORY, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_anti_light_anti_air(state)), - LocationData("The Spear of Adun", "The Spear of Adun: Close Warp Gate", SC2LOTV_LOC_ID_OFFSET + 601, LocationType.VANILLA), - LocationData("The Spear of Adun", "The Spear of Adun: West Warp Gate", SC2LOTV_LOC_ID_OFFSET + 602, LocationType.VANILLA, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_anti_light_anti_air(state)), - LocationData("The Spear of Adun", "The Spear of Adun: North Warp Gate", SC2LOTV_LOC_ID_OFFSET + 603, LocationType.VANILLA, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_anti_light_anti_air(state)), - LocationData("The Spear of Adun", "The Spear of Adun: North Power Cell", SC2LOTV_LOC_ID_OFFSET + 604, LocationType.EXTRA, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_anti_light_anti_air(state)), - LocationData("The Spear of Adun", "The Spear of Adun: East Power Cell", SC2LOTV_LOC_ID_OFFSET + 605, LocationType.EXTRA, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_anti_light_anti_air(state)), - LocationData("The Spear of Adun", "The Spear of Adun: South Power Cell", SC2LOTV_LOC_ID_OFFSET + 606, LocationType.EXTRA, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_anti_light_anti_air(state)), - LocationData("The Spear of Adun", "The Spear of Adun: Southeast Power Cell", SC2LOTV_LOC_ID_OFFSET + 607, LocationType.EXTRA, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_anti_light_anti_air(state)), - LocationData("Sky Shield", "Sky Shield: Victory", SC2LOTV_LOC_ID_OFFSET + 700, LocationType.VICTORY, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_basic_anti_air(state)), - LocationData("Sky Shield", "Sky Shield: Mid EMP Scrambler", SC2LOTV_LOC_ID_OFFSET + 701, LocationType.VANILLA, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_basic_anti_air(state)), - LocationData("Sky Shield", "Sky Shield: Southeast EMP Scrambler", SC2LOTV_LOC_ID_OFFSET + 702, LocationType.VANILLA, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_basic_anti_air(state)), - LocationData("Sky Shield", "Sky Shield: North EMP Scrambler", SC2LOTV_LOC_ID_OFFSET + 703, LocationType.VANILLA, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_basic_anti_air(state)), - LocationData("Sky Shield", "Sky Shield: Mid Stabilizer", SC2LOTV_LOC_ID_OFFSET + 704, LocationType.EXTRA), - LocationData("Sky Shield", "Sky Shield: Southwest Stabilizer", SC2LOTV_LOC_ID_OFFSET + 705, LocationType.EXTRA, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_basic_anti_air(state)), - LocationData("Sky Shield", "Sky Shield: Northwest Stabilizer", SC2LOTV_LOC_ID_OFFSET + 706, LocationType.EXTRA, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_basic_anti_air(state)), - LocationData("Sky Shield", "Sky Shield: Northeast Stabilizer", SC2LOTV_LOC_ID_OFFSET + 707, LocationType.EXTRA, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_basic_anti_air(state)), - LocationData("Sky Shield", "Sky Shield: Southeast Stabilizer", SC2LOTV_LOC_ID_OFFSET + 708, LocationType.EXTRA, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_basic_anti_air(state)), - LocationData("Sky Shield", "Sky Shield: West Raynor Base", SC2LOTV_LOC_ID_OFFSET + 709, LocationType.EXTRA, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_basic_anti_air(state)), - LocationData("Sky Shield", "Sky Shield: East Raynor Base", SC2LOTV_LOC_ID_OFFSET + 710, LocationType.EXTRA, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_basic_anti_air(state)), - LocationData("Brothers in Arms", "Brothers in Arms: Victory", SC2LOTV_LOC_ID_OFFSET + 800, LocationType.VICTORY, - lambda state: logic.brothers_in_arms_requirement(state)), - LocationData("Brothers in Arms", "Brothers in Arms: Mid Science Facility", SC2LOTV_LOC_ID_OFFSET + 801, LocationType.VANILLA, - lambda state: logic.protoss_common_unit(state) or logic.take_over_ai_allies), - LocationData("Brothers in Arms", "Brothers in Arms: North Science Facility", SC2LOTV_LOC_ID_OFFSET + 802, LocationType.VANILLA, - lambda state: logic.brothers_in_arms_requirement(state) - or logic.take_over_ai_allies - and logic.advanced_tactics - and ( - logic.terran_common_unit(state) - or logic.protoss_common_unit(state) - ) - ), - LocationData("Brothers in Arms", "Brothers in Arms: South Science Facility", SC2LOTV_LOC_ID_OFFSET + 803, LocationType.VANILLA, - lambda state: logic.brothers_in_arms_requirement(state)), - LocationData("Amon's Reach", "Amon's Reach: Victory", SC2LOTV_LOC_ID_OFFSET + 900, LocationType.VICTORY, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_anti_light_anti_air(state)), - LocationData("Amon's Reach", "Amon's Reach: Close Solarite Reserve", SC2LOTV_LOC_ID_OFFSET + 901, LocationType.VANILLA), - LocationData("Amon's Reach", "Amon's Reach: North Solarite Reserve", SC2LOTV_LOC_ID_OFFSET + 902, LocationType.VANILLA, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_anti_light_anti_air(state)), - LocationData("Amon's Reach", "Amon's Reach: East Solarite Reserve", SC2LOTV_LOC_ID_OFFSET + 903, LocationType.VANILLA, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_anti_light_anti_air(state)), - LocationData("Amon's Reach", "Amon's Reach: West Launch Bay", SC2LOTV_LOC_ID_OFFSET + 904, LocationType.EXTRA, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_anti_light_anti_air(state)), - LocationData("Amon's Reach", "Amon's Reach: South Launch Bay", SC2LOTV_LOC_ID_OFFSET + 905, LocationType.EXTRA, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_anti_light_anti_air(state)), - LocationData("Amon's Reach", "Amon's Reach: Northwest Launch Bay", SC2LOTV_LOC_ID_OFFSET + 906, LocationType.EXTRA, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_anti_light_anti_air(state)), - LocationData("Amon's Reach", "Amon's Reach: East Launch Bay", SC2LOTV_LOC_ID_OFFSET + 907, LocationType.EXTRA, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_anti_light_anti_air(state)), - LocationData("Last Stand", "Last Stand: Victory", SC2LOTV_LOC_ID_OFFSET + 1000, LocationType.VICTORY, - lambda state: logic.last_stand_requirement(state)), - LocationData("Last Stand", "Last Stand: West Zenith Stone", SC2LOTV_LOC_ID_OFFSET + 1001, LocationType.VANILLA, - lambda state: logic.last_stand_requirement(state)), - LocationData("Last Stand", "Last Stand: North Zenith Stone", SC2LOTV_LOC_ID_OFFSET + 1002, LocationType.VANILLA, - lambda state: logic.last_stand_requirement(state)), - LocationData("Last Stand", "Last Stand: East Zenith Stone", SC2LOTV_LOC_ID_OFFSET + 1003, LocationType.VANILLA, - lambda state: logic.last_stand_requirement(state)), - LocationData("Last Stand", "Last Stand: 1 Billion Zerg", SC2LOTV_LOC_ID_OFFSET + 1004, LocationType.EXTRA, - lambda state: logic.last_stand_requirement(state)), - LocationData("Last Stand", "Last Stand: 1.5 Billion Zerg", SC2LOTV_LOC_ID_OFFSET + 1005, LocationType.VANILLA, - lambda state: logic.last_stand_requirement(state) and ( - state.has_all({ItemNames.KHAYDARIN_MONOLITH, ItemNames.PHOTON_CANNON, ItemNames.SHIELD_BATTERY}, player) - or state.has_any({ItemNames.SOA_SOLAR_LANCE, ItemNames.SOA_DEPLOY_FENIX}, player) - )), - LocationData("Forbidden Weapon", "Forbidden Weapon: Victory", SC2LOTV_LOC_ID_OFFSET + 1100, LocationType.VICTORY, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_anti_armor_anti_air(state)), - LocationData("Forbidden Weapon", "Forbidden Weapon: South Solarite", SC2LOTV_LOC_ID_OFFSET + 1101, LocationType.VANILLA, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_anti_armor_anti_air(state)), - LocationData("Forbidden Weapon", "Forbidden Weapon: North Solarite", SC2LOTV_LOC_ID_OFFSET + 1102, LocationType.VANILLA, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_anti_armor_anti_air(state)), - LocationData("Forbidden Weapon", "Forbidden Weapon: Northwest Solarite", SC2LOTV_LOC_ID_OFFSET + 1103, LocationType.VANILLA, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_anti_armor_anti_air(state)), - LocationData("Temple of Unification", "Temple of Unification: Victory", SC2LOTV_LOC_ID_OFFSET + 1200, LocationType.VICTORY, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_anti_armor_anti_air(state)), - LocationData("Temple of Unification", "Temple of Unification: Mid Celestial Lock", SC2LOTV_LOC_ID_OFFSET + 1201, LocationType.EXTRA, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_anti_armor_anti_air(state)), - LocationData("Temple of Unification", "Temple of Unification: West Celestial Lock", SC2LOTV_LOC_ID_OFFSET + 1202, LocationType.EXTRA, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_anti_armor_anti_air(state)), - LocationData("Temple of Unification", "Temple of Unification: South Celestial Lock", SC2LOTV_LOC_ID_OFFSET + 1203, LocationType.EXTRA, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_anti_armor_anti_air(state)), - LocationData("Temple of Unification", "Temple of Unification: East Celestial Lock", SC2LOTV_LOC_ID_OFFSET + 1204, LocationType.EXTRA, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_anti_armor_anti_air(state)), - LocationData("Temple of Unification", "Temple of Unification: North Celestial Lock", SC2LOTV_LOC_ID_OFFSET + 1205, LocationType.EXTRA, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_anti_armor_anti_air(state)), - LocationData("Temple of Unification", "Temple of Unification: Titanic Warp Prism", SC2LOTV_LOC_ID_OFFSET + 1206, LocationType.VANILLA, - lambda state: logic.protoss_common_unit(state) - and logic.protoss_anti_armor_anti_air(state)), - LocationData("The Infinite Cycle", "The Infinite Cycle: Victory", SC2LOTV_LOC_ID_OFFSET + 1300, LocationType.VICTORY, - lambda state: logic.the_infinite_cycle_requirement(state)), - LocationData("The Infinite Cycle", "The Infinite Cycle: First Hall of Revelation", SC2LOTV_LOC_ID_OFFSET + 1301, LocationType.EXTRA, - lambda state: logic.the_infinite_cycle_requirement(state)), - LocationData("The Infinite Cycle", "The Infinite Cycle: Second Hall of Revelation", SC2LOTV_LOC_ID_OFFSET + 1302, LocationType.EXTRA, - lambda state: logic.the_infinite_cycle_requirement(state)), - LocationData("The Infinite Cycle", "The Infinite Cycle: First Xel'Naga Device", SC2LOTV_LOC_ID_OFFSET + 1303, LocationType.VANILLA, - lambda state: logic.the_infinite_cycle_requirement(state)), - LocationData("The Infinite Cycle", "The Infinite Cycle: Second Xel'Naga Device", SC2LOTV_LOC_ID_OFFSET + 1304, LocationType.VANILLA, - lambda state: logic.the_infinite_cycle_requirement(state)), - LocationData("The Infinite Cycle", "The Infinite Cycle: Third Xel'Naga Device", SC2LOTV_LOC_ID_OFFSET + 1305, LocationType.VANILLA, - lambda state: logic.the_infinite_cycle_requirement(state)), - LocationData("Harbinger of Oblivion", "Harbinger of Oblivion: Victory", SC2LOTV_LOC_ID_OFFSET + 1400, LocationType.VICTORY, - lambda state: logic.harbinger_of_oblivion_requirement(state)), - LocationData("Harbinger of Oblivion", "Harbinger of Oblivion: Artanis", SC2LOTV_LOC_ID_OFFSET + 1401, LocationType.EXTRA), - LocationData("Harbinger of Oblivion", "Harbinger of Oblivion: Northwest Void Crystal", SC2LOTV_LOC_ID_OFFSET + 1402, LocationType.EXTRA, - lambda state: logic.harbinger_of_oblivion_requirement(state)), - LocationData("Harbinger of Oblivion", "Harbinger of Oblivion: Northeast Void Crystal", SC2LOTV_LOC_ID_OFFSET + 1403, LocationType.EXTRA, - lambda state: logic.harbinger_of_oblivion_requirement(state)), - LocationData("Harbinger of Oblivion", "Harbinger of Oblivion: Southwest Void Crystal", SC2LOTV_LOC_ID_OFFSET + 1404, LocationType.EXTRA, - lambda state: logic.harbinger_of_oblivion_requirement(state)), - LocationData("Harbinger of Oblivion", "Harbinger of Oblivion: Southeast Void Crystal", SC2LOTV_LOC_ID_OFFSET + 1405, LocationType.EXTRA, - lambda state: logic.harbinger_of_oblivion_requirement(state)), - LocationData("Harbinger of Oblivion", "Harbinger of Oblivion: South Xel'Naga Vessel", SC2LOTV_LOC_ID_OFFSET + 1406, LocationType.VANILLA), - LocationData("Harbinger of Oblivion", "Harbinger of Oblivion: Mid Xel'Naga Vessel", SC2LOTV_LOC_ID_OFFSET + 1407, LocationType.VANILLA, - lambda state: logic.harbinger_of_oblivion_requirement(state)), - LocationData("Harbinger of Oblivion", "Harbinger of Oblivion: North Xel'Naga Vessel", SC2LOTV_LOC_ID_OFFSET + 1408, LocationType.VANILLA, - lambda state: logic.harbinger_of_oblivion_requirement(state)), - LocationData("Unsealing the Past", "Unsealing the Past: Victory", SC2LOTV_LOC_ID_OFFSET + 1500, LocationType.VICTORY, - lambda state: logic.protoss_basic_splash(state) - and logic.protoss_anti_light_anti_air(state)), - LocationData("Unsealing the Past", "Unsealing the Past: Zerg Cleared", SC2LOTV_LOC_ID_OFFSET + 1501, LocationType.EXTRA), - LocationData("Unsealing the Past", "Unsealing the Past: First Stasis Lock", SC2LOTV_LOC_ID_OFFSET + 1502, LocationType.EXTRA, - lambda state: logic.advanced_tactics \ - or logic.protoss_basic_splash(state) - and logic.protoss_anti_light_anti_air(state)), - LocationData("Unsealing the Past", "Unsealing the Past: Second Stasis Lock", SC2LOTV_LOC_ID_OFFSET + 1503, LocationType.EXTRA, - lambda state: logic.protoss_basic_splash(state) - and logic.protoss_anti_light_anti_air(state)), - LocationData("Unsealing the Past", "Unsealing the Past: Third Stasis Lock", SC2LOTV_LOC_ID_OFFSET + 1504, LocationType.EXTRA, - lambda state: logic.protoss_basic_splash(state) - and logic.protoss_anti_light_anti_air(state)), - LocationData("Unsealing the Past", "Unsealing the Past: Fourth Stasis Lock", SC2LOTV_LOC_ID_OFFSET + 1505, LocationType.EXTRA, - lambda state: logic.protoss_basic_splash(state) - and logic.protoss_anti_light_anti_air(state)), - LocationData("Unsealing the Past", "Unsealing the Past: South Power Core", SC2LOTV_LOC_ID_OFFSET + 1506, LocationType.VANILLA, - lambda state: logic.protoss_basic_splash(state) - and logic.protoss_anti_light_anti_air(state)), - LocationData("Unsealing the Past", "Unsealing the Past: East Power Core", SC2LOTV_LOC_ID_OFFSET + 1507, LocationType.VANILLA, - lambda state: logic.protoss_basic_splash(state) - and logic.protoss_anti_light_anti_air(state)), - LocationData("Purification", "Purification: Victory", SC2LOTV_LOC_ID_OFFSET + 1600, LocationType.VICTORY, - lambda state: logic.protoss_competent_comp(state)), - LocationData("Purification", "Purification: North Sector: West Null Circuit", SC2LOTV_LOC_ID_OFFSET + 1601, LocationType.VANILLA, - lambda state: logic.protoss_competent_comp(state)), - LocationData("Purification", "Purification: North Sector: Northeast Null Circuit", SC2LOTV_LOC_ID_OFFSET + 1602, LocationType.EXTRA, - lambda state: logic.protoss_competent_comp(state)), - LocationData("Purification", "Purification: North Sector: Southeast Null Circuit", SC2LOTV_LOC_ID_OFFSET + 1603, LocationType.EXTRA, - lambda state: logic.protoss_competent_comp(state)), - LocationData("Purification", "Purification: South Sector: West Null Circuit", SC2LOTV_LOC_ID_OFFSET + 1604, LocationType.VANILLA, - lambda state: logic.protoss_competent_comp(state)), - LocationData("Purification", "Purification: South Sector: North Null Circuit", SC2LOTV_LOC_ID_OFFSET + 1605, LocationType.EXTRA, - lambda state: logic.protoss_competent_comp(state)), - LocationData("Purification", "Purification: South Sector: East Null Circuit", SC2LOTV_LOC_ID_OFFSET + 1606, LocationType.EXTRA, - lambda state: logic.protoss_competent_comp(state)), - LocationData("Purification", "Purification: West Sector: West Null Circuit", SC2LOTV_LOC_ID_OFFSET + 1607, LocationType.VANILLA, - lambda state: logic.protoss_competent_comp(state)), - LocationData("Purification", "Purification: West Sector: Mid Null Circuit", SC2LOTV_LOC_ID_OFFSET + 1608, LocationType.EXTRA, - lambda state: logic.protoss_competent_comp(state)), - LocationData("Purification", "Purification: West Sector: East Null Circuit", SC2LOTV_LOC_ID_OFFSET + 1609, LocationType.EXTRA, - lambda state: logic.protoss_competent_comp(state)), - LocationData("Purification", "Purification: East Sector: North Null Circuit", SC2LOTV_LOC_ID_OFFSET + 1610, LocationType.VANILLA, - lambda state: logic.protoss_competent_comp(state)), - LocationData("Purification", "Purification: East Sector: West Null Circuit", SC2LOTV_LOC_ID_OFFSET + 1611, LocationType.EXTRA, - lambda state: logic.protoss_competent_comp(state)), - LocationData("Purification", "Purification: East Sector: South Null Circuit", SC2LOTV_LOC_ID_OFFSET + 1612, LocationType.EXTRA, - lambda state: logic.protoss_competent_comp(state)), - LocationData("Purification", "Purification: Purifier Warden", SC2LOTV_LOC_ID_OFFSET + 1613, LocationType.VANILLA, - lambda state: logic.protoss_competent_comp(state)), - LocationData("Steps of the Rite", "Steps of the Rite: Victory", SC2LOTV_LOC_ID_OFFSET + 1700, LocationType.VICTORY, - lambda state: logic.steps_of_the_rite_requirement(state)), - LocationData("Steps of the Rite", "Steps of the Rite: First Terrazine Fog", SC2LOTV_LOC_ID_OFFSET + 1701, LocationType.EXTRA, - lambda state: logic.steps_of_the_rite_requirement(state)), - LocationData("Steps of the Rite", "Steps of the Rite: Southwest Guardian", SC2LOTV_LOC_ID_OFFSET + 1702, LocationType.EXTRA, - lambda state: logic.steps_of_the_rite_requirement(state)), - LocationData("Steps of the Rite", "Steps of the Rite: West Guardian", SC2LOTV_LOC_ID_OFFSET + 1703, LocationType.EXTRA, - lambda state: logic.steps_of_the_rite_requirement(state)), - LocationData("Steps of the Rite", "Steps of the Rite: Northwest Guardian", SC2LOTV_LOC_ID_OFFSET + 1704, LocationType.EXTRA, - lambda state: logic.steps_of_the_rite_requirement(state)), - LocationData("Steps of the Rite", "Steps of the Rite: Northeast Guardian", SC2LOTV_LOC_ID_OFFSET + 1705, LocationType.EXTRA, - lambda state: logic.steps_of_the_rite_requirement(state)), - LocationData("Steps of the Rite", "Steps of the Rite: North Mothership", SC2LOTV_LOC_ID_OFFSET + 1706, LocationType.VANILLA, - lambda state: logic.steps_of_the_rite_requirement(state)), - LocationData("Steps of the Rite", "Steps of the Rite: South Mothership", SC2LOTV_LOC_ID_OFFSET + 1707, LocationType.VANILLA, - lambda state: logic.steps_of_the_rite_requirement(state)), - LocationData("Rak'Shir", "Rak'Shir: Victory", SC2LOTV_LOC_ID_OFFSET + 1800, LocationType.VICTORY, - lambda state: logic.protoss_competent_comp(state)), - LocationData("Rak'Shir", "Rak'Shir: North Slayn Elemental", SC2LOTV_LOC_ID_OFFSET + 1801, LocationType.VANILLA, - lambda state: logic.protoss_competent_comp(state)), - LocationData("Rak'Shir", "Rak'Shir: Southwest Slayn Elemental", SC2LOTV_LOC_ID_OFFSET + 1802, LocationType.VANILLA, - lambda state: logic.protoss_competent_comp(state)), - LocationData("Rak'Shir", "Rak'Shir: East Slayn Elemental", SC2LOTV_LOC_ID_OFFSET + 1803, LocationType.VANILLA, - lambda state: logic.protoss_competent_comp(state)), - LocationData("Templar's Charge", "Templar's Charge: Victory", SC2LOTV_LOC_ID_OFFSET + 1900, LocationType.VICTORY, - lambda state: logic.templars_charge_requirement(state)), - LocationData("Templar's Charge", "Templar's Charge: Northwest Power Core", SC2LOTV_LOC_ID_OFFSET + 1901, LocationType.EXTRA, - lambda state: logic.templars_charge_requirement(state)), - LocationData("Templar's Charge", "Templar's Charge: Northeast Power Core", SC2LOTV_LOC_ID_OFFSET + 1902, LocationType.EXTRA, - lambda state: logic.templars_charge_requirement(state)), - LocationData("Templar's Charge", "Templar's Charge: Southeast Power Core", SC2LOTV_LOC_ID_OFFSET + 1903, LocationType.EXTRA, - lambda state: logic.templars_charge_requirement(state)), - LocationData("Templar's Charge", "Templar's Charge: West Hybrid Stasis Chamber", SC2LOTV_LOC_ID_OFFSET + 1904, LocationType.VANILLA, - lambda state: logic.templars_charge_requirement(state)), - LocationData("Templar's Charge", "Templar's Charge: Southeast Hybrid Stasis Chamber", SC2LOTV_LOC_ID_OFFSET + 1905, LocationType.VANILLA, - lambda state: logic.protoss_fleet(state)), - LocationData("Templar's Return", "Templar's Return: Victory", SC2LOTV_LOC_ID_OFFSET + 2000, LocationType.VICTORY, - lambda state: logic.templars_return_requirement(state)), - LocationData("Templar's Return", "Templar's Return: Citadel: First Gate", SC2LOTV_LOC_ID_OFFSET + 2001, LocationType.EXTRA), - LocationData("Templar's Return", "Templar's Return: Citadel: Second Gate", SC2LOTV_LOC_ID_OFFSET + 2002, LocationType.EXTRA), - LocationData("Templar's Return", "Templar's Return: Citadel: Power Structure", SC2LOTV_LOC_ID_OFFSET + 2003, LocationType.VANILLA), - LocationData("Templar's Return", "Templar's Return: Temple Grounds: Gather Army", SC2LOTV_LOC_ID_OFFSET + 2004, LocationType.VANILLA, - lambda state: logic.templars_return_requirement(state)), - LocationData("Templar's Return", "Templar's Return: Temple Grounds: Power Structure", SC2LOTV_LOC_ID_OFFSET + 2005, LocationType.VANILLA, - lambda state: logic.templars_return_requirement(state)), - LocationData("Templar's Return", "Templar's Return: Caverns: Purifier", SC2LOTV_LOC_ID_OFFSET + 2006, LocationType.EXTRA, - lambda state: logic.templars_return_requirement(state)), - LocationData("Templar's Return", "Templar's Return: Caverns: Dark Templar", SC2LOTV_LOC_ID_OFFSET + 2007, LocationType.EXTRA, - lambda state: logic.templars_return_requirement(state)), - LocationData("The Host", "The Host: Victory", SC2LOTV_LOC_ID_OFFSET + 2100, LocationType.VICTORY, - lambda state: logic.the_host_requirement(state)), - LocationData("The Host", "The Host: Southeast Void Shard", SC2LOTV_LOC_ID_OFFSET + 2101, LocationType.EXTRA, - lambda state: logic.the_host_requirement(state)), - LocationData("The Host", "The Host: South Void Shard", SC2LOTV_LOC_ID_OFFSET + 2102, LocationType.EXTRA, - lambda state: logic.the_host_requirement(state)), - LocationData("The Host", "The Host: Southwest Void Shard", SC2LOTV_LOC_ID_OFFSET + 2103, LocationType.EXTRA, - lambda state: logic.the_host_requirement(state)), - LocationData("The Host", "The Host: North Void Shard", SC2LOTV_LOC_ID_OFFSET + 2104, LocationType.EXTRA, - lambda state: logic.the_host_requirement(state)), - LocationData("The Host", "The Host: Northwest Void Shard", SC2LOTV_LOC_ID_OFFSET + 2105, LocationType.EXTRA, - lambda state: logic.the_host_requirement(state)), - LocationData("The Host", "The Host: Nerazim Warp in Zone", SC2LOTV_LOC_ID_OFFSET + 2106, LocationType.VANILLA, - lambda state: logic.the_host_requirement(state)), - LocationData("The Host", "The Host: Tal'darim Warp in Zone", SC2LOTV_LOC_ID_OFFSET + 2107, LocationType.VANILLA, - lambda state: logic.the_host_requirement(state)), - LocationData("The Host", "The Host: Purifier Warp in Zone", SC2LOTV_LOC_ID_OFFSET + 2108, LocationType.VANILLA, - lambda state: logic.the_host_requirement(state)), - LocationData("Salvation", "Salvation: Victory", SC2LOTV_LOC_ID_OFFSET + 2200, LocationType.VICTORY, - lambda state: logic.salvation_requirement(state)), - LocationData("Salvation", "Salvation: Fabrication Matrix", SC2LOTV_LOC_ID_OFFSET + 2201, LocationType.EXTRA, - lambda state: logic.salvation_requirement(state)), - LocationData("Salvation", "Salvation: Assault Cluster", SC2LOTV_LOC_ID_OFFSET + 2202, LocationType.EXTRA, - lambda state: logic.salvation_requirement(state)), - LocationData("Salvation", "Salvation: Hull Breach", SC2LOTV_LOC_ID_OFFSET + 2203, LocationType.EXTRA, - lambda state: logic.salvation_requirement(state)), - LocationData("Salvation", "Salvation: Core Critical", SC2LOTV_LOC_ID_OFFSET + 2204, LocationType.EXTRA, - lambda state: logic.salvation_requirement(state)), - - # Epilogue - LocationData("Into the Void", "Into the Void: Victory", SC2LOTV_LOC_ID_OFFSET + 2300, LocationType.VICTORY, - lambda state: logic.into_the_void_requirement(state)), - LocationData("Into the Void", "Into the Void: Corruption Source", SC2LOTV_LOC_ID_OFFSET + 2301, LocationType.EXTRA), - LocationData("Into the Void", "Into the Void: Southwest Forward Position", SC2LOTV_LOC_ID_OFFSET + 2302, LocationType.VANILLA, - lambda state: logic.into_the_void_requirement(state)), - LocationData("Into the Void", "Into the Void: Northwest Forward Position", SC2LOTV_LOC_ID_OFFSET + 2303, LocationType.VANILLA, - lambda state: logic.into_the_void_requirement(state)), - LocationData("Into the Void", "Into the Void: Southeast Forward Position", SC2LOTV_LOC_ID_OFFSET + 2304, LocationType.VANILLA, - lambda state: logic.into_the_void_requirement(state)), - LocationData("Into the Void", "Into the Void: Northeast Forward Position", SC2LOTV_LOC_ID_OFFSET + 2305, LocationType.VANILLA), - LocationData("The Essence of Eternity", "The Essence of Eternity: Victory", SC2LOTV_LOC_ID_OFFSET + 2400, LocationType.VICTORY, - lambda state: logic.essence_of_eternity_requirement(state)), - LocationData("The Essence of Eternity", "The Essence of Eternity: Void Trashers", SC2LOTV_LOC_ID_OFFSET + 2401, LocationType.EXTRA), - LocationData("Amon's Fall", "Amon's Fall: Victory", SC2LOTV_LOC_ID_OFFSET + 2500, LocationType.VICTORY, - lambda state: logic.amons_fall_requirement(state)), - - # Nova Covert Ops - LocationData("The Escape", "The Escape: Victory", SC2NCO_LOC_ID_OFFSET + 100, LocationType.VICTORY, - lambda state: logic.the_escape_requirement(state)), - LocationData("The Escape", "The Escape: Rifle", SC2NCO_LOC_ID_OFFSET + 101, LocationType.VANILLA, - lambda state: logic.the_escape_first_stage_requirement(state)), - LocationData("The Escape", "The Escape: Grenades", SC2NCO_LOC_ID_OFFSET + 102, LocationType.VANILLA, - lambda state: logic.the_escape_first_stage_requirement(state)), - LocationData("The Escape", "The Escape: Agent Delta", SC2NCO_LOC_ID_OFFSET + 103, LocationType.VANILLA, - lambda state: logic.the_escape_requirement(state)), - LocationData("The Escape", "The Escape: Agent Pierce", SC2NCO_LOC_ID_OFFSET + 104, LocationType.VANILLA, - lambda state: logic.the_escape_requirement(state)), - LocationData("The Escape", "The Escape: Agent Stone", SC2NCO_LOC_ID_OFFSET + 105, LocationType.VANILLA, - lambda state: logic.the_escape_requirement(state)), - LocationData("Sudden Strike", "Sudden Strike: Victory", SC2NCO_LOC_ID_OFFSET + 200, LocationType.VICTORY, - lambda state: logic.sudden_strike_requirement(state)), - LocationData("Sudden Strike", "Sudden Strike: Research Center", SC2NCO_LOC_ID_OFFSET + 201, LocationType.VANILLA, - lambda state: logic.sudden_strike_can_reach_objectives(state)), - LocationData("Sudden Strike", "Sudden Strike: Weaponry Labs", SC2NCO_LOC_ID_OFFSET + 202, LocationType.VANILLA, - lambda state: logic.sudden_strike_can_reach_objectives(state)), - LocationData("Sudden Strike", "Sudden Strike: Brutalisk", SC2NCO_LOC_ID_OFFSET + 203, LocationType.EXTRA, - lambda state: logic.sudden_strike_requirement(state)), - LocationData("Enemy Intelligence", "Enemy Intelligence: Victory", SC2NCO_LOC_ID_OFFSET + 300, LocationType.VICTORY, - lambda state: logic.enemy_intelligence_third_stage_requirement(state)), - LocationData("Enemy Intelligence", "Enemy Intelligence: West Garrison", SC2NCO_LOC_ID_OFFSET + 301, LocationType.EXTRA, - lambda state: logic.enemy_intelligence_first_stage_requirement(state)), - LocationData("Enemy Intelligence", "Enemy Intelligence: Close Garrison", SC2NCO_LOC_ID_OFFSET + 302, LocationType.EXTRA, - lambda state: logic.enemy_intelligence_first_stage_requirement(state)), - LocationData("Enemy Intelligence", "Enemy Intelligence: Northeast Garrison", SC2NCO_LOC_ID_OFFSET + 303, LocationType.EXTRA, - lambda state: logic.enemy_intelligence_first_stage_requirement(state)), - LocationData("Enemy Intelligence", "Enemy Intelligence: Southeast Garrison", SC2NCO_LOC_ID_OFFSET + 304, LocationType.EXTRA, - lambda state: logic.enemy_intelligence_first_stage_requirement(state) - and logic.enemy_intelligence_cliff_garrison(state)), - LocationData("Enemy Intelligence", "Enemy Intelligence: South Garrison", SC2NCO_LOC_ID_OFFSET + 305, LocationType.EXTRA, - lambda state: logic.enemy_intelligence_first_stage_requirement(state)), - LocationData("Enemy Intelligence", "Enemy Intelligence: All Garrisons", SC2NCO_LOC_ID_OFFSET + 306, LocationType.VANILLA, - lambda state: logic.enemy_intelligence_first_stage_requirement(state) - and logic.enemy_intelligence_cliff_garrison(state)), - LocationData("Enemy Intelligence", "Enemy Intelligence: Forces Rescued", SC2NCO_LOC_ID_OFFSET + 307, LocationType.VANILLA, - lambda state: logic.enemy_intelligence_first_stage_requirement(state)), - LocationData("Enemy Intelligence", "Enemy Intelligence: Communications Hub", SC2NCO_LOC_ID_OFFSET + 308, LocationType.VANILLA, - lambda state: logic.enemy_intelligence_second_stage_requirement(state)), - LocationData("Trouble In Paradise", "Trouble In Paradise: Victory", SC2NCO_LOC_ID_OFFSET + 400, LocationType.VICTORY, - lambda state: logic.trouble_in_paradise_requirement(state)), - LocationData("Trouble In Paradise", "Trouble In Paradise: North Base: West Hatchery", SC2NCO_LOC_ID_OFFSET + 401, LocationType.VANILLA, - lambda state: logic.trouble_in_paradise_requirement(state)), - LocationData("Trouble In Paradise", "Trouble In Paradise: North Base: North Hatchery", SC2NCO_LOC_ID_OFFSET + 402, LocationType.VANILLA, - lambda state: logic.trouble_in_paradise_requirement(state)), - LocationData("Trouble In Paradise", "Trouble In Paradise: North Base: East Hatchery", SC2NCO_LOC_ID_OFFSET + 403, LocationType.VANILLA), - LocationData("Trouble In Paradise", "Trouble In Paradise: South Base: Northwest Hatchery", SC2NCO_LOC_ID_OFFSET + 404, LocationType.VANILLA, - lambda state: logic.trouble_in_paradise_requirement(state)), - LocationData("Trouble In Paradise", "Trouble In Paradise: South Base: Southwest Hatchery", SC2NCO_LOC_ID_OFFSET + 405, LocationType.VANILLA, - lambda state: logic.trouble_in_paradise_requirement(state)), - LocationData("Trouble In Paradise", "Trouble In Paradise: South Base: East Hatchery", SC2NCO_LOC_ID_OFFSET + 406, LocationType.VANILLA), - LocationData("Trouble In Paradise", "Trouble In Paradise: North Shield Projector", SC2NCO_LOC_ID_OFFSET + 407, LocationType.EXTRA, - lambda state: logic.trouble_in_paradise_requirement(state)), - LocationData("Trouble In Paradise", "Trouble In Paradise: East Shield Projector", SC2NCO_LOC_ID_OFFSET + 408, LocationType.EXTRA, - lambda state: logic.trouble_in_paradise_requirement(state)), - LocationData("Trouble In Paradise", "Trouble In Paradise: South Shield Projector", SC2NCO_LOC_ID_OFFSET + 409, LocationType.EXTRA, - lambda state: logic.trouble_in_paradise_requirement(state)), - LocationData("Trouble In Paradise", "Trouble In Paradise: West Shield Projector", SC2NCO_LOC_ID_OFFSET + 410, LocationType.EXTRA, - lambda state: logic.trouble_in_paradise_requirement(state)), - LocationData("Trouble In Paradise", "Trouble In Paradise: Fleet Beacon", SC2NCO_LOC_ID_OFFSET + 411, LocationType.VANILLA, - lambda state: logic.trouble_in_paradise_requirement(state)), - LocationData("Night Terrors", "Night Terrors: Victory", SC2NCO_LOC_ID_OFFSET + 500, LocationType.VICTORY, - lambda state: logic.night_terrors_requirement(state)), - LocationData("Night Terrors", "Night Terrors: 1 Terrazine Node Collected", SC2NCO_LOC_ID_OFFSET + 501, LocationType.EXTRA, - lambda state: logic.night_terrors_requirement(state)), - LocationData("Night Terrors", "Night Terrors: 2 Terrazine Nodes Collected", SC2NCO_LOC_ID_OFFSET + 502, LocationType.EXTRA, - lambda state: logic.night_terrors_requirement(state)), - LocationData("Night Terrors", "Night Terrors: 3 Terrazine Nodes Collected", SC2NCO_LOC_ID_OFFSET + 503, LocationType.EXTRA, - lambda state: logic.night_terrors_requirement(state)), - LocationData("Night Terrors", "Night Terrors: 4 Terrazine Nodes Collected", SC2NCO_LOC_ID_OFFSET + 504, LocationType.EXTRA, - lambda state: logic.night_terrors_requirement(state)), - LocationData("Night Terrors", "Night Terrors: 5 Terrazine Nodes Collected", SC2NCO_LOC_ID_OFFSET + 505, LocationType.EXTRA, - lambda state: logic.night_terrors_requirement(state)), - LocationData("Night Terrors", "Night Terrors: HERC Outpost", SC2NCO_LOC_ID_OFFSET + 506, LocationType.VANILLA, - lambda state: logic.night_terrors_requirement(state)), - LocationData("Night Terrors", "Night Terrors: Umojan Mine", SC2NCO_LOC_ID_OFFSET + 507, LocationType.EXTRA, - lambda state: logic.night_terrors_requirement(state)), - LocationData("Night Terrors", "Night Terrors: Blightbringer", SC2NCO_LOC_ID_OFFSET + 508, LocationType.VANILLA, - lambda state: logic.night_terrors_requirement(state) - and logic.nova_ranged_weapon(state) - and state.has_any( - {ItemNames.NOVA_HELLFIRE_SHOTGUN, ItemNames.NOVA_PULSE_GRENADES, ItemNames.NOVA_STIM_INFUSION, - ItemNames.NOVA_HOLO_DECOY}, player)), - LocationData("Night Terrors", "Night Terrors: Science Facility", SC2NCO_LOC_ID_OFFSET + 509, LocationType.EXTRA, - lambda state: logic.night_terrors_requirement(state)), - LocationData("Night Terrors", "Night Terrors: Eradicators", SC2NCO_LOC_ID_OFFSET + 510, LocationType.VANILLA, - lambda state: logic.night_terrors_requirement(state) - and logic.nova_any_weapon(state)), - LocationData("Flashpoint", "Flashpoint: Victory", SC2NCO_LOC_ID_OFFSET + 600, LocationType.VICTORY, - lambda state: logic.flashpoint_far_requirement(state)), - LocationData("Flashpoint", "Flashpoint: Close North Evidence Coordinates", SC2NCO_LOC_ID_OFFSET + 601, LocationType.EXTRA, - lambda state: state.has_any( - {ItemNames.LIBERATOR_RAID_ARTILLERY, ItemNames.RAVEN_HUNTER_SEEKER_WEAPON}, player) - or logic.terran_common_unit(state)), - LocationData("Flashpoint", "Flashpoint: Close East Evidence Coordinates", SC2NCO_LOC_ID_OFFSET + 602, LocationType.EXTRA, - lambda state: state.has_any( - {ItemNames.LIBERATOR_RAID_ARTILLERY, ItemNames.RAVEN_HUNTER_SEEKER_WEAPON}, player) - or logic.terran_common_unit(state)), - LocationData("Flashpoint", "Flashpoint: Far North Evidence Coordinates", SC2NCO_LOC_ID_OFFSET + 603, LocationType.EXTRA, - lambda state: logic.flashpoint_far_requirement(state)), - LocationData("Flashpoint", "Flashpoint: Far East Evidence Coordinates", SC2NCO_LOC_ID_OFFSET + 604, LocationType.EXTRA, - lambda state: logic.flashpoint_far_requirement(state)), - LocationData("Flashpoint", "Flashpoint: Experimental Weapon", SC2NCO_LOC_ID_OFFSET + 605, LocationType.VANILLA, - lambda state: logic.flashpoint_far_requirement(state)), - LocationData("Flashpoint", "Flashpoint: Northwest Subway Entrance", SC2NCO_LOC_ID_OFFSET + 606, LocationType.VANILLA, - lambda state: state.has_any( - {ItemNames.LIBERATOR_RAID_ARTILLERY, ItemNames.RAVEN_HUNTER_SEEKER_WEAPON}, player) - and logic.terran_common_unit(state) - or logic.flashpoint_far_requirement(state)), - LocationData("Flashpoint", "Flashpoint: Southeast Subway Entrance", SC2NCO_LOC_ID_OFFSET + 607, LocationType.VANILLA, - lambda state: state.has_any( - {ItemNames.LIBERATOR_RAID_ARTILLERY, ItemNames.RAVEN_HUNTER_SEEKER_WEAPON}, player) - and logic.terran_common_unit(state) - or logic.flashpoint_far_requirement(state)), - LocationData("Flashpoint", "Flashpoint: Northeast Subway Entrance", SC2NCO_LOC_ID_OFFSET + 608, LocationType.VANILLA, - lambda state: logic.flashpoint_far_requirement(state)), - LocationData("Flashpoint", "Flashpoint: Expansion Hatchery", SC2NCO_LOC_ID_OFFSET + 609, LocationType.EXTRA, - lambda state: state.has(ItemNames.LIBERATOR_RAID_ARTILLERY, player) and logic.terran_common_unit(state) - or logic.flashpoint_far_requirement(state)), - LocationData("Flashpoint", "Flashpoint: Baneling Spawns", SC2NCO_LOC_ID_OFFSET + 610, LocationType.EXTRA, - lambda state: logic.flashpoint_far_requirement(state)), - LocationData("Flashpoint", "Flashpoint: Mutalisk Spawns", SC2NCO_LOC_ID_OFFSET + 611, LocationType.EXTRA, - lambda state: logic.flashpoint_far_requirement(state)), - LocationData("Flashpoint", "Flashpoint: Nydus Worm Spawns", SC2NCO_LOC_ID_OFFSET + 612, LocationType.EXTRA, - lambda state: logic.flashpoint_far_requirement(state)), - LocationData("Flashpoint", "Flashpoint: Lurker Spawns", SC2NCO_LOC_ID_OFFSET + 613, LocationType.EXTRA, - lambda state: logic.flashpoint_far_requirement(state)), - LocationData("Flashpoint", "Flashpoint: Brood Lord Spawns", SC2NCO_LOC_ID_OFFSET + 614, LocationType.EXTRA, - lambda state: logic.flashpoint_far_requirement(state)), - LocationData("Flashpoint", "Flashpoint: Ultralisk Spawns", SC2NCO_LOC_ID_OFFSET + 615, LocationType.EXTRA, - lambda state: logic.flashpoint_far_requirement(state)), - LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Victory", SC2NCO_LOC_ID_OFFSET + 700, LocationType.VICTORY, - lambda state: logic.enemy_shadow_victory(state)), - LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Sewers: Domination Visor", SC2NCO_LOC_ID_OFFSET + 701, LocationType.VANILLA, - lambda state: logic.enemy_shadow_domination(state)), - LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Sewers: Resupply Crate", SC2NCO_LOC_ID_OFFSET + 702, LocationType.EXTRA, - lambda state: logic.enemy_shadow_first_stage(state)), - LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Sewers: Facility Access", SC2NCO_LOC_ID_OFFSET + 703, LocationType.VANILLA, - lambda state: logic.enemy_shadow_first_stage(state)), - LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Facility: Northwest Door Lock", SC2NCO_LOC_ID_OFFSET + 704, LocationType.VANILLA, - lambda state: logic.enemy_shadow_door_controls(state)), - LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Facility: Southeast Door Lock", SC2NCO_LOC_ID_OFFSET + 705, LocationType.VANILLA, - lambda state: logic.enemy_shadow_door_controls(state)), - LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Facility: Blazefire Gunblade", SC2NCO_LOC_ID_OFFSET + 706, LocationType.VANILLA, - lambda state: logic.enemy_shadow_second_stage(state) - and (story_tech_granted - or state.has(ItemNames.NOVA_BLINK, player) - or (adv_tactics and state.has_all({ItemNames.NOVA_DOMINATION, ItemNames.NOVA_HOLO_DECOY, ItemNames.NOVA_JUMP_SUIT_MODULE}, player)) - ) - ), - LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Facility: Blink Suit", SC2NCO_LOC_ID_OFFSET + 707, LocationType.VANILLA, - lambda state: logic.enemy_shadow_second_stage(state)), - LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Facility: Advanced Weaponry", SC2NCO_LOC_ID_OFFSET + 708, LocationType.VANILLA, - lambda state: logic.enemy_shadow_second_stage(state)), - LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Facility: Entrance Resupply Crate", SC2NCO_LOC_ID_OFFSET + 709, LocationType.EXTRA, - lambda state: logic.enemy_shadow_first_stage(state)), - LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Facility: West Resupply Crate", SC2NCO_LOC_ID_OFFSET + 710, LocationType.EXTRA, - lambda state: logic.enemy_shadow_second_stage(state)), - LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Facility: North Resupply Crate", SC2NCO_LOC_ID_OFFSET + 711, LocationType.EXTRA, - lambda state: logic.enemy_shadow_second_stage(state)), - LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Facility: East Resupply Crate", SC2NCO_LOC_ID_OFFSET + 712, LocationType.EXTRA, - lambda state: logic.enemy_shadow_second_stage(state)), - LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Facility: South Resupply Crate", SC2NCO_LOC_ID_OFFSET + 713, LocationType.EXTRA, - lambda state: logic.enemy_shadow_second_stage(state)), - LocationData("Dark Skies", "Dark Skies: Victory", SC2NCO_LOC_ID_OFFSET + 800, LocationType.VICTORY, - lambda state: logic.dark_skies_requirement(state)), - LocationData("Dark Skies", "Dark Skies: First Squadron of Dominion Fleet", SC2NCO_LOC_ID_OFFSET + 801, LocationType.EXTRA, - lambda state: logic.dark_skies_requirement(state)), - LocationData("Dark Skies", "Dark Skies: Remainder of Dominion Fleet", SC2NCO_LOC_ID_OFFSET + 802, LocationType.EXTRA, - lambda state: logic.dark_skies_requirement(state)), - LocationData("Dark Skies", "Dark Skies: Ji'nara", SC2NCO_LOC_ID_OFFSET + 803, LocationType.EXTRA, - lambda state: logic.dark_skies_requirement(state)), - LocationData("Dark Skies", "Dark Skies: Science Facility", SC2NCO_LOC_ID_OFFSET + 804, LocationType.VANILLA, - lambda state: logic.dark_skies_requirement(state)), - LocationData("End Game", "End Game: Victory", SC2NCO_LOC_ID_OFFSET + 900, LocationType.VICTORY, - lambda state: logic.end_game_requirement(state) and logic.nova_any_weapon(state)), - LocationData("End Game", "End Game: Xanthos", SC2NCO_LOC_ID_OFFSET + 901, LocationType.VANILLA, - lambda state: logic.end_game_requirement(state)), - ] - - beat_events = [] - # Filtering out excluded locations - if world is not None: - excluded_location_types = get_location_types(world, LocationInclusion.option_disabled) - plando_locations = get_plando_locations(world) - exclude_locations = get_option_value(world, "exclude_locations") - location_table = [location for location in location_table - if (location.type is LocationType.VICTORY or location.name not in exclude_locations) - and location.type not in excluded_location_types - or location.name in plando_locations] - for i, location_data in enumerate(location_table): - # Removing all item-based logic on No Logic - if logic_level == RequiredTactics.option_no_logic: - 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) - -lookup_location_id_to_type = {loc.code: loc.type for loc in get_locations(None) if loc.code is not None} \ No newline at end of file diff --git a/worlds/sc2/MissionTables.py b/worlds/sc2/MissionTables.py deleted file mode 100644 index 08e1f133..00000000 --- a/worlds/sc2/MissionTables.py +++ /dev/null @@ -1,739 +0,0 @@ -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 - - def __lt__(self, other: "SC2Campaign"): - return self.id < other.id - - 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: SC2CampaignGoal(SC2Mission.END_GAME, "End Game: Victory"), -} - -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.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 and mission in campaign_alt_goals: - return campaign_alt_goals.get(mission) - - return mission.mission_name + ": Victory" - - -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] diff --git a/worlds/sc2/Options.py b/worlds/sc2/Options.py deleted file mode 100644 index 88febb70..00000000 --- a/worlds/sc2/Options.py +++ /dev/null @@ -1,908 +0,0 @@ -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, -] \ No newline at end of file diff --git a/worlds/sc2/PoolFilter.py b/worlds/sc2/PoolFilter.py deleted file mode 100644 index f5f6faa9..00000000 --- a/worlds/sc2/PoolFilter.py +++ /dev/null @@ -1,661 +0,0 @@ -from typing import Callable, Dict, List, Set, Union, Tuple, Optional -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: List[SC2Campaign] = [campaign for campaign, goal_priority in goal_priorities.items() if goal_priority == goal_level] - candidate_campaigns.sort(key=lambda it: it.id) - 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_mission: Optional[SC2Mission] = None - if mission_order_type in campaign_depending_orders: - # Prefer long campaigns over shorter ones and harder missions over easier ones - goal_priorities = {campaign: get_campaign_goal_priority(campaign, excluded_missions) for campaign in enabled_campaigns} - goal_level = max(goal_priorities.values()) - candidate_campaigns: List[SC2Campaign] = [campaign for campaign, goal_priority in goal_priorities.items() if goal_priority == goal_level] - candidate_campaigns.sort(key=lambda it: it.id) - - 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 - else: - # Find one of the missions with the hardest difficulty - available_missions: List[SC2Mission] = \ - [mission for mission in SC2Mission - if (mission not in excluded_missions and mission.campaign in enabled_campaigns)] - available_missions.sort(key=lambda it: it.id) - # Loop over pools, from hardest to easiest - for mission_pool in range(MissionPools.VERY_HARD, MissionPools.STARTER - 1, -1): - pool_missions: List[SC2Mission] = [mission for mission in available_missions if mission.pool == mission_pool] - if pool_missions: - goal_mission = world.random.choice(pool_missions) - break - if goal_mission is None: - raise Exception("There are no valid goal missions. Please exclude fewer missions.") - - # 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[Item] = list(self.item_pool) - locked_items: List[Item] = 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: List[str] = [] # 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) - - pool_items: List[str] = [item.name for item in (inventory + locked_items + self.existing_items)] - unused_items = [ - unused_item for unused_item in unused_items - if item_list[unused_item].parent_item is None - or item_list[unused_item].parent_item in pool_items - ] - - # 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)")] - unused_items = [item_name for item_name in unused_items 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)] - unused_items = [ - item_name for item_name in unused_items - 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)] - unused_items = [item_name for item_name in unused_items 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)] - unused_items = [item_name for item_name in unused_items 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] - unused_items = [item_name for item_name in unused_items if item_name != ItemNames.ZERGLING_BANELING_ASPECT] - unused_items = [item_name for item_name in unused_items 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] - unused_items = [item_name for item_name in unused_items if item_name != ItemNames.ZERGLING_BANELING_ASPECT] - unused_items = [item_name for item_name in unused_items 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)] - unused_items = [item_name for item_name in unused_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] - unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Mutalisk/Corruptor)")] - unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.MUTALISK_CORRUPTOR_GUARDIAN_ASPECT] - unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.MUTALISK_CORRUPTOR_DEVOURER_ASPECT] - unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT] - unused_items = [item_name for item_name in unused_items 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] - unused_items = [item_name for item_name in unused_items if item_name != ItemNames.ROACH_RAVAGER_ASPECT] - unused_items = [item_name for item_name in unused_items 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] - unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Hydralisk)")] - unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.HYDRALISK_LURKER_ASPECT] - unused_items = [item_name for item_name in unused_items 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)")] - unused_items = [item_name for item_name in unused_items 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)")] - unused_items = [item_name for item_name in unused_items 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)")] - unused_items = [item_name for item_name in unused_items 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)")] - unused_items = [item_name for item_name in unused_items 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)")] - unused_items = [item_name for item_name in unused_items 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)")] - unused_items = [item_name for item_name in unused_items 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)")] - unused_items = [item_name for item_name in unused_items 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] - unused_items = [item_name for item_name in unused_items 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)")] - unused_items = [item_name for item_name in unused_items 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)")] - unused_items = [item_name for item_name in unused_items 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)")] - unused_items = [item_name for item_name in unused_items 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)")] - unused_items = [item_name for item_name in unused_items 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] - unused_items = [item_name for item_name in unused_items 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] - unused_items = [item_name for item_name in unused_items 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") in kerrigan_unit_available \ - 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 \ - 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()]) diff --git a/worlds/sc2/Regions.py b/worlds/sc2/Regions.py deleted file mode 100644 index 273bc4a5..00000000 --- a/worlds/sc2/Regions.py +++ /dev/null @@ -1,691 +0,0 @@ -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 sorted(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, "Ghosts in the Fog", "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 Trouble In Paradise", 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 - diff --git a/worlds/sc2/Rules.py b/worlds/sc2/Rules.py deleted file mode 100644 index 8b9097ea..00000000 --- a/worlds/sc2/Rules.py +++ /dev/null @@ -1,952 +0,0 @@ -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") diff --git a/worlds/sc2/Starcraft2.kv b/worlds/sc2/Starcraft2.kv deleted file mode 100644 index 6b112c2f..00000000 --- a/worlds/sc2/Starcraft2.kv +++ /dev/null @@ -1,28 +0,0 @@ - - scroll_type: ["content", "bars"] - bar_width: dp(12) - effect_cls: "ScrollEffect" - - - cols: 1 - size_hint_y: None - height: self.minimum_height + 15 - padding: [5,0,dp(12),0] - -: - cols: 1 - -: - rows: 1 - -: - cols: 1 - spacing: [0,5] - -: - text_size: self.size - markup: True - halign: 'center' - valign: 'middle' - padding: [5,0,5,0] - outline_width: 1 diff --git a/worlds/sc2/__init__.py b/worlds/sc2/__init__.py index f11059a5..0201ebf6 100644 --- a/worlds/sc2/__init__.py +++ b/worlds/sc2/__init__.py @@ -1,25 +1,49 @@ -import typing from dataclasses import fields +import logging -from typing import List, Set, Iterable, Sequence, Dict, Callable, Union +from typing import * from math import floor, ceil -from BaseClasses import Item, MultiWorld, Location, Tutorial, ItemClassification +from BaseClasses import Item, MultiWorld, Location, Tutorial, ItemClassification, CollectionState +from Options import Accessibility, OptionError 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 +from . import location_groups +from .item.item_groups import unreleased_items, war_council_upgrades +from .item.item_tables import ( + get_full_item_list, + not_balanced_starting_units, WEAPON_ARMOR_UPGRADE_MAX_LEVEL, +) +from .item import FilterItem, ItemFilterFlags, StarcraftItem, item_groups, item_names, item_tables, item_parents, \ + ZergItemType, ProtossItemType, ItemData +from .locations import ( + get_locations, DEFAULT_LOCATION_LIST, get_location_types, get_location_flags, + get_plando_locations, LocationType, lookup_location_id_to_type +) +from .mission_order.layout_types import Gauntlet +from .options import ( + get_option_value, LocationInclusion, KerriganLevelItemDistribution, + KerriganPresence, KerriganPrimalStatus, kerrigan_unit_available, StarterUnit, SpearOfAdunPresence, + get_enabled_campaigns, SpearOfAdunPassiveAbilityPresence, Starcraft2Options, + GrantStoryTech, GenericUpgradeResearch, RequiredTactics, + upgrade_included_names, EnableVoidTrade, FillerItemsDistribution, MissionOrderScouting, option_groups, + NovaGhostOfAChanceVariant, MissionOrder, VanillaItemsOnly, ExcludeOverpoweredItems, + is_mission_in_soa_presence, +) +from .rules import get_basic_units, SC2Logic +from . import settings +from .pool_filter import filter_items +from .mission_tables import SC2Campaign, SC2Mission, SC2Race, MissionFlag +from .regions import create_mission_order +from .mission_order import SC2MissionOrder +from worlds.LauncherComponents import components, Component, launch as launch_component +logger = logging.getLogger("Starcraft 2") +VICTORY_MODULO = 100 + +def launch_client(*args: str): + from .client import launch + launch_component(launch, name="Starcraft 2 Client", args=args) + +components.append(Component('Starcraft 2 Client', func=launch_client, game_name='Starcraft 2', supports_uri=True)) class Starcraft2WebWorld(WebWorld): setup_en = Tutorial( @@ -40,8 +64,18 @@ class Starcraft2WebWorld(WebWorld): ["Neocerber"] ) - tutorials = [setup_en, setup_fr] + custom_mission_orders_en = Tutorial( + "Custom Mission Order Usage Guide", + "Documentation for the custom_mission_order YAML option", + "English", + "custom_mission_orders_en.md", + "custom_mission_orders/en", + ["Salzkorn"] + ) + + tutorials = [setup_en, setup_fr, custom_mission_orders_en] game_info_languages = ["en", "fr"] + option_groups = option_groups class SC2World(World): @@ -52,90 +86,279 @@ class SC2World(World): game = "Starcraft 2" web = Starcraft2WebWorld() + settings: ClassVar[settings.Starcraft2Settings] 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)} + location_name_to_id = {location.name: location.code for location in DEFAULT_LOCATION_LIST} 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 + item_name_groups = item_groups.item_name_groups # type: ignore + location_name_groups = location_groups.get_location_groups() + locked_locations: List[str] + """Locations locked to contain specific items, such as victory events or forced resources""" + location_cache: List[Location] + final_missions: List[int] + required_client_version = 0, 6, 4 + custom_mission_order: SC2MissionOrder + logic: Optional['SC2Logic'] + filler_items_distribution: Dict[str, int] def __init__(self, multiworld: MultiWorld, player: int): super(SC2World, self).__init__(multiworld, player) self.location_cache = [] self.locked_locations = [] + self.filler_items_distribution = FillerItemsDistribution.default + self.logic = None - def create_item(self, name: str) -> Item: + def create_item(self, name: str) -> StarcraftItem: 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.logic = SC2Logic(self) + self.custom_mission_order = create_mission_order( self, get_locations(self), self.location_cache ) + self.logic.nova_used = ( + MissionFlag.Nova in self.custom_mission_order.get_used_flags() + or ( + MissionFlag.WoLNova in self.custom_mission_order.get_used_flags() + and self.options.nova_ghost_of_a_chance_variant == NovaGhostOfAChanceVariant.option_nco + ) + ) + + def create_items(self) -> None: + # Starcraft 2-specific item setup: + # * Filter item pool based on player options + # * Plando starter units + # * Start-inventory units if necessary for logic + # * Plando filler items based on location exclusions + # * If the item pool is less than the location count, add some filler items - def create_items(self): setup_events(self.player, self.locked_locations, self.location_cache) + set_up_filler_items_distribution(self) - excluded_items = get_excluded_items(self) + item_list: List[FilterItem] = create_and_flag_explicit_item_locks_and_excludes(self) + flag_excludes_by_faction_presence(self, item_list) + flag_mission_based_item_excludes(self, item_list) + flag_allowed_orphan_items(self, item_list) + flag_start_inventory(self, item_list) + flag_unused_upgrade_types(self, item_list) + flag_unreleased_items(item_list) + flag_user_excluded_item_sets(self, item_list) + flag_war_council_items(self, item_list) + flag_and_add_resource_locations(self, item_list) + flag_mission_order_required_items(self, item_list) + pruned_items: List[StarcraftItem] = prune_item_pool(self, item_list) - starter_items = assign_starter_items(self, excluded_items, self.locked_locations, self.location_cache) + start_inventory = [item for item in pruned_items if ItemFilterFlags.StartInventory in item.filter_flags] + pool = [item for item in pruned_items if ItemFilterFlags.StartInventory not in item.filter_flags] - fill_resource_locations(self, self.locked_locations, self.location_cache) + # Tell the logic which unit classes are used for required W/A upgrades + used_item_names: Set[str] = {item.name for item in pruned_items} + used_item_names = used_item_names.union(item.name for item in self.multiworld.itempool if item.player == self.player) + assert self.logic is not None + if used_item_names.isdisjoint(item_groups.barracks_wa_group): + self.logic.has_barracks_unit = False + if used_item_names.isdisjoint(item_groups.factory_wa_group): + self.logic.has_factory_unit = False + if used_item_names.isdisjoint(item_groups.starport_wa_group): + self.logic.has_starport_unit = False + if used_item_names.isdisjoint(item_groups.zerg_melee_wa): + self.logic.has_zerg_melee_unit = False + if used_item_names.isdisjoint(item_groups.zerg_ranged_wa): + self.logic.has_zerg_ranged_unit = False + if used_item_names.isdisjoint(item_groups.zerg_air_units): + self.logic.has_zerg_air_unit = False + if used_item_names.isdisjoint(item_groups.protoss_ground_wa): + self.logic.has_protoss_ground_unit = False + if used_item_names.isdisjoint(item_groups.protoss_air_wa): + self.logic.has_protoss_air_unit = False - pool = get_item_pool(self, self.mission_req_table, starter_items, excluded_items, self.location_cache) + pad_item_pool_with_filler(self, len(self.location_cache) - len(self.locked_locations) - len(pool), pool) - fill_item_pool_with_dummy_items(self, self.locked_locations, self.location_cache, pool) + push_precollected_items_to_multiworld(self, start_inventory) self.multiworld.itempool += pool - def set_rules(self): - self.multiworld.completion_condition[self.player] = lambda state: state.has(self.victory_item, self.player) + def set_rules(self) -> None: + if self.options.required_tactics == RequiredTactics.option_no_logic: + # Forcing completed goal and minimal accessibility on no logic + self.options.accessibility.value = Accessibility.option_minimal + required_items = self.custom_mission_order.get_items_to_lock() + self.multiworld.completion_condition[self.player] = lambda state, required_items=required_items: all( # type: ignore + state.has(item, self.player, amount) for (item, amount) in required_items.items() + ) + else: + self.multiworld.completion_condition[self.player] = self.custom_mission_order.get_completion_condition(self.player) def get_filler_item_name(self) -> str: - return self.random.choice(filler_items) + # Assume `self.filler_items_distribution` is validated and has at least one non-zero entry + return self.random.choices(tuple(self.filler_items_distribution), weights=self.filler_items_distribution.values())[0] # type: ignore - def fill_slot_data(self): - slot_data = {} + def fill_slot_data(self) -> Mapping[str, Any]: + slot_data: Dict[str, Any] = {} 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 + slot_data["use_nova_nco_fallback"] = ( + enabled_campaigns == {SC2Campaign.NCO} + and self.options.mission_order == MissionOrder.option_vanilla + ) + if (self.options.nova_ghost_of_a_chance_variant == NovaGhostOfAChanceVariant.option_nco + or ( + self.options.nova_ghost_of_a_chance_variant == NovaGhostOfAChanceVariant.option_auto + and MissionFlag.Nova in self.custom_mission_order.get_used_flags().keys() + ) + ): + slot_data["use_nova_wol_fallback"] = False + else: + slot_data["use_nova_wol_fallback"] = True + slot_data["final_mission_ids"] = self.custom_mission_order.get_final_mission_ids() + slot_data["custom_mission_order"] = self.custom_mission_order.get_slot_data() + slot_data["version"] = 4 if SC2Campaign.HOTS not in enabled_campaigns: slot_data["kerrigan_presence"] = KerriganPresence.option_not_present + + if self.options.mission_order_scouting != MissionOrderScouting.option_none: + mission_item_classification: Dict[str, int] = {} + for location in self.multiworld.get_locations(self.player): + # Event do not hold items + if not location.is_event: + assert location.address is not None + assert location.item is not None + if lookup_location_id_to_type[location.address] == LocationType.VICTORY_CACHE: + # Ensure that if there are multiple items given for finishing a mission and that at least + # one is progressive, the flag kept is progressive. + location_name = self.location_id_to_name[(location.address // VICTORY_MODULO) * VICTORY_MODULO] + old_classification = mission_item_classification.get(location_name, 0) + mission_item_classification[location_name] = old_classification | location.item.classification.as_flag() + else: + mission_item_classification[location.name] = location.item.classification.as_flag() + slot_data["mission_item_classification"] = mission_item_classification + + # Disable trade if there is no trade partner + traders = [ + world + for world in self.multiworld.worlds.values() + if world.game == self.game and world.options.enable_void_trade == EnableVoidTrade.option_true # type: ignore + ] + if len(traders) < 2: + slot_data["enable_void_trade"] = EnableVoidTrade.option_false + return slot_data + def pre_fill(self) -> None: + assert self.logic is not None + self.logic.total_mission_count = self.custom_mission_order.get_mission_count() + if ( + self.options.generic_upgrade_missions > 0 + and self.options.required_tactics != RequiredTactics.option_no_logic + ): + # Attempt to resolve a situation when the option is too high for the mission order rolled + weapon_armor_item_names = [ + item_names.PROGRESSIVE_TERRAN_WEAPON_ARMOR_UPGRADE, + item_names.PROGRESSIVE_ZERG_WEAPON_ARMOR_UPGRADE, + item_names.PROGRESSIVE_PROTOSS_WEAPON_ARMOR_UPGRADE + ] + def state_with_kerrigan_levels() -> CollectionState: + state: CollectionState = self.multiworld.get_all_state(False) + # Ignore dead ends caused by Kerrigan -> solve those in the next stage + state.collect(self.create_item(item_names.KERRIGAN_LEVELS_70)) + state.update_reachable_regions(self.player) + return state -def setup_events(player: int, locked_locations: typing.List[str], location_cache: typing.List[Location]): + self._fill_needed_items(state_with_kerrigan_levels, weapon_armor_item_names, WEAPON_ARMOR_UPGRADE_MAX_LEVEL) + if ( + self.options.kerrigan_levels_per_mission_completed > 0 + and self.options.required_tactics != RequiredTactics.option_no_logic + ): + # Attempt to solve being locked by Kerrigan level requirements + self._fill_needed_items(lambda: self.multiworld.get_all_state(False), [item_names.KERRIGAN_LEVELS_1], 70) + + + def _fill_needed_items(self, all_state_getter: Callable[[],CollectionState], items_to_use: List[str], max_attempts: int) -> None: + """ + Helper for pre-fill, seeks if the world is actually solvable and inserts items to start inventory if necessary. + :param all_state_getter: + :param items_to_use: + :param max_attempts: + :return: + """ + for attempt in range(0, max_attempts): + all_state: CollectionState = all_state_getter() + location_failed = False + for location in self.location_cache: + if not (all_state.can_reach_location(location.name, self.player) + and all_state.can_reach_region(location.parent_region.name, self.player)): + location_failed = True + break + if location_failed: + for item_name in items_to_use: + item = self.multiworld.create_item(item_name, self.player) + self.multiworld.push_precollected(item) + else: + return + + + def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None: + """ + Generate information to hint where each mission is actually located in the mission order + :param hint_data: + """ + hint_data[self.player] = {} + for campaign in self.custom_mission_order.mission_order_node.campaigns: + for layout in campaign.layouts: + columns = layout.layout_type.get_visual_layout() + is_single_row_layout = max([len(column) for column in columns]) == 1 + for column_index, column in enumerate(columns): + for row_index, layout_mission in enumerate(column): + slot = layout.missions[layout_mission] + if hasattr(slot, "mission") and slot.mission is not None: + mission = slot.mission + campaign_name = campaign.get_visual_name() + layout_name = layout.get_visual_name() + if isinstance(layout.layout_type, Gauntlet): + # Linearize Gauntlet + column_name = str( + layout_mission + 1 + if layout_mission >= 0 + else layout.layout_type.size + layout_mission + 1 + ) + row_name = "" + else: + column_name = "" if len(columns) == 1 else _get_column_display(column_index, is_single_row_layout) + row_name = "" if is_single_row_layout else str(1 + row_index) + mission_position_name: str = campaign_name + " " + layout_name + " " + column_name + row_name + mission_position_name = mission_position_name.strip().replace(" ", " ") + if mission_position_name != "": + for location in self.get_region(mission.mission_name).get_locations(): + if location.address is not None: + hint_data[self.player][location.address] = mission_position_name + + +def _get_column_display(index: int, single_row_layout: bool) -> str: + """ + Helper function to display column name + :param index: + :param single_row_layout: + :return: + """ + if single_row_layout: + return str(index + 1) + else: + # Convert column name to a letter, from Z continue with AA and so on + f: Callable[[int], str] = lambda x: "" if x == 0 else f((x - 1) // 26) + chr((x - 1) % 26 + ord("A")) + return f(index + 1) + + +def setup_events(player: int, locked_locations: List[str], location_cache: List[Location]) -> None: for location in location_cache: if location.address is None: item = Item(location.name, ItemClassification.progression, None, player) @@ -145,319 +368,661 @@ def setup_events(player: int, locked_locations: typing.List[str], location_cache 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) +def create_and_flag_explicit_item_locks_and_excludes(world: SC2World) -> List[FilterItem]: + """ + Handles `excluded_items`, `locked_items`, and `start_inventory` + Returns a list of all possible non-filler items that can be added, with an accompanying flags bitfield. + """ + excluded_items = world.options.excluded_items + unexcluded_items = world.options.unexcluded_items + locked_items = world.options.locked_items + start_inventory = world.options.start_inventory + key_items = world.custom_mission_order.get_items_to_lock() - # 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 resolve_count(count: Optional[int], max_count: int) -> int: + if count == 0: + return max_count + if count is None: + return 0 + if max_count == 0: + return count + return min(count, max_count) + + auto_excludes = {item_name: 1 for item_name in item_groups.legacy_items} + if world.options.exclude_overpowered_items.value == ExcludeOverpoweredItems.option_true: + for item_name in item_groups.overpowered_items: + auto_excludes[item_name] = 1 - 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)) + result: List[FilterItem] = [] + for item_name, item_data in item_tables.item_table.items(): + max_count = item_data.quantity + auto_excluded_count = auto_excludes.get(item_name) + excluded_count = excluded_items.get(item_name, auto_excluded_count) + unexcluded_count = unexcluded_items.get(item_name) + locked_count = locked_items.get(item_name) + start_count: Optional[int] = start_inventory.get(item_name) + key_count = key_items.get(item_name, 0) + # specifying 0 in the yaml means exclude / lock all + # start_inventory doesn't allow specifying 0 + # not specifying means don't exclude/lock/start + excluded_count = resolve_count(excluded_count, max_count) + unexcluded_count = resolve_count(unexcluded_count, max_count) + locked_count = resolve_count(locked_count, max_count) + start_count = resolve_count(start_count, max_count) - # Nova gear exclusion if NCO not in campaigns - if SC2Campaign.NCO not in enabled_campaigns: - excluded_items = excluded_items.union(nova_equipment) + excluded_count = max(0, excluded_count - unexcluded_count) - 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) + # Priority: start_inventory >> locked_items >> excluded_items >> unspecified + if max_count == 0: + if excluded_count: + logger.warning(f"Item {item_name} was listed as excluded, but as a filler item, it cannot be explicitly excluded.") + excluded_count = 0 + max_count = start_count + locked_count + elif start_count > max_count: + logger.warning(f"Item {item_name} had start amount greater than maximum amount ({start_count} > {max_count}). Capping start amount to max.") + start_count = max_count + locked_count = 0 + excluded_count = 0 + elif locked_count + start_count > max_count: + logger.warning(f"Item {item_name} had locked + start amount greater than maximum amount " + f"({locked_count} + {start_count} > {max_count}). Capping locked amount to max - start.") + locked_count = max_count - start_count + excluded_count = 0 + elif excluded_count + locked_count + start_count > max_count: + logger.warning(f"Item {item_name} had excluded + locked + start amounts greater than maximum amount " + f"({excluded_count} + {locked_count} + {start_count} > {max_count}). Decreasing excluded amount.") + excluded_count = max_count - start_count - locked_count + # Make sure the final count creates enough items to satisfy key requirements + final_count = max(max_count, key_count) + for index in range(final_count): + result.append(FilterItem(item_name, item_data, index)) + if index < start_count: + result[-1].flags |= ItemFilterFlags.StartInventory + if index < locked_count + start_count: + result[-1].flags |= ItemFilterFlags.Locked + if item_name in world.options.non_local_items: + result[-1].flags |= ItemFilterFlags.NonLocal + if index >= max(max_count - excluded_count, key_count): + result[-1].flags |= ItemFilterFlags.UserExcluded + return result - # 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) + +def flag_excludes_by_faction_presence(world: SC2World, item_list: List[FilterItem]) -> None: + """Excludes items based on if their faction has a mission present where they can be used""" + missions = get_all_missions(world.custom_mission_order) + if world.options.take_over_ai_allies.value: + terran_missions = [mission for mission in missions if (MissionFlag.Terran|MissionFlag.AiTerranAlly) & mission.flags] + zerg_missions = [mission for mission in missions if (MissionFlag.Zerg|MissionFlag.AiZergAlly) & mission.flags] + protoss_missions = [mission for mission in missions if (MissionFlag.Protoss|MissionFlag.AiProtossAlly) & mission.flags] 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) + terran_missions = [mission for mission in missions if MissionFlag.Terran in mission.flags] + zerg_missions = [mission for mission in missions if MissionFlag.Zerg in mission.flags] + protoss_missions = [mission for mission in missions if MissionFlag.Protoss in mission.flags] + terran_build_missions = [mission for mission in terran_missions if MissionFlag.NoBuild not in mission.flags] + zerg_build_missions = [mission for mission in zerg_missions if MissionFlag.NoBuild not in mission.flags] + protoss_build_missions = [mission for mission in protoss_missions if MissionFlag.NoBuild not in mission.flags] + auto_upgrades_in_nobuilds = ( + world.options.generic_upgrade_research.value + in (GenericUpgradeResearch.option_always_auto, GenericUpgradeResearch.option_auto_in_no_build) + ) - # 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) + for item in item_list: + # Catch-all for all of a faction's items + if not terran_missions and item.data.race == SC2Race.TERRAN: + if item.name not in item_groups.nova_equipment: + item.flags |= ItemFilterFlags.FilterExcluded + continue + if not zerg_missions and item.data.race == SC2Race.ZERG: + if item.data.type != item_tables.ZergItemType.Ability \ + and item.data.type != ZergItemType.Level: + item.flags |= ItemFilterFlags.FilterExcluded + continue + if not protoss_missions and item.data.race == SC2Race.PROTOSS: + if item.name not in item_groups.soa_items: + item.flags |= ItemFilterFlags.FilterExcluded + continue - return excluded_items + # Faction units + if (not terran_build_missions + and item.data.type in (item_tables.TerranItemType.Unit, item_tables.TerranItemType.Building, item_tables.TerranItemType.Mercenary) + ): + item.flags |= ItemFilterFlags.FilterExcluded + if (not zerg_build_missions + and item.data.type in (item_tables.ZergItemType.Unit, item_tables.ZergItemType.Mercenary, item_tables.ZergItemType.Evolution_Pit) + ): + if (SC2Mission.ENEMY_WITHIN not in missions + or world.options.grant_story_tech.value == GrantStoryTech.option_grant + or item.name not in (item_names.ZERGLING, item_names.ROACH, item_names.HYDRALISK, item_names.INFESTOR) + ): + item.flags |= ItemFilterFlags.FilterExcluded + if (not protoss_build_missions + and item.data.type in ( + item_tables.ProtossItemType.Unit, + item_tables.ProtossItemType.Unit_2, + item_tables.ProtossItemType.Building, + ) + ): + # Note(mm): This doesn't exclude things like automated assimilators or warp gate improvements + # because that item type is mixed in with e.g. Reconstruction Beam and Overwatch + if (SC2Mission.TEMPLAR_S_RETURN not in missions + or world.options.grant_story_tech.value == GrantStoryTech.option_grant + or item.name not in ( + item_names.IMMORTAL, item_names.ANNIHILATOR, + item_names.COLOSSUS, item_names.VANGUARD, item_names.REAVER, item_names.DARK_TEMPLAR, + item_names.SENTRY, item_names.HIGH_TEMPLAR, + ) + ): + item.flags |= ItemFilterFlags.FilterExcluded + + # Faction +attack/armour upgrades + if (item.data.type == item_tables.TerranItemType.Upgrade + and not terran_build_missions + and not auto_upgrades_in_nobuilds + ): + item.flags |= ItemFilterFlags.FilterExcluded + if (item.data.type == item_tables.ZergItemType.Upgrade + and not zerg_build_missions + and not auto_upgrades_in_nobuilds + ): + item.flags |= ItemFilterFlags.FilterExcluded + if (item.data.type == item_tables.ProtossItemType.Upgrade + and not protoss_build_missions + and not auto_upgrades_in_nobuilds + ): + item.flags |= ItemFilterFlags.FilterExcluded -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 +def flag_mission_based_item_excludes(world: SC2World, item_list: List[FilterItem]) -> None: + """ + Excludes items based on mission / campaign presence: Nova Gear, Kerrigan abilities, SOA + """ + missions = get_all_missions(world.custom_mission_order) + + kerrigan_missions = [mission for mission in missions if MissionFlag.Kerrigan in mission.flags] + kerrigan_build_missions = [mission for mission in kerrigan_missions if MissionFlag.NoBuild not in mission.flags] + nova_missions = [ + mission for mission in missions + if MissionFlag.Nova in mission.flags + or ( + world.options.nova_ghost_of_a_chance_variant == NovaGhostOfAChanceVariant.option_nco + and MissionFlag.WoLNova in mission.flags + ) + ] + + kerrigan_is_present = ( + len(kerrigan_missions) > 0 + and world.options.kerrigan_presence in kerrigan_unit_available + and SC2Campaign.HOTS in get_enabled_campaigns(world) # TODO: Kerrigan available all Zerg/Everywhere + ) + + # TvX build missions -- check flags + if world.options.take_over_ai_allies: + terran_build_missions = [mission for mission in missions if ( + (MissionFlag.Terran in mission.flags or MissionFlag.AiTerranAlly in mission.flags) + and MissionFlag.NoBuild not in mission.flags + )] + else: + terran_build_missions = [mission for mission in missions if ( + MissionFlag.Terran in mission.flags + and MissionFlag.NoBuild not in mission.flags + )] + tvz_build_missions = [mission for mission in terran_build_missions if MissionFlag.VsZerg in mission.flags] + tvp_build_missions = [mission for mission in terran_build_missions if MissionFlag.VsProtoss in mission.flags] + tvt_build_missions = [mission for mission in terran_build_missions if MissionFlag.VsTerran in mission.flags] + + # Check if SOA actives should be present + if world.options.spear_of_adun_presence != SpearOfAdunPresence.option_not_present: + soa_missions = missions + soa_missions = [ + m for m in soa_missions + if is_mission_in_soa_presence(world.options.spear_of_adun_presence.value, m) + ] + if not world.options.spear_of_adun_present_in_no_build: + soa_missions = [m for m in soa_missions if MissionFlag.NoBuild not in m.flags] + soa_presence = len(soa_missions) > 0 + else: + soa_presence = False + + # Check if SOA passives should be present + if world.options.spear_of_adun_passive_ability_presence != SpearOfAdunPassiveAbilityPresence.option_not_present: + soa_missions = missions + soa_missions = [ + m for m in soa_missions + if is_mission_in_soa_presence( + world.options.spear_of_adun_passive_ability_presence.value, + m, + SpearOfAdunPassiveAbilityPresence + ) + ] + if not world.options.spear_of_adun_passive_present_in_no_build: + soa_missions = [m for m in soa_missions if MissionFlag.NoBuild not in m.flags] + soa_passive_presence = len(soa_missions) > 0 + else: + soa_passive_presence = False + + remove_kerrigan_abils = ( + # TODO: Kerrigan presence Zerg/Everywhere + not kerrigan_is_present + or (world.options.grant_story_tech.value == GrantStoryTech.option_grant and not kerrigan_build_missions) + or ( + world.options.grant_story_tech.value == GrantStoryTech.option_allow_substitutes + and len(kerrigan_missions) == 1 + and kerrigan_missions[0] == SC2Mission.SUPREME + ) + ) + + for item in item_list: + # Filter Nova equipment if you never get Nova + if not nova_missions and (item.name in item_groups.nova_equipment): + item.flags |= ItemFilterFlags.FilterExcluded + + # Todo(mm): How should no-build only / grant_story_tech affect excluding Kerrigan items? + # Exclude Primal form based on Kerrigan presence or primal form option + if (item.data.type == item_tables.ZergItemType.Primal_Form + and ((not kerrigan_is_present) or world.options.kerrigan_primal_status != KerriganPrimalStatus.option_item) + ): + item.flags |= ItemFilterFlags.FilterExcluded + + # Remove Kerrigan abilities if there's no kerrigan + if item.data.type == item_tables.ZergItemType.Ability and remove_kerrigan_abils: + item.flags |= ItemFilterFlags.FilterExcluded + + # Remove Spear of Adun if it's off + if item.name in item_tables.spear_of_adun_calldowns and not soa_presence: + item.flags |= ItemFilterFlags.FilterExcluded + + # Remove Spear of Adun passives + if item.name in item_tables.spear_of_adun_castable_passives and not soa_passive_presence: + item.flags |= ItemFilterFlags.FilterExcluded + + # Remove matchup-specific items if you don't play that matchup + if (item.name in (item_names.HIVE_MIND_EMULATOR, item_names.PSI_DISRUPTER) + and not tvz_build_missions + ): + item.flags |= ItemFilterFlags.FilterExcluded + if (item.name in (item_names.PSI_INDOCTRINATOR, item_names.SONIC_DISRUPTER) + and not tvt_build_missions + ): + item.flags |= ItemFilterFlags.FilterExcluded + if (item.name in (item_names.PSI_SCREEN, item_names.ARGUS_AMPLIFIER) + and not tvp_build_missions + ): + item.flags |= ItemFilterFlags.FilterExcluded + return + + +def flag_allowed_orphan_items(world: SC2World, item_list: List[FilterItem]) -> None: + """Adds the `Allowed_Orphan` flag to items that shouldn't be filtered with their parents, like combat shield""" + missions = get_all_missions(world.custom_mission_order) + terran_nobuild_missions = any((MissionFlag.Terran|MissionFlag.NoBuild) in mission.flags and mission.campaign != SC2Campaign.NCO for mission in missions) + if terran_nobuild_missions: + for item in item_list: + if item.name in ( + item_names.MARINE_COMBAT_SHIELD, item_names.MARINE_PROGRESSIVE_STIMPACK, item_names.MARINE_MAGRAIL_MUNITIONS, + item_names.MEDIC_STABILIZER_MEDPACKS, item_names.MEDIC_NANO_PROJECTOR, item_names.MARINE_LASER_TARGETING_SYSTEM, + ): + item.flags |= ItemFilterFlags.AllowedOrphan + # These rules only trigger on Standard tactics + if SC2Mission.BELLY_OF_THE_BEAST in missions and world.options.required_tactics == RequiredTactics.option_standard: + for item in item_list: + if item.name in (item_names.FIREBAT_NANO_PROJECTORS, item_names.FIREBAT_NANO_PROJECTORS, item_names.FIREBAT_PROGRESSIVE_STIMPACK): + item.flags |= ItemFilterFlags.AllowedOrphan + if SC2Mission.EVIL_AWOKEN in missions and world.options.required_tactics == RequiredTactics.option_standard: + for item in item_list: + if item.name in (item_names.STALKER_PHASE_REACTOR, item_names.STALKER_INSTIGATOR_SLAYER_DISINTEGRATING_PARTICLES, item_names.STALKER_INSTIGATOR_SLAYER_PARTICLE_REFLECTION): + item.flags |= ItemFilterFlags.AllowedOrphan + + +def flag_start_inventory(world: SC2World, item_list: List[FilterItem]) -> None: + """Adds items to start_inventory based on first mission logic and options like `starter_unit` and `start_primary_abilities`""" + potential_starters = world.custom_mission_order.get_starting_missions() + starter_mission_names = [mission.mission_name for mission in potential_starters] + starter_unit = int(world.options.starter_unit) + + # If starter_unit is off and the first mission doesn't have a no-logic location, force starter_unit on 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] + start_collection_state = CollectionState(world.multiworld) + starter_mission_locations = [location.name for location in world.location_cache + if location.parent_region + and location.parent_region.name in starter_mission_names + and location.access_rule(start_collection_state)] 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 + flag_start_unit(world, item_list, starter_unit) - 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 + flag_start_abilities(world, item_list) -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 flag_start_unit(world: SC2World, item_list: List[FilterItem], starter_unit: int) -> None: + first_mission = get_random_first_mission(world, world.custom_mission_order) + first_race = first_mission.race + + if first_race == SC2Race.ANY: + # If the first mission is a logic-less no-build + missions = get_all_missions(world.custom_mission_order) + build_missions = [mission for mission in missions if MissionFlag.NoBuild not in mission.flags] + races = {mission.race for mission in build_missions if mission.race != SC2Race.ANY} + if races: + first_race = world.random.choice(list(races)) + + if first_race != SC2Race.ANY: + possible_starter_items = { + item.name: item for item in item_list if (ItemFilterFlags.Plando|ItemFilterFlags.UserExcluded|ItemFilterFlags.FilterExcluded) & item.flags == 0 + } + + # The race of the early unit has been chosen + basic_units = get_basic_units(world.options.required_tactics.value, first_race) + if starter_unit == StarterUnit.option_balanced: + basic_units = basic_units.difference(not_balanced_starting_units) + if first_mission == SC2Mission.DARK_WHISPERS: + # Special case - you don't have a logicless location but need an AA + basic_units = basic_units.difference( + {item_names.ZEALOT, item_names.CENTURION, item_names.SENTINEL, item_names.BLOOD_HUNTER, + item_names.AVENGER, item_names.IMMORTAL, item_names.ANNIHILATOR, item_names.VANGUARD}) + if first_mission == SC2Mission.SUDDEN_STRIKE: + # Special case - cliffjumpers + basic_units = {item_names.REAPER, item_names.GOLIATH, item_names.SIEGE_TANK, item_names.VIKING, item_names.BANSHEE} + basic_unit_options = [ + item for item in possible_starter_items.values() + if item.name in basic_units + and ItemFilterFlags.StartInventory not in item.flags + ] + + # For Sudden Strike, starter units need an upgrade to help them get around + nco_support_items = { + item_names.REAPER: item_names.REAPER_SPIDER_MINES, + item_names.GOLIATH: item_names.GOLIATH_JUMP_JETS, + item_names.SIEGE_TANK: item_names.SIEGE_TANK_JUMP_JETS, + item_names.VIKING: item_names.VIKING_SMART_SERVOS, + } + if first_mission == SC2Mission.SUDDEN_STRIKE: + basic_unit_options = [ + item for item in basic_unit_options + if item.name not in nco_support_items + or nco_support_items[item.name] in possible_starter_items + and ((ItemFilterFlags.Plando|ItemFilterFlags.UserExcluded|ItemFilterFlags.FilterExcluded) & possible_starter_items[nco_support_items[item.name]].flags) == 0 + ] + if not basic_unit_options: + raise OptionError("Early Unit: At least one basic unit must be included") + local_basic_unit = [item for item in basic_unit_options if ItemFilterFlags.NonLocal not in item.flags] + if local_basic_unit: + basic_unit_options = local_basic_unit + + unit = world.random.choice(basic_unit_options) + unit.flags |= ItemFilterFlags.StartInventory + + # NCO-only specific rules + if first_mission == SC2Mission.SUDDEN_STRIKE: + if unit.name in nco_support_items: + support_item = possible_starter_items[nco_support_items[unit.name]] + support_item.flags |= ItemFilterFlags.StartInventory + if item_names.NOVA_JUMP_SUIT_MODULE in possible_starter_items: + possible_starter_items[item_names.NOVA_JUMP_SUIT_MODULE].flags |= ItemFilterFlags.StartInventory + if MissionFlag.Nova in first_mission.flags: + possible_starter_weapons = ( + item_names.NOVA_HELLFIRE_SHOTGUN, + item_names.NOVA_PLASMA_RIFLE, + item_names.NOVA_PULSE_GRENADES, + ) + starter_weapon_options = [item for item in possible_starter_items.values() if item.name in possible_starter_weapons] + starter_weapon = world.random.choice(starter_weapon_options) + starter_weapon.flags |= ItemFilterFlags.StartInventory -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 flag_start_abilities(world: SC2World, item_list: List[FilterItem]) -> None: + starter_abilities = world.options.start_primary_abilities + if not starter_abilities: + return + assert starter_abilities <= 4 + ability_count = int(starter_abilities) + available_abilities = item_groups.kerrigan_non_ulimates + for i in range(ability_count): + potential_starter_abilities = [ + item for item in item_list + if item.name in available_abilities + and (ItemFilterFlags.UserExcluded|ItemFilterFlags.StartInventory|ItemFilterFlags.Plando) & item.flags == 0 + ] + if len(potential_starter_abilities) == 0 or i >= 3: + # Avoid picking an ultimate unless 4 starter abilities were asked for. + # Without this check, it would be possible to pick an ultimate if a previous tier failed + # to pick due to exclusions + available_abilities = item_groups.kerrigan_abilities + potential_starter_abilities = [ + item for item in item_list + if item.name in available_abilities + and (ItemFilterFlags.UserExcluded|ItemFilterFlags.StartInventory|ItemFilterFlags.Plando) & item.flags == 0 + ] + # Try to avoid giving non-local items unless there is no alternative + abilities = [item for item in potential_starter_abilities if ItemFilterFlags.NonLocal not in item.flags] + if not abilities: + abilities = potential_starter_abilities + if abilities: + ability = world.random.choice(abilities) + ability.flags |= ItemFilterFlags.StartInventory -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) +def flag_unused_upgrade_types(world: SC2World, item_list: List[FilterItem]) -> None: + """Excludes +armour/attack upgrades based on generic upgrade strategy. + Caps upgrade items based on `max_upgrade_level`.""" + include_upgrades = world.options.generic_upgrade_missions == 0 + upgrade_items = world.options.generic_upgrade_items.value + upgrade_included_counts: Dict[str, int] = {} + for item in item_list: + if item.data.type in item_tables.upgrade_item_types: + if not include_upgrades or (item.name not in upgrade_included_names[upgrade_items]): + item.flags |= ItemFilterFlags.Removed else: - pool.append(item) + included = upgrade_included_counts.get(item.name, 0) + if ( + included >= world.options.max_upgrade_level + and not (ItemFilterFlags.Locked|ItemFilterFlags.StartInventory) & item.flags + ): + item.flags |= ItemFilterFlags.FilterExcluded + elif ItemFilterFlags.UserExcluded not in item.flags: + upgrade_included_counts[item.name] = included + 1 - 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 flag_unreleased_items(item_list: List[FilterItem]) -> None: + """Remove all unreleased items unless they're explicitly locked""" + for item in item_list: + if (item.name in unreleased_items + and not (ItemFilterFlags.Locked|ItemFilterFlags.StartInventory) & item.flags): + item.flags |= ItemFilterFlags.Removed -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 flag_user_excluded_item_sets(world: SC2World, item_list: List[FilterItem]) -> None: + """Excludes items based on item set options (`only_vanilla_items`)""" + vanilla_nonprogressive_count = { + item_name: 0 for item_name in item_groups.terran_original_progressive_upgrades + } + if world.options.vanilla_items_only.value == VanillaItemsOnly.option_true: + vanilla_items = item_groups.vanilla_items + item_groups.nova_equipment + for item in item_list: + if ItemFilterFlags.UserExcluded in item.flags: + continue + if item.name not in vanilla_items: + item.flags |= ItemFilterFlags.UserExcluded + if item.name in item_groups.terran_original_progressive_upgrades: + if vanilla_nonprogressive_count[item.name]: + item.flags |= ItemFilterFlags.UserExcluded + vanilla_nonprogressive_count[item.name] += 1 + + excluded_count: Dict[str, int] = dict() -def create_item_with_correct_settings(player: int, name: str) -> Item: - data = get_full_item_list()[name] +def flag_war_council_items(world: SC2World, item_list: List[FilterItem]) -> None: + """Excludes / start-inventories items based on `nerf_unit_baselines` option. + Will skip items that are excluded by other sources.""" + if world.options.war_council_nerfs: + return - item = Item(name, data.classification, data.code, player) - - return item + flagged_item_names = [] + for item in item_list: + if ( + item.name in war_council_upgrades + and not ItemFilterFlags.Excluded & item.flags + and item.name not in flagged_item_names + ): + flagged_item_names.append(item.name) + item.flags |= ItemFilterFlags.StartInventory -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]): +def flag_and_add_resource_locations(world: SC2World, item_list: List[FilterItem]) -> None: """ Filters the locations in the world using a trash or Nothing item - :param multiworld: - :param player: - :param locked_locations: - :param location_cache: - :return: + :param world: The sc2 world object + :param item_list: The current list of items to append to """ - open_locations = [location for location in location_cache if location.item is None] + open_locations = [location for location in world.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)} + filler_location_types = get_location_types(world, LocationInclusion.option_filler) + filler_location_flags = get_location_flags(world, LocationInclusion.option_filler) + location_data = {sc2_location.name: sc2_location for sc2_location in DEFAULT_LOCATION_LIST} 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) + if (sc2_location.type in filler_location_types + or (sc2_location.flags & filler_location_flags) + ): + item_name = world.get_filler_item_name() item = create_item_with_correct_settings(world.player, item_name) + if item.classification & ItemClassification.progression: + # Scouting shall show Filler (or a trap) + item.classification = ItemClassification.filler location.place_locked_item(item) - locked_locations.append(location.name) + world.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 flag_mission_order_required_items(world: SC2World, item_list: List[FilterItem]) -> None: + """Marks items that are necessary for item rules in the mission order and forces them to be progression.""" + locks_required = world.custom_mission_order.get_items_to_lock() + locks_done = {item: 0 for item in locks_required} + for item in item_list: + if item.name in locks_required and locks_done[item.name] < locks_required[item.name]: + item.flags |= ItemFilterFlags.Locked + item.flags |= ItemFilterFlags.ForceProgression + locks_done[item.name] += 1 -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): +def prune_item_pool(world: SC2World, item_list: List[FilterItem]) -> List[StarcraftItem]: + """Prunes the item pool size to be less than the number of available locations""" + + item_list = [ + item for item in item_list + if (ItemFilterFlags.Removed not in item.flags) + and (ItemFilterFlags.Unexcludable & item.flags or ItemFilterFlags.FilterExcluded not in item.flags) + ] + num_items = len(item_list) + last_num_items = -1 + while num_items != last_num_items: + # Remove orphan items until there are no more being removed + item_name_list = [item.name for item in item_list] + item_list = [item for item in item_list + if (ItemFilterFlags.Unexcludable|ItemFilterFlags.AllowedOrphan) & item.flags + or item_list_contains_parent(world, item.data, item_name_list)] + last_num_items = num_items + num_items = len(item_list) + + pool: List[StarcraftItem] = [] + for item in item_list: + ap_item = create_item_with_correct_settings(world.player, item.name, item.flags) + if ItemFilterFlags.ForceProgression in item.flags: + ap_item.classification = ItemClassification.progression + pool.append(ap_item) + + fill_pool_with_kerrigan_levels(world, pool) + filtered_pool = filter_items(world, world.location_cache, pool) + return filtered_pool + + +def item_list_contains_parent(world: SC2World, item_data: ItemData, item_name_list: List[str]) -> bool: + if item_data.parent is None: + # The item has no associated parent, the item is valid + return True + return item_parents.parent_present[item_data.parent](item_name_list, world.options) + + +def pad_item_pool_with_filler(world: SC2World, num_items: int, pool: List[StarcraftItem]): + for _ in range(num_items): + item = create_item_with_correct_settings(world.player, world.get_filler_item_name()) + pool.append(item) + + +def set_up_filler_items_distribution(world: SC2World) -> None: + world.filler_items_distribution = world.options.filler_items_distribution.value.copy() + + prune_fillers(world) + if sum(world.filler_items_distribution.values()) == 0: + world.filler_items_distribution = FillerItemsDistribution.default.copy() + prune_fillers(world) + + +def prune_fillers(world): + mission_flags = world.custom_mission_order.get_used_flags() + include_protoss = ( + MissionFlag.Protoss in mission_flags + or (world.options.take_over_ai_allies and (MissionFlag.AiProtossAlly in mission_flags)) + ) + include_kerrigan = ( + MissionFlag.Kerrigan in mission_flags + and world.options.kerrigan_presence in kerrigan_unit_available + ) + generic_upgrade_research = world.options.generic_upgrade_research + if not include_protoss: + world.filler_items_distribution.pop(item_names.SHIELD_REGENERATION, 0) + if not include_kerrigan: + world.filler_items_distribution.pop(item_names.KERRIGAN_LEVELS_1, 0) + if (generic_upgrade_research in + [ + GenericUpgradeResearch.option_always_auto, + GenericUpgradeResearch.option_auto_in_build + ] + ): + world.filler_items_distribution.pop(item_names.UPGRADE_RESEARCH_SPEED, 0) + world.filler_items_distribution.pop(item_names.UPGRADE_RESEARCH_COST, 0) + + +def get_random_first_mission(world: SC2World, mission_order: SC2MissionOrder) -> SC2Mission: + # Pick an arbitrary lowest-difficulty starer mission + starting_missions = mission_order.get_starting_missions() + mission_difficulties = [ + (mission_order.mission_pools.get_modified_mission_difficulty(mission), mission) + for mission in starting_missions + ] + mission_difficulties.sort(key = lambda difficulty_mission_tuple: difficulty_mission_tuple[0]) + (lowest_difficulty, _) = mission_difficulties[0] + first_mission_candidates = [mission for (difficulty, mission) in mission_difficulties if difficulty == lowest_difficulty] + return world.random.choice(first_mission_candidates) + + +def get_all_missions(mission_order: SC2MissionOrder) -> List[SC2Mission]: + return mission_order.get_used_missions() + + +def create_item_with_correct_settings(player: int, name: str, filter_flags: ItemFilterFlags = ItemFilterFlags.Available) -> StarcraftItem: + data = item_tables.item_table[name] + + item = StarcraftItem(name, data.classification, data.code, player, filter_flags) + if ItemFilterFlags.ForceProgression & filter_flags: + item.classification = ItemClassification.progression + + return item + + +def fill_pool_with_kerrigan_levels(world: SC2World, item_pool: List[StarcraftItem]): + total_levels = world.options.kerrigan_level_item_sum.value + missions = get_all_missions(world.custom_mission_order) + kerrigan_missions = [mission for mission in missions if MissionFlag.Kerrigan in mission.flags] + kerrigan_build_missions = [mission for mission in kerrigan_missions if MissionFlag.NoBuild not in mission.flags] + if (world.options.kerrigan_presence.value not in kerrigan_unit_available + or total_levels == 0 + or not kerrigan_missions + or (world.options.grant_story_levels and not kerrigan_build_missions) + ): return def add_kerrigan_level_items(level_amount: int, item_amount: int): @@ -468,7 +1033,7 @@ def fill_pool_with_kerrigan_levels(world: World, item_pool: List[Item]): 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") + option = world.options.kerrigan_level_item_distribution.value assert isinstance(option, int) assert isinstance(total_levels, int) @@ -489,3 +1054,17 @@ def fill_pool_with_kerrigan_levels(world: World, item_pool: List[Item]): else: round_func = ceil add_kerrigan_level_items(size, round_func(float(total_levels) / size)) + + +def push_precollected_items_to_multiworld(world: SC2World, item_list: List[StarcraftItem]) -> None: + # Clear the pre-collected items, as AP will try to do this for us, + # and we want to be able to filer out precollected items in the case of upgrade packages. + auto_precollected_items = world.multiworld.precollected_items[world.player].copy() + world.multiworld.precollected_items[world.player].clear() + for item in auto_precollected_items: + world.multiworld.state.remove(item) + + for item in item_list: + if ItemFilterFlags.StartInventory not in item.filter_flags: + continue + world.multiworld.push_precollected(create_item_with_correct_settings(world.player, item.name, item.filter_flags)) diff --git a/worlds/sc2/client.py b/worlds/sc2/client.py new file mode 100644 index 00000000..d64d44ae --- /dev/null +++ b/worlds/sc2/client.py @@ -0,0 +1,2352 @@ +from __future__ import annotations + +import asyncio +import collections +import copy +import ctypes +import enum +import functools +import inspect +import logging +import multiprocessing +import os.path +import re +import sys +import tempfile +import typing +import queue +import zipfile +import io +import random +import concurrent.futures +import time +import uuid +from pathlib import Path + +# CommonClient import first to trigger ModuleUpdater +from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser +from Utils import init_logging, is_windows, async_start +from .item import item_names, item_parents, race_to_item_type +from .item.item_annotations import ITEM_NAME_ANNOTATIONS +from .item.item_groups import item_name_groups, unlisted_item_name_groups, ItemGroupNames +from . import options, VICTORY_MODULO +from .options import ( + MissionOrder, KerriganPrimalStatus, kerrigan_unit_available, KerriganPresence, EnableMorphling, GameDifficulty, + GameSpeed, GenericUpgradeItems, GenericUpgradeResearch, ColorChoice, GenericUpgradeMissions, MaxUpgradeLevel, + LocationInclusion, ExtraLocations, MasteryLocations, SpeedrunLocations, PreventativeLocations, ChallengeLocations, + VanillaLocations, + DisableForcedCamera, SkipCutscenes, GrantStoryTech, GrantStoryLevels, TakeOverAIAllies, RequiredTactics, + SpearOfAdunPresence, SpearOfAdunPresentInNoBuild, SpearOfAdunPassiveAbilityPresence, + SpearOfAdunPassivesPresentInNoBuild, EnableVoidTrade, VoidTradeAgeLimit, void_trade_age_limits_ms, VoidTradeWorkers, + DifficultyDamageModifier, MissionOrderScouting, GenericUpgradeResearchSpeedup, MercenaryHighlanders, WarCouncilNerfs, + is_mission_in_soa_presence, +) +from .mission_order.slot_data import CampaignSlotData, LayoutSlotData, MissionSlotData, MissionOrderObjectSlotData +from .mission_order.entry_rules import SubRuleRuleData, CountMissionsRuleData, MissionEntryRules +from .mission_tables import MissionFlag +from .transfer_data import normalized_unit_types, worker_units +from . import SC2World + + +if __name__ == "__main__": + init_logging("SC2Client", exception_logger="Client") + +logger = logging.getLogger("Client") +sc2_logger = logging.getLogger("Starcraft2") + +import nest_asyncio +from worlds._sc2common import bot +from worlds._sc2common.bot.data import Race +from worlds._sc2common.bot.main import run_game +from worlds._sc2common.bot.player import Bot +from .item.item_tables import ( + lookup_id_to_name, get_full_item_list, ItemData, + ZergItemType, upgrade_bundles, + WEAPON_ARMOR_UPGRADE_MAX_LEVEL, +) +from .locations import SC2WOL_LOC_ID_OFFSET, LocationType, LocationFlag, SC2HOTS_LOC_ID_OFFSET, VICTORY_CACHE_OFFSET +from .mission_tables import ( + lookup_id_to_mission, SC2Campaign, MissionInfo, + lookup_id_to_campaign, SC2Mission, campaign_mission_table, SC2Race +) + +import colorama +from .options import Option, upgrade_included_names +from NetUtils import ClientStatus, NetworkItem, JSONtoTextParser, JSONMessagePart, add_json_item, add_json_location, add_json_text, JSONTypes +from MultiServer import mark_raw + +pool = concurrent.futures.ThreadPoolExecutor(1) +loop = asyncio.get_event_loop_policy().new_event_loop() +nest_asyncio.apply(loop) +MAX_BONUS: int = 28 + +# GitHub repo where the Map/mod data is hosted for /download_data command +DATA_REPO_OWNER = "Ziktofel" +DATA_REPO_NAME = "Archipelago-SC2-data" +DATA_API_VERSION = "API4" + +# Bot controller +CONTROLLER_HEALTH: int = 38281 +CONTROLLER2_HEALTH: int = 38282 + +# Void Trade +TRADE_UNIT = "AP_TradeStructure" # ID of the unit +TRADE_SEND_BUTTON = "AP_TradeStructureDummySend" # ID of the button +TRADE_RECEIVE_1_BUTTON = "AP_TradeStructureDummyReceive" # ID of the button +TRADE_RECEIVE_5_BUTTON = "AP_TradeStructureDummyReceive5" # ID of the button +TRADE_DATASTORAGE_TEAM = "SC2_VoidTrade_" # + Team +TRADE_DATASTORAGE_SLOT = "slot_" # + Slot +TRADE_DATASTORAGE_LOCK = "_lock" +TRADE_LOCK_TIME = 5 # Time in seconds that the DataStorage may be considered safe to edit +TRADE_LOCK_WAIT_LIMIT = 540000 / 1.4 # Time in ms that the client may spend trying to get a lock (540000 = 9 minutes, 1.4 is 'faster' game speed's time scale) + +# Games +STARCRAFT2 = "Starcraft 2" +STARCRAFT2_WOL = "Starcraft 2 Wings of Liberty" + + +# Data version file path. +# This file is used to tell if the downloaded data are outdated +# Associated with /download_data command +def get_metadata_file() -> str: + return os.environ["SC2PATH"] + os.sep + "ArchipelagoSC2Metadata.txt" + + +def _remap_color_option(slot_data_version: int, color: int) -> int: + """Remap colour options for backwards compatibility with older slot data""" + if slot_data_version < 4 and color == ColorChoice.option_mengsk: + return ColorChoice.option_default + return color + + +class ConfigurableOptionType(enum.Enum): + INTEGER = enum.auto() + ENUM = enum.auto() + +class ConfigurableOptionInfo(typing.NamedTuple): + name: str + variable_name: str + option_class: typing.Type[Option] + option_type: ConfigurableOptionType = ConfigurableOptionType.ENUM + can_break_logic: bool = False + + +class ColouredMessage: + def __init__(self, text: str = '', *, keep_markup: bool = False) -> None: + self.parts: typing.List[dict] = [] + if text: + self(text, keep_markup=keep_markup) + def __call__(self, text: str, *, keep_markup: bool = False) -> 'ColouredMessage': + add_json_text(self.parts, text, keep_markup=keep_markup) + return self + def coloured(self, text: str, colour: str, *, keep_markup: bool = False) -> 'ColouredMessage': + add_json_text(self.parts, text, type="color", color=colour, keep_markup=keep_markup) + return self + def location(self, location_id: int, player_id: int) -> 'ColouredMessage': + add_json_location(self.parts, location_id, player_id) + return self + def item(self, item_id: int, player_id: int, flags: int = 0) -> 'ColouredMessage': + add_json_item(self.parts, item_id, player_id, flags) + return self + def player(self, player_id: int) -> 'ColouredMessage': + add_json_text(self.parts, str(player_id), type=JSONTypes.player_id) + return self + def send(self, ctx: SC2Context) -> None: + ctx.on_print_json({"data": self.parts, "cmd": "PrintJSON"}) + + +class StarcraftClientProcessor(ClientCommandProcessor): + ctx: SC2Context + + def formatted_print(self, text: str) -> None: + """Prints with kivy formatting to the GUI, and also prints to command-line and to all logs""" + # Note(mm): Bold/underline can help readability, but unfortunately the CommonClient does not filter bold tags from command-line output. + # Regardless, using `on_print_json` to get formatted text in the GUI and output in the command-line and in the logs, + # without having to branch code from CommonClient + self.ctx.on_print_json({"data": [{"text": text, "keep_markup": True}]}) + + def _cmd_difficulty(self, difficulty: str = "") -> bool: + """Overrides the current difficulty set for the world. Takes the argument casual, normal, hard, or brutal""" + arguments = difficulty.split() + num_arguments = len(arguments) + + if num_arguments > 0: + difficulty_choice = arguments[0].lower() + if difficulty_choice == "casual": + self.ctx.difficulty_override = 0 + elif difficulty_choice == "normal": + self.ctx.difficulty_override = 1 + elif difficulty_choice == "hard": + self.ctx.difficulty_override = 2 + elif difficulty_choice == "brutal": + self.ctx.difficulty_override = 3 + else: + self.output("Unable to parse difficulty '" + arguments[0] + "'") + return False + + self.output("Difficulty set to " + arguments[0]) + return True + + else: + if self.ctx.difficulty == -1: + self.output("Please connect to a seed before checking difficulty.") + else: + current_difficulty = self.ctx.difficulty + if self.ctx.difficulty_override >= 0: + current_difficulty = self.ctx.difficulty_override + self.output("Current difficulty: " + ["Casual", "Normal", "Hard", "Brutal"][current_difficulty]) + self.output("To change the difficulty, add the name of the difficulty after the command.") + return False + + + def _cmd_game_speed(self, game_speed: str = "") -> bool: + """Overrides the current game speed for the world. + Takes the arguments default, slower, slow, normal, fast, faster""" + arguments = game_speed.split() + num_arguments = len(arguments) + + if num_arguments > 0: + speed_choice = arguments[0].lower() + if speed_choice == "default": + self.ctx.game_speed_override = 0 + elif speed_choice == "slower": + self.ctx.game_speed_override = 1 + elif speed_choice == "slow": + self.ctx.game_speed_override = 2 + elif speed_choice == "normal": + self.ctx.game_speed_override = 3 + elif speed_choice == "fast": + self.ctx.game_speed_override = 4 + elif speed_choice == "faster": + self.ctx.game_speed_override = 5 + else: + self.output("Unable to parse game speed '" + arguments[0] + "'") + return False + + self.output("Game speed set to " + arguments[0]) + return True + + else: + if self.ctx.game_speed == -1: + self.output("Please connect to a seed before checking game speed.") + else: + current_speed = self.ctx.game_speed + if self.ctx.game_speed_override >= 0: + current_speed = self.ctx.game_speed_override + self.output("Current game speed: " + + ["Default", "Slower", "Slow", "Normal", "Fast", "Faster"][current_speed]) + self.output("To change the game speed, add the name of the speed after the command," + " or Default to select based on difficulty.") + return False + + @mark_raw + def _cmd_received(self, filter_search: str = "") -> bool: + """List received items. + Pass in a parameter to filter the search by partial item name or exact item group. + Use '/received recent ' to list the last 'number' items received (default 20).""" + if self.ctx.slot is None: + self.formatted_print("Connect to a slot to view what items are received.") + return True + if filter_search.casefold().startswith('recent'): + return self._received_recent(filter_search[len('recent'):].strip()) + # Groups must be matched case-sensitively, so we properly capitalize the search term + # eg. "Spear of Adun" over "Spear Of Adun" or "spear of adun" + # This fails a lot of item name matches, but those should be found by partial name match + group_filter = '' + for group_name in item_name_groups: + if group_name in unlisted_item_name_groups: + continue + if filter_search.casefold() == group_name.casefold(): + group_filter = group_name + break + + def item_matches_filter(item_name: str) -> bool: + # The filter can be an exact group name or a partial item name + # Partial item name can be matched case-insensitively + if filter_search.casefold() in item_name.casefold(): + return True + # The search term should already be formatted as a group name + if group_filter and item_name in item_name_groups[group_filter]: + return True + return False + + items = get_full_item_list() + categorized_items: typing.Dict[SC2Race, typing.List[typing.Union[int, str]]] = {} + parent_to_child: typing.Dict[typing.Union[int, str], typing.List[int]] = {} + items_received: typing.Dict[int, typing.List[NetworkItem]] = {} + for item in self.ctx.items_received: + items_received.setdefault(item.item, []).append(item) + items_received_set = set(items_received) + for item_data in items.values(): + if item_data.parent: + parent_rule = item_parents.parent_present[item_data.parent] + if parent_rule.constraint_group is not None and parent_rule.constraint_group in items: + parent_to_child.setdefault(items[parent_rule.constraint_group].code, []).append(item_data.code) + continue + race = items[parent_rule.parent_items()[0]].race + categorized_items.setdefault(race, []) + if parent_rule.display_string not in categorized_items[race]: + categorized_items[race].append(parent_rule.display_string) + parent_to_child.setdefault(parent_rule.display_string, []).append(item_data.code) + else: + categorized_items.setdefault(item_data.race, []).append(item_data.code) + + def display_info(element: typing.Union[SC2Race, str, int]) -> tuple: + """Return (should display, name, type, children, sum(obtained), sum(matching filter))""" + have_item = isinstance(element, int) and element in items_received_set + if isinstance(element, SC2Race): + children: typing.Sequence[typing.Union[str, int]] = categorized_items[faction] + name = element.name + elif isinstance(element, int): + children = parent_to_child.get(element, []) + name = self.ctx.item_names.lookup_in_game(element) + else: + assert isinstance(element, str) + children = parent_to_child[element] + name = element + matches_filter = item_matches_filter(name) + child_states = [display_info(child) for child in children] + return ( + (have_item and matches_filter) or any(child_state[0] for child_state in child_states), + name, + element, + child_states, + sum(child_state[4] for child_state in child_states) + have_item, + sum(child_state[5] for child_state in child_states) + (have_item and matches_filter), + ) + + def display_tree( + should_display: bool, name: str, element: typing.Union[SC2Race, str, int], child_states: tuple, indent: int = 0 + ) -> None: + if not should_display: + return + assert self.ctx.slot is not None + indent_str = " " * indent + if isinstance(element, SC2Race): + self.formatted_print(f" [u]{name}[/u] ") + for child in child_states: + display_tree(*child[:4]) + elif isinstance(element, str): + ColouredMessage(indent_str)("- ").coloured(name, "white").send(self.ctx) + for child in child_states: + display_tree(*child[:4], indent=indent+2) + elif isinstance(element, int): + items = items_received.get(element, []) + if not items: + ColouredMessage(indent_str)("- ").coloured(name, "red")(" - not obtained").send(self.ctx) + for item in items: + (ColouredMessage(indent_str)('- ') + .item(item.item, self.ctx.slot, flags=item.flags) + (" from ").location(item.location, item.player) + (" by ").player(item.player) + ).send(self.ctx) + for child in child_states: + display_tree(*child[:4], indent=indent+2) + non_matching_descendents = sum(child[5] - child[4] for child in children) + if non_matching_descendents > 0: + self.formatted_print(f"{indent_str} + {non_matching_descendents} child items that don't match the filter") + + + item_types_obtained = 0 + items_obtained_matching_filter = 0 + for faction in SC2Race: + should_display, name, element, children, faction_items_obtained, faction_items_matching_filter = display_info(faction) + item_types_obtained += faction_items_obtained + items_obtained_matching_filter += faction_items_matching_filter + display_tree(should_display, name, element, children) + if filter_search == "": + self.formatted_print(f"[b]Obtained: {len(self.ctx.items_received)} items ({item_types_obtained} types)[/b]") + else: + self.formatted_print(f"[b]Filter \"{filter_search}\" found {items_obtained_matching_filter} out of {item_types_obtained} obtained item types[/b]") + return True + + def _received_recent(self, amount: str) -> bool: + assert self.ctx.slot is not None + try: + display_amount = int(amount) + except ValueError: + display_amount = 20 + display_amount = min(display_amount, len(self.ctx.items_received)) + self.formatted_print(f"Last {display_amount} of {len(self.ctx.items_received)} items received (most recent last):") + for item in self.ctx.items_received[-display_amount:]: + ( + ColouredMessage() + .item(item.item, self.ctx.slot, item.flags) + (" from ").location(item.location, item.player) + (" by ").player(item.player) + ).send(self.ctx) + return True + + def _cmd_option(self, option_name: str = "", option_value: str = "") -> None: + """Sets a Starcraft game option that can be changed after generation. Use "/option list" to see all options.""" + + LOGIC_WARNING = " *Note changing this may result in logically unbeatable games*\n" + + configurable_options = ( + ConfigurableOptionInfo('speed', 'game_speed', options.GameSpeed), + ConfigurableOptionInfo('kerrigan_presence', 'kerrigan_presence', options.KerriganPresence, can_break_logic=True), + ConfigurableOptionInfo('kerrigan_level_cap', 'kerrigan_total_level_cap', options.KerriganTotalLevelCap, ConfigurableOptionType.INTEGER, can_break_logic=True), + ConfigurableOptionInfo('kerrigan_mission_level_cap', 'kerrigan_levels_per_mission_completed_cap', options.KerriganLevelsPerMissionCompletedCap, ConfigurableOptionType.INTEGER), + ConfigurableOptionInfo('kerrigan_levels_per_mission', 'kerrigan_levels_per_mission_completed', options.KerriganLevelsPerMissionCompleted, ConfigurableOptionType.INTEGER), + ConfigurableOptionInfo('grant_story_levels', 'grant_story_levels', options.GrantStoryLevels, can_break_logic=True), + ConfigurableOptionInfo('grant_story_tech', 'grant_story_tech', options.GrantStoryTech, can_break_logic=True), + ConfigurableOptionInfo('control_ally', 'take_over_ai_allies', options.TakeOverAIAllies, can_break_logic=True), + ConfigurableOptionInfo('soa_presence', 'spear_of_adun_presence', options.SpearOfAdunPresence, can_break_logic=True), + ConfigurableOptionInfo('soa_in_nobuilds', 'spear_of_adun_present_in_no_build', options.SpearOfAdunPresentInNoBuild, can_break_logic=True), + # Note(mm): Technically SOA passive presence is in the logic for Amon's Fall if Takeover AI Allies is true, + # but that's edge case enough I don't think we should warn about it. + ConfigurableOptionInfo('soa_passive_presence', 'spear_of_adun_passive_ability_presence', options.SpearOfAdunPassiveAbilityPresence), + ConfigurableOptionInfo('soa_passives_in_nobuilds', 'spear_of_adun_passive_present_in_no_build', options.SpearOfAdunPassivesPresentInNoBuild), + ConfigurableOptionInfo('max_upgrade_level', 'max_upgrade_level', options.MaxUpgradeLevel, ConfigurableOptionType.INTEGER), + ConfigurableOptionInfo('generic_upgrade_research', 'generic_upgrade_research', options.GenericUpgradeResearch), + ConfigurableOptionInfo('generic_upgrade_research_speedup', 'generic_upgrade_research_speedup', options.GenericUpgradeResearchSpeedup), + ConfigurableOptionInfo('minerals_per_item', 'minerals_per_item', options.MineralsPerItem, ConfigurableOptionType.INTEGER), + ConfigurableOptionInfo('gas_per_item', 'vespene_per_item', options.VespenePerItem, ConfigurableOptionType.INTEGER), + ConfigurableOptionInfo('supply_per_item', 'starting_supply_per_item', options.StartingSupplyPerItem, ConfigurableOptionType.INTEGER), + ConfigurableOptionInfo('max_supply_per_item', 'maximum_supply_per_item', options.MaximumSupplyPerItem, ConfigurableOptionType.INTEGER), + ConfigurableOptionInfo('reduced_supply_per_item', 'maximum_supply_reduction_per_item', options.MaximumSupplyReductionPerItem, ConfigurableOptionType.INTEGER), + ConfigurableOptionInfo('lowest_max_supply', 'lowest_maximum_supply', options.LowestMaximumSupply, ConfigurableOptionType.INTEGER), + ConfigurableOptionInfo('research_cost_per_item', 'research_cost_reduction_per_item', options.ResearchCostReductionPerItem, ConfigurableOptionType.INTEGER), + ConfigurableOptionInfo('no_forced_camera', 'disable_forced_camera', options.DisableForcedCamera), + ConfigurableOptionInfo('skip_cutscenes', 'skip_cutscenes', options.SkipCutscenes), + ConfigurableOptionInfo('enable_morphling', 'enable_morphling', options.EnableMorphling, can_break_logic=True), + ConfigurableOptionInfo('difficulty_damage_modifier', 'difficulty_damage_modifier', options.DifficultyDamageModifier), + ConfigurableOptionInfo('void_trade_age_limit', 'trade_age_limit', options.VoidTradeAgeLimit), + ConfigurableOptionInfo('void_trade_workers', 'trade_workers_allowed', options.VoidTradeWorkers), + ConfigurableOptionInfo('mercenary_highlanders', 'mercenary_highlanders', options.MercenaryHighlanders), + ) + + WARNING_COLOUR = "salmon" + CMD_COLOUR = "slateblue" + boolean_option_map = { + 'y': 'true', 'yes': 'true', 'n': 'false', 'no': 'false', 'true': 'true', 'false': 'false', + } + + help_message = ColouredMessage(inspect.cleandoc(""" + Options + -------------------- + """))('\n') + for option in configurable_options: + option_help_text = inspect.cleandoc(option.option_class.__doc__ or "No description provided.").split('\n', 1)[0] + help_message.coloured(option.name, CMD_COLOUR)(": " + " | ".join(option.option_class.options) + + f" -- {option_help_text}\n") + if option.can_break_logic: + help_message.coloured(LOGIC_WARNING, WARNING_COLOUR) + help_message("--------------------\nEnter an option without arguments to see its current value.\n") + + if not option_name or option_name == 'list' or option_name == 'help': + help_message.send(self.ctx) + return + for option in configurable_options: + if option_name == option.name: + option_value = boolean_option_map.get(option_value.lower(), option_value) + if not option_value: + pass + elif option.option_type == ConfigurableOptionType.ENUM and option_value in option.option_class.options: + self.ctx.__dict__[option.variable_name] = option.option_class.options[option_value] + elif option.option_type == ConfigurableOptionType.INTEGER: + try: + self.ctx.__dict__[option.variable_name] = int(option_value, base=0) + except: + self.output(f"{option_value} is not a valid integer") + else: + self.output(f"Unknown option value '{option_value}'") + ColouredMessage(f"{option.name} is '{option.option_class.get_option_name(self.ctx.__dict__[option.variable_name])}'").send(self.ctx) + break + else: + self.output(f"Unknown option '{option_name}'") + help_message.send(self.ctx) + + def _cmd_color(self, faction: str = "", color: str = "") -> None: + """Changes the player color for a given faction.""" + player_colors = [ + "White", "Red", "Blue", "Teal", + "Purple", "Yellow", "Orange", "Green", + "LightPink", "Violet", "LightGrey", "DarkGreen", + "Brown", "LightGreen", "DarkGrey", "Pink", + "Rainbow", "Mengsk", "BrightLime", "Arcane", "Ember", "HotPink", + "Random", "Default" + ] + var_names = { + 'raynor': 'player_color_raynor', + 'kerrigan': 'player_color_zerg', + 'primal': 'player_color_zerg_primal', + 'protoss': 'player_color_protoss', + 'nova': 'player_color_nova', + } + faction = faction.lower() + if not faction: + for faction_name, key in var_names.items(): + self.output(f"Current player color for {faction_name}: {player_colors[self.ctx.__dict__[key]]}") + self.output("To change your color, add the faction name and color after the command.") + self.output("Available factions: " + ', '.join(var_names)) + self.output("Available colors: " + ', '.join(player_colors)) + return + elif faction not in var_names: + self.output(f"Unknown faction '{faction}'.") + self.output("Available factions: " + ', '.join(var_names)) + return + match_colors = [player_color.lower() for player_color in player_colors] + if not color: + self.output(f"Current player color for {faction}: {player_colors[self.ctx.__dict__[var_names[faction]]]}") + self.output("To change this faction's colors, add the name of the color after the command.") + self.output("Available colors: " + ', '.join(player_colors)) + else: + if color.lower() not in match_colors: + self.output(color + " is not a valid color. Available colors: " + ', '.join(player_colors)) + return + if color.lower() == "random": + color = random.choice(player_colors[:-2]) + self.ctx.__dict__[var_names[faction]] = match_colors.index(color.lower()) + self.ctx.pending_color_update = True + self.output(f"Color for {faction} set to " + player_colors[self.ctx.__dict__[var_names[faction]]]) + + def _cmd_windowed_mode(self, value="") -> None: + """Controls whether sc2 will launch in Windowed mode. Persists across sessions.""" + if not value: + sc2_logger.info("Use `/windowed_mode [true|false]` to set the windowed mode") + elif value.casefold() in ('t', 'true', 'yes', 'y'): + SC2World.settings.game_windowed_mode = True + force_settings_save_on_close() + else: + SC2World.settings.game_windowed_mode = False + force_settings_save_on_close() + sc2_logger.info(f"Windowed mode is: {SC2World.settings.game_windowed_mode}") + + def _cmd_disable_mission_check(self) -> bool: + """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.""" + self.ctx.missions_unlocked = True + sc2_logger.info("Mission check has been disabled") + return True + + @mark_raw + def _cmd_set_path(self, path: str = '') -> bool: + """Manually set the SC2 install directory (if the automatic detection fails).""" + if path: + os.environ["SC2PATH"] = path + is_mod_installed_correctly() + return True + else: + sc2_logger.warning("When using set_path, you must type the path to your SC2 install directory.") + return False + + def _cmd_download_data(self) -> bool: + """Download the most recent release of the necessary files for playing SC2 with + Archipelago. Will overwrite existing files.""" + pool.submit(self._download_data, self.ctx) + return True + + @staticmethod + def _download_data(ctx: SC2Context) -> bool: + if "SC2PATH" not in os.environ: + check_game_install_path() + + if os.path.exists(get_metadata_file()): + with open(get_metadata_file(), "r") as f: + metadata = f.read() + else: + metadata = None + + tempzip, metadata = download_latest_release_zip( + DATA_REPO_OWNER, DATA_REPO_NAME, DATA_API_VERSION, metadata=metadata, force_download=True) + + if tempzip: + try: + zipfile.ZipFile(tempzip).extractall(path=os.environ["SC2PATH"]) + sc2_logger.info("Download complete. Package installed.") + if metadata is not None: + with open(get_metadata_file(), "w") as f: + f.write(metadata) + finally: + os.remove(tempzip) + else: + sc2_logger.warning("Download aborted/failed. Read the log for more information.") + return False + ctx.data_out_of_date = False + return True + + +class SC2JSONtoTextParser(JSONtoTextParser): + def __init__(self, ctx: SC2Context) -> None: + self.handlers = { + "ItemSend": self._handle_color, + "ItemCheat": self._handle_color, + "Hint": self._handle_color, + } + super().__init__(ctx) + + def _handle_color(self, node: JSONMessagePart) -> str: + codes = node["color"].split(";") + buffer = "".join(self.color_code(code) for code in codes if code in self.color_codes) + return buffer + self._handle_text(node) + '
' + + def _handle_item_name(self, node: JSONMessagePart) -> str: + if self.ctx.slot_info[node["player"]].game == STARCRAFT2: + annotation = ITEM_NAME_ANNOTATIONS.get(node["text"]) + if annotation is not None: + node["text"] += f" {annotation}" + return super()._handle_item_name(node) + + def color_code(self, code: str) -> str: + return '' + + +class SC2Context(CommonContext): + command_processor = StarcraftClientProcessor + game = STARCRAFT2 + items_handling = 0b111 + + def __init__(self, *args, **kwargs) -> None: + super(SC2Context, self).__init__(*args, **kwargs) + self.raw_text_parser = SC2JSONtoTextParser(self) + + self.data_out_of_date: bool = False + self.difficulty = -1 + self.game_speed = -1 + self.disable_forced_camera = 0 + self.skip_cutscenes = 0 + self.all_in_choice = 0 + self.mission_order = 0 + self.player_color_raynor = ColorChoice.option_blue + self.player_color_zerg = ColorChoice.option_orange + self.player_color_zerg_primal = ColorChoice.option_purple + self.player_color_protoss = ColorChoice.option_blue + self.player_color_nova = ColorChoice.option_dark_grey + self.pending_color_update = False + self.kerrigan_presence: int = KerriganPresence.default + self.kerrigan_primal_status = 0 + self.enable_morphling = EnableMorphling.default + self.custom_mission_order: typing.List[CampaignSlotData] = [] + self.mission_id_to_entry_rules: typing.Dict[int, MissionEntryRules] + self.final_mission_ids: typing.List[int] = [29] + self.final_locations: typing.List[int] = [] + self.announcements: queue.Queue = queue.Queue() + self.sc2_run_task: typing.Optional[asyncio.Task] = None + self.missions_unlocked: bool = False # allow launching missions ignoring requirements + self.max_upgrade_level: int = MaxUpgradeLevel.default + self.generic_upgrade_missions = 0 + self.generic_upgrade_research = 0 + self.generic_upgrade_research_speedup: int = GenericUpgradeResearchSpeedup.default + self.generic_upgrade_items = 0 + self.location_inclusions: typing.Dict[LocationType, int] = {} + self.location_inclusions_by_flag: typing.Dict[LocationFlag, int] = {} + self.plando_locations: typing.List[str] = [] + self.difficulty_override = -1 + self.game_speed_override = -1 + self.mission_id_to_location_ids: typing.Dict[int, typing.List[int]] = {} + self.last_bot: typing.Optional[ArchipelagoBot] = None + self.slot_data_version = 2 + self.required_tactics: int = RequiredTactics.default + self.grant_story_tech: int = GrantStoryTech.default + self.grant_story_levels: int = GrantStoryLevels.default + self.take_over_ai_allies: int = TakeOverAIAllies.default + self.spear_of_adun_presence = SpearOfAdunPresence.option_not_present + self.spear_of_adun_present_in_no_build = SpearOfAdunPresentInNoBuild.option_false + self.spear_of_adun_passive_ability_presence = SpearOfAdunPassiveAbilityPresence.option_not_present + self.spear_of_adun_passive_present_in_no_build = SpearOfAdunPassivesPresentInNoBuild.option_false + self.minerals_per_item: int = 15 # For backwards compat with games generated pre-0.4.5 + self.vespene_per_item: int = 15 # For backwards compat with games generated pre-0.4.5 + self.starting_supply_per_item: int = 2 # For backwards compat with games generated pre-0.4.5 + self.maximum_supply_per_item: int = 2 + self.maximum_supply_reduction_per_item: int = options.MaximumSupplyReductionPerItem.default + self.lowest_maximum_supply: int = options.LowestMaximumSupply.default + self.research_cost_reduction_per_item: int = options.ResearchCostReductionPerItem.default + self.use_nova_wol_fallback: bool = False + self.use_nova_nco_fallback: bool = False + self.mercenary_highlanders: bool = False + self.kerrigan_levels_per_mission_completed = 0 + self.trade_enabled: int = EnableVoidTrade.default + self.trade_age_limit: int = VoidTradeAgeLimit.default + self.trade_workers_allowed: int = VoidTradeWorkers.default + self.trade_underway: bool = False + self.trade_latest_reply: typing.Optional[dict] = None + self.trade_reply_event = asyncio.Event() + self.trade_lock_wait: int = 0 + self.trade_lock_start: typing.Optional[float] = None + self.trade_response: typing.Optional[str] = None + self.difficulty_damage_modifier: int = DifficultyDamageModifier.default + self.mission_order_scouting = MissionOrderScouting.option_none + self.mission_item_classification: typing.Optional[typing.Dict[str, int]] = None + self.war_council_nerfs: bool = False + + async def server_auth(self, password_requested: bool = False) -> None: + self.game = STARCRAFT2 + if password_requested and not self.password: + await super(SC2Context, self).server_auth(password_requested) + await self.get_username() + await self.send_connect() + if self.ui: + self.ui.first_check = True + + def is_legacy_game(self): + return self.game == STARCRAFT2_WOL + + def event_invalid_game(self): + if self.is_legacy_game(): + self.game = STARCRAFT2 + super().event_invalid_game() + else: + self.game = STARCRAFT2_WOL + async_start(self.send_connect()) + + def trade_storage_team(self) -> str: + return f"{TRADE_DATASTORAGE_TEAM}{self.team}" + + def trade_storage_slot(self) -> str: + return f"{TRADE_DATASTORAGE_SLOT}{self.slot}" + + def _apply_host_settings_to_options(self) -> None: + if str(SC2World.settings.game_difficulty).casefold() == 'casual': + self.difficulty = GameDifficulty.option_casual + elif str(SC2World.settings.game_difficulty).casefold() == 'normal': + self.difficulty = GameDifficulty.option_normal + elif str(SC2World.settings.game_difficulty).casefold() == 'hard': + self.difficulty = GameDifficulty.option_hard + elif str(SC2World.settings.game_difficulty).casefold() == 'brutal': + self.difficulty = GameDifficulty.option_brutal + + if str(SC2World.settings.game_speed).casefold() == 'slower': + self.game_speed = GameSpeed.option_slower + elif str(SC2World.settings.game_speed).casefold() == 'slow': + self.game_speed = GameSpeed.option_slow + elif str(SC2World.settings.game_speed).casefold() == 'normal': + self.game_speed = GameSpeed.option_normal + elif str(SC2World.settings.game_speed).casefold() == 'fast': + self.game_speed = GameSpeed.option_fast + elif str(SC2World.settings.game_speed).casefold() == 'faster': + self.game_speed = GameSpeed.option_faster + + if str(SC2World.settings.disable_forced_camera).casefold() == 'true': + self.disable_forced_camera = DisableForcedCamera.option_true + elif str(SC2World.settings.disable_forced_camera).casefold() == 'false': + self.disable_forced_camera = DisableForcedCamera.option_false + + if str(SC2World.settings.skip_cutscenes).casefold() == 'true': + self.skip_cutscenes = SkipCutscenes.option_true + elif str(SC2World.settings.skip_cutscenes).casefold() == 'false': + self.skip_cutscenes = SkipCutscenes.option_false + + def on_package(self, cmd: str, args: dict) -> None: + if cmd == "Connected": + # Set up the trade storage + async_start(self.send_msgs([ + { # We want to know about other clients' Set commands for locking + "cmd": "SetNotify", + "keys": [self.trade_storage_team()], + }, + { + "cmd": "Set", + "key": self.trade_storage_team(), + "default": { TRADE_DATASTORAGE_LOCK: 0 }, + "operations": [{"operation": "default", "value": None}] # value is ignored + } + ])) + + self.difficulty = args["slot_data"]["game_difficulty"] + self.game_speed = args["slot_data"].get("game_speed", GameSpeed.option_default) + self.disable_forced_camera = args["slot_data"].get("disable_forced_camera", DisableForcedCamera.default) + self.skip_cutscenes = args["slot_data"].get("skip_cutscenes", SkipCutscenes.default) + self.all_in_choice = args["slot_data"]["all_in_map"] + self.slot_data_version = args["slot_data"].get("version", 2) + + self._apply_host_settings_to_options() + + if self.slot_data_version < 4: + # Maintaining backwards compatibility with older slot data + slot_req_table: dict = args["slot_data"]["mission_req"] + + first_item = list(slot_req_table.keys())[0] + if first_item in [str(campaign.id) for campaign in SC2Campaign]: + # Multi-campaign + mission_req_table = {} + for campaign_id in slot_req_table: + campaign = lookup_id_to_campaign[int(campaign_id)] + mission_req_table[campaign] = { + mission: self.parse_mission_info(mission_info) + for mission, mission_info in slot_req_table[campaign_id].items() + } + else: + # Old format + mission_req_table = {SC2Campaign.GLOBAL: { + mission: self.parse_mission_info(mission_info) + for mission, mission_info in slot_req_table.items() + } + } + + self.custom_mission_order = self.parse_mission_req_table(mission_req_table) + + if self.slot_data_version >= 4: + self.custom_mission_order = [ + CampaignSlotData( + **{field:value for field, value in campaign_data.items() if field not in ["layouts", "entry_rule"]}, + entry_rule = SubRuleRuleData.parse_from_dict(campaign_data["entry_rule"]), + layouts = [ + LayoutSlotData( + **{field:value for field, value in layout_data.items() if field not in ["missions", "entry_rule"]}, + entry_rule = SubRuleRuleData.parse_from_dict(layout_data["entry_rule"]), + missions = [ + [ + MissionSlotData( + **{field:value for field, value in mission_data.items() if field != "entry_rule"}, + entry_rule = SubRuleRuleData.parse_from_dict(mission_data["entry_rule"]) + ) for mission_data in column + ] for column in layout_data["missions"] + ] + ) for layout_data in campaign_data["layouts"] + ] + ) for campaign_data in args["slot_data"]["custom_mission_order"] + ] + self.mission_id_to_entry_rules = { + mission.mission_id: MissionEntryRules(mission.entry_rule, layout.entry_rule, campaign.entry_rule) + for campaign in self.custom_mission_order for layout in campaign.layouts + for column in layout.missions for mission in column + } + + self.mission_order = args["slot_data"].get("mission_order", MissionOrder.option_vanilla) + if self.slot_data_version < 4: + self.final_mission_ids = [args["slot_data"].get("final_mission", SC2Mission.ALL_IN.id)] + else: + self.final_mission_ids = args["slot_data"].get("final_mission_ids", [SC2Mission.ALL_IN.id]) + self.final_locations = [get_location_id(mission_id, 0) for mission_id in self.final_mission_ids] + + self.player_color_raynor = _remap_color_option( + self.slot_data_version, + args["slot_data"].get("player_color_terran_raynor", ColorChoice.option_blue) + ) + self.player_color_zerg = _remap_color_option( + self.slot_data_version, + args["slot_data"].get("player_color_zerg", ColorChoice.option_orange) + ) + self.player_color_zerg_primal = _remap_color_option( + self.slot_data_version, + args["slot_data"].get("player_color_zerg_primal", ColorChoice.option_purple) + ) + self.player_color_protoss = _remap_color_option( + self.slot_data_version, + args["slot_data"].get("player_color_protoss", ColorChoice.option_blue) + ) + self.player_color_nova = _remap_color_option( + self.slot_data_version, + args["slot_data"].get("player_color_nova", ColorChoice.option_dark_grey) + ) + self.war_council_nerfs = args["slot_data"].get("war_council_nerfs", WarCouncilNerfs.option_false) + self.mercenary_highlanders = args["slot_data"].get("mercenary_highlanders", MercenaryHighlanders.option_false) + self.generic_upgrade_missions = args["slot_data"].get("generic_upgrade_missions", GenericUpgradeMissions.default) + self.max_upgrade_level = args["slot_data"].get("max_upgrade_level", MaxUpgradeLevel.default) + self.generic_upgrade_items = args["slot_data"].get("generic_upgrade_items", GenericUpgradeItems.option_individual_items) + self.generic_upgrade_research = args["slot_data"].get("generic_upgrade_research", GenericUpgradeResearch.option_vanilla) + self.generic_upgrade_research_speedup = args["slot_data"].get("generic_upgrade_research_speedup", GenericUpgradeResearchSpeedup.default) + self.kerrigan_presence = args["slot_data"].get("kerrigan_presence", KerriganPresence.option_vanilla) + self.kerrigan_primal_status = args["slot_data"].get("kerrigan_primal_status", KerriganPrimalStatus.option_vanilla) + self.kerrigan_levels_per_mission_completed = args["slot_data"].get("kerrigan_levels_per_mission_completed", 0) + self.kerrigan_levels_per_mission_completed_cap = args["slot_data"].get("kerrigan_levels_per_mission_completed_cap", -1) + self.kerrigan_total_level_cap = args["slot_data"].get("kerrigan_total_level_cap", -1) + self.enable_morphling = args["slot_data"].get("enable_morphling", EnableMorphling.option_false) + self.grant_story_tech = args["slot_data"].get("grant_story_tech", GrantStoryTech.option_no_grant) + self.grant_story_levels = args["slot_data"].get("grant_story_levels", GrantStoryLevels.option_additive) + self.required_tactics = args["slot_data"].get("required_tactics", RequiredTactics.option_standard) + self.take_over_ai_allies = args["slot_data"].get("take_over_ai_allies", TakeOverAIAllies.option_false) + self.spear_of_adun_presence = args["slot_data"].get("spear_of_adun_presence", SpearOfAdunPresence.option_not_present) + self.spear_of_adun_present_in_no_build = args["slot_data"].get("spear_of_adun_present_in_no_build", SpearOfAdunPresentInNoBuild.option_false) + if self.slot_data_version < 4: + self.spear_of_adun_passive_ability_presence = args["slot_data"].get("spear_of_adun_autonomously_cast_ability_presence", SpearOfAdunPassiveAbilityPresence.option_not_present) + self.spear_of_adun_passive_present_in_no_build = args["slot_data"].get("spear_of_adun_autonomously_cast_present_in_no_build", SpearOfAdunPassivesPresentInNoBuild.option_false) + else: + self.spear_of_adun_passive_ability_presence = args["slot_data"].get("spear_of_adun_passive_ability_presence", SpearOfAdunPassiveAbilityPresence.option_not_present) + self.spear_of_adun_passive_present_in_no_build = args["slot_data"].get("spear_of_adun_passive_present_in_no_build", SpearOfAdunPassivesPresentInNoBuild.option_false) + self.minerals_per_item = args["slot_data"].get("minerals_per_item", 15) + self.vespene_per_item = args["slot_data"].get("vespene_per_item", 15) + self.starting_supply_per_item = args["slot_data"].get("starting_supply_per_item", 2) + self.maximum_supply_per_item = args["slot_data"].get("maximum_supply_per_item", options.MaximumSupplyPerItem.default) + self.maximum_supply_reduction_per_item = args["slot_data"].get("maximum_supply_reduction_per_item", options.MaximumSupplyReductionPerItem.default) + self.lowest_maximum_supply = args["slot_data"].get("lowest_maximum_supply", options.LowestMaximumSupply.default) + self.research_cost_reduction_per_item = args["slot_data"].get("research_cost_reduction_per_item", options.ResearchCostReductionPerItem.default) + self.use_nova_wol_fallback = args["slot_data"].get("use_nova_wol_fallback", True) + if self.slot_data_version < 4: + self.use_nova_nco_fallback = args["slot_data"].get("nova_covert_ops_only", False) and self.mission_order == MissionOrder.option_vanilla + else: + self.use_nova_nco_fallback = args["slot_data"].get("use_nova_nco_fallback", False) + self.trade_enabled = args["slot_data"].get("enable_void_trade", EnableVoidTrade.option_false) + self.trade_age_limit = args["slot_data"].get("void_trade_age_limit", VoidTradeAgeLimit.default) + self.trade_workers_allowed = args["slot_data"].get("void_trade_workers", VoidTradeWorkers.default) + self.difficulty_damage_modifier = args["slot_data"].get("difficulty_damage_modifier", DifficultyDamageModifier.option_true) + self.mission_order_scouting = args["slot_data"].get("mission_order_scouting", MissionOrderScouting.option_none) + self.mission_item_classification = args["slot_data"].get("mission_item_classification") + + if self.required_tactics == RequiredTactics.option_no_logic: + # Locking Grant Story Tech/Levels if no logic + self.grant_story_tech = GrantStoryTech.option_grant + self.grant_story_levels = GrantStoryLevels.option_minimum + + self.location_inclusions = { + LocationType.VICTORY: LocationInclusion.option_enabled, # Victory checks are always enabled + LocationType.VICTORY_CACHE: LocationInclusion.option_enabled, # Victory checks are always enabled + LocationType.VANILLA: args["slot_data"].get("vanilla_locations", VanillaLocations.default), + LocationType.EXTRA: args["slot_data"].get("extra_locations", ExtraLocations.default), + LocationType.CHALLENGE: args["slot_data"].get("challenge_locations", ChallengeLocations.default), + LocationType.MASTERY: args["slot_data"].get("mastery_locations", MasteryLocations.default), + } + self.location_inclusions_by_flag = { + LocationFlag.SPEEDRUN: args["slot_data"].get("speedrun_locations", SpeedrunLocations.default), + LocationFlag.PREVENTATIVE: args["slot_data"].get("preventative_locations", PreventativeLocations.default), + } + self.plando_locations = args["slot_data"].get("plando_locations", []) + + self.build_location_to_mission_mapping() + + # Looks for the required maps and mods for SC2. Runs check_game_install_path. + maps_present = is_mod_installed_correctly() + if os.path.exists(get_metadata_file()): + with open(get_metadata_file(), "r") as f: + current_ver = f.read() + sc2_logger.debug(f"Current version: {current_ver}") + if is_mod_update_available(DATA_REPO_OWNER, DATA_REPO_NAME, DATA_API_VERSION, current_ver): + ( + ColouredMessage().coloured("NOTICE: Update for required files found. ", colour="red") + ("Run ").coloured("/download_data", colour="slateblue") + (" to install.") + ).send(self) + self.data_out_of_date = True + elif maps_present: + ( + ColouredMessage() + .coloured("NOTICE: Your map files may be outdated (version number not found). ", colour="red") + ("Run ").coloured("/download_data", colour="slateblue") + (" to install.") + ).send(self) + self.data_out_of_date = True + + ColouredMessage("[b]Check the Launcher tab to start playing.[/b]", keep_markup=True).send(self) + + elif cmd == "SetReply": + # Currently can only be Void Trade reply + self.trade_latest_reply = args + self.trade_reply_event.set() + + @staticmethod + def parse_mission_info(mission_info: dict[str, typing.Any]) -> MissionInfo: + if mission_info.get("id") is not None: + mission_info["mission"] = lookup_id_to_mission[mission_info["id"]] + elif isinstance(mission_info["mission"], int): + mission_info["mission"] = lookup_id_to_mission[mission_info["mission"]] + + return MissionInfo( + **{field: value for field, value in mission_info.items() if field in MissionInfo._fields} + ) + + @staticmethod + def parse_mission_req_table(mission_req_table: typing.Dict[SC2Campaign, typing.Dict[typing.Any, MissionInfo]]) -> typing.List[CampaignSlotData]: + campaigns: typing.List[typing.Tuple[int, CampaignSlotData]] = [] + rolling_rule_id = 0 + for (campaign, campaign_data) in mission_req_table.items(): + if campaign.campaign_name == "Global": + campaign_name = "" + else: + campaign_name = campaign.campaign_name + + categories: typing.Dict[str, typing.List[MissionSlotData]] = {} + for mission in campaign_data.values(): + if mission.category not in categories: + categories[mission.category] = [] + mission_id = mission.mission.id + sub_rules: typing.List[CountMissionsRuleData] = [] + missions: typing.List[int] + if mission.number: + amount = mission.number + missions = [ + mission.mission.id + for mission in mission_req_table[campaign].values() + ] + sub_rules.append(CountMissionsRuleData(missions, amount, [campaign_name])) + prev_missions: typing.List[int] = [] + if len(mission.required_world) > 0: + missions = [] + for connection in mission.required_world: + if isinstance(connection, dict): + required_campaign = {} + for camp, camp_data in mission_req_table.items(): + if camp.id == connection["campaign"]: + required_campaign = camp_data + break + required_mission_id = connection["connect_to"] + else: + required_campaign = mission_req_table[connection.campaign] + required_mission_id = connection.connect_to + required_mission = list(required_campaign.values())[required_mission_id - 1] + missions.append(required_mission.mission.id) + if required_mission.category == mission.category: + prev_missions.append(required_mission.mission.id) + if mission.or_requirements: + amount = 1 + else: + amount = len(missions) + sub_rules.append(CountMissionsRuleData(missions, amount, missions)) + entry_rule = SubRuleRuleData(rolling_rule_id, sub_rules, len(sub_rules)) + rolling_rule_id += 1 + categories[mission.category].append(MissionSlotData.legacy(mission_id, prev_missions, entry_rule)) + + layouts: typing.List[LayoutSlotData] = [] + for (layout, mission_slots) in categories.items(): + if layout.startswith("_"): + layout_name = "" + else: + layout_name = layout + layouts.append(LayoutSlotData.legacy(layout_name, [mission_slots])) + campaigns.append((campaign.id, CampaignSlotData.legacy(campaign_name, layouts))) + return [data for (_, data) in sorted(campaigns)] + + + def on_print_json(self, args: dict) -> None: + # goes to this world + if "receiving" in args and self.slot_concerns_self(args["receiving"]): + relevant = True + # found in this world + elif "item" in args and self.slot_concerns_self(args["item"].player): + relevant = True + # not related + else: + relevant = False + + if relevant: + self.announcements.put(self.raw_text_parser(copy.deepcopy(args["data"]))) + + super(SC2Context, self).on_print_json(args) + + def run_gui(self) -> None: + from .client_gui import start_gui + start_gui(self) + + async def shutdown(self) -> None: + await super(SC2Context, self).shutdown() + if self.last_bot: + self.last_bot.want_close = True + # If the client is not set up yet, the game is not done loading and must be force-closed + if not hasattr(self.last_bot, "client"): + bot.sc2process.kill_switch.kill_all() + if self.sc2_run_task: + self.sc2_run_task.cancel() + + async def disconnect(self, allow_autoreconnect: bool = False): + self.finished_game = False + await super(SC2Context, self).disconnect(allow_autoreconnect=allow_autoreconnect) + + def play_mission(self, mission_id: int) -> bool: + if self.missions_unlocked or is_mission_available(self, mission_id): + if self.sc2_run_task: + if not self.sc2_run_task.done(): + sc2_logger.warning("Starcraft 2 Client is still running!") + self.sc2_run_task.cancel() # doesn't actually close the game, just stops the python task + if self.slot is None: + sc2_logger.warning("Launching Mission without Archipelago authentication, " + "checks will not be registered to server.") + self.sc2_run_task = asyncio.create_task(starcraft_launch(self, mission_id), + name="Starcraft 2 Launch") + return True + else: + sc2_logger.info(f"{lookup_id_to_mission[mission_id].mission_name} is not currently unlocked.") + return False + + def build_location_to_mission_mapping(self) -> None: + mission_id_to_location_ids: typing.Dict[int, typing.Set[int]] = { + mission.mission_id: set() + for campaign in self.custom_mission_order for layout in campaign.layouts + for column in layout.missions for mission in column + } + + for loc in self.server_locations: + offset = ( + SC2WOL_LOC_ID_OFFSET + if loc < SC2HOTS_LOC_ID_OFFSET + else (SC2HOTS_LOC_ID_OFFSET - SC2Mission.ALL_IN.id * VICTORY_MODULO) + ) + mission_id, objective = divmod(loc - offset, VICTORY_MODULO) + mission_id_to_location_ids[mission_id].add(objective) + self.mission_id_to_location_ids = { + mission_id: sorted(objectives) + for mission_id, objectives in mission_id_to_location_ids.items() + } + + def locations_for_mission(self, mission: SC2Mission) -> typing.Iterable[int]: + mission_id: int = mission.id + objectives = self.mission_id_to_location_ids[mission_id] + for objective in objectives: + yield get_location_id(mission_id, objective) + + def locations_for_mission_id(self, mission_id: int) -> typing.Iterable[int]: + objectives = self.mission_id_to_location_ids[mission_id] + for objective in objectives: + yield get_location_id(mission_id, objective) + + def uncollected_locations_in_mission(self, mission: SC2Mission) -> typing.Iterable[int]: + for location_id in self.locations_for_mission(mission): + if location_id in self.missing_locations: + yield location_id + + def is_mission_completed(self, mission_id: int) -> bool: + return get_location_id(mission_id, 0) in self.checked_locations + + + async def trade_acquire_storage(self, keep_trying: bool = False) -> typing.Optional[dict]: + # This function was largely taken from the Pokemon Emerald client + """ + Acquires a lock on the Void Trade DataStorage. + Locking the key means you have exclusive access + to modifying the value until you unlock it or the key expires (5 seconds). + + If `keep_trying` is `True`, it will keep trying to acquire the lock + until successful. Otherwise it will return `None` if it fails to + acquire the lock. + """ + while not self.exit_event.is_set() and self.last_bot and self.last_bot.game_running: + lock = int(time.time_ns() / 1000000000) # in seconds + + # Make sure we're not past the waiting limit + # SC2 needs to be notified within 10 minutes of game time (training time of the dummy units) + if self.trade_lock_start is not None: + if self.last_bot.time - self.trade_lock_start >= TRADE_LOCK_WAIT_LIMIT: + self.trade_lock_wait = 0 + self.trade_lock_start = None + return None + elif keep_trying: + self.trade_lock_start = self.last_bot.time + + message_uuid = str(uuid.uuid4()) + await self.send_msgs([{ + "cmd": "Set", + "key": self.trade_storage_team(), + "default": { TRADE_DATASTORAGE_LOCK: 0 }, + "want_reply": True, + "operations": [{ "operation": "update", "value": { TRADE_DATASTORAGE_LOCK: lock } }], + "uuid": message_uuid, + }]) + + self.trade_reply_event.clear() + try: + await asyncio.wait_for(self.trade_reply_event.wait(), 5) + except asyncio.TimeoutError: + if not keep_trying: + return None + continue + + assert self.trade_latest_reply is not None + reply = copy.deepcopy(self.trade_latest_reply) + + # Make sure the most recently received update was triggered by our lock attempt + if reply.get("uuid", None) != message_uuid: + if not keep_trying: + return None + await asyncio.sleep(TRADE_LOCK_TIME) + continue + + # Make sure the current value of the lock is what we set it to + # (I think this should theoretically never run) + if reply["value"][TRADE_DATASTORAGE_LOCK] != lock: + if not keep_trying: + return None + await asyncio.sleep(TRADE_LOCK_TIME) + continue + + # Make sure that the lock value we replaced is at least 5 seconds old + # If it was unlocked before our change, its value was 0 and it will look decades old + if lock - reply["original_value"][TRADE_DATASTORAGE_LOCK] < TRADE_LOCK_TIME: + if not keep_trying: + return None + + # Multiple clients trying to lock the key may get stuck in a loop of checking the lock + # by trying to set it, which will extend its expiration. So if we see that the lock was + # too new when we replaced it, we should wait for increasingly longer periods so that + # eventually the lock will expire and a client will acquire it. + self.trade_lock_wait += TRADE_LOCK_TIME + self.trade_lock_wait += random.randrange(100, 500) / 1000 + + await asyncio.sleep(self.trade_lock_wait) + continue + + # We have the lock, reset the waiting period and return + self.trade_lock_wait = 0 + self.trade_lock_start = None + return reply + return None + + + async def trade_receive(self, amount: int = 1): + """ + Tries to pop `amount` units out of the trade storage. + """ + reply = await self.trade_acquire_storage(True) + + if reply is None: + self.trade_response = "?TradeFail Void Trade failed: Could not communicate with server. Trade cost refunded." + return None + + # Find available units + # Ignore units we sent ourselves + allowed_slots: typing.List[str] = [ + slot for slot in reply["value"] + if slot != TRADE_DATASTORAGE_LOCK \ + and slot != self.trade_storage_slot() + ] + # Filter out trades that are too old + if self.trade_age_limit != VoidTradeAgeLimit.option_disabled: + trade_time = reply["value"][TRADE_DATASTORAGE_LOCK] + allowed_age = void_trade_age_limits_ms[self.trade_age_limit] + is_young_enough = lambda send_time: trade_time - send_time <= allowed_age + else: + is_young_enough = lambda _: True + # Filter out banned units + if self.trade_workers_allowed == VoidTradeWorkers.option_false: + is_unit_allowed = lambda unit: unit not in worker_units + else: + is_unit_allowed = lambda _: True + + available_units: typing.List[typing.Tuple[str, str, int]] = [] + available_counts: typing.List[int] = [] + for slot in allowed_slots: + for (send_time, units) in reply["value"][slot].items(): + if is_young_enough(int(send_time)): + for (unit, count) in units.items(): + if is_unit_allowed(str(unit)): + available_units.append((unit, slot, send_time)) + available_counts.append(count) + + # Pick units to receive + # If there's not enough units in total, just pick as many as possible + # SC2 should handle the refund + available = sum(available_counts) + refunds = 0 + if available < amount: + refunds = amount - available + amount = available + if available == 0: + # random.sample crashes if counts is an empty list + units = [] + else: + units = random.sample(available_units, amount, counts = available_counts) + + # Build response data + unit_counts: typing.Dict[str, int] = {} + slots_to_update: typing.Dict[str, typing.Dict[int, typing.Dict[str, int]]] = {} + for (unit, slot, send_time) in units: + unit_counts[unit] = unit_counts.get(unit, 0) + 1 + if slot not in slots_to_update: + slots_to_update[slot] = copy.deepcopy(reply["value"][slot]) + slots_to_update[slot][send_time][unit] -= 1 + # Clean up units that were removed completely + if slots_to_update[slot][send_time][unit] == 0: + slots_to_update[slot][send_time].pop(unit) + # Clean up trades that were completely exhausted + if len(slots_to_update[slot][send_time]) == 0: + slots_to_update[slot].pop(send_time) + + await self.send_msgs([ + { # Update server storage + "cmd": "Set", + "key": self.trade_storage_team(), + "operations": [{ "operation": "update", "value": slots_to_update }] + }, + { # Release the lock + "cmd": "Set", + "key": self.trade_storage_team(), + "operations": [{ "operation": "update", "value": { TRADE_DATASTORAGE_LOCK: 0 } }] + } + ]) + + # Give units to bot + self.trade_response = f"?Trade {refunds} " + " ".join(f"{unit} {count}" for (unit, count) in unit_counts.items()) + + + async def trade_send(self, units: typing.List[str]): + """ + Tries to upload `units` to the trade DataStorage. + """ + reply = await self.trade_acquire_storage(True) + + if reply is None: + self.trade_response = "?TradeFail Void Trade failed: Could not communicate with server. Your units remain." + return None + + # Create a storage entry for the time the trade was confirmed + trade_time = reply["value"][TRADE_DATASTORAGE_LOCK] + storage_entry = {} + for unit in units: + storage_entry[unit] = storage_entry.get(unit, 0) + 1 + + # Update the storage with the new units + data: typing.Dict[int, typing.Dict[str, int]] = copy.deepcopy(reply["value"].get(self.trade_storage_slot(), {})) + data[trade_time] = storage_entry + + await self.send_msgs([ + { # Send the updated data + "cmd": "Set", + "key": self.trade_storage_team(), + "operations": [{ "operation": "update", "value": { self.trade_storage_slot(): data } }] + }, + { # Release the lock + "cmd": "Set", + "key": self.trade_storage_team(), + "operations": [{ "operation": "update", "value": { TRADE_DATASTORAGE_LOCK: 0 } }] + } + ]) + + # Notify the game + self.trade_response = "?TradeSuccess Void Trade successful: Units sent!" + + +class CompatItemHolder(typing.NamedTuple): + name: str + quantity: int = 1 + + +def parse_uri(uri: str) -> str: + if "://" in uri: + uri = uri.split("://", 1)[1] + return uri.split('?', 1)[0] + + +async def main(): + multiprocessing.freeze_support() + parser = get_base_parser() + parser.add_argument('--name', default=None, help="Slot Name to connect as.") + args, uri = parser.parse_known_args() + + if uri and uri[0].startswith('archipelago://'): + args.connect = parse_uri(' '.join(uri)) + + ctx = SC2Context(args.connect, args.password) + ctx.auth = args.name + if ctx.server_task is None: + ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") + + if gui_enabled: + ctx.run_gui() + ctx.run_cli() + + await ctx.exit_event.wait() + + await ctx.shutdown() + +# These items must be given to the player if the game is generated on older versions +API2_TO_API3_COMPAT_ITEMS: typing.Set[CompatItemHolder] = { + CompatItemHolder(item_names.PHOTON_CANNON), + CompatItemHolder(item_names.OBSERVER), + CompatItemHolder(item_names.WARP_HARMONIZATION), + CompatItemHolder(item_names.PROGRESSIVE_PROTOSS_WEAPON_ARMOR_UPGRADE, 3) +} +API3_TO_API4_COMPAT_ITEMS: typing.Set[CompatItemHolder] = { + # War Council + CompatItemHolder(item_names.ZEALOT_WHIRLWIND), + CompatItemHolder(item_names.CENTURION_RESOURCE_EFFICIENCY), + CompatItemHolder(item_names.SENTINEL_RESOURCE_EFFICIENCY), + CompatItemHolder(item_names.STALKER_PHASE_REACTOR), + CompatItemHolder(item_names.DRAGOON_PHALANX_SUIT), + CompatItemHolder(item_names.INSTIGATOR_MODERNIZED_SERVOS), + CompatItemHolder(item_names.ADEPT_DISRUPTIVE_TRANSFER), + CompatItemHolder(item_names.SLAYER_PHASE_BLINK), + CompatItemHolder(item_names.AVENGER_KRYHAS_CLOAK), + CompatItemHolder(item_names.DARK_TEMPLAR_LESSER_SHADOW_FURY), + CompatItemHolder(item_names.DARK_TEMPLAR_GREATER_SHADOW_FURY), + CompatItemHolder(item_names.BLOOD_HUNTER_BRUTAL_EFFICIENCY), + CompatItemHolder(item_names.SENTRY_DOUBLE_SHIELD_RECHARGE), + CompatItemHolder(item_names.ENERGIZER_MOBILE_CHRONO_BEAM), + CompatItemHolder(item_names.HAVOC_ENDURING_SIGHT), + CompatItemHolder(item_names.HIGH_TEMPLAR_PLASMA_SURGE), + CompatItemHolder(item_names.SIGNIFIER_FEEDBACK), + CompatItemHolder(item_names.ASCENDANT_BREATH_OF_CREATION), + CompatItemHolder(item_names.DARK_ARCHON_INDOMITABLE_WILL), + CompatItemHolder(item_names.IMMORTAL_IMPROVED_BARRIER), + CompatItemHolder(item_names.VANGUARD_RAPIDFIRE_CANNON), + CompatItemHolder(item_names.VANGUARD_FUSION_MORTARS), + CompatItemHolder(item_names.ANNIHILATOR_TWILIGHT_CHASSIS), + CompatItemHolder(item_names.COLOSSUS_FIRE_LANCE), + CompatItemHolder(item_names.WRATHWALKER_AERIAL_TRACKING), + CompatItemHolder(item_names.REAVER_KHALAI_REPLICATORS), + CompatItemHolder(item_names.PHOENIX_DOUBLE_GRAVITON_BEAM), + CompatItemHolder(item_names.CORSAIR_NETWORK_DISRUPTION), + CompatItemHolder(item_names.MIRAGE_GRAVITON_BEAM), + CompatItemHolder(item_names.VOID_RAY_PRISMATIC_RANGE), + CompatItemHolder(item_names.CARRIER_REPAIR_DRONES), + CompatItemHolder(item_names.TEMPEST_DISINTEGRATION), + CompatItemHolder(item_names.ARBITER_VESSEL_OF_THE_CONCLAVE), + CompatItemHolder(item_names.MOTHERSHIP_INTEGRATED_POWER), + # Other items + CompatItemHolder(item_names.ASCENDANT_ARCHON_MERGE), + CompatItemHolder(item_names.DARK_TEMPLAR_ARCHON_MERGE), + CompatItemHolder(item_names.SPORE_CRAWLER_BIO_BONUS), +} + +def compat_item_to_network_items(compat_item: CompatItemHolder) -> typing.List[NetworkItem]: + item_id = get_full_item_list()[compat_item.name].code + network_item = NetworkItem(item_id, 0, 0, 0) + return compat_item.quantity * [network_item] + + +def calculate_items(ctx: SC2Context) -> typing.Dict[SC2Race, typing.List[int]]: + items = ctx.items_received.copy() + item_list = get_full_item_list() + def create_network_item(item_name: str) -> NetworkItem: + return NetworkItem(item_list[item_name].code, 0, 0, 0) + + # Items unlocked in earlier generator versions by default (Prophecy defaults, war council, rebalances) + if ctx.slot_data_version < 3: + for compat_item in API2_TO_API3_COMPAT_ITEMS: + items.extend(compat_item_to_network_items(compat_item)) + if ctx.slot_data_version < 4: + for compat_item in API3_TO_API4_COMPAT_ITEMS: + items.extend(compat_item_to_network_items(compat_item)) + received_item_ids = set(item.item for item in ctx.items_received) + if item_list[item_names.GHOST_RESOURCE_EFFICIENCY].code in received_item_ids: + items.append(create_network_item(item_names.GHOST_BARGAIN_BIN_PRICES)) + if item_list[item_names.SPECTRE_RESOURCE_EFFICIENCY].code in received_item_ids: + items.append(create_network_item(item_names.SPECTRE_BARGAIN_BIN_PRICES)) + if item_list[item_names.ROGUE_FORCES].code in received_item_ids: + items.append(create_network_item(item_names.UNRESTRICTED_MUTATION)) + if item_list[item_names.SCOUT_RESOURCE_EFFICIENCY].code in received_item_ids: + items.append(create_network_item(item_names.SCOUT_SUPPLY_EFFICIENCY)) + if item_list[item_names.REAVER_RESOURCE_EFFICIENCY].code in received_item_ids: + items.append(create_network_item(item_names.REAVER_BARGAIN_BIN_PRICES)) + + # API < 4 Orbital Command Count (Deprecated item) + orbital_command_count: int = 0 + + network_item: NetworkItem + accumulators: typing.Dict[SC2Race, typing.List[int]] = { + race: [0 for element in item_type_enum_class if element.flag_word >= 0] + for race, item_type_enum_class in race_to_item_type.items() + } + + # Protoss Shield grouped item specific logic + shields_from_ground_upgrade: int = 0 + shields_from_air_upgrade: int = 0 + + for network_item in items: + name = lookup_id_to_name.get(network_item.item) + if name is None: + continue + item_data: ItemData = item_list[name] + + if item_data.type.flag_word < 0: + continue + + # exists exactly once + if item_data.quantity == 1 or name in item_name_groups[ItemGroupNames.UNRELEASED_ITEMS]: + accumulators[item_data.race][item_data.type.flag_word] |= 1 << item_data.number + + # exists multiple times + elif item_data.quantity > 1: + flaggroup = item_data.type.flag_word + + # Generic upgrades apply only to Weapon / Armor upgrades + if item_data.number >= 0: + accumulators[item_data.race][flaggroup] += 1 << item_data.number + else: + if name == item_names.PROGRESSIVE_PROTOSS_GROUND_UPGRADE: + shields_from_ground_upgrade += 1 + if name == item_names.PROGRESSIVE_PROTOSS_AIR_UPGRADE: + shields_from_air_upgrade += 1 + for bundled_number in get_bundle_upgrade_member_numbers(name): + accumulators[item_data.race][flaggroup] += 1 << bundled_number + + # Regen bio-steel nerf with API3 - undo for older games + if ctx.slot_data_version < 3 and name == item_names.PROGRESSIVE_REGENERATIVE_BIO_STEEL: + current_level = (accumulators[item_data.race][flaggroup] >> item_data.number) % 4 + if current_level == 2: + # Switch from level 2 to level 3 for compatibility + accumulators[item_data.race][flaggroup] += 1 << item_data.number + # sum + # Fillers, deprecated items + else: + if name == item_names.PROGRESSIVE_ORBITAL_COMMAND: + orbital_command_count += 1 + elif item_data.type == ZergItemType.Level: + accumulators[item_data.race][item_data.type.flag_word] += item_data.number + elif name == item_names.STARTING_MINERALS: + accumulators[item_data.race][item_data.type.flag_word] += ctx.minerals_per_item + elif name == item_names.STARTING_VESPENE: + accumulators[item_data.race][item_data.type.flag_word] += ctx.vespene_per_item + elif name == item_names.STARTING_SUPPLY: + accumulators[item_data.race][item_data.type.flag_word] += ctx.starting_supply_per_item + elif name == item_names.UPGRADE_RESEARCH_COST: + accumulators[item_data.race][item_data.type.flag_word] += ctx.research_cost_reduction_per_item + else: + accumulators[item_data.race][item_data.type.flag_word] += 1 + + # Fix Shields from generic upgrades by unit class (Maximum of ground/air upgrades) + if shields_from_ground_upgrade > 0 or shields_from_air_upgrade > 0: + shield_upgrade_level = max(shields_from_ground_upgrade, shields_from_air_upgrade) + shield_upgrade_item = item_list[item_names.PROGRESSIVE_PROTOSS_SHIELDS] + for _ in range(0, shield_upgrade_level): + accumulators[shield_upgrade_item.race][shield_upgrade_item.type.flag_word] += 1 << shield_upgrade_item.number + + # Deprecated Orbital Command handling (Backwards compatibility): + if orbital_command_count > 0: + orbital_command_replacement_items: typing.List[str] = [ + item_names.COMMAND_CENTER_SCANNER_SWEEP, + item_names.COMMAND_CENTER_MULE, + item_names.COMMAND_CENTER_EXTRA_SUPPLIES, + item_names.PLANETARY_FORTRESS_ORBITAL_MODULE + ] + replacement_item_ids = [get_full_item_list()[item_name].code for item_name in orbital_command_replacement_items] + if sum(item_id in replacement_item_ids for item_id in items) > 0: + logger.warning(inspect.cleandoc(""" + Both old Orbital Command and its replacements are present in the world. Skipping compatibility handling. + """)) + else: + # None of replacement items are present + # L1: MULE and Scanner Sweep + scanner_sweep_data = get_full_item_list()[item_names.COMMAND_CENTER_SCANNER_SWEEP] + mule_data = get_full_item_list()[item_names.COMMAND_CENTER_MULE] + accumulators[scanner_sweep_data.race][scanner_sweep_data.type.flag_word] += 1 << scanner_sweep_data.number + accumulators[mule_data.race][mule_data.type.flag_word] += 1 << mule_data.number + if orbital_command_count >= 2: + # L2 MULE and Scanner Sweep usable even in Planetary Fortress Mode + planetary_orbital_module_data = get_full_item_list()[item_names.PLANETARY_FORTRESS_ORBITAL_MODULE] + accumulators[planetary_orbital_module_data.race][planetary_orbital_module_data.type.flag_word] += \ + 1 << planetary_orbital_module_data.number + + # Upgrades from completed missions + if ctx.generic_upgrade_missions > 0: + total_missions = sum(len(column) for campaign in ctx.custom_mission_order for layout in campaign.layouts for column in layout.missions) + num_missions = int((ctx.generic_upgrade_missions / 100) * total_missions) + completed = len([mission_id for mission_id in ctx.mission_id_to_location_ids if ctx.is_mission_completed(mission_id)]) + upgrade_count = min(completed // num_missions, ctx.max_upgrade_level) if num_missions > 0 else ctx.max_upgrade_level + upgrade_count = min(upgrade_count, WEAPON_ARMOR_UPGRADE_MAX_LEVEL) + + # Equivalent to "Progressive Weapon/Armor Upgrade" item + global_upgrades: typing.Set[str] = upgrade_included_names[GenericUpgradeItems.option_bundle_all] + for global_upgrade in global_upgrades: + race = get_full_item_list()[global_upgrade].race + upgrade_flaggroup = race_to_item_type[race]["Upgrade"].flag_word + for bundled_number in get_bundle_upgrade_member_numbers(global_upgrade): + accumulators[race][upgrade_flaggroup] += upgrade_count << bundled_number + + return accumulators + + +def get_bundle_upgrade_member_numbers(bundled_item: str) -> typing.List[int]: + upgrade_elements: typing.List[str] = upgrade_bundles[bundled_item] + if bundled_item in (item_names.PROGRESSIVE_PROTOSS_GROUND_UPGRADE, item_names.PROGRESSIVE_PROTOSS_AIR_UPGRADE): + # Shields are handled as a maximum of those two + upgrade_elements = [item_name for item_name in upgrade_elements if item_name != item_names.PROGRESSIVE_PROTOSS_SHIELDS] + return [get_full_item_list()[item_name].number for item_name in upgrade_elements] + + +def calc_difficulty(difficulty: int): + if difficulty == 0: + return 'C' + elif difficulty == 1: + return 'N' + elif difficulty == 2: + return 'H' + elif difficulty == 3: + return 'B' + + return 'X' + + +def get_kerrigan_level(ctx: SC2Context, items: typing.Dict[SC2Race, typing.List[int]], missions_beaten: int) -> int: + item_value = items[SC2Race.ZERG][ZergItemType.Level.flag_word] + mission_value = missions_beaten * ctx.kerrigan_levels_per_mission_completed + if ctx.kerrigan_levels_per_mission_completed_cap != -1: + mission_value = min(mission_value, ctx.kerrigan_levels_per_mission_completed_cap) + total_value = item_value + mission_value + if ctx.kerrigan_total_level_cap != -1: + total_value = min(total_value, ctx.kerrigan_total_level_cap) + return total_value + + +def calculate_kerrigan_options(ctx: SC2Context) -> int: + result = 0 + + # Bits 0, 1 + # Kerrigan unit available + if ctx.kerrigan_presence in kerrigan_unit_available: + result |= 1 << 0 + + # Bit 2 + # Kerrigan primal status by map + if ctx.kerrigan_primal_status == KerriganPrimalStatus.option_vanilla: + result |= 1 << 2 + + return result + + +def caclulate_soa_options(ctx: SC2Context, mission: SC2Mission) -> int: + """ + Pack SOA options into a single integer with bitflags. + 0b000011 = SOA presence + 0b000100 = SOA in no-builds + 0b011000 = Passives presence + 0b100000 = PAssives in no-builds + """ + result = 0 + + # Bits 0, 1 + # SoA Calldowns available + soa_presence_value = 0 + if is_mission_in_soa_presence(ctx.spear_of_adun_presence, mission): + soa_presence_value = 3 + result |= soa_presence_value << 0 + + # Bit 2 + # SoA Calldowns for no-builds + if ctx.spear_of_adun_present_in_no_build == SpearOfAdunPresentInNoBuild.option_true: + result |= 1 << 2 + + # Bits 3,4 + # Autocasts + soa_autocasts_presence_value = 0 + if is_mission_in_soa_presence(ctx.spear_of_adun_passive_ability_presence, mission, SpearOfAdunPassiveAbilityPresence): + soa_autocasts_presence_value = 3 + # Guardian Shell breaks without SoA on version 4+, but can be generated without SoA on version 3 + if ctx.slot_data_version < 4 and MissionFlag.Protoss in mission.flags: + soa_autocasts_presence_value = 3 + result |= soa_autocasts_presence_value << 3 + + # Bit 5 + # Autocasts in no-builds + if ctx.spear_of_adun_passive_present_in_no_build == SpearOfAdunPassivesPresentInNoBuild.option_true: + result |= 1 << 5 + + return result + +def calculate_generic_upgrade_options(ctx: SC2Context) -> int: + result = 0 + + # Bits 0,1 + # Research mode + research_mode_value = 0 + if ctx.generic_upgrade_research == GenericUpgradeResearch.option_vanilla: + research_mode_value = 0 + elif ctx.generic_upgrade_research == GenericUpgradeResearch.option_auto_in_no_build: + research_mode_value = 1 + elif ctx.generic_upgrade_research == GenericUpgradeResearch.option_auto_in_build: + research_mode_value = 2 + elif ctx.generic_upgrade_research == GenericUpgradeResearch.option_always_auto: + research_mode_value = 3 + result |= research_mode_value << 0 + + # Bit 2 + # Speedup + if ctx.generic_upgrade_research_speedup == GenericUpgradeResearchSpeedup.option_true: + result |= 1 << 2 + + return result + +def calculate_trade_options(ctx: SC2Context) -> int: + result = 0 + + # Bit 0 + # Trade enabled + if ctx.trade_enabled: + result |= 1 << 0 + + # Bit 1 + # Workers allowed + if ctx.trade_workers_allowed == VoidTradeWorkers.option_true: + result |= 1 << 1 + + return result + +def kerrigan_primal(ctx: SC2Context, kerrigan_level: int) -> bool: + if ctx.kerrigan_primal_status == KerriganPrimalStatus.option_always_zerg: + return True + elif ctx.kerrigan_primal_status == KerriganPrimalStatus.option_always_human: + return False + elif ctx.kerrigan_primal_status == KerriganPrimalStatus.option_level_35: + return kerrigan_level >= 35 + elif ctx.kerrigan_primal_status == KerriganPrimalStatus.option_half_completion: + total_missions = len(ctx.mission_id_to_location_ids) + completed = sum(ctx.is_mission_completed(mission_id) + for mission_id in ctx.mission_id_to_location_ids) + return completed >= (total_missions / 2) + elif ctx.kerrigan_primal_status == KerriganPrimalStatus.option_item: + codes = [item.item for item in ctx.items_received] + return get_full_item_list()[item_names.KERRIGAN_PRIMAL_FORM].code in codes + return False + + +def get_mission_variant(mission_id: int) -> int: + mission_flags = lookup_id_to_mission[mission_id].flags + if MissionFlag.RaceSwap not in mission_flags: + return 0 + if MissionFlag.Terran in mission_flags: + return 1 + elif MissionFlag.Zerg in mission_flags: + return 2 + elif MissionFlag.Protoss in mission_flags: + return 3 + return 0 + + +def get_item_flag_word(item_name: str) -> int: + return get_full_item_list()[item_name].type.flag_word + + +async def starcraft_launch(ctx: SC2Context, mission_id: int): + sc2_logger.info(f"Launching {lookup_id_to_mission[mission_id].mission_name}. If game does not launch check log file for errors.") + + with DllDirectory(None): + run_game( + bot.maps.get(lookup_id_to_mission[mission_id].map_file), + [Bot(Race.Terran, ArchipelagoBot(ctx, mission_id), name="Archipelago", fullscreen=not SC2World.settings.game_windowed_mode)], + realtime=True, + ) + + +class ArchipelagoBot(bot.bot_ai.BotAI): + __slots__ = [ + 'game_running', + 'mission_completed', + 'boni', + 'setup_done', + 'ctx', + 'mission_id', + 'want_close', + 'can_read_game', + 'last_received_update', + 'last_trade_cargo', + 'last_supply_used' + ] + ctx: SC2Context + # defined in bot_ai_internal.py; seems to be mis-annotated as a float and later re-annotated as an int + supply_used: int + + def __init__(self, ctx: SC2Context, mission_id: int): + self.game_running = False + self.mission_completed = False + self.want_close = False + self.can_read_game = False + self.last_received_update: int = 0 + self.last_trade_cargo: set = set() + self.last_supply_used: int = 0 + self.trade_reply_cooldown: int = 0 + self.setup_done = False + self.ctx = ctx + self.ctx.last_bot = self + self.mission_id = mission_id + self.boni = [False for _ in range(MAX_BONUS)] + + super(ArchipelagoBot, self).__init__() + + async def on_step(self, iteration: int): + if self.want_close: + self.want_close = False + await self._client.leave() + return + game_state = 0 + if not self.setup_done: + self.setup_done = True + mission = lookup_id_to_mission[self.mission_id] + start_items = calculate_items(self.ctx) + missions_beaten = self.missions_beaten_count() + kerrigan_level = get_kerrigan_level(self.ctx, start_items, missions_beaten) + kerrigan_options = calculate_kerrigan_options(self.ctx) + soa_options = caclulate_soa_options(self.ctx, mission) + generic_upgrade_options = calculate_generic_upgrade_options(self.ctx) + trade_options = calculate_trade_options(self.ctx) + mission_variant = get_mission_variant(self.mission_id) # 0/1/2/3 for unchanged/Terran/Zerg/Protoss + nova_fallback: bool + if MissionFlag.Nova in mission.flags: + nova_fallback = self.ctx.use_nova_nco_fallback + elif MissionFlag.WoLNova in mission.flags: + nova_fallback = self.ctx.use_nova_wol_fallback + else: + nova_fallback = False + uncollected_objectives: typing.List[int] = self.get_uncollected_objectives() + if self.ctx.difficulty_override >= 0: + difficulty = calc_difficulty(self.ctx.difficulty_override) + else: + difficulty = calc_difficulty(self.ctx.difficulty) + if self.ctx.game_speed_override >= 0: + game_speed = self.ctx.game_speed_override + else: + game_speed = self.ctx.game_speed + await self.chat_send( + "?SetOptions" + f" {difficulty}" + f" {generic_upgrade_options}" + f" {self.ctx.all_in_choice}" + f" {game_speed}" + f" {self.ctx.disable_forced_camera}" + f" {self.ctx.skip_cutscenes}" + f" {kerrigan_options}" + f" {self.ctx.grant_story_tech}" + f" {self.ctx.take_over_ai_allies}" + f" {soa_options}" + f" {self.ctx.mission_order}" + f" {int(nova_fallback)}" + f" {self.ctx.grant_story_levels}" + f" {self.ctx.enable_morphling}" + f" {mission_variant}" + f" {trade_options}" + f" {self.ctx.difficulty_damage_modifier}" + f" {self.ctx.mercenary_highlanders}" # TODO: Possibly rework it into unit options in the next cycle + f" {self.ctx.war_council_nerfs}" + ) + await self.update_resources(start_items) + await self.update_terran_tech(start_items) + await self.update_zerg_tech(start_items, kerrigan_level) + await self.update_protoss_tech(start_items) + await self.update_misc_tech(start_items) + await self.update_colors() + if uncollected_objectives: + await self.chat_send("?UncollectedLocations {}".format( + functools.reduce(lambda a, b: a + " " + b, [str(x) for x in uncollected_objectives]) + )) + await self.chat_send("?LoadFinished") + self.last_received_update = len(self.ctx.items_received) + + else: + if self.ctx.pending_color_update: + await self.update_colors() + + if not self.ctx.announcements.empty(): + message = self.ctx.announcements.get(timeout=1) + await self.chat_send("?SendMessage " + message) + self.ctx.announcements.task_done() + + # Archipelago reads the health + controller1_state = 0 + controller2_state = 0 + for unit in self.all_own_units(): + if unit.health_max == CONTROLLER_HEALTH: + controller1_state = int(CONTROLLER_HEALTH - unit.health) + self.can_read_game = True + elif unit.health_max == CONTROLLER2_HEALTH: + controller2_state = int(CONTROLLER2_HEALTH - unit.health) + self.can_read_game = True + elif unit.name == TRADE_UNIT: + # Handle Void Trade requests + # Check for orders (for buildings this is usually research or training) + if not unit.is_idle and not self.ctx.trade_underway: + button = unit.orders[0].ability.button_name + if button == TRADE_SEND_BUTTON and len(self.last_trade_cargo) > 0: + units_to_send: typing.List[str] = [] + non_ap_units: typing.Set[str] = set() + for passenger in self.last_trade_cargo: + # Alternatively passenger._type_data.name but passenger.name seems to always match + unit_name = passenger.name + if unit_name.startswith("AP_"): + units_to_send.append(normalized_unit_types.get(unit_name, unit_name)) + else: + non_ap_units.add(unit_name) + if len(non_ap_units) > 0: + sc2_logger.info(f"Void Trade tried to send non-AP units: {', '.join(non_ap_units)}") + self.ctx.trade_response = "?TradeFail Void Trade rejected: Trade contains invalid units." + self.ctx.trade_underway = True + else: + self.ctx.trade_response = None + self.ctx.trade_underway = True + async_start(self.ctx.trade_send(units_to_send)) + elif button == TRADE_RECEIVE_1_BUTTON: + self.ctx.trade_underway = True + if self.supply_used != self.last_supply_used: + self.ctx.trade_response = None + async_start(self.ctx.trade_receive(1)) + else: + self.ctx.trade_response = "?TradeFail Void Trade rejected: Not enough supply." + elif button == TRADE_RECEIVE_5_BUTTON: + self.ctx.trade_underway = True + if self.supply_used != self.last_supply_used: + self.ctx.trade_response = None + async_start(self.ctx.trade_receive(5)) + else: + self.ctx.trade_response = "?TradeFail Void Trade rejected: Not enough supply." + elif not unit.is_idle and self.trade_reply_cooldown > 0: + self.trade_reply_cooldown -= 1 + elif unit.is_idle and self.trade_reply_cooldown > 0: + self.trade_reply_cooldown = 0 + self.ctx.trade_response = None + self.ctx.trade_underway = False + else: + # The API returns no passengers for researching/training buildings, + # so we need to buffer the passengers each frame + self.last_trade_cargo = unit.passengers + # SC2 has no good means of detecting when a unit is queued while supply capped, + # so a supply buffer here is the best we can do + self.last_supply_used = self.supply_used + game_state = controller1_state + (controller2_state << 15) + + if iteration == 160 and not game_state & 1: + await self.chat_send("?SendMessage Warning: Archipelago unable to connect or has lost connection to " + + "Starcraft 2 (This is likely a map issue)") + + if self.last_received_update < len(self.ctx.items_received): + current_items = calculate_items(self.ctx) + missions_beaten = self.missions_beaten_count() + kerrigan_level = get_kerrigan_level(self.ctx, current_items, missions_beaten) + await self.update_resources(current_items) + await self.update_terran_tech(current_items) + await self.update_zerg_tech(current_items, kerrigan_level) + await self.update_protoss_tech(current_items) + await self.update_misc_tech(current_items) + self.last_received_update = len(self.ctx.items_received) + + if game_state & 1: + if not self.game_running: + print("Archipelago Connected") + self.game_running = True + + if self.can_read_game: + if game_state & (1 << 1) and not self.mission_completed: + victory_locations = [get_location_id(self.mission_id, 0)] + send_victory = ( + self.mission_id in self.ctx.final_mission_ids and + len(self.ctx.final_locations) == len(self.ctx.checked_locations.union(victory_locations).intersection(self.ctx.final_locations)) + ) + + # Old slots don't have locations on goal + if not send_victory or self.ctx.slot_data_version >= 4: + sc2_logger.info("Mission Completed") + location_ids = self.ctx.mission_id_to_location_ids[self.mission_id] + victory_locations += sorted([ + get_location_id(self.mission_id, location_id) + for location_id in location_ids + if (location_id % VICTORY_MODULO) >= VICTORY_CACHE_OFFSET + ]) + await self.ctx.send_msgs( + [{"cmd": 'LocationChecks', + "locations": victory_locations}]) + self.mission_completed = True + + if send_victory: + print("Game Complete") + await self.ctx.send_msgs([{"cmd": 'StatusUpdate', "status": ClientStatus.CLIENT_GOAL}]) + self.mission_completed = True + self.ctx.finished_game = True + + for x, completed in enumerate(self.boni): + if not completed and game_state & (1 << (x + 2)): + await self.ctx.send_msgs( + [{"cmd": 'LocationChecks', + "locations": [get_location_id(self.mission_id, x + 1)]}]) + self.boni[x] = True + + # Send Void Trade results + if self.ctx.trade_response is not None and self.trade_reply_cooldown == 0: + await self.chat_send(self.ctx.trade_response) + # Wait an arbitrary amount of frames before trying again + self.trade_reply_cooldown = 60 + else: + await self.chat_send("?SendMessage LostConnection - Lost connection to game.") + + def get_uncollected_objectives(self) -> typing.List[int]: + result = [ + location % VICTORY_MODULO + for location in self.ctx.uncollected_locations_in_mission(lookup_id_to_mission[self.mission_id]) + if (location % VICTORY_MODULO) < VICTORY_CACHE_OFFSET + ] + return result + + def missions_beaten_count(self) -> int: + return len([location for location in self.ctx.checked_locations if location % VICTORY_MODULO == 0]) + + async def update_colors(self): + await self.chat_send("?SetColor rr " + str(self.ctx.player_color_raynor)) + await self.chat_send("?SetColor ks " + str(self.ctx.player_color_zerg)) + await self.chat_send("?SetColor pz " + str(self.ctx.player_color_zerg_primal)) + await self.chat_send("?SetColor da " + str(self.ctx.player_color_protoss)) + await self.chat_send("?SetColor nova " + str(self.ctx.player_color_nova)) + self.ctx.pending_color_update = False + + async def update_resources(self, current_items: typing.Dict[SC2Race, typing.List[int]]): + DEFAULT_MAX_SUPPLY = 200 + max_supply_amount = max( + DEFAULT_MAX_SUPPLY + + ( + current_items[SC2Race.ANY][get_item_flag_word(item_names.MAX_SUPPLY)] + * self.ctx.maximum_supply_per_item + ) + - ( + current_items[SC2Race.ANY][get_item_flag_word(item_names.REDUCED_MAX_SUPPLY)] + * self.ctx.maximum_supply_reduction_per_item + ), + self.ctx.lowest_maximum_supply, + ) + await self.chat_send("?GiveResources {} {} {} {}".format( + current_items[SC2Race.ANY][get_item_flag_word(item_names.STARTING_MINERALS)], + current_items[SC2Race.ANY][get_item_flag_word(item_names.STARTING_VESPENE)], + current_items[SC2Race.ANY][get_item_flag_word(item_names.STARTING_SUPPLY)], + max_supply_amount - DEFAULT_MAX_SUPPLY, + )) + + async def update_terran_tech(self, current_items: typing.Dict[SC2Race, typing.List[int]]): + terran_items = current_items[SC2Race.TERRAN] + await self.chat_send("?GiveTerranTech " + " ".join(map(str, terran_items))) + + async def update_zerg_tech(self, current_items: typing.Dict[SC2Race, typing.List[int]], kerrigan_level: int): + zerg_items = current_items[SC2Race.ZERG] + zerg_items = [value for index, value in enumerate(zerg_items) if index not in [ZergItemType.Level.flag_word, ZergItemType.Primal_Form.flag_word]] + kerrigan_primal_by_items = kerrigan_primal(self.ctx, kerrigan_level) + kerrigan_primal_bot_value = 1 if kerrigan_primal_by_items else 0 + await self.chat_send(f"?GiveZergTech {kerrigan_level} {kerrigan_primal_bot_value} " + ' '.join(map(str, zerg_items))) + + async def update_protoss_tech(self, current_items: typing.Dict[SC2Race, typing.List[int]]): + protoss_items = current_items[SC2Race.PROTOSS] + await self.chat_send("?GiveProtossTech " + " ".join(map(str, protoss_items))) + + async def update_misc_tech(self, current_items: typing.Dict[SC2Race, typing.List[int]]): + await self.chat_send("?GiveMiscTech {} {} {}".format( + current_items[SC2Race.ANY][get_item_flag_word(item_names.BUILDING_CONSTRUCTION_SPEED)], + current_items[SC2Race.ANY][get_item_flag_word(item_names.UPGRADE_RESEARCH_SPEED)], + current_items[SC2Race.ANY][get_item_flag_word(item_names.UPGRADE_RESEARCH_COST)], + )) + +def calc_unfinished_nodes( + ctx: SC2Context +) -> typing.Tuple[typing.List[int], typing.Dict[int, typing.List[int]], typing.List[int], typing.Set[int]]: + unfinished_missions: typing.Set[int] = set() + + available_missions, available_layouts, available_campaigns = calc_available_nodes(ctx) + + for mission_id in available_missions: + objectives = set(ctx.locations_for_mission_id(mission_id)) + if objectives: + objectives_completed = ctx.checked_locations & objectives + if len(objectives_completed) < len(objectives): + unfinished_missions.add(mission_id) + + return available_missions, available_layouts, available_campaigns, unfinished_missions + +def is_mission_available(ctx: SC2Context, mission_id_to_check: int) -> bool: + available_missions, _, _ = calc_available_nodes(ctx) + + return mission_id_to_check in available_missions + +def calc_available_nodes(ctx: SC2Context) -> typing.Tuple[typing.List[int], typing.Dict[int, typing.List[int]], typing.List[int]]: + beaten_missions: typing.Set[int] = {mission_id for mission_id in ctx.mission_id_to_entry_rules if ctx.is_mission_completed(mission_id)} + received_items = compute_received_items(ctx) + + mission_order_objects: typing.List[MissionOrderObjectSlotData] = [] + parent_objects: typing.List[typing.List[MissionOrderObjectSlotData]] = [] + for campaign in ctx.custom_mission_order: + mission_order_objects.append(campaign) + parent_objects.append([]) + for layout in campaign.layouts: + mission_order_objects.append(layout) + parent_objects.append([campaign]) + for column in layout.missions: + for mission in column: + if mission.mission_id == -1: + continue + mission_order_objects.append(mission) + parent_objects.append([campaign, layout]) + + candidate_accessible_objects: typing.List[MissionOrderObjectSlotData] = [ + mission_order_object for mission_order_object in mission_order_objects + if mission_order_object.entry_rule.is_accessible(beaten_missions, received_items) + ] + + accessible_objects: typing.List[MissionOrderObjectSlotData] = [] + + while len(candidate_accessible_objects) > 0: + accessible_missions: typing.List[MissionSlotData] = [mission_order_object for mission_order_object in accessible_objects if isinstance(mission_order_object, MissionSlotData)] + beaten_accessible_missions: typing.Set[int] = {mission.mission_id for mission in accessible_missions if mission.mission_id in beaten_missions} + accessible_objects_to_add: typing.List[MissionOrderObjectSlotData] = [] + for mission_order_object in candidate_accessible_objects: + if ( + mission_order_object.entry_rule.is_accessible(beaten_accessible_missions, received_items) + and all([ + parent_object.entry_rule.is_accessible(beaten_accessible_missions, received_items) + for parent_object in parent_objects[mission_order_objects.index(mission_order_object)] + ]) + ): + accessible_objects_to_add.append(mission_order_object) + if len(accessible_objects_to_add) > 0: + accessible_objects.extend(accessible_objects_to_add) + candidate_accessible_objects = [ + mission_order_object for mission_order_object in candidate_accessible_objects + if mission_order_object not in accessible_objects_to_add + ] + else: + break + + accessible_missions: typing.List[MissionSlotData] = [mission_order_object for mission_order_object in accessible_objects if isinstance(mission_order_object, MissionSlotData)] + beaten_accessible_missions: typing.Set[int] = {mission.mission_id for mission in accessible_missions if mission.mission_id in beaten_missions} + for mission_order_object in mission_order_objects: + # re-generate tooltip accessibility + for sub_rule in mission_order_object.entry_rule.sub_rules: + sub_rule.was_accessible = False + mission_order_object.entry_rule.is_accessible(beaten_accessible_missions, received_items) + + available_missions: typing.List[int] = [ + mission_order_object.mission_id for mission_order_object in accessible_objects + if isinstance(mission_order_object, MissionSlotData) + ] + available_campaign_objects: typing.List[CampaignSlotData] = [ + mission_order_object for mission_order_object in accessible_objects + if isinstance(mission_order_object, CampaignSlotData) + ] + available_campaigns: typing.List[int] = [ + campaign_idx for campaign_idx, campaign in enumerate(ctx.custom_mission_order) + if campaign in available_campaign_objects + ] + available_layout_objects: typing.List[LayoutSlotData] = [ + mission_order_object for mission_order_object in accessible_objects + if isinstance(mission_order_object, LayoutSlotData) + ] + available_layouts: typing.Dict[int, typing.List[int]] = { + campaign_idx: [ + layout_idx for layout_idx, layout in enumerate(campaign.layouts) if layout in available_layout_objects + ] + for campaign_idx, campaign in enumerate(ctx.custom_mission_order) + } + + return available_missions, available_layouts, available_campaigns + +def compute_received_items(ctx: SC2Context) -> typing.Counter[int]: + received_items: typing.Counter[int] = collections.Counter() + for network_item in ctx.items_received: + received_items[network_item.item] += 1 + return received_items + +def check_game_install_path() -> bool: + # First thing: go to the default location for ExecuteInfo. + # An exception for Windows is included because it's very difficult to find ~\Documents if the user moved it. + if is_windows: + # The next five lines of utterly inscrutable code are brought to you by copy-paste from Stack Overflow. + # https://stackoverflow.com/questions/6227590/finding-the-users-my-documents-path/30924555# + import ctypes.wintypes + CSIDL_PERSONAL = 5 # My Documents + SHGFP_TYPE_CURRENT = 0 # Get current, not default value + + buf = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH) + ctypes.windll.shell32.SHGetFolderPathW(None, CSIDL_PERSONAL, None, SHGFP_TYPE_CURRENT, buf) + documentspath: str = buf.value + einfo = str(documentspath / Path("StarCraft II\\ExecuteInfo.txt")) + else: + einfo = str(bot.paths.get_home() / Path(bot.paths.USERPATH[bot.paths.PF])) + + # Check if the file exists. + if os.path.isfile(einfo): + + # Open the file and read it, picking out the latest executable's path. + with open(einfo) as f: + content = f.read() + if content: + search_result = re.search(r" = (.*)Versions", content) + if not search_result: + sc2_logger.warning(f"Found {einfo}, but it was empty. Run SC2 through the Blizzard launcher, " + "then try again.") + return False + base = search_result.group(1) + + if os.path.exists(base): + executable = bot.paths.latest_executeble(Path(base).expanduser() / "Versions") + + # Finally, check the path for an actual executable. + # If we find one, great. Set up the SC2PATH. + if os.path.isfile(executable): + sc2_logger.info(f"Found an SC2 install at {base}!") + sc2_logger.debug(f"Latest executable at {executable}.") + os.environ["SC2PATH"] = base + sc2_logger.debug(f"SC2PATH set to {base}.") + return True + else: + sc2_logger.warning(f"We may have found an SC2 install at {base}, but couldn't find {executable}.") + else: + sc2_logger.warning(f"{einfo} pointed to {base}, but we could not find an SC2 install there.") + else: + sc2_logger.warning(f"Couldn't find {einfo}. Run SC2 through the Blizzard launcher, then try again. " + f"If that fails, please run /set_path with your SC2 install directory.") + return False + + +def is_mod_installed_correctly() -> bool: + """Searches for all required files.""" + if "SC2PATH" not in os.environ: + check_game_install_path() + sc2_path: str = os.environ["SC2PATH"] + mapdir = sc2_path / Path('Maps/ArchipelagoCampaign') + mods = ["ArchipelagoCore", "ArchipelagoPlayer", "ArchipelagoPlayerSuper", "ArchipelagoPatches", + "ArchipelagoTriggers", "ArchipelagoPlayerWoL", "ArchipelagoPlayerHotS", + "ArchipelagoPlayerLotV", "ArchipelagoPlayerLotVPrologue", "ArchipelagoPlayerNCO"] + modfiles = [sc2_path / Path("Mods/" + mod + ".SC2Mod") for mod in mods] + wol_required_maps: typing.List[str] = ["WoL" + os.sep + mission.map_file + ".SC2Map" for mission in SC2Mission + if mission.campaign in (SC2Campaign.WOL, SC2Campaign.PROPHECY)] + hots_required_maps: typing.List[str] = ["HotS" + os.sep + mission.map_file + ".SC2Map" for mission in campaign_mission_table[SC2Campaign.HOTS]] + lotv_required_maps: typing.List[str] = ["LotV" + os.sep + mission.map_file + ".SC2Map" for mission in SC2Mission + if mission.campaign in (SC2Campaign.LOTV, SC2Campaign.PROLOGUE, SC2Campaign.EPILOGUE)] + nco_required_maps: typing.List[str] = ["NCO" + os.sep + mission.map_file + ".SC2Map" for mission in campaign_mission_table[SC2Campaign.NCO]] + required_maps = wol_required_maps + hots_required_maps + lotv_required_maps + nco_required_maps + needs_files = False + + # Check for maps. + missing_maps: typing.List[str] = [] + for mapfile in required_maps: + if not os.path.isfile(mapdir / mapfile): + missing_maps.append(mapfile) + if len(missing_maps) >= 19: + sc2_logger.warning(f"All map files missing from {mapdir}.") + needs_files = True + elif len(missing_maps) > 0: + for map in missing_maps: + sc2_logger.debug(f"Missing {map} from {mapdir}.") + sc2_logger.warning(f"Missing {len(missing_maps)} map files.") + needs_files = True + else: # Must be no maps missing + sc2_logger.debug(f"All maps found in {mapdir}.") + + # Check for mods. + for modfile in modfiles: + if os.path.isfile(modfile) or os.path.isdir(modfile): + sc2_logger.debug(f"Archipelago mod found at {modfile}.") + else: + sc2_logger.warning(f"Archipelago mod could not be found at {modfile}.") + needs_files = True + + # Final verdict. + if needs_files: + sc2_logger.warning("Required files are missing. Run /download_data to acquire them.") + return False + else: + sc2_logger.debug("All map/mod files are properly installed.") + return True + + +class DllDirectory: + # Credit to Black Sliver for this code. + # More info: https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setdlldirectoryw + _old: typing.Optional[str] = None + _new: typing.Optional[str] = None + + def __init__(self, new: typing.Optional[str]): + self._new = new + + def __enter__(self): + old = self.get() + if self.set(self._new): + self._old = old + + def __exit__(self, *args): + if self._old is not None: + self.set(self._old) + + @staticmethod + def get() -> typing.Optional[str]: + if sys.platform == "win32": + n = ctypes.windll.kernel32.GetDllDirectoryW(0, None) + buf = ctypes.create_unicode_buffer(n) + ctypes.windll.kernel32.GetDllDirectoryW(n, buf) + return buf.value + # NOTE: other OS may support os.environ["LD_LIBRARY_PATH"], but this fix is windows-specific + return None + + @staticmethod + def set(s: typing.Optional[str]) -> bool: + if sys.platform == "win32": + return ctypes.windll.kernel32.SetDllDirectoryW(s) != 0 + # NOTE: other OS may support os.environ["LD_LIBRARY_PATH"], but this fix is windows-specific + return False + + +def download_latest_release_zip( + owner: str, + repo: str, + api_version: str, + metadata: typing.Optional[str] = None, + force_download=False +) -> typing.Tuple[str, typing.Optional[str]]: + """Downloads the latest release of a GitHub repo to the current directory as a .zip file.""" + import requests + + headers = {"Accept": 'application/vnd.github.v3+json'} + url = f"https://api.github.com/repos/{owner}/{repo}/releases/tags/{api_version}" + + try: + r1 = requests.get(url, headers=headers) + if r1.status_code == 200: + latest_metadata = r1.json() + cleanup_downloaded_metadata(latest_metadata) + latest_metadata = str(latest_metadata) + # sc2_logger.info(f"Latest version: {latest_metadata}.") + else: + sc2_logger.warning(f"Status code: {r1.status_code}") + sc2_logger.warning("Failed to reach GitHub. Could not find download link.") + sc2_logger.warning(f"text: {r1.text}") + return "", metadata + + if (force_download is False) and (metadata == latest_metadata): + sc2_logger.info("Latest version already installed.") + return "", metadata + + sc2_logger.info(f"Attempting to download latest version of API version {api_version} of {repo}.") + download_url = r1.json()["assets"][0]["browser_download_url"] + + r2 = requests.get(download_url, headers=headers) + if r2.status_code == 200 and zipfile.is_zipfile(io.BytesIO(r2.content)): + tempdir = tempfile.gettempdir() + file = tempdir + os.sep + f"{repo}.zip" + with open(file, "wb") as fh: + fh.write(r2.content) + sc2_logger.info(f"Successfully downloaded {repo}.zip. Installing...") + return file, latest_metadata + else: + sc2_logger.warning(f"Status code: {r2.status_code}") + sc2_logger.warning("Download failed.") + sc2_logger.warning(f"text: {r2.text}") + return "", metadata + except requests.ConnectionError: + sc2_logger.warning("Failed to reach GitHub. Could not find download link.") + return "", metadata + + +def cleanup_downloaded_metadata(medatada_json: dict) -> None: + for asset in medatada_json['assets']: + del asset['download_count'] + + +def is_mod_update_available(owner: str, repo: str, api_version: str, metadata: str) -> bool: + import requests + + headers = {"Accept": 'application/vnd.github.v3+json'} + url = f"https://api.github.com/repos/{owner}/{repo}/releases/tags/{api_version}" + + try: + r1 = requests.get(url, headers=headers) + if r1.status_code == 200: + latest_metadata = r1.json() + cleanup_downloaded_metadata(latest_metadata) + latest_metadata = str(latest_metadata) + if metadata != latest_metadata: + return True + else: + return False + + else: + sc2_logger.warning("Failed to reach GitHub while checking for updates.") + sc2_logger.warning(f"Status code: {r1.status_code}") + sc2_logger.warning(f"text: {r1.text}") + return False + except requests.ConnectionError: + sc2_logger.warning("Failed to reach GitHub while checking for updates.") + return False + + +def get_location_offset(mission_id: int) -> int: + return SC2WOL_LOC_ID_OFFSET if mission_id <= SC2Mission.ALL_IN.id \ + else (SC2HOTS_LOC_ID_OFFSET - SC2Mission.ALL_IN.id * VICTORY_MODULO) + +def get_location_id(mission_id: int, objective_id: int) -> int: + return get_location_offset(mission_id) + mission_id * VICTORY_MODULO + objective_id + + +_has_forced_save = False +def force_settings_save_on_close() -> None: + """ + Settings has an existing auto-save feature, but it only triggers if a new key was introduced. + Force it to mark things as changed by introducing a new key and then cleaning up. + """ + global _has_forced_save + if _has_forced_save: + return + SC2World.settings.update({'invalid_attribute': True}) + del SC2World.settings.invalid_attribute + _has_forced_save = True + + +def launch(): + colorama.just_fix_windows_console() + asyncio.run(main()) + colorama.deinit() diff --git a/worlds/sc2/client_gui.py b/worlds/sc2/client_gui.py new file mode 100644 index 00000000..6b2abcd9 --- /dev/null +++ b/worlds/sc2/client_gui.py @@ -0,0 +1,655 @@ +from typing import * +import asyncio +import logging + +from BaseClasses import ItemClassification +from NetUtils import JSONMessagePart +from kvui import GameManager, HoverBehavior, ServerToolTip, KivyJSONtoTextParser, LogtoUI +from kivy.app import App +from kivy.clock import Clock +from kivy.core.clipboard import Clipboard +from kivy.uix.gridlayout import GridLayout +from kivy.lang import Builder +from kivy.metrics import dp +from kivy.uix.label import Label +from kivy.uix.button import Button +from kivymd.uix.menu import MDDropdownMenu +from kivymd.uix.tooltip import MDTooltip +from kivy.uix.scrollview import ScrollView +from kivy.properties import StringProperty, BooleanProperty, NumericProperty + +from .client import SC2Context, calc_unfinished_nodes, is_mission_available, compute_received_items, STARCRAFT2 +from .item.item_descriptions import item_descriptions +from .item.item_annotations import ITEM_NAME_ANNOTATIONS +from .mission_order.entry_rules import RuleData, SubRuleRuleData, ItemRuleData +from .mission_tables import lookup_id_to_mission, campaign_race_exceptions, \ + SC2Mission, SC2Race +from .locations import LocationType, lookup_location_id_to_type, lookup_location_id_to_flags +from .options import LocationInclusion, MissionOrderScouting +from . import SC2World + + +class HoverableButton(HoverBehavior, Button): + pass + + +class MissionButton(HoverableButton, MDTooltip): + tooltip_text = StringProperty("Test") + mission_id = NumericProperty(-1) + is_exit = BooleanProperty(False) + is_goal = BooleanProperty(False) + showing_tooltip = BooleanProperty(False) + + def __init__(self, *args, **kwargs): + super(HoverableButton, self).__init__(**kwargs) + self._tooltip = ServerToolTip(text=self.text, markup=True) + self._tooltip.padding = [5, 2, 5, 2] + + def on_enter(self): + self._tooltip.text = self.tooltip_text + + if self.tooltip_text != "": + self.display_tooltip() + + def on_leave(self): + self.remove_tooltip() + + def display_tooltip(self, *args): + self.showing_tooltip = True + return super().display_tooltip(*args) + + def remove_tooltip(self, *args): + self.showing_tooltip = False + return super().remove_tooltip(*args) + + @property + def ctx(self) -> SC2Context: + return App.get_running_app().ctx + +class CampaignScroll(ScrollView): + border_on = BooleanProperty(False) + +class MultiCampaignLayout(GridLayout): + pass + +class DownloadDataWarningMessage(Label): + pass + +class CampaignLayout(GridLayout): + pass + +class RegionLayout(GridLayout): + pass + +class ColumnLayout(GridLayout): + pass + +class MissionLayout(GridLayout): + pass + +class MissionCategory(GridLayout): + pass + + +class SC2JSONtoKivyParser(KivyJSONtoTextParser): + def _handle_item_name(self, node: JSONMessagePart): + item_name = node["text"] + if self.ctx.slot_info[node["player"]].game != STARCRAFT2 or item_name not in item_descriptions: + return super()._handle_item_name(node) + + flags = node.get("flags", 0) + item_types = [] + if flags & ItemClassification.progression: + item_types.append("progression") + if flags & ItemClassification.useful: + item_types.append("useful") + if flags & ItemClassification.trap: + item_types.append("trap") + if not item_types: + item_types.append("normal") + + # TODO: Some descriptions are too long and get cut off. Is there a general solution or does someone need to manually check every description? + desc = item_descriptions[item_name].replace(". \n", ".
").replace(". ", ".
").replace("\n", "
") + annotation = ITEM_NAME_ANNOTATIONS.get(item_name) + if annotation is not None: + desc = f"{annotation}
{desc}" + ref = "Item Class: " + ", ".join(item_types) + "

" + desc + node.setdefault("refs", []).append(ref) + return super(KivyJSONtoTextParser, self)._handle_item_name(node) + + def _handle_text(self, node: JSONMessagePart): + if node.get("keep_markup", False): + for ref in node.get("refs", []): + node["text"] = f"[ref={self.ref_count}|{ref}]{node['text']}[/ref]" + self.ref_count += 1 + return super(KivyJSONtoTextParser, self)._handle_text(node) + else: + return super()._handle_text(node) + + +class SC2Manager(GameManager): + base_title = "Archipelago Starcraft 2 Client" + + campaign_panel: Optional[MultiCampaignLayout] = None + campaign_scroll_panel: Optional[CampaignScroll] = None + last_checked_locations: Set[int] = set() + last_items_received: List[int] = [] + last_shown_tooltip: int = -1 + last_data_out_of_date = False + mission_buttons: List[MissionButton] = [] + launching: Union[bool, int] = False # if int -> mission ID + refresh_from_launching = True + first_check = True + first_mission = "" + button_colors: Dict[SC2Race, Tuple[float, float, float]] = {} + ctx: SC2Context + + def __init__(self, ctx: SC2Context) -> None: + super().__init__(ctx) + self.json_to_kivy_parser = SC2JSONtoKivyParser(ctx) + self.minimized = False + + def on_start(self) -> None: + from . import gui_config + warnings, window_width, window_height = gui_config.get_window_defaults() + from kivy.core.window import Window + original_size_x, original_size_y = Window.size + Window.size = window_width, window_height + Window.left -= max((window_width - original_size_x) // 2, 0) + Window.top -= max((window_height - original_size_y) // 2, 0) + # Add the logging handler manually here instead of using `logging_pairs` to avoid adding 2 unnecessary tabs + logging.getLogger("Starcraft2").addHandler(LogtoUI(self.log_panels["All"].on_log)) + for startup_warning in warnings: + logging.getLogger("Starcraft2").warning(f"Startup WARNING: {startup_warning}") + for race in (SC2Race.TERRAN, SC2Race.PROTOSS, SC2Race.ZERG): + errors, color = gui_config.get_button_color(race.name) + self.button_colors[race] = color + for error in errors: + logging.getLogger("Starcraft2").warning(f"{race.name.title()} button color setting: {error}") + + def clear_tooltip(self) -> None: + for button in self.mission_buttons: + button.remove_tooltip() + + def shown_tooltip(self) -> int: + for button in self.mission_buttons: + if button.showing_tooltip: + return button.mission_id + return -1 + + def build(self): + container = super().build() + + panel = self.add_client_tab("Starcraft 2 Launcher", CampaignScroll()) + self.campaign_scroll_panel = panel.content + self.campaign_panel = MultiCampaignLayout() + panel.content.add_widget(self.campaign_panel) + + Clock.schedule_interval(self.build_mission_table, 0.5) + + return container + + def build_mission_table(self, dt) -> None: + if 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() + return + + sorted_items_received = sorted([item.item for item in self.ctx.items_received]) + shown_tooltip = self.shown_tooltip() + hovering_tooltip = ( + self.last_shown_tooltip != -1 + and self.last_shown_tooltip == shown_tooltip + ) + data_changed = ( + self.last_checked_locations != self.ctx.checked_locations + or self.last_items_received != sorted_items_received + ) + needs_redraw = ( + data_changed + and not hovering_tooltip + or not self.refresh_from_launching + or self.last_data_out_of_date != self.ctx.data_out_of_date + or self.first_check + ) + self.last_shown_tooltip = shown_tooltip + if not needs_redraw: + return + + assert self.campaign_panel is not None + self.refresh_from_launching = True + + self.clear_tooltip() + self.campaign_panel.clear_widgets() + if self.ctx.data_out_of_date: + self.campaign_panel.add_widget(Label(text="", padding=[0, 5, 0, 5])) + warning_label = DownloadDataWarningMessage( + text="Map/Mod data is out of date. Run /download_data in the client", + padding=[0, 25, 0, 25], + ) + self.campaign_scroll_panel.border_on = True + self.campaign_panel.add_widget(warning_label) + else: + self.campaign_scroll_panel.border_on = False + self.last_data_out_of_date = self.ctx.data_out_of_date + if len(self.ctx.custom_mission_order) == 0: + self.campaign_panel.add_widget(Label(text="Connect to a world to see a mission layout here.")) + return + + self.last_checked_locations = self.ctx.checked_locations.copy() + self.last_items_received = sorted_items_received + self.first_check = False + + self.mission_buttons = [] + + available_missions, available_layouts, available_campaigns, unfinished_missions = calc_unfinished_nodes(self.ctx) + + # The MultiCampaignLayout widget needs a default height of 15 (set in the .kv) to display the above Labels correctly + multi_campaign_layout_height = 15 + + # Fetching IDs of all the locations with hints + self.hints_to_highlight = [] + hints = self.ctx.stored_data.get(f"_read_hints_{self.ctx.team}_{self.ctx.slot}") + if hints: + for hint in hints: + if hint['finding_player'] == self.ctx.slot and not hint['found']: + self.hints_to_highlight.append(hint['location']) + + MISSION_BUTTON_HEIGHT = 50 + MISSION_BUTTON_PADDING = 6 + for campaign_idx, campaign in enumerate(self.ctx.custom_mission_order): + longest_column = max(len(col) for layout in campaign.layouts for col in layout.missions) + if longest_column == 1: + campaign_layout_height = 115 + else: + campaign_layout_height = (longest_column + 2) * (MISSION_BUTTON_HEIGHT + MISSION_BUTTON_PADDING) + multi_campaign_layout_height += campaign_layout_height + campaign_layout = CampaignLayout(size_hint_y=None, height=campaign_layout_height) + campaign_layout.add_widget( + Label(text=campaign.name, size_hint_y=None, height=25, outline_width=1) + ) + mission_layout = MissionLayout(padding=[10,0,10,0]) + for layout_idx, layout in enumerate(campaign.layouts): + layout_panel = RegionLayout() + layout_panel.add_widget( + Label(text=layout.name, size_hint_y=None, height=25, outline_width=1)) + column_panel = ColumnLayout() + + for column in layout.missions: + category_panel = MissionCategory(padding=[3,MISSION_BUTTON_PADDING,3,MISSION_BUTTON_PADDING]) + + for mission in column: + mission_id = mission.mission_id + + # Empty mission slots + if mission_id == -1: + column_spacer = Label(text='', size_hint_y=None, height=MISSION_BUTTON_HEIGHT) + category_panel.add_widget(column_spacer) + continue + + mission_obj = lookup_id_to_mission[mission_id] + mission_finished = self.ctx.is_mission_completed(mission_id) + is_layout_exit = mission_id in layout.exits and not mission_finished + is_campaign_exit = mission_id in campaign.exits and not mission_finished + + text, tooltip = self.mission_text( + self.ctx, mission_id, mission_obj, + layout_idx, is_layout_exit, layout.name, + campaign_idx, is_campaign_exit, campaign.name, + available_missions, available_layouts, available_campaigns, unfinished_missions + ) + + mission_button = MissionButton(text=text, size_hint_y=None, height=MISSION_BUTTON_HEIGHT) + + mission_button.mission_id = mission_id + + if mission_id in self.ctx.final_mission_ids: + mission_button.is_goal = True + if is_layout_exit or is_campaign_exit: + mission_button.is_exit = True + + 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) + if race in self.button_colors: + mission_button.background_color = self.button_colors[race] + mission_button.tooltip_text = tooltip + mission_button.bind(on_press=self.mission_callback) + self.mission_buttons.append(mission_button) + category_panel.add_widget(mission_button) + + # layout_panel.add_widget(Label(text="")) + column_panel.add_widget(category_panel) + layout_panel.add_widget(column_panel) + mission_layout.add_widget(layout_panel) + campaign_layout.add_widget(mission_layout) + self.campaign_panel.add_widget(campaign_layout) + self.campaign_panel.height = multi_campaign_layout_height + + # For some reason the AP HoverBehavior won't send an enter event if a button spawns under the cursor, + # so manually send an enter event if a button is hovered immediately + for button in self.mission_buttons: + if button.hovered: + button.dispatch("on_enter") + break + + def mission_text( + self, ctx: SC2Context, mission_id: int, mission_obj: SC2Mission, + layout_id: int, is_layout_exit: bool, layout_name: str, campaign_id: int, is_campaign_exit: bool, campaign_name: str, + available_missions: List[int], available_layouts: Dict[int, List[int]], available_campaigns: List[int], + unfinished_missions: List[int] + ) -> Tuple[str, str]: + COLOR_MISSION_IMPORTANT = "6495ED" # blue + COLOR_MISSION_UNIMPORTANT = "A0BEF4" # lighter blue + COLOR_MISSION_CLEARED = "FFFFFF" # white + COLOR_MISSION_LOCKED = "A9A9A9" # gray + COLOR_PARENT_LOCKED = "848484" # darker gray + COLOR_MISSION_FINAL = "FFBC95" # orange + COLOR_MISSION_FINAL_LOCKED = "D0C0BE" # gray + orange + COLOR_FINAL_PARENT_LOCKED = "D0C0BE" # gray + orange + COLOR_FINAL_MISSION_REMINDER = "FF5151" # light red + COLOR_VICTORY_LOCATION = "FFC156" # gold + COLOR_TOOLTIP_DONE = "51FF51" # light green + COLOR_TOOLTIP_NOT_DONE = "FF5151" # light red + + text = mission_obj.mission_name + tooltip: str = "" + remaining_locations, plando_locations, remaining_count = self.sort_unfinished_locations(mission_id) + campaign_locked = campaign_id not in available_campaigns + layout_locked = layout_id not in available_layouts[campaign_id] + + # Map has uncollected locations + if mission_id in unfinished_missions: + if self.any_valuable_locations(remaining_locations): + text = f"[color={COLOR_MISSION_IMPORTANT}]{text}[/color]" + else: + text = f"[color={COLOR_MISSION_UNIMPORTANT}]{text}[/color]" + elif mission_id in available_missions: + text = f"[color={COLOR_MISSION_CLEARED}]{text}[/color]" + # Map requirements not met + else: + mission_rule, layout_rule, campaign_rule = ctx.mission_id_to_entry_rules[mission_id] + mission_has_rule = mission_rule.amount > 0 + layout_has_rule = layout_rule.amount > 0 + extra_reqs = False + if campaign_locked: + text = f"[color={COLOR_PARENT_LOCKED}]{text}[/color]" + tooltip += "To unlock this campaign, " + shown_rule = campaign_rule + extra_reqs = layout_has_rule or mission_has_rule + elif layout_locked: + text = f"[color={COLOR_PARENT_LOCKED}]{text}[/color]" + tooltip += "To unlock this questline, " + shown_rule = layout_rule + extra_reqs = mission_has_rule + else: + text = f"[color={COLOR_MISSION_LOCKED}]{text}[/color]" + tooltip += "To unlock this mission, " + shown_rule = mission_rule + rule_tooltip = shown_rule.tooltip(0, lookup_id_to_mission, COLOR_TOOLTIP_DONE, COLOR_TOOLTIP_NOT_DONE) + tooltip += rule_tooltip.replace(rule_tooltip[0], rule_tooltip[0].lower(), 1) + extra_word = "are" + if shown_rule.shows_single_rule(): + extra_word = "is" + tooltip += "." + if extra_reqs: + tooltip += f"\nThis mission has additional requirements\nthat will be shown once the above {extra_word} met." + + # Mark exit missions + exit_for: str = "" + if is_layout_exit: + exit_for += layout_name if layout_name else "this questline" + if is_campaign_exit: + if exit_for: + exit_for += " and " + exit_for += campaign_name if campaign_name else "this campaign" + if exit_for: + if tooltip: + tooltip += "\n\n" + tooltip += f"Required to beat {exit_for}" + + # Mark goal missions + if mission_id in self.ctx.final_mission_ids: + if mission_id in available_missions: + text = f"[color={COLOR_MISSION_FINAL}]{mission_obj.mission_name}[/color]" + elif campaign_locked or layout_locked: + text = f"[color={COLOR_FINAL_PARENT_LOCKED}]{mission_obj.mission_name}[/color]" + else: + text = f"[color={COLOR_MISSION_FINAL_LOCKED}]{mission_obj.mission_name}[/color]" + if tooltip and not exit_for: + tooltip += "\n\n" + elif exit_for: + tooltip += "\n" + if any(location_type == LocationType.VICTORY for (location_type, _, _) in remaining_locations): + tooltip += f"[color={COLOR_FINAL_MISSION_REMINDER}]Required to beat the world[/color]" + else: + tooltip += "This goal mission is already beaten.\nBeat the remaining goal missions to beat the world." + + # Populate remaining location list + if remaining_count > 0: + if tooltip: + tooltip += "\n\n" + tooltip += f"[b][color={COLOR_MISSION_IMPORTANT}]Uncollected locations[/color][/b]" + last_location_type = LocationType.VICTORY + victory_printed = False + + if self.ctx.mission_order_scouting != MissionOrderScouting.option_none: + mission_available = mission_id in available_missions + + scoutable = self.is_scoutable(remaining_locations, mission_available, layout_locked, campaign_locked) + else: + scoutable = False + + for location_type, location_name, _ in remaining_locations: + if location_type in (LocationType.VICTORY, LocationType.VICTORY_CACHE) and victory_printed: + continue + if location_type != last_location_type: + tooltip += f"\n[color={COLOR_MISSION_IMPORTANT}]{self.get_location_type_title(location_type)}:[/color]" + last_location_type = location_type + if location_type == LocationType.VICTORY: + victory_count = len([loc for loc in remaining_locations if loc[0] in (LocationType.VICTORY, LocationType.VICTORY_CACHE)]) + victory_loc = location_name.replace(":", f":[color={COLOR_VICTORY_LOCATION}]") + if victory_count > 1: + victory_loc += f' ({victory_count})' + tooltip += f"\n- {victory_loc}[/color]" + victory_printed = True + else: + tooltip += f"\n- {location_name}" + if scoutable: + tooltip += self.handle_scout_display(location_name) + if len(plando_locations) > 0: + tooltip += "\n[b]Plando:[/b]\n- " + tooltip += "\n- ".join(plando_locations) + + tooltip = f"[b]{text}[/b]\n" + tooltip + + #If the mission has any hints pointing to a check, add asterisks around the mission name + if any(tuple(x in self.hints_to_highlight for x in self.ctx.locations_for_mission_id(mission_id))): + text = "* " + text + " *" + + return text, tooltip + + + def mission_callback(self, button: MissionButton) -> None: + if button.last_touch.button == 'right': + self.open_mission_menu(button) + return + if not self.launching: + mission_id: int = button.mission_id + if self.ctx.play_mission(mission_id): + self.launching = mission_id + Clock.schedule_once(self.finish_launching, 10) + + def open_mission_menu(self, button: MissionButton) -> None: + # Will be assigned later, used to close menu in callbacks + menu = None + mission_id = button.mission_id + + def copy_mission_name(): + Clipboard.copy(lookup_id_to_mission[mission_id].mission_name) + menu.dismiss() + + menu_items = [ + { + "text": "Copy Mission Name", + "on_release": copy_mission_name, + } + ] + width_override = None + + hinted_item_ids = Counter() + hints = self.ctx.stored_data.get(f"_read_hints_{self.ctx.team}_{self.ctx.slot}") + if hints: + for hint in hints: + if hint['receiving_player'] == self.ctx.slot and not hint['found']: + hinted_item_ids[hint['item']] += 1 + + if not self.ctx.is_mission_completed(mission_id) and not is_mission_available(self.ctx, mission_id): + # Uncompleted and inaccessible missions can have items hinted if they're needed + # The inaccessible restriction is to ensure users don't waste hints on missions that they can already access + items_needed = self.resolve_items_needed(mission_id) + received_items = compute_received_items(self.ctx) + for item_id, amount in items_needed.items(): + # If we have already received or hinted enough of this item, skip it + if received_items[item_id] + hinted_item_ids[item_id] >= amount: + continue + if width_override is None: + width_override = dp(500) + item_name = self.ctx.item_names.lookup_in_game(item_id) + label_text = f"Hint Required Item: {item_name}" + + def hint_and_close(): + self.ctx.command_processor(self.ctx)(f"!hint {item_name}") + menu.dismiss() + + menu_items.append({ + "text": label_text, + "on_release": hint_and_close, + }) + + menu = MDDropdownMenu( + caller=button, + items=menu_items, + **({"width": width_override} if width_override else {}), + ) + menu.open() + + def resolve_items_needed(self, mission_id: int) -> Counter[int]: + def resolve_rule_to_items(rule: RuleData) -> Counter[int]: + if isinstance(rule, SubRuleRuleData): + all_items = Counter() + for sub_rule in rule.sub_rules: + # Take max of each item across all sub-rules + all_items |= resolve_rule_to_items(sub_rule) + return all_items + elif isinstance(rule, ItemRuleData): + return Counter(rule.item_ids) + else: + return Counter() + + rules = self.ctx.mission_id_to_entry_rules[mission_id] + # Take max value of each item across all rules using '|' + return (resolve_rule_to_items(rules.mission_rule) | + resolve_rule_to_items(rules.layout_rule) | + resolve_rule_to_items(rules.campaign_rule)) + + def finish_launching(self, dt): + self.launching = False + + def sort_unfinished_locations(self, mission_id: int) -> Tuple[List[Tuple[LocationType, str, int]], List[str], int]: + locations: List[Tuple[LocationType, str, int]] = [] + location_name_to_index: Dict[str, int] = {} + for loc in self.ctx.locations_for_mission_id(mission_id): + if loc in self.ctx.missing_locations: + location_name = self.ctx.location_names.lookup_in_game(loc) + location_name_to_index[location_name] = len(locations) + locations.append(( + lookup_location_id_to_type[loc], + location_name, + loc, + )) + count = len(locations) + + plando_locations = [] + elements_to_remove: Set[Tuple[LocationType, str, int]] = set() + for plando_loc_name in self.ctx.plando_locations: + if plando_loc_name in location_name_to_index: + elements_to_remove.add(locations[location_name_to_index[plando_loc_name]]) + plando_locations.append(plando_loc_name) + for element in elements_to_remove: + locations.remove(element) + + return sorted(locations), plando_locations, count + + def any_valuable_locations(self, locations: List[Tuple[LocationType, str, int]]) -> bool: + for location_type, _, location_id in locations: + if (self.ctx.location_inclusions[location_type] == LocationInclusion.option_enabled + and all( + self.ctx.location_inclusions_by_flag[flag] == LocationInclusion.option_enabled + for flag in lookup_location_id_to_flags[location_id].values() + ) + ): + 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_filler: + title += " (Filler)" + else: + title += "" + return title + + def is_scoutable(self, remaining_locations, mission_available: bool, layout_locked: bool, campaign_locked: bool) -> bool: + if self.ctx.mission_order_scouting == MissionOrderScouting.option_all: + return True + elif self.ctx.mission_order_scouting == MissionOrderScouting.option_campaign and not campaign_locked: + return True + elif self.ctx.mission_order_scouting == MissionOrderScouting.option_layout and not layout_locked: + return True + elif self.ctx.mission_order_scouting == MissionOrderScouting.option_available and mission_available: + return True + elif self.ctx.mission_order_scouting == MissionOrderScouting.option_completed and len([loc for loc in remaining_locations if loc[0] in (LocationType.VICTORY, LocationType.VICTORY_CACHE)]) == 0: + # Assuming that when a mission is completed, all victory location are removed + return True + else: + return False + + def handle_scout_display(self, location_name: str) -> str: + if self.ctx.mission_item_classification is None: + return "" + # Only one information is provided for the victory locations of a mission + if " Cache (" in location_name: + location_name = location_name.split(" Cache")[0] + item_classification_key = self.ctx.mission_item_classification[location_name] + if ((ItemClassification.progression & item_classification_key) + and (ItemClassification.useful & item_classification_key) + ): + # Uncommon, but some games do this to show off that an item is super-important + # This can also happen on a victory display if the cache holds both progression and useful + return " [color=AF99EF](Useful+Progression)[/color]" + if ItemClassification.progression & item_classification_key: + return " [color=AF99EF](Progression)[/color]" + if ItemClassification.useful & item_classification_key: + return " [color=6D8BE8](Useful)[/color]" + if SC2World.settings.show_traps and ItemClassification.trap & item_classification_key: + return " [color=FA8072](Trap)[/color]" + return " [color=00EEEE](Filler)[/color]" + + +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) diff --git a/worlds/sc2/docs/contributors.md b/worlds/sc2/docs/contributors.md index 5b62466d..b1e7e655 100644 --- a/worlds/sc2/docs/contributors.md +++ b/worlds/sc2/docs/contributors.md @@ -1,19 +1,66 @@ # Contributors -Contibutors are listed with preferred or Discord names first, with github usernames prepended with an `@` +Contributors are listed with preferred or Discord names first, with GitHub usernames prepended with an `@`. +Within an update, contributors for earlier sections are not repeated for their contributions in later sections; +code contributors also reported bugs and participated in beta testing. -## Update 2024.0 +## Update 2025 ### Code Changes * Ziktofel (@Ziktofel) * Salzkorn (@Salzkorn) * EnvyDragon (@EnvyDragon) -* Phanerus (@MatthewMarinets) +* Phaneros (@MatthewMarinets) +* Magnemania (@Magnemania) +* Bones (@itsjustbones) +* Gemster (@Gemster312) +* SirChuckOfTheChuckles (@SirChuckOfTheChuckles) +* Snarky (@Snarky) +* MindHawk (@MindHawk) +* Cristall (@Cristall) +* WaikinDN (@WaikinDN) +* blorp77 (@blorp77) +* Dikhovinka (@AYaroslavskiy91) +* Subsourian (@Subsourian) + +### Additional Assets +* Alice Voltaire + +### Voice Acting +@-handles in this section are social media contacts rather than specifically GitHub in this section. + +* Subsourian (@Subsourian) - Signifier, Slayer +* GiantGrantGames (@GiantGrantGames) - Trireme +* Phaneros (@MatthewMarinets)- Skirmisher +* Durygathn - Dawnbringer +* 7thAce (@7thAce) - Pulsar +* Panicmoon (@panicmoon.bsky.social) - Skylord +* JayborinoPlays (@Jayborino) - Oppressor + +## Maintenance of 2024 release +* Ziktofel (@Ziktofel) +* Phaneros (@MatthewMarinets) +* Salzkorn (@Salzkorn) +* neocerber (@neocerber) +* Alchav (@Alchav) +* Berserker (@Berserker66) +* Exempt-Medic (@Exempt-Medic) + +And many members of the greater Archipelago community for core changes that affected the StarCraft 2 apworld. + +## Update 2024 +### Code Changes +* Ziktofel (@Ziktofel) +* Salzkorn (@Salzkorn) +* EnvyDragon (@EnvyDragon) +* Phaneros (@MatthewMarinets) * Madi Sylveon (@MadiMadsen) * Magnemania (@Magnemania) * Subsourian (@Subsourian) +* neocerber (@neocerber) * Hopop (@hopop201) * Alice Voltaire (@AliceVoltaire) * Genderdruid (@ArchonofFail) * CrazedCollie (@FoxOfWar) +* Bones (@itsjustbones) ### Additional Beta testing and bug reports * Varcklen (@Varcklen) diff --git a/worlds/sc2/docs/custom_mission_orders_en.md b/worlds/sc2/docs/custom_mission_orders_en.md new file mode 100644 index 00000000..6aba753b --- /dev/null +++ b/worlds/sc2/docs/custom_mission_orders_en.md @@ -0,0 +1,1092 @@ +# Custom Mission Orders for Starcraft 2 + +
+ Table of Contents + +- [Custom Mission Orders for Starcraft 2](#custom-mission-orders-for-starcraft-2) + - [What is this?](#what-is-this) + - [Basic structure](#basic-structure) + - [Interactions with other YAML options](#interactions-with-other-yaml-options) + - [Instructions for building a mission order](#instructions-for-building-a-mission-order) + - [Shared options](#shared-options) + - [Display Name](#display-name) + - [Unique name](#unique-name) + - [Goal](#goal) + - [Exit](#exit) + - [Entry rules](#entry-rules) + - [Unique progression track](#unique-progression-track) + - [Difficulty](#difficulty) + - [Mission Pool](#mission-pool) + - [Campaign Options](#campaign-options) + - [Preset](#preset) + - [Campaign Presets](#campaign-presets) + - [Static Presets](#static-presets) + - [Preset Options](#preset-options) + - [Missions](#missions) + - [Shuffle Raceswaps](#shuffle-raceswaps) + - [Keys](#keys) + - [Golden Path](#golden-path) + - [Layout Options](#layout-options) + - [Type](#type) + - [Size](#size) + - [Missions](#missions-1) + - [Mission Slot Options](#mission-slot-options) + - [Entrance](#entrance) + - [Empty](#empty) + - [Next](#next) + - [Victory Cache](#victory-cache) + - [Layout Types](#layout-types) + - [Column](#column) + - [Grid](#grid) + - [Grid Index Functions](#grid-index-functions) + - [point(x, y)](#pointx-y) + - [rect(x, y, width, height)](#rectx-y-width-height) + - [Canvas](#canvas) + - [Canvas Index Functions](#canvas-index-functions) + - [group(character)](#groupcharacter) + - [Hopscotch](#hopscotch) + - [Hopscotch Index Functions](#hopscotch-index-functions) + - [top](#top) + - [bottom](#bottom) + - [middle](#middle) + - [corner(index)](#cornerindex) + - [Gauntlet](#gauntlet) + - [Blitz](#blitz) + - [Blitz Index Functions](#blitz-index-functions) + - [row(height)](#rowheight) +
+ +## What is this? + +This is usage documentation for the `custom_mission_order` YAML option for Starcraft 2. You can enable Custom Mission Orders by setting `mission_order: custom` in your YAML. + +You will need to know how to write a YAML before engaging with this feature, and should read the [Archipelago YAML documentation](https://archipelago.gg/tutorial/Archipelago/advanced_settings/en) before continuing here. + +Every example in this document should be valid to generate. + +## Basic structure + +Custom Mission Orders consist of three kinds of structures: +- The mission order itself contains campaigns (like Wings of Liberty) +- Campaigns contain layouts (like Mar Sara) +- Layouts contain mission slots (like Liberation Day) + +As a note, layouts are also called questlines in the UI. Layouts and questlines refer to the same thing, though this document will only use layouts. + +To illustrate, the following is what the default custom mission order currently looks like. If you're not sure what some options mean, they will be explained in more depth later. +```yaml + custom_mission_order: + # This is a campaign, defined by its name + Default Campaign: + # The campaign's name as displayed in the client + display_name: "null" + # Whether this campaign must have a unique name in the client + unique_name: false + # Conditions that must be fulfilled to access this campaign + entry_rules: [] + # Whether beating this campaign is part of the world's goal + goal: true + # The lowest difficulty of missions in this campaign + min_difficulty: relative + # The highest difficulty of missions in this campaign + max_difficulty: relative + # This is a special layout that defines defaults + # for other layouts in the campaign + global: + # The layout's name as displayed in the client + display_name: "null" + # Whether this layout must have a unique name in the client + unique_name: false + # Whether beating this layout is part of the world's goal + goal: false + # Whether this layout must be beaten to beat the campaign + exit: false + # Conditions that must be fulfilled to access this layout + entry_rules: [] + # Which missions are allowed to appear in this layout + mission_pool: + - all missions + # The lowest difficulty of missions in this layout + min_difficulty: relative + # The highest difficulty of missions in this layout + max_difficulty: relative + # Used for overwriting default options of mission slots, + # which are set by the layout type (see Default Layout) + missions: [] + # This is a regular layout, defined by its name + Default Layout: + # This defines how missions in the layout are organized, + # as well as how they connect to one another + type: grid + # How many total missions should appear in this layout + size: 9 +``` +This default option also defines default values (though you won't get the Default Campaign and Default Layout), so you can omit the options you don't want to change in your own YAML. + +Notably however, layouts are required to have both a `type` and a `size`, but neither have defaults. You must define both of them for every layout, either through your own `global` layout, or in the options of every individual layout. + +If you want multiple campaigns or layouts, it would look like this: +```yaml + custom_mission_order: + My first campaign!: + # Campaign options here + global: # Can be omitted if the above defaults work for you + # Makes all the other layouts only have Terran missions + mission_pool: + - terran missions + # Other layout options here + My first layout: + # Defining at least type and size of a layout is mandatory + type: column + size: 3 + # Other layout options here + my second layout: + type: grid + size: 4 + layout number 3: + type: column + size: 3 + # etc. + Second campaign: + the other first layout: + type: grid + size: 10 + # etc. +``` +If you don't want to have a campaign container for your layouts, you can also forego the campaign layer like this: +```yaml + custom_mission_order: + Example campaign-level layout: + # Make sure to always declare these two, like with regular layouts + type: column + size: 3 + + # Regular campaigns and campaign-less layouts + # can be mixed however you want + Some Campaign: + Some Layout: + type: column + size: 3 +``` +It is also possible to access mission slots by their index, which is defined by the type of the layout they are in. The below shows an example of how to access a mission slot, as well as the defaults for their options. + +However, keep in mind that layout types will set their own options for specific slots, overwriting the below defaults, and using this option in turn overwrites the values set by layout types. As before, the options are explained in more depth later. +```yaml + custom_mission_order: + My Campaign: + My Layout: + type: column + size: 5 + missions: + # 0 is often the layout's starting mission + # Any index between 0 and (size - 1) is accessible + - index: 0 + # Whether this mission is part of the world's goal + goal: false + # Whether this mission is accessible as soon as the + # layout is accessible + entrance: false + # Whether this mission is required to beat the layout + exit: false + # Whether this slot contains a mission at all + empty: false + # Conditions that must be fulfilled to access this mission + entry_rules: [] + # Which missions in the layout are unlocked by this mission + # This is normally set by the layout's type + next: [] + # Which missions are allowed to appear in this slot + # If not defined, the slot inherits the layout's pool + mission_pool: + - all missions + # Which specific difficulty this mission should have + difficulty: relative +``` +## Interactions with other YAML options + +Custom Mission Orders respect all the options that change which missions can appear as if the options' relevant missions had been excluded. For example, `selected_races: protoss` is equivalent to excluding all Zerg and Terran missions, and `enabled_campaigns: ["Wings of Liberty"]` is equivalent to excluding all but WoL missions. + +This means that if you want total control over available missions in your mission order via `mission_pool`s, you should enable all races and campaigns and leave your `excluded_missions` list empty, but you can also use these options to get rid of particular missions you never want and can then ignore those missions in your `mission_pool`s. + +There are, however, several options that are ignored by Custom Mission Orders: +- `mission_order`, because it has to be `custom` for your Custom Mission Order to apply +- `maximum_campaign_size`, because you determine the size of the mission order via layout `size` attributes +- `two_start_positions`, which you can instead determine in individual layouts of the appropriate `type`s (see Grid and Hopscotch sections below) +- `key_mode`, which you can still specify for presets (see Campaign Presets section), and can otherwise manually set up using Item entry rules + +## Instructions for building a mission order + +Normally when you play a Starcraft 2 world, you have a table of missions in the Archipelago SC2 Client, and hovering over a mission tells you what missions are required to access it. This is still true for custom mission orders, but you now have control over the way missions are visually organized, as well as their access requirements. + +This section is meant to offer some guidance when making your own mission order for the first time. + +To begin making your own mission order, think about how you visually want your missions laid out. This should inform the layout `type`s you want to use, and give you some idea about the overall structure of your mission order. + +For example, if you want to make a custom campaign like the vanilla ones, you will want a lot of layouts of [`type: column`](#column). If you want a Hopscotch layout with certain missions or races, a single layout with [`type: hopscotch`](#hopscotch) will suffice. If you want to play through a funny shape, you will want to draw with a [`type: canvas`](#canvas). If you just want to make a minor change to a vanilla campaign, you will want to start with a [`preset` campaign](#preset). + +The natural flow of a mission order is defined by the types of its layouts. It makes sense for a mission to unlock its neighbors, it makes sense for a Hopscotch layout to wrap around the sides, and it makes sense for a Column's final mission to be at the bottom. Layout types create their flow by setting [`next`](#next), [`entrance`](#entrance), [`exit`](#exit), and [`entry_rules`](#entry-rules) on missions. More on these in a little bit. + +Layout types dictate their own visual structure, and will only rarely make mission slots with `empty: true`. If you want a certain shape that's not exactly like an existing type, you can pick a type with more slots than you want and remove the extras by setting `empty: true` on them. + +With the basic setup in place, you should decide on what the goal of your mission order is. By default every campaign has `goal: true`, meaning all campaigns must be beaten to complete the world. You can additionally set `goal: true` on layouts and mission slots to require them to be beaten as well. If you set `goal: false` on everything, the mission order will default to setting the last campaign (lowest in your YAML) as the goal. + +After deciding on a goal, you can complicate your way towards it. At the start of a world, the only accessible missions in the mission order are all the missions marked `entrance: true`. When you beat one of these missions, it unlocks all the missions in the beaten mission's `next` list. This process repeats until all the missions are accessible. + +If this behavior isn't enough for your planned mission order, you can interrupt the natural flow of layout types using `entry_rules` in combination with `exit`. + +When this document refers to "beating" something, it means the following: +- A mission is beaten if it is accessible and its victory location is checked. +- Beating a layout means beating all the missions in the layout with `exit: true` +- Beating a campaign means beating all the layouts in the campaign with `exit: true` + +Note victory checks may be claimed by someone else running `!collect` in a multiworld and receiving an item on a victory check. Collecting victory cache checks do not count, only victory checks. + +Layouts will have their default exit missions set by the layout type. If you don't want to use this default, you will have to manually set `exit: false` on the default exits. Campaigns default to using the last layout in them (the lowest in your YAML) as their exit, but only if you don't manually set `exit: true` on a layout. + +Using `entry_rules`, you can make a mission require beating things other than those missions whose `next` points to it, and you can make layouts and campaigns not available from the start. + +Note that `entry_rules` are an addition to the `next` behavior. If you want a mission to completely ignore the natural flow and only use your `entry_rules`, simply set `entrance: true` on it. + +Please see the [`entry_rules`](#entry-rules) section below for available rules and examples. + +With your playthrough sufficiently complicated, it only remains to add flavor to your mission order by changing [`mission_pool`](#mission-pool) and [`difficulty`](#difficulty) options as you like them. These options are also explained below. + +To summarize: +- Start by setting up campaigns and layouts with appropriate layout `type`s and `size`s +- Decide the mission order's `goal`s +- Customize access requirements as desired: + - Use `entrance`, `next`, and `empty` on mission slots to change the unlocking order of missions within a layout + - Use `entry_rules` in combination with `exit` to add additional restrictions to missions, layouts, and campaigns +- Use the `mission_pool` and `difficulty` options to add flavor +- Finally, generate and have fun! + +## Shared options + +These are the options that are shared between at least two of campaigns, layouts and missions. All the options below are listed with their defaults. + +--- +### Display Name +```yaml +# For campaigns and layouts +display_name: "null" +``` +As shown in the examples, every campaign and layout is defined with a name in your YAML. This name is used to find campaigns and layouts within the mission order (see `entry_rules` section), and by default (meaning with `display_name: "null"`) it is also shown in the client. + +This option changes the name shown in the client without affecting the definition name. + +There are two special use cases for this option: +```yaml +# This means the campaign or layout +# will not have a title in the client +display_name: "" +``` +```yaml +# This will randomly pick a name from the given list of options +display_name: + - My First Choice + - My Second Choice + - My Third Choice +``` + +--- +### Unique name +```yaml +# For campaigns and layouts +unique_name: false +``` +This option prevents names from showing up multiple times in the client. It is recommended to be used in combination with lists of `display_name`s to prevent the generator from picking duplicate names. + +--- +### Goal +```yaml +# For campaigns +goal: true +``` +```yaml +# For layouts and missions +goal: false +``` +This determines whether the campaign, layout or mission is required to beat the world. If you turn this off for everything, the last defined campaign (meaning the lowest one in your YAML) is chosen by default. + +--- +### Exit +```yaml +# For layouts and missions +exit: false +``` +This determines whether beating the mission is required to beat its parent layout, and whether beating the layout is required to beat its parent campaign. + +--- +### Entry rules +```yaml +# For campaigns, layouts, and missions +entry_rules: [] +``` +This defines access restrictions for parts of the mission order. + +These are the available rules: +```yaml +entry_rules: + # Beat these things ("Beat rule") + - scope: [] + # Beat X amount of missions from these things ("Count rule") + - scope: [] + amount: -1 + # Find these items ("Item rule") + - items: {} + # Fulfill X amount of other conditions ("Subrule rule") + - rules: [] + amount: -1 +``` +Note that Item rules take both a name and amount for each item (see the example below). In general this rule treats items like the `locked_items` option, including that it will override `excluded_items`, but as a notable difference all items required for Item rules are marked as progression. If multiple Item rules require the same item, the largest required amount will be locked, **not** the sum of all amounts. + +Additionally, Item rules accept a special item: +```yaml +entry_rules: + - items: + Key: 1 +``` +This is a generic item that is converted to a key item for the specific scope it is under. Missions get Mission Keys, layouts get Questline Keys, and campaigns get Campaign Keys. If you want to know which specific key is created (for example to tie multiple unlocks to the same key), you can generate a test game and check in the client. + +You can also use one of the following key items for this purpose: +
+ Custom keys + + - `Terran Key` + - `Zerg Key` + - `Protoss Key` + - `Raynor Key` + - `Tychus Key` + - `Swann Key` + - `Stetmann Key` + - `Hanson Key` + - `Nova Key` + - `Tosh Key` + - `Valerian Key` + - `Warfield Key` + - `Mengsk Key` + - `Han Key` + - `Horner Key` + - `Kerrigan Key` + - `Zagara Key` + - `Abathur Key` + - `Yagdra Key` + - `Kraith Key` + - `Slivan Key` + - `Zurvan Key` + - `Brakk Key` + - `Stukov Key` + - `Dehaka Key` + - `Niadra Key` + - `Izsha Key` + - `Artanis Key` + - `Zeratul Key` + - `Tassadar Key` + - `Karax Key` + - `Vorazun Key` + - `Alarak Key` + - `Fenix Key` + - `Urun Key` + - `Mohandar Key` + - `Selendis Key` + - `Rohana Key` + - `Reigel Key` + - `Davis Key` + - `Ji'nara Key` + +
+ +These keys will never be used by the generator unless you specify them yourself. + +There is also a special type of key: +```yaml +entry_rules: + - items: + # These two forms are equivalent + Progressive Key: 5 + Progressive Key 5: 1 +``` +Progressive keys come in two forms: `Progressive Key: ` and `Progressive Key : 1`. In the latter form the item amount is ignored. Their track is used to group them, so all progressive keys with track 1 belong together, as do all with track 2, and so on. Item rules using progressive keys are sorted by how far into the mission order they appear and have their required amounts set automatically so that deeper rules require more keys, with each track of progressive keys performing its own sorting. + +Note that if any Item rule within a track belongs to a mission, the generator will accept ties, in which case the affected rules will require the same number of progressive keys. If a track only contains Item rules belonging to layouts and campaigns, the track will be sorted in definition order (top to bottom in your YAML), so there will be no ties. + +If you prefer not to manually specify the track, use the [`unique_progression_track`](#unique-progression-track) option. + +The Beat and Count rules both require a list of scopes. This list accepts addresses towards other parts of the mission order. + +The basic form of an address is `//`, where `` and `` are the definition names (not `display_names`!) of a campaign and a layout within that campaign, and `` is the index of a mission slot in that layout or an index function for the layout's type. See the section on your layout's type to find valid indices and functions. + +If you don't want to point all the way down to a mission slot, you can omit the later parts. `` and `/` are valid addresses, and will point to the entire specified campaign or layout. + +Futhermore, you can generically refer to the parent of an object using `..`, so if you are creating entry rules for a given layout and want to point at a different `` in the same ``, the following are identical: +- `../` +- `/` + +You can also chain these, so for a given mission `../..` will point to its parent campaign. + +Lastly, you can point to the whole mission order via `/..` (or the equivalent number of `..`s from a given layer), but this is only supported for Count rules and not Beat rules. + +Note that if you have a campaign-less layout, you will not require a `` part to find it, and `..` will skip the campaign layer. + +Below are examples of the available entry rules: +```yaml + custom_mission_order: + Some Missions: + type: grid + size: 9 + entry_rules: + # Item rule: + # To access the Some Missions layout, + # you have to find or receive your Marine + - items: + Marine: 1 + + Wings of Liberty: + Mar Sara: + type: column + size: 3 + Artifact: + type: column + size: 3 + entry_rules: + # Beat rule: + # To access the Artifact layout, + # you have to first beat Mar Sara + - scope: ../Mar Sara + Prophecy: + type: column + size: 3 + entry_rules: + # Beat rule: + # Beat the mission at index 1 in the Artifact layout + - scope: ../Artifact/1 + # This is identical to the above + # because this layout is already in Wings of Liberty + - scope: Wings of Liberty/Artifact/1 + Covert: + type: column + size: 3 + entry_rules: + # Count rule: + # Beat any 7 missions from Wings of Liberty + - scope: Wings of Liberty + amount: 7 + + Complicated Access: + type: column + size: 3 + entry_rules: + # Subrule rule: + # To access this layout, + # fulfill any 1 of the nested rules + # (See amount value at the bottom) + - rules: + # Nested Subrule rule: + # Fulfill all of the nested rules + # Amount can be at the top if you prefer + - amount: -1 # -1 means "all of them" + rules: + # Count rule: + # Beat any 5 missions from Wings of Liberty + - scope: Wings of Liberty + amount: 5 + # Count rule: + # Beat any 5 missions from Some Missions + - scope: Some Missions + amount: 5 + # Count rule: + # Beat any 10 combined missions from + # Wings of Liberty or Some Missions + - scope: + - Wings of Liberty + - Some Missions + amount: 10 + amount: 1 +``` +As this last example shows, the Subrule rule is a powerful tool for making arbitrarily complex requirements. Put plainly, the example accomplishes the following: To unlock the `Complicated Access` layout, either beat 5 missions in both the `Wings of Liberty` campaign and the `Some Missions` layout, or beat 10 missions across both of them. + +--- +### Unique progression track +```yaml +# For campaigns and layouts +unique_progression_track: 0 +``` +This option specifically affects Item entry rules using progressive keys. Progressive keys used by children of this campaign/layout that are on the given track will automatically be put on a track that is unique to the container instead. +```yaml + custom_mission_order: + First Column: + type: column + size: 3 + unique_progression_track: 0 # Default + missions: + - index: [1, 2] + entry_rules: + - items: + Progressive Key: 0 + Second Column: + type: column + size: 3 + unique_progression_track: 0 # Default + missions: + - index: [1, 2] + entry_rules: + - items: + Progressive Key: 0 +``` +In this example the two columns will use separate progressive keys for their missions. + +In the case that a mission slot uses a progressive key whose track matches the `unique_progression_track` of both its containing layout and campaign, the key will use the layout's unique track and not the campaign's. To avoid this behavior simply use different `unique_progression_track` values for the layout and campaign. + +--- +### Difficulty +```yaml +# These two apply to campaigns and layouts +min_difficulty: relative +max_difficulty: relative +# This one applies to missions +difficulty: relative +``` +Valid values are: +- Relative +- Starter +- Easy +- Medium +- Hard +- Very Hard + +These determine the difficulty of missions within campaigns, layouts, or specific mission slots. + +On `relative`, the difficulty of mission slots is dynamically scaled based on earliest possible access to that mission. By default, this scales the entire mission order to go from Starter missions at the start to Very Hard missions at the end. + +Campaigns can override these limits, layouts can likewise override the limits set by their campaigns, and missions can simply define their desired difficulty. + +In every case, if a mission's mission pool does not contain missions of an appropriate difficulty, it will attempt to find a mission of a nearby difficulty, preferring lower ones. + +```yaml + custom_mission_order: + Campaign: + min_difficulty: easy + max_difficulty: medium + Layout 1: + max_difficulty: hard + type: column + size: 3 + Layout 2: + type: column + size: 3 + missions: + - index: 0 + difficulty: starter +``` +In this example, `Campaign` is restricted to missions between Easy and Medium. `Layout 1` overrides Medium to be Hard instead, so its 3 missions will go from Easy to Hard. `Layout 2` keeps the campaign's limits, but its first mission is set to Starter. In this case, the first mission will be a Starter mission, but the other two missions will scale towards Medium as if the first had been an Easy one. + +--- +### Mission Pool +```yaml +# For layouts and missions +mission_pool: + - all missions +``` +Valid values are names of specific missions and names of mission groups. Group names can be looked up here: [APSC2 Mission Groups](https://matthewmarinets.github.io/ap_sc2_icons/missiongroups) + +If a mission defines this, it ignores the pool of its containing layout. To define a pool for a full campaign, define it in the `global` layout. + +This is a list of instructions for constructing a mission pool, executed from top to bottom, so the order of values is important. + +There are three available instructions: +- Addition: ``, `+` or `+ ` + - This adds the missions of the specified group into the pool +- Subtraction: `~` or `~ ` + - This removes the missions of the specified group from the pool + - Note that the operator is `~` and not `-`, because the latter is a reserved symbol in YAML. +- Intersection: `^` or `^ ` + - This removes all the missions from the pool that are not in the specified group. + +As a reminder, `` can also be the name of a specific mission. + +The first instruction in a pool must always be an addition. + +```yaml + custom_mission_order: + Campaign: + global: + type: column + size: 3 + mission_pool: + - terran missions + - ~ no-build missions + Layout A-1: + mission_pool: + - zerg missions + - ^ kerrigan missions + - + Lab Rat + Layout A-2: + missions: + - index: 0 + mission_pool: + - For Aiur! + - Liberation Day +``` +The following pools are constructed in this example: +- `Campaign` defines a pool that contains Terran missions, and then removes all No-Build missions from it. +- `Layout A-1` overrides this pool with Zerg missions, then keeps only the ones with Kerrigan in them, and then adds Lab Rat back to it. + - Lab Rat does not contain Kerrigan, but because the instruction to add it is placed after the instruction to remove non-Kerrigan missions, it is added regardless. +- The pool for the first mission of `Layout A-2` contains For Aiur! and Liberation Day. The remaining missions of `Layout A-2` use the Terran pool set by the `global` layout. + +## Campaign Options + +These options can only be used in campaigns. + +--- +### Preset +```yaml +preset: none +``` +This option loads a pre-built campaign into your mission order. Presets may accept additional options in addition to regular campaign options. + +With all presets, you can override their layout options by defining the layouts like normal in your YAML. +```yaml + custom_mission_order: + My Campaign: + preset: wol + prophecy + missions: random # Optional + shuffle_raceswaps: false # Optional + keys: none # Optional + Prophecy: + mission_pool: + - zerg missions +``` +This example loads the Wol + Prophecy preset and then changes Prophecy's missions to be Zerg instead of Protoss. + +See the following section for available presets. + +## Campaign Presets + +There are two kinds of presets: Static presets that are based on vanilla campaigns, and scripted presets that dynamically create a complex campaign based on extra required options. + +--- +### Static Presets +Available static presets are the following: +- `WoL + Prophecy` +- `WoL` +- `Prophecy` +- `HotS` +- `Prologue`, `LotV Prologue` +- `LotV` +- `Epilogue`, `LotV Epilogue` +- `NCO` +- `Mini WoL + Prophecy` +- `Mini WoL` +- `Mini Prophecy` +- `Mini HotS` +- `Mini Prologue`, `Mini LotV Prologue` +- `Mini LotV` +- `Mini Epilogue`, `Mini LotV Epilogue` +- `Mini NCO` + +For these presets, the layout names used to override settings match the names shown in the client, with some exceptions: +- Prophecy, Prologue and Epilogue contain a single Gauntlet each, which are named `Prophecy`, `Prologue` and `Epilogue` respectively. +- The Gauntlets in the Mini variants of the above are also named `Prophecy`, `Prologue` and `Epilogue`. +- NCO and Mini NCO contain three columns each, named `Mission Pack 1`, `Mission Pack 2` and `Mission Pack 3`. + +#### Preset Options +All static presets accept these options, as shown in the example above: + +##### Missions +The `missions` option accepts these possible values: +- `random` (default), which removes pre-defined `mission_pool` options from layouts and missions, meaning all missions will follow the pool defined in your campaign's `global` layout. This is the default if you don't define the `missions` option. +- `vanilla_shuffled`, which will leave `mission_pool`s on layouts to shuffle vanilla missions within their respective campaigns. +- `vanilla`, which will leave all missions as they are in the vanilla campaigns. + +##### Shuffle Raceswaps +The `shuffle_raceswaps` option accepts `true` and `false` (default). If enabled, the missions pools in the preset will contain raceswapped missions. This means `missions: vanilla_shuffled` will shuffle raceswaps alongside their regular variants, and `missions: vanilla` will allow a random variant of the mission in each slot. This option does nothing if `missions` is set to `random`. + +##### Keys +The `keys` option accepts these possible values: +- `none` (default), which does not add any Key Item rules to the preset. +- `layouts`, which adds Key Item rules to layouts besides the preset's left-most layout, in addition to their regular entry rules. +- `missions`, which adds Key Item rules to missions besides the preset's starter mission, in addition to their regular entry rules. +- `progressive_layouts`, which adds Progressive Key Item rules to layouts besides the preset's left-most layout, in addition to their regular entry rules. These progressive keys use track 0, with presets using the default `unique_progression_track: 0`. +- `progressive_missions`, which adds Progressive Key Item rules to missions besides the preset's starter mission, in addition to their regular entry rules. These progressive keys use track 1 and do not make use of `unique_progression_track`. +- `progressive_per_layout`, which adds Progressive Key Item rules to all missions within each layout besides the preset's left-most one. These progressive keys use track 0, with presets and their layouts using the default `unique_progression_track: 0`. + +--- +### Golden Path +```yaml +preset: golden path +size: # Required, no default, accepts positive numbers +two_start_positions: false +keys: none # Optional +``` +Golden Path aims to create a dynamically-sized campaign with branching paths to create a similar experience to the Wings of Liberty campaign. It accomplishes this by having a main column that requires an increasing number of missions to be beaten to advance, and a number of side columns that require progressing the main column to advance. The exit of a Golden Path campaign is the last mission of the main column. + +The `size` option defines the number of missions in the campaign. + +If `two_start_positions`, the first mission will be skipped, and the first two branches will be available from the start instead. + +The columns in a Golden Path get random names from a `display_name` list and have `unique_name: true` set on them. Their definition names for overriding options are `"0"`, `"1"`, `"2"`, etc., with `"0"` always being the main column, `"1"` being the left-most side column, and so on. + +Since the number of side columns depends on the number of missions, it is best to generate a test game for a given size to see how many columns are generated. + +Golden Path also accepts a `keys` option, which works like the same option for static presets, and accepts the following values: +- `none` (default), which does not add any Key Item rules to the preset. +- `layouts`, which adds Key Item rules to all side columns, in addition to their regular entry rules. +- `missions`, which adds Key Item rules to missions besides the preset's starter mission, in addition to their regular entry rules. +- `progressive_layouts`, which adds Progressive Key Item rules to all side columns, in addition to their regular entry rules. These progressive keys use track 0, with this preset using the default `unique_progression_track: 0`. +- `progressive_missions`, which adds Progressive Key Item rules to missions besides the preset's starter mission, in addition to their regular entry rules. These progressive keys use track 1 and do not make use of `unique_progression_track`. +- `progressive_per_layout`, which adds Progressive Key Item rules to all missions within each side column. These progressive keys use track 0, with this preset and its layouts using the default `unique_progression_track: 0`. + +## Layout Options + +Layouts may have special options depending on their `type`. These are covered in the section on Layout Types. +Below are the options that apply to every layout. + +--- +### Type +```yaml +type: # There is no default +``` +Determines how missions are placed relative to one another within a layout, as well as how they connect to each other. + +Currently, valid values are: +- Column +- Grid +- Hopscotch +- Gauntlet +- Blitz + +Details about specific layout types are covered at the end of this document. + +--- +### Size +```yaml +size: # There is no default +``` +Determines how many missions a layout contains. Valid values are positive numbers. + +### Missions +```yaml +missions: [] +``` +This is used to access mission slots and overwrite the options that the layout type set for them. Valid options for mission slots are covered below, but the `index` option used to find mission slots is explained here. + +Note that this list is evaluated from top to bottom, meaning if you perform conflicting changes on the same mission slot, the last defined operation (lowest in your YAML) will be the one that takes effect. + +The following example shows ways to access and modify missions: +```yaml + custom_mission_order: + My Example: + type: grid + size: 4 + missions: + # Indices can be a numerical value + # This sets the mission at index 1 to be an exit + - index: 1 + exit: true + # Indices can be special index functions + # Valid functions are 'exits', 'entrances', and 'all' + # These are available for all types of layouts + # This takes all exits, including the one set above, + # and turns them into non-exits + - index: exits + exit: false + # Indices can be index functions + # Available functions depend on the layout's type + # In this case the function will return the indices 1 and 3 + # and then mark those two slots as empty + - index: rect(1, 0, 1, 2) + empty: true + # Indices can be a list of valid values + # This takes all entrances as well as the mission at index 2 + # and marks all of them as both entrances and exits + - index: + - entrances + - 2 + entrance: true + exit: true +``` +The result of this example will be a grid where the two missions on the right are empty, and the two missions on the left are both entrances and exits. + +## Mission Slot Options + +For all options in mission slots, the layout type containing the mission slot choses the defaults, and any values you define override the type's defaults. + +--- +### Entrance +```yaml +entrance: false +``` +Determines whether this mission is an entrance for its containing layout. An entrance mission becomes available its parent layout's and campaign's `entry_rules` are fulfilled, but may further be restricted by its own `entry_rules`. + +If for any reason a mission cannot be unlocked by beating other missions, meaning that there is no mission whose `next` points at this mission, then this missions will be automatically marked as entrances. However, this cannot detect circular dependencies, for example if you cut off a section of a grid, so make sure to manually set entrances as appropriate in those cases. + +--- +### Empty +```yaml +empty: false +``` +Determines whether this mission slot contains a mission at all. If set to `true`, the slot is empty and will show up as a blank space in the client. + +Layout types have their own means of creating blank spaces in the client, and so rarely use this option. If you want complete control over a layout's slots, use a layout of `type: grid`. + +--- +### Next +```yaml +next: [] +``` +Valid values are indices of other missions within the same layout and index functions for the layout's type. Note that this does not accept addresses. + +This is the mechanism layout types use to establish mission flow. Overriding this will break the intended order of missions within a type. If you wish to add on to the type's flow rather than replace it, you must manually include the indices intended by the type. + +Mechanically, a mission is unlocked when any other mission that contains the former in its `next` list is beaten. If a mission is not present in any other mission's `next` list, it is automatically marked as an entrance. +```yaml + custom_mission_order: + Wings of Liberty: + Char: + type: column + size: 4 + missions: + - index: 0 + next: + - 1 + - 2 + - index: 1 + next: + - 3 + # The below two are default for a column + # and could be removed from this list + - index: 2 + next: + - 3 + - index: 3 + next: [] + +``` +This example creates the branching path within `Char` in the Vanilla mission order. + +--- +### Victory Cache +```yaml +victory_cache: 0 +``` +Valid values are integers in the range 0 to 10. Sets the number of extra locations given for victory on a mission. + +By default, when this value is not set, the option is set to 0 for goal missions and to the global `victory_cache` option for all other missions. + +## Layout Types + +The below types are listed with their custom options and their defaults. + +--- +### Column +```yaml +type: column +``` + +This is a linear order going from top to bottom. + +A `size: 5` column has the following indices: +```yaml +0 # This is the default entrance +1 +2 +3 +4 # This is the default exit (size - 1) +``` + +--- +### Grid +```yaml +type: grid +width: 0 # Accepts positive numbers +two_start_positions: false # Accepts true/false +``` +This is a rectangular order. Beating a mission unlocks adjacent missions in cardinal directions. + +`width` sets the width of the grid, and height is determined via `size` and `width`. If `width` is set to 0, the width and height are determined automatically. + +If `two_start_positions`, the top left corner will be set to `empty: true`, and its two neighbors will be entrances instead. + +If `size` is too small for the determined width and height, then slots in the bottom left and top right corners will be removed to fit the given `size`. These empty slots are still accessible by index. + +A `size: 25`, `width: 5` grid has the following indices: +```yaml + 0 1 2 3 4 + 5 6 7 8 9 +10 11 12 13 14 +15 16 17 18 19 +20 21 22 23 24 +``` +The top left corner (index `0`) is the default entrance. The bottom right corner (index `size - 1`) is the default exit. + +#### Grid Index Functions +Grid supports the following index functions: + +##### point(x, y) +`point(x, y)` returns the index at the given zero-based X and Y coordinates. In the above example, `point(2, 4)` is index `22`. + +##### rect(x, y, width, height) +`rect(x, y, width, height)` returns the indices within the rectangle defined by the starting point at the X and Y coordinates and the width and height arguments. In the above example, `rect(1, 2, 3, 2)` returns the indices `11, 12, 13, 16, 17, 18`. + +--- +### Canvas +```yaml +type: canvas +canvas: # No default +jump_distance_orthogonal: 1 # Accepts numbers >= 1 +jump_distance_diagonal: 1 # Accepts numbers >= 0 +``` + +This is a special type of grid that is created from a drawn canvas. For this type of layout `canvas` is required and `size` is ignored if specified. + +`canvas` is a list of strings that form a rectangular grid, from which the layout's `size` is determined automatically. Every space in the canvas creates an empty slot, while every character that is not a space creates a filled mission slot. The resulting grid determines its indices like [Grid](#Grid). + +```yaml +type: canvas +canvas: +- ' ggg ' # 0 +- ' ggggg ' # 1 +- ' ggggg ' # 2 +- ' bbb ggg rrr ' # 3 +- 'bbbbb g rrrrr' # 4 +- 'bbbbb rrrrr' # 5 +- ' ggg bbb ' # 6 +- 'ggggg bbbbb' # 7 +- 'gggg bbbb' # 8 +- 'ggg rrr bbb' # 9 +- ' gg rrrrr bb ' # 10 +- ' rrrrr ' # 11 +- ' rrrrr ' # 12 +- ' rrr ' # 13 +jump_distance_orthogonal: 2 +jump_distance_diagonal: 1 +missions: +- index: group(g) + mission_pool: Terran Missions +- index: group(b) + mission_pool: Protoss Missions +- index: group(r) + mission_pool: Zerg Missions +``` +This example draws the Archipelago logo using missions of different races as its colors. Note that while this example fits into 13 lines, there is no set limit for how many lines you may use, and likewise lines may be as long as you need them to be. Short lines are padded with spaces to match the longest line in the canvas, so lines are left-aligned in this case. + +You may have noticed that the above example has gaps between missions. Canvas layouts support jumping over gaps via `jump_distance_orthogonal` and `jump_distance_diagonal`, which determine the maximum distance over which two missions may be connected, in orthogonal and diagonal directions respectively. Missions at higher distances will only connect if there is no other mission in front of them. + +```yaml +type: canvas +canvas: +- 'A A' +- 'B XB' +jump_distance_orthogonal: 3 +jump_distance_diagonal: 0 +``` +In this example the two `A`s will connect because they are less than 3 missions apart, but the two `B`s will not connect because `X` is between them, and both `B`s will connect to `X` instead. Both sets of `AB`s will also connect because they are neighbors. + +Diagonal jumps function identically, with one exception: +```yaml +type: canvas +canvas: +- 'A ' +- ' B ' +- ' XC' +jump_distance_orthogonal: 1 +jump_distance_diagonal: 1 +``` +Missions that are diagonal neighbors only connect if they do not already share an orthogonal neighbor. In this example `A` and `B` connect, but `B` and `C` don't because `X` already connects them. No such restriction exists for higher-distance diagonal jumps, so it is recommended to keep `jump_distance_diagonal` low. + +Finally, the default entrance and exit on a canvas are dynamically set to be the non-empty slots that are closest to the top left and bottom right corner respectively, but only if you don't set any entrances or exits yourself. It is highly recommended to set your own entrance and exit. + +#### Canvas Index Functions +Canvas supports all of [Grid's index functions](#grid-index-functions), as well as the following: + +##### group(character) +`group(character)` returns the indices which match the given character on the canvas. In the Archipelago logo example, `group(g)` gives the indices of all the `g`s on the canvas. Note that there is no group for spaces, so `group(" ")` does not work. + +--- +### Hopscotch +```yaml +type: hopscotch +width: 7 # Accepts numbers >= 4 +spacer: 2 # Accepts numbers >= 1 +two_start_positions: false # Accepts true/false +``` + +This order alternates between one and two missions becoming available at a time. + +`width` determines how many mini columns are allowed to be next to one another before they wrap around the sides. `spacer` determines the amount of empty slots between diagonals in the client. + +If `two_start_positions`, the top left corner will be set to `empty: true`, and its two neighbors will be entrances instead. + +A `size: 23`, `width: 4`, `spacer: 1` Hopscotch layout has the following indices: +```yaml + 0 2 + 1 3 5 + 4 6 8 +11 7 9 +12 14 10 +13 15 17 + 16 18 20 + 19 21 + 22 +``` +The top left corner (index `0`) is the default entrance. The bottom-most mission of the lowest column (index `size - 1`) is the default exit. + +#### Hopscotch Index Functions +Hopscotch supports the following index functions: + +##### top +`top()` (or `top`) returns the indices of all the top-right corners. In the above example, it returns the indices `2, 5, 8, 11, 14, 17, 20`. + +##### bottom +`bottom()` (or `bottom`) returns the indices of all the bottom-left corners. In the above example, it returns the indices `1, 4, 7, 10, 13, 16, 19, 22`. + +##### middle +`middle()` (or `middle`) returns the indices of all the middle slots. In the above example, it returns the indices `0, 3, 6, 9, 12, 15, 18, 21`. + +##### corner(index) +`corner(index)` returns the indices within the given corner. A corner is a slot in the middle and the slots to the bottom and right of it. `corner(0)` would return `0, 1, 2`, `corner(1)` would return `3, 4, 5`, and so on. In the above example, `corner(7)` will only return `21, 22` because it does not have a right mission. + +--- +### Gauntlet +```yaml +type: gauntlet +width: 7 # Accepts positive numbers +``` +This type works the same way as column, but it goes horizontally instead of vertically. + +`width` is the maximum allowed missions on a row before it wraps around into a new row. + +A `size: 21`, `width: 7` gauntlet has the following indices: +```yaml + 0 1 2 3 4 5 6 + + 7 8 9 10 11 12 13 + +14 15 16 17 18 19 20 +``` +The left-most mission on the top row (index `0`) is the default entrance. The right-most mission on the bottom row (index `size - 1`) is the default exit. + +--- +### Blitz +```yaml +type: blitz +width: 0 # Accepts positive numbers +``` +This type features rows of missions, where beating a mission in a row unlocks the entire next row. + +`width` determines how many missions there are in a row. If set to 0, the width is determined automatically based on the total number of missions (the layout's `size`), but limited to be between 2 and 5. + +A `size: 20`, `width: 5` Blitz layout has the following indices: +```yaml + 0 1 2 3 4 + 5 6 7 8 9 +10 11 12 13 14 +15 16 17 18 19 +``` +The top left corner (index `0`) is the default entrance. The right-most mission on the bottom row (index `size - 1`) is the default exit. + +#### Blitz Index Functions +Blitz supports the following index function: + +##### row(height) +`row(height)` returns the indices of the row at the given zero-based height. In the above example, `row(1)` would return `5, 6, 7, 8, 9`. diff --git a/worlds/sc2/docs/en_Starcraft 2.md b/worlds/sc2/docs/en_Starcraft 2.md index e860e8a6..74ba46c3 100644 --- a/worlds/sc2/docs/en_Starcraft 2.md +++ b/worlds/sc2/docs/en_Starcraft 2.md @@ -12,7 +12,7 @@ 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. +3. Your ability to get the generic unit upgrades, such as attack and armor 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. @@ -94,22 +94,31 @@ Will overwrite existing files * Run without arguments to list all factions and colors that are available. * `/option [option_name] [option_value]` Sets an option normally controlled by your yaml after generation. * Run without arguments to list all options. + * Run without `option_value` to check the current value of the option * 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) +* `/windowed_mode [true|false]` to toggle whether the game will start in windowed mode. Note that the behavior of the command `/received` was modified in the StarCraft 2 client. -In the Common client of Archipelago, the command returns the list of items received in the reverse order they were -received. -In the StarCraft 2 client, the returned list will be divided by races (i.e., Any, Protoss, Terran, and Zerg). -Additionally, upgrades are grouped beneath their corresponding units or buildings. -A filter parameter can be provided, e.g., `/received Thor`, to limit the number of items shown. -Every item whose name, race, or group name contains the provided parameter will be shown. + +* In the Common client of Archipelago, the command returns the list of items received in the reverse order they were + received. +* In the StarCraft 2 client, the returned list will be divided by races (i.e., Any, Protoss, Terran, and Zerg). + Additionally, upgrades are grouped beneath their corresponding units or buildings. +* A filter parameter can be provided, e.g., `/received Thor`, to limit the number of items shown. + * Every item whose name, race, or group name contains the provided parameter will be shown. +* Use `/received recent [amount]` to display the last `amount` items received in chronological order + * `amount` defaults to 20 if not specified + +## Client-side settings +Some settings can be set or overridden on the client side rather than within a world's options. +This can allow, for example, overriding difficulty to always be `hard` no matter what the world specified. +It can also modify display properties, like the client's window size on startup or the launcher button colours. + +Modify these within the `sc2_options` section of the host.yaml file within the Archipelago directory. ## Particularities in a multiworld @@ -118,9 +127,9 @@ Every item whose name, race, or group name contains the provided parameter will One of the default options of multiworlds is that once a world has achieved its goal, it collects its items from all other worlds. If you do not want this to happen, you should ask the person generating the multiworld to set the `Collect Permission` -option to something else, e.g., manual. +option to something else, such as "Manual" or "Allow on goal completion." If the generation is not done via the website, the person that does the generation should modify the `collect_mode` -option in their `host.yaml` file prior to generation. +option in their `host.yaml` file prior to generation. If the multiworld has already been generated, the host can use the command `/option collect_mode [value]` to change this option. @@ -135,4 +144,6 @@ This does not affect the game and can be ignored. - Currently, the StarCraft 2 client uses the Victory locations to determine which missions have been completed. As a result, the Archipelago collect feature can sometime grant access to missions that are connected to a mission that you did not complete. + - If all victory locations are collected in this manner, victory is not sent until the player replays a final mission + and recollects the victory location. diff --git a/worlds/sc2/docs/fr_Starcraft 2.md b/worlds/sc2/docs/fr_Starcraft 2.md index 190802e9..c340ecc2 100644 --- a/worlds/sc2/docs/fr_Starcraft 2.md +++ b/worlds/sc2/docs/fr_Starcraft 2.md @@ -112,10 +112,6 @@ supplémentaires données au début des missions, la capacité de contrôler les * `/disable_mission_check` Désactive les requit pour lancer les missions. Cette option a pour but de permettre de jouer en mode coopératif en permettant à un joueur de jouer à la prochaine mission de la chaîne qu'un autre joueur est en train d'entamer. -* `/play [mission_id]` Lance la mission correspondant à l'identifiant donné. -* `/available` Affiche les missions qui sont présentement accessibles. -* `/unfinished` Affiche les missions qui sont présentement accessibles et dont certains des objectifs permettant -l'accès à un *item* n'ont pas été accomplis. * `/set_path [path]` Permet de définir manuellement où *StarCraft 2* est installé ce qui est pertinent seulement si la détection automatique de cette dernière échoue. @@ -151,4 +147,4 @@ Cela n'affecte pas le jeu et peut être ignoré. - Actuellement, le client de *StarCraft 2* utilise la *location* associée à la victoire d'une mission pour déterminer si celle-ci a été complétée. En conséquence, la fonctionnalité *collect* d'*Archipelago* peut rendre accessible des missions connectées à une -mission que vous n'avez pas terminée. \ No newline at end of file +mission que vous n'avez pas terminée. diff --git a/worlds/sc2/docs/setup_en.md b/worlds/sc2/docs/setup_en.md index 4364008b..0ddb9a3b 100644 --- a/worlds/sc2/docs/setup_en.md +++ b/worlds/sc2/docs/setup_en.md @@ -62,69 +62,128 @@ If the Progression Balancing of one world is greater than that of others, items obtained early, and vice versa if its value is smaller. However, StarCraft 2 is more permissive regarding the items that can be used to progress, so this option has little influence on progression in a StarCraft 2 world. -StarCraft 2. Since this option increases the time required to generate a MultiWorld, we recommend deactivating it (i.e., setting it to zero) for a StarCraft 2 world. -#### How do I specify items in a list, like in excluded items? +#### What does Tactics Level do? + +Tactics level allows controlling the difficulty through what items you're likely to get early. +This is independent of game difficulty like causal, normal, hard, or brutal. + +"Standard" and "Advanced" levels are guaranteed to be beatable with the items you are given. +The logic is a little more restrictive than a player's creativity, so an advanced player is likely to have +more items than they need in any situation. These levels are entirely safe to use in a multiworld. + +The "Any Units" level only guarantees that a minimum number of faction-appropriate units or buildings are reachable +early on, with minimal restrictions on what those units are. +Generation will guarantee a number of faction-appropriate units are reachable before starting a mission, +based on the depth of that mission. For example, if the third mission is a zerg mission, it is guaranteed that 2 +zerg units are somewhere in the preceding 2 missions. This logic level is not guaranteed to be beatable, and may +require lowering the difficulty level (`/difficulty` in the client) if many no-build missions are excluded. + +The "No Logic" level provides no logical safeguards for beatability. It is only safe to use in a multiworld if the player curates +a start inventory or the organizer is okay with the possibility of the StarCraft 2 world being unbeatable. +Safeguards exist so that other games' items placed in the StarCraft 2 world are reachable under "Advanced" logic rules. + +#### How do I specify items in a list, like in enabled campaigns? 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: +For lists, every item goes on its own line, started with a hyphen. +Putting each element on its own line makes it easy to toggle elements by commenting +(ie adding a `#` character at the start of the line). ```yaml -excluded_items: - - Battlecruiser - - Drop-Pods (Kerrigan Tier 7) + enabled_campaigns: + - Wings of Liberty + # - Heart of the Swarm + - Legacy of the Void + - Nova Covert Ops + - Prophecy + - 'Whispers of Oblivion (Legacy of the Void: Prologue)' + # - 'Into the Void (Legacy of the Void: Epilogue)' +``` + +An inline syntax may also be used for short lists: + +```yaml + enabled_campaigns: ['Wings of Liberty', 'Nova Covert Ops'] ``` 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. +That's often the default value in the template, which should let you know to use this syntax. -#### How do I specify items for the starting inventory? +#### How do I specify items for key-value mappings, like starting inventory or filler item distribution? -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: +Many options pertaining to the item pool are yaml mappings. +These are several lines, where each line looks like a name, followed by a colon, then a space, then a value. ```yaml -start_inventory: - Micro-Filtering: 1 - Additional Starting Vespene: 5 + start_inventory: + Micro-Filtering: 1 + Additional Starting Vespene: 5 + + locked_items: + MULE (Command Center): 1 ``` +For options like `start_inventory`, `locked_items`, `excluded_items`, and `unexcluded_items`, the value +is a number specifying how many copies of an item to start with/exclude/lock. +Note the name can also be an item group, and the value will then be added to the values for all the items +within the group. A value of `0` will exclude all copies of an item, but will add +0 if the value +is also specified by another name. + +For options like `filler_items_distribution`, the value is a number specifying the relative weight of +a filler item being that particular item. + +For the `custom_mission_order` option, the value is a nested structure of other mapppings to specify the structure +of the mission order. See the [Custom Mission Order documentation](/tutorial/Starcraft%202/custom_mission_orders_en) + 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 and locations? -The [*datapackage*](/datapackage) page of the Archipelago website provides a complete list of the items and locations -for each game that it currently supports, including StarCraft 2. - -You can also look up a complete list of the item names in the +You can look up a complete list of the item names in the [Icon Repository](https://matthewmarinets.github.io/ap_sc2_icons/) page. This page also contains supplementary information of each item. -However, the items shown in that page might differ from those shown in the datapackage page of Archipelago since the -former is generated, most of the time, from beta versions of StarCraft 2 Archipelago undergoing development. -As for the locations, you can see all the locations associated to a mission in your world by placing your cursor over -the mission in the 'StarCraft 2 Launcher' tab in the client. +Locations are of the format `: `. Names are most easily looked up by hovering +your mouse over a mission in the launcher tab of a client. Note this requires already generating a game connect to. + +This information can also be found in the [*datapackage*](/datapackage) page of the Archipelago website. +This page includes all data associated with all games. ## 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]`. +2. In the Archipelago tab, type `/connect [server IP]`. - If you're running through the website, the server IP should be displayed near the top of the room page. + - The server IP may also be typed into the top bar, and then clicking "Connect" 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! +world. + +Unreachable missions will have greyed-out text. Completed missions (all locations collected) will have white text. +Accessible but incomplete missions will have blue text. Goal missions will have a gold border. +Mission buttons will have a color corresponding to the faction you play as in that mission. + +Click on an available mission to start it. ## The game isn't launching when I try to start a mission. -First, check the log file for issues (stored at `[Archipelago Directory]/logs/SC2Client.txt`). +Usually, this is caused by the mod files not being downloaded. +Make sure you have run `/download_data` in the Archipelago tab before playing. +You should only have to run `/download_data` again to pick up bugfixes and updates. + +Make sure that you are running an up-to-date version of the client. +Check the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) to +look up what the latest version is (RC releases are not necessary; that stands for "Release Candidate"). + +If these things are in order, check the log file for issues (stored at `[Archipelago Directory]/logs/Starcraft2Client.txt`). If you can't figure out the log file, visit our [Discord's](https://discord.com/invite/8Z65BR2) tech-support channel for help. Please include a specific description of what's going wrong and attach your log file to your message. @@ -150,16 +209,15 @@ Note: to launch the client, you will need to run the command `python3 Starcraft2 ## Running in Linux -To run StarCraft 2 through Archipelago in Linux, you will need to install the game using Wine, then run the Linux build +To run StarCraft 2 through Archipelago on Linux, you will need to install the game using Wine, then run the Linux build of the Archipelago client. -Make sure you have StarCraft 2 installed using Wine, and that you have followed the -[installation procedures](#how-do-i-install-this-randomizer?) to add the Archipelago maps to the correct location. -You will not need to copy the `.dll` files. -If you're having trouble installing or running StarCraft 2 on Linux, it is recommend to use the Lutris installer. +Make sure you have StarCraft 2 installed using Wine, and you know where Wine and Starcraft 2 are installed. +If you're having trouble installing or running StarCraft 2 on Linux, it is recommended to use the Lutris installer. -Copy the following into a .sh file, replacing the values of **WINE** and **SC2PATH** variables with the relevant -locations, as well as setting **PATH_TO_ARCHIPELAGO** to the directory containing the AppImage if it is not in the same +Copy the following into a .sh file, preferably within your Archipelago directory, +replacing the values of **WINE** and **SC2PATH** variables with the relevant locations, +as well as setting **PATH_TO_ARCHIPELAGO** to the directory containing the AppImage if it is not in the same folder as the script. ```sh @@ -170,6 +228,13 @@ export PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python # FIXME Replace with path to the version of Wine used to run SC2 export WINE="/usr/bin/wine" +# FIXME If using nondefault wineprefix for SC2 install (usual for Lutris installs), uncomment the next line and change the path +#export WINEPREFIX="/path/to/wineprefix" + +# FIXME Uncomment the following lines if experiencing issues with DXVK (like DDRAW.ddl does not exist) +#export WINEDLLOVERRIDES=d3d10core,d3d11,d3d12,d3d12core,d3d9,d3dcompiler_33,d3dcompiler_34,d3dcompiler_35,d3dcompiler_36,d3dcompiler_37,d3dcompiler_38,d3dcompiler_39,d3dcompiler_40,d3dcompiler_41,d3dcompiler_42,d3dcompiler_43,d3dcompiler_46,d3dcompiler_47,d3dx10,d3dx10_33,d3dx10_34,d3dx10_35,d3dx10_36,d3dx10_37,d3dx10_38,d3dx10_39,d3dx10_40,d3dx10_41,d3dx10_42,d3dx10_43,d3dx11_42,d3dx11_43,d3dx9_24,d3dx9_25,d3dx9_26,d3dx9_27,d3dx9_28,d3dx9_29,d3dx9_30,d3dx9_31,d3dx9_32,d3dx9_33,d3dx9_34,d3dx9_35,d3dx9_36,d3dx9_37,d3dx9_38,d3dx9_39,d3dx9_40,d3dx9_41,d3dx9_42,d3dx9_43,dxgi,nvapi,nvapi64 +#export DXVK_ENABLE_NVAPI=1 + # FIXME Replace with path to StarCraft II install folder export SC2PATH="/home/user/Games/starcraft-ii/drive_c/Program Files (x86)/StarCraft II/" @@ -193,3 +258,6 @@ below, replacing **${ID}** with the numerical ID. This will get all of the relevant environment variables Lutris sets to run StarCraft 2 in a script, including the path to the Wine binary that Lutris uses. You can then remove the line that runs the Battle.Net launcher and copy the code above into the existing script. + +Finally, you can run the script to start your Archipelago client, +and it should be able to launch Starcraft 2 when you start a mission. diff --git a/worlds/sc2/docs/setup_fr.md b/worlds/sc2/docs/setup_fr.md index 7cdb7225..5ce9b4b9 100644 --- a/worlds/sc2/docs/setup_fr.md +++ b/worlds/sc2/docs/setup_fr.md @@ -87,7 +87,7 @@ Pour les listes, chaque *item* doit être sur sa propre ligne et doit être pré ```yaml excluded_items: - Battlecruiser - - Drop-Pods (Kerrigan Tier 7) + - Drop-Pods (Kerrigan Ability) ``` Une liste vide est représentée par une paire de crochets: `[]`. diff --git a/worlds/sc2/gui_config.py b/worlds/sc2/gui_config.py new file mode 100644 index 00000000..b3039da1 --- /dev/null +++ b/worlds/sc2/gui_config.py @@ -0,0 +1,98 @@ +""" +Import this before importing client_gui.py to set window defaults from world settings. +""" +from .settings import Starcraft2Settings +from typing import List, Tuple, Any + + +def get_window_defaults() -> Tuple[List[str], int, int]: + """ + Gets the window size options from the sc2 settings. + Returns a list of warnings to be printed once the GUI is started, followed by the window width and height + """ + from . import SC2World + + # validate settings + warnings: List[str] = [] + if isinstance(SC2World.settings.window_height, int) and SC2World.settings.window_height > 0: + window_height = SC2World.settings.window_height + else: + warnings.append(f"Invalid value for options.yaml key sc2_options.window_height: '{SC2World.settings.window_height}'. Expected a positive integer.") + window_height = Starcraft2Settings.window_height + if isinstance(SC2World.settings.window_width, int) and SC2World.settings.window_width > 0: + window_width = SC2World.settings.window_width + else: + warnings.append(f"Invalid value for options.yaml key sc2_options.window_width: '{SC2World.settings.window_width}'. Expected a positive integer.") + window_width = Starcraft2Settings.window_width + + return warnings, window_width, window_height + + +def validate_color(color: Any, default: Tuple[float, float, float]) -> Tuple[Tuple[str, ...], Tuple[float, float, float]]: + if isinstance(color, int): + if color < 0: + return ('Integer color was negative; expected a value from 0 to 0xffffff',), default + return (), ( + ((color >> 8) & 0xff) / 255, + ((color >> 4) & 0xff) / 255, + ((color >> 0) & 0xff) / 255, + ) + elif color == 'default': + return (), default + elif color == 'white': + return (), (0.9, 0.9, 0.9) + elif color == 'black': + return (), (0.0, 0.0, 0.0) + elif color == 'grey': + return (), (0.345, 0.345, 0.345) + elif color == 'red': + return (), (0.85, 0.2, 0.1) + elif color == 'orange': + return (), (1.0, 0.65, 0.37) + elif color == 'green': + return (), (0.24, 0.84, 0.55) + elif color == 'blue': + return (), (0.3, 0.4, 1.0) + elif color == 'pink': + return (), (0.886, 0.176, 0.843) + elif not isinstance(color, list): + return (f'Invalid type {type(color)}; expected 3-element list or integer',), default + elif len(color) != 3: + return (f'Wrong number of elements in color; expected 3, got {len(color)}',), default + result: List[float] = [0.0, 0.0, 0.0] + errors: List[str] = [] + expected = 'expected a number from 0 to 1' + for index, element in enumerate(color): + if isinstance(element, int): + element = float(element) + if not isinstance(element, float): + errors.append(f'Invalid type {type(element)} at index {index}; {expected}') + continue + if element < 0: + errors.append(f'Negative element {element} at index {index}; {expected}') + continue + if element > 1: + errors.append(f'Element {element} at index {index} is greater than 1; {expected}') + result[index] = 1.0 + continue + result[index] = element + return tuple(errors), tuple(result) + + +def get_button_color(race: str) -> Tuple[Tuple[str, ...], Tuple[float, float, float]]: + from . import SC2World + baseline_color = 0.345 # the button graphic is grey, with this value in each color channel + if race == 'TERRAN': + user_color: list = SC2World.settings.terran_button_color + default_color = (0.0838, 0.2898, 0.2346) + elif race == 'PROTOSS': + user_color = SC2World.settings.protoss_button_color + default_color = (0.345, 0.22425, 0.12765) + elif race == 'ZERG': + user_color = SC2World.settings.zerg_button_color + default_color = (0.18975, 0.2415, 0.345) + else: + user_color = [baseline_color, baseline_color, baseline_color] + default_color = (baseline_color, baseline_color, baseline_color) + errors, color = validate_color(user_color, default_color) + return errors, tuple(x / baseline_color for x in color) diff --git a/worlds/sc2/item/__init__.py b/worlds/sc2/item/__init__.py new file mode 100644 index 00000000..7316a5f1 --- /dev/null +++ b/worlds/sc2/item/__init__.py @@ -0,0 +1,173 @@ +import enum +import typing +from dataclasses import dataclass +from typing import Optional, Union, Dict, Type + +from BaseClasses import Item, ItemClassification +from ..mission_tables import SC2Race + + +class ItemFilterFlags(enum.IntFlag): + """Removed > Start Inventory > Locked > Excluded > Requested > Culled""" + Available = 0 + StartInventory = enum.auto() + Locked = enum.auto() + """Used to flag items that are never allowed to be culled.""" + LogicLocked = enum.auto() + """Locked by item cull logic checks; logic-locked w/a upgrades may be removed if all parents are removed""" + Requested = enum.auto() + """Soft-locked items by item count checks during item culling; may be re-added""" + Removed = enum.auto() + """Marked for immediate removal""" + UserExcluded = enum.auto() + """Excluded by the user; display an error message if failing to exclude""" + FilterExcluded = enum.auto() + """Excluded by item filtering""" + Culled = enum.auto() + """Soft-removed by the item culling""" + NonLocal = enum.auto() + Plando = enum.auto() + AllowedOrphan = enum.auto() + """Used to flag items that shouldn't be filtered out with their parents""" + ForceProgression = enum.auto() + """Used to flag items that aren't classified as progression by default""" + + Unexcludable = StartInventory|Plando|Locked|LogicLocked + UnexcludableUpgrade = StartInventory|Plando|Locked + Uncullable = StartInventory|Plando|Locked|LogicLocked|Requested + Excluded = UserExcluded|FilterExcluded + RequestedOrBetter = StartInventory|Locked|LogicLocked|Requested + CulledOrBetter = Removed|Excluded|Culled + + +class StarcraftItem(Item): + game: str = "Starcraft 2" + filter_flags: ItemFilterFlags = ItemFilterFlags.Available + + def __init__(self, name: str, classification: ItemClassification, code: Optional[int], player: int, filter_flags: ItemFilterFlags = ItemFilterFlags.Available): + super().__init__(name, classification, code, player) + self.filter_flags = filter_flags + +class ItemTypeEnum(enum.Enum): + def __new__(cls, *args, **kwargs): + value = len(cls.__members__) + 1 + obj = object.__new__(cls) + obj._value_ = value + return obj + + def __init__(self, name: str, flag_word: int): + self.display_name = name + self.flag_word = flag_word + + +class TerranItemType(ItemTypeEnum): + Armory_1 = "Armory", 0 + """General Terran unit upgrades""" + Armory_2 = "Armory", 1 + Armory_3 = "Armory", 2 + Armory_4 = "Armory", 3 + Armory_5 = "Armory", 4 + Armory_6 = "Armory", 5 + Armory_7 = "Armory", 6 + Progressive = "Progressive Upgrade", 7 + Laboratory = "Laboratory", 8 + Upgrade = "Upgrade", 9 + Unit = "Unit", 10 + Building = "Building", 11 + Mercenary = "Mercenary", 12 + Nova_Gear = "Nova Gear", 13 + Progressive_2 = "Progressive Upgrade", 14 + Unit_2 = "Unit", 15 + + +class ZergItemType(ItemTypeEnum): + Ability = "Ability", 0 + """Kerrigan abilities""" + Mutation_1 = "Mutation", 1 + Strain = "Strain", 2 + Morph = "Morph", 3 + Upgrade = "Upgrade", 4 + Mercenary = "Mercenary", 5 + Unit = "Unit", 6 + Level = "Level", 7 + """Kerrigan level packs""" + Primal_Form = "Primal Form", 8 + Evolution_Pit = "Evolution Pit", 9 + """Zerg global economy upgrades, like automated extractors""" + Mutation_2 = "Mutation", 10 + Mutation_3 = "Mutation", 11 + Mutation_4 = "Mutation", 12 + Progressive = "Progressive Upgrade", 13 + Mutation_5 = "Mutation", 14 + + +class ProtossItemType(ItemTypeEnum): + Unit = "Unit", 0 + Unit_2 = "Unit", 1 + Upgrade = "Upgrade", 2 + Building = "Building", 3 + Progressive = "Progressive Upgrade", 4 + Spear_Of_Adun = "Spear of Adun", 5 + Solarite_Core = "Solarite Core", 6 + """Protoss global effects, such as reconstruction beam or automated assimilators""" + Forge_1 = "Forge", 7 + """General Protoss unit upgrades""" + Forge_2 = "Forge", 8 + """General Protoss unit upgrades""" + Forge_3 = "Forge", 9 + """General Protoss unit upgrades""" + Forge_4 = "Forge", 10 + """General Protoss unit upgrades""" + Forge_5 = "Forge", 11 + """General Protoss unit upgrades""" + War_Council = "War Council", 12 + War_Council_2 = "War Council", 13 + ShieldRegeneration = "Shield Regeneration Group", 14 + + +class FactionlessItemType(ItemTypeEnum): + Minerals = "Minerals", 0 + Vespene = "Vespene", 1 + Supply = "Supply", 2 + MaxSupply = "Max Supply", 3 + BuildingSpeed = "Building Speed", 4 + Nothing = "Nothing Group", 5 + Deprecated = "Deprecated", 6 + MaxSupplyTrap = "Max Supply Trap", 7 + ResearchSpeed = "Research Speed", 8 + ResearchCost = "Research Cost", 9 + Keys = "Keys", -1 + + +ItemType = Union[TerranItemType, ZergItemType, ProtossItemType, FactionlessItemType] +race_to_item_type: Dict[SC2Race, Type[ItemTypeEnum]] = { + SC2Race.ANY: FactionlessItemType, + SC2Race.TERRAN: TerranItemType, + SC2Race.ZERG: ZergItemType, + SC2Race.PROTOSS: ProtossItemType, +} + + +class ItemData(typing.NamedTuple): + code: int + type: ItemType + number: int # Important for bot commands to send the item into the game + race: SC2Race + classification: ItemClassification = ItemClassification.useful + quantity: int = 1 + parent: typing.Optional[str] = None + important_for_filtering: bool = False + + def is_important_for_filtering(self): + return ( + self.important_for_filtering + or self.classification == ItemClassification.progression + or self.classification == ItemClassification.progression_skip_balancing + ) + +@dataclass +class FilterItem: + name: str + data: ItemData + index: int = 0 + flags: ItemFilterFlags = ItemFilterFlags.Available diff --git a/worlds/sc2/item/item_annotations.py b/worlds/sc2/item/item_annotations.py new file mode 100644 index 00000000..cd28e071 --- /dev/null +++ b/worlds/sc2/item/item_annotations.py @@ -0,0 +1,178 @@ +""" +Annotations to add to item names sent to the in-game message panel +""" +from . import item_names + +ITEM_NAME_ANNOTATIONS = { + item_names.MARINE: "(Barracks)", + item_names.MEDIC: "(Barracks)", + item_names.FIREBAT: "(Barracks)", + item_names.MARAUDER: "(Barracks)", + item_names.REAPER: "(Barracks)", + item_names.HELLION: "(Factory)", + item_names.VULTURE: "(Factory)", + item_names.GOLIATH: "(Factory)", + item_names.DIAMONDBACK: "(Factory)", + item_names.SIEGE_TANK: "(Factory)", + item_names.MEDIVAC: "(Starport)", + item_names.WRAITH: "(Starport)", + item_names.VIKING: "(Starport)", + item_names.BANSHEE: "(Starport)", + item_names.BATTLECRUISER: "(Starport)", + item_names.GHOST: "(Barracks)", + item_names.SPECTRE: "(Barracks)", + item_names.THOR: "(Factory)", + item_names.RAVEN: "(Starport)", + item_names.SCIENCE_VESSEL: "(Starport)", + item_names.PREDATOR: "(Factory)", + item_names.HERCULES: "(Starport)", + + item_names.HERC: "(Barracks)", + item_names.DOMINION_TROOPER: "(Barracks)", + item_names.WIDOW_MINE: "(Factory)", + item_names.CYCLONE: "(Factory)", + item_names.WARHOUND: "(Factory)", + item_names.LIBERATOR: "(Starport)", + item_names.VALKYRIE: "(Starport)", + + item_names.SON_OF_KORHAL: "(Elite Barracks)", + item_names.AEGIS_GUARD: "(Elite Barracks)", + item_names.FIELD_RESPONSE_THETA: "(Elite Barracks)", + item_names.EMPERORS_SHADOW: "(Elite Barracks)", + item_names.BULWARK_COMPANY: "(Elite Factory)", + item_names.SHOCK_DIVISION: "(Elite Factory)", + item_names.BLACKHAMMER: "(Elite Factory)", + item_names.SKY_FURY: "(Elite Starport)", + item_names.NIGHT_HAWK: "(Elite Starport)", + item_names.NIGHT_WOLF: "(Elite Starport)", + item_names.EMPERORS_GUARDIAN: "(Elite Starport)", + item_names.PRIDE_OF_AUGUSTRGRAD: "(Elite Starport)", + + item_names.WAR_PIGS: "(Terran Mercenary)", + item_names.DEVIL_DOGS: "(Terran Mercenary)", + item_names.HAMMER_SECURITIES: "(Terran Mercenary)", + item_names.SPARTAN_COMPANY: "(Terran Mercenary)", + item_names.SIEGE_BREAKERS: "(Terran Mercenary)", + item_names.HELS_ANGELS: "(Terran Mercenary)", + item_names.DUSK_WINGS: "(Terran Mercenary)", + item_names.JACKSONS_REVENGE: "(Terran Mercenary)", + item_names.SKIBIS_ANGELS: "(Terran Mercenary)", + item_names.DEATH_HEADS: "(Terran Mercenary)", + item_names.WINGED_NIGHTMARES: "(Terran Mercenary)", + item_names.MIDNIGHT_RIDERS: "(Terran Mercenary)", + item_names.BRYNHILDS: "(Terran Mercenary)", + item_names.JOTUN: "(Terran Mercenary)", + + item_names.BUNKER: "(Terran Building)", + item_names.MISSILE_TURRET: "(Terran Building)", + item_names.SENSOR_TOWER: "(Terran Building)", + item_names.PLANETARY_FORTRESS: "(Terran Building)", + item_names.PERDITION_TURRET: "(Terran Building)", + item_names.DEVASTATOR_TURRET: "(Terran Building)", + item_names.PSI_DISRUPTER: "(Terran Building)", + item_names.HIVE_MIND_EMULATOR: "(Terran Building)", + + item_names.ZERGLING: "(Larva)", + item_names.SWARM_QUEEN: "(Hatchery)", + item_names.ROACH: "(Larva)", + item_names.HYDRALISK: "(Larva)", + item_names.ABERRATION: "(Larva)", + item_names.MUTALISK: "(Larva)", + item_names.SWARM_HOST: "(Larva)", + item_names.INFESTOR: "(Larva)", + item_names.ULTRALISK: "(Larva)", + item_names.PYGALISK: "(Larva)", + item_names.CORRUPTOR: "(Larva)", + item_names.SCOURGE: "(Larva)", + item_names.BROOD_QUEEN: "(Larva)", + item_names.DEFILER: "(Larva)", + item_names.INFESTED_MARINE: "(Infested Barracks)", + item_names.INFESTED_SIEGE_TANK: "(Infested Factory)", + item_names.INFESTED_DIAMONDBACK: "(Infested Factory)", + item_names.BULLFROG: "(Infested Factory)", + item_names.INFESTED_BANSHEE: "(Infested Starport)", + item_names.INFESTED_LIBERATOR: "(Infested Starport)", + + item_names.ZERGLING_BANELING_ASPECT: "(Zergling Morph)", + item_names.HYDRALISK_IMPALER_ASPECT: "(Hydralisk Morph)", + item_names.HYDRALISK_LURKER_ASPECT: "(Hydralisk Morph)", + item_names.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT: "(Mutalisk/Corruptor Morph)", + item_names.MUTALISK_CORRUPTOR_VIPER_ASPECT: "(Mutalisk/Corruptor Morph)", + item_names.MUTALISK_CORRUPTOR_GUARDIAN_ASPECT: "(Mutalisk/Corruptor Morph)", + item_names.MUTALISK_CORRUPTOR_DEVOURER_ASPECT: "(Mutalisk/Corruptor Morph)", + item_names.ROACH_RAVAGER_ASPECT: "(Roach Morph)", + item_names.OVERLORD_OVERSEER_ASPECT: "(Overlord Morph)", + item_names.ROACH_PRIMAL_IGNITER_ASPECT: "(Roach Morph)", + item_names.ULTRALISK_TYRANNOZOR_ASPECT: "(Ultralisk Morph)", + + item_names.INFESTED_MEDICS: "(Zerg Mercenary)", + item_names.INFESTED_SIEGE_BREAKERS: "(Zerg Mercenary)", + item_names.INFESTED_DUSK_WINGS: "(Zerg Mercenary)", + item_names.DEVOURING_ONES: "(Zerg Mercenary)", + item_names.HUNTER_KILLERS: "(Zerg Mercenary)", + item_names.TORRASQUE_MERC: "(Zerg Mercenary)", + item_names.HUNTERLING: "(Zerg Mercenary)", + item_names.YGGDRASIL: "(Zerg Mercenary)", + item_names.CAUSTIC_HORRORS: "(Zerg Mercenary)", + + item_names.SPORE_CRAWLER: "(Zerg Building)", + item_names.SPINE_CRAWLER: "(Zerg Building)", + item_names.BILE_LAUNCHER: "(Zerg Building)", + item_names.INFESTED_BUNKER: "(Zerg Building)", + item_names.INFESTED_MISSILE_TURRET: "(Zerg Building)", + item_names.NYDUS_WORM: "(Nydus Network)", + item_names.ECHIDNA_WORM: "(Nydus Network)", + + item_names.ZEALOT: "(Gateway, Aiur)", + item_names.CENTURION: "(Gateway, Nerazim)", + item_names.SENTINEL: "(Gateway, Purifier)", + item_names.SUPPLICANT: "(Gateway, Tal'darim)", + item_names.STALKER: "(Gateway, Nerazim)", + item_names.INSTIGATOR: "(Gateway, Purifier)", + item_names.SLAYER: "(Gateway, Tal'darim)", + item_names.SENTRY: "(Gateway, Aiur)", + item_names.ENERGIZER: "(Gateway, Purifier)", + item_names.HAVOC: "(Gateway, Tal'darim)", + item_names.HIGH_TEMPLAR: "(Gateway, Aiur)", + item_names.SIGNIFIER: "(Gateway, Nerazim)", + item_names.ASCENDANT: "(Gateway, Tal'darim)", + item_names.DARK_TEMPLAR: "(Gateway, Nerazim)", + item_names.AVENGER: "(Gateway, Aiur)", + item_names.BLOOD_HUNTER: "(Gateway, Tal'darim)", + item_names.DRAGOON: "(Gateway, Aiur)", + item_names.DARK_ARCHON: "(Gateway, Nerazim)", + item_names.ADEPT: "(Gateway, Purifier)", + item_names.OBSERVER: "(Robotics Facility)", + item_names.WARP_PRISM: "(Robotics Facility)", + item_names.IMMORTAL: "(Robotics Facility, Aiur)", + item_names.ANNIHILATOR: "(Robotics Facility, Nerazim)", + item_names.VANGUARD: "(Robotics Facility, Tal'darim)", + item_names.STALWART: "(Robotics Facility, Purifier)", + item_names.COLOSSUS: "(Robotics Facility, Purifier)", + item_names.WRATHWALKER: "(Robotics Facility, Tal'darim)", + item_names.REAVER: "(Robotics Facility, Aiur)", + item_names.DISRUPTOR: "(Robotics Facility, Purifier)", + item_names.PHOENIX: "(Stargate, Aiur)", + item_names.MIRAGE: "(Stargate, Purifier)", + item_names.SKIRMISHER: "(Stargate, Tal'darim)", + item_names.CORSAIR: "(Stargate, Nerazim)", + item_names.VOID_RAY: "(Stargate, Nerazim)", + item_names.DESTROYER: "(Stargate, Tal'darim)", + item_names.PULSAR: "(Stargate, Aiur)", + item_names.DAWNBRINGER: "(Stargate, Purifier)", + item_names.SCOUT: "(Stargate, Aiur)", + item_names.OPPRESSOR: "(Stargate, Tal'darim)", + item_names.CALADRIUS: "(Stargate, Purifier)", + item_names.MISTWING: "(Stargate, Nerazim)", + item_names.CARRIER: "(Stargate, Aiur)", + item_names.SKYLORD: "(Stargate, Tal'darim)", + item_names.TRIREME: "(Stargate, Purifier)", + item_names.TEMPEST: "(Stargate, Purifier)", + item_names.MOTHERSHIP: "(Stargate, Tal'darim)", + item_names.ARBITER: "(Stargate, Aiur)", + item_names.ORACLE: "(Stargate, Nerazim)", + + item_names.PHOTON_CANNON: "(Protoss Building)", + item_names.KHAYDARIN_MONOLITH: "(Protoss Building)", + item_names.SHIELD_BATTERY: "(Protoss Building)", +} \ No newline at end of file diff --git a/worlds/sc2/item/item_descriptions.py b/worlds/sc2/item/item_descriptions.py new file mode 100644 index 00000000..f1520df1 --- /dev/null +++ b/worlds/sc2/item/item_descriptions.py @@ -0,0 +1,1127 @@ +""" +Contains descriptions for Starcraft 2 items. +""" +import inspect + +from . import item_tables, item_names + +WEAPON_ARMOR_UPGRADE_NOTE = inspect.cleandoc(""" + Must be researched during the mission if the mission type isn't set to auto-unlock generic upgrades. +""") +GENERIC_UPGRADE_TEMPLATE = "Increases {} of {} {}.\n" + WEAPON_ARMOR_UPGRADE_NOTE +TERRAN = "Terran" +ZERG = "Zerg" +PROTOSS = "Protoss" + +LASER_TARGETING_SYSTEMS_DESCRIPTION = "Increases vision by 2 and weapon range by 1." +STIMPACK_SMALL_COST = 10 +STIMPACK_SMALL_HEAL = 30 +STIMPACK_LARGE_COST = 20 +STIMPACK_LARGE_HEAL = 60 +STIMPACK_TEMPLATE = inspect.cleandoc(""" + Level 1: Stimpack: Increases unit movement and attack speed for 15 seconds. Injures the unit for {} life. + Level 2: Super Stimpack: Instead of injuring the unit, heals the unit for {} life instead. +""") +STIMPACK_SMALL_DESCRIPTION = STIMPACK_TEMPLATE.format(STIMPACK_SMALL_COST, STIMPACK_SMALL_HEAL) +STIMPACK_LARGE_DESCRIPTION = STIMPACK_TEMPLATE.format(STIMPACK_LARGE_COST, STIMPACK_LARGE_HEAL) +SMART_SERVOS_DESCRIPTION = "Increases transformation speed between modes." +INTERNAL_TECH_MODULE_DESCRIPTION_TEMPLATE = "{} can be trained from a {} without an attached Tech Lab." +CLOAK_DESCRIPTION_TEMPLATE = "Allows {} to use the Cloak ability." + +DISPLAY_NAME_BROOD_LORD = "Brood Lord" +DISPLAY_NAME_CLOAKED_ASSASSIN = "Dark Templar, Avenger, and Blood Hunter" +DISPLAY_NAME_WORMS = "Nydus Worm and Echidna Worm" + +GENERIC_KEY_DESC = "Unlocks a part of the mission order." + +resource_efficiency_cost_reduction = { + item_names.REAPER: (0, 50, 0), + item_names.MEDIC: (25, 25, 1), + item_names.FIREBAT: (50, 0, 1), + item_names.GOLIATH: (50, 0, 1), + item_names.SIEGE_TANK: (0, 25, 1), + item_names.DIAMONDBACK: (0, 50, 1), + item_names.PREDATOR: (0, 75, 1), + item_names.WARHOUND: (75, 0, 0), + item_names.HERC: (25, 25, 1), + item_names.WRAITH: (0, 50, 0), + item_names.GHOST: (25, 25, 0), + item_names.SPECTRE: (25, 25, 0), + item_names.RAVEN: (0, 50, 0), + item_names.CYCLONE: (25, 50, 1), + item_names.WIDOW_MINE: (0, 25, 1), + item_names.LIBERATOR: (0, 25, 0), + item_names.VALKYRIE: (100, 25, 1), + item_names.MEDIVAC: (0, 50, 0), + item_names.DEVASTATOR_TURRET: (50, 0, 0), + item_names.MISSILE_TURRET: (25, 0, 0), + item_names.SCOURGE: (0, 50, 0), + item_names.HYDRALISK: (25, 25, 1), + item_names.SWARM_HOST: (100, 25, 0), + item_names.ULTRALISK: (100, 0, 2), + item_names.ABERRATION: (50, 25, 0), + item_names.CORRUPTOR: (50, 25, 0), + DISPLAY_NAME_BROOD_LORD: (0, 75, 0), + item_names.SWARM_QUEEN: (0, 50, 0), + item_names.ARBITER: (50, 0, 0), + item_names.REAVER: (50, 25, 1), + DISPLAY_NAME_CLOAKED_ASSASSIN: (0, 50, 0), + item_names.SCOUT: (75, 25, 0), + item_names.DESTROYER: (50, 25, 1), + DISPLAY_NAME_WORMS: (50, 75, 0), + + # Frightful Fleshwelder + item_names.INFESTED_SIEGE_TANK: (0, 25, 0), + item_names.INFESTED_DIAMONDBACK: (50, 0, 0), + item_names.INFESTED_BANSHEE: (25, 0, 0), + item_names.INFESTED_LIBERATOR: (0, 25, 0), + + # War Council + item_names.CENTURION: (0, 40, 0), + item_names.SENTINEL: (60, 0, 1), +} + +op_re_cost_reduction = { + item_names.GHOST: (100, 50, 1), + item_names.SPECTRE: (100, 50, 1), + item_names.REAVER: (50, 75, 1), + item_names.SCOUT: (50, 0, 1), +} + + +def _get_resource_efficiency_desc(item_name: str, reduction_map: dict = resource_efficiency_cost_reduction) -> str: + cost = reduction_map[item_name] + parts = [f"{cost[0]} minerals"] if cost[0] else [] + parts += [f"{cost[1]} gas"] if cost[1] else [] + parts += [f"{cost[2]} supply"] if cost[2] else [] + assert parts, f"{item_name} doesn't reduce cost by anything" + if len(parts) == 1: + amount = parts[0] + elif len(parts) == 2: + amount = " and ".join(parts) + else: + amount = ", ".join(parts[:-1]) + ", and " + parts[-1] + return (f"Reduces {item_name} cost by {amount}.") + + + +def _get_start_and_max_energy_desc(unit_name_plural: str, starting_amount_increase: int = 150, maximum_amount_increase: int = 50) -> str: + return f"{unit_name_plural} gain +{starting_amount_increase} starting energy and +{maximum_amount_increase} maximum energy." + + +def _ability_desc(unit_name_plural: str, ability_name: str, ability_description: str = '') -> str: + if ability_description: + suffix = f", \nwhich {ability_description}" + else: + suffix = "" + return f"{unit_name_plural} gain the {ability_name} ability{suffix}." + + +item_descriptions = { + item_names.MARINE: "General-purpose infantry.", + item_names.MEDIC: "Support trooper. Heals nearby biological units.", + item_names.FIREBAT: "Specialized anti-infantry attacker.", + item_names.MARAUDER: "Heavy assault infantry.", + item_names.REAPER: "Raider. Capable of jumping up and down cliffs. Throws explosive mines.", + item_names.HELLION: "Fast scout. Has a flame attack that damages all enemy units in its line of fire.", + item_names.VULTURE: "Fast skirmish unit. Can use the Spider Mine ability.", + item_names.GOLIATH: "Heavy-fire support unit.", + item_names.DIAMONDBACK: "Fast, high-damage hovertank. Rail Gun can fire while the Diamondback is moving.", + item_names.SIEGE_TANK: "Heavy tank. Long-range artillery in Siege Mode.", + item_names.MEDIVAC: "Air transport. Heals nearby biological units.", + item_names.WRAITH: "Highly mobile flying unit. Excellent at surgical strikes.", + item_names.VIKING: inspect.cleandoc(""" + Durable support flyer. Loaded with strong anti-capital air missiles. + Can switch into Assault Mode to attack ground units. + """), + item_names.BANSHEE: "Tactical-strike aircraft.", + item_names.BATTLECRUISER: "Powerful warship.", + item_names.GHOST: + "Infiltration unit. Can use Snipe and Cloak abilities. Can also call down Tactical Nukes.", + item_names.SPECTRE: inspect.cleandoc(""" + Infiltration unit. Can use Ultrasonic Pulse, Psionic Lash, and Cloak. + Can also call down Tactical Nukes. + """), + item_names.THOR: "Heavy assault mech.", + item_names.LIBERATOR: inspect.cleandoc(""" + Artillery fighter. Loaded with missiles that deal area damage to enemy air targets. + Can switch into Defender Mode to provide siege support. + """), + item_names.VALKYRIE: inspect.cleandoc(""" + Advanced anti-aircraft fighter. + Able to use cluster missiles that deal area damage to air targets. + """), + item_names.WIDOW_MINE: inspect.cleandoc(""" + Robotic mine. Launches missiles at nearby enemy units while burrowed. + Attacks deal splash damage in a small area around the target. + Widow Mine is revealed when Sentinel Missile is on cooldown. + """), + item_names.CYCLONE: "Mobile assault vehicle. Can use Lock On to quickly fire while moving.", + item_names.HERC: "Front-line infantry. Can use Grapple.", + item_names.WARHOUND: "Anti-vehicle mech. Haywire missiles do bonus damage to mechanical units.", + item_names.DOMINION_TROOPER: + "General-purpose infantry. Can be outfitted with weapons for different combat situations.", + item_names.PRIDE_OF_AUGUSTRGRAD: "Powerful Royal Guard warship.", + item_names.SKY_FURY: inspect.cleandoc(""" + Durable Royal Guard support flyer. Loaded with strong anti-capital air missiles. + Can switch into Assault Mode to attack ground units. + """), + item_names.SHOCK_DIVISION: "Royal Guard heavy tank. Long-range artillery in Siege Mode.", + item_names.BLACKHAMMER: "Royal Guard heavy assault mech.", + item_names.AEGIS_GUARD: "Royal Guard heavy assault infantry.", + item_names.EMPERORS_SHADOW: "Royal Guard specialist. Can use Pyrokinetic Immolation and EMP Blast abilities. Can call down Tactical missiles.", + item_names.SON_OF_KORHAL: "Royal Guard general-purpose infantry.", + item_names.BULWARK_COMPANY: "Royal Guard heavy-fire support unit.", + item_names.FIELD_RESPONSE_THETA: "Royal Guard support trooper. Heals nearby biological units.", + item_names.EMPERORS_GUARDIAN: inspect.cleandoc(""" + Royal Guard artillery fighter. Loaded with missiles that deal area damage to enemy air targets. + Can switch into Defender Mode to provide siege support. + """), + item_names.NIGHT_HAWK: "Royal Guard highly mobile flying unit. Excellent at surgical strikes.", + item_names.NIGHT_WOLF: "Royal Guard tactical-strike aircraft.", + item_names.PROGRESSIVE_TERRAN_INFANTRY_WEAPON: GENERIC_UPGRADE_TEMPLATE.format("damage", TERRAN, "infantry"), + item_names.PROGRESSIVE_TERRAN_INFANTRY_ARMOR: GENERIC_UPGRADE_TEMPLATE.format("armor", TERRAN, "infantry"), + item_names.PROGRESSIVE_TERRAN_VEHICLE_WEAPON: GENERIC_UPGRADE_TEMPLATE.format("damage", TERRAN, "vehicles"), + item_names.PROGRESSIVE_TERRAN_VEHICLE_ARMOR: GENERIC_UPGRADE_TEMPLATE.format("armor", TERRAN, "vehicles"), + item_names.PROGRESSIVE_TERRAN_SHIP_WEAPON: GENERIC_UPGRADE_TEMPLATE.format("damage", TERRAN, "starships"), + item_names.PROGRESSIVE_TERRAN_SHIP_ARMOR: GENERIC_UPGRADE_TEMPLATE.format("armor", TERRAN, "starships"), + item_names.PROGRESSIVE_TERRAN_WEAPON_UPGRADE: GENERIC_UPGRADE_TEMPLATE.format("damage", TERRAN, "units"), + item_names.PROGRESSIVE_TERRAN_ARMOR_UPGRADE: GENERIC_UPGRADE_TEMPLATE.format("armor", TERRAN, "units"), + item_names.PROGRESSIVE_TERRAN_INFANTRY_UPGRADE: GENERIC_UPGRADE_TEMPLATE.format("damage and armor", TERRAN, "infantry"), + item_names.PROGRESSIVE_TERRAN_VEHICLE_UPGRADE: GENERIC_UPGRADE_TEMPLATE.format("damage and armor", TERRAN, "vehicles"), + item_names.PROGRESSIVE_TERRAN_SHIP_UPGRADE: GENERIC_UPGRADE_TEMPLATE.format("damage and armor", TERRAN, "starships"), + item_names.PROGRESSIVE_TERRAN_WEAPON_ARMOR_UPGRADE: GENERIC_UPGRADE_TEMPLATE.format("damage and armor", TERRAN, "units"), + item_names.BUNKER_PROJECTILE_ACCELERATOR: "Increases range of all units in the Bunker by 1.", + item_names.BUNKER_NEOSTEEL_BUNKER: "Increases the number of Bunker slots by 2.", + item_names.MISSILE_TURRET_TITANIUM_HOUSING: "Increases Missile Turret life by 75.", + item_names.MISSILE_TURRET_HELLSTORM_BATTERIES: "The Missile Turret unleashes an additional flurry of missiles with each attack.", + item_names.SCV_ADVANCED_CONSTRUCTION: "Multiple SCVs can construct a structure, reducing its construction time.", + item_names.SCV_DUAL_FUSION_WELDERS: "SCVs repair twice as fast.", + item_names.SCV_CONSTRUCTION_JUMP_JETS: "Allows SCVs to jump up and down cliffs.", + item_names.PROGRESSIVE_FIRE_SUPPRESSION_SYSTEM: inspect.cleandoc(""" + Level 1: While on low health, Terran structures are repaired to half health instead of burning down. + Level 2: Terran structures are repaired to full health instead of half health. + """), + item_names.PROGRESSIVE_ORBITAL_COMMAND: inspect.cleandoc(""" + Deprecated. Replaced by Scanner Sweep, MULE, and Orbital Module (Planetary Fortress) + Level 1: Allows Command Centers to use Scanner Sweep and Calldown: MULE abilities. + Level 2: Orbital Command abilities work even in Planetary Fortress mode. + """), + item_names.MARINE_PROGRESSIVE_STIMPACK: STIMPACK_SMALL_DESCRIPTION, + item_names.MARINE_COMBAT_SHIELD: "Increases Marine life by 10.", + item_names.MEDIC_ADVANCED_MEDIC_FACILITIES: INTERNAL_TECH_MODULE_DESCRIPTION_TEMPLATE.format("Medics", "Barracks"), + item_names.MEDIC_STABILIZER_MEDPACKS: "Increases Medic heal speed. Reduces the amount of energy required for each heal.", + item_names.FIREBAT_INCINERATOR_GAUNTLETS: "Increases Firebat's damage radius by 40%.", + item_names.FIREBAT_JUGGERNAUT_PLATING: "Increases Firebat's armor by 2.", + item_names.MARAUDER_CONCUSSIVE_SHELLS: "Marauder attack temporarily slows all units in target area.", + item_names.MARAUDER_KINETIC_FOAM: "Increases Marauder life by 25.", + item_names.REAPER_U238_ROUNDS: inspect.cleandoc(""" + Increases Reaper pistol attack range by 1. + Reaper pistols do additional 3 damage to Light Armor. + """), + item_names.REAPER_G4_CLUSTERBOMB: "Timed explosive that does heavy area damage.", + item_names.CYCLONE_MAG_FIELD_ACCELERATORS: "Increases Cyclone Lock-On damage.", + item_names.CYCLONE_MAG_FIELD_LAUNCHERS: "Increases Cyclone attack range by 2.", + item_names.MARINE_LASER_TARGETING_SYSTEM: LASER_TARGETING_SYSTEMS_DESCRIPTION, + item_names.MARINE_MAGRAIL_MUNITIONS: "Deals 20 damage to target unit. Autocast on attack with a cooldown.", + item_names.MARINE_OPTIMIZED_LOGISTICS: "Increases Marine training speed.", + item_names.MEDIC_RESTORATION: _ability_desc("Medics", "Restoration", "removes negative status effects from a target allied unit"), + item_names.MEDIC_OPTICAL_FLARE: _ability_desc("Medics", "Optical Flare", "reduces vision range of target enemy unit. Disables detection"), + item_names.MEDIC_RESOURCE_EFFICIENCY: _get_resource_efficiency_desc(item_names.MEDIC), + item_names.FIREBAT_PROGRESSIVE_STIMPACK: STIMPACK_LARGE_DESCRIPTION, + item_names.FIREBAT_RESOURCE_EFFICIENCY: _get_resource_efficiency_desc(item_names.FIREBAT), + item_names.MARAUDER_PROGRESSIVE_STIMPACK: STIMPACK_LARGE_DESCRIPTION, + item_names.MARAUDER_LASER_TARGETING_SYSTEM: LASER_TARGETING_SYSTEMS_DESCRIPTION, + item_names.MARAUDER_MAGRAIL_MUNITIONS: "Deals 20 damage to target unit. Autocast on attack with a cooldown.", + item_names.MARAUDER_INTERNAL_TECH_MODULE: INTERNAL_TECH_MODULE_DESCRIPTION_TEMPLATE.format("Marauders", "Barracks"), + item_names.SCV_HOSTILE_ENVIRONMENT_ADAPTATION: "Increases SCV life by 15 and attack speed slightly.", + item_names.MEDIC_ADAPTIVE_MEDPACKS: "Allows Medics to heal mechanical and air units.", + item_names.MEDIC_NANO_PROJECTOR: "Increases Medic heal range by 2.", + item_names.FIREBAT_INFERNAL_PRE_IGNITER: "Firebats do an additional 4 damage to Light Armor.", + item_names.FIREBAT_KINETIC_FOAM: "Increases Firebat life by 100.", + item_names.FIREBAT_NANO_PROJECTORS: "Increases Firebat attack range by 2.", + item_names.MARAUDER_JUGGERNAUT_PLATING: "Increases Marauder's armor by 2.", + item_names.REAPER_JET_PACK_OVERDRIVE: inspect.cleandoc(""" + Allows the Reaper to fly for 10 seconds. + While flying, the Reaper can attack air units. + """), + item_names.HELLION_INFERNAL_PLATING: "Increases Hellion and Hellbat armor by 2.", + item_names.VULTURE_JERRYRIGGED_PATCHUP: "Vultures regenerate life.", + item_names.GOLIATH_SHAPED_HULL: "Increases Goliath life by 25.", + item_names.GOLIATH_RESOURCE_EFFICIENCY: _get_resource_efficiency_desc(item_names.GOLIATH), + item_names.GOLIATH_INTERNAL_TECH_MODULE: INTERNAL_TECH_MODULE_DESCRIPTION_TEMPLATE.format("Goliaths", "Factory"), + item_names.SIEGE_TANK_SHAPED_HULL: "Increases Siege Tank life by 25.", + item_names.SIEGE_TANK_RESOURCE_EFFICIENCY: _get_resource_efficiency_desc(item_names.SIEGE_TANK), + item_names.PREDATOR_CLOAK: "Allows Predators to briefly cloak. Predators ignore unit collision while cloaked.", + item_names.PREDATOR_CHARGE: "Allows Predators to intercept enemy ground units, and applies an AoE slow on arrival.", + item_names.MEDIVAC_SCATTER_VEIL: "Medivacs get 100 shields.", + item_names.REAPER_PROGRESSIVE_STIMPACK: STIMPACK_SMALL_DESCRIPTION, + item_names.REAPER_LASER_TARGETING_SYSTEM: LASER_TARGETING_SYSTEMS_DESCRIPTION, + item_names.REAPER_ADVANCED_CLOAKING_FIELD: "Reapers are permanently cloaked.", + item_names.REAPER_SPIDER_MINES: "Allows Reapers to lay Spider Mines. 3 charges per Reaper.", + item_names.REAPER_COMBAT_DRUGS: "Reapers regenerate life while out of combat.", + item_names.HELLION_HELLBAT: "Allows Hellions to transform into Hellbats.", + item_names.HELLION_SMART_SERVOS: "Transforms faster between modes. Hellions can attack while moving.", + item_names.HELLION_OPTIMIZED_LOGISTICS: "Increases Hellion training speed.", + item_names.HELLION_JUMP_JETS: inspect.cleandoc(""" + Increases movement speed in Hellion mode. + In Hellbat mode, launches the Hellbat toward enemy ground units and briefly stuns them. + """), + item_names.HELLION_PROGRESSIVE_STIMPACK: STIMPACK_LARGE_DESCRIPTION, + item_names.VULTURE_ION_THRUSTERS: "Increases Vulture movement speed.", + item_names.VULTURE_AUTO_LAUNCHERS: "Allows Vultures to attack while moving.", + item_names.SPIDER_MINE_HIGH_EXPLOSIVE_MUNITION: "Increases Spider mine damage.", + item_names.GOLIATH_JUMP_JETS: "Allows Goliaths to jump up and down cliffs.", + item_names.GOLIATH_OPTIMIZED_LOGISTICS: "Increases Goliath training speed.", + item_names.DIAMONDBACK_HYPERFLUXOR: "Increases Diamondback attack speed.", + item_names.DIAMONDBACK_BURST_CAPACITORS: inspect.cleandoc(""" + While not attacking, the Diamondback charges its weapon. + The next attack does 10 additional damage. + """), + item_names.DIAMONDBACK_RESOURCE_EFFICIENCY: _get_resource_efficiency_desc(item_names.DIAMONDBACK), + item_names.SIEGE_TANK_JUMP_JETS: inspect.cleandoc(""" + Repositions Siege Tank to a target location. + Can be used in either mode and to jump up and down cliffs. + """), + item_names.SIEGE_TANK_SPIDER_MINES: inspect.cleandoc(""" + Allows Siege Tanks to lay Spider Mines. + Lays 3 Spider Mines at once. 3 charges. + """), + item_names.SIEGE_TANK_SMART_SERVOS: SMART_SERVOS_DESCRIPTION, + item_names.SIEGE_TANK_GRADUATING_RANGE: inspect.cleandoc(""" + Increases the Siege Tank's attack range by 1 every 3 seconds while in Siege Mode, + up to a maximum of 5 additional range. + """), + item_names.SIEGE_TANK_LASER_TARGETING_SYSTEM: LASER_TARGETING_SYSTEMS_DESCRIPTION, + item_names.SIEGE_TANK_ADVANCED_SIEGE_TECH: "Siege Tanks gain +3 armor in Siege Mode.", + item_names.SIEGE_TANK_INTERNAL_TECH_MODULE: INTERNAL_TECH_MODULE_DESCRIPTION_TEMPLATE.format("Siege Tanks", "Factory"), + item_names.PREDATOR_RESOURCE_EFFICIENCY: _get_resource_efficiency_desc(item_names.PREDATOR), + item_names.MEDIVAC_EXPANDED_HULL: "Increases Medivac cargo space by 4.", + item_names.MEDIVAC_AFTERBURNERS: "Ability. Temporarily increases the Medivac's movement speed by 70%.", + item_names.WRAITH_ADVANCED_LASER_TECHNOLOGY: inspect.cleandoc(""" + Burst Lasers do more damage and can hit both ground and air targets. + Replaces Gemini Missiles weapon. + """), + item_names.VIKING_SMART_SERVOS: SMART_SERVOS_DESCRIPTION, + item_names.VIKING_ANTI_MECHANICAL_MUNITION: "Increases Viking damage to mechanical units while in Assault Mode.", + item_names.DIAMONDBACK_MAGLEV_PROPULSION: "Increases Diamondback movement speed.", + item_names.WARHOUND_RESOURCE_EFFICIENCY: _get_resource_efficiency_desc(item_names.WARHOUND), + item_names.WARHOUND_AXIOM_PLATING: "Increases Warhound armor by 2.", + item_names.WARHOUND_DEPLOY_TURRET: "Each Warhound can deploy a single-use Auto-Turret.", + item_names.HERC_RESOURCE_EFFICIENCY: _get_resource_efficiency_desc(item_names.HERC), + item_names.HERC_JUGGERNAUT_PLATING: "Increases HERC armor by 2.", + item_names.HERC_KINETIC_FOAM: "Increases HERC life by 50.", + item_names.REAPER_RESOURCE_EFFICIENCY: _get_resource_efficiency_desc(item_names.REAPER), + item_names.REAPER_BALLISTIC_FLIGHTSUIT: "Increases Reaper life by 10.", + item_names.SIEGE_TANK_PROGRESSIVE_TRANSPORT_HOOK: inspect.cleandoc(""" + Level 1: Allows Siege Tanks to be transported in Siege Mode. + Level 2: Siege Tanks in Siege Mode can attack air units while transported by a Medivac. + """), + item_names.SIEGE_TANK_ALLTERRAIN_TREADS: "Increases movement speed of Siege Tanks in Tank Mode.", + item_names.MEDIVAC_RAPID_REIGNITION_SYSTEMS: inspect.cleandoc(""" + Slightly increases Medivac movement speed. + Reduces Medivac's Afterburners ability cooldown. + """), + item_names.BATTLECRUISER_BEHEMOTH_REACTOR: "All Battlecruiser spells require 25 less energy to cast.", + item_names.THOR_RAPID_RELOAD: "Increases Thor's ground attack speed.", + item_names.LIBERATOR_GUERILLA_MISSILES: "Liberators in Fighter Mode apply an attack and movement debuff to enemies they attack.", + item_names.WIDOW_MINE_RESOURCE_EFFICIENCY: _get_resource_efficiency_desc(item_names.WIDOW_MINE), + item_names.HERC_GRAPPLE_PULL: "Allows HERCs to use their grappling gun to pull a ground unit towards the HERC.", + item_names.COMMAND_CENTER_SCANNER_SWEEP: "Temporarily reveals an area of the map, detecting cloaked and burrowed units.", + item_names.COMMAND_CENTER_MULE: "Summons a unit that gathers minerals more quickly than regular SCVs. Has timed life.", + item_names.COMMAND_CENTER_EXTRA_SUPPLIES: "Drops additional supplies, permanently increasing the supply output of the target Supply Depot by 8.", + item_names.HELLION_TWIN_LINKED_FLAMETHROWER: "Doubles the width of the Hellion's flame attack.", + item_names.HELLION_THERMITE_FILAMENTS: "Hellions do an additional 10 damage to Light Armor.", + item_names.SPIDER_MINE_CERBERUS_MINE: "Increases trigger and blast radius of Spider Mines.", + item_names.VULTURE_PROGRESSIVE_REPLENISHABLE_MAGAZINE: inspect.cleandoc(""" + Level 1: Allows Vultures to replace used Spider Mines. Costs 15 minerals. + Level 2: Replacing used Spider Mines no longer costs minerals. + """), + item_names.GOLIATH_MULTI_LOCK_WEAPONS_SYSTEM: "Goliaths can attack both ground and air targets simultaneously.", + item_names.GOLIATH_ARES_CLASS_TARGETING_SYSTEM: "Increases Goliath ground attack range by 1 and air by 3.", + item_names.DIAMONDBACK_PROGRESSIVE_TRI_LITHIUM_POWER_CELL: inspect.cleandoc(""" + Level 1: Tri-Lithium Power Cell: Increases Diamondback attack range by 1. + Level 2: Tungsten Spikes: Increases Diamondback attack range by 3. + """), + item_names.DIAMONDBACK_SHAPED_HULL: "Increases Diamondback life by 50.", + item_names.SIEGE_TANK_MAELSTROM_ROUNDS: "Siege Tanks do an additional 40 damage to the primary target in Siege Mode.", + item_names.SIEGE_TANK_SHAPED_BLAST: "Reduces splash damage to friendly targets while in Siege Mode by 75%.", + item_names.MEDIVAC_RAPID_DEPLOYMENT_TUBE: "Medivacs deploy loaded troops almost instantly.", + item_names.MEDIVAC_ADVANCED_HEALING_AI: "Medivacs can heal two targets at once.", + item_names.WRAITH_PROGRESSIVE_TOMAHAWK_POWER_CELLS: inspect.cleandoc(""" + Level 1: Tomahawk Power Cells: Increases Wraith starting energy by 100. + Level 2: Unregistered Cloaking Module: Wraiths do not require energy to cloak and remain cloaked. + """), + item_names.WRAITH_DISPLACEMENT_FIELD: "Wraiths evade 20% of incoming attacks while cloaked.", + item_names.VIKING_RIPWAVE_MISSILES: "Vikings do area damage while in Fighter Mode.", + item_names.VIKING_PHOBOS_CLASS_WEAPONS_SYSTEM: "Increases Viking attack range by 1 in Assault mode and 2 in Fighter mode.", + item_names.BANSHEE_PROGRESSIVE_CROSS_SPECTRUM_DAMPENERS: inspect.cleandoc(""" + Level 1: Banshees can remain cloaked twice as long. + Level 2: Banshees do not require energy to cloak and remain cloaked. + """), + item_names.BANSHEE_SHOCKWAVE_MISSILE_BATTERY: "Banshees do area damage in a straight line.", + item_names.BATTLECRUISER_PROGRESSIVE_MISSILE_PODS: inspect.cleandoc(f""" + {_ability_desc('Battlecruisers', 'Missile Pods', 'deals damage to air units in a target area')} + Level 1: Deals 40 damage (+50 vs light). + Level 2: Deals 110 damage and costs -50 less energy. + """), + item_names.BATTLECRUISER_PROGRESSIVE_DEFENSIVE_MATRIX: inspect.cleandoc(""" + Level 1: Spell. For 20 seconds the Battlecruiser gains a shield that can absorb up to 200 damage. + Level 2: Passive. Battlecruiser gets 200 shields. Can spend energy to fully recharge shields. + """), + item_names.GHOST_OCULAR_IMPLANTS: "Increases Ghost sight range by 3 and attack range by 2.", + item_names.GHOST_CRIUS_SUIT: "Cloak no longer requires energy to activate or maintain.", + item_names.SPECTRE_PSIONIC_LASH: "Spell. Deals 200 damage to a single target.", + item_names.SPECTRE_NYX_CLASS_CLOAKING_MODULE: "Cloak no longer requires energy to activate or maintain.", + item_names.THOR_330MM_BARRAGE_CANNON: inspect.cleandoc(""" + Improves 250mm Strike Cannons ability to deal area damage and stun units in a small area. + Can be also freely aimed on ground. + """), + item_names.THOR_PROGRESSIVE_IMMORTALITY_PROTOCOL: inspect.cleandoc(""" + Level 1: Allows destroyed Thors to be reconstructed on the field. Costs Vespene Gas. + Level 2: Thors are automatically reconstructed after falling for free. + """), + item_names.LIBERATOR_ADVANCED_BALLISTICS: "Increases Liberator range by 3 in Defender Mode.", + item_names.LIBERATOR_RAID_ARTILLERY: "Allows Liberators to attack structures while in Defender Mode.", + item_names.WIDOW_MINE_DRILLING_CLAWS: "Allows Widow Mines to burrow and unburrow faster.", + item_names.WIDOW_MINE_CONCEALMENT: "Burrowed Widow Mines are no longer revealed when the Sentinel Missile is on cooldown.", + item_names.WIDOW_MINE_DEMOLITION_PAYLOAD: "Allows Widow Mines to attack and damage structures.", + item_names.MEDIVAC_ADVANCED_CLOAKING_FIELD: "Medivacs are permanently cloaked.", + item_names.WRAITH_TRIGGER_OVERRIDE: "Wraith attack speed increases by 10% with each attack, up to a maximum of 100%.", + item_names.WRAITH_INTERNAL_TECH_MODULE: INTERNAL_TECH_MODULE_DESCRIPTION_TEMPLATE.format("Wraiths", "Starport"), + item_names.WRAITH_RESOURCE_EFFICIENCY: _get_resource_efficiency_desc(item_names.WRAITH), + item_names.VIKING_SHREDDER_ROUNDS: "Attacks in Assault mode do line splash damage.", + item_names.VIKING_WILD_MISSILES: "Launches 5 rockets at the target unit. Each rocket does 25 (40 vs armored) damage.", + item_names.BANSHEE_SHAPED_HULL: "Increases Banshee life by 100.", + item_names.BANSHEE_ADVANCED_TARGETING_OPTICS: "Increases Banshee attack range by 2 while cloaked.", + item_names.BANSHEE_DISTORTION_BLASTERS: "Increases Banshee attack damage by 25% while cloaked.", + item_names.BANSHEE_ROCKET_BARRAGE: _ability_desc("Banshees", "Rocket Barrage", "deals 75 damage to enemy ground units in the target area"), + item_names.GHOST_RESOURCE_EFFICIENCY: _get_resource_efficiency_desc(item_names.GHOST), + item_names.GHOST_BARGAIN_BIN_PRICES: _get_resource_efficiency_desc(item_names.GHOST, op_re_cost_reduction), + item_names.SPECTRE_RESOURCE_EFFICIENCY: _get_resource_efficiency_desc(item_names.SPECTRE), + item_names.SPECTRE_BARGAIN_BIN_PRICES: _get_resource_efficiency_desc(item_names.SPECTRE, op_re_cost_reduction), + item_names.THOR_BUTTON_WITH_A_SKULL_ON_IT: "Allows Thors to launch nukes.", + item_names.THOR_LASER_TARGETING_SYSTEM: LASER_TARGETING_SYSTEMS_DESCRIPTION, + item_names.THOR_LARGE_SCALE_FIELD_CONSTRUCTION: "Allows Thors to be built by SCVs like a structure.", + item_names.RAVEN_RESOURCE_EFFICIENCY: _get_resource_efficiency_desc(item_names.RAVEN), + item_names.RAVEN_DURABLE_MATERIALS: "Extends timed life duration of Raven's summoned objects.", + item_names.SCIENCE_VESSEL_IMPROVED_NANO_REPAIR: "Nano-Repair no longer requires energy to use.", + item_names.SCIENCE_VESSEL_MAGELLAN_COMPUTATION_SYSTEMS: "Science Vessel can use Nano-Repair at two targets at once.", + item_names.CYCLONE_RESOURCE_EFFICIENCY: _get_resource_efficiency_desc(item_names.CYCLONE), + item_names.BANSHEE_HYPERFLIGHT_ROTORS: "Increases Banshee movement speed.", + item_names.BANSHEE_LASER_TARGETING_SYSTEM: LASER_TARGETING_SYSTEMS_DESCRIPTION, + item_names.BANSHEE_INTERNAL_TECH_MODULE: INTERNAL_TECH_MODULE_DESCRIPTION_TEMPLATE.format("Banshees", "Starport"), + item_names.BATTLECRUISER_TACTICAL_JUMP: inspect.cleandoc(""" + Allows Battlecruisers to warp to a target location anywhere on the map. + """), + item_names.BATTLECRUISER_CLOAK: CLOAK_DESCRIPTION_TEMPLATE.format("Battlecruisers"), + item_names.BATTLECRUISER_ATX_LASER_BATTERY: inspect.cleandoc(""" + Battlecruisers can attack while moving, + do the same damage to both ground and air targets, and fire faster. + """), + item_names.BATTLECRUISER_OPTIMIZED_LOGISTICS: "Increases Battlecruiser training speed.", + item_names.BATTLECRUISER_INTERNAL_TECH_MODULE: INTERNAL_TECH_MODULE_DESCRIPTION_TEMPLATE.format("Battlecruisers", "Starport"), + item_names.GHOST_EMP_ROUNDS: inspect.cleandoc(""" + Spell. Does 100 damage to shields and drains all energy from units in the targeted area. + Cloaked units hit by EMP are revealed for a short time. + """), + item_names.GHOST_LOCKDOWN: "Spell. Stuns a target mechanical unit for a long time.", + item_names.SPECTRE_IMPALER_ROUNDS: "Spectres do additional damage to armored targets.", + item_names.THOR_PROGRESSIVE_HIGH_IMPACT_PAYLOAD: inspect.cleandoc(f""" + Level 1: Allows Thors to transform in order to use an alternative air attack. + Level 2: {SMART_SERVOS_DESCRIPTION} + """), + item_names.RAVEN_BIO_MECHANICAL_REPAIR_DRONE: "Spell. Deploys a drone that can heal biological or mechanical units.", + item_names.RAVEN_SPIDER_MINES: "Spell. Deploys 3 Spider Mines to a target location.", + item_names.RAVEN_RAILGUN_TURRET: inspect.cleandoc(""" + Spell. Allows Ravens to deploy an advanced Auto-Turret, that can attack enemy ground units in a straight line. + """), + item_names.RAVEN_HUNTER_SEEKER_WEAPON: "Allows Ravens to attack with a Hunter-Seeker weapon.", + item_names.RAVEN_INTERFERENCE_MATRIX: inspect.cleandoc(""" + Spell. Target enemy Mechanical or Psionic unit can't attack or use abilities for a short duration. + """), + item_names.RAVEN_ANTI_ARMOR_MISSILE: "Spell. Decreases target and nearby enemy units armor by 2.", + item_names.RAVEN_INTERNAL_TECH_MODULE: INTERNAL_TECH_MODULE_DESCRIPTION_TEMPLATE.format("Ravens", "Starport"), + item_names.SCIENCE_VESSEL_EMP_SHOCKWAVE: "Spell. Depletes all energy and shields of all units in a target area.", + item_names.SCIENCE_VESSEL_DEFENSIVE_MATRIX: inspect.cleandoc(""" + Spell. Provides a target unit with a defensive barrier that can absorb up to 250 damage. + """), + item_names.CYCLONE_TARGETING_OPTICS: "Increases Cyclone Lock On casting range and the range while Locked On.", + item_names.CYCLONE_RAPID_FIRE_LAUNCHERS: "The first 12 shots of Lock On are fired more quickly.", + item_names.LIBERATOR_CLOAK: CLOAK_DESCRIPTION_TEMPLATE.format("Liberators"), + item_names.LIBERATOR_LASER_TARGETING_SYSTEM: LASER_TARGETING_SYSTEMS_DESCRIPTION, + item_names.LIBERATOR_OPTIMIZED_LOGISTICS: "Increases Liberator training speed.", + item_names.WIDOW_MINE_BLACK_MARKET_LAUNCHERS: "Increases Widow Mine Sentinel Missile range.", + item_names.WIDOW_MINE_EXECUTIONER_MISSILES: inspect.cleandoc(""" + Reduces Sentinel Missile cooldown. + When killed, Widow Mines will launch several missiles at random enemy targets. + """), + item_names.VALKYRIE_ENHANCED_CLUSTER_LAUNCHERS: "Valkyries fire 2 additional rockets each volley.", + item_names.VALKYRIE_SHAPED_HULL: "Increases Valkyrie life by 50.", + item_names.VALKYRIE_FLECHETTE_MISSILES: "Equips Valkyries with Air-to-Surface missiles to attack ground units.", + item_names.VALKYRIE_AFTERBURNERS: "Ability. Temporarily increases the Valkyries's movement speed by 70%.", + item_names.CYCLONE_INTERNAL_TECH_MODULE: INTERNAL_TECH_MODULE_DESCRIPTION_TEMPLATE.format("Cyclones", "Factory"), + item_names.LIBERATOR_SMART_SERVOS: SMART_SERVOS_DESCRIPTION, + item_names.LIBERATOR_RESOURCE_EFFICIENCY: _get_resource_efficiency_desc(item_names.LIBERATOR), + item_names.HERCULES_INTERNAL_FUSION_MODULE: "Hercules can be trained from a Starport without having a Fusion Core.", + item_names.HERCULES_TACTICAL_JUMP: inspect.cleandoc(""" + Allows Hercules to warp to a target location anywhere on the map. + """), + item_names.PLANETARY_FORTRESS_PROGRESSIVE_AUGMENTED_THRUSTERS: inspect.cleandoc(""" + Level 1: Lift Off - Planetary Fortress can lift off. + Level 2: Armament Stabilizers - Planetary Fortress can attack while lifted off. + """), + item_names.PLANETARY_FORTRESS_IBIKS_TRACKING_SCANNERS: "Planetary Fortress can attack air units.", + item_names.VALKYRIE_LAUNCHING_VECTOR_COMPENSATOR: "Allows Valkyries to shoot air while moving.", + item_names.VALKYRIE_RESOURCE_EFFICIENCY: _get_resource_efficiency_desc(item_names.VALKYRIE), + item_names.PREDATOR_VESPENE_SYNTHESIS: "Gives 1 free Vespene per target hit with Lightning Field.", + item_names.PREDATOR_ADAPTIVE_DEFENSES: "Predators gain a shield that halves incoming ranged and splash damage while active.", + item_names.BATTLECRUISER_BEHEMOTH_PLATING: "Increases Battlecruiser armor by 2.", + item_names.BATTLECRUISER_MOIRAI_IMPULSE_DRIVE: "Increases Battlecruiser movement speed.", + item_names.PLANETARY_FORTRESS_ORBITAL_MODULE: inspect.cleandoc(""" + Allows Planetary Fortresses to use Scanner Sweep, MULE, and Extra Supplies if those abilities are owned. + """), + item_names.DEVASTATOR_TURRET_CONCUSSIVE_GRENADES: "Devastator Turrets slow enemies they hit. Does not stack with Marauder Concussive Shells.", + item_names.DEVASTATOR_TURRET_ANTI_ARMOR_MUNITIONS: "Increases Devastator Turret damage to armored targets by 10.", + item_names.DEVASTATOR_TURRET_RESOURCE_EFFICIENCY: _get_resource_efficiency_desc(item_names.DEVASTATOR_TURRET), + item_names.MISSILE_TURRET_RESOURCE_EFFICENCY: _get_resource_efficiency_desc(item_names.MISSILE_TURRET), + item_names.SENSOR_TOWER_ASSISTIVE_TARGETING: "Sensor Towers increase the attack range of defensive buildings in their direct sight range.", + item_names.SENSOR_TOWER_MUILTISPECTRUM_DOPPLER: "Sensor Towers gain +10 sight range and +5 radar range.", + item_names.SCIENCE_VESSEL_TACTICAL_JUMP: "Allows Science Vessels to warp to a target location anywhere on the map.", + item_names.LIBERATOR_UED_MISSILE_TECHNOLOGY: "Increases Liberator attack range in Fighter mode by 4.", + item_names.BATTLECRUISER_FIELD_ASSIST_TARGETING_SYSTEM: "Battlecruisers increase the attack range of nearby friendly ground units by 1.", + item_names.VIKING_AESIR_TURBINES: "Increases Viking movement speed by 55%.", + item_names.MEDIVAC_RESOURCE_EFFICIENCY: _get_resource_efficiency_desc(item_names.MEDIVAC), + item_names.EMPERORS_SHADOW_SOVEREIGN_TACTICAL_MISSILES: "Tactical Missile Strikes no longer need to be channeled.", + item_names.DOMINION_TROOPER_B2_HIGH_CAL_LMG: "Allows the Troopers to arm with a more powerful weapon, effective against all unit types.", + item_names.DOMINION_TROOPER_HAILSTORM_LAUNCHER: "Allows the Troopers to arm with a more powerful weapon, especially effective against armored air units.", + item_names.DOMINION_TROOPER_CPO7_SALAMANDER_FLAMETHROWER: "Allows the Troopers to arm with a more powerful weapon, especially effective against light ground units.", + item_names.DOMINION_TROOPER_ADVANCED_ALLOYS: "Trooper weapons cost 20 fewer gas and now last for 5 minutes when dropped on death.", + item_names.DOMINION_TROOPER_OPTIMIZED_LOGISTICS: "Increases Dominion Trooper training speed.", + item_names.BUNKER: "Defensive structure. Able to load infantry units, giving them +1 range to their attacks.", + item_names.MISSILE_TURRET: "Anti-air defensive structure.", + item_names.SENSOR_TOWER: "Reveals locations of enemy units at long range.", + item_names.WAR_PIGS: "Mercenary Marines.", + item_names.DEVIL_DOGS: "Mercenary Firebats.", + item_names.HAMMER_SECURITIES: "Mercenary Marauders.", + item_names.SPARTAN_COMPANY: "Mercenary Goliaths.", + item_names.SIEGE_BREAKERS: "Mercenary Siege Tanks.", + item_names.HELS_ANGELS: "Mercenary Vikings.", + item_names.DUSK_WINGS: "Mercenary Banshees.", + item_names.JACKSONS_REVENGE: "Mercenary Battlecruiser.", + item_names.SKIBIS_ANGELS: "Mercenary Medics.", + item_names.DEATH_HEADS: "Mercenary Reapers.", + item_names.WINGED_NIGHTMARES: "Mercenary Wraiths.", + item_names.MIDNIGHT_RIDERS: "Mercenary Liberators.", + item_names.BRYNHILDS: "Mercenary Valkyries.", + item_names.JOTUN: "Mercenary Thor.", + item_names.ULTRA_CAPACITORS: "Increases attack speed of units by 5% per weapon upgrade.", + item_names.VANADIUM_PLATING: "Increases the life of units by 5% per armor upgrade.", + item_names.ORBITAL_DEPOTS: "Supply depots are built instantly.", + item_names.MICRO_FILTERING: "Refineries produce Vespene gas 25% faster.", + item_names.AUTOMATED_REFINERY: "Eliminates the need for SCVs in vespene gas production.", + item_names.COMMAND_CENTER_COMMAND_CENTER_REACTOR: "Command Centers can train two SCVs at once.", + item_names.RAVEN: "Aerial Caster unit.", + item_names.SCIENCE_VESSEL: "Aerial Caster unit. Can repair mechanical units.", + item_names.TECH_REACTOR: "Merges Tech Labs and Reactors into one add on structure to provide both functions.", + item_names.ORBITAL_STRIKE: "Trained units from Barracks are instantly deployed on rally point.", + item_names.BUNKER_SHRIKE_TURRET: "Adds an automated turret to Bunkers.", + item_names.BUNKER_FORTIFIED_BUNKER: "Bunkers have more life.", + item_names.PLANETARY_FORTRESS: inspect.cleandoc(""" + Allows Command Centers to upgrade into a defensive structure with a turret and additional armor. + Planetary Fortresses cannot Lift Off, or cast Orbital Command spells. + """), + item_names.PERDITION_TURRET: "Automated defensive turret. Burrows down while no enemies are nearby.", + item_names.PREDATOR: "Anti-infantry specialist that deals area damage with each attack.", + item_names.HERCULES: "Massive transport ship.", + item_names.CELLULAR_REACTOR: "All Terran spellcasters get +100 starting and maximum energy.", + item_names.PROGRESSIVE_REGENERATIVE_BIO_STEEL: inspect.cleandoc(""" + Allows Terran mechanical units to regenerate health while not in combat. + Each level increases life regeneration speed. + """), + item_names.HIVE_MIND_EMULATOR: "Unlocks the Hive Mind Emulator defensive structure, and allows it to permanently Mind Control Zerg units.", + item_names.ARGUS_AMPLIFIER: "Unlocks the Hive Mind Emulator defensive structure, and allows it to permanently Mind Control Protoss units.", + item_names.PSI_INDOCTRINATOR: "Unlocks the Hive Mind Emulator defensive structure, and allows it to permanently Mind Control Terran units.", + item_names.PSI_DISRUPTER: "Unlocks the Psi Disrupter defensive structure, and allows it to slow the attack and movement speeds of all nearby Zerg units.", + item_names.PSI_SCREEN: "Unlocks the Psi Disrupter defensive structure, and allows it to slow the attack and movement speeds of all nearby Protoss units.", + item_names.SONIC_DISRUPTER: "Unlocks the Psi Disrupter defensive structure, and allows it to slow the attack and movement speeds of all nearby Terran units.", + item_names.DEVASTATOR_TURRET: "Defensive structure. Deals increased damage to armored targets. Attacks ground units.", + item_names.STRUCTURE_ARMOR: "Increases armor of all Terran structures by 2.", + item_names.HI_SEC_AUTO_TRACKING: "Increases attack range of all Terran structures by 1.", + item_names.ADVANCED_OPTICS: "Increases attack range of all Terran mechanical units by 1.", + item_names.ROGUE_FORCES: "Terran Mercenary calldowns are no longer limited by charges.", + item_names.MECHANICAL_KNOW_HOW: "Increases mechanical unit life by 20%.", + item_names.MERCENARY_MUNITIONS: "Increases attack speed of all Terran combat units by 15%.", + item_names.PROGRESSIVE_FAST_DELIVERY: "At level 1, you can request one Mercenary unit immediately at the start of a mission. Level 2 allows you to calldown 3 Mercenary units immediately.", + item_names.RAPID_REINFORCEMENT: "Reduces cooldowns of all Terran Mercenary calldowns by 60s.", + item_names.SIGNAL_BEACON: "Terran Mercenary Calldowns are instantly deployed on rally point.", + item_names.FUSION_CORE_FUSION_REACTOR: "Fusion Cores increase the energy regeneration of nearby units by +1 energy per second.", + item_names.ZEALOT: "Powerful melee warrior. Can use the charge ability.", + item_names.STALKER: "Ranged attack strider. Can use the Blink ability.", + item_names.HIGH_TEMPLAR: "Potent psionic master. Can use the Feedback and Psionic Storm abilities. Can merge into an Archon.", + item_names.DARK_TEMPLAR: "Deadly warrior-assassin. Permanently cloaked. Can use the Shadow Fury ability.", + item_names.IMMORTAL: "Assault strider. Can use Barrier to absorb damage.", + item_names.COLOSSUS: "Battle strider with a powerful area attack. Can walk up and down cliffs. Attacks set fire to the ground, dealing extra damage to enemies over time.", + item_names.PHOENIX: "Air superiority starfighter. Can use Graviton Beam and Phasing Armor abilities.", + item_names.VOID_RAY: "Surgical strike craft. Has the Prismatic Alignment and Prismatic Range abilities.", + item_names.CARRIER: "Capital ship. Builds and launches Interceptors that attack enemy targets. Repair Drones heal nearby mechanical units.", + item_names.STARTING_MINERALS: "Increases the starting minerals for all missions.", + item_names.STARTING_VESPENE: "Increases the starting vespene for all missions.", + item_names.STARTING_SUPPLY: "Increases the starting supply for all missions.", + item_names.NOTHING: "Does nothing. Used to remove a location from the game.", + item_names.MAX_SUPPLY: "Increases the maximum supply cap for all missions.", + item_names.REDUCED_MAX_SUPPLY: "Trap Item. Decreases the maximum supply cap for all missions.", + item_names.SHIELD_REGENERATION: "Increases shield regeneration of all own units.", + item_names.BUILDING_CONSTRUCTION_SPEED: "Increases building construction speed.", + item_names.UPGRADE_RESEARCH_SPEED: "Increases weapon and armor research speed.", + item_names.UPGRADE_RESEARCH_COST: "Decreases weapon and armor upgrade research cost.", + item_names.NOVA_GHOST_VISOR: "Reveals the locations of enemy units in the fog of war around Nova. Can detect cloaked units.", + item_names.NOVA_RANGEFINDER_OCULUS: "Increases Nova's vision range and non-melee weapon attack range by 2. Also increases range of melee weapons by 1.", + item_names.NOVA_DOMINATION: "Gives Nova the ability to mind-control a target enemy unit.", + item_names.NOVA_BLINK: "Gives Nova the ability to teleport a short distance and cloak for 10s.", + item_names.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE: inspect.cleandoc(""" + Level 1: Gives Nova the ability to cloak. + Level 2: Nova is permanently cloaked. + """), + item_names.NOVA_ENERGY_SUIT_MODULE: "Increases Nova's maximum energy and energy regeneration rate.", + item_names.NOVA_ARMORED_SUIT_MODULE: "Increases Nova's health by 100 and armor by 1. Nova also regenerates life quickly out of combat.", + item_names.NOVA_JUMP_SUIT_MODULE: "Increases Nova's movement speed and allows her to jump up and down cliffs.", + item_names.NOVA_C20A_CANISTER_RIFLE: "Allows Nova to equip the C20A Canister Rifle, which has a ranged attack and allows Nova to cast Snipe.", + item_names.NOVA_HELLFIRE_SHOTGUN: "Allows Nova to equip the Hellfire Shotgun, which has a short-range area attack in a cone and allows Nova to cast Penetrating Blast.", + item_names.NOVA_PLASMA_RIFLE: "Allows Nova to equip the Plasma Rifle, which has a rapidfire ranged attack and allows Nova to cast Plasma Shot.", + item_names.NOVA_MONOMOLECULAR_BLADE: "Allows Nova to equip the Monomolecular Blade, which has a melee attack and allows Nova to cast Dash Attack.", + item_names.NOVA_BLAZEFIRE_GUNBLADE: "Allows Nova to equip the Blazefire Gunblade, which has a melee attack and allows Nova to cast Fury of One.", + item_names.NOVA_STIM_INFUSION: "Gives Nova the ability to heal herself and temporarily increase her movement and attack speeds.", + item_names.NOVA_PULSE_GRENADES: "Gives Nova the ability to throw a grenade dealing large damage in an area.", + item_names.NOVA_FLASHBANG_GRENADES: "Gives Nova the ability to throw a grenade to stun enemies and disable detection in a large area.", + item_names.NOVA_IONIC_FORCE_FIELD: "Gives Nova the ability to shield herself temporarily.", + item_names.NOVA_HOLO_DECOY: "Gives Nova the ability to summon a decoy unit which enemies will prefer to target and takes reduced damage.", + item_names.NOVA_NUKE: "Gives Nova the ability to launch tactical nukes built from the Shadow Ops.", + item_names.ZERGLING: "Fast inexpensive melee attacker. Hatches in pairs from a single larva. Can morph into a Baneling.", + item_names.SWARM_QUEEN: "Ranged support caster. Can use the Spawn Creep Tumor and Rapid Transfusion abilities.", + item_names.ROACH: "Durable short ranged attacker. Regenerates life quickly when burrowed.", + item_names.HYDRALISK: "High-damage generalist ranged attacker.", + item_names.ZERGLING_BANELING_ASPECT: "Anti-ground suicide unit. Does damage over a small area on death. Morphed from the Zergling.", + item_names.ABERRATION: "Durable melee attacker that deals heavy damage and can walk over other units.", + item_names.MUTALISK: "Fragile flying attacker. Attacks bounce between targets.", + item_names.SWARM_HOST: "Siege unit that attacks by rooting in place and continually spawning Locusts.", + item_names.INFESTOR: "Support caster that can move while burrowed. Can use the Fungal Growth, Parasitic Domination, and Consumption abilities.", + item_names.ULTRALISK: "Massive melee attacker. Has an area-damage cleave attack.", + item_names.PYGALISK: "Miniature melee attacker.", + item_names.SPORE_CRAWLER: "Anti-air defensive structure. Detects cloaked units and can uproot.", + item_names.SPINE_CRAWLER: "Anti-ground defensive structure. Can uproot to reposition itself.", + item_names.BILE_LAUNCHER: "Long-range anti-ground bombardment structure.", + item_names.CORRUPTOR: "Anti-air flying attacker specializing in taking down enemy capital ships.", + item_names.SCOURGE: "Flying anti-air suicide unit. Hatches in pairs from a single larva.", + item_names.BROOD_QUEEN: "Flying support caster. Can cast the Ocular Symbiote and Spawn Broodlings abilities.", + item_names.DEFILER: "Support caster. Can use the Dark Swarm, Consume, and Plague abilities.", + item_names.INFESTED_MARINE: "General-purpose Infested infantry. Has a timed life of 90 seconds.", + item_names.INFESTED_BUNKER: "Defensive structure. Periodically spawns Infested infantry that fight from inside. Acts as a mobile ground transport while uprooted.", + item_names.INFESTED_MISSILE_TURRET: "Anti-air defensive structure. Detects cloaked units and can uproot.", + item_names.INFESTED_SIEGE_TANK: "Siege tank. Can uproot itself to provide mobile tank support.", + item_names.INFESTED_DIAMONDBACK: "Fast, high-damage attacker. Can attack while moving and can bring flying units to the ground.", + item_names.BULLFROG: "Grounded transport. Launches itself through the air, dealing damage and unloading cargo on impact.", + item_names.INFESTED_BANSHEE: "Tactical-strike aircraft. Can cloak and can be upgraded to burrow.", + item_names.INFESTED_LIBERATOR: "Anti-Air flying attacker. Attacks deal high area-damage.", + item_names.PROGRESSIVE_ZERG_MELEE_ATTACK: GENERIC_UPGRADE_TEMPLATE.format("damage", ZERG, "melee ground units"), + item_names.PROGRESSIVE_ZERG_MISSILE_ATTACK: GENERIC_UPGRADE_TEMPLATE.format("damage", ZERG, "ranged ground units"), + item_names.PROGRESSIVE_ZERG_GROUND_CARAPACE: GENERIC_UPGRADE_TEMPLATE.format("armor", ZERG, "ground units"), + item_names.PROGRESSIVE_ZERG_FLYER_ATTACK: GENERIC_UPGRADE_TEMPLATE.format("damage", ZERG, "flyers"), + item_names.PROGRESSIVE_ZERG_FLYER_CARAPACE: GENERIC_UPGRADE_TEMPLATE.format("armor", ZERG, "flyers"), + item_names.PROGRESSIVE_ZERG_WEAPON_UPGRADE: GENERIC_UPGRADE_TEMPLATE.format("damage", ZERG, "units"), + item_names.PROGRESSIVE_ZERG_ARMOR_UPGRADE: GENERIC_UPGRADE_TEMPLATE.format("armor", ZERG, "units"), + item_names.PROGRESSIVE_ZERG_GROUND_UPGRADE: GENERIC_UPGRADE_TEMPLATE.format("damage and armor", ZERG, "ground units"), + item_names.PROGRESSIVE_ZERG_FLYER_UPGRADE: GENERIC_UPGRADE_TEMPLATE.format("damage and armor", ZERG, "flyers"), + item_names.PROGRESSIVE_ZERG_WEAPON_ARMOR_UPGRADE: GENERIC_UPGRADE_TEMPLATE.format("damage and armor", ZERG, "units"), + item_names.ZERGLING_HARDENED_CARAPACE: "Increases Zergling health by +10.", + item_names.ZERGLING_ADRENAL_OVERLOAD: "Increases Zergling attack speed.", + item_names.ZERGLING_METABOLIC_BOOST: "Increases Zergling movement speed.", + item_names.ROACH_HYDRIODIC_BILE: "Roaches deal +8 damage to light targets.", + item_names.ROACH_ADAPTIVE_PLATING: "Roaches gain +3 armor when their life is below 50%.", + item_names.ROACH_TUNNELING_CLAWS: "Allows Roaches to move while burrowed.", + item_names.HYDRALISK_FRENZY: "Allows Hydralisks to use the Frenzy ability, which increases their attack speed by 50%.", + item_names.HYDRALISK_ANCILLARY_CARAPACE: "Hydralisks gain +20 health.", + item_names.HYDRALISK_GROOVED_SPINES: "Hydralisks gain +1 range.", + item_names.BANELING_CORROSIVE_ACID: "Increases the damage banelings deal to their primary target. Splash damage remains the same.", + item_names.BANELING_RUPTURE: "Increases the splash radius of baneling attacks.", + item_names.BANELING_REGENERATIVE_ACID: "Banelings will heal nearby friendly units when they explode.", + item_names.MUTALISK_VICIOUS_GLAIVE: "Mutalisks attacks will bounce an additional 3 times.", + item_names.MUTALISK_RAPID_REGENERATION: "Mutalisks will regenerate quickly when out of combat.", + item_names.MUTALISK_SUNDERING_GLAIVE: "Mutalisks deal increased damage to their primary target.", + item_names.SWARM_HOST_BURROW: "Allows Swarm Hosts to burrow instead of root to spawn locusts.", + item_names.SWARM_HOST_RAPID_INCUBATION: "Swarm Hosts will spawn locusts 20% faster.", + item_names.SWARM_HOST_PRESSURIZED_GLANDS: "Allows Swarm Host Locusts to attack air targets.", + item_names.ULTRALISK_BURROW_CHARGE: "Allows Ultralisks to burrow and charge at enemy units, knocking back and stunning units when it emerges.", + item_names.ULTRALISK_TISSUE_ASSIMILATION: "Ultralisks recover health when they deal damage.", + item_names.ULTRALISK_MONARCH_BLADES: "Ultralisks gain increased splash damage.", + item_names.PYGALISK_STIM: _ability_desc("Pygalisks", "Stimpack", f"temporarily increases movement and attack speed at the cost of {STIMPACK_SMALL_COST} health"), + item_names.PYGALISK_DUCAL_BLADES: "Pygalisks do splash damage.", + item_names.PYGALISK_COMBAT_CARAPACE: "Increases Pygalisk health by +25.", + item_names.CORRUPTOR_CAUSTIC_SPRAY: "Allows Corruptors to use the Caustic Spray ability, which deals ramping damage to buildings over time.", + item_names.CORRUPTOR_CORRUPTION: "Allows Corruptors to use the Corruption ability, which causes a target enemy unit to take increased damage.", + item_names.SCOURGE_VIRULENT_SPORES: "Scourge will deal splash damage.", + item_names.SCOURGE_RESOURCE_EFFICIENCY: _get_resource_efficiency_desc(item_names.SCOURGE), + item_names.SCOURGE_SWARM_SCOURGE: "An extra Scourge will be built from each egg at no additional cost.", + item_names.ZERGLING_SHREDDING_CLAWS: "Zergling attacks will temporarily reduce their target's armor to 0.", + item_names.ROACH_GLIAL_RECONSTITUTION: "Increases Roach movement speed.", + item_names.ROACH_ORGANIC_CARAPACE: "Increases Roach health by +25.", + item_names.HYDRALISK_MUSCULAR_AUGMENTS: "Increases Hydralisk movement speed.", + item_names.HYDRALISK_RESOURCE_EFFICIENCY: _get_resource_efficiency_desc(item_names.HYDRALISK), + item_names.BANELING_CENTRIFUGAL_HOOKS: "Increases the movement speed of Banelings.", + item_names.BANELING_TUNNELING_JAWS: "Allows Banelings to move while burrowed.", + item_names.BANELING_RAPID_METAMORPH: "Banelings morph faster and no longer cost vespene gas to morph.", + item_names.MUTALISK_SEVERING_GLAIVE: "Mutalisk bounce attacks will deal full damage.", + item_names.MUTALISK_AERODYNAMIC_GLAIVE_SHAPE: "Increases the attack range of Mutalisks by 2.", + item_names.SWARM_HOST_LOCUST_METABOLIC_BOOST: "Increases Locust movement speed.", + item_names.SWARM_HOST_ENDURING_LOCUSTS: "Increases the duration of Swarm Hosts' Locusts by 10s.", + item_names.SWARM_HOST_ORGANIC_CARAPACE: "Increases Swarm Host health by +40.", + item_names.SWARM_HOST_RESOURCE_EFFICIENCY: _get_resource_efficiency_desc(item_names.SWARM_HOST), + item_names.ULTRALISK_ANABOLIC_SYNTHESIS: "Ultralisks gain increased movement speed.", + item_names.ULTRALISK_CHITINOUS_PLATING: "Ultralisks gain +2 armor.", + item_names.ULTRALISK_ORGANIC_CARAPACE: "Ultralisks gain +100 life.", + item_names.ULTRALISK_RESOURCE_EFFICIENCY: _get_resource_efficiency_desc(item_names.ULTRALISK), + item_names.DEVOURER_CORROSIVE_SPRAY: "Devourer attacks will now deal area damage.", + item_names.DEVOURER_GAPING_MAW: "Devourer's attack speed increased by 25%.", + item_names.DEVOURER_IMPROVED_OSMOSIS: "Devourer's Acid Spores duration increased by 50%.", + item_names.DEVOURER_PRESCIENT_SPORES: "Allows Devourers to attack ground targets.", + item_names.GUARDIAN_PROLONGED_DISPERSION: "Guardians gain +3 range.", + item_names.GUARDIAN_PRIMAL_ADAPTATION: "Allows Guardians to attack air units with a decreased attack damage.", + item_names.GUARDIAN_SORONAN_ACID: "Guardians deal +10 increased base damage to ground targets.", + item_names.GUARDIAN_PROPELLANT_SACS: "Guardians gain increased movement speed.", + item_names.GUARDIAN_EXPLOSIVE_SPORES: "Allows Guardians to launch an explosive spore at ground targets, dealing damage and knocking them back in an area.", + item_names.GUARDIAN_PRIMORDIAL_FURY: "Guardians gain increasing attack speed as they attack.", + item_names.IMPALER_ADAPTIVE_TALONS: "Impalers burrow faster.", + item_names.IMPALER_SECRETION_GLANDS: "Impalers generate creep while standing still or burrowed.", + item_names.IMPALER_SUNKEN_SPINES: "Impalers deal increased damage.", + item_names.LURKER_SEISMIC_SPINES: "Lurkers gain +6 range.", + item_names.LURKER_ADAPTED_SPINES: "Lurkers deal increased damage to non-light targets.", + item_names.RAVAGER_POTENT_BILE: "Ravager Corrosive Bile deals an additional +40 damage.", + item_names.RAVAGER_BLOATED_BILE_DUCTS: "Ravager Corrosive Bile hits a much larger area.", + item_names.RAVAGER_DEEP_TUNNEL: _ability_desc("Ravagers", "Deep Tunnel", "allows them to burrow to any visible location on the map"), + item_names.VIPER_PARASITIC_BOMB: _ability_desc("Vipers", "Parasitic Bomb", "inflicts an area-damaging effect on an enemy air unit"), + item_names.VIPER_PARALYTIC_BARBS: "Viper Abduct stuns units for an additional 5 seconds.", + item_names.VIPER_VIRULENT_MICROBES: "All Viper abilities gain +4 range.", + item_names.BROOD_LORD_POROUS_CARTILAGE: "Brood Lords gain increased movement speed.", + item_names.BROOD_LORD_BEHEMOTH_STELLARSKIN: "Brood Lords gain +100 life and +1 armor.", + item_names.BROOD_LORD_SPLITTER_MITOSIS: "Brood Lord attacks spawn twice as many broodlings.", + item_names.BROOD_LORD_RESOURCE_EFFICIENCY: _get_resource_efficiency_desc(DISPLAY_NAME_BROOD_LORD), + item_names.INFESTOR_INFESTED_TERRAN: _ability_desc("Infestors", "Spawn Infested Terran"), + item_names.INFESTOR_MICROBIAL_SHROUD: _ability_desc("Infestors", "Microbial Shroud", "reduces incoming damage from air units in an area"), + item_names.SPORE_CRAWLER_BIO_BONUS: "Spore Crawler gain +30 bonus damage against biological units.", + item_names.SWARM_QUEEN_SPAWN_LARVAE: _ability_desc("Swarm Queens", "Spawn Larvae"), + item_names.SWARM_QUEEN_DEEP_TUNNEL: _ability_desc("Swarm Queens", "Deep Tunnel"), + item_names.SWARM_QUEEN_ORGANIC_CARAPACE: "Swarm Queens gain +25 life.", + item_names.SWARM_QUEEN_BIO_MECHANICAL_TRANSFUSION: "Swarm Queen Burst Heal heals an additional +10 life and can now target mechanical units.", + item_names.SWARM_QUEEN_RESOURCE_EFFICIENCY: _get_resource_efficiency_desc(item_names.SWARM_QUEEN), + item_names.SWARM_QUEEN_INCUBATOR_CHAMBER: "Swarm Queens may now be built two at a time from the Hatchery, Lair, or Hive.", + item_names.BROOD_QUEEN_FUNGAL_GROWTH: _ability_desc("Brood Queens", "Fungal Growth"), + item_names.BROOD_QUEEN_ENSNARE: _ability_desc("Brood Queens", "Ensnare"), + item_names.BROOD_QUEEN_ENHANCED_MITOCHONDRIA: "Brood Queens start with maximum energy and gain increased energy regeneration. Like powerhouses (of the cell).", + item_names.DEFILER_PATHOGEN_PROJECTORS: "Defilers gain +4 cast range for Dark Swarm and Plague.", + item_names.DEFILER_TRAPDOOR_ADAPTATION: "Defilers can now use abilities while burrowed.", + item_names.DEFILER_PREDATORY_CONSUMPTION: "Defilers can now use Consume on any non-heroic biological unit, not just friendly Zerg.", + item_names.DEFILER_COMORBIDITY: "Plague now stacks up to three times, and depletes energy as well as health.", + item_names.ABERRATION_MONSTROUS_RESILIENCE: "Aberrations gain +140 life.", + item_names.ABERRATION_CONSTRUCT_REGENERATION: "Aberrations gain increased life regeneration.", + item_names.ABERRATION_PROTECTIVE_COVER: "Aberrations grant damage reduction to allied units directly beneath them.", + item_names.ABERRATION_BANELING_INCUBATION: "Aberrations spawn 2 Banelings upon death.", + item_names.ABERRATION_RESOURCE_EFFICIENCY: _get_resource_efficiency_desc(item_names.ABERRATION), + item_names.ABERRATION_PROGRESSIVE_BANELING_LAUNCH: inspect.cleandoc(""" + Level 1: Allows Aberrations to periodically throw generated Banelings at air targets. + Level 2: Can store up to 3 Banelings. Can consume Banelings to recharge faster. Thrown Banelings benefit from Baneling upgrades. + """), + item_names.CORRUPTOR_MONSTROUS_RESILIENCE: "Corruptors gain +100 life.", + item_names.CORRUPTOR_CONSTRUCT_REGENERATION: "Corruptors gain increased life regeneration.", + item_names.CORRUPTOR_SCOURGE_INCUBATION: "Corruptors spawn 2 Scourge upon death (3 with Swarm Scourge).", + item_names.CORRUPTOR_RESOURCE_EFFICIENCY: _get_resource_efficiency_desc(item_names.CORRUPTOR), + item_names.PRIMAL_IGNITER_CONCENTRATED_FIRE: "Primal Igniters deal +15 damage vs light armor.", + item_names.PRIMAL_IGNITER_PRIMAL_TENACITY: "Primal Igniters gain +100 health and +1 armor.", + item_names.INFESTED_SCV_BUILD_CHARGES: "Starting Infested SCV charges increased to 3. Maximum charges increased to 5.", + item_names.INFESTED_MARINE_PLAGUED_MUNITIONS: "Infested Marines deal an extra 50 damage over 15 seconds to targets they attack.", + item_names.INFESTED_MARINE_RETINAL_AUGMENTATION: "Infested Marines gain +1 range.", + item_names.INFESTED_BUNKER_CALCIFIED_ARMOR: "Infested Bunkers gain +3 armor.", + item_names.INFESTED_BUNKER_REGENERATIVE_PLATING: "Infested Bunkers gain increased life regeneration while rooted.", + item_names.INFESTED_BUNKER_ENGORGED_BUNKERS: "Infested Bunkers gain +2 cargo slots. Infested Trooper spawn cooldown is reduced by 20%.", + item_names.INFESTED_MISSILE_TURRET_BIOELECTRIC_PAYLOAD: "Increases anti-mechanical damage of Infested Missile Turrets by +6 per missile.", + item_names.INFESTED_MISSILE_TURRET_ACID_SPORE_VENTS: "Infested Missile Turrets gain a secondary weapon that applies Devourer Acid Spores in an area around the target.", + item_names.TYRANNOZOR_TYRANTS_PROTECTION: "Tyrannozors grant nearby friendly units 1 armor.", + item_names.TYRANNOZOR_BARRAGE_OF_SPIKES: _ability_desc("Tyrannozors", "Barrage of Spikes", "deals 60 damage to enemy ground units around the Tyrannozor"), + item_names.TYRANNOZOR_IMPALING_STRIKE: "Tyrannozor melee attacks have a 20% chance to stun for 2 seconds.", + item_names.TYRANNOZOR_HEALING_ADAPTATION: "Tyrannozors regenerate life quickly when out of combat.", + item_names.BILE_LAUNCHER_ARTILLERY_DUCTS: "Increases Bile Launcher range by +8.", + item_names.BILE_LAUNCHER_RAPID_BOMBARMENT: "Bile Launchers attack 40% faster.", + item_names.NYDUS_WORM_ECHIDNA_WORM_SUBTERRANEAN_SCALES: f"Increases {DISPLAY_NAME_WORMS} maximum health by 250 and armor by 1.", + item_names.NYDUS_WORM_ECHIDNA_WORM_JORMUNGANDR_STRAIN: f"Removes emerge time for {DISPLAY_NAME_WORMS}, and allows them to be salvaged to return the resources spent on them.", + item_names.NYDUS_WORM_RAVENOUS_APPETITE: "Allows Nydus Worms to unload and load units nearly instantly.", + item_names.NYDUS_WORM_ECHIDNA_WORM_RESOURCE_EFFICIENCY: _get_resource_efficiency_desc(DISPLAY_NAME_WORMS), + item_names.ECHIDNA_WORM_OUROBOROS_STRAIN: "Allows Echidna Worms to train a limited assortment of combat units (Zerglings, Roaches, Hydralisks, and Aberrations) at a reduced time and cost.", + item_names.INFESTED_SIEGE_TANK_PROGRESSIVE_AUTOMATED_MITOSIS: inspect.cleandoc(""" + Level 1: Infested Siege Tanks generate 1 Volatile Biomass every 30 seconds. + Level 2: Infested Siege Tanks generate 1 Volatile Biomass every 10 seconds. + """), + item_names.INFESTED_SIEGE_TANK_ACIDIC_ENZYMES: "Infested Siege Tanks deal an additional 15 damage to armored units and structures in both modes.", + item_names.INFESTED_SIEGE_TANK_DEEP_TUNNEL: _ability_desc("Infested Siege Tanks", "Deep Tunnel", "allows them to burrow to any visible location on the map covered in creep"), + item_names.INFESTED_SIEGE_TANK_SEISMIC_SONAR: "Infested Siege Tank Tentacle weapon gains +1 range. Volatile Burst weapon gains +3 range.", + item_names.INFESTED_SIEGE_TANK_BALANCED_ROOTS: "Allows Infested Siege Tanks to attack while moving with their Tentacle weapons.", + item_names.INFESTED_DIAMONDBACK_CAUSTIC_MUCUS: "Infested Diamondbacks leave behind a trail of acid when moving that deals 12 damage per second to enemy units.", + item_names.INFESTED_DIAMONDBACK_VIOLENT_ENZYMES: "Infested Diamondbacks deal an additional +8 damage.", + item_names.INFESTED_DIAMONDBACK_CONCENTRATED_SPEW: "Infested Diamondbacks gain +2 weapon range. Fungal Snare gains +2 range.", + item_names.INFESTED_DIAMONDBACK_PROGRESSIVE_FUNGAL_SNARE: inspect.cleandoc(""" + Level 1: Infested Diamondbacks gain the Fungal Snare ability, allowing them to temporarily ground flying units. + Level 2: Infested Diamondback Fungal Snare ability cooldown reduced by 15 seconds. + """), + item_names.BULLFROG_WILD_MUTATION: "Bullfrogs grant themselves and their cargo temporary health and an attack speed boost on impact.", + item_names.BULLFROG_RANGE: "Bullfrog leap gains +4 range, and unload-leap gains +6 range.", + item_names.BULLFROG_BROODLINGS: "Bullfrogs spawn two broodlings on impact, in addition to unloading their cargo.", + item_names.BULLFROG_HARD_IMPACT: "Bullfrogs deal more damage and stun longer on impact.", + item_names.INFESTED_BANSHEE_BRACED_EXOSKELETON: "Infested Banshees gain +100 life.", + item_names.INFESTED_BANSHEE_RAPID_HIBERNATION: "Infested Banshees regenerate 20 life and energy per second while burrowed.", + item_names.INFESTED_BANSHEE_FLESHFUSED_TARGETING_OPTICS: "Infested Banshees gain +2 range while cloaked.", + item_names.INFESTED_LIBERATOR_CLOUD_DISPERSAL: "Infested Liberators instantly transform into a cloud of microscopic organisms while attacking, reducing the damage they take by 85%.", + item_names.INFESTED_LIBERATOR_VIRAL_CONTAMINATION: "Increases the damage Infested Liberators deal to their primary target by 100%.", + item_names.INFESTED_LIBERATOR_DEFENDER_MODE: "Allows Infested Liberators to deploy into Defender Mode to attack ground units. Weapon knocks back the attack target and damages units behind it.", + item_names.INFESTED_SIEGE_TANK_FRIGHTFUL_FLESHWELDER: _get_resource_efficiency_desc(item_names.INFESTED_SIEGE_TANK), + item_names.INFESTED_DIAMONDBACK_FRIGHTFUL_FLESHWELDER: _get_resource_efficiency_desc(item_names.INFESTED_DIAMONDBACK), + item_names.INFESTED_BANSHEE_FRIGHTFUL_FLESHWELDER: _get_resource_efficiency_desc(item_names.INFESTED_BANSHEE), + item_names.INFESTED_LIBERATOR_FRIGHTFUL_FLESHWELDER: _get_resource_efficiency_desc(item_names.INFESTED_LIBERATOR), + item_names.ZERG_EXCAVATING_CLAWS: "Increases movement speed of uprooted Zerg structures, especially off creep. Also increases root speed.", + item_names.HIVE_CLUSTER_MATURATION: "Lairs are replaced with Hives, and Hatcheries can now upgrade directly to Hives at the Lair's original cost.", + item_names.MACROSCOPIC_RECUPERATION: "Zerg structures regenerate health rapidly while on creep and out of combat. Does not apply to uprooted structures, or structures with the Mechanical tag.", + item_names.BIOMECHANICAL_STOCKPILING: "Infested Factories and Starports can store 3 additional unit charges.", + item_names.BROODLING_SPORE_SATURATION: "Zerg buildings release twice as many broodlings on death. Zerg defensive structures release 4 broodlings on death.", + item_names.UNRESTRICTED_MUTATION: "Zerg Mercenary units are no longer limited by charges.", + item_names.CELL_DIVISION: "Adds additional units to Zerg Mercenary calldowns.", + item_names.EVOLUTIONARY_LEAP: "Halves the initial cooldown for all Zerg Mercenaries.", + item_names.SELF_SUFFICIENT: "Zerg Mercenaries no longer use supply.", + item_names.ZERGLING_RAPTOR_STRAIN: "Allows Zerglings to jump up and down cliffs and leap onto enemies. Also increases Zergling attack damage by 2.", + item_names.ZERGLING_SWARMLING_STRAIN: "Zerglings will spawn instantly and with an extra Zergling per egg at no additional cost.", + item_names.ROACH_VILE_STRAIN: "Roach attacks will slow the movement and attack speed of enemies.", + item_names.ROACH_CORPSER_STRAIN: "Units killed after being attacked by Roaches will spawn 2 Roachlings.", + item_names.HYDRALISK_IMPALER_ASPECT: "Allows Hydralisks to morph into Impalers.", + item_names.HYDRALISK_LURKER_ASPECT: "Allows Hydralisks to morph into Lurkers.", + item_names.BANELING_SPLITTER_STRAIN: "Banelings will split into two smaller Splitterlings on exploding.", + item_names.BANELING_HUNTER_STRAIN: "Allows Banelings to jump up and down cliffs and leap onto enemies.", + item_names.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT: "Allows Mutalisks and Corruptors to morph into Brood Lords.", + item_names.MUTALISK_CORRUPTOR_VIPER_ASPECT: "Allows Mutalisks and Corruptors to morph into Vipers.", + item_names.SWARM_HOST_CARRION_STRAIN: "Swarm Hosts will spawn Flying Locusts.", + item_names.SWARM_HOST_CREEPER_STRAIN: "Allows Swarm Hosts to teleport to any creep on the map in vision. Swarm Hosts will spread creep around them when rooted or burrowed.", + item_names.ULTRALISK_NOXIOUS_STRAIN: "Ultralisks will periodically spread poison, damaging nearby biological enemies.", + item_names.ULTRALISK_TORRASQUE_STRAIN: "Ultralisks will revive after being killed.", + item_names.KERRIGAN_KINETIC_BLAST: "Kerrigan deals 300 damage to target unit or structure from long range.", + item_names.KERRIGAN_HEROIC_FORTITUDE: "Kerrigan gains +200 maximum life and double life regeneration rate.", + item_names.KERRIGAN_LEAPING_STRIKE: "Kerrigan leaps to her target and deals 150 damage.", + item_names.KERRIGAN_CRUSHING_GRIP: "Kerrigan stuns enemies in a target area for 3 seconds and deals 30 damage over time. Heroic units are not stunned.", + item_names.KERRIGAN_CHAIN_REACTION: "Kerrigan's attacks deal normal damage to her target then jump to additional nearby enemies.", + item_names.KERRIGAN_PSIONIC_SHIFT: "Kerrigan dashes through enemies, dealing 50 damage to all enemies in her path.", + item_names.ZERGLING_RECONSTITUTION: "Killed Zerglings respawn from your primary Hatchery at no cost.", + item_names.OVERLORD_IMPROVED_OVERLORDS: "Overlords morph instantly and provide 50% more supply.", + item_names.AUTOMATED_EXTRACTORS: "Extractors automatically harvest Vespene Gas without the need for Drones.", + item_names.KERRIGAN_WILD_MUTATION: "Kerrigan gives all units in an area +200 max life and double attack speed for 10 seconds.", + item_names.KERRIGAN_SPAWN_BANELINGS: "Kerrigan spawns six Banelings with timed life.", + item_names.KERRIGAN_MEND: "Kerrigan heals for 150 life and heals nearby friendly units for 50 life. An additional +50% life is healed over 15 seconds.", + item_names.TWIN_DRONES: "Drones morph in groups of two at no additional cost and require less supply.", + item_names.MALIGNANT_CREEP: "Your units and structures gain increased life regeneration and 30% increased attack speed while on creep. Creep Tumors also spread creep faster and farther.", + item_names.VESPENE_EFFICIENCY: "Extractors produce Vespene gas 25% faster.", + item_names.ZERG_CREEP_STOMACH: "Zerg buildings no longer take damage off-creep. Defensive structures can now root off-creep.", + item_names.KERRIGAN_INFEST_BROODLINGS: "Enemies damaged by Kerrigan become infested and will spawn Broodlings with timed life if killed quickly.", + item_names.KERRIGAN_FURY: "Each of Kerrigan's attacks temporarily increase her attack speed by 15%. Can stack up to 75%.", + item_names.KERRIGAN_ABILITY_EFFICIENCY: "Kerrigan's abilities have their cooldown and energy cost reduced by 20%.", + item_names.KERRIGAN_APOCALYPSE: "Kerrigan deals 300 damage (+400 vs Structure) to enemies in a large area.", + item_names.KERRIGAN_SPAWN_LEVIATHAN: "Kerrigan summons a mighty flying Leviathan with timed life. Deals massive damage and has energy-based abilities.", + item_names.KERRIGAN_DROP_PODS: "Kerrigan drops Primal Zerg forces with timed life to the battlefield.", + item_names.KERRIGAN_PRIMAL_FORM: "Kerrigan takes on her Primal Zerg form and gains greatly increased energy regeneration.", + item_names.KERRIGAN_ASSIMILATION_AURA: "Causes all nearby enemies to drop resources when killed.", + item_names.KERRIGAN_IMMOBILIZATION_WAVE: "Deals 100 damage to enemies around a large area and stuns them for 10 seconds.", + item_names.KERRIGAN_LEVELS_10: "Gives Kerrigan +10 Levels.", + item_names.KERRIGAN_LEVELS_9: "Gives Kerrigan +9 Levels.", + item_names.KERRIGAN_LEVELS_8: "Gives Kerrigan +8 Levels.", + item_names.KERRIGAN_LEVELS_7: "Gives Kerrigan +7 Levels.", + item_names.KERRIGAN_LEVELS_6: "Gives Kerrigan +6 Levels.", + item_names.KERRIGAN_LEVELS_5: "Gives Kerrigan +5 Levels.", + item_names.KERRIGAN_LEVELS_4: "Gives Kerrigan +4 Levels.", + item_names.KERRIGAN_LEVELS_3: "Gives Kerrigan +3 Levels.", + item_names.KERRIGAN_LEVELS_2: "Gives Kerrigan +2 Levels.", + item_names.KERRIGAN_LEVELS_1: "Gives Kerrigan +1 Level.", + item_names.KERRIGAN_LEVELS_14: "Gives Kerrigan +14 Levels.", + item_names.KERRIGAN_LEVELS_35: "Gives Kerrigan +35 Levels.", + item_names.KERRIGAN_LEVELS_70: "Gives Kerrigan +70 Levels.", + item_names.INFESTED_MEDICS: "Mercenary infested Medics that may be called in from the Predator Nest.", + item_names.INFESTED_SIEGE_BREAKERS: "Mercenary infested Siege Breakers that may be called in from the Predator Nest.", + item_names.INFESTED_DUSK_WINGS: "Mercenary infested Dusk Wings that may be called in from the Predator Nest.", + item_names.HUNTER_KILLERS: "Elite Hydralisk strain. Summoned at the Predator Nest.", + item_names.DEVOURING_ONES: "Elite Zergling strain. Summoned at the Predator Nest.", + item_names.TORRASQUE_MERC: "Elite Ultralisk strain. Summoned at the Predator Nest.", + item_names.HUNTERLING: "Elite strain. Can jump up and down cliffs and stun enemies by jumping on them. Summoned at the Predator Nest.", + item_names.YGGDRASIL: "Elite Overlord strain that has the ability to transport buildings and ground units. Summoned at the Predator Nest.", + item_names.CAUSTIC_HORRORS: "Elite Roach Strain that has the ability to attack air units. Summoned at the Predator Nest.", + item_names.OVERLORD_VENTRAL_SACS: "Overlords gain the ability to transport ground units.", + item_names.OVERLORD_GENERATE_CREEP: "Overlords gain the ability to generate creep while standing still.", + item_names.OVERLORD_ANTENNAE: "Increases Overlord sight range.", + item_names.OVERLORD_PNEUMATIZED_CARAPACE: "Increases Overlord movement speed.", + item_names.OVERLORD_OVERSEER_ASPECT: "Allows Overlords to morph into Overseers. Overseers can use the Spawn Creep Tumor and Contaminate abilities.", + item_names.MUTALISK_CORRUPTOR_GUARDIAN_ASPECT: "Long-range anti-ground flyer. Can attack ground units. Morphed from the Mutalisk or Corruptor.", + item_names.MUTALISK_CORRUPTOR_DEVOURER_ASPECT: "Anti-air flyer. Attack inflict Acid Spores. Can attack air units. Morphed from the Mutalisk or Corruptor.", + item_names.ROACH_RAVAGER_ASPECT: "Ranged artillery. Can use Corrosive Bile. Can attack ground units. Morphed from the Roach.", + item_names.ROACH_PRIMAL_IGNITER_ASPECT: "Assault unit. Has an area-damage attack. Regenerates life quickly when burrowed. Can attack ground units. Morphed by merging two Roaches.", + item_names.NYDUS_WORM: "Long-range transport network. Nydus Worms and Nydus Networks can load friendly ground units to be unloaded to any other Nydus structure on the map.", + item_names.ECHIDNA_WORM: "Long-range deployable base. Unable to load and unload units, but can generate Creep and Creep Tumors. Can also serve as a dropoff point for resources and can create Drones.", + item_names.ULTRALISK_TYRANNOZOR_ASPECT: "Heavy assault beast. Has a ground-area attack, and powerful anti-air attack. Morphed by merging two Ultralisks.", + item_names.OBSERVER: "Flying spy. Cloak renders the unit invisible to enemies without detection.", + item_names.CENTURION: "Powerful melee warrior. Has the Shadow Charge and Darkcoil abilities.", + item_names.SENTINEL: "Powerful melee warrior. Has the Charge and Reconstruction abilities.", + item_names.SUPPLICANT: "Powerful melee warrior. Has powerful damage-resistant shields.", + item_names.INSTIGATOR: "Ranged support strider. Can store multiple Blink charges.", + item_names.SLAYER: "Ranged attack strider. Can use the Phase Blink and Phasing Armor abilities.", + item_names.SENTRY: "Robotic support unit. Can use the Guardian Shield ability and can restore the shields of nearby Protoss units.", + item_names.ENERGIZER: "Robotic support unit. Can use the Chrono Beam ability and can become stationary to power nearby structures.", + item_names.HAVOC: "Robotic support unit. Can use the Target Lock and Force Field abilities and increases the range of nearby Protoss units.", + item_names.SIGNIFIER: "Potent permanently cloaked psionic master. Can use the Feedback and Crippling Psionic Storm abilities. Can merge into an Archon.", + item_names.ASCENDANT: "Potent psionic master. Can use the Psionic Orb, Mind Blast, and Sacrifice abilities.", + item_names.AVENGER: "Deadly warrior-assassin. Permanently cloaked. Recalls to the nearest Dark Shrine upon death.", + item_names.BLOOD_HUNTER: "Deadly warrior-assassin. Permanently cloaked. Can use the Void Stasis ability.", + item_names.DRAGOON: "Ranged assault strider. Has enhanced health and damage.", + item_names.DARK_ARCHON: "Potent psionic master. Can use the Confuse and Mind Control abilities.", + item_names.ADEPT: "Ranged specialist. Can use the Psionic Transfer ability.", + item_names.WARP_PRISM: "Flying transport. Can carry units and become stationary to deploy a power field.", + item_names.ANNIHILATOR: "Assault Strider. Can use the Shadow Cannon ability to damage air and ground units.", + item_names.STALWART: "Assault strider. Has shields that deflect high-damage attacks.", + item_names.VANGUARD: "Assault Strider. Deals splash damage around the primary target.", + item_names.WRATHWALKER: "Battle strider with a powerful single-target attack. Can walk up and down cliffs.", + item_names.REAVER: "Area damage siege unit. Builds and launches explosive Scarabs for high burst damage.", + item_names.DISRUPTOR: "Robotic disruption unit. Can use the Purification Nova ability to deal heavy area damage.", + item_names.MIRAGE: "Air superiority starfighter. Can use Graviton Beam and Phasing Armor abilities.", + item_names.SKIRMISHER: "Fast skirmish starfighter. Can target ground units.", + item_names.CORSAIR: "Air superiority starfighter. Can use the Disruption Web ability.", + item_names.DESTROYER: "Area assault craft. Can use the Destruction Beam ability to attack multiple units at once.", + item_names.PULSAR: "Support craft. Applies a stacking slow to targets.", + item_names.DAWNBRINGER: "Flying Anti-Surface Assault Ship. Attacks in an area around the target. Attack count increases as it continues firing.", + item_names.SCOUT: "Versatile high-speed fighter. Has a powerful anti-armored air attack and a weaker anti-ground attack.", + item_names.OPPRESSOR: "Tal'Darim Scout variant. Has a weaker air attack, but a stronger ground attack. Can use the Vulcan Blaster ability.", + item_names.CALADRIUS: "Purifier Scout variant. Has no ground attack, but a stronger air attack, which can be upgraded to hit multiple targets. Can use the Corona Beam ability.", + item_names.MISTWING: "Nerazim Scout variant. Specialized stealth fighter. Can use the Cloak, Phantom Dash and Pilot (Transport) abilities.", + item_names.TEMPEST: "Siege artillery craft. Attacks from long range. Can use the Disintegration ability.", + item_names.MOTHERSHIP: "Ultimate Protoss vessel. Can use the Vortex and Mass Recall abilities.", + item_names.ARBITER: "Army support craft. Has the Stasis Field and Recall abilities. Cloaks nearby units.", + item_names.ORACLE: "Flying caster. Can use the Revelation and Stasis Ward abilities.", + item_names.SKYLORD: "Capital ship. Fires a powerful laser that deals damage in a line. Can use Tactical Jump ability.", + item_names.TRIREME: "Capital ship. Builds and launches Bombers that attack enemy targets.", + item_names.PROGRESSIVE_PROTOSS_GROUND_WEAPON: GENERIC_UPGRADE_TEMPLATE.format("damage", PROTOSS, "ground units"), + item_names.PROGRESSIVE_PROTOSS_GROUND_ARMOR: GENERIC_UPGRADE_TEMPLATE.format("armor", PROTOSS, "ground units"), + item_names.PROGRESSIVE_PROTOSS_SHIELDS: GENERIC_UPGRADE_TEMPLATE.format("shields", PROTOSS, "units"), + item_names.PROGRESSIVE_PROTOSS_AIR_WEAPON: GENERIC_UPGRADE_TEMPLATE.format("damage", PROTOSS, "starships"), + item_names.PROGRESSIVE_PROTOSS_AIR_ARMOR: GENERIC_UPGRADE_TEMPLATE.format("armor", PROTOSS, "starships"), + item_names.PROGRESSIVE_PROTOSS_WEAPON_UPGRADE: GENERIC_UPGRADE_TEMPLATE.format("damage", PROTOSS, "units"), + item_names.PROGRESSIVE_PROTOSS_ARMOR_UPGRADE: GENERIC_UPGRADE_TEMPLATE.format("armor", PROTOSS, "units"), + item_names.PROGRESSIVE_PROTOSS_GROUND_UPGRADE: GENERIC_UPGRADE_TEMPLATE.format("damage and armor", PROTOSS, "ground units"), + item_names.PROGRESSIVE_PROTOSS_AIR_UPGRADE: GENERIC_UPGRADE_TEMPLATE.format("damage and armor", PROTOSS, "starships"), + item_names.PROGRESSIVE_PROTOSS_WEAPON_ARMOR_UPGRADE: GENERIC_UPGRADE_TEMPLATE.format("damage and armor", PROTOSS, "units"), + item_names.PHOTON_CANNON: "Protoss defensive structure. Can attack ground and air units.", + item_names.KHAYDARIN_MONOLITH: "Advanced Protoss defensive structure. Has superior range and damage, but is very expensive and attacks slowly.", + item_names.SHIELD_BATTERY: "Protoss defensive structure. Restores shields to nearby friendly units and structures.", + item_names.SUPPLICANT_BLOOD_SHIELD: "Increases the armor value of Supplicant shields.", + item_names.SUPPLICANT_SOUL_AUGMENTATION: "Increases Supplicant max shields by +25.", + item_names.SUPPLICANT_ENDLESS_SERVITUDE: "Increases Supplicant shield regeneration rate.", + item_names.SUPPLICANT_ZENITH_PITCH: "Allows Supplicants to attack air units.", + item_names.ADEPT_SHOCKWAVE: "When Adepts deal a finishing blow, their projectiles can jump onto 2 additional targets.", + item_names.ADEPT_RESONATING_GLAIVES: "Increases Adept attack speed.", + item_names.ADEPT_PHASE_BULWARK: "Increases Adept shield maximum by +50.", + item_names.STALKER_INSTIGATOR_SLAYER_DISINTEGRATING_PARTICLES: "Increases weapon damage of Stalkers, Instigators, and Slayers.", + item_names.STALKER_INSTIGATOR_SLAYER_PARTICLE_REFLECTION: "Attacks fired by Stalkers, Instigators, and Slayers have a chance to bounce to additional targets for reduced damage.", + item_names.INSTIGATOR_BLINK_OVERDRIVE: "Instigators gain +2 maximum blink charges and +1 blink range.", + item_names.INSTIGATOR_RECONSTRUCTION: "Instigators gain the Reconstruction ability, allowing them to be reconstructed on death with a 240 seconds cooldown. Using Blink reduces the cooldown.", + item_names.DRAGOON_CONCENTRATED_ANTIMATTER: "Dragoons deal increased damage.", + item_names.DRAGOON_TRILLIC_COMPRESSION_SYSTEM: "Dragoons gain +20 life and their shield regeneration rate is doubled. Allows Dragoons to regenerate shields in combat.", + item_names.DRAGOON_SINGULARITY_CHARGE: "Increases Dragoon range by +2.", + item_names.DRAGOON_ENHANCED_STRIDER_SERVOS: "Increases Dragoon movement speed.", + item_names.SCOUT_COMBAT_SENSOR_ARRAY: "All Scout variants gain increased range against air and ground.", + item_names.SCOUT_APIAL_SENSORS: "Scouts gain increased sight range.", + item_names.SCOUT_GRAVITIC_THRUSTERS: "All Scout variants gain increased movement speed.", + item_names.SCOUT_ADVANCED_PHOTON_BLASTERS: "Scouts, Oppressors and Mist Wings gain increased damage against ground targets.", + item_names.SCOUT_RESOURCE_EFFICIENCY: _get_resource_efficiency_desc(item_names.SCOUT), + item_names.SCOUT_SUPPLY_EFFICIENCY: _get_resource_efficiency_desc(item_names.SCOUT, op_re_cost_reduction), + item_names.OPPRESSOR_ACCELERATED_WARP: "Oppressors gain increased training and warp-in speed.", + item_names.OPPRESSOR_ARMOR_MELTING_BLASTERS: "Oppressor ground weapons gain bonus damage to armored targets. Allows Vulcan Blaster to hit structures.", + item_names.CALADRIUS_SIDE_MISSILES: "Caladrius can hit up to 4 additional air targets with their missiles.", + item_names.CALADRIUS_STRUCTURE_TARGETING: "Allows Caladrius to hit ground structures with their anti-air missiles.", + item_names.CALADRIUS_SOLARITE_REACTOR: "If the Caladrius is low on shields, it recovers shields quickly for a short time.", + item_names.MISTWING_NULL_SHROUD: "Cloak no longer drains energy (but still prevents base energy regeneration). The Mist Wing becomes undetectable for 5 seconds upon cloaking.", + item_names.MISTWING_PILOT: _ability_desc("Mistwings", "Pilot", "can transport one unit as an additional co-pilot. A pilot grants a small bonus to damage and armor"), + item_names.TEMPEST_TECTONIC_DESTABILIZERS: "Tempests deal increased damage to buildings.", + item_names.TEMPEST_QUANTIC_REACTOR: "Tempests deal increased damage to massive units.", + item_names.TEMPEST_GRAVITY_SLING: "Tempests gain +8 range against air targets and +8 cast range.", + item_names.TEMPEST_INTERPLANETARY_RANGE: "Tempests gain +8 weapon range against all targets.", + item_names.PHOENIX_CLASS_IONIC_WAVELENGTH_FLUX: "Increases Phoenix, Mirage, and Skirmisher weapon damage by +2.", + item_names.PHOENIX_CLASS_ANION_PULSE_CRYSTALS: "Increases Phoenix, Mirage, and Skirmiser range by +2.", + item_names.CORSAIR_STEALTH_DRIVE: "Corsairs become permanently cloaked.", + item_names.CORSAIR_ARGUS_JEWEL: "Corsairs can store 2 charges of disruption web.", + item_names.CORSAIR_SUSTAINING_DISRUPTION: "Corsair disruption webs last longer.", + item_names.CORSAIR_NEUTRON_SHIELDS: "Increases corsair maximum shields by +20.", + item_names.ORACLE_STEALTH_DRIVE: "Oracles become permanently cloaked.", + item_names.ORACLE_SKYWARD_CHRONOANOMALY: "The Oracle's Stasis Ward can affect air units.", + item_names.ORACLE_TEMPORAL_ACCELERATION_BEAM: "Oracles no longer need to to spend energy to attack.", + item_names.ORACLE_BOSONIC_CORE: "Increases starting energy by 150 and maximum energy by 50.", + item_names.ARBITER_CHRONOSTATIC_REINFORCEMENT: "Arbiters gain +50 maximum life and +1 armor.", + item_names.ARBITER_KHAYDARIN_CORE: _get_start_and_max_energy_desc("Arbiters"), + item_names.ARBITER_SPACETIME_ANCHOR: "Allows Arbiters to use an alternate version of Stasis Field which lasts 50 seconds longer.", + item_names.ARBITER_RESOURCE_EFFICIENCY: _get_resource_efficiency_desc(item_names.ARBITER), + item_names.ARBITER_JUDICATORS_VEIL: "Increases Arbiter Cloaking Field range.", + item_names.CARRIER_TRIREME_GRAVITON_CATAPULT: "Carriers and Triremes can launch Interceptors and Bombers more quickly.", + item_names.CARRIER_SKYLORD_TRIREME_HULL_OF_PAST_GLORIES: "Carrier-class ships gain +2 armor.", + item_names.VOID_RAY_DESTROYER_PULSAR_DAWNBRINGER_FLUX_VANES: "Increases movement speed of Void Ray variants.", + item_names.DAWNBRINGER_ANTI_SURFACE_COUNTERMEASURES: "Dawnbringers take reduced damage from non-spell ground sources.", + item_names.DAWNBRINGER_ENHANCED_SHIELD_GENERATOR: "Increases Dawnbringer maximum shields by +50.", + item_names.PULSAR_CHRONOCLYSM: "Pulsar slow effect is also applied in an area around the primary target.", + item_names.PULSAR_ENTROPIC_REVERSAL: "Pulsars now regenerate life when out of combat.", + item_names.DESTROYER_RESOURCE_EFFICIENCY: _get_resource_efficiency_desc(item_names.DESTROYER), + item_names.WARP_PRISM_GRAVITIC_DRIVE: "Increases the movement speed of Warp Prisms.", + item_names.WARP_PRISM_PHASE_BLASTER: "Equips Warp Prisms with an auto-attack that can hit ground and air targets.", + item_names.WARP_PRISM_WAR_CONFIGURATION: "Warp Prisms transform faster and gain increased power radius in Phasing Mode.", + item_names.OBSERVER_GRAVITIC_BOOSTERS: "Increases Observer movement speed.", + item_names.OBSERVER_SENSOR_ARRAY: "Increases Observer sight range.", + item_names.REAVER_SCARAB_DAMAGE: "Reaver Scarabs deal +25 damage.", + item_names.REAVER_SOLARITE_PAYLOAD: "Reaver Scarabs gain increased splash damage radius.", + item_names.REAVER_REAVER_CAPACITY: "Reavers can store 10 Scarabs.", + item_names.REAVER_RESOURCE_EFFICIENCY: _get_resource_efficiency_desc(item_names.REAVER), + item_names.REAVER_BARGAIN_BIN_PRICES: _get_resource_efficiency_desc(item_names.REAVER, op_re_cost_reduction), + item_names.VANGUARD_AGONY_LAUNCHERS: "Increases Vanguard attack range by +2.", + item_names.VANGUARD_MATTER_DISPERSION: "Increases Vanguard attack area.", + item_names.IMMORTAL_ANNIHILATOR_SINGULARITY_CHARGE: "Increases Immortal and Annihilator attack range by +2.", + item_names.IMMORTAL_ANNIHILATOR_ADVANCED_TARGETING: "Immortals and Annihilators can attack air units.", + item_names.IMMORTAL_ANNIHILATOR_DISRUPTOR_DISPERSION: "Immortals and Annihilators deal minor splash damage.", + item_names.STALWART_HIGH_VOLTAGE_CAPACITORS: "Increases Stalwart attack bounce range by +1.", + item_names.STALWART_REINTEGRATED_FRAMEWORK: "Increases the movement speed of Stalwarts.", + item_names.STALWART_STABILIZED_ELECTRODES: "Allows Stalwarts to attack while moving, and increases attack range by +1.", + item_names.STALWART_LATTICED_SHIELDING: "Increases Stalwart max shields by +50.", + item_names.DISRUPTOR_CLOAKING_MODULE: "Disruptors are permanently cloaked.", + item_names.DISRUPTOR_PERFECTED_POWER: "Allows Purification Nova to hit air units. Bonus damage to shields is now baseline for enemies (friendly damage unaffected).", + item_names.DISRUPTOR_RESTRAINED_DESTRUCTION: "Purification Nova does 50% reduced damage to friendly units and structures.", + item_names.COLOSSUS_PACIFICATION_PROTOCOL: "Increases Colossus attack speed.", + item_names.WRATHWALKER_RAPID_POWER_CYCLING: "Reduces the charging time and increases attack speed of the Wrathwalker's Charged Blast.", + item_names.WRATHWALKER_EYE_OF_WRATH: "Increases Wrathwalker weapon range by +1.", + item_names.DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_SHROUD_OF_ADUN: f"Increases {DISPLAY_NAME_CLOAKED_ASSASSIN} maximum shields by +80.", + item_names.DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_SHADOW_GUARD_TRAINING: f"Increases {DISPLAY_NAME_CLOAKED_ASSASSIN} maximum life by +40.", + item_names.DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_BLINK: _ability_desc("Dark Templar, Avengers, and Blood Hunters", "Blink"), + item_names.DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_RESOURCE_EFFICIENCY: _get_resource_efficiency_desc(DISPLAY_NAME_CLOAKED_ASSASSIN), + item_names.DARK_TEMPLAR_DARK_ARCHON_MELD: "Allows 2 Dark Templar to meld into a Dark Archon.", + item_names.DARK_TEMPLAR_ARCHON_MERGE: "Allows 2 Dark Templar to merge into a Archon.", + item_names.HIGH_TEMPLAR_SIGNIFIER_UNSHACKLED_PSIONIC_STORM: "High Templar and Signifiers deal increased damage with Psi Storm.", + item_names.HIGH_TEMPLAR_SIGNIFIER_HALLUCINATION: _ability_desc("High Templar and Signifiers", "Hallucination", "creates 2 hallucinated copies of a target unit"), + item_names.HIGH_TEMPLAR_SIGNIFIER_KHAYDARIN_AMULET: _get_start_and_max_energy_desc("High Templar and Signifiers"), + item_names.ARCHON_HIGH_ARCHON: "Archons can use High Templar abilities.", + item_names.ARCHON_TRANSCENDENCE: "Archons can float in the air. Can traverse cliffs and phase through most units. Increases Archon attack range by 1.", + item_names.ARCHON_POWER_SIPHON: _ability_desc("Archons", "Power Siphon", "deals damage to a target and replenishes shields over 2 seconds"), + item_names.ARCHON_ERADICATE: "On death, Archons launch towards nearby enemy ground units, dealing damage on impact.", + item_names.ARCHON_OBLITERATE: "Archon attacks get increased area of effect, and deal their biological bonus damage to all targets.", + item_names.DARK_ARCHON_FEEDBACK: _ability_desc("Dark Archons", "Feedback", "drains all energy from a target and deals 1 damage per point of energy drained"), + item_names.DARK_ARCHON_MAELSTROM: _ability_desc("Dark Archons", "Maelstrom", "stuns biological units in an area"), + item_names.DARK_ARCHON_ARGUS_TALISMAN: _get_start_and_max_energy_desc("Dark Archons"), + item_names.ASCENDANT_POWER_OVERWHELMING: "Ascendants gain the ability to sacrifice Supplicants for increased shields and spell damage.", + item_names.ASCENDANT_CHAOTIC_ATTUNEMENT: "Ascendants' Psionic Orbs gain 25% increased travel distance.", + item_names.ASCENDANT_BLOOD_AMULET: _get_start_and_max_energy_desc("Ascendants"), + item_names.ASCENDANT_ARCHON_MERGE: "Allows 2 Ascendants to merge into a Archon.", + item_names.SENTRY_ENERGIZER_HAVOC_CLOAKING_MODULE: "Sentries, Energizers, and Havocs become permanently cloaked.", + item_names.SENTRY_ENERGIZER_HAVOC_SHIELD_BATTERY_RAPID_RECHARGING: "Sentries, Energizers, and Havocs gain +100% energy regeneration rate.", + item_names.SENTRY_FORCE_FIELD: _ability_desc("Sentries", "Force Field", "creates a force field that blocks units from walking through"), + item_names.SENTRY_HALLUCINATION: _ability_desc("Sentries", "Hallucination", "creates hallucinated versions of Protoss units"), + item_names.ENERGIZER_RECLAMATION: _ability_desc("Energizers", "Reclamation", "temporarily takes control of an enemy mechanical unit. When the ability expires, the enemy unit self-destructs"), + item_names.ENERGIZER_FORGED_CHASSIS: "Increases Energizer Life by +20.", + item_names.HAVOC_DETECT_WEAKNESS: "Havocs' Target Lock gives an additional +15% damage bonus.", + item_names.HAVOC_BLOODSHARD_RESONANCE: "Havocs gain increased range for Squad Sight, Target Lock, and Force Field.", + item_names.ZEALOT_SENTINEL_CENTURION_LEG_ENHANCEMENTS: "Zealots, Sentinels, and Centurions gain increased movement speed.", + item_names.ZEALOT_SENTINEL_CENTURION_SHIELD_CAPACITY: "Zealots, Sentinels, and Centurions gain +30 maximum shields.", + item_names.ZEALOT_WHIRLWIND: "Zealot War Council ability.\nGives Zealots the whirlwind ability, dealing damage in an area over 3 seconds.", + item_names.CENTURION_RESOURCE_EFFICIENCY: "Centurion War Council upgrade.\n" + _get_resource_efficiency_desc( + item_names.CENTURION), + item_names.SENTINEL_RESOURCE_EFFICIENCY: "Sentinel War Council upgrade.\n" + _get_resource_efficiency_desc( + item_names.SENTINEL), + item_names.STALKER_PHASE_REACTOR: "Stalker War Council upgrade.\nStalkers restore 80 shields over 5 seconds after they Blink.", + item_names.DRAGOON_PHALANX_SUIT: "Dragoon War Council upgrade.\nDragoons gain +1 range, move slightly faster, and can form tighter formations.", + item_names.INSTIGATOR_MODERNIZED_SERVOS: "Instigator War Council upgrade.\nInstigators move 28% faster.", + item_names.ADEPT_DISRUPTIVE_TRANSFER: "Adept War Council upgrade.\nAdept shades apply a debuff to enemies they touch, increasing damage taken by +5.", + item_names.SLAYER_PHASE_BLINK: "Slayer War Council ability.\nSlayers can now blink. After blinking, the Slayer's next attack within 8 seconds deals double damage.", + item_names.AVENGER_KRYHAS_CLOAK: "Avenger War Council upgrade.\nAvengers are now permanently cloaked.", + item_names.DARK_TEMPLAR_LESSER_SHADOW_FURY: "Dark Templar War Council ability.\nDark Templar gain two strikes of their Shadow Fury ability.", + item_names.DARK_TEMPLAR_GREATER_SHADOW_FURY: "Dark Templar War Council ability.\nDark Templar gain three strikes of their Shadow Fury ability.", + item_names.BLOOD_HUNTER_BRUTAL_EFFICIENCY: "Blood Hunter War Council upgrade.\nBlood Hunters attack twice as quickly.", + item_names.SENTRY_DOUBLE_SHIELD_RECHARGE: "Sentry War Council upgrade.\nSentries can heal the shields of two targets at once.", + item_names.ENERGIZER_MOBILE_CHRONO_BEAM: "Energizer War Council upgrade.\nAllows Energizers to use Chrono Beam in Mobile Mode.", + item_names.HAVOC_ENDURING_SIGHT: "Havoc War Council upgrade.\nHavoc Squad Sight stays up indefinitely and no longer takes energy.", + item_names.HIGH_TEMPLAR_PLASMA_SURGE: "High Templar War Council upgrade.\nHigh Templar Psionic Storm will heal fiendly protoss shields under it.", + item_names.SIGNIFIER_FEEDBACK: "Signifier War Council ability.\n" + _ability_desc("Signifiers", "Feedback", "drains all energy from a target and deals 1 damage per point of energy drained"), + item_names.ASCENDANT_BREATH_OF_CREATION: "Ascendant War Council upgrade.\nAscendant spells cost -25 energy.", + item_names.DARK_ARCHON_INDOMITABLE_WILL: "Dark Archon War Council upgrade.\nCasting Mind Control will no longer deplete the Dark Archon's shields.", + item_names.IMMORTAL_IMPROVED_BARRIER: "Immortal War Council upgrade.\nThe Immortal's Barrier ability absorbs an additional +100 damage.", + item_names.VANGUARD_RAPIDFIRE_CANNON: "Vanguard War Council upgrade.\nVanguards attack 60% faster.", + item_names.VANGUARD_FUSION_MORTARS: "Vanguard War Council upgrade.\nVanguards deal +7 damage to armored targets per attack.", + item_names.ANNIHILATOR_TWILIGHT_CHASSIS: "Annihilator War Council upgrade.\nThe Annihilator gains +100 maximum life.", + item_names.STALWART_ARC_INDUCERS: "Stalwart War Council upgrade.\nStalwarts damage up to 3 additional units when attacking.", + item_names.COLOSSUS_FIRE_LANCE: "Colossus War Council upgrade.\nColossi set the ground on fire with their attacks, dealing damage to enemies over time.", + item_names.WRATHWALKER_AERIAL_TRACKING: "Wrathwalker War Council upgrade.\nWrathwalkers can now target air units.", + item_names.REAVER_KHALAI_REPLICATORS: "Reaver War Council upgrade.\nReaver Scarabs no longer cost minerals.", + item_names.DISRUPTOR_MOBILITY_PROTOCOLS: "Disruptor War Council upgrade.\nAllows the Disruptor to move while casting Purification Nova. Also allows the Disruptor to Blink.", + item_names.WARP_PRISM_WARP_REFRACTION: "Warp Prism War Council upgrade.\nWarp Prisms gain +5 pickup range and unload units 10 times faster.", + item_names.OBSERVER_INDUCE_SCOPOPHOBIA: "Observer War Council ability.\n" + _ability_desc("Observers", "Induce Scopophobia", "reduces the attack and movement speed of an enemy by 20%"), + item_names.PHOENIX_DOUBLE_GRAVITON_BEAM: "Phoenix War Council upgrade.\nPhoenixes can now use Graviton Beam to lift two targets at once.", + item_names.CORSAIR_NETWORK_DISRUPTION: "Corsair War Council upgrade.\nTriples the radius of Disruption Web.", + item_names.MIRAGE_GRAVITON_BEAM: "Mirage War Council ability.\nAllows Mirages to use Graviton Beam.", + item_names.SKIRMISHER_PEER_CONTEMPT: "Skirmisher War Council upgrade.\nAllows Skirmishers to target air units.", + item_names.VOID_RAY_PRISMATIC_RANGE: "Void Ray War Council upgrade.\nVoid Rays gain increased range as they charge their beam.", + item_names.DESTROYER_REFORGED_BLOODSHARD_CORE: "Destroyer War Council upgrade.\nIncreases the Destroyer's bounce attack damage to 3 (+2 vs armored) at all charge levels, and allows the bounces to benefit from protoss air weapon upgrades.", + item_names.PULSAR_CHRONO_SHEAR: "Pulsar War Council upgrade.\nFully-stacked slow on non-heroic targets also applies a defense debuff.", + item_names.DAWNBRINGER_SOLARITE_LENS: "Dawnbringer War Council upgrade.\nDawnbringers gain +2 range.", + item_names.CARRIER_REPAIR_DRONES: "Carrier War Council upgrade.\nCarriers gain 2 repair drones which heal nearby mechanical units.", + item_names.SKYLORD_JUMP: "Skylord War Council ability.\n" + _ability_desc("Skylords", "Jump", "instantly teleports the Skylord a short distance"), + item_names.TRIREME_SOLAR_BEAM: "Trireme War Council weapon.\nTriremes gain an anti-air laser attack that deals more damage over time.", + item_names.TEMPEST_DISINTEGRATION: "Tempest War Council ability.\n" + _ability_desc("Tempests", "Disintegration", "deals 500 damage to a target unit or structure over 20 seconds"), + item_names.SCOUT_EXPEDITIONARY_HULL: "Scout War Council upgrade.\nScouts gain +25 shields, +50 health, +1 shield armor, and reduced shield regeneration delay.", + item_names.ARBITER_VESSEL_OF_THE_CONCLAVE: "Arbiter War Council upgrade.\nReduces the energy cost of Recall by 50 and Stasis Field by 100.", + item_names.ORACLE_STASIS_CALIBRATION: "Oracle War Council upgrade.\nEnemies caught by the Oracle's Stasis Ward may now be attacked for the first 5 seconds of stasis.", + item_names.MOTHERSHIP_INTEGRATED_POWER: "Mothership War Council upgrade.\nAllows Motherships to move at full speed outside pylon power.", + item_names.OPPRESSOR_VULCAN_BLASTER: "Oppressor War Council ability.\n" + _ability_desc("Oppressors", "Vulcan Blaster", "activates a powerful short range anti-ground weapon for a limited time. Greatly reduces movement and turning speed, and disables other weapons while active"), + item_names.CALADRIUS_CORONA_BEAM: "Caladrius War Council ability.\n" + _ability_desc("Caladrius", "Corona Beam", "channels a beam that drains up to 100 of the Caladrius' shields to deal up to 200 damage over time to a single target"), + item_names.MISTWING_PHANTOM_DASH: "Mist Wing War Council ability.\n" + _ability_desc("Mist Wings", "Phantom Dash", "dashes forward to cover some distance quickly. Deals damage in a line if the Mist Wing is cloaked"), + item_names.SUPPLICANT_SACRIFICE: "Supplicant War Council ability.\nAllows Supplicants to sacrifice themselves to save nearby units from death.", + + + item_names.SOA_CHRONO_SURGE: "The Spear of Adun increases a target structure's unit warp in and research speeds by +1000% for 20 seconds.", + item_names.SOA_PROGRESSIVE_PROXY_PYLON: inspect.cleandoc(""" + Level 1: The Spear of Adun quickly warps in a Pylon to a target location. + Level 2: The Spear of Adun warps in a Pylon, 2 melee warriors, and 2 ranged warriors to a target location. + """), + item_names.SOA_PYLON_OVERCHARGE: "The Spear of Adun temporarily gives a target Pylon increased shields and a powerful attack.", + item_names.SOA_ORBITAL_STRIKE: "The Spear of Adun fires 5 laser blasts from orbit.", + item_names.SOA_TEMPORAL_FIELD: "The Spear of Adun creates 3 temporal fields that freeze enemy units and structures in time.", + item_names.SOA_SOLAR_LANCE: "The Spear of Adun strafes a target area with 3 laser beams.", + item_names.SOA_MASS_RECALL: "The Spear of Adun warps all units in a target area back to the primary Nexus and gives them a temporary shield.", + item_names.SOA_SHIELD_OVERCHARGE: "The Spear of Adun gives all friendly units a shield that absorbs 200 damage. Lasts 20 seconds.", + item_names.SOA_DEPLOY_FENIX: "The Spear of Adun drops Fenix onto the battlefield. Fenix is a powerful warrior who will fight for 30 seconds.", + item_names.SOA_PURIFIER_BEAM: "The Spear of Adun fires a wide laser that deals large amounts of damage in a moveable area. Lasts 15 seconds.", + item_names.SOA_TIME_STOP: "The Spear of Adun freezes all enemy units and structures in time for 20 seconds.", + item_names.SOA_SOLAR_BOMBARDMENT: "The Spear of Adun fires 200 laser blasts randomly over a wide area.", + item_names.MATRIX_OVERLOAD: "All friendly units gain 25% movement speed and 15% attack speed within a Pylon's power field and for 15 seconds after leaving it.", + item_names.QUATRO: "All friendly Protoss units gain the equivalent of their +1 armor, attack, and shield upgrades.", + item_names.NEXUS_OVERCHARGE: "The Protoss Nexus gains a long-range auto-attack.", + item_names.ORBITAL_ASSIMILATORS: "Assimilators automatically harvest Vespene Gas without the need for Probes.", + item_names.WARP_HARMONIZATION: "Stargates and Robotics Facilities can transform to utilize Warp In technology. Warp In cooldowns are 20% faster than original build times.", + item_names.GUARDIAN_SHELL: "The Spear of Adun passively shields friendly Protoss units before death, making them invulnerable for 5 seconds. Each unit can only be shielded once every 60 seconds.", + item_names.RECONSTRUCTION_BEAM: "The Spear of Adun will passively heal mechanical units for 5 and non-biological structures for 10 life per second. Up to 3 targets can be repaired at once.", + item_names.OVERWATCH: "Once per second, the Spear of Adun will last-hit a damaged enemy unit that is below 50 health.", + item_names.SUPERIOR_WARP_GATES: "Protoss Warp Gates can hold up to 3 charges of unit warp-ins.", + item_names.ENHANCED_TARGETING: "Protoss defensive structures gain +2 range.", + item_names.OPTIMIZED_ORDNANCE: "Increases the attack speed of Protoss defensive structures by 25%.", + item_names.KHALAI_INGENUITY: "Pylons, Photon Cannons, Monoliths, and Shield Batteries warp in near-instantly.", + item_names.AMPLIFIED_ASSIMILATORS: "Assimilators produce Vespene gas 25% faster.", + item_names.PROGRESSIVE_WARP_RELOCATE: inspect.cleandoc(""" + Level 1: Protoss structures can be moved anywhere within pylon power after a brief delay. Max 3 charges, shared globally. + Level 2: No longer consumes or requires charges. + """), + item_names.PROBE_WARPIN: "You can warp in additonal Probes from your Nexus to any visible location within a Pylon's power field. Has a 30 second cooldown and can store up to 2 charges.", + item_names.ELDER_PROBES: "You can warp in a group of 5 Elder Probes, tough builders from the Brood War. Elder Probes can provide a Power Field and get reconstructed on death. Can only be used once per mission.", +} + +# Key descriptions +key_descriptions = { + key: GENERIC_KEY_DESC + for key in item_tables.key_item_table.keys() +} +item_descriptions.update(key_descriptions) diff --git a/worlds/sc2/item/item_groups.py b/worlds/sc2/item/item_groups.py new file mode 100644 index 00000000..ea65dc3e --- /dev/null +++ b/worlds/sc2/item/item_groups.py @@ -0,0 +1,902 @@ +import typing +from . import item_tables, item_names +from .item_tables import key_item_table +from ..mission_tables import campaign_mission_table, SC2Campaign, SC2Mission, SC2Race + +""" +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", + item_tables.TerranItemType.Progressive.display_name, + item_tables.TerranItemType.Nova_Gear.display_name, + item_tables.TerranItemType.Mercenary.display_name, + item_tables.ZergItemType.Ability.display_name, + item_tables.ZergItemType.Morph.display_name, + item_tables.ZergItemType.Strain.display_name, +} + +# 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 [item_names.__dict__[name] for name in item_names.__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 item_tables.get_full_item_list().items(): + # Items get assigned to their flaggroup's display type + item_name_groups.setdefault(data.type.display_name, []).append(item) + # 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: + # The parent groups need a special name, otherwise they are ambiguous with the parent + parent_group = f"{data.parent} 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 +class ItemGroupNames: + TERRAN_ITEMS = "Terran Items" + """All Terran items""" + TERRAN_UNITS = "Terran Units" + TERRAN_GENERIC_UPGRADES = "Terran Generic Upgrades" + """+attack/armour upgrades""" + BARRACKS_UNITS = "Barracks Units" + FACTORY_UNITS = "Factory Units" + STARPORT_UNITS = "Starport Units" + WOL_UNITS = "WoL Units" + WOL_MERCS = "WoL Mercenaries" + WOL_BUILDINGS = "WoL Buildings" + WOL_UPGRADES = "WoL Upgrades" + WOL_ITEMS = "WoL Items" + """All items from vanilla WoL. Note some items are progressive where level 2 is not vanilla.""" + NCO_UNITS = "NCO Units" + NCO_BUILDINGS = "NCO Buildings" + NCO_UNIT_TECHNOLOGY = "NCO Unit Technology" + NCO_BASELINE_UPGRADES = "NCO Baseline Upgrades" + NCO_UPGRADES = "NCO Upgrades" + NOVA_EQUIPMENT = "Nova Equipment" + NOVA_WEAPONS = "Nova Weapons" + NOVA_GADGETS = "Nova Gadgets" + NCO_MAX_PROGRESSIVE_ITEMS = "NCO +Items" + """NCO item groups that should be set to maximum progressive amounts""" + NCO_MIN_PROGRESSIVE_ITEMS = "NCO -Items" + """NCO item groups that should be set to minimum progressive amounts (1)""" + TERRAN_BUILDINGS = "Terran Buildings" + TERRAN_MERCENARIES = "Terran Mercenaries" + TERRAN_STIMPACKS = "Terran Stimpacks" + TERRAN_PROGRESSIVE_UPGRADES = "Terran Progressive Upgrades" + TERRAN_ORIGINAL_PROGRESSIVE_UPGRADES = "Terran Original Progressive Upgrades" + """Progressive items where level 1 appeared in WoL""" + MENGSK_UNITS = "Mengsk Units" + TERRAN_VETERANCY_UNITS = "Terran Veterancy Units" + ORBITAL_COMMAND_ABILITIES = "Orbital Command Abilities" + WOL_ORBITAL_COMMAND_ABILITIES = "WoL Command Center Abilities" + + ZERG_ITEMS = "Zerg Items" + ZERG_UNITS = "Zerg Units" + ZERG_NONMORPH_UNITS = "Zerg Non-morph Units" + ZERG_GENERIC_UPGRADES = "Zerg Generic Upgrades" + """+attack/armour upgrades""" + HOTS_UNITS = "HotS Units" + HOTS_BUILDINGS = "HotS Buildings" + HOTS_STRAINS = "HotS Strains" + """Vanilla HotS strains (the upgrades you play a mini-mission for)""" + HOTS_MUTATIONS = "HotS Mutations" + """Vanilla HotS Mutations (basic toggleable unit upgrades)""" + HOTS_GLOBAL_UPGRADES = "HotS Global Upgrades" + HOTS_MORPHS = "HotS Morphs" + KERRIGAN_ABILITIES = "Kerrigan Abilities" + KERRIGAN_HOTS_ABILITIES = "Kerrigan HotS Abilities" + KERRIGAN_ACTIVE_ABILITIES = "Kerrigan Active Abilities" + KERRIGAN_LOGIC_ACTIVE_ABILITIES = "Kerrigan Logic Active Abilities" + KERRIGAN_PASSIVES = "Kerrigan Passives" + KERRIGAN_TIER_1 = "Kerrigan Tier 1" + KERRIGAN_TIER_2 = "Kerrigan Tier 2" + KERRIGAN_TIER_3 = "Kerrigan Tier 3" + KERRIGAN_TIER_4 = "Kerrigan Tier 4" + KERRIGAN_TIER_5 = "Kerrigan Tier 5" + KERRIGAN_TIER_6 = "Kerrigan Tier 6" + KERRIGAN_TIER_7 = "Kerrigan Tier 7" + KERRIGAN_ULTIMATES = "Kerrigan Ultimates" + KERRIGAN_LOGIC_ULTIMATES = "Kerrigan Logic Ultimates" + KERRIGAN_NON_ULTIMATES = "Kerrigan Non-Ultimates" + KERRIGAN_NON_ULTIMATE_ACTIVE_ABILITIES = "Kerrigan Non-Ultimate Active Abilities" + HOTS_ITEMS = "HotS Items" + """All items from vanilla HotS""" + OVERLORD_UPGRADES = "Overlord Upgrades" + ZERG_MORPHS = "Zerg Morphs" + ZERG_MERCENARIES = "Zerg Mercenaries" + ZERG_BUILDINGS = "Zerg Buildings" + INF_TERRAN_ITEMS = "Infested Terran Items" + """All items from Stukov co-op subfaction""" + INF_TERRAN_UNITS = "Infested Terran Units" + INF_TERRAN_UPGRADES = "Infested Terran Upgrades" + + PROTOSS_ITEMS = "Protoss Items" + PROTOSS_UNITS = "Protoss Units" + PROTOSS_GENERIC_UPGRADES = "Protoss Generic Upgrades" + """+attack/armour upgrades""" + GATEWAY_UNITS = "Gateway Units" + ROBO_UNITS = "Robo Units" + STARGATE_UNITS = "Stargate Units" + PROPHECY_UNITS = "Prophecy Units" + PROPHECY_BUILDINGS = "Prophecy Buildings" + LOTV_UNITS = "LotV Units" + LOTV_ITEMS = "LotV Items" + LOTV_GLOBAL_UPGRADES = "LotV Global Upgrades" + SOA_ITEMS = "SOA" + PROTOSS_GLOBAL_UPGRADES = "Protoss Global Upgrades" + PROTOSS_BUILDINGS = "Protoss Buildings" + WAR_COUNCIL = "Protoss War Council Upgrades" + AIUR_UNITS = "Aiur" + NERAZIM_UNITS = "Nerazim" + TAL_DARIM_UNITS = "Tal'Darim" + PURIFIER_UNITS = "Purifier" + + VANILLA_ITEMS = "Vanilla Items" + OVERPOWERED_ITEMS = "Overpowered Items" + UNRELEASED_ITEMS = "Unreleased Items" + LEGACY_ITEMS = "Legacy Items" + + KEYS = "Keys" + + @classmethod + def get_all_group_names(cls) -> typing.Set[str]: + return { + name for identifier, name in cls.__dict__.items() + if not identifier.startswith('_') + and not identifier.startswith('get_') + } + + +# Terran +item_name_groups[ItemGroupNames.TERRAN_ITEMS] = terran_items = [ + item_name for item_name, item_data in item_tables.item_table.items() + if item_data.race == SC2Race.TERRAN +] +item_name_groups[ItemGroupNames.TERRAN_UNITS] = terran_units = [ + item_name for item_name, item_data in item_tables.item_table.items() + if item_data.type in ( + item_tables.TerranItemType.Unit, item_tables.TerranItemType.Unit_2, item_tables.TerranItemType.Mercenary) +] +item_name_groups[ItemGroupNames.TERRAN_GENERIC_UPGRADES] = terran_generic_upgrades = [ + item_name for item_name, item_data in item_tables.item_table.items() + if item_data.type == item_tables.TerranItemType.Upgrade +] +barracks_wa_group = [ + item_names.MARINE, item_names.FIREBAT, item_names.MARAUDER, + item_names.REAPER, item_names.GHOST, item_names.SPECTRE, item_names.HERC, item_names.AEGIS_GUARD, + item_names.EMPERORS_SHADOW, item_names.DOMINION_TROOPER, item_names.SON_OF_KORHAL, +] +item_name_groups[ItemGroupNames.BARRACKS_UNITS] = barracks_units = (barracks_wa_group + [ + item_names.MEDIC, + item_names.FIELD_RESPONSE_THETA, +]) +factory_wa_group = [ + item_names.HELLION, item_names.VULTURE, item_names.GOLIATH, item_names.DIAMONDBACK, + item_names.SIEGE_TANK, item_names.THOR, item_names.PREDATOR, + item_names.CYCLONE, item_names.WARHOUND, item_names.SHOCK_DIVISION, item_names.BLACKHAMMER, + item_names.BULWARK_COMPANY, +] +item_name_groups[ItemGroupNames.FACTORY_UNITS] = factory_units = (factory_wa_group + [ + item_names.WIDOW_MINE, +]) +starport_wa_group = [ + item_names.WRAITH, item_names.VIKING, item_names.BANSHEE, + item_names.BATTLECRUISER, item_names.RAVEN_HUNTER_SEEKER_WEAPON, + item_names.LIBERATOR, item_names.VALKYRIE, item_names.PRIDE_OF_AUGUSTRGRAD, item_names.SKY_FURY, + item_names.EMPERORS_GUARDIAN, item_names.NIGHT_HAWK, item_names.NIGHT_WOLF, +] +item_name_groups[ItemGroupNames.STARPORT_UNITS] = starport_units = [ + item_names.MEDIVAC, item_names.WRAITH, item_names.VIKING, item_names.BANSHEE, + item_names.BATTLECRUISER, item_names.HERCULES, item_names.SCIENCE_VESSEL, item_names.RAVEN, + item_names.LIBERATOR, item_names.VALKYRIE, item_names.PRIDE_OF_AUGUSTRGRAD, item_names.SKY_FURY, + item_names.EMPERORS_GUARDIAN, item_names.NIGHT_HAWK, item_names.NIGHT_WOLF, +] +item_name_groups[ItemGroupNames.TERRAN_MERCENARIES] = terran_mercenaries = [ + item_name for item_name, item_data in item_tables.item_table.items() + if item_data.type == item_tables.TerranItemType.Mercenary +] +item_name_groups[ItemGroupNames.NCO_UNITS] = nco_units = [ + item_names.MARINE, item_names.MARAUDER, item_names.REAPER, + item_names.HELLION, item_names.GOLIATH, item_names.SIEGE_TANK, + item_names.RAVEN, item_names.LIBERATOR, item_names.BANSHEE, item_names.BATTLECRUISER, + item_names.HERC, # From that one bonus objective in mission 5 +] +item_name_groups[ItemGroupNames.NCO_BUILDINGS] = nco_buildings = [ + item_names.BUNKER, item_names.MISSILE_TURRET, item_names.PLANETARY_FORTRESS, +] +item_name_groups[ItemGroupNames.NOVA_EQUIPMENT] = nova_equipment = [ + *[item_name for item_name, item_data in item_tables.item_table.items() + if item_data.type == item_tables.TerranItemType.Nova_Gear], + item_names.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE, +] +item_name_groups[ItemGroupNames.NOVA_WEAPONS] = nova_weapons = [ + item_names.NOVA_C20A_CANISTER_RIFLE, + item_names.NOVA_HELLFIRE_SHOTGUN, + item_names.NOVA_PLASMA_RIFLE, + item_names.NOVA_MONOMOLECULAR_BLADE, + item_names.NOVA_BLAZEFIRE_GUNBLADE, +] +item_name_groups[ItemGroupNames.NOVA_GADGETS] = nova_gadgets = [ + item_names.NOVA_STIM_INFUSION, + item_names.NOVA_PULSE_GRENADES, + item_names.NOVA_FLASHBANG_GRENADES, + item_names.NOVA_IONIC_FORCE_FIELD, + item_names.NOVA_HOLO_DECOY, +] +item_name_groups[ItemGroupNames.WOL_UNITS] = wol_units = [ + item_names.MARINE, item_names.MEDIC, item_names.FIREBAT, item_names.MARAUDER, item_names.REAPER, + item_names.HELLION, item_names.VULTURE, item_names.GOLIATH, item_names.DIAMONDBACK, item_names.SIEGE_TANK, + item_names.MEDIVAC, item_names.WRAITH, item_names.VIKING, item_names.BANSHEE, item_names.BATTLECRUISER, + item_names.GHOST, item_names.SPECTRE, item_names.THOR, + item_names.PREDATOR, item_names.HERCULES, + item_names.SCIENCE_VESSEL, item_names.RAVEN, +] +item_name_groups[ItemGroupNames.WOL_MERCS] = wol_mercs = [ + item_names.WAR_PIGS, item_names.DEVIL_DOGS, item_names.HAMMER_SECURITIES, + item_names.SPARTAN_COMPANY, item_names.SIEGE_BREAKERS, + item_names.HELS_ANGELS, item_names.DUSK_WINGS, item_names.JACKSONS_REVENGE, +] +item_name_groups[ItemGroupNames.WOL_BUILDINGS] = wol_buildings = [ + item_names.BUNKER, item_names.MISSILE_TURRET, item_names.SENSOR_TOWER, + item_names.PERDITION_TURRET, item_names.PLANETARY_FORTRESS, + item_names.HIVE_MIND_EMULATOR, item_names.PSI_DISRUPTER, +] +item_name_groups[ItemGroupNames.TERRAN_BUILDINGS] = terran_buildings = [ + item_name for item_name, item_data in item_tables.item_table.items() + if item_data.type == item_tables.TerranItemType.Building or item_name in wol_buildings +] +item_name_groups[ItemGroupNames.MENGSK_UNITS] = [ + item_names.AEGIS_GUARD, item_names.EMPERORS_SHADOW, + item_names.SHOCK_DIVISION, item_names.BLACKHAMMER, + item_names.PRIDE_OF_AUGUSTRGRAD, item_names.SKY_FURY, + item_names.DOMINION_TROOPER, +] +item_name_groups[ItemGroupNames.TERRAN_VETERANCY_UNITS] = [ + item_names.AEGIS_GUARD, item_names.EMPERORS_SHADOW, item_names.SHOCK_DIVISION, item_names.BLACKHAMMER, + item_names.PRIDE_OF_AUGUSTRGRAD, item_names.SKY_FURY, item_names.SON_OF_KORHAL, item_names.FIELD_RESPONSE_THETA, + item_names.BULWARK_COMPANY, item_names.NIGHT_HAWK, item_names.EMPERORS_GUARDIAN, item_names.NIGHT_WOLF, +] +item_name_groups[ItemGroupNames.ORBITAL_COMMAND_ABILITIES] = orbital_command_abilities = [ + item_names.COMMAND_CENTER_SCANNER_SWEEP, + item_names.COMMAND_CENTER_MULE, + item_names.COMMAND_CENTER_EXTRA_SUPPLIES, +] +item_name_groups[ItemGroupNames.WOL_ORBITAL_COMMAND_ABILITIES] = wol_orbital_command_abilities = [ + item_names.COMMAND_CENTER_SCANNER_SWEEP, + item_names.COMMAND_CENTER_MULE, +] +spider_mine_sources = [ + item_names.VULTURE, + item_names.REAPER_SPIDER_MINES, + item_names.SIEGE_TANK_SPIDER_MINES, + item_names.RAVEN_SPIDER_MINES, +] + +# Terran Upgrades +item_name_groups[ItemGroupNames.WOL_UPGRADES] = wol_upgrades = [ + # Armory Base + item_names.BUNKER_PROJECTILE_ACCELERATOR, item_names.BUNKER_NEOSTEEL_BUNKER, + item_names.MISSILE_TURRET_TITANIUM_HOUSING, item_names.MISSILE_TURRET_HELLSTORM_BATTERIES, + item_names.SCV_ADVANCED_CONSTRUCTION, item_names.SCV_DUAL_FUSION_WELDERS, + item_names.PROGRESSIVE_FIRE_SUPPRESSION_SYSTEM, item_names.COMMAND_CENTER_MULE, item_names.COMMAND_CENTER_SCANNER_SWEEP, + # Armory Infantry + item_names.MARINE_PROGRESSIVE_STIMPACK, item_names.MARINE_COMBAT_SHIELD, + item_names.MEDIC_ADVANCED_MEDIC_FACILITIES, item_names.MEDIC_STABILIZER_MEDPACKS, + item_names.FIREBAT_INCINERATOR_GAUNTLETS, item_names.FIREBAT_JUGGERNAUT_PLATING, + item_names.MARAUDER_CONCUSSIVE_SHELLS, item_names.MARAUDER_KINETIC_FOAM, + item_names.REAPER_U238_ROUNDS, item_names.REAPER_G4_CLUSTERBOMB, + # Armory Vehicles + item_names.HELLION_TWIN_LINKED_FLAMETHROWER, item_names.HELLION_THERMITE_FILAMENTS, + item_names.SPIDER_MINE_CERBERUS_MINE, item_names.VULTURE_PROGRESSIVE_REPLENISHABLE_MAGAZINE, + item_names.GOLIATH_MULTI_LOCK_WEAPONS_SYSTEM, item_names.GOLIATH_ARES_CLASS_TARGETING_SYSTEM, + item_names.DIAMONDBACK_PROGRESSIVE_TRI_LITHIUM_POWER_CELL, item_names.DIAMONDBACK_SHAPED_HULL, + item_names.SIEGE_TANK_MAELSTROM_ROUNDS, item_names.SIEGE_TANK_SHAPED_BLAST, + # Armory Starships + item_names.MEDIVAC_RAPID_DEPLOYMENT_TUBE, item_names.MEDIVAC_ADVANCED_HEALING_AI, + item_names.WRAITH_PROGRESSIVE_TOMAHAWK_POWER_CELLS, item_names.WRAITH_DISPLACEMENT_FIELD, + item_names.VIKING_RIPWAVE_MISSILES, item_names.VIKING_PHOBOS_CLASS_WEAPONS_SYSTEM, + item_names.BANSHEE_PROGRESSIVE_CROSS_SPECTRUM_DAMPENERS, item_names.BANSHEE_SHOCKWAVE_MISSILE_BATTERY, + item_names.BATTLECRUISER_PROGRESSIVE_MISSILE_PODS, item_names.BATTLECRUISER_PROGRESSIVE_DEFENSIVE_MATRIX, + # Armory Dominion + item_names.GHOST_OCULAR_IMPLANTS, item_names.GHOST_CRIUS_SUIT, + item_names.SPECTRE_PSIONIC_LASH, item_names.SPECTRE_NYX_CLASS_CLOAKING_MODULE, + item_names.THOR_330MM_BARRAGE_CANNON, item_names.THOR_PROGRESSIVE_IMMORTALITY_PROTOCOL, + # Lab Zerg + item_names.BUNKER_FORTIFIED_BUNKER, item_names.BUNKER_SHRIKE_TURRET, + item_names.PROGRESSIVE_REGENERATIVE_BIO_STEEL, item_names.CELLULAR_REACTOR, + # Other 3 levels are units/buildings (Perdition, PF, Hercules, Predator, HME, Psi Disrupter) + # Lab Protoss + item_names.VANADIUM_PLATING, item_names.ULTRA_CAPACITORS, + item_names.AUTOMATED_REFINERY, item_names.MICRO_FILTERING, + item_names.ORBITAL_DEPOTS, item_names.COMMAND_CENTER_COMMAND_CENTER_REACTOR, + item_names.ORBITAL_STRIKE, item_names.TECH_REACTOR, + # Other level is units (Raven, Science Vessel) +] +item_name_groups[ItemGroupNames.TERRAN_STIMPACKS] = terran_stimpacks = [ + item_names.MARINE_PROGRESSIVE_STIMPACK, + item_names.MARAUDER_PROGRESSIVE_STIMPACK, + item_names.REAPER_PROGRESSIVE_STIMPACK, + item_names.FIREBAT_PROGRESSIVE_STIMPACK, + item_names.HELLION_PROGRESSIVE_STIMPACK, +] +item_name_groups[ItemGroupNames.TERRAN_ORIGINAL_PROGRESSIVE_UPGRADES] = terran_original_progressive_upgrades = [ + item_names.PROGRESSIVE_FIRE_SUPPRESSION_SYSTEM, + item_names.MARINE_PROGRESSIVE_STIMPACK, + item_names.VULTURE_PROGRESSIVE_REPLENISHABLE_MAGAZINE, + item_names.DIAMONDBACK_PROGRESSIVE_TRI_LITHIUM_POWER_CELL, + item_names.WRAITH_PROGRESSIVE_TOMAHAWK_POWER_CELLS, + item_names.BANSHEE_PROGRESSIVE_CROSS_SPECTRUM_DAMPENERS, + item_names.BATTLECRUISER_PROGRESSIVE_MISSILE_PODS, + item_names.BATTLECRUISER_PROGRESSIVE_DEFENSIVE_MATRIX, + item_names.THOR_PROGRESSIVE_IMMORTALITY_PROTOCOL, + item_names.PROGRESSIVE_REGENERATIVE_BIO_STEEL, +] +item_name_groups[ItemGroupNames.NCO_BASELINE_UPGRADES] = nco_baseline_upgrades = [ + item_names.BUNKER_NEOSTEEL_BUNKER, # Baseline from mission 2 + item_names.BUNKER_FORTIFIED_BUNKER, # Baseline from mission 2 + item_names.MARINE_COMBAT_SHIELD, # Baseline from mission 2 + item_names.MARAUDER_KINETIC_FOAM, # Baseline outside WOL + item_names.MARAUDER_CONCUSSIVE_SHELLS, # Baseline from mission 2 + item_names.REAPER_BALLISTIC_FLIGHTSUIT, # Baseline from mission 2 + item_names.HELLION_HELLBAT, # Baseline from mission 3 + item_names.GOLIATH_INTERNAL_TECH_MODULE, # Baseline from mission 4 + item_names.GOLIATH_SHAPED_HULL, + # ItemNames.GOLIATH_RESOURCE_EFFICIENCY, # Supply savings baseline in NCO, mineral savings is non-NCO + item_names.SIEGE_TANK_SHAPED_HULL, # Baseline NCO gives +10; this upgrade gives +25 + item_names.SIEGE_TANK_SHAPED_BLAST, # Baseline from mission 3 + item_names.LIBERATOR_RAID_ARTILLERY, # Baseline in mission 5 + item_names.RAVEN_BIO_MECHANICAL_REPAIR_DRONE, # Baseline in mission 5 + item_names.BATTLECRUISER_TACTICAL_JUMP, + item_names.BATTLECRUISER_MOIRAI_IMPULSE_DRIVE, + item_names.PROGRESSIVE_FIRE_SUPPRESSION_SYSTEM, # Baseline from mission 2 + item_names.ORBITAL_DEPOTS, # Baseline from mission 2 + item_names.COMMAND_CENTER_SCANNER_SWEEP, # In NCO you must actually morph Command Center into Orbital Command + item_names.COMMAND_CENTER_EXTRA_SUPPLIES, # But in AP this works WoL-style +] + nco_buildings +item_name_groups[ItemGroupNames.NCO_UNIT_TECHNOLOGY] = nco_unit_technology = [ + item_names.MARINE_LASER_TARGETING_SYSTEM, + item_names.MARINE_PROGRESSIVE_STIMPACK, + item_names.MARINE_MAGRAIL_MUNITIONS, + item_names.MARINE_OPTIMIZED_LOGISTICS, + item_names.MARAUDER_LASER_TARGETING_SYSTEM, + item_names.MARAUDER_INTERNAL_TECH_MODULE, + item_names.MARAUDER_PROGRESSIVE_STIMPACK, + item_names.MARAUDER_MAGRAIL_MUNITIONS, + item_names.REAPER_SPIDER_MINES, + item_names.REAPER_LASER_TARGETING_SYSTEM, + item_names.REAPER_PROGRESSIVE_STIMPACK, + item_names.REAPER_ADVANCED_CLOAKING_FIELD, + # Reaper special ordnance gives anti-building attack, which is baseline in AP + item_names.HELLION_JUMP_JETS, + item_names.HELLION_PROGRESSIVE_STIMPACK, + item_names.HELLION_SMART_SERVOS, + item_names.HELLION_OPTIMIZED_LOGISTICS, + item_names.HELLION_THERMITE_FILAMENTS, # Called Infernal Pre-Igniter in NCO + item_names.GOLIATH_ARES_CLASS_TARGETING_SYSTEM, # Called Laser Targeting System in NCO + item_names.GOLIATH_JUMP_JETS, + item_names.GOLIATH_OPTIMIZED_LOGISTICS, + item_names.GOLIATH_MULTI_LOCK_WEAPONS_SYSTEM, + item_names.SIEGE_TANK_SPIDER_MINES, + item_names.SIEGE_TANK_JUMP_JETS, + item_names.SIEGE_TANK_INTERNAL_TECH_MODULE, + item_names.SIEGE_TANK_SMART_SERVOS, + # Tanks can't get Laser targeting system in NCO + item_names.BANSHEE_INTERNAL_TECH_MODULE, + item_names.BANSHEE_PROGRESSIVE_CROSS_SPECTRUM_DAMPENERS, + item_names.BANSHEE_SHOCKWAVE_MISSILE_BATTERY, # Banshee Special Ordnance + # Banshees can't get laser targeting systems in NCO + item_names.LIBERATOR_CLOAK, + item_names.LIBERATOR_SMART_SERVOS, + item_names.LIBERATOR_OPTIMIZED_LOGISTICS, + # Liberators can't get laser targeting system in NCO + item_names.RAVEN_SPIDER_MINES, + item_names.RAVEN_INTERNAL_TECH_MODULE, + item_names.RAVEN_RAILGUN_TURRET, # Raven Magrail Munitions + item_names.RAVEN_HUNTER_SEEKER_WEAPON, # Raven Special Ordnance + item_names.BATTLECRUISER_INTERNAL_TECH_MODULE, + item_names.BATTLECRUISER_CLOAK, + item_names.BATTLECRUISER_ATX_LASER_BATTERY, # Battlecruiser Special Ordnance + item_names.PROGRESSIVE_REGENERATIVE_BIO_STEEL, +] +item_name_groups[ItemGroupNames.NCO_UPGRADES] = nco_upgrades = nco_baseline_upgrades + nco_unit_technology +item_name_groups[ItemGroupNames.NCO_MAX_PROGRESSIVE_ITEMS] = nco_unit_technology + nova_equipment + terran_generic_upgrades +item_name_groups[ItemGroupNames.NCO_MIN_PROGRESSIVE_ITEMS] = nco_units + nco_baseline_upgrades +item_name_groups[ItemGroupNames.TERRAN_PROGRESSIVE_UPGRADES] = terran_progressive_items = [ + item_name for item_name, item_data in item_tables.item_table.items() + if item_data.type in (item_tables.TerranItemType.Progressive, item_tables.TerranItemType.Progressive_2) +] +item_name_groups[ItemGroupNames.WOL_ITEMS] = vanilla_wol_items = ( + wol_units + + wol_buildings + + wol_mercs + + wol_upgrades + + orbital_command_abilities + + terran_generic_upgrades +) + +# Zerg +item_name_groups[ItemGroupNames.ZERG_ITEMS] = zerg_items = [ + item_name for item_name, item_data in item_tables.item_table.items() + if item_data.race == SC2Race.ZERG +] +item_name_groups[ItemGroupNames.ZERG_BUILDINGS] = zerg_buildings = [ + item_names.SPINE_CRAWLER, + item_names.SPORE_CRAWLER, + item_names.BILE_LAUNCHER, + item_names.INFESTED_BUNKER, + item_names.INFESTED_MISSILE_TURRET, + item_names.NYDUS_WORM, + item_names.ECHIDNA_WORM] +item_name_groups[ItemGroupNames.ZERG_NONMORPH_UNITS] = zerg_nonmorph_units = [ + item_name for item_name, item_data in item_tables.item_table.items() + if item_data.type in ( + item_tables.ZergItemType.Unit, item_tables.ZergItemType.Mercenary + ) + and item_name not in zerg_buildings +] +item_name_groups[ItemGroupNames.ZERG_MORPHS] = zerg_morphs = [ + item_name for item_name, item_data in item_tables.item_table.items() if item_data.type == item_tables.ZergItemType.Morph +] +item_name_groups[ItemGroupNames.ZERG_UNITS] = zerg_units = zerg_nonmorph_units + zerg_morphs +# For W/A upgrades +zerg_ground_units = [ + item_names.ZERGLING, item_names.SWARM_QUEEN, item_names.ROACH, item_names.HYDRALISK, item_names.ABERRATION, + item_names.SWARM_HOST, item_names.INFESTOR, item_names.ULTRALISK, item_names.ZERGLING_BANELING_ASPECT, + item_names.HYDRALISK_LURKER_ASPECT, item_names.HYDRALISK_IMPALER_ASPECT, item_names.ULTRALISK_TYRANNOZOR_ASPECT, + item_names.ROACH_RAVAGER_ASPECT, item_names.DEFILER, item_names.ROACH_PRIMAL_IGNITER_ASPECT, + item_names.PYGALISK, + item_names.INFESTED_MARINE, item_names.INFESTED_BUNKER, item_names.INFESTED_DIAMONDBACK, + item_names.INFESTED_SIEGE_TANK, +] +zerg_melee_wa = [ + item_names.ZERGLING, item_names.ABERRATION, item_names.ULTRALISK, item_names.ZERGLING_BANELING_ASPECT, + item_names.ULTRALISK_TYRANNOZOR_ASPECT, item_names.INFESTED_BUNKER, item_names.PYGALISK, +] +zerg_ranged_wa = [ + item_names.SWARM_QUEEN, item_names.ROACH, item_names.HYDRALISK, item_names.SWARM_HOST, + item_names.HYDRALISK_LURKER_ASPECT, item_names.HYDRALISK_IMPALER_ASPECT, item_names.ULTRALISK_TYRANNOZOR_ASPECT, + item_names.ROACH_RAVAGER_ASPECT, item_names.ROACH_PRIMAL_IGNITER_ASPECT, item_names.INFESTED_MARINE, + item_names.INFESTED_BUNKER, item_names.INFESTED_DIAMONDBACK, item_names.INFESTED_SIEGE_TANK, +] +zerg_air_units = [ + item_names.MUTALISK, item_names.MUTALISK_CORRUPTOR_VIPER_ASPECT, item_names.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT, + item_names.CORRUPTOR, item_names.BROOD_QUEEN, item_names.SCOURGE, item_names.MUTALISK_CORRUPTOR_GUARDIAN_ASPECT, + item_names.MUTALISK_CORRUPTOR_DEVOURER_ASPECT, item_names.INFESTED_BANSHEE, item_names.INFESTED_LIBERATOR, +] +item_name_groups[ItemGroupNames.ZERG_GENERIC_UPGRADES] = zerg_generic_upgrades = [ + item_name for item_name, item_data in item_tables.item_table.items() + if item_data.type == item_tables.ZergItemType.Upgrade +] +item_name_groups[ItemGroupNames.HOTS_UNITS] = hots_units = [ + item_names.ZERGLING, item_names.SWARM_QUEEN, item_names.ROACH, item_names.HYDRALISK, + item_names.ABERRATION, item_names.SWARM_HOST, item_names.MUTALISK, + item_names.INFESTOR, item_names.ULTRALISK, + item_names.ZERGLING_BANELING_ASPECT, + item_names.HYDRALISK_LURKER_ASPECT, + item_names.HYDRALISK_IMPALER_ASPECT, + item_names.MUTALISK_CORRUPTOR_VIPER_ASPECT, + item_names.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT, +] +item_name_groups[ItemGroupNames.HOTS_BUILDINGS] = hots_buildings = [ + item_names.SPINE_CRAWLER, + item_names.SPORE_CRAWLER, +] +item_name_groups[ItemGroupNames.HOTS_MORPHS] = hots_morphs = [ + item_names.ZERGLING_BANELING_ASPECT, + item_names.HYDRALISK_IMPALER_ASPECT, + item_names.HYDRALISK_LURKER_ASPECT, + item_names.MUTALISK_CORRUPTOR_VIPER_ASPECT, + item_names.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT, +] +item_name_groups[ItemGroupNames.ZERG_MERCENARIES] = zerg_mercenaries = [ + item_name for item_name, item_data in item_tables.item_table.items() if item_data.type == item_tables.ZergItemType.Mercenary +] +item_name_groups[ItemGroupNames.KERRIGAN_ABILITIES] = kerrigan_abilities = [ + item_name for item_name, item_data in item_tables.item_table.items() if item_data.type == item_tables.ZergItemType.Ability +] +item_name_groups[ItemGroupNames.KERRIGAN_PASSIVES] = kerrigan_passives = [ + item_names.KERRIGAN_HEROIC_FORTITUDE, item_names.KERRIGAN_CHAIN_REACTION, + item_names.KERRIGAN_INFEST_BROODLINGS, item_names.KERRIGAN_FURY, item_names.KERRIGAN_ABILITY_EFFICIENCY, +] +item_name_groups[ItemGroupNames.KERRIGAN_ACTIVE_ABILITIES] = kerrigan_active_abilities = [ + item_name for item_name in kerrigan_abilities if item_name not in kerrigan_passives +] +item_name_groups[ItemGroupNames.KERRIGAN_LOGIC_ACTIVE_ABILITIES] = kerrigan_logic_active_abilities = [ + item_name for item_name in kerrigan_active_abilities if item_name != item_names.KERRIGAN_ASSIMILATION_AURA +] +item_name_groups[ItemGroupNames.KERRIGAN_TIER_1] = kerrigan_tier_1 = [ + item_names.KERRIGAN_CRUSHING_GRIP, item_names.KERRIGAN_HEROIC_FORTITUDE, item_names.KERRIGAN_LEAPING_STRIKE +] +item_name_groups[ItemGroupNames.KERRIGAN_TIER_2] = kerrigan_tier_2= [ + item_names.KERRIGAN_CRUSHING_GRIP, item_names.KERRIGAN_CHAIN_REACTION, item_names.KERRIGAN_PSIONIC_SHIFT +] +item_name_groups[ItemGroupNames.KERRIGAN_TIER_3] = kerrigan_tier_3 = [ + item_names.TWIN_DRONES, item_names.AUTOMATED_EXTRACTORS, item_names.ZERGLING_RECONSTITUTION +] +item_name_groups[ItemGroupNames.KERRIGAN_TIER_4] = kerrigan_tier_4 = [ + item_names.KERRIGAN_MEND, item_names.KERRIGAN_SPAWN_BANELINGS, item_names.KERRIGAN_WILD_MUTATION +] +item_name_groups[ItemGroupNames.KERRIGAN_TIER_5] = kerrigan_tier_5 = [ + item_names.MALIGNANT_CREEP, item_names.VESPENE_EFFICIENCY, item_names.OVERLORD_IMPROVED_OVERLORDS +] +item_name_groups[ItemGroupNames.KERRIGAN_TIER_6] = kerrigan_tier_6 = [ + item_names.KERRIGAN_INFEST_BROODLINGS, item_names.KERRIGAN_FURY, item_names.KERRIGAN_ABILITY_EFFICIENCY +] +item_name_groups[ItemGroupNames.KERRIGAN_TIER_7] = kerrigan_tier_7 = [ + item_names.KERRIGAN_APOCALYPSE, item_names.KERRIGAN_SPAWN_LEVIATHAN, item_names.KERRIGAN_DROP_PODS +] +item_name_groups[ItemGroupNames.KERRIGAN_ULTIMATES] = kerrigan_ultimates = [ + *kerrigan_tier_7, item_names.KERRIGAN_ASSIMILATION_AURA, item_names.KERRIGAN_IMMOBILIZATION_WAVE +] +item_name_groups[ItemGroupNames.KERRIGAN_NON_ULTIMATES] = kerrigan_non_ulimates = [ + item for item in kerrigan_abilities if item not in kerrigan_ultimates +] +item_name_groups[ItemGroupNames.KERRIGAN_LOGIC_ULTIMATES] = kerrigan_logic_ultimates = [ + item for item in kerrigan_ultimates if item != item_names.KERRIGAN_ASSIMILATION_AURA +] +item_name_groups[ItemGroupNames.KERRIGAN_NON_ULTIMATE_ACTIVE_ABILITIES] = kerrigan_non_ulimate_active_abilities = [ + item for item in kerrigan_non_ulimates if item in kerrigan_active_abilities +] +item_name_groups[ItemGroupNames.KERRIGAN_HOTS_ABILITIES] = kerrigan_hots_abilities = [ + ability for tiers in [ + kerrigan_tier_1, kerrigan_tier_2, kerrigan_tier_4, kerrigan_tier_6, kerrigan_tier_7 + ] for ability in tiers +] + +item_name_groups[ItemGroupNames.OVERLORD_UPGRADES] = [ + item_names.OVERLORD_ANTENNAE, + item_names.OVERLORD_VENTRAL_SACS, + item_names.OVERLORD_GENERATE_CREEP, + item_names.OVERLORD_PNEUMATIZED_CARAPACE, + item_names.OVERLORD_IMPROVED_OVERLORDS, + item_names.OVERLORD_OVERSEER_ASPECT, +] + +# Zerg Upgrades +item_name_groups[ItemGroupNames.HOTS_STRAINS] = hots_strains = [ + item_name for item_name, item_data in item_tables.item_table.items() if item_data.type == item_tables.ZergItemType.Strain +] +item_name_groups[ItemGroupNames.HOTS_MUTATIONS] = hots_mutations = [ + item_names.ZERGLING_HARDENED_CARAPACE, item_names.ZERGLING_ADRENAL_OVERLOAD, item_names.ZERGLING_METABOLIC_BOOST, + item_names.BANELING_CORROSIVE_ACID, item_names.BANELING_RUPTURE, item_names.BANELING_REGENERATIVE_ACID, + item_names.ROACH_HYDRIODIC_BILE, item_names.ROACH_ADAPTIVE_PLATING, item_names.ROACH_TUNNELING_CLAWS, + item_names.HYDRALISK_FRENZY, item_names.HYDRALISK_ANCILLARY_CARAPACE, item_names.HYDRALISK_GROOVED_SPINES, + item_names.SWARM_HOST_BURROW, item_names.SWARM_HOST_RAPID_INCUBATION, item_names.SWARM_HOST_PRESSURIZED_GLANDS, + item_names.MUTALISK_VICIOUS_GLAIVE, item_names.MUTALISK_RAPID_REGENERATION, item_names.MUTALISK_SUNDERING_GLAIVE, + item_names.ULTRALISK_BURROW_CHARGE, item_names.ULTRALISK_TISSUE_ASSIMILATION, item_names.ULTRALISK_MONARCH_BLADES, +] +item_name_groups[ItemGroupNames.HOTS_GLOBAL_UPGRADES] = hots_global_upgrades = [ + item_names.ZERGLING_RECONSTITUTION, + item_names.OVERLORD_IMPROVED_OVERLORDS, + item_names.AUTOMATED_EXTRACTORS, + item_names.TWIN_DRONES, + item_names.MALIGNANT_CREEP, + item_names.VESPENE_EFFICIENCY, +] +item_name_groups[ItemGroupNames.HOTS_ITEMS] = vanilla_hots_items = ( + hots_units + + hots_buildings + + kerrigan_hots_abilities + + hots_mutations + + hots_strains + + hots_global_upgrades + + zerg_generic_upgrades +) + +# Zerg - Infested Terran (Stukov Co-op) +item_name_groups[ItemGroupNames.INF_TERRAN_UNITS] = infterr_units = [ + item_names.INFESTED_MARINE, + item_names.INFESTED_BUNKER, + item_names.BULLFROG, + item_names.INFESTED_DIAMONDBACK, + item_names.INFESTED_SIEGE_TANK, + item_names.INFESTED_LIBERATOR, + item_names.INFESTED_BANSHEE, +] +item_name_groups[ItemGroupNames.INF_TERRAN_UPGRADES] = infterr_upgrades = [ + item_names.INFESTED_SCV_BUILD_CHARGES, + item_names.INFESTED_MARINE_PLAGUED_MUNITIONS, + item_names.INFESTED_MARINE_RETINAL_AUGMENTATION, + item_names.INFESTED_BUNKER_CALCIFIED_ARMOR, + item_names.INFESTED_BUNKER_REGENERATIVE_PLATING, + item_names.INFESTED_BUNKER_ENGORGED_BUNKERS, + item_names.BULLFROG_WILD_MUTATION, + item_names.BULLFROG_BROODLINGS, + item_names.BULLFROG_HARD_IMPACT, + item_names.BULLFROG_RANGE, + item_names.INFESTED_DIAMONDBACK_CAUSTIC_MUCUS, + item_names.INFESTED_DIAMONDBACK_CONCENTRATED_SPEW, + item_names.INFESTED_DIAMONDBACK_PROGRESSIVE_FUNGAL_SNARE, + item_names.INFESTED_DIAMONDBACK_VIOLENT_ENZYMES, + item_names.INFESTED_SIEGE_TANK_ACIDIC_ENZYMES, + item_names.INFESTED_SIEGE_TANK_BALANCED_ROOTS, + item_names.INFESTED_SIEGE_TANK_DEEP_TUNNEL, + item_names.INFESTED_SIEGE_TANK_PROGRESSIVE_AUTOMATED_MITOSIS, + item_names.INFESTED_SIEGE_TANK_SEISMIC_SONAR, + item_names.INFESTED_LIBERATOR_CLOUD_DISPERSAL, + item_names.INFESTED_LIBERATOR_DEFENDER_MODE, + item_names.INFESTED_LIBERATOR_VIRAL_CONTAMINATION, + item_names.INFESTED_BANSHEE_FLESHFUSED_TARGETING_OPTICS, + item_names.INFESTED_BANSHEE_BRACED_EXOSKELETON, + item_names.INFESTED_BANSHEE_RAPID_HIBERNATION, + item_names.INFESTED_DIAMONDBACK_FRIGHTFUL_FLESHWELDER, + item_names.INFESTED_SIEGE_TANK_FRIGHTFUL_FLESHWELDER, + item_names.INFESTED_LIBERATOR_FRIGHTFUL_FLESHWELDER, + item_names.INFESTED_BANSHEE_FRIGHTFUL_FLESHWELDER, + item_names.INFESTED_MISSILE_TURRET_BIOELECTRIC_PAYLOAD, + item_names.INFESTED_MISSILE_TURRET_ACID_SPORE_VENTS, +] +item_name_groups[ItemGroupNames.INF_TERRAN_ITEMS] = ( + infterr_units + + infterr_upgrades + + [item_names.INFESTED_MISSILE_TURRET] +) + +# Protoss +item_name_groups[ItemGroupNames.PROTOSS_ITEMS] = protoss_items = [ + item_name for item_name, item_data in item_tables.item_table.items() + if item_data.race == SC2Race.PROTOSS +] +item_name_groups[ItemGroupNames.PROTOSS_UNITS] = protoss_units = [ + item_name for item_name, item_data in item_tables.item_table.items() + if item_data.type in (item_tables.ProtossItemType.Unit, item_tables.ProtossItemType.Unit_2) +] +protoss_ground_wa = [ + item_names.ZEALOT, item_names.CENTURION, item_names.SENTINEL, item_names.SUPPLICANT, + item_names.SENTRY, item_names.ENERGIZER, + item_names.STALKER, item_names.INSTIGATOR, item_names.SLAYER, item_names.DRAGOON, item_names.ADEPT, + item_names.HIGH_TEMPLAR, item_names.SIGNIFIER, item_names.ASCENDANT, + item_names.DARK_TEMPLAR, item_names.BLOOD_HUNTER, item_names.AVENGER, + item_names.DARK_ARCHON, + item_names.IMMORTAL, item_names.ANNIHILATOR, item_names.VANGUARD, item_names.STALWART, + item_names.COLOSSUS, item_names.WRATHWALKER, + item_names.REAVER, +] +protoss_air_wa = [ + item_names.WARP_PRISM_PHASE_BLASTER, + item_names.PHOENIX, item_names.MIRAGE, item_names.CORSAIR, item_names.SKIRMISHER, + item_names.VOID_RAY, item_names.DESTROYER, item_names.PULSAR, item_names.DAWNBRINGER, + item_names.CARRIER, item_names.SKYLORD, item_names.TRIREME, + item_names.SCOUT, item_names.TEMPEST, item_names.MOTHERSHIP, + item_names.ARBITER, item_names.ORACLE, item_names.OPPRESSOR, + item_names.CALADRIUS, item_names.MISTWING, +] +item_name_groups[ItemGroupNames.PROTOSS_GENERIC_UPGRADES] = protoss_generic_upgrades = [ + item_name for item_name, item_data in item_tables.item_table.items() + if item_data.type == item_tables.ProtossItemType.Upgrade +] +item_name_groups[ItemGroupNames.LOTV_UNITS] = lotv_units = [ + item_names.ZEALOT, item_names.CENTURION, item_names.SENTINEL, + item_names.STALKER, item_names.DRAGOON, item_names.ADEPT, + item_names.SENTRY, item_names.HAVOC, item_names.ENERGIZER, + item_names.HIGH_TEMPLAR, item_names.DARK_ARCHON, item_names.ASCENDANT, + item_names.DARK_TEMPLAR, item_names.AVENGER, item_names.BLOOD_HUNTER, + item_names.IMMORTAL, item_names.ANNIHILATOR, item_names.VANGUARD, + item_names.COLOSSUS, item_names.WRATHWALKER, item_names.REAVER, + item_names.PHOENIX, item_names.MIRAGE, item_names.CORSAIR, + item_names.VOID_RAY, item_names.DESTROYER, item_names.ARBITER, + item_names.CARRIER, item_names.TEMPEST, item_names.MOTHERSHIP, +] +item_name_groups[ItemGroupNames.PROPHECY_UNITS] = prophecy_units = [ + item_names.ZEALOT, item_names.STALKER, item_names.HIGH_TEMPLAR, item_names.DARK_TEMPLAR, + item_names.OBSERVER, item_names.COLOSSUS, + item_names.PHOENIX, item_names.VOID_RAY, item_names.CARRIER, +] +item_name_groups[ItemGroupNames.PROPHECY_BUILDINGS] = prophecy_buildings = [ + item_names.PHOTON_CANNON, +] +item_name_groups[ItemGroupNames.GATEWAY_UNITS] = gateway_units = [ + item_names.ZEALOT, item_names.CENTURION, item_names.SENTINEL, item_names.SUPPLICANT, + item_names.STALKER, item_names.INSTIGATOR, item_names.SLAYER, + item_names.SENTRY, item_names.HAVOC, item_names.ENERGIZER, + item_names.DRAGOON, item_names.ADEPT, item_names.DARK_ARCHON, + item_names.HIGH_TEMPLAR, item_names.SIGNIFIER, item_names.ASCENDANT, + item_names.DARK_TEMPLAR, item_names.AVENGER, item_names.BLOOD_HUNTER, +] +item_name_groups[ItemGroupNames.ROBO_UNITS] = robo_units = [ + item_names.WARP_PRISM, item_names.OBSERVER, + item_names.IMMORTAL, item_names.ANNIHILATOR, item_names.VANGUARD, item_names.STALWART, + item_names.COLOSSUS, item_names.WRATHWALKER, + item_names.REAVER, item_names.DISRUPTOR, +] +item_name_groups[ItemGroupNames.STARGATE_UNITS] = stargate_units = [ + item_names.PHOENIX, item_names.SKIRMISHER, item_names.MIRAGE, item_names.CORSAIR, + item_names.VOID_RAY, item_names.DESTROYER, item_names.PULSAR, item_names.DAWNBRINGER, + item_names.CARRIER, item_names.SKYLORD, item_names.TRIREME, + item_names.TEMPEST, item_names.SCOUT, item_names.MOTHERSHIP, + item_names.ARBITER, item_names.ORACLE, item_names.OPPRESSOR, + item_names.CALADRIUS, item_names.MISTWING, +] +item_name_groups[ItemGroupNames.PROTOSS_BUILDINGS] = protoss_buildings = [ + item_name for item_name, item_data in item_tables.item_table.items() + if item_data.type == item_tables.ProtossItemType.Building +] +item_name_groups[ItemGroupNames.AIUR_UNITS] = [ + item_names.ZEALOT, item_names.DRAGOON, item_names.SENTRY, item_names.AVENGER, item_names.HIGH_TEMPLAR, + item_names.IMMORTAL, item_names.REAVER, + item_names.PHOENIX, item_names.SCOUT, item_names.ARBITER, item_names.CARRIER, +] +item_name_groups[ItemGroupNames.NERAZIM_UNITS] = [ + item_names.CENTURION, item_names.STALKER, item_names.DARK_TEMPLAR, item_names.SIGNIFIER, item_names.DARK_ARCHON, + item_names.ANNIHILATOR, + item_names.CORSAIR, item_names.ORACLE, item_names.VOID_RAY, item_names.MISTWING, +] +item_name_groups[ItemGroupNames.TAL_DARIM_UNITS] = [ + item_names.SUPPLICANT, item_names.SLAYER, item_names.HAVOC, item_names.BLOOD_HUNTER, item_names.ASCENDANT, + item_names.VANGUARD, item_names.WRATHWALKER, + item_names.SKIRMISHER, item_names.DESTROYER, item_names.SKYLORD, item_names.MOTHERSHIP, item_names.OPPRESSOR, +] +item_name_groups[ItemGroupNames.PURIFIER_UNITS] = [ + item_names.SENTINEL, item_names.ADEPT, item_names.INSTIGATOR, item_names.ENERGIZER, + item_names.STALWART, item_names.COLOSSUS, item_names.DISRUPTOR, + item_names.MIRAGE, item_names.DAWNBRINGER, item_names.TRIREME, item_names.TEMPEST, + item_names.CALADRIUS, +] +item_name_groups[ItemGroupNames.SOA_ITEMS] = soa_items = [ + *[item_name for item_name, item_data in item_tables.item_table.items() if item_data.type == item_tables.ProtossItemType.Spear_Of_Adun], + item_names.SOA_PROGRESSIVE_PROXY_PYLON, +] +lotv_soa_items = [item_name for item_name in soa_items if item_name != item_names.SOA_PYLON_OVERCHARGE] +item_name_groups[ItemGroupNames.PROTOSS_GLOBAL_UPGRADES] = [ + item_name for item_name, item_data in item_tables.item_table.items() if item_data.type == item_tables.ProtossItemType.Solarite_Core +] +item_name_groups[ItemGroupNames.LOTV_GLOBAL_UPGRADES] = lotv_global_upgrades = [ + item_names.NEXUS_OVERCHARGE, + item_names.ORBITAL_ASSIMILATORS, + item_names.WARP_HARMONIZATION, + item_names.MATRIX_OVERLOAD, + item_names.GUARDIAN_SHELL, + item_names.RECONSTRUCTION_BEAM, +] +item_name_groups[ItemGroupNames.WAR_COUNCIL] = war_council_upgrades = [ + item_name for item_name, item_data in item_tables.item_table.items() + if item_data.type in (item_tables.ProtossItemType.War_Council, item_tables.ProtossItemType.War_Council_2) +] + +lotv_war_council_upgrades = [ + item_name for item_name, item_data in item_tables.item_table.items() + if ( + item_name in war_council_upgrades + and item_data.parent in item_name_groups[ItemGroupNames.LOTV_UNITS] + # Destroyers get a custom (non-vanilla) buff, not a nerf over their vanilla council state + and item_name != item_names.DESTROYER_REFORGED_BLOODSHARD_CORE + ) +] +item_name_groups[ItemGroupNames.LOTV_ITEMS] = vanilla_lotv_items = ( + lotv_units + + protoss_buildings + + lotv_soa_items + + lotv_global_upgrades + + protoss_generic_upgrades + + lotv_war_council_upgrades +) + +item_name_groups[ItemGroupNames.VANILLA_ITEMS] = vanilla_items = ( + vanilla_wol_items + vanilla_hots_items + vanilla_lotv_items +) + +item_name_groups[ItemGroupNames.OVERPOWERED_ITEMS] = overpowered_items = [ + # Terran general + item_names.SIEGE_TANK_GRADUATING_RANGE, + item_names.RAVEN_HUNTER_SEEKER_WEAPON, + item_names.BATTLECRUISER_ATX_LASER_BATTERY, + item_names.PROGRESSIVE_REGENERATIVE_BIO_STEEL, + item_names.MECHANICAL_KNOW_HOW, + item_names.MERCENARY_MUNITIONS, + + # Terran Mind Control + item_names.HIVE_MIND_EMULATOR, + item_names.PSI_INDOCTRINATOR, + item_names.ARGUS_AMPLIFIER, + + # Zerg Mind Control + item_names.INFESTOR, + + # Protoss Mind Control + item_names.DARK_ARCHON_INDOMITABLE_WILL, + + # Nova + item_names.NOVA_PLASMA_RIFLE, + + # Kerrigan + item_names.KERRIGAN_APOCALYPSE, + item_names.KERRIGAN_DROP_PODS, + item_names.KERRIGAN_SPAWN_LEVIATHAN, + item_names.KERRIGAN_IMMOBILIZATION_WAVE, + + # SOA + item_names.SOA_TIME_STOP, + item_names.SOA_SOLAR_LANCE, + item_names.SOA_DEPLOY_FENIX, + # Note: This is more an issue of having multiple ults at the same time, rather than solar bombardment in particular. + # Can be removed from the list if we get an SOA ult combined cooldown or energy cost on it. + item_names.SOA_SOLAR_BOMBARDMENT, + + # Protoss general + item_names.QUATRO, + item_names.MOTHERSHIP_INTEGRATED_POWER, + item_names.IMMORTAL_ANNIHILATOR_ADVANCED_TARGETING, + + # Mindless Broodwar garbage + item_names.GHOST_BARGAIN_BIN_PRICES, + item_names.SPECTRE_BARGAIN_BIN_PRICES, + item_names.REAVER_BARGAIN_BIN_PRICES, + item_names.SCOUT_SUPPLY_EFFICIENCY, +] + +# Items not aimed to be officially released +# These need further balancing, and they shouldn't generate normally unless explicitly locked +# Added here to not confuse the client +item_name_groups[ItemGroupNames.UNRELEASED_ITEMS] = unreleased_items = [ + item_names.PRIDE_OF_AUGUSTRGRAD, + item_names.SKY_FURY, + item_names.SHOCK_DIVISION, + item_names.BLACKHAMMER, + item_names.AEGIS_GUARD, + item_names.EMPERORS_SHADOW, + item_names.SON_OF_KORHAL, + item_names.BULWARK_COMPANY, + item_names.FIELD_RESPONSE_THETA, + item_names.EMPERORS_GUARDIAN, + item_names.NIGHT_HAWK, + item_names.NIGHT_WOLF, + item_names.EMPERORS_SHADOW_SOVEREIGN_TACTICAL_MISSILES, +] + +# A place for traits that were released before but are to be taken down by default. +# If an item gets split to multiple ones, the original one should be set deprecated instead (see Orbital Command for an example). +# This is a place if you want to nerf or disable by default a previously released trait. +# Currently, it disables only the topmost level of the progressives. +# Don't place here anything that's present in the vanilla campaigns (if it's overpowered, use overpowered items instead) +item_name_groups[ItemGroupNames.LEGACY_ITEMS] = legacy_items = [ + item_names.ASCENDANT_ARCHON_MERGE, +] + +item_name_groups[ItemGroupNames.KEYS] = keys = [ + item_name for item_name in key_item_table.keys() +] diff --git a/worlds/sc2/item/item_names.py b/worlds/sc2/item/item_names.py new file mode 100644 index 00000000..2fbd64bd --- /dev/null +++ b/worlds/sc2/item/item_names.py @@ -0,0 +1,957 @@ +""" +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" +DOMINION_TROOPER = "Dominion Trooper" +# Elites +PRIDE_OF_AUGUSTRGRAD = "Pride of Augustgrad" +SKY_FURY = "Sky Fury" +SHOCK_DIVISION = "Shock Division" +BLACKHAMMER = "Blackhammer" +AEGIS_GUARD = "Aegis Guard" +EMPERORS_SHADOW = "Emperor's Shadow" +SON_OF_KORHAL = "Son of Korhal" +BULWARK_COMPANY = "Bulwark Company" +FIELD_RESPONSE_THETA = "Field Response Theta" +EMPERORS_GUARDIAN = "Emperor's Guardian" +NIGHT_HAWK = "Night Hawk" +NIGHT_WOLF = "Night Wolf" + +# 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"# moved to Lab / Global upgrades +# PSI_DISRUPTER = "Psi Disrupter" # moved to Lab / Global upgrades +DEVASTATOR_TURRET = "Devastator Turret" + +# 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 (Terran)" +VANADIUM_PLATING = "Vanadium Plating (Terran)" +ORBITAL_DEPOTS = "Orbital Depots (Terran)" +MICRO_FILTERING = "Micro-Filtering (Terran)" +AUTOMATED_REFINERY = "Automated Refinery (Terran)" +COMMAND_CENTER_COMMAND_CENTER_REACTOR = "Command Center Reactor (Command Center)" +COMMAND_CENTER_SCANNER_SWEEP = "Scanner Sweep (Command Center)" +COMMAND_CENTER_MULE = "MULE (Command Center)" +COMMAND_CENTER_EXTRA_SUPPLIES = "Extra Supplies (Command Center)" +TECH_REACTOR = "Tech Reactor (Terran)" +ORBITAL_STRIKE = "Orbital Strike (Barracks)" +CELLULAR_REACTOR = "Cellular Reactor (Terran)" +PROGRESSIVE_REGENERATIVE_BIO_STEEL = "Progressive Regenerative Bio-Steel (Terran)" +PROGRESSIVE_FIRE_SUPPRESSION_SYSTEM = "Progressive Fire-Suppression System (Terran)" +STRUCTURE_ARMOR = "Structure Armor (Terran)" +HI_SEC_AUTO_TRACKING = "Hi-Sec Auto Tracking (Terran)" +ADVANCED_OPTICS = "Advanced Optics (Terran)" +ROGUE_FORCES = "Rogue Forces (Terran)" +MECHANICAL_KNOW_HOW = "Mechanical Know-how (Terran)" +MERCENARY_MUNITIONS = "Mercenary Munitions (Terran)" +PROGRESSIVE_FAST_DELIVERY = "Progressive Fast Delivery (Terran)" +RAPID_REINFORCEMENT = "Rapid Reinforcement (Terran)" +FUSION_CORE_FUSION_REACTOR = "Fusion Reactor (Fusion Core)" +PSI_DISRUPTER = "Psi Disrupter" +PSI_SCREEN = "Psi Screen (Psi Disrupter)" +SONIC_DISRUPTER = "Sonic Disrupter (Psi Disrupter)" +HIVE_MIND_EMULATOR = "Hive Mind Emulator" +PSI_INDOCTRINATOR = "Psi Indoctrinator (Hive Mind Emulator)" +ARGUS_AMPLIFIER = "Argus Amplifier (Hive Mind Emulator)" +SIGNAL_BEACON = "Signal Beacon (Terran)" + +# 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_MOIRAI_IMPULSE_DRIVE = "Moirai Impulse Drive (Battlecruiser)" +BATTLECRUISER_BEHEMOTH_REACTOR = "Behemoth Reactor (Battlecruiser)" +BATTLECRUISER_FIELD_ASSIST_TARGETING_SYSTEM = "Field-Assist Target System (Battlecruiser)" +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_MAGLEV_PROPULSION = "Maglev Propulsion (Diamondback)" +DOMINION_TROOPER_B2_HIGH_CAL_LMG = "B-2 High-Cal LMG (Dominion Trooper)" +DOMINION_TROOPER_CPO7_SALAMANDER_FLAMETHROWER = "CPO-7 Salamander Flamethrower (Dominion Trooper)" +DOMINION_TROOPER_HAILSTORM_LAUNCHER = "Hailstorm Launcher (Dominion Trooper)" +DOMINION_TROOPER_ADVANCED_ALLOYS = "Advanced Alloys (Dominion Trooper)" +DOMINION_TROOPER_OPTIMIZED_LOGISTICS = "Optimized Logistics (Dominion Trooper)" +EMPERORS_SHADOW_SOVEREIGN_TACTICAL_MISSILES = "Sovereign Tactical Missiles (Emperor's Shadow)" +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)" +GHOST_BARGAIN_BIN_PRICES = "Bargain Bin Prices (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 = "Hellbat (Hellion Morph)" +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)" +HERC_GRAPPLE_PULL = "Grapple Pull (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)" +LIBERATOR_GUERILLA_MISSILES = "Guerilla Missiles (Liberator)" +LIBERATOR_UED_MISSILE_TECHNOLOGY = "UED Missile Technology (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)" +MEDIVAC_RAPID_REIGNITION_SYSTEMS = "Rapid Reignition Systems (Medivac)" +MEDIVAC_RESOURCE_EFFICIENCY = "Resource Efficiency (Medivac)" +PREDATOR_RESOURCE_EFFICIENCY = "Resource Efficiency (Predator)" +PREDATOR_CLOAK = "Phase Cloak (Predator)" +PREDATOR_CHARGE = "Concussive Charge (Predator)" +PREDATOR_VESPENE_SYNTHESIS = "Vespene Synthesis (Predator)" +PREDATOR_ADAPTIVE_DEFENSES = "Adaptive Defenses (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)" +REAPER_RESOURCE_EFFICIENCY = "Resource Efficiency (Reaper)" +REAPER_BALLISTIC_FLIGHTSUIT = "Ballistic Flightsuit (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_MAGELLAN_COMPUTATION_SYSTEMS = "Magellan Computation Systems (Science Vessel)" +SCIENCE_VESSEL_TACTICAL_JUMP = "Tactical Jump (Science Vessel)" +SCV_ADVANCED_CONSTRUCTION = "Advanced Construction (SCV)" +SCV_DUAL_FUSION_WELDERS = "Dual-Fusion Welders (SCV)" +SCV_HOSTILE_ENVIRONMENT_ADAPTATION = "Hostile Environment Adaptation (SCV)" +SCV_CONSTRUCTION_JUMP_JETS = "Construction Jump Jets (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)" +SIEGE_TANK_PROGRESSIVE_TRANSPORT_HOOK = "Progressive Transport Hook (Siege Tank)" +SIEGE_TANK_ALLTERRAIN_TREADS = "All-Terrain Treads (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)" +SPECTRE_BARGAIN_BIN_PRICES = "Bargain Bin Prices (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)" +THOR_RAPID_RELOAD = "Rapid Reload (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)" +VIKING_AESIR_TURBINES = "Aesir Turbines (Viking)" +VULTURE_AUTO_LAUNCHERS = "Auto Launchers (Vulture)" +VULTURE_ION_THRUSTERS = "Ion Thrusters (Vulture)" +VULTURE_PROGRESSIVE_REPLENISHABLE_MAGAZINE = "Progressive Replenishable Magazine (Vulture)" +VULTURE_JERRYRIGGED_PATCHUP = "Jerry-Rigged Patchup (Vulture)" +WARHOUND_RESOURCE_EFFICIENCY = "Resource Efficiency (Warhound)" +WARHOUND_AXIOM_PLATING = "Axiom Plating (Warhound)" +WARHOUND_DEPLOY_TURRET = "Deploy Turret (Warhound)" +WIDOW_MINE_BLACK_MARKET_LAUNCHERS = "Black Market Launchers (Widow Mine)" +WIDOW_MINE_CONCEALMENT = "Concealment (Widow Mine)" +WIDOW_MINE_DEMOLITION_PAYLOAD = "Demolition Payload (Widow Mine)" +WIDOW_MINE_DRILLING_CLAWS = "Drilling Claws (Widow Mine)" +WIDOW_MINE_EXECUTIONER_MISSILES = "Executioner Missiles (Widow Mine)" +WIDOW_MINE_RESOURCE_EFFICIENCY = "Resource Efficiency (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)" + +# Terran Building upgrades +BUNKER_NEOSTEEL_BUNKER = "Neosteel Bunker (Bunker)" +BUNKER_PROJECTILE_ACCELERATOR = "Projectile Accelerator (Bunker)" +BUNKER_SHRIKE_TURRET = "Shrike Turret (Bunker)" +BUNKER_FORTIFIED_BUNKER = "Fortified Bunker (Bunker)" +DEVASTATOR_TURRET_ANTI_ARMOR_MUNITIONS = "Anti-Armor Munitions (Devastator Turret)" +DEVASTATOR_TURRET_CONCUSSIVE_GRENADES = "Concussive Grenades (Devastator Turret)" +DEVASTATOR_TURRET_RESOURCE_EFFICIENCY = "Resource Efficiency (Devastator Turret)" +MISSILE_TURRET_HELLSTORM_BATTERIES = "Hellstorm Batteries (Missile Turret)" +MISSILE_TURRET_TITANIUM_HOUSING = "Titanium Housing (Missile Turret)" +MISSILE_TURRET_RESOURCE_EFFICENCY = "Resource Efficiency (Missile Turret)" +PLANETARY_FORTRESS_PROGRESSIVE_AUGMENTED_THRUSTERS = "Progressive Augmented Thrusters (Planetary Fortress)" +PLANETARY_FORTRESS_IBIKS_TRACKING_SCANNERS = "Ibiks Tracking Scanners (Planetary Fortress)" +PLANETARY_FORTRESS_ORBITAL_MODULE = "Orbital Module (Planetary Fortress)" +SENSOR_TOWER_ASSISTIVE_TARGETING = "Assistive Targeting (Sensor Tower)" +SENSOR_TOWER_MUILTISPECTRUM_DOPPLER = "Multispectrum Doppler (Sensor Tower)" + +# 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" +PYGALISK = "Pygalisk" +CORRUPTOR = "Corruptor" +SCOURGE = "Scourge" +BROOD_QUEEN = "Brood Queen" +DEFILER = "Defiler" +INFESTED_MARINE = "Infested Marine" +INFESTED_SIEGE_TANK = "Infested Siege Tank" +INFESTED_DIAMONDBACK = "Infested Diamondback" +BULLFROG = "Bullfrog" +INFESTED_BANSHEE = "Infested Banshee" +INFESTED_LIBERATOR = "Infested Liberator" + +# Zerg Buildings +SPORE_CRAWLER = "Spore Crawler" +SPINE_CRAWLER = "Spine Crawler" +BILE_LAUNCHER = "Bile Launcher" +INFESTED_BUNKER = "Infested Bunker" +INFESTED_MISSILE_TURRET = "Infested Missile Turret" +NYDUS_WORM = "Nydus Worm" +ECHIDNA_WORM = "Echidna Worm" + +# 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)" +SPORE_CRAWLER_BIO_BONUS = "Caustic Enzymes (Spore Crawler)" +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)" +PYGALISK_STIM = "Stimpack (Pygalisk)" +PYGALISK_DUCAL_BLADES = "Ducal Blades (Pygalisk)" +PYGALISK_COMBAT_CARAPACE = "Combat Carapace (Pygalisk)" +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)" +GUARDIAN_PROPELLANT_SACS = "Propellant Sacs (Guardian)" +GUARDIAN_EXPLOSIVE_SPORES = "Explosive Spores (Guardian)" +GUARDIAN_PRIMORDIAL_FURY = "Primordial Fury (Guardian)" +IMPALER_ADAPTIVE_TALONS = "Adaptive Talons (Impaler)" +IMPALER_SECRETION_GLANDS = "Secretion Glands (Impaler)" +IMPALER_SUNKEN_SPINES = "Sunken 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_BEHEMOTH_STELLARSKIN = "Behemoth Stellarskin (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)" +DEFILER_PATHOGEN_PROJECTORS = "Pathogen Projectors (Defiler)" +DEFILER_TRAPDOOR_ADAPTATION = "Trapdoor Adaptation (Defiler)" +DEFILER_PREDATORY_CONSUMPTION = "Predatory Consumption (Defiler)" +DEFILER_COMORBIDITY = "Comorbidity (Defiler)" +ABERRATION_MONSTROUS_RESILIENCE = "Monstrous Resilience (Aberration)" +ABERRATION_CONSTRUCT_REGENERATION = "Construct Regeneration (Aberration)" +ABERRATION_BANELING_INCUBATION = "Baneling Incubation (Aberration)" +ABERRATION_PROTECTIVE_COVER = "Protective Cover (Aberration)" +ABERRATION_RESOURCE_EFFICIENCY = "Resource Efficiency (Aberration)" +ABERRATION_PROGRESSIVE_BANELING_LAUNCH = "Progressive Baneling Launch (Aberration)" +CORRUPTOR_MONSTROUS_RESILIENCE = "Monstrous Resilience (Corruptor)" +CORRUPTOR_CONSTRUCT_REGENERATION = "Construct Regeneration (Corruptor)" +CORRUPTOR_SCOURGE_INCUBATION = "Scourge Incubation (Corruptor)" +CORRUPTOR_RESOURCE_EFFICIENCY = "Resource Efficiency (Corruptor)" +PRIMAL_IGNITER_CONCENTRATED_FIRE = "Concentrated Fire (Primal Igniter)" +PRIMAL_IGNITER_PRIMAL_TENACITY = "Primal Tenacity (Primal Igniter)" +OVERLORD_IMPROVED_OVERLORDS = "Improved Overlords (Overlord)" +OVERLORD_VENTRAL_SACS = "Ventral Sacs (Overlord)" +OVERLORD_GENERATE_CREEP = "Generate Creep (Overlord)" +OVERLORD_PNEUMATIZED_CARAPACE = "Pneumatized Carapace (Overlord)" +OVERLORD_ANTENNAE = "Antennae (Overlord)" +INFESTED_SCV_BUILD_CHARGES = "Sustained Cultivation Ventricles (Infested SCV)" +INFESTED_MARINE_PLAGUED_MUNITIONS = "Plagued Munitions (Infested Marine)" +INFESTED_MARINE_RETINAL_AUGMENTATION = "Retinal Augmentation (Infested Marine)" +INFESTED_BUNKER_CALCIFIED_ARMOR = "Calcified Armor (Infested Bunker)" +INFESTED_BUNKER_REGENERATIVE_PLATING = "Regenerative Plating (Infested Bunker)" +INFESTED_BUNKER_ENGORGED_BUNKERS = "Engorged Bunkers (Infested Bunker)" +TYRANNOZOR_BARRAGE_OF_SPIKES = "Barrage of Spikes (Tyrannozor)" +TYRANNOZOR_TYRANTS_PROTECTION = "Tyrant's Protection (Tyrannozor)" +TYRANNOZOR_HEALING_ADAPTATION = "Healing Adaptation (Tyrannozor)" +TYRANNOZOR_IMPALING_STRIKE = "Impaling Strike (Tyrannozor)" +BILE_LAUNCHER_ARTILLERY_DUCTS = "Artillery Ducts (Bile Launcher)" +BILE_LAUNCHER_RAPID_BOMBARMENT = "Rapid Bombardment (Bile Launcher)" +NYDUS_WORM_ECHIDNA_WORM_SUBTERRANEAN_SCALES = "Subterranean Scales (Nydus Worm/Echidna Worm)" +NYDUS_WORM_ECHIDNA_WORM_JORMUNGANDR_STRAIN = "Jormungandr Strain (Nydus Worm/Echidna Worm)" +NYDUS_WORM_ECHIDNA_WORM_RESOURCE_EFFICIENCY = "Resource Efficiency (Nydus Worm/Echidna Worm)" +NYDUS_WORM_RAVENOUS_APPETITE = "Ravenous Appetite (Nydus Worm)" +ECHIDNA_WORM_OUROBOROS_STRAIN = "Ouroboros Strain (Echidna Worm)" +INFESTED_SIEGE_TANK_PROGRESSIVE_AUTOMATED_MITOSIS = "Progressive Automated Mitosis (Infested Siege Tank)" +INFESTED_SIEGE_TANK_ACIDIC_ENZYMES = "Acidic Enzymes (Infested Siege Tank)" +INFESTED_SIEGE_TANK_DEEP_TUNNEL = "Deep Tunnel (Infested Siege Tank)" +INFESTED_SIEGE_TANK_SEISMIC_SONAR = "Seismic Sonar (Infested Siege Tank)" +INFESTED_SIEGE_TANK_BALANCED_ROOTS = "Balanced Roots (Infested Siege Tank)" +INFESTED_DIAMONDBACK_CAUSTIC_MUCUS = "Caustic Mucus (Infested Diamondback)" +INFESTED_DIAMONDBACK_VIOLENT_ENZYMES = "Violent Enzymes (Infested Diamondback)" +INFESTED_DIAMONDBACK_CONCENTRATED_SPEW = "Concentrated Spew (Infested Diamondback)" +INFESTED_DIAMONDBACK_PROGRESSIVE_FUNGAL_SNARE = "Progressive Fungal Snare (Infested Diamondback)" +INFESTED_BANSHEE_BRACED_EXOSKELETON = "Braced Exoskeleton (Infested Banshee)" +INFESTED_BANSHEE_RAPID_HIBERNATION = "Rapid Hibernation (Infested Banshee)" +INFESTED_BANSHEE_FLESHFUSED_TARGETING_OPTICS = "Fleshfused Targeting Optics (Infested Banshee)" +INFESTED_LIBERATOR_CLOUD_DISPERSAL = "Cloud Dispersal (Infested Liberator)" +INFESTED_LIBERATOR_VIRAL_CONTAMINATION = "Viral Contamination (Infested Liberator)" +INFESTED_LIBERATOR_DEFENDER_MODE = "Defender Mode (Infested Liberator)" +INFESTED_SIEGE_TANK_FRIGHTFUL_FLESHWELDER = "Frightful Fleshwelder (Infested Siege Tank)" +INFESTED_DIAMONDBACK_FRIGHTFUL_FLESHWELDER = "Frightful Fleshwelder (Infested Diamondback)" +INFESTED_BANSHEE_FRIGHTFUL_FLESHWELDER = "Frightful Fleshwelder (Infested Banshee)" +INFESTED_LIBERATOR_FRIGHTFUL_FLESHWELDER = "Frightful Fleshwelder (Infested Liberator)" +INFESTED_MISSILE_TURRET_BIOELECTRIC_PAYLOAD = "Bioelectric Payload (Infested Missile Turret)" +INFESTED_MISSILE_TURRET_ACID_SPORE_VENTS = "Acid Spore Vents (Infested Missile Turret)" +BULLFROG_WILD_MUTATION = "Mutagen Vents (Bullfrog)" +BULLFROG_BROODLINGS = "Suffused With Vermin (Bullfrog)" +BULLFROG_HARD_IMPACT = "Lethal Impact (Bullfrog)" +BULLFROG_RANGE = "Catalytic Boosters (Bullfrog)" + +# 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" +HYDRALISK_IMPALER_ASPECT = "Impaler" +HYDRALISK_LURKER_ASPECT = "Lurker" +MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT = "Brood Lord" +MUTALISK_CORRUPTOR_VIPER_ASPECT = "Viper" +MUTALISK_CORRUPTOR_GUARDIAN_ASPECT = "Guardian" +MUTALISK_CORRUPTOR_DEVOURER_ASPECT = "Devourer" +ROACH_RAVAGER_ASPECT = "Ravager" +OVERLORD_OVERSEER_ASPECT = "Overseer" +ROACH_PRIMAL_IGNITER_ASPECT = "Primal Igniter" +ULTRALISK_TYRANNOZOR_ASPECT = "Tyrannozor" + +# Zerg Mercs +INFESTED_MEDICS = "Infested Medics" +INFESTED_SIEGE_BREAKERS = "Infested Siege Breakers" +INFESTED_DUSK_WINGS = "Infested Dusk Wings" +DEVOURING_ONES = "Devouring Ones" +HUNTER_KILLERS = "Hunter Killers" +TORRASQUE_MERC = "Wise Old Torrasque" +HUNTERLING = "Hunterling" +YGGDRASIL = "Yggdrasil" +CAUSTIC_HORRORS = "Caustic Horrors" + + +# Kerrigan Upgrades +KERRIGAN_KINETIC_BLAST = "Kinetic Blast (Kerrigan Ability)" +KERRIGAN_HEROIC_FORTITUDE = "Heroic Fortitude (Kerrigan Passive)" +KERRIGAN_LEAPING_STRIKE = "Leaping Strike (Kerrigan Ability)" +KERRIGAN_CRUSHING_GRIP = "Crushing Grip (Kerrigan Ability)" +KERRIGAN_CHAIN_REACTION = "Chain Reaction (Kerrigan Passive)" +KERRIGAN_PSIONIC_SHIFT = "Psionic Shift (Kerrigan Ability)" +KERRIGAN_WILD_MUTATION = "Wild Mutation (Kerrigan Ability)" +KERRIGAN_SPAWN_BANELINGS = "Spawn Banelings (Kerrigan Ability)" +KERRIGAN_MEND = "Mend (Kerrigan Ability)" +KERRIGAN_INFEST_BROODLINGS = "Infest Broodlings (Kerrigan Passive)" +KERRIGAN_FURY = "Fury (Kerrigan Passive)" +KERRIGAN_ABILITY_EFFICIENCY = "Ability Efficiency (Kerrigan Passive)" +KERRIGAN_APOCALYPSE = "Apocalypse (Kerrigan Ability)" +KERRIGAN_SPAWN_LEVIATHAN = "Spawn Leviathan (Kerrigan Ability)" +KERRIGAN_DROP_PODS = "Drop-Pods (Kerrigan Ability)" +KERRIGAN_ASSIMILATION_AURA = "Assimilation Aura (Kerrigan Ability)" +KERRIGAN_IMMOBILIZATION_WAVE = "Immobilization Wave (Kerrigan Ability)" +KERRIGAN_PRIMAL_FORM = "Primal Form (Kerrigan)" + +# Misc Upgrades +ZERGLING_RECONSTITUTION = "Zergling Reconstitution (Zerg)" +AUTOMATED_EXTRACTORS = "Automated Extractors (Zerg)" +TWIN_DRONES = "Twin Drones (Zerg)" +MALIGNANT_CREEP = "Malignant Creep (Zerg)" +VESPENE_EFFICIENCY = "Vespene Efficiency (Zerg)" +ZERG_CREEP_STOMACH = "Creep Stomach (Zerg)" +ZERG_EXCAVATING_CLAWS = "Excavating Claws (Zerg)" +HIVE_CLUSTER_MATURATION = "Hive Cluster Maturation (Zerg)" +MACROSCOPIC_RECUPERATION = "Macroscopic Recuperation (Zerg)" +BIOMECHANICAL_STOCKPILING = "Bio-Mechanical Stockpiling (Zerg)" +BROODLING_SPORE_SATURATION = "Broodling Spore Saturation (Zerg)" +UNRESTRICTED_MUTATION = "Unrestricted Mutation (Zerg)" +CELL_DIVISION = "Cell Division (Zerg)" +EVOLUTIONARY_LEAP = "Evolutionary Leap (Zerg)" +SELF_SUFFICIENT = "Self-Sufficient (Zerg)" + +# 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" +SKYLORD = "Skylord" +TRIREME = "Trireme" +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" +STALWART = "Stalwart" +WRATHWALKER = "Wrathwalker" +REAVER = "Reaver" +DISRUPTOR = "Disruptor" +MIRAGE = "Mirage" +SKIRMISHER = "Skirmisher" +CORSAIR = "Corsair" +DESTROYER = "Destroyer" +PULSAR = "Pulsar" +DAWNBRINGER = "Dawnbringer" +SCOUT = "Scout" +OPPRESSOR = "Oppressor" +CALADRIUS = "Caladrius" +MISTWING = "Mist Wing" +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_ENDLESS_SERVITUDE = "Endless Servitude (Supplicant)" +SUPPLICANT_ZENITH_PITCH = "Zenith Pitch (Supplicant)" +SUPPLICANT_SACRIFICE = "Sacrifice (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)" +INSTIGATOR_BLINK_OVERDRIVE = "Blink Overdrive (Instigator)" +INSTIGATOR_RECONSTRUCTION = "Reconstruction (Instigator)" +DRAGOON_CONCENTRATED_ANTIMATTER = "Concentrated Antimatter (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/Oppressor/Caladrius/Mist Wing)" +SCOUT_APIAL_SENSORS = "Apial Sensors (Scout)" +SCOUT_GRAVITIC_THRUSTERS = "Gravitic Thrusters (Scout/Oppressor/Caladrius/Mist Wing)" +SCOUT_ADVANCED_PHOTON_BLASTERS = "Advanced Photon Blasters (Scout/Oppressor/Mist Wing)" +SCOUT_RESOURCE_EFFICIENCY = "Resource Efficiency (Scout)" +SCOUT_SUPPLY_EFFICIENCY = "Supply Efficiency (Scout)" +TEMPEST_TECTONIC_DESTABILIZERS = "Tectonic Destabilizers (Tempest)" +TEMPEST_QUANTIC_REACTOR = "Quantic Reactor (Tempest)" +TEMPEST_GRAVITY_SLING = "Gravity Sling (Tempest)" +TEMPEST_INTERPLANETARY_RANGE = "Interplanetary Range (Tempest)" +PHOENIX_CLASS_IONIC_WAVELENGTH_FLUX = "Ionic Wavelength Flux (Phoenix/Mirage/Skirmisher)" +PHOENIX_CLASS_ANION_PULSE_CRYSTALS = "Anion Pulse-Crystals (Phoenix/Mirage/Skirmisher)" +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_SKYWARD_CHRONOANOMALY = "Skyward Chronoanomaly (Oracle)" +ORACLE_TEMPORAL_ACCELERATION_BEAM = "Temporal Acceleration Beam (Oracle)" +ORACLE_BOSONIC_CORE = "Bosonic Core (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_JUDICATORS_VEIL = "Judicator's Veil (Arbiter)" +CARRIER_TRIREME_GRAVITON_CATAPULT = "Graviton Catapult (Carrier/Trireme)" +CARRIER_SKYLORD_TRIREME_HULL_OF_PAST_GLORIES = "Hull of Past Glories (Carrier/Skylord/Trireme)" +VOID_RAY_DESTROYER_PULSAR_DAWNBRINGER_FLUX_VANES = "Flux Vanes (Void Ray/Destroyer/Pulsar/Dawnbringer)" +DAWNBRINGER_ANTI_SURFACE_COUNTERMEASURES = "Anti-Surface Countermeasures (Dawnbringer)" +DAWNBRINGER_ENHANCED_SHIELD_GENERATOR = "Enhanced Shield Generator (Dawnbringer)" +PULSAR_CHRONOCLYSM = "Chronoclysm (Pulsar)" +PULSAR_ENTROPIC_REVERSAL = "Entropic Reversal (Pulsar)" +DESTROYER_RESOURCE_EFFICIENCY = "Resource Efficiency (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)" +REAVER_BARGAIN_BIN_PRICES = "Bargain Bin Prices (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 = "Advanced Targeting (Immortal/Annihilator)" +IMMORTAL_ANNIHILATOR_DISRUPTOR_DISPERSION = "Disruptor Dispersion (Immortal/Annihilator)" +STALWART_HIGH_VOLTAGE_CAPACITORS = "High Voltage Capacitors (Stalwart)" +STALWART_REINTEGRATED_FRAMEWORK = "Reintegrated Framework (Stalwart)" +STALWART_STABILIZED_ELECTRODES = "Stabilized Electrodes (Stalwart)" +STALWART_LATTICED_SHIELDING = "Latticed Shielding (Stalwart)" +DISRUPTOR_CLOAKING_MODULE = "Cloaking Module (Disruptor)" +DISRUPTOR_PERFECTED_POWER = "Perfected Power (Disruptor)" +DISRUPTOR_RESTRAINED_DESTRUCTION = "Restrained Destruction (Disruptor)" +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)" +DARK_TEMPLAR_ARCHON_MERGE = "Archon Merge (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)" +ARCHON_TRANSCENDENCE = "Transcendence (Archon)" +ARCHON_POWER_SIPHON = "Power Siphon (Archon)" +ARCHON_ERADICATE = "Eradicate (Archon)" +ARCHON_OBLITERATE = "Obliterate (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)" +ASCENDANT_ARCHON_MERGE = "Archon Merge (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)" +OPPRESSOR_ACCELERATED_WARP = "Accelerated Warp (Oppressor)" +OPPRESSOR_ARMOR_MELTING_BLASTERS = "Armor Melting Blasters (Oppressor)" +CALADRIUS_SIDE_MISSILES = "Side Missiles (Caladrius)" +CALADRIUS_STRUCTURE_TARGETING = "Structure Targeting (Caladrius)" +CALADRIUS_SOLARITE_REACTOR = "Solarite Reactor (Caladrius)" +MISTWING_NULL_SHROUD = "Null Shroud (Mist Wing)" +MISTWING_PILOT = "Pilot (Mist Wing)" + +# War Council +ZEALOT_WHIRLWIND = "Whirlwind (Zealot)" +CENTURION_RESOURCE_EFFICIENCY = "Resource Efficiency (Centurion)" +SENTINEL_RESOURCE_EFFICIENCY = "Resource Efficiency (Sentinel)" +STALKER_PHASE_REACTOR = "Phase Reactor (Stalker)" +DRAGOON_PHALANX_SUIT = "Phalanx Suit (Dragoon)" +INSTIGATOR_MODERNIZED_SERVOS = "Modernized Servos (Instigator)" +ADEPT_DISRUPTIVE_TRANSFER = "Disruptive Transfer (Adept)" +SLAYER_PHASE_BLINK = "Phase Blink (Slayer)" +AVENGER_KRYHAS_CLOAK = "Kryhas Cloak (Avenger)" +DARK_TEMPLAR_LESSER_SHADOW_FURY = "Lesser Shadow Fury (Dark Templar)" +DARK_TEMPLAR_GREATER_SHADOW_FURY = "Greater Shadow Fury (Dark Templar)" +BLOOD_HUNTER_BRUTAL_EFFICIENCY = "Brutal Efficiency (Blood Hunter)" +SENTRY_DOUBLE_SHIELD_RECHARGE = "Double Shield Recharge (Sentry)" +ENERGIZER_MOBILE_CHRONO_BEAM = "Mobile Chrono Beam (Energizer)" +HAVOC_ENDURING_SIGHT = "Enduring Sight (Havoc)" +HIGH_TEMPLAR_PLASMA_SURGE = "Plasma Surge (High Templar)" +SIGNIFIER_FEEDBACK = "Feedback (Signifier)" +ASCENDANT_BREATH_OF_CREATION = "Breath of Creation (Ascendant)" +DARK_ARCHON_INDOMITABLE_WILL = "Indomitable Will (Dark Archon)" +IMMORTAL_IMPROVED_BARRIER = "Improved Barrier (Immortal)" +VANGUARD_RAPIDFIRE_CANNON = "Rapid-Fire Cannon (Vanguard)" +VANGUARD_FUSION_MORTARS = "Fusion Mortars (Vanguard)" +ANNIHILATOR_TWILIGHT_CHASSIS = "Twilight Chassis (Annihilator)" +STALWART_ARC_INDUCERS = "Arc Inducers (Stalwart)" +COLOSSUS_FIRE_LANCE = "Fire Lance (Colossus)" +WRATHWALKER_AERIAL_TRACKING = "Aerial Tracking (Wrathwalker)" +REAVER_KHALAI_REPLICATORS = "Khalai Replicators (Reaver)" +DISRUPTOR_MOBILITY_PROTOCOLS = "Mobility Protocols (Disruptor)" +WARP_PRISM_WARP_REFRACTION = "Warp Refraction (Warp Prism)" +OBSERVER_INDUCE_SCOPOPHOBIA = "Induce Scopophobia (Observer)" +PHOENIX_DOUBLE_GRAVITON_BEAM = "Double Graviton Beam (Phoenix)" +CORSAIR_NETWORK_DISRUPTION = "Network Disruption (Corsair)" +MIRAGE_GRAVITON_BEAM = "Graviton Beam (Mirage)" +SKIRMISHER_PEER_CONTEMPT = "Peer Contempt (Skirmisher)" +VOID_RAY_PRISMATIC_RANGE = "Prismatic Range (Void Ray)" +DESTROYER_REFORGED_BLOODSHARD_CORE = "Reforged Bloodshard Core (Destroyer)" +PULSAR_CHRONO_SHEAR = "Chrono Shear (Pulsar)" +DAWNBRINGER_SOLARITE_LENS = "Solarite Lens (Dawnbringer)" +CARRIER_REPAIR_DRONES = "Repair Drones (Carrier)" +SKYLORD_JUMP = "Jump (Skylord)" +TRIREME_SOLAR_BEAM = "Solar Beam (Trireme)" +TEMPEST_DISINTEGRATION = "Disintegration (Tempest)" +SCOUT_EXPEDITIONARY_HULL = "Expeditionary Hull (Scout)" +ARBITER_VESSEL_OF_THE_CONCLAVE = "Vessel of the Conclave (Arbiter)" +ORACLE_STASIS_CALIBRATION = "Stasis Calibration (Oracle)" +MOTHERSHIP_INTEGRATED_POWER = "Integrated Power (Mothership)" +OPPRESSOR_VULCAN_BLASTER = "Vulcan Blaster (Oppressor)" +CALADRIUS_CORONA_BEAM = "Corona Beam (Caladrius)" +MISTWING_PHANTOM_DASH = "Phantom Dash (Mist Wing)" + +# Spear Of Adun +SOA_CHRONO_SURGE = "Chrono Surge (Spear of Adun)" +SOA_PROGRESSIVE_PROXY_PYLON = "Progressive Proxy Pylon (Spear of Adun)" +SOA_PYLON_OVERCHARGE = "Pylon Overcharge (Spear of Adun)" +SOA_ORBITAL_STRIKE = "Orbital Strike (Spear of Adun)" +SOA_TEMPORAL_FIELD = "Temporal Field (Spear of Adun)" +SOA_SOLAR_LANCE = "Solar Lance (Spear of Adun)" +SOA_MASS_RECALL = "Mass Recall (Spear of Adun)" +SOA_SHIELD_OVERCHARGE = "Shield Overcharge (Spear of Adun)" +SOA_DEPLOY_FENIX = "Deploy Fenix (Spear of Adun)" +SOA_PURIFIER_BEAM = "Purifier Beam (Spear of Adun)" +SOA_TIME_STOP = "Time Stop (Spear of Adun)" +SOA_SOLAR_BOMBARDMENT = "Solar Bombardment (Spear of Adun)" + +# Generic upgrades +MATRIX_OVERLOAD = "Matrix Overload (Protoss)" +QUATRO = "Quatro (Protoss)" +NEXUS_OVERCHARGE = "Nexus Overcharge (Protoss)" +ORBITAL_ASSIMILATORS = "Orbital Assimilators (Protoss)" +WARP_HARMONIZATION = "Warp Harmonization (Protoss)" +GUARDIAN_SHELL = "Guardian Shell (Spear of Adun)" +RECONSTRUCTION_BEAM = "Reconstruction Beam (Spear of Adun)" +OVERWATCH = "Overwatch (Spear of Adun)" +SUPERIOR_WARP_GATES = "Superior Warp Gates (Protoss)" +ENHANCED_TARGETING = "Enhanced Targeting (Protoss)" +OPTIMIZED_ORDNANCE = "Optimized Ordnance (Protoss)" +KHALAI_INGENUITY = "Khalai Ingenuity (Protoss)" +AMPLIFIED_ASSIMILATORS = "Amplified Assimilators (Protoss)" +PROGRESSIVE_WARP_RELOCATE = "Progressive Warp Relocate (Protoss)" +PROBE_WARPIN = "Probe Warp-In (Protoss)" +ELDER_PROBES = "Elder Probes (Protoss)" + +# Filler items +STARTING_MINERALS = "Additional Starting Minerals" +STARTING_VESPENE = "Additional Starting Vespene" +STARTING_SUPPLY = "Additional Starting Supply" +MAX_SUPPLY = "Additional Maximum Supply" +SHIELD_REGENERATION = "Increased Shield Regeneration" +BUILDING_CONSTRUCTION_SPEED = "Increased Building Construction Speed" +UPGRADE_RESEARCH_SPEED = "Increased Upgrade Research Speed" +UPGRADE_RESEARCH_COST = "Reduced Upgrade Research Cost" + +# Trap +REDUCED_MAX_SUPPLY = "Decreased Maximum Supply" +NOTHING = "Nothing" + +# Deprecated +PROGRESSIVE_ORBITAL_COMMAND = "Progressive Orbital Command (Deprecated)" + +# Keys +_TEMPLATE_MISSION_KEY = "{} Mission Key" +_TEMPLATE_NAMED_LAYOUT_KEY = "{} ({}) Questline Key" +_TEMPLATE_NUMBERED_LAYOUT_KEY = "Questline Key #{}" +_TEMPLATE_NAMED_CAMPAIGN_KEY = "{} Campaign Key" +_TEMPLATE_NUMBERED_CAMPAIGN_KEY = "Campaign Key #{}" +_TEMPLATE_FLAVOR_KEY = "{} Key" +PROGRESSIVE_MISSION_KEY = "Progressive Mission Key" +PROGRESSIVE_QUESTLINE_KEY = "Progressive Questline Key" +_TEMPLATE_PROGRESSIVE_KEY = "Progressive Key #{}" + +# Names for flavor keys, feel free to add more, but add them to the Custom Mission Order docs too +# These will never be randomly created by the generator +_flavor_key_names = [ + "Terran", "Zerg", "Protoss", + "Raynor", "Tychus", "Swann", "Stetmann", "Hanson", "Nova", "Tosh", "Valerian", "Warfield", "Mengsk", "Han", "Horner", + "Kerrigan", "Zagara", "Abathur", "Yagdra", "Kraith", "Slivan", "Zurvan", "Brakk", "Stukov", "Dehaka", "Niadra", "Izsha", + "Artanis", "Zeratul", "Tassadar", "Karax", "Vorazun", "Alarak", "Fenix", "Urun", "Mohandar", "Selendis", "Rohana", + "Reigel", "Davis", "Ji'nara" +] diff --git a/worlds/sc2/item/item_parents.py b/worlds/sc2/item/item_parents.py new file mode 100644 index 00000000..18b27b79 --- /dev/null +++ b/worlds/sc2/item/item_parents.py @@ -0,0 +1,266 @@ +""" +Utilities for telling item parentage hierarchy. +ItemData in item_tables.py will point from child item -> parent rule. +Rules have a `parent_items()` method which links rule -> parent items. +Rules may be more complex than all or any items being present. Call them to determine if they are satisfied. +""" + +from typing import Dict, List, Iterable, Sequence, Optional, TYPE_CHECKING +import abc +from . import item_names, parent_names, item_tables, item_groups + +if TYPE_CHECKING: + from ..options import Starcraft2Options + + +class PresenceRule(abc.ABC): + """Contract for a parent presence rule. This should be a protocol in Python 3.10+""" + constraint_group: Optional[str] + """Identifies the group this item rule is a part of, subject to min/max upgrades per unit""" + display_string: str + """Main item to count as the parent for min/max upgrades per unit purposes""" + @abc.abstractmethod + def __call__(self, inventory: Iterable[str], options: 'Starcraft2Options') -> bool: ... + @abc.abstractmethod + def parent_items(self) -> Sequence[str]: ... + + +class ItemPresent(PresenceRule): + def __init__(self, item_name: str) -> None: + self.item_name = item_name + self.constraint_group = item_name + self.display_string = item_name + + def __call__(self, inventory: Iterable[str], options: 'Starcraft2Options') -> bool: + return self.item_name in inventory + + def parent_items(self) -> List[str]: + return [self.item_name] + + +class AnyOf(PresenceRule): + def __init__(self, group: Iterable[str], main_item: Optional[str] = None, display_string: Optional[str] = None) -> None: + self.group = set(group) + self.constraint_group = main_item + self.display_string = display_string or main_item or ' | '.join(group) + + def __call__(self, inventory: Iterable[str], options: 'Starcraft2Options') -> bool: + return len(self.group.intersection(inventory)) > 0 + + def parent_items(self) -> List[str]: + return sorted(self.group) + + +class AllOf(PresenceRule): + def __init__(self, group: Iterable[str], main_item: Optional[str] = None) -> None: + self.group = set(group) + self.constraint_group = main_item + self.display_string = main_item or ' & '.join(group) + + def __call__(self, inventory: Iterable[str], options: 'Starcraft2Options') -> bool: + return len(self.group.intersection(inventory)) == len(self.group) + + def parent_items(self) -> List[str]: + return sorted(self.group) + + +class AnyOfGroupAndOneOtherItem(PresenceRule): + def __init__(self, group: Iterable[str], item_name: str) -> None: + self.group = set(group) + self.item_name = item_name + self.constraint_group = item_name + self.display_string = item_name + + def __call__(self, inventory: Iterable[str], options: 'Starcraft2Options') -> bool: + return (len(self.group.intersection(inventory)) > 0) and self.item_name in inventory + + def parent_items(self) -> List[str]: + return sorted(self.group) + [self.item_name] + + +class MorphlingOrItem(PresenceRule): + def __init__(self, item_name: str, has_parent: bool = True) -> None: + self.item_name = item_name + self.constraint_group = None # Keep morphs from counting towards the parent unit's upgrade count + self.display_string = f'{item_name} Morphs' + + def __call__(self, inventory: Iterable[str], options: 'Starcraft2Options') -> bool: + return (options.enable_morphling.value != 0) or self.item_name in inventory + + def parent_items(self) -> List[str]: + return [self.item_name] + + +class MorphlingOrAnyOf(PresenceRule): + def __init__(self, group: Iterable[str], display_string: str, main_item: Optional[str] = None) -> None: + self.group = set(group) + self.constraint_group = main_item + self.display_string = display_string + + def __call__(self, inventory: Iterable[str], options: 'Starcraft2Options') -> bool: + return (options.enable_morphling.value != 0) or (len(self.group.intersection(inventory)) > 0) + + def parent_items(self) -> List[str]: + return sorted(self.group) + + +parent_present: Dict[str, PresenceRule] = { + item_name: ItemPresent(item_name) + for item_name in item_tables.item_table +} + +# Terran +parent_present[parent_names.DOMINION_TROOPER_WEAPONS] = AnyOf([ + item_names.DOMINION_TROOPER_B2_HIGH_CAL_LMG, + item_names.DOMINION_TROOPER_CPO7_SALAMANDER_FLAMETHROWER, + item_names.DOMINION_TROOPER_HAILSTORM_LAUNCHER, +], main_item=item_names.DOMINION_TROOPER) +parent_present[parent_names.INFANTRY_UNITS] = AnyOf(item_groups.barracks_units, display_string='Terran Infantry') +parent_present[parent_names.INFANTRY_WEAPON_UNITS] = AnyOf(item_groups.barracks_wa_group, display_string='Terran Infantry') +parent_present[parent_names.ORBITAL_COMMAND_AND_PLANETARY] = AnyOfGroupAndOneOtherItem( + item_groups.orbital_command_abilities, + item_names.PLANETARY_FORTRESS, +) +parent_present[parent_names.SIEGE_TANK_AND_TRANSPORT] = AnyOfGroupAndOneOtherItem( + (item_names.MEDIVAC, item_names.HERCULES), + item_names.SIEGE_TANK, +) +parent_present[parent_names.SIEGE_TANK_AND_MEDIVAC] = AllOf((item_names.SIEGE_TANK, item_names.MEDIVAC), item_names.SIEGE_TANK) +parent_present[parent_names.SPIDER_MINE_SOURCE] = AnyOf(item_groups.spider_mine_sources, display_string='Spider Mines') +parent_present[parent_names.STARSHIP_UNITS] = AnyOf(item_groups.starport_units, display_string='Terran Starships') +parent_present[parent_names.STARSHIP_WEAPON_UNITS] = AnyOf(item_groups.starport_wa_group, display_string='Terran Starships') +parent_present[parent_names.VEHICLE_UNITS] = AnyOf(item_groups.factory_units, display_string='Terran Vehicles') +parent_present[parent_names.VEHICLE_WEAPON_UNITS] = AnyOf(item_groups.factory_wa_group, display_string='Terran Vehicles') +parent_present[parent_names.TERRAN_MERCENARIES] = AnyOf(item_groups.terran_mercenaries, display_string='Terran Mercenaries') + +# Zerg +parent_present[parent_names.ANY_NYDUS_WORM] = AnyOf((item_names.NYDUS_WORM, item_names.ECHIDNA_WORM), item_names.NYDUS_WORM) +parent_present[parent_names.BANELING_SOURCE] = AnyOf( + (item_names.ZERGLING_BANELING_ASPECT, item_names.KERRIGAN_SPAWN_BANELINGS), + item_names.ZERGLING_BANELING_ASPECT, +) +parent_present[parent_names.INFESTED_UNITS] = AnyOf(item_groups.infterr_units, display_string='Infested') +parent_present[parent_names.INFESTED_FACTORY_OR_STARPORT] = AnyOf( + (item_names.INFESTED_DIAMONDBACK, item_names.INFESTED_SIEGE_TANK, item_names.INFESTED_LIBERATOR, item_names.INFESTED_BANSHEE, item_names.BULLFROG) +) +parent_present[parent_names.MORPH_SOURCE_AIR] = MorphlingOrAnyOf((item_names.MUTALISK, item_names.CORRUPTOR), "Mutalisk/Corruptor Morphs") +parent_present[parent_names.MORPH_SOURCE_ROACH] = MorphlingOrItem(item_names.ROACH) +parent_present[parent_names.MORPH_SOURCE_ZERGLING] = MorphlingOrItem(item_names.ZERGLING) +parent_present[parent_names.MORPH_SOURCE_HYDRALISK] = MorphlingOrItem(item_names.HYDRALISK) +parent_present[parent_names.MORPH_SOURCE_ULTRALISK] = MorphlingOrItem(item_names.ULTRALISK) +parent_present[parent_names.ZERG_UPROOTABLE_BUILDINGS] = AnyOf( + (item_names.SPINE_CRAWLER, item_names.SPORE_CRAWLER, item_names.INFESTED_MISSILE_TURRET, item_names.INFESTED_BUNKER), +) +parent_present[parent_names.ZERG_MELEE_ATTACKER] = AnyOf(item_groups.zerg_melee_wa, display_string='Zerg Ground') +parent_present[parent_names.ZERG_MISSILE_ATTACKER] = AnyOf(item_groups.zerg_ranged_wa, display_string='Zerg Ground') +parent_present[parent_names.ZERG_CARAPACE_UNIT] = AnyOf(item_groups.zerg_ground_units, display_string='Zerg Flyers') +parent_present[parent_names.ZERG_FLYING_UNIT] = AnyOf(item_groups.zerg_air_units, display_string='Zerg Flyers') +parent_present[parent_names.ZERG_MERCENARIES] = AnyOf(item_groups.zerg_mercenaries, display_string='Zerg Mercenaries') +parent_present[parent_names.ZERG_OUROBOUROS_CONDITION] = AnyOfGroupAndOneOtherItem( + (item_names.ZERGLING, item_names.ROACH, item_names.HYDRALISK, item_names.ABERRATION), + item_names.ECHIDNA_WORM +) + +# Protoss +parent_present[parent_names.ARCHON_SOURCE] = AnyOf( + (item_names.HIGH_TEMPLAR, item_names.SIGNIFIER, item_names.ASCENDANT_ARCHON_MERGE, item_names.DARK_TEMPLAR_ARCHON_MERGE), + main_item="Archon", +) +parent_present[parent_names.CARRIER_CLASS] = AnyOf( + (item_names.CARRIER, item_names.TRIREME, item_names.SKYLORD), + main_item=item_names.CARRIER, +) +parent_present[parent_names.CARRIER_OR_TRIREME] = AnyOf( + (item_names.CARRIER, item_names.TRIREME), + main_item=item_names.CARRIER, +) +parent_present[parent_names.DARK_ARCHON_SOURCE] = AnyOf( + (item_names.DARK_ARCHON, item_names.DARK_TEMPLAR_DARK_ARCHON_MELD), + main_item=item_names.DARK_ARCHON, +) +parent_present[parent_names.DARK_TEMPLAR_CLASS] = AnyOf( + (item_names.DARK_TEMPLAR, item_names.AVENGER, item_names.BLOOD_HUNTER), + main_item=item_names.DARK_TEMPLAR, +) +parent_present[parent_names.STORM_CASTER] = AnyOf( + (item_names.HIGH_TEMPLAR, item_names.SIGNIFIER), + main_item=item_names.HIGH_TEMPLAR, +) +parent_present[parent_names.IMMORTAL_OR_ANNIHILATOR] = AnyOf( + (item_names.IMMORTAL, item_names.ANNIHILATOR), + main_item=item_names.IMMORTAL, +) +parent_present[parent_names.PHOENIX_CLASS] = AnyOf( + (item_names.PHOENIX, item_names.MIRAGE, item_names.SKIRMISHER), + main_item=item_names.PHOENIX, +) +parent_present[parent_names.SENTRY_CLASS] = AnyOf( + (item_names.SENTRY, item_names.ENERGIZER, item_names.HAVOC), + main_item=item_names.SENTRY, +) +parent_present[parent_names.SENTRY_CLASS_OR_SHIELD_BATTERY] = AnyOf( + (item_names.SENTRY, item_names.ENERGIZER, item_names.HAVOC, item_names.SHIELD_BATTERY), + main_item=item_names.SENTRY, +) +parent_present[parent_names.STALKER_CLASS] = AnyOf( + (item_names.STALKER, item_names.SLAYER, item_names.INSTIGATOR), + main_item=item_names.STALKER, +) +parent_present[parent_names.SUPPLICANT_AND_ASCENDANT] = AllOf( + (item_names.SUPPLICANT, item_names.ASCENDANT), + main_item=item_names.ASCENDANT, +) +parent_present[parent_names.VOID_RAY_CLASS] = AnyOf( + (item_names.VOID_RAY, item_names.DESTROYER, item_names.PULSAR, item_names.DAWNBRINGER), + main_item=item_names.VOID_RAY, +) +parent_present[parent_names.ZEALOT_OR_SENTINEL_OR_CENTURION] = AnyOf( + (item_names.ZEALOT, item_names.SENTINEL, item_names.CENTURION), + main_item=item_names.ZEALOT, +) +parent_present[parent_names.SCOUT_CLASS] = AnyOf( + (item_names.SCOUT, item_names.OPPRESSOR, item_names.CALADRIUS, item_names.MISTWING), + main_item=item_names.SCOUT, +) +parent_present[parent_names.SCOUT_OR_OPPRESSOR_OR_MISTWING] = AnyOf( + (item_names.SCOUT, item_names.OPPRESSOR, item_names.MISTWING), + main_item=item_names.SCOUT, +) +parent_present[parent_names.PROTOSS_STATIC_DEFENSE] = AnyOf( + (item_names.NEXUS_OVERCHARGE, item_names.PHOTON_CANNON, item_names.KHAYDARIN_MONOLITH, item_names.SHIELD_BATTERY), + main_item=item_names.PHOTON_CANNON, +) +parent_present[parent_names.PROTOSS_ATTACKING_BUILDING] = AnyOf( + (item_names.NEXUS_OVERCHARGE, item_names.PHOTON_CANNON, item_names.KHAYDARIN_MONOLITH), + main_item=item_names.PHOTON_CANNON, +) + + +parent_id_to_children: Dict[str, Sequence[str]] = {} +"""Parent identifier to child items. Only contains parent rules with children.""" +child_item_to_parent_items: Dict[str, Sequence[str]] = {} +"""Child item name to all parent items that can possibly affect its presence rule. Populated for all item names.""" + +parent_item_to_ids: Dict[str, Sequence[str]] = {} +"""Parent item to parent identifiers it affects. Populated for all items and parent IDs.""" +parent_item_to_children: Dict[str, Sequence[str]] = {} +"""Parent item to child item names. Populated for all items and parent IDs.""" +item_upgrade_groups: Dict[str, Sequence[str]] = {} +"""Mapping of upgradable item group -> child items. Only populated for groups with child items.""" +# Note(mm): "All items" promise satisfied by the basic ItemPresent auto-generated rules + +def _init() -> None: + for item_name, item_data in item_tables.item_table.items(): + if item_data.parent is None: + continue + parent_id_to_children.setdefault(item_data.parent, []).append(item_name) # type: ignore + child_item_to_parent_items[item_name] = parent_present[item_data.parent].parent_items() + + for parent_id, presence_func in parent_present.items(): + for parent_item in presence_func.parent_items(): + parent_item_to_ids.setdefault(parent_item, []).append(parent_id) # type: ignore + parent_item_to_children.setdefault(parent_item, []).extend(parent_id_to_children.get(parent_id, [])) # type: ignore + if presence_func.constraint_group is not None and parent_id_to_children.get(parent_id): + item_upgrade_groups.setdefault(presence_func.constraint_group, []).extend(parent_id_to_children[parent_id]) # type: ignore + +_init() diff --git a/worlds/sc2/item/item_tables.py b/worlds/sc2/item/item_tables.py new file mode 100644 index 00000000..7fb198ea --- /dev/null +++ b/worlds/sc2/item/item_tables.py @@ -0,0 +1,2415 @@ +from typing import * + +from BaseClasses import ItemClassification +import typing + +from ..mission_tables import SC2Mission, SC2Race, SC2Campaign +from ..item import parent_names, ItemData, TerranItemType, FactionlessItemType, ProtossItemType, ZergItemType +from ..mission_order.presets_static import get_used_layout_names +from . import item_names + + + +def get_full_item_list(): + return item_table + + +SC2WOL_ITEM_ID_OFFSET = 1000 +SC2HOTS_ITEM_ID_OFFSET = SC2WOL_ITEM_ID_OFFSET + 1000 +SC2LOTV_ITEM_ID_OFFSET = SC2HOTS_ITEM_ID_OFFSET + 1000 +SC2_KEY_ITEM_ID_OFFSET = SC2LOTV_ITEM_ID_OFFSET + 1000 +# Reserve this many IDs for missions, layouts, campaigns, and generic keys each +SC2_KEY_ITEM_SECTION_SIZE = 1000 + +WEAPON_ARMOR_UPGRADE_MAX_LEVEL = 5 + + +# The items are sorted by their IDs. The IDs shall be kept for compatibility with older games. +item_table = { + # WoL + item_names.MARINE: + ItemData(0 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit, 0, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.MEDIC: + ItemData(1 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit, 1, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.FIREBAT: + ItemData(2 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit, 2, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.MARAUDER: + ItemData(3 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit, 3, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.REAPER: + ItemData(4 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit, 4, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.HELLION: + ItemData(5 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit, 5, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.VULTURE: + ItemData(6 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit, 6, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.GOLIATH: + ItemData(7 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit, 7, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.DIAMONDBACK: + ItemData(8 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit, 8, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.SIEGE_TANK: + ItemData(9 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit, 9, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.MEDIVAC: + ItemData(10 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit, 10, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.WRAITH: + ItemData(11 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit, 11, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.VIKING: + ItemData(12 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit, 12, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.BANSHEE: + ItemData(13 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit, 13, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.BATTLECRUISER: + ItemData(14 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit, 14, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.GHOST: + ItemData(15 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit, 15, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.SPECTRE: + ItemData(16 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit, 16, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.THOR: + ItemData(17 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit, 17, SC2Race.TERRAN, + classification=ItemClassification.progression), + # EE units + item_names.LIBERATOR: + ItemData(18 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit, 18, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.VALKYRIE: + ItemData(19 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit, 19, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.WIDOW_MINE: + ItemData(20 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit, 20, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.CYCLONE: + ItemData(21 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit, 21, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.HERC: + ItemData(22 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit, 26, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.WARHOUND: + ItemData(23 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit, 27, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.DOMINION_TROOPER: + ItemData(24 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit_2, 4, SC2Race.TERRAN, + classification=ItemClassification.progression), + # Elites, currently disabled for balance + item_names.PRIDE_OF_AUGUSTRGRAD: + ItemData(50 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit, 28, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.SKY_FURY: + ItemData(51 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit, 29, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.SHOCK_DIVISION: + ItemData(52 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit_2, 0, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.BLACKHAMMER: + ItemData(53 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit_2, 1, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.AEGIS_GUARD: + ItemData(54 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit_2, 2, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.EMPERORS_SHADOW: + ItemData(55 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit_2, 3, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.SON_OF_KORHAL: + ItemData(56 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit_2, 5, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.BULWARK_COMPANY: + ItemData(57 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit_2, 6, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.FIELD_RESPONSE_THETA: + ItemData(58 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit_2, 7, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.EMPERORS_GUARDIAN: + ItemData(59 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit_2, 8, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.NIGHT_HAWK: + ItemData(60 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit_2, 9, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.NIGHT_WOLF: + ItemData(61 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit_2, 10, SC2Race.TERRAN, + classification=ItemClassification.progression), + + # Some other items are moved to Upgrade group because of the way how the bot message is parsed + item_names.PROGRESSIVE_TERRAN_INFANTRY_WEAPON: ItemData(100 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Upgrade, 0, SC2Race.TERRAN, classification=ItemClassification.progression, quantity=WEAPON_ARMOR_UPGRADE_MAX_LEVEL, parent=parent_names.INFANTRY_WEAPON_UNITS), + item_names.PROGRESSIVE_TERRAN_INFANTRY_ARMOR: ItemData(102 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Upgrade, 4, SC2Race.TERRAN, classification=ItemClassification.progression, quantity=WEAPON_ARMOR_UPGRADE_MAX_LEVEL, parent=parent_names.INFANTRY_UNITS), + item_names.PROGRESSIVE_TERRAN_VEHICLE_WEAPON: ItemData(103 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Upgrade, 8, SC2Race.TERRAN, classification=ItemClassification.progression, quantity=WEAPON_ARMOR_UPGRADE_MAX_LEVEL, parent=parent_names.VEHICLE_WEAPON_UNITS), + item_names.PROGRESSIVE_TERRAN_VEHICLE_ARMOR: ItemData(104 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Upgrade, 12, SC2Race.TERRAN, classification=ItemClassification.progression, quantity=WEAPON_ARMOR_UPGRADE_MAX_LEVEL, parent=parent_names.VEHICLE_UNITS), + item_names.PROGRESSIVE_TERRAN_SHIP_WEAPON: ItemData(105 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Upgrade, 16, SC2Race.TERRAN, classification=ItemClassification.progression, quantity=WEAPON_ARMOR_UPGRADE_MAX_LEVEL, parent=parent_names.STARSHIP_WEAPON_UNITS), + item_names.PROGRESSIVE_TERRAN_SHIP_ARMOR: ItemData(106 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Upgrade, 20, SC2Race.TERRAN, classification=ItemClassification.progression, quantity=WEAPON_ARMOR_UPGRADE_MAX_LEVEL, parent=parent_names.STARSHIP_UNITS), + # Bundles + item_names.PROGRESSIVE_TERRAN_WEAPON_UPGRADE: ItemData(107 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Upgrade, -1, SC2Race.TERRAN, classification=ItemClassification.progression, quantity=WEAPON_ARMOR_UPGRADE_MAX_LEVEL), + item_names.PROGRESSIVE_TERRAN_ARMOR_UPGRADE: ItemData(108 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Upgrade, -1, SC2Race.TERRAN, classification=ItemClassification.progression, quantity=WEAPON_ARMOR_UPGRADE_MAX_LEVEL), + item_names.PROGRESSIVE_TERRAN_INFANTRY_UPGRADE: ItemData(109 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Upgrade, -1, SC2Race.TERRAN, classification=ItemClassification.progression, quantity=WEAPON_ARMOR_UPGRADE_MAX_LEVEL, parent=parent_names.INFANTRY_UNITS), + item_names.PROGRESSIVE_TERRAN_VEHICLE_UPGRADE: ItemData(110 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Upgrade, -1, SC2Race.TERRAN, classification=ItemClassification.progression, quantity=WEAPON_ARMOR_UPGRADE_MAX_LEVEL, parent=parent_names.VEHICLE_UNITS), + item_names.PROGRESSIVE_TERRAN_SHIP_UPGRADE: ItemData(111 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Upgrade, -1, SC2Race.TERRAN, classification=ItemClassification.progression, quantity=WEAPON_ARMOR_UPGRADE_MAX_LEVEL, parent=parent_names.STARSHIP_UNITS), + item_names.PROGRESSIVE_TERRAN_WEAPON_ARMOR_UPGRADE: ItemData(112 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Upgrade, -1, SC2Race.TERRAN, classification=ItemClassification.progression, quantity=WEAPON_ARMOR_UPGRADE_MAX_LEVEL), + + # Unit and structure upgrades + item_names.BUNKER_PROJECTILE_ACCELERATOR: + ItemData(200 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_1, 0, SC2Race.TERRAN, + parent=item_names.BUNKER), + item_names.BUNKER_NEOSTEEL_BUNKER: + ItemData(201 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_1, 1, SC2Race.TERRAN, + parent=item_names.BUNKER), + item_names.MISSILE_TURRET_TITANIUM_HOUSING: + ItemData(202 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_1, 2, SC2Race.TERRAN, + parent=item_names.MISSILE_TURRET), + item_names.MISSILE_TURRET_HELLSTORM_BATTERIES: + ItemData(203 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_1, 3, SC2Race.TERRAN, + parent=item_names.MISSILE_TURRET), + item_names.SCV_ADVANCED_CONSTRUCTION: + ItemData(204 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_1, 4, SC2Race.TERRAN), + item_names.SCV_DUAL_FUSION_WELDERS: + ItemData(205 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_1, 5, SC2Race.TERRAN), + item_names.PROGRESSIVE_FIRE_SUPPRESSION_SYSTEM: + ItemData(206 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Progressive, 24, SC2Race.TERRAN, + quantity=2), + item_names.PROGRESSIVE_ORBITAL_COMMAND: + ItemData(207 + SC2WOL_ITEM_ID_OFFSET, FactionlessItemType.Deprecated, -1, SC2Race.TERRAN, + quantity=0, classification=ItemClassification.progression), + item_names.MARINE_PROGRESSIVE_STIMPACK: + ItemData(208 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Progressive, 0, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.MARINE, quantity=2), + item_names.MARINE_COMBAT_SHIELD: + ItemData(209 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_1, 9, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.MARINE), + item_names.MEDIC_ADVANCED_MEDIC_FACILITIES: + ItemData(210 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_1, 10, SC2Race.TERRAN, + parent=item_names.MEDIC), + item_names.MEDIC_STABILIZER_MEDPACKS: + ItemData(211 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_1, 11, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.MEDIC), + item_names.FIREBAT_INCINERATOR_GAUNTLETS: + ItemData(212 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_1, 12, SC2Race.TERRAN, + parent=item_names.FIREBAT), + item_names.FIREBAT_JUGGERNAUT_PLATING: + ItemData(213 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_1, 13, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.FIREBAT), + item_names.MARAUDER_CONCUSSIVE_SHELLS: + ItemData(214 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_1, 14, SC2Race.TERRAN, + parent=item_names.MARAUDER), + item_names.MARAUDER_KINETIC_FOAM: + ItemData(215 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_1, 15, SC2Race.TERRAN, + parent=item_names.MARAUDER), + item_names.REAPER_U238_ROUNDS: + ItemData(216 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_1, 16, SC2Race.TERRAN, + parent=item_names.REAPER), + item_names.REAPER_G4_CLUSTERBOMB: + ItemData(217 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_1, 17, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.REAPER), + item_names.CYCLONE_MAG_FIELD_ACCELERATORS: + ItemData(218 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_1, 18, SC2Race.TERRAN, + parent=item_names.CYCLONE), + item_names.CYCLONE_MAG_FIELD_LAUNCHERS: + ItemData(219 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_1, 19, SC2Race.TERRAN, + parent=item_names.CYCLONE), + item_names.MARINE_LASER_TARGETING_SYSTEM: + ItemData(220 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_1, 8, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.MARINE), + item_names.MARINE_MAGRAIL_MUNITIONS: + ItemData(221 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_1, 20, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.MARINE), + item_names.MARINE_OPTIMIZED_LOGISTICS: + ItemData(222 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_1, 21, SC2Race.TERRAN, + parent=item_names.MARINE), + item_names.MEDIC_RESTORATION: + ItemData(223 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_1, 22, SC2Race.TERRAN, + parent=item_names.MEDIC), + item_names.MEDIC_OPTICAL_FLARE: + ItemData(224 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_1, 23, SC2Race.TERRAN, + parent=item_names.MEDIC), + item_names.MEDIC_RESOURCE_EFFICIENCY: + ItemData(225 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_1, 24, SC2Race.TERRAN, + parent=item_names.MEDIC), + item_names.FIREBAT_PROGRESSIVE_STIMPACK: + ItemData(226 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Progressive, 6, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.FIREBAT, quantity=2), + item_names.FIREBAT_RESOURCE_EFFICIENCY: + ItemData(227 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_1, 25, SC2Race.TERRAN, + parent=item_names.FIREBAT), + item_names.MARAUDER_PROGRESSIVE_STIMPACK: + ItemData(228 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Progressive, 8, SC2Race.TERRAN, + parent=item_names.MARAUDER, quantity=2), + item_names.MARAUDER_LASER_TARGETING_SYSTEM: + ItemData(229 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_1, 26, SC2Race.TERRAN, + parent=item_names.MARAUDER), + item_names.MARAUDER_MAGRAIL_MUNITIONS: + ItemData(230 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_1, 27, SC2Race.TERRAN, + parent=item_names.MARAUDER), + item_names.MARAUDER_INTERNAL_TECH_MODULE: + ItemData(231 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_1, 28, SC2Race.TERRAN, + parent=item_names.MARAUDER), + item_names.SCV_HOSTILE_ENVIRONMENT_ADAPTATION: + ItemData(232 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_1, 29, SC2Race.TERRAN), + item_names.MEDIC_ADAPTIVE_MEDPACKS: + ItemData(233 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_2, 0, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.MEDIC), + item_names.MEDIC_NANO_PROJECTOR: + ItemData(234 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_2, 1, SC2Race.TERRAN, + parent=item_names.MEDIC), + item_names.FIREBAT_INFERNAL_PRE_IGNITER: + ItemData(235 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_2, 2, SC2Race.TERRAN, + parent=item_names.FIREBAT), + item_names.FIREBAT_KINETIC_FOAM: + ItemData(236 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_2, 3, SC2Race.TERRAN, + parent=item_names.FIREBAT), + item_names.FIREBAT_NANO_PROJECTORS: + ItemData(237 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_2, 4, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.FIREBAT), + item_names.MARAUDER_JUGGERNAUT_PLATING: + ItemData(238 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_2, 5, SC2Race.TERRAN, + parent=item_names.MARAUDER), + item_names.REAPER_JET_PACK_OVERDRIVE: + ItemData(239 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_2, 6, SC2Race.TERRAN, + classification=ItemClassification.progression_skip_balancing, parent=item_names.REAPER), + item_names.HELLION_INFERNAL_PLATING: + ItemData(240 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_2, 7, SC2Race.TERRAN, + parent=item_names.HELLION), + item_names.VULTURE_JERRYRIGGED_PATCHUP: + ItemData(241 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_2, 8, SC2Race.TERRAN, + parent=item_names.VULTURE), + item_names.GOLIATH_SHAPED_HULL: + ItemData(242 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_2, 9, SC2Race.TERRAN, + parent=item_names.GOLIATH), + item_names.GOLIATH_RESOURCE_EFFICIENCY: + ItemData(243 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_2, 10, SC2Race.TERRAN, + parent=item_names.GOLIATH), + item_names.GOLIATH_INTERNAL_TECH_MODULE: + ItemData(244 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_2, 11, SC2Race.TERRAN, + parent=item_names.GOLIATH), + item_names.SIEGE_TANK_SHAPED_HULL: + ItemData(245 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_2, 12, SC2Race.TERRAN, + parent=item_names.SIEGE_TANK), + item_names.SIEGE_TANK_RESOURCE_EFFICIENCY: + ItemData(246 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_2, 13, SC2Race.TERRAN, + parent=item_names.SIEGE_TANK), + item_names.PREDATOR_CLOAK: + ItemData(247 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_2, 14, SC2Race.TERRAN, + parent=item_names.PREDATOR), + item_names.PREDATOR_CHARGE: + ItemData(248 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_2, 15, SC2Race.TERRAN, + parent=item_names.PREDATOR), + item_names.MEDIVAC_SCATTER_VEIL: + ItemData(249 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_2, 16, SC2Race.TERRAN, + parent=item_names.MEDIVAC), + item_names.REAPER_PROGRESSIVE_STIMPACK: + ItemData(250 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Progressive, 10, SC2Race.TERRAN, + parent=item_names.REAPER, quantity=2), + item_names.REAPER_LASER_TARGETING_SYSTEM: + ItemData(251 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_2, 17, SC2Race.TERRAN, + parent=item_names.REAPER), + item_names.REAPER_ADVANCED_CLOAKING_FIELD: + ItemData(252 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_2, 18, SC2Race.TERRAN, + parent=item_names.REAPER), + item_names.REAPER_SPIDER_MINES: + ItemData(253 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_2, 19, SC2Race.TERRAN, + parent=item_names.REAPER, + important_for_filtering=True), + item_names.REAPER_COMBAT_DRUGS: + ItemData(254 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_2, 20, SC2Race.TERRAN, + parent=item_names.REAPER), + item_names.HELLION_HELLBAT: + ItemData(255 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_2, 21, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.HELLION), + item_names.HELLION_SMART_SERVOS: + ItemData(256 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_2, 22, SC2Race.TERRAN, + parent=item_names.HELLION), + item_names.HELLION_OPTIMIZED_LOGISTICS: + ItemData(257 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_2, 23, SC2Race.TERRAN, + parent=item_names.HELLION), + item_names.HELLION_JUMP_JETS: + ItemData(258 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_2, 24, SC2Race.TERRAN, + parent=item_names.HELLION), + item_names.HELLION_PROGRESSIVE_STIMPACK: + ItemData(259 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Progressive, 12, SC2Race.TERRAN, + parent=item_names.HELLION, quantity=2), + item_names.VULTURE_ION_THRUSTERS: + ItemData(260 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_2, 25, SC2Race.TERRAN, + parent=item_names.VULTURE), + item_names.VULTURE_AUTO_LAUNCHERS: + ItemData(261 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_2, 26, SC2Race.TERRAN, + parent=item_names.VULTURE), + item_names.SPIDER_MINE_HIGH_EXPLOSIVE_MUNITION: + ItemData(262 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_2, 27, SC2Race.TERRAN, + parent=parent_names.SPIDER_MINE_SOURCE), + item_names.GOLIATH_JUMP_JETS: + ItemData(263 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_2, 28, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.GOLIATH), + item_names.GOLIATH_OPTIMIZED_LOGISTICS: + ItemData(264 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_2, 29, SC2Race.TERRAN, + parent=item_names.GOLIATH), + item_names.DIAMONDBACK_HYPERFLUXOR: + ItemData(265 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_3, 0, SC2Race.TERRAN, + parent=item_names.DIAMONDBACK), + item_names.DIAMONDBACK_BURST_CAPACITORS: + ItemData(266 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_3, 1, SC2Race.TERRAN, + parent=item_names.DIAMONDBACK), + item_names.DIAMONDBACK_RESOURCE_EFFICIENCY: + ItemData(267 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_3, 2, SC2Race.TERRAN, + parent=item_names.DIAMONDBACK), + item_names.SIEGE_TANK_JUMP_JETS: + ItemData(268 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_3, 3, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.SIEGE_TANK), + item_names.SIEGE_TANK_SPIDER_MINES: + ItemData(269 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_3, 4, SC2Race.TERRAN, + parent=item_names.SIEGE_TANK, + important_for_filtering=True), + item_names.SIEGE_TANK_SMART_SERVOS: + ItemData(270 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_3, 5, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.SIEGE_TANK), + item_names.SIEGE_TANK_GRADUATING_RANGE: + ItemData(271 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_3, 6, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.SIEGE_TANK), + item_names.SIEGE_TANK_LASER_TARGETING_SYSTEM: + ItemData(272 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_3, 7, SC2Race.TERRAN, + parent=item_names.SIEGE_TANK), + item_names.SIEGE_TANK_ADVANCED_SIEGE_TECH: + ItemData(273 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_3, 8, SC2Race.TERRAN, + parent=item_names.SIEGE_TANK), + item_names.SIEGE_TANK_INTERNAL_TECH_MODULE: + ItemData(274 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_3, 9, SC2Race.TERRAN, + parent=item_names.SIEGE_TANK), + item_names.PREDATOR_RESOURCE_EFFICIENCY: + ItemData(275 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_3, 10, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.PREDATOR), + item_names.MEDIVAC_EXPANDED_HULL: + ItemData(276 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_3, 11, SC2Race.TERRAN, + parent=item_names.MEDIVAC), + item_names.MEDIVAC_AFTERBURNERS: + ItemData(277 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_3, 12, SC2Race.TERRAN, + parent=item_names.MEDIVAC), + item_names.WRAITH_ADVANCED_LASER_TECHNOLOGY: + ItemData(278 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_3, 13, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.WRAITH), + item_names.VIKING_SMART_SERVOS: + ItemData(279 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_3, 14, SC2Race.TERRAN, + parent=item_names.VIKING), + item_names.VIKING_ANTI_MECHANICAL_MUNITION: + ItemData(280 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_3, 15, SC2Race.TERRAN, + parent=item_names.VIKING), + item_names.DIAMONDBACK_MAGLEV_PROPULSION: + ItemData(281 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_3, 21, SC2Race.TERRAN, + parent=item_names.DIAMONDBACK), + item_names.WARHOUND_RESOURCE_EFFICIENCY: + ItemData(282 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_6, 13, SC2Race.TERRAN, + parent=item_names.WARHOUND), + item_names.WARHOUND_AXIOM_PLATING: + ItemData(283 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_6, 14, SC2Race.TERRAN, + parent=item_names.WARHOUND), + item_names.HERC_RESOURCE_EFFICIENCY: + ItemData(284 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_6, 15, SC2Race.TERRAN, + parent=item_names.HERC), + item_names.HERC_JUGGERNAUT_PLATING: + ItemData(285 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_6, 16, SC2Race.TERRAN, + parent=item_names.HERC), + item_names.HERC_KINETIC_FOAM: + ItemData(286 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_6, 17, SC2Race.TERRAN, + parent=item_names.HERC), + item_names.REAPER_RESOURCE_EFFICIENCY: + ItemData(287 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_6, 18, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.REAPER), + item_names.REAPER_BALLISTIC_FLIGHTSUIT: + ItemData(288 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_6, 19, SC2Race.TERRAN, + parent=item_names.REAPER), + item_names.SIEGE_TANK_PROGRESSIVE_TRANSPORT_HOOK: + ItemData(289 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Progressive_2, 6, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=parent_names.SIEGE_TANK_AND_TRANSPORT, quantity=2), + item_names.SIEGE_TANK_ALLTERRAIN_TREADS : + ItemData(290 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_6, 20, SC2Race.TERRAN, + parent=item_names.SIEGE_TANK), + item_names.MEDIVAC_RAPID_REIGNITION_SYSTEMS: + ItemData(291 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_6, 21, SC2Race.TERRAN, + parent=item_names.MEDIVAC), + item_names.BATTLECRUISER_BEHEMOTH_REACTOR: + ItemData(292 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_6, 22, SC2Race.TERRAN, + parent=item_names.BATTLECRUISER), + item_names.THOR_RAPID_RELOAD: + ItemData(293 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_6, 23, SC2Race.TERRAN, + parent=item_names.THOR), + item_names.LIBERATOR_GUERILLA_MISSILES: + ItemData(294 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_6, 24, SC2Race.TERRAN, + parent=item_names.LIBERATOR), + item_names.WIDOW_MINE_RESOURCE_EFFICIENCY: + ItemData(295 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_6, 25, SC2Race.TERRAN, + parent=item_names.WIDOW_MINE), + item_names.HERC_GRAPPLE_PULL: + ItemData(296 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_6, 26, SC2Race.TERRAN, + parent=item_names.HERC), + item_names.COMMAND_CENTER_SCANNER_SWEEP: + ItemData(297 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_6, 27, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.COMMAND_CENTER_MULE: + ItemData(298 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_6, 28, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.COMMAND_CENTER_EXTRA_SUPPLIES: + ItemData(299 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_6, 29, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.HELLION_TWIN_LINKED_FLAMETHROWER: + ItemData(300 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_3, 16, SC2Race.TERRAN, + parent=item_names.HELLION), + item_names.HELLION_THERMITE_FILAMENTS: + ItemData(301 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_3, 17, SC2Race.TERRAN, + parent=item_names.HELLION), + item_names.SPIDER_MINE_CERBERUS_MINE: + ItemData(302 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_3, 18, SC2Race.TERRAN, + parent=parent_names.SPIDER_MINE_SOURCE), + item_names.VULTURE_PROGRESSIVE_REPLENISHABLE_MAGAZINE: + ItemData(303 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Progressive, 16, SC2Race.TERRAN, + parent=item_names.VULTURE, quantity=2), + item_names.GOLIATH_MULTI_LOCK_WEAPONS_SYSTEM: + ItemData(304 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_3, 19, SC2Race.TERRAN, + parent=item_names.GOLIATH), + item_names.GOLIATH_ARES_CLASS_TARGETING_SYSTEM: + ItemData(305 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_3, 20, SC2Race.TERRAN, + parent=item_names.GOLIATH), + item_names.DIAMONDBACK_PROGRESSIVE_TRI_LITHIUM_POWER_CELL: + ItemData(306 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Progressive_2, 4, SC2Race.TERRAN, + parent=item_names.DIAMONDBACK, quantity=2), + item_names.DIAMONDBACK_SHAPED_HULL: + ItemData(307 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_3, 22, SC2Race.TERRAN, + parent=item_names.DIAMONDBACK), + item_names.SIEGE_TANK_MAELSTROM_ROUNDS: + ItemData(308 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_3, 23, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.SIEGE_TANK), + item_names.SIEGE_TANK_SHAPED_BLAST: + ItemData(309 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_3, 24, SC2Race.TERRAN, + parent=item_names.SIEGE_TANK), + item_names.MEDIVAC_RAPID_DEPLOYMENT_TUBE: + ItemData(310 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_3, 25, SC2Race.TERRAN, + parent=item_names.MEDIVAC), + item_names.MEDIVAC_ADVANCED_HEALING_AI: + ItemData(311 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_3, 26, SC2Race.TERRAN, + parent=item_names.MEDIVAC), + item_names.WRAITH_PROGRESSIVE_TOMAHAWK_POWER_CELLS: + ItemData(312 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Progressive, 18, SC2Race.TERRAN, + parent=item_names.WRAITH, quantity=2), + item_names.WRAITH_DISPLACEMENT_FIELD: + ItemData(313 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_3, 27, SC2Race.TERRAN, + parent=item_names.WRAITH), + item_names.VIKING_RIPWAVE_MISSILES: + ItemData(314 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_3, 28, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.VIKING), + item_names.VIKING_PHOBOS_CLASS_WEAPONS_SYSTEM: + ItemData(315 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_3, 29, SC2Race.TERRAN, + parent=item_names.VIKING), + item_names.BANSHEE_PROGRESSIVE_CROSS_SPECTRUM_DAMPENERS: + ItemData(316 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Progressive, 2, SC2Race.TERRAN, + parent=item_names.BANSHEE, quantity=2), + item_names.BANSHEE_SHOCKWAVE_MISSILE_BATTERY: + ItemData(317 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_4, 0, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.BANSHEE), + item_names.BATTLECRUISER_PROGRESSIVE_MISSILE_PODS: + ItemData(318 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Progressive_2, 2, SC2Race.TERRAN, + parent=item_names.BATTLECRUISER, quantity=2), + item_names.BATTLECRUISER_PROGRESSIVE_DEFENSIVE_MATRIX: + ItemData(319 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Progressive, 20, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.BATTLECRUISER, quantity=2), + item_names.GHOST_OCULAR_IMPLANTS: + ItemData(320 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_4, 2, SC2Race.TERRAN, + parent=item_names.GHOST), + item_names.GHOST_CRIUS_SUIT: + ItemData(321 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_4, 3, SC2Race.TERRAN, + parent=item_names.GHOST), + item_names.SPECTRE_PSIONIC_LASH: + ItemData(322 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_4, 4, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.SPECTRE), + item_names.SPECTRE_NYX_CLASS_CLOAKING_MODULE: + ItemData(323 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_4, 5, SC2Race.TERRAN, + parent=item_names.SPECTRE), + item_names.THOR_330MM_BARRAGE_CANNON: + ItemData(324 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_4, 6, SC2Race.TERRAN, + parent=item_names.THOR), + item_names.THOR_PROGRESSIVE_IMMORTALITY_PROTOCOL: + ItemData(325 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Progressive, 22, SC2Race.TERRAN, + parent=item_names.THOR, quantity=2), + item_names.LIBERATOR_ADVANCED_BALLISTICS: + ItemData(326 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_4, 7, SC2Race.TERRAN, + parent=item_names.LIBERATOR), + item_names.LIBERATOR_RAID_ARTILLERY: + ItemData(327 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_4, 8, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.LIBERATOR), + item_names.WIDOW_MINE_DRILLING_CLAWS: + ItemData(328 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_4, 9, SC2Race.TERRAN, + parent=item_names.WIDOW_MINE), + item_names.WIDOW_MINE_CONCEALMENT: + ItemData(329 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_4, 10, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.WIDOW_MINE), + item_names.MEDIVAC_ADVANCED_CLOAKING_FIELD: + ItemData(330 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_4, 11, SC2Race.TERRAN, + parent=item_names.MEDIVAC), + item_names.WRAITH_TRIGGER_OVERRIDE: + ItemData(331 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_4, 12, SC2Race.TERRAN, + parent=item_names.WRAITH), + item_names.WRAITH_INTERNAL_TECH_MODULE: + ItemData(332 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_4, 13, SC2Race.TERRAN, + parent=item_names.WRAITH), + item_names.WRAITH_RESOURCE_EFFICIENCY: + ItemData(333 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_4, 14, SC2Race.TERRAN, + parent=item_names.WRAITH), + item_names.VIKING_SHREDDER_ROUNDS: + ItemData(334 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_4, 15, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.VIKING), + item_names.VIKING_WILD_MISSILES: + ItemData(335 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_4, 16, SC2Race.TERRAN, + parent=item_names.VIKING), + item_names.BANSHEE_SHAPED_HULL: + ItemData(336 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_4, 17, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.BANSHEE), + item_names.BANSHEE_ADVANCED_TARGETING_OPTICS: + ItemData(337 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_4, 18, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.BANSHEE), + item_names.BANSHEE_DISTORTION_BLASTERS: + ItemData(338 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_4, 19, SC2Race.TERRAN, + parent=item_names.BANSHEE), + item_names.BANSHEE_ROCKET_BARRAGE: + ItemData(339 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_4, 20, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.BANSHEE), + item_names.GHOST_RESOURCE_EFFICIENCY: + ItemData(340 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_4, 21, SC2Race.TERRAN, + parent=item_names.GHOST), + item_names.SPECTRE_RESOURCE_EFFICIENCY: + ItemData(341 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_4, 22, SC2Race.TERRAN, + parent=item_names.SPECTRE), + item_names.THOR_BUTTON_WITH_A_SKULL_ON_IT: + ItemData(342 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_4, 23, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.THOR), + item_names.THOR_LASER_TARGETING_SYSTEM: + ItemData(343 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_4, 24, SC2Race.TERRAN, + parent=item_names.THOR), + item_names.THOR_LARGE_SCALE_FIELD_CONSTRUCTION: + ItemData(344 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_4, 25, SC2Race.TERRAN, + parent=item_names.THOR), + item_names.RAVEN_RESOURCE_EFFICIENCY: + ItemData(345 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_4, 26, SC2Race.TERRAN, + parent=item_names.RAVEN), + item_names.RAVEN_DURABLE_MATERIALS: + ItemData(346 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_4, 27, SC2Race.TERRAN, + parent=item_names.RAVEN), + item_names.SCIENCE_VESSEL_IMPROVED_NANO_REPAIR: + ItemData(347 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_4, 28, SC2Race.TERRAN, + parent=item_names.SCIENCE_VESSEL), + item_names.SCIENCE_VESSEL_MAGELLAN_COMPUTATION_SYSTEMS: + ItemData(348 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_4, 29, SC2Race.TERRAN, + parent=item_names.SCIENCE_VESSEL), + item_names.CYCLONE_RESOURCE_EFFICIENCY: + ItemData(349 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_5, 0, SC2Race.TERRAN, + parent=item_names.CYCLONE), + item_names.BANSHEE_HYPERFLIGHT_ROTORS: + ItemData(350 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_5, 1, SC2Race.TERRAN, + parent=item_names.BANSHEE), + item_names.BANSHEE_LASER_TARGETING_SYSTEM: + ItemData(351 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_5, 2, SC2Race.TERRAN, + parent=item_names.BANSHEE), + item_names.BANSHEE_INTERNAL_TECH_MODULE: + ItemData(352 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_5, 3, SC2Race.TERRAN, + parent=item_names.BANSHEE), + item_names.BATTLECRUISER_TACTICAL_JUMP: + ItemData(353 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_5, 4, SC2Race.TERRAN, + parent=item_names.BATTLECRUISER), + item_names.BATTLECRUISER_CLOAK: + ItemData(354 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_5, 5, SC2Race.TERRAN, + parent=item_names.BATTLECRUISER), + item_names.BATTLECRUISER_ATX_LASER_BATTERY: + ItemData(355 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_5, 6, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.BATTLECRUISER), + item_names.BATTLECRUISER_OPTIMIZED_LOGISTICS: + ItemData(356 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_5, 7, SC2Race.TERRAN, + parent=item_names.BATTLECRUISER), + item_names.BATTLECRUISER_INTERNAL_TECH_MODULE: + ItemData(357 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_5, 8, SC2Race.TERRAN, + parent=item_names.BATTLECRUISER), + item_names.GHOST_EMP_ROUNDS: + ItemData(358 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_5, 9, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.GHOST), + item_names.GHOST_LOCKDOWN: + ItemData(359 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_5, 10, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.GHOST), + item_names.SPECTRE_IMPALER_ROUNDS: + ItemData(360 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_5, 11, SC2Race.TERRAN, + parent=item_names.SPECTRE), + item_names.THOR_PROGRESSIVE_HIGH_IMPACT_PAYLOAD: + ItemData(361 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Progressive, 14, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.THOR, quantity=2), + item_names.RAVEN_BIO_MECHANICAL_REPAIR_DRONE: + ItemData(363 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_5, 12, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.RAVEN), + item_names.RAVEN_SPIDER_MINES: + ItemData(364 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_5, 13, SC2Race.TERRAN, + parent=item_names.RAVEN, important_for_filtering=True), + item_names.RAVEN_RAILGUN_TURRET: + ItemData(365 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_5, 14, SC2Race.TERRAN, + parent=item_names.RAVEN), + item_names.RAVEN_HUNTER_SEEKER_WEAPON: + ItemData(366 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_5, 15, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.RAVEN), + item_names.RAVEN_INTERFERENCE_MATRIX: + ItemData(367 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_5, 16, SC2Race.TERRAN, + parent=item_names.RAVEN), + item_names.RAVEN_ANTI_ARMOR_MISSILE: + ItemData(368 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_5, 17, SC2Race.TERRAN, + parent=item_names.RAVEN), + item_names.RAVEN_INTERNAL_TECH_MODULE: + ItemData(369 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_5, 18, SC2Race.TERRAN, + parent=item_names.RAVEN), + item_names.SCIENCE_VESSEL_EMP_SHOCKWAVE: + ItemData(370 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_5, 19, SC2Race.TERRAN, + parent=item_names.SCIENCE_VESSEL), + item_names.SCIENCE_VESSEL_DEFENSIVE_MATRIX: + ItemData(371 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_5, 20, SC2Race.TERRAN, + parent=item_names.SCIENCE_VESSEL), + item_names.CYCLONE_TARGETING_OPTICS: + ItemData(372 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_5, 21, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.CYCLONE), + item_names.CYCLONE_RAPID_FIRE_LAUNCHERS: + ItemData(373 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_5, 22, SC2Race.TERRAN, + parent=item_names.CYCLONE), + item_names.LIBERATOR_CLOAK: + ItemData(374 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_5, 23, SC2Race.TERRAN, + parent=item_names.LIBERATOR), + item_names.LIBERATOR_LASER_TARGETING_SYSTEM: + ItemData(375 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_5, 24, SC2Race.TERRAN, + parent=item_names.LIBERATOR), + item_names.LIBERATOR_OPTIMIZED_LOGISTICS: + ItemData(376 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_5, 25, SC2Race.TERRAN, + parent=item_names.LIBERATOR), + item_names.WIDOW_MINE_BLACK_MARKET_LAUNCHERS: + ItemData(377 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_5, 26, SC2Race.TERRAN, + parent=item_names.WIDOW_MINE), + item_names.WIDOW_MINE_EXECUTIONER_MISSILES: + ItemData(378 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_5, 27, SC2Race.TERRAN, + parent=item_names.WIDOW_MINE), + item_names.VALKYRIE_ENHANCED_CLUSTER_LAUNCHERS: + ItemData(379 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_5, 28, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.VALKYRIE), + item_names.VALKYRIE_SHAPED_HULL: + ItemData(380 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_5, 29, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.VALKYRIE), + item_names.VALKYRIE_FLECHETTE_MISSILES: + ItemData(381 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_6, 0, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.VALKYRIE), + item_names.VALKYRIE_AFTERBURNERS: + ItemData(382 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_6, 1, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.VALKYRIE), + item_names.CYCLONE_INTERNAL_TECH_MODULE: + ItemData(383 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_6, 2, SC2Race.TERRAN, + parent=item_names.CYCLONE), + item_names.LIBERATOR_SMART_SERVOS: + ItemData(384 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_6, 3, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.LIBERATOR), + item_names.LIBERATOR_RESOURCE_EFFICIENCY: + ItemData(385 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_6, 4, SC2Race.TERRAN, + parent=item_names.LIBERATOR), + item_names.HERCULES_INTERNAL_FUSION_MODULE: + ItemData(386 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_6, 5, SC2Race.TERRAN, + parent=item_names.HERCULES), + item_names.HERCULES_TACTICAL_JUMP: + ItemData(387 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_6, 6, SC2Race.TERRAN, + parent=item_names.HERCULES), + item_names.PLANETARY_FORTRESS_PROGRESSIVE_AUGMENTED_THRUSTERS: + ItemData(388 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Progressive, 28, SC2Race.TERRAN, + parent=item_names.PLANETARY_FORTRESS, quantity=2), + item_names.PLANETARY_FORTRESS_IBIKS_TRACKING_SCANNERS: + ItemData(389 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_6, 7, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.PLANETARY_FORTRESS), + item_names.VALKYRIE_LAUNCHING_VECTOR_COMPENSATOR: + ItemData(390 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_6, 8, SC2Race.TERRAN, + parent=item_names.VALKYRIE), + item_names.VALKYRIE_RESOURCE_EFFICIENCY: + ItemData(391 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_6, 9, SC2Race.TERRAN, + parent=item_names.VALKYRIE), + item_names.PREDATOR_VESPENE_SYNTHESIS: + ItemData(392 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_6, 10, SC2Race.TERRAN, + parent=item_names.PREDATOR), + item_names.BATTLECRUISER_BEHEMOTH_PLATING: + ItemData(393 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_6, 11, SC2Race.TERRAN, + parent=item_names.BATTLECRUISER), + item_names.BATTLECRUISER_MOIRAI_IMPULSE_DRIVE: + ItemData(394 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_6, 12, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.BATTLECRUISER), + item_names.PLANETARY_FORTRESS_ORBITAL_MODULE: + ItemData(395 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_4, 1, SC2Race.TERRAN, + parent=parent_names.ORBITAL_COMMAND_AND_PLANETARY), + item_names.DEVASTATOR_TURRET_CONCUSSIVE_GRENADES: + ItemData(396 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_7, 0, SC2Race.TERRAN, + parent=item_names.DEVASTATOR_TURRET), + item_names.DEVASTATOR_TURRET_ANTI_ARMOR_MUNITIONS: + ItemData(397 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_7, 1, SC2Race.TERRAN, + parent=item_names.DEVASTATOR_TURRET), + item_names.DEVASTATOR_TURRET_RESOURCE_EFFICIENCY: + ItemData(398 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_7, 2, SC2Race.TERRAN, + parent=item_names.DEVASTATOR_TURRET), + item_names.MISSILE_TURRET_RESOURCE_EFFICENCY: + ItemData(399 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_7, 3, SC2Race.TERRAN, + parent=item_names.MISSILE_TURRET), + # Note(mm): WoL ID 400 collides with buildings; jump forward to leave buildings room + + #Buildings + item_names.BUNKER: + ItemData(400 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Building, 0, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.MISSILE_TURRET: + ItemData(401 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Building, 1, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.SENSOR_TOWER: + ItemData(402 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Building, 2, SC2Race.TERRAN, + classification=ItemClassification.progression_skip_balancing), + item_names.DEVASTATOR_TURRET: + ItemData(403 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Building, 7, SC2Race.TERRAN, + classification=ItemClassification.progression), + + item_names.WAR_PIGS: + ItemData(500 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Mercenary, 0, SC2Race.TERRAN, + classification=ItemClassification.progression_skip_balancing), + item_names.DEVIL_DOGS: + ItemData(501 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Mercenary, 1, SC2Race.TERRAN, + classification=ItemClassification.progression_skip_balancing), + item_names.HAMMER_SECURITIES: + ItemData(502 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Mercenary, 2, SC2Race.TERRAN, + classification=ItemClassification.progression_skip_balancing), + item_names.SPARTAN_COMPANY: + ItemData(503 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Mercenary, 3, SC2Race.TERRAN, + classification=ItemClassification.progression_skip_balancing), + item_names.SIEGE_BREAKERS: + ItemData(504 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Mercenary, 4, SC2Race.TERRAN, + classification=ItemClassification.progression_skip_balancing), + item_names.HELS_ANGELS: + ItemData(505 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Mercenary, 5, SC2Race.TERRAN, + classification=ItemClassification.progression_skip_balancing), + item_names.DUSK_WINGS: + ItemData(506 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Mercenary, 6, SC2Race.TERRAN, + classification=ItemClassification.progression_skip_balancing), + item_names.JACKSONS_REVENGE: + ItemData(507 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Mercenary, 7, SC2Race.TERRAN, + classification=ItemClassification.progression_skip_balancing), + item_names.SKIBIS_ANGELS: + ItemData(508 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Mercenary, 8, SC2Race.TERRAN, + classification=ItemClassification.progression_skip_balancing), + item_names.DEATH_HEADS: + ItemData(509 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Mercenary, 9, SC2Race.TERRAN, + classification=ItemClassification.progression_skip_balancing), + item_names.WINGED_NIGHTMARES: + ItemData(510 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Mercenary, 10, SC2Race.TERRAN, + classification=ItemClassification.progression_skip_balancing), + item_names.MIDNIGHT_RIDERS: + ItemData(511 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Mercenary, 11, SC2Race.TERRAN, + classification=ItemClassification.progression_skip_balancing), + item_names.BRYNHILDS: + ItemData(512 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Mercenary, 12, SC2Race.TERRAN, + classification=ItemClassification.progression_skip_balancing), + item_names.JOTUN: + ItemData(513 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Mercenary, 13, SC2Race.TERRAN, + classification=ItemClassification.progression_skip_balancing), + + item_names.ULTRA_CAPACITORS: + ItemData(600 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Laboratory, 0, SC2Race.TERRAN), + item_names.VANADIUM_PLATING: + ItemData(601 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Laboratory, 1, SC2Race.TERRAN), + item_names.ORBITAL_DEPOTS: + ItemData(602 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Laboratory, 2, SC2Race.TERRAN, classification=ItemClassification.progression), + item_names.MICRO_FILTERING: + ItemData(603 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Laboratory, 3, SC2Race.TERRAN, classification=ItemClassification.progression), + item_names.AUTOMATED_REFINERY: + ItemData(604 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Laboratory, 4, SC2Race.TERRAN, classification=ItemClassification.progression), + item_names.COMMAND_CENTER_COMMAND_CENTER_REACTOR: + ItemData(605 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Laboratory, 5, SC2Race.TERRAN, classification=ItemClassification.progression), + item_names.RAVEN: + ItemData(606 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit, 22, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.SCIENCE_VESSEL: + ItemData(607 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit, 23, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.TECH_REACTOR: + ItemData(608 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Laboratory, 6, SC2Race.TERRAN, classification=ItemClassification.progression), + item_names.ORBITAL_STRIKE: + ItemData(609 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Laboratory, 7, SC2Race.TERRAN, + parent=parent_names.INFANTRY_UNITS), + item_names.BUNKER_SHRIKE_TURRET: + ItemData(610 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_1, 6, SC2Race.TERRAN, + parent=item_names.BUNKER), + item_names.BUNKER_FORTIFIED_BUNKER: + ItemData(611 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_1, 7, SC2Race.TERRAN, + parent=item_names.BUNKER), + item_names.PLANETARY_FORTRESS: + ItemData(612 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Building, 3, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.PERDITION_TURRET: + ItemData(613 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Building, 4, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.PREDATOR: + ItemData(614 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit, 24, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.HERCULES: + ItemData(615 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Unit, 25, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.CELLULAR_REACTOR: + ItemData(616 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Laboratory, 8, SC2Race.TERRAN), + item_names.PROGRESSIVE_REGENERATIVE_BIO_STEEL: + ItemData(617 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Progressive, 4, SC2Race.TERRAN, quantity=3, + classification= ItemClassification.progression), + item_names.HIVE_MIND_EMULATOR: + ItemData(618 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Laboratory, 21, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.PSI_DISRUPTER: + ItemData(619 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Laboratory, 18, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.STRUCTURE_ARMOR: + ItemData(620 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Laboratory, 9, SC2Race.TERRAN), + item_names.HI_SEC_AUTO_TRACKING: + ItemData(621 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Laboratory, 10, SC2Race.TERRAN), + item_names.ADVANCED_OPTICS: + ItemData(622 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Laboratory, 11, SC2Race.TERRAN), + item_names.ROGUE_FORCES: + ItemData(623 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Laboratory, 12, SC2Race.TERRAN, classification=ItemClassification.progression, parent=parent_names.TERRAN_MERCENARIES), + item_names.MECHANICAL_KNOW_HOW: + ItemData(624 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Laboratory, 13, SC2Race.TERRAN), + item_names.MERCENARY_MUNITIONS: + ItemData(625 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Laboratory, 14, SC2Race.TERRAN), + item_names.PROGRESSIVE_FAST_DELIVERY: + ItemData(626 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Progressive_2, 8, SC2Race.TERRAN, quantity=2, classification=ItemClassification.progression, parent=parent_names.TERRAN_MERCENARIES), + item_names.RAPID_REINFORCEMENT: + ItemData(627 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Laboratory, 16, SC2Race.TERRAN, classification=ItemClassification.progression, parent=parent_names.TERRAN_MERCENARIES), + item_names.FUSION_CORE_FUSION_REACTOR: + ItemData(628 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Laboratory, 17, SC2Race.TERRAN), + item_names.SONIC_DISRUPTER: + ItemData(629 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Laboratory, 19, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.PSI_SCREEN: + ItemData(630 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Laboratory, 20, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.ARGUS_AMPLIFIER: + ItemData(631 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Laboratory, 22, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.PSI_INDOCTRINATOR: + ItemData(632 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Laboratory, 23, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.SIGNAL_BEACON: + ItemData(633 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Laboratory, 24, SC2Race.TERRAN, parent=parent_names.TERRAN_MERCENARIES), + + # WoL Protoss takes SC2WOL + 700~708 + + item_names.SCIENCE_VESSEL_TACTICAL_JUMP: + ItemData(750 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_7, 4, SC2Race.TERRAN, + parent=item_names.SCIENCE_VESSEL), + item_names.LIBERATOR_UED_MISSILE_TECHNOLOGY: + ItemData(751 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_7, 5, SC2Race.TERRAN, + parent=item_names.LIBERATOR), + item_names.BATTLECRUISER_FIELD_ASSIST_TARGETING_SYSTEM: + ItemData(752 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_7, 6, SC2Race.TERRAN, + parent=item_names.BATTLECRUISER), + item_names.PREDATOR_ADAPTIVE_DEFENSES: + ItemData(753 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_7, 7, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.PREDATOR), + item_names.VIKING_AESIR_TURBINES: + ItemData(754 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_7, 8, SC2Race.TERRAN, + parent=item_names.VIKING), + item_names.MEDIVAC_RESOURCE_EFFICIENCY: + ItemData(755 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_7, 9, SC2Race.TERRAN, + parent=item_names.MEDIVAC), + item_names.EMPERORS_SHADOW_SOVEREIGN_TACTICAL_MISSILES: + ItemData(756 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_7, 10, SC2Race.TERRAN, + parent=item_names.EMPERORS_SHADOW), + item_names.DOMINION_TROOPER_B2_HIGH_CAL_LMG: + ItemData(757 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_7, 11, SC2Race.TERRAN, + parent=item_names.DOMINION_TROOPER, important_for_filtering=True), + item_names.DOMINION_TROOPER_HAILSTORM_LAUNCHER: + ItemData(758 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_7, 12, SC2Race.TERRAN, + parent=item_names.DOMINION_TROOPER, important_for_filtering=True), + item_names.DOMINION_TROOPER_CPO7_SALAMANDER_FLAMETHROWER: + ItemData(759 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_7, 13, SC2Race.TERRAN, + parent=item_names.DOMINION_TROOPER, important_for_filtering=True), + item_names.DOMINION_TROOPER_ADVANCED_ALLOYS: + ItemData(760 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_7, 14, SC2Race.TERRAN, + parent=parent_names.DOMINION_TROOPER_WEAPONS), + item_names.DOMINION_TROOPER_OPTIMIZED_LOGISTICS: + ItemData(761 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_7, 15, SC2Race.TERRAN, + parent=item_names.DOMINION_TROOPER), + item_names.SCV_CONSTRUCTION_JUMP_JETS: + ItemData(762 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_7, 16, SC2Race.TERRAN), + item_names.WIDOW_MINE_DEMOLITION_PAYLOAD: + ItemData(763 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_7, 17, SC2Race.TERRAN, + classification=ItemClassification.progression, parent=item_names.WIDOW_MINE), + item_names.SENSOR_TOWER_ASSISTIVE_TARGETING: + ItemData(764 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_7, 18, SC2Race.TERRAN, + parent=item_names.SENSOR_TOWER), + item_names.SENSOR_TOWER_MUILTISPECTRUM_DOPPLER: + ItemData(765 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_7, 19, SC2Race.TERRAN, + parent=item_names.SENSOR_TOWER), + item_names.WARHOUND_DEPLOY_TURRET: + ItemData(766 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_7, 20, SC2Race.TERRAN, + parent=item_names.WARHOUND), + item_names.GHOST_BARGAIN_BIN_PRICES: + ItemData(767 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_7, 21, SC2Race.TERRAN, + parent=item_names.GHOST), + item_names.SPECTRE_BARGAIN_BIN_PRICES: + ItemData(768 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Armory_7, 22, SC2Race.TERRAN, + parent=item_names.SPECTRE), + + # Filler items to fill remaining spots + item_names.STARTING_MINERALS: + ItemData(800 + SC2WOL_ITEM_ID_OFFSET, FactionlessItemType.Minerals, -1, SC2Race.ANY, quantity=0, + classification=ItemClassification.filler), + item_names.STARTING_VESPENE: + ItemData(801 + SC2WOL_ITEM_ID_OFFSET, FactionlessItemType.Vespene, -1, SC2Race.ANY, quantity=0, + classification=ItemClassification.filler), + item_names.STARTING_SUPPLY: + ItemData(802 + SC2WOL_ITEM_ID_OFFSET, FactionlessItemType.Supply, -1, SC2Race.ANY, quantity=0, + classification=ItemClassification.filler), + # This item is used to "remove" location from the game. Never placed unless plando'd + item_names.NOTHING: + ItemData(803 + SC2WOL_ITEM_ID_OFFSET, FactionlessItemType.Nothing, -1, SC2Race.ANY, quantity=0, + classification=ItemClassification.trap), + item_names.MAX_SUPPLY: + ItemData(804 + SC2WOL_ITEM_ID_OFFSET, FactionlessItemType.MaxSupply, -1, SC2Race.ANY, quantity=0, + classification=ItemClassification.filler), + item_names.SHIELD_REGENERATION: + ItemData(805 + SC2WOL_ITEM_ID_OFFSET, ProtossItemType.ShieldRegeneration, 1, SC2Race.PROTOSS, quantity=0, + classification=ItemClassification.filler), + item_names.BUILDING_CONSTRUCTION_SPEED: + ItemData(806 + SC2WOL_ITEM_ID_OFFSET, FactionlessItemType.BuildingSpeed, 1, SC2Race.ANY, quantity=0, + classification=ItemClassification.filler), + item_names.UPGRADE_RESEARCH_SPEED: + ItemData(807 + SC2WOL_ITEM_ID_OFFSET, FactionlessItemType.ResearchSpeed, 1, SC2Race.ANY, quantity=0, + classification=ItemClassification.filler), + item_names.UPGRADE_RESEARCH_COST: + ItemData(808 + SC2WOL_ITEM_ID_OFFSET, FactionlessItemType.ResearchCost, 1, SC2Race.ANY, quantity=0, + classification=ItemClassification.filler), + + # Trap Filler + item_names.REDUCED_MAX_SUPPLY: + ItemData(850 + SC2WOL_ITEM_ID_OFFSET, FactionlessItemType.MaxSupplyTrap, -1, SC2Race.ANY, quantity=0, + classification=ItemClassification.trap), + + + # Nova gear + item_names.NOVA_GHOST_VISOR: + ItemData(900 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Nova_Gear, 0, SC2Race.TERRAN, classification=ItemClassification.progression), + item_names.NOVA_RANGEFINDER_OCULUS: + ItemData(901 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Nova_Gear, 1, SC2Race.TERRAN), + item_names.NOVA_DOMINATION: + ItemData(902 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Nova_Gear, 2, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.NOVA_BLINK: + ItemData(903 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Nova_Gear, 3, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE: + ItemData(904 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Progressive_2, 0, SC2Race.TERRAN, quantity=2, + classification=ItemClassification.progression), + item_names.NOVA_ENERGY_SUIT_MODULE: + ItemData(905 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Nova_Gear, 4, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.NOVA_ARMORED_SUIT_MODULE: + ItemData(906 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Nova_Gear, 5, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.NOVA_JUMP_SUIT_MODULE: + ItemData(907 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Nova_Gear, 6, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.NOVA_C20A_CANISTER_RIFLE: + ItemData(908 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Nova_Gear, 7, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.NOVA_HELLFIRE_SHOTGUN: + ItemData(909 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Nova_Gear, 8, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.NOVA_PLASMA_RIFLE: + ItemData(910 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Nova_Gear, 9, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.NOVA_MONOMOLECULAR_BLADE: + ItemData(911 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Nova_Gear, 10, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.NOVA_BLAZEFIRE_GUNBLADE: + ItemData(912 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Nova_Gear, 11, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.NOVA_STIM_INFUSION: + ItemData(913 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Nova_Gear, 12, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.NOVA_PULSE_GRENADES: + ItemData(914 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Nova_Gear, 13, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.NOVA_FLASHBANG_GRENADES: + ItemData(915 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Nova_Gear, 14, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.NOVA_IONIC_FORCE_FIELD: + ItemData(916 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Nova_Gear, 15, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.NOVA_HOLO_DECOY: + ItemData(917 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Nova_Gear, 16, SC2Race.TERRAN, + classification=ItemClassification.progression), + item_names.NOVA_NUKE: + ItemData(918 + SC2WOL_ITEM_ID_OFFSET, TerranItemType.Nova_Gear, 17, SC2Race.TERRAN, + classification=ItemClassification.progression), + + # HotS + item_names.ZERGLING: + ItemData(0 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Unit, 0, SC2Race.ZERG, + classification=ItemClassification.progression), + item_names.SWARM_QUEEN: + ItemData(1 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Unit, 1, SC2Race.ZERG, + classification=ItemClassification.progression), + item_names.ROACH: + ItemData(2 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Unit, 2, SC2Race.ZERG, + classification=ItemClassification.progression), + item_names.HYDRALISK: + ItemData(3 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Unit, 3, SC2Race.ZERG, + classification=ItemClassification.progression), + item_names.ZERGLING_BANELING_ASPECT: + ItemData(4 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Morph, 5, SC2Race.ZERG, + classification=ItemClassification.progression, parent=parent_names.MORPH_SOURCE_ZERGLING), + item_names.ABERRATION: + ItemData(5 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Unit, 5, SC2Race.ZERG, + classification=ItemClassification.progression), + item_names.MUTALISK: + ItemData(6 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Unit, 6, SC2Race.ZERG, + classification=ItemClassification.progression), + item_names.SWARM_HOST: + ItemData(7 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Unit, 7, SC2Race.ZERG, + classification=ItemClassification.progression), + item_names.INFESTOR: + ItemData(8 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Unit, 8, SC2Race.ZERG, + classification=ItemClassification.progression), + item_names.ULTRALISK: + ItemData(9 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Unit, 9, SC2Race.ZERG, + classification=ItemClassification.progression), + item_names.SPORE_CRAWLER: + ItemData(10 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Unit, 10, SC2Race.ZERG, + classification=ItemClassification.progression), + item_names.SPINE_CRAWLER: + ItemData(11 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Unit, 11, SC2Race.ZERG, + classification=ItemClassification.progression), + item_names.CORRUPTOR: + ItemData(12 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Unit, 12, SC2Race.ZERG, + classification=ItemClassification.progression), + item_names.SCOURGE: + ItemData(13 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Unit, 13, SC2Race.ZERG, + classification=ItemClassification.progression), + item_names.BROOD_QUEEN: + ItemData(14 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Unit, 4, SC2Race.ZERG, + classification=ItemClassification.progression), + item_names.DEFILER: + ItemData(15 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Unit, 14, SC2Race.ZERG, + classification=ItemClassification.progression), + item_names.INFESTED_MARINE: + ItemData(16 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Unit, 15, SC2Race.ZERG, + classification=ItemClassification.progression), + item_names.INFESTED_BUNKER: + ItemData(17 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Unit, 16, SC2Race.ZERG, + classification=ItemClassification.progression), + item_names.NYDUS_WORM: + ItemData(18 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Unit, 17, SC2Race.ZERG, + classification=ItemClassification.progression), + item_names.ECHIDNA_WORM: + ItemData(19 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Unit, 18, SC2Race.ZERG, + classification=ItemClassification.progression), + item_names.INFESTED_SIEGE_TANK: + ItemData(20 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Unit, 19, SC2Race.ZERG, + classification=ItemClassification.progression), + item_names.INFESTED_DIAMONDBACK: + ItemData(21 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Unit, 20, SC2Race.ZERG, + classification=ItemClassification.progression), + item_names.INFESTED_BANSHEE: + ItemData(22 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Unit, 21, SC2Race.ZERG, + classification=ItemClassification.progression), + item_names.INFESTED_LIBERATOR: + ItemData(23 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Unit, 22, SC2Race.ZERG, + classification=ItemClassification.progression), + item_names.INFESTED_MISSILE_TURRET: + ItemData(24 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Unit, 23, SC2Race.ZERG, + classification=ItemClassification.progression), + item_names.PYGALISK: + ItemData(25 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Unit, 24, SC2Race.ZERG, + classification=ItemClassification.progression), + item_names.BILE_LAUNCHER: + ItemData(26 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Unit, 25, SC2Race.ZERG, + classification=ItemClassification.progression), + item_names.BULLFROG: + ItemData(27 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Unit, 26, SC2Race.ZERG, + classification=ItemClassification.progression), + + item_names.PROGRESSIVE_ZERG_MELEE_ATTACK: ItemData(100 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Upgrade, 0, SC2Race.ZERG, classification=ItemClassification.progression, quantity=WEAPON_ARMOR_UPGRADE_MAX_LEVEL, parent=parent_names.ZERG_MELEE_ATTACKER), + item_names.PROGRESSIVE_ZERG_MISSILE_ATTACK: ItemData(101 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Upgrade, 4, SC2Race.ZERG, classification=ItemClassification.progression, quantity=WEAPON_ARMOR_UPGRADE_MAX_LEVEL, parent=parent_names.ZERG_MISSILE_ATTACKER), + item_names.PROGRESSIVE_ZERG_GROUND_CARAPACE: ItemData(102 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Upgrade, 8, SC2Race.ZERG, classification=ItemClassification.progression, quantity=WEAPON_ARMOR_UPGRADE_MAX_LEVEL, parent=parent_names.ZERG_CARAPACE_UNIT), + item_names.PROGRESSIVE_ZERG_FLYER_ATTACK: ItemData(103 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Upgrade, 12, SC2Race.ZERG, classification=ItemClassification.progression, quantity=WEAPON_ARMOR_UPGRADE_MAX_LEVEL, parent=parent_names.ZERG_FLYING_UNIT), + item_names.PROGRESSIVE_ZERG_FLYER_CARAPACE: ItemData(104 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Upgrade, 16, SC2Race.ZERG, classification=ItemClassification.progression, quantity=WEAPON_ARMOR_UPGRADE_MAX_LEVEL, parent=parent_names.ZERG_FLYING_UNIT), + # Bundles + item_names.PROGRESSIVE_ZERG_WEAPON_UPGRADE: ItemData(105 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Upgrade, -1, SC2Race.ZERG, classification=ItemClassification.progression, quantity=WEAPON_ARMOR_UPGRADE_MAX_LEVEL), + item_names.PROGRESSIVE_ZERG_ARMOR_UPGRADE: ItemData(106 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Upgrade, -1, SC2Race.ZERG, classification=ItemClassification.progression, quantity=WEAPON_ARMOR_UPGRADE_MAX_LEVEL), + item_names.PROGRESSIVE_ZERG_GROUND_UPGRADE: ItemData(107 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Upgrade, -1, SC2Race.ZERG, classification=ItemClassification.progression, quantity=WEAPON_ARMOR_UPGRADE_MAX_LEVEL, parent=parent_names.ZERG_CARAPACE_UNIT), + item_names.PROGRESSIVE_ZERG_FLYER_UPGRADE: ItemData(108 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Upgrade, -1, SC2Race.ZERG, classification=ItemClassification.progression, quantity=WEAPON_ARMOR_UPGRADE_MAX_LEVEL, parent=parent_names.ZERG_FLYING_UNIT), + item_names.PROGRESSIVE_ZERG_WEAPON_ARMOR_UPGRADE: ItemData(109 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Upgrade, -1, SC2Race.ZERG, classification=ItemClassification.progression, quantity=WEAPON_ARMOR_UPGRADE_MAX_LEVEL), + + item_names.ZERGLING_HARDENED_CARAPACE: + ItemData(200 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_1, 0, SC2Race.ZERG, parent=item_names.ZERGLING), + item_names.ZERGLING_ADRENAL_OVERLOAD: + ItemData(201 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_1, 1, SC2Race.ZERG, classification=ItemClassification.progression, parent=item_names.ZERGLING), + item_names.ZERGLING_METABOLIC_BOOST: + ItemData(202 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_1, 2, SC2Race.ZERG, classification=ItemClassification.progression, parent=item_names.ZERGLING), + item_names.ROACH_HYDRIODIC_BILE: + ItemData(203 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_1, 3, SC2Race.ZERG, classification=ItemClassification.progression, parent=item_names.ROACH), + item_names.ROACH_ADAPTIVE_PLATING: + ItemData(204 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_1, 4, SC2Race.ZERG, classification=ItemClassification.progression, parent=item_names.ROACH), + item_names.ROACH_TUNNELING_CLAWS: + ItemData(205 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_1, 5, SC2Race.ZERG, parent=item_names.ROACH), + item_names.HYDRALISK_FRENZY: + ItemData(206 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_1, 6, SC2Race.ZERG, classification=ItemClassification.progression, parent=item_names.HYDRALISK), + item_names.HYDRALISK_ANCILLARY_CARAPACE: + ItemData(207 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_1, 7, SC2Race.ZERG, parent=item_names.HYDRALISK), + item_names.HYDRALISK_GROOVED_SPINES: + ItemData(208 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_1, 8, SC2Race.ZERG, parent=item_names.HYDRALISK), + item_names.BANELING_CORROSIVE_ACID: + ItemData(209 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_1, 9, SC2Race.ZERG, + classification=ItemClassification.progression, parent=parent_names.BANELING_SOURCE), + item_names.BANELING_RUPTURE: + ItemData(210 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_1, 10, SC2Race.ZERG, + parent=parent_names.BANELING_SOURCE), + item_names.BANELING_REGENERATIVE_ACID: + ItemData(211 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_1, 11, SC2Race.ZERG, + parent=parent_names.BANELING_SOURCE), + item_names.MUTALISK_VICIOUS_GLAIVE: + ItemData(212 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_1, 12, SC2Race.ZERG, classification=ItemClassification.progression, parent=item_names.MUTALISK), + item_names.MUTALISK_RAPID_REGENERATION: + ItemData(213 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_1, 13, SC2Race.ZERG, classification=ItemClassification.progression, parent=item_names.MUTALISK), + item_names.MUTALISK_SUNDERING_GLAIVE: + ItemData(214 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_1, 14, SC2Race.ZERG, classification=ItemClassification.progression, parent=item_names.MUTALISK), + item_names.SWARM_HOST_BURROW: + ItemData(215 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_1, 15, SC2Race.ZERG, parent=item_names.SWARM_HOST), + item_names.SWARM_HOST_RAPID_INCUBATION: + ItemData(216 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_1, 16, SC2Race.ZERG, parent=item_names.SWARM_HOST), + item_names.SWARM_HOST_PRESSURIZED_GLANDS: + ItemData(217 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_1, 17, SC2Race.ZERG, classification=ItemClassification.progression, parent=item_names.SWARM_HOST), + item_names.ULTRALISK_BURROW_CHARGE: + ItemData(218 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_1, 18, SC2Race.ZERG, parent=item_names.ULTRALISK), + item_names.ULTRALISK_TISSUE_ASSIMILATION: + ItemData(219 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_1, 19, SC2Race.ZERG, parent=item_names.ULTRALISK), + item_names.ULTRALISK_MONARCH_BLADES: + ItemData(220 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_1, 20, SC2Race.ZERG, classification=ItemClassification.progression, parent=item_names.ULTRALISK), + item_names.CORRUPTOR_CAUSTIC_SPRAY: + ItemData(221 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_1, 21, SC2Race.ZERG, parent=item_names.CORRUPTOR), + item_names.CORRUPTOR_CORRUPTION: + ItemData(222 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_1, 22, SC2Race.ZERG, parent=item_names.CORRUPTOR), + item_names.SCOURGE_VIRULENT_SPORES: + ItemData(223 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_1, 23, SC2Race.ZERG, parent=item_names.SCOURGE), + item_names.SCOURGE_RESOURCE_EFFICIENCY: + ItemData(224 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_1, 24, SC2Race.ZERG, classification=ItemClassification.progression, parent=item_names.SCOURGE), + item_names.SCOURGE_SWARM_SCOURGE: + ItemData(225 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_1, 25, SC2Race.ZERG, parent=item_names.SCOURGE), + item_names.ZERGLING_SHREDDING_CLAWS: + ItemData(226 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_1, 26, SC2Race.ZERG, classification=ItemClassification.progression, parent=item_names.ZERGLING), + item_names.ROACH_GLIAL_RECONSTITUTION: + ItemData(227 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_1, 27, SC2Race.ZERG, classification=ItemClassification.progression, parent=item_names.ROACH), + item_names.ROACH_ORGANIC_CARAPACE: + ItemData(228 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_1, 28, SC2Race.ZERG, parent=item_names.ROACH), + item_names.HYDRALISK_MUSCULAR_AUGMENTS: + ItemData(229 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_1, 29, SC2Race.ZERG, classification=ItemClassification.progression, parent=item_names.HYDRALISK), + item_names.HYDRALISK_RESOURCE_EFFICIENCY: + ItemData(230 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_2, 0, SC2Race.ZERG, classification=ItemClassification.progression, parent=item_names.HYDRALISK), + item_names.BANELING_CENTRIFUGAL_HOOKS: + ItemData(231 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_2, 1, SC2Race.ZERG, + parent=parent_names.BANELING_SOURCE), + item_names.BANELING_TUNNELING_JAWS: + ItemData(232 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_2, 2, SC2Race.ZERG, + parent=parent_names.BANELING_SOURCE), + item_names.BANELING_RAPID_METAMORPH: + ItemData(233 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_2, 3, SC2Race.ZERG, + parent=item_names.ZERGLING_BANELING_ASPECT), + item_names.MUTALISK_SEVERING_GLAIVE: + ItemData(234 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_2, 4, SC2Race.ZERG, classification=ItemClassification.progression, parent=item_names.MUTALISK), + item_names.MUTALISK_AERODYNAMIC_GLAIVE_SHAPE: + ItemData(235 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_2, 5, SC2Race.ZERG, classification=ItemClassification.progression, parent=item_names.MUTALISK), + item_names.SWARM_HOST_LOCUST_METABOLIC_BOOST: + ItemData(236 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_2, 6, SC2Race.ZERG, parent=item_names.SWARM_HOST), + item_names.SWARM_HOST_ENDURING_LOCUSTS: + ItemData(237 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_2, 7, SC2Race.ZERG, parent=item_names.SWARM_HOST), + item_names.SWARM_HOST_ORGANIC_CARAPACE: + ItemData(238 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_2, 8, SC2Race.ZERG, parent=item_names.SWARM_HOST), + item_names.SWARM_HOST_RESOURCE_EFFICIENCY: + ItemData(239 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_2, 9, SC2Race.ZERG, classification=ItemClassification.progression_skip_balancing, parent=item_names.SWARM_HOST), + item_names.ULTRALISK_ANABOLIC_SYNTHESIS: + ItemData(240 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_2, 10, SC2Race.ZERG, parent=item_names.ULTRALISK), + item_names.ULTRALISK_CHITINOUS_PLATING: + ItemData(241 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_2, 11, SC2Race.ZERG, classification=ItemClassification.progression, parent=item_names.ULTRALISK), + item_names.ULTRALISK_ORGANIC_CARAPACE: + ItemData(242 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_2, 12, SC2Race.ZERG, parent=item_names.ULTRALISK), + item_names.ULTRALISK_RESOURCE_EFFICIENCY: + ItemData(243 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_2, 13, SC2Race.ZERG, parent=item_names.ULTRALISK), + item_names.DEVOURER_CORROSIVE_SPRAY: + ItemData(244 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_2, 14, SC2Race.ZERG, + parent=item_names.MUTALISK_CORRUPTOR_DEVOURER_ASPECT), + item_names.DEVOURER_GAPING_MAW: + ItemData(245 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_2, 15, SC2Race.ZERG, + parent=item_names.MUTALISK_CORRUPTOR_DEVOURER_ASPECT), + item_names.DEVOURER_IMPROVED_OSMOSIS: + ItemData(246 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_2, 16, SC2Race.ZERG, + parent=item_names.MUTALISK_CORRUPTOR_DEVOURER_ASPECT), + item_names.DEVOURER_PRESCIENT_SPORES: + ItemData(247 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_2, 17, SC2Race.ZERG, + parent=item_names.MUTALISK_CORRUPTOR_DEVOURER_ASPECT, + classification=ItemClassification.progression), + item_names.GUARDIAN_PROLONGED_DISPERSION: + ItemData(248 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_2, 18, SC2Race.ZERG, + parent=item_names.MUTALISK_CORRUPTOR_GUARDIAN_ASPECT), + item_names.GUARDIAN_PRIMAL_ADAPTATION: + ItemData(249 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_2, 19, SC2Race.ZERG, + parent=item_names.MUTALISK_CORRUPTOR_GUARDIAN_ASPECT, + classification=ItemClassification.progression), + item_names.GUARDIAN_SORONAN_ACID: + ItemData(250 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_2, 20, SC2Race.ZERG, + classification=ItemClassification.progression, parent=item_names.MUTALISK_CORRUPTOR_GUARDIAN_ASPECT), + item_names.IMPALER_ADAPTIVE_TALONS: + ItemData(251 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_2, 21, SC2Race.ZERG, + parent=item_names.HYDRALISK_IMPALER_ASPECT), + item_names.IMPALER_SECRETION_GLANDS: + ItemData(252 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_2, 22, SC2Race.ZERG, + parent=item_names.HYDRALISK_IMPALER_ASPECT), + item_names.IMPALER_SUNKEN_SPINES: + ItemData(253 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_2, 23, SC2Race.ZERG, + classification=ItemClassification.progression, parent=item_names.HYDRALISK_IMPALER_ASPECT), + item_names.LURKER_SEISMIC_SPINES: + ItemData(254 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_2, 24, SC2Race.ZERG, + classification=ItemClassification.progression, parent=item_names.HYDRALISK_LURKER_ASPECT), + item_names.LURKER_ADAPTED_SPINES: + ItemData(255 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_2, 25, SC2Race.ZERG, + classification=ItemClassification.progression, parent=item_names.HYDRALISK_LURKER_ASPECT), + item_names.RAVAGER_POTENT_BILE: + ItemData(256 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_2, 26, SC2Race.ZERG, + parent=item_names.ROACH_RAVAGER_ASPECT), + item_names.RAVAGER_BLOATED_BILE_DUCTS: + ItemData(257 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_2, 27, SC2Race.ZERG, + parent=item_names.ROACH_RAVAGER_ASPECT), + item_names.RAVAGER_DEEP_TUNNEL: + ItemData(258 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_2, 28, SC2Race.ZERG, + classification=ItemClassification.progression_skip_balancing, parent=item_names.ROACH_RAVAGER_ASPECT), + item_names.VIPER_PARASITIC_BOMB: + ItemData(259 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_2, 29, SC2Race.ZERG, + parent=item_names.MUTALISK_CORRUPTOR_VIPER_ASPECT, + classification=ItemClassification.progression), + item_names.VIPER_PARALYTIC_BARBS: + ItemData(260 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_3, 0, SC2Race.ZERG, + parent=item_names.MUTALISK_CORRUPTOR_VIPER_ASPECT), + item_names.VIPER_VIRULENT_MICROBES: + ItemData(261 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_3, 1, SC2Race.ZERG, + parent=item_names.MUTALISK_CORRUPTOR_VIPER_ASPECT), + item_names.BROOD_LORD_POROUS_CARTILAGE: + ItemData(262 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_3, 2, SC2Race.ZERG, + parent=item_names.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT), + item_names.BROOD_LORD_BEHEMOTH_STELLARSKIN: + ItemData(263 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_3, 3, SC2Race.ZERG, + parent=item_names.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT), + item_names.BROOD_LORD_SPLITTER_MITOSIS: + ItemData(264 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_3, 4, SC2Race.ZERG, + parent=item_names.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT), + item_names.BROOD_LORD_RESOURCE_EFFICIENCY: + ItemData(265 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_3, 5, SC2Race.ZERG, + parent=item_names.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT), + item_names.INFESTOR_INFESTED_TERRAN: + ItemData(266 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_3, 6, SC2Race.ZERG, classification=ItemClassification.progression, parent=item_names.INFESTOR), + item_names.INFESTOR_MICROBIAL_SHROUD: + ItemData(267 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_3, 7, SC2Race.ZERG, parent=item_names.INFESTOR), + item_names.SWARM_QUEEN_SPAWN_LARVAE: + ItemData(268 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_3, 8, SC2Race.ZERG, parent=item_names.SWARM_QUEEN), + item_names.SWARM_QUEEN_DEEP_TUNNEL: + ItemData(269 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_3, 9, SC2Race.ZERG, classification=ItemClassification.progression_skip_balancing, parent=item_names.SWARM_QUEEN), + item_names.SWARM_QUEEN_ORGANIC_CARAPACE: + ItemData(270 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_3, 10, SC2Race.ZERG, parent=item_names.SWARM_QUEEN), + item_names.SWARM_QUEEN_BIO_MECHANICAL_TRANSFUSION: + ItemData(271 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_3, 11, SC2Race.ZERG, classification=ItemClassification.progression, parent=item_names.SWARM_QUEEN), + item_names.SWARM_QUEEN_RESOURCE_EFFICIENCY: + ItemData(272 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_3, 12, SC2Race.ZERG, classification=ItemClassification.progression, parent=item_names.SWARM_QUEEN), + item_names.SWARM_QUEEN_INCUBATOR_CHAMBER: + ItemData(273 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_3, 13, SC2Race.ZERG, parent=item_names.SWARM_QUEEN), + item_names.BROOD_QUEEN_FUNGAL_GROWTH: + ItemData(274 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_3, 14, SC2Race.ZERG, parent=item_names.BROOD_QUEEN), + item_names.BROOD_QUEEN_ENSNARE: + ItemData(275 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_3, 15, SC2Race.ZERG, parent=item_names.BROOD_QUEEN), + item_names.BROOD_QUEEN_ENHANCED_MITOCHONDRIA: + ItemData(276 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_3, 16, SC2Race.ZERG, parent=item_names.BROOD_QUEEN), + item_names.DEFILER_PATHOGEN_PROJECTORS: + ItemData(277 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_3, 17, SC2Race.ZERG, parent=item_names.DEFILER), + item_names.DEFILER_TRAPDOOR_ADAPTATION: + ItemData(278 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_3, 18, SC2Race.ZERG, parent=item_names.DEFILER), + item_names.DEFILER_PREDATORY_CONSUMPTION: + ItemData(279 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_3, 19, SC2Race.ZERG, parent=item_names.DEFILER), + item_names.DEFILER_COMORBIDITY: + ItemData(280 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_3, 20, SC2Race.ZERG, parent=item_names.DEFILER), + item_names.ABERRATION_MONSTROUS_RESILIENCE: + ItemData(281 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_3, 21, SC2Race.ZERG, parent=item_names.ABERRATION), + item_names.ABERRATION_CONSTRUCT_REGENERATION: + ItemData(282 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_3, 22, SC2Race.ZERG, parent=item_names.ABERRATION), + item_names.ABERRATION_BANELING_INCUBATION: + ItemData(283 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_3, 23, SC2Race.ZERG, parent=item_names.ABERRATION), + item_names.ABERRATION_PROTECTIVE_COVER: + ItemData(284 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_3, 24, SC2Race.ZERG, parent=item_names.ABERRATION), + item_names.ABERRATION_RESOURCE_EFFICIENCY: + ItemData(285 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_3, 25, SC2Race.ZERG, parent=item_names.ABERRATION), + item_names.CORRUPTOR_MONSTROUS_RESILIENCE: + ItemData(286 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_3, 26, SC2Race.ZERG, parent=item_names.CORRUPTOR), + item_names.CORRUPTOR_CONSTRUCT_REGENERATION: + ItemData(287 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_3, 27, SC2Race.ZERG, parent=item_names.CORRUPTOR), + item_names.CORRUPTOR_SCOURGE_INCUBATION: + ItemData(288 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_3, 28, SC2Race.ZERG, parent=item_names.CORRUPTOR), + item_names.CORRUPTOR_RESOURCE_EFFICIENCY: + ItemData(289 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_3, 29, SC2Race.ZERG, parent=item_names.CORRUPTOR), + item_names.PRIMAL_IGNITER_CONCENTRATED_FIRE: + ItemData(290 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_4, 0, SC2Race.ZERG, parent=item_names.ROACH_PRIMAL_IGNITER_ASPECT), + item_names.PRIMAL_IGNITER_PRIMAL_TENACITY: + ItemData(291 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_4, 1, SC2Race.ZERG, classification=ItemClassification.progression, parent=item_names.ROACH_PRIMAL_IGNITER_ASPECT), + item_names.INFESTED_SCV_BUILD_CHARGES: + ItemData(292 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_4, 2, SC2Race.ZERG, parent=parent_names.INFESTED_UNITS), + item_names.INFESTED_MARINE_PLAGUED_MUNITIONS: + ItemData(293 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_4, 3, SC2Race.ZERG, parent=item_names.INFESTED_MARINE), + item_names.INFESTED_MARINE_RETINAL_AUGMENTATION: + ItemData(294 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_4, 4, SC2Race.ZERG, parent=item_names.INFESTED_MARINE), + item_names.INFESTED_BUNKER_CALCIFIED_ARMOR: + ItemData(295 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_4, 6, SC2Race.ZERG, parent=item_names.INFESTED_BUNKER), + item_names.INFESTED_BUNKER_REGENERATIVE_PLATING: + ItemData(296 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_4, 5, SC2Race.ZERG, parent=item_names.INFESTED_BUNKER), + item_names.INFESTED_BUNKER_ENGORGED_BUNKERS: + ItemData(297 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_4, 7, SC2Race.ZERG, parent=item_names.INFESTED_BUNKER), + item_names.INFESTED_MISSILE_TURRET_BIOELECTRIC_PAYLOAD: + ItemData(298 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_5, 6, SC2Race.ZERG, parent=item_names.INFESTED_MISSILE_TURRET), + item_names.INFESTED_MISSILE_TURRET_ACID_SPORE_VENTS: + ItemData(299 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_5, 7, SC2Race.ZERG, parent=item_names.INFESTED_MISSILE_TURRET), + + item_names.ZERGLING_RAPTOR_STRAIN: + ItemData(300 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Strain, 0, SC2Race.ZERG, classification=ItemClassification.progression, parent=item_names.ZERGLING), + item_names.ZERGLING_SWARMLING_STRAIN: + ItemData(301 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Strain, 1, SC2Race.ZERG, parent=item_names.ZERGLING), + item_names.ROACH_VILE_STRAIN: + ItemData(302 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Strain, 2, SC2Race.ZERG, parent=item_names.ROACH), + item_names.ROACH_CORPSER_STRAIN: + ItemData(303 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Strain, 3, SC2Race.ZERG, classification=ItemClassification.progression, parent=item_names.ROACH), + item_names.HYDRALISK_IMPALER_ASPECT: + ItemData(304 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Morph, 0, SC2Race.ZERG, + classification=ItemClassification.progression, parent=parent_names.MORPH_SOURCE_HYDRALISK), + item_names.HYDRALISK_LURKER_ASPECT: + ItemData(305 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Morph, 1, SC2Race.ZERG, + classification=ItemClassification.progression, parent=parent_names.MORPH_SOURCE_HYDRALISK), + item_names.BANELING_SPLITTER_STRAIN: + ItemData(306 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Strain, 6, SC2Race.ZERG, classification=ItemClassification.progression, parent=parent_names.BANELING_SOURCE), + item_names.BANELING_HUNTER_STRAIN: + ItemData(307 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Strain, 7, SC2Race.ZERG, parent=parent_names.BANELING_SOURCE), + item_names.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT: + ItemData(308 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Morph, 2, SC2Race.ZERG, + classification=ItemClassification.progression, parent=parent_names.MORPH_SOURCE_AIR), + item_names.MUTALISK_CORRUPTOR_VIPER_ASPECT: + ItemData(309 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Morph, 3, SC2Race.ZERG, + classification=ItemClassification.progression, parent=parent_names.MORPH_SOURCE_AIR), + item_names.SWARM_HOST_CARRION_STRAIN: + ItemData(310 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Strain, 10, SC2Race.ZERG, parent=item_names.SWARM_HOST), + item_names.SWARM_HOST_CREEPER_STRAIN: + ItemData(311 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Strain, 11, SC2Race.ZERG, parent=item_names.SWARM_HOST), + item_names.ULTRALISK_NOXIOUS_STRAIN: + ItemData(312 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Strain, 12, SC2Race.ZERG, parent=item_names.ULTRALISK), + item_names.ULTRALISK_TORRASQUE_STRAIN: + ItemData(313 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Strain, 13, SC2Race.ZERG, classification=ItemClassification.progression, parent=item_names.ULTRALISK), + + item_names.TYRANNOZOR_TYRANTS_PROTECTION: + ItemData(350 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_4, 8, SC2Race.ZERG, parent=item_names.ULTRALISK_TYRANNOZOR_ASPECT), + item_names.TYRANNOZOR_BARRAGE_OF_SPIKES: + ItemData(351 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_4, 9, SC2Race.ZERG, parent=item_names.ULTRALISK_TYRANNOZOR_ASPECT), + item_names.TYRANNOZOR_IMPALING_STRIKE: + ItemData(352 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_4, 10, SC2Race.ZERG, parent=item_names.ULTRALISK_TYRANNOZOR_ASPECT), + item_names.TYRANNOZOR_HEALING_ADAPTATION: + ItemData(353 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_4, 11, SC2Race.ZERG, classification=ItemClassification.progression, parent=item_names.ULTRALISK_TYRANNOZOR_ASPECT), + item_names.NYDUS_WORM_ECHIDNA_WORM_SUBTERRANEAN_SCALES: + ItemData(354 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_4, 12, SC2Race.ZERG, parent=parent_names.ANY_NYDUS_WORM), + item_names.NYDUS_WORM_ECHIDNA_WORM_JORMUNGANDR_STRAIN: + ItemData(355 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_4, 13, SC2Race.ZERG, parent=parent_names.ANY_NYDUS_WORM), + item_names.NYDUS_WORM_ECHIDNA_WORM_RESOURCE_EFFICIENCY: + ItemData(356 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_4, 14, SC2Race.ZERG, parent=parent_names.ANY_NYDUS_WORM), + item_names.ECHIDNA_WORM_OUROBOROS_STRAIN: + ItemData(357 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_4, 15, SC2Race.ZERG, parent=parent_names.ZERG_OUROBOUROS_CONDITION), + item_names.NYDUS_WORM_RAVENOUS_APPETITE: + ItemData(358 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_4, 16, SC2Race.ZERG, parent=item_names.NYDUS_WORM), + item_names.INFESTED_SIEGE_TANK_PROGRESSIVE_AUTOMATED_MITOSIS: + ItemData(359 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Progressive, 0, SC2Race.ZERG, + classification=ItemClassification.progression, parent=item_names.INFESTED_SIEGE_TANK, quantity=2), + item_names.INFESTED_SIEGE_TANK_ACIDIC_ENZYMES: + ItemData(360 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_4, 17, SC2Race.ZERG, parent=item_names.INFESTED_SIEGE_TANK), + item_names.INFESTED_SIEGE_TANK_DEEP_TUNNEL: + ItemData(361 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_4, 18, SC2Race.ZERG, classification=ItemClassification.progression_skip_balancing, parent=item_names.INFESTED_SIEGE_TANK), + item_names.INFESTED_DIAMONDBACK_CAUSTIC_MUCUS: + ItemData(362 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_4, 19, SC2Race.ZERG, parent=item_names.INFESTED_DIAMONDBACK), + item_names.INFESTED_DIAMONDBACK_VIOLENT_ENZYMES: + ItemData(363 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_4, 20, SC2Race.ZERG, parent=item_names.INFESTED_DIAMONDBACK), + item_names.INFESTED_BANSHEE_BRACED_EXOSKELETON: + ItemData(364 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_4, 21, SC2Race.ZERG, parent=item_names.INFESTED_BANSHEE), + item_names.INFESTED_BANSHEE_RAPID_HIBERNATION: + ItemData(365 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_4, 22, SC2Race.ZERG, classification=ItemClassification.progression, parent=item_names.INFESTED_BANSHEE), + item_names.INFESTED_LIBERATOR_CLOUD_DISPERSAL: + ItemData(366 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_4, 23, SC2Race.ZERG, classification=ItemClassification.progression, parent=item_names.INFESTED_LIBERATOR), + item_names.INFESTED_LIBERATOR_VIRAL_CONTAMINATION: + ItemData(367 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_4, 24, SC2Race.ZERG, parent=item_names.INFESTED_LIBERATOR), + item_names.GUARDIAN_PROPELLANT_SACS: + ItemData(368 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_4, 25, SC2Race.ZERG, classification=ItemClassification.progression, parent=item_names.MUTALISK_CORRUPTOR_GUARDIAN_ASPECT), + item_names.GUARDIAN_EXPLOSIVE_SPORES: + ItemData(369 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_4, 26, SC2Race.ZERG, classification=ItemClassification.progression, parent=item_names.MUTALISK_CORRUPTOR_GUARDIAN_ASPECT), + item_names.GUARDIAN_PRIMORDIAL_FURY: + ItemData(370 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_4, 27, SC2Race.ZERG, classification=ItemClassification.progression, parent=item_names.MUTALISK_CORRUPTOR_GUARDIAN_ASPECT), + item_names.INFESTED_SIEGE_TANK_SEISMIC_SONAR: + ItemData(371 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_4, 28, SC2Race.ZERG, parent=item_names.INFESTED_SIEGE_TANK), + item_names.INFESTED_BANSHEE_FLESHFUSED_TARGETING_OPTICS: + ItemData(372 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_4, 29, SC2Race.ZERG, classification=ItemClassification.progression, parent=item_names.INFESTED_BANSHEE), + item_names.INFESTED_SIEGE_TANK_BALANCED_ROOTS: + ItemData(373 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_5, 0, SC2Race.ZERG, parent=item_names.INFESTED_SIEGE_TANK), + item_names.INFESTED_DIAMONDBACK_PROGRESSIVE_FUNGAL_SNARE: + ItemData(374 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Progressive, 2, SC2Race.ZERG, + classification=ItemClassification.progression, parent=item_names.INFESTED_DIAMONDBACK, quantity=2), + item_names.INFESTED_DIAMONDBACK_CONCENTRATED_SPEW: + ItemData(375 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_5, 1, SC2Race.ZERG, parent=item_names.INFESTED_DIAMONDBACK), + item_names.INFESTED_SIEGE_TANK_FRIGHTFUL_FLESHWELDER: + ItemData(376 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_5, 2, SC2Race.ZERG, parent=item_names.INFESTED_SIEGE_TANK), + item_names.INFESTED_DIAMONDBACK_FRIGHTFUL_FLESHWELDER: + ItemData(377 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_5, 3, SC2Race.ZERG, parent=item_names.INFESTED_DIAMONDBACK), + item_names.INFESTED_BANSHEE_FRIGHTFUL_FLESHWELDER: + ItemData(378 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_5, 4, SC2Race.ZERG, parent=item_names.INFESTED_BANSHEE), + item_names.INFESTED_LIBERATOR_FRIGHTFUL_FLESHWELDER: + ItemData(379 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_5, 5, SC2Race.ZERG, parent=item_names.INFESTED_LIBERATOR), + item_names.INFESTED_LIBERATOR_DEFENDER_MODE: + ItemData(380 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_5, 8, SC2Race.ZERG, parent=item_names.INFESTED_LIBERATOR, + classification=ItemClassification.progression), + item_names.ABERRATION_PROGRESSIVE_BANELING_LAUNCH: + ItemData(381 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Progressive, 4, SC2Race.ZERG, classification=ItemClassification.progression, parent=item_names.ABERRATION, quantity=2), + item_names.PYGALISK_STIM: + ItemData(382 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_5, 9, SC2Race.ZERG, parent=item_names.PYGALISK), + item_names.PYGALISK_DUCAL_BLADES: + ItemData(383 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_5, 10, SC2Race.ZERG, parent=item_names.PYGALISK), + item_names.PYGALISK_COMBAT_CARAPACE: + ItemData(384 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_5, 11, SC2Race.ZERG, parent=item_names.PYGALISK), + item_names.BILE_LAUNCHER_ARTILLERY_DUCTS: + ItemData(385 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_5, 12, SC2Race.ZERG, parent=item_names.BILE_LAUNCHER), + item_names.BILE_LAUNCHER_RAPID_BOMBARMENT: + ItemData(386 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_5, 13, SC2Race.ZERG, classification=ItemClassification.progression, parent=item_names.BILE_LAUNCHER), + item_names.BULLFROG_WILD_MUTATION: + ItemData(387 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_5, 14, SC2Race.ZERG, parent=item_names.BULLFROG), + item_names.BULLFROG_BROODLINGS: + ItemData(388 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_5, 15, SC2Race.ZERG, parent=item_names.BULLFROG), + item_names.BULLFROG_HARD_IMPACT: + ItemData(389 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_5, 16, SC2Race.ZERG, parent=item_names.BULLFROG), + item_names.BULLFROG_RANGE: + ItemData(390 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_5, 17, SC2Race.ZERG, parent=item_names.BULLFROG), + item_names.SPORE_CRAWLER_BIO_BONUS: + ItemData(391 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mutation_5, 18, SC2Race.ZERG, parent=item_names.SPORE_CRAWLER), + + item_names.KERRIGAN_KINETIC_BLAST: ItemData(400 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Ability, 0, SC2Race.ZERG, classification=ItemClassification.progression), + item_names.KERRIGAN_HEROIC_FORTITUDE: ItemData(401 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Ability, 1, SC2Race.ZERG, classification=ItemClassification.progression), + item_names.KERRIGAN_LEAPING_STRIKE: ItemData(402 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Ability, 2, SC2Race.ZERG, classification=ItemClassification.progression), + item_names.KERRIGAN_CRUSHING_GRIP: ItemData(403 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Ability, 3, SC2Race.ZERG, classification=ItemClassification.progression), + item_names.KERRIGAN_CHAIN_REACTION: ItemData(404 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Ability, 4, SC2Race.ZERG, classification=ItemClassification.progression), + item_names.KERRIGAN_PSIONIC_SHIFT: ItemData(405 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Ability, 5, SC2Race.ZERG, classification=ItemClassification.progression), + item_names.ZERGLING_RECONSTITUTION: ItemData(406 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Evolution_Pit, 0, SC2Race.ZERG, parent=item_names.ZERGLING), + item_names.OVERLORD_IMPROVED_OVERLORDS: ItemData(407 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Evolution_Pit, 1, SC2Race.ZERG, classification=ItemClassification.progression), + item_names.AUTOMATED_EXTRACTORS: ItemData(408 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Evolution_Pit, 2, SC2Race.ZERG, classification=ItemClassification.progression), + item_names.KERRIGAN_WILD_MUTATION: ItemData(409 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Ability, 6, SC2Race.ZERG, classification=ItemClassification.progression), + item_names.KERRIGAN_SPAWN_BANELINGS: ItemData(410 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Ability, 7, SC2Race.ZERG, classification=ItemClassification.progression), + item_names.KERRIGAN_MEND: ItemData(411 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Ability, 8, SC2Race.ZERG, classification=ItemClassification.progression), + item_names.TWIN_DRONES: ItemData(412 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Evolution_Pit, 3, SC2Race.ZERG, classification=ItemClassification.progression), + item_names.MALIGNANT_CREEP: ItemData(413 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Evolution_Pit, 4, SC2Race.ZERG, classification=ItemClassification.progression), + item_names.VESPENE_EFFICIENCY: ItemData(414 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Evolution_Pit, 5, SC2Race.ZERG, classification=ItemClassification.progression), + item_names.KERRIGAN_INFEST_BROODLINGS: ItemData(415 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Ability, 9, SC2Race.ZERG, classification=ItemClassification.progression), + item_names.KERRIGAN_FURY: ItemData(416 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Ability, 10, SC2Race.ZERG, classification=ItemClassification.progression), + item_names.KERRIGAN_ABILITY_EFFICIENCY: ItemData(417 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Ability, 11, SC2Race.ZERG, classification=ItemClassification.progression), + item_names.KERRIGAN_APOCALYPSE: ItemData(418 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Ability, 12, SC2Race.ZERG, classification=ItemClassification.progression), + item_names.KERRIGAN_SPAWN_LEVIATHAN: ItemData(419 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Ability, 13, SC2Race.ZERG, classification=ItemClassification.progression), + item_names.KERRIGAN_DROP_PODS: ItemData(420 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Ability, 14, SC2Race.ZERG, classification=ItemClassification.progression), + # Handled separately from other abilities + item_names.KERRIGAN_PRIMAL_FORM: ItemData(421 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Primal_Form, 0, SC2Race.ZERG), + item_names.KERRIGAN_ASSIMILATION_AURA: ItemData(422 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Ability, 15, SC2Race.ZERG), + item_names.KERRIGAN_IMMOBILIZATION_WAVE: ItemData(423 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Ability, 16, SC2Race.ZERG, classification=ItemClassification.progression), + + item_names.KERRIGAN_LEVELS_10: ItemData(500 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Level, 10, SC2Race.ZERG, quantity=0, classification=ItemClassification.progression), + item_names.KERRIGAN_LEVELS_9: ItemData(501 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Level, 9, SC2Race.ZERG, quantity=0, classification=ItemClassification.progression), + item_names.KERRIGAN_LEVELS_8: ItemData(502 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Level, 8, SC2Race.ZERG, quantity=0, classification=ItemClassification.progression), + item_names.KERRIGAN_LEVELS_7: ItemData(503 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Level, 7, SC2Race.ZERG, quantity=0, classification=ItemClassification.progression), + item_names.KERRIGAN_LEVELS_6: ItemData(504 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Level, 6, SC2Race.ZERG, quantity=0, classification=ItemClassification.progression), + item_names.KERRIGAN_LEVELS_5: ItemData(505 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Level, 5, SC2Race.ZERG, quantity=0, classification=ItemClassification.progression), + item_names.KERRIGAN_LEVELS_4: ItemData(506 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Level, 4, SC2Race.ZERG, quantity=0, classification=ItemClassification.progression_skip_balancing), + item_names.KERRIGAN_LEVELS_3: ItemData(507 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Level, 3, SC2Race.ZERG, quantity=0, classification=ItemClassification.progression_skip_balancing), + item_names.KERRIGAN_LEVELS_2: ItemData(508 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Level, 2, SC2Race.ZERG, quantity=0, classification=ItemClassification.progression_skip_balancing), + item_names.KERRIGAN_LEVELS_1: ItemData(509 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Level, 1, SC2Race.ZERG, quantity=0, classification=ItemClassification.progression_skip_balancing), + item_names.KERRIGAN_LEVELS_14: ItemData(510 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Level, 14, SC2Race.ZERG, quantity=0, classification=ItemClassification.progression), + item_names.KERRIGAN_LEVELS_35: ItemData(511 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Level, 35, SC2Race.ZERG, quantity=0, classification=ItemClassification.progression), + item_names.KERRIGAN_LEVELS_70: ItemData(512 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Level, 70, SC2Race.ZERG, quantity=0, classification=ItemClassification.progression), + + # Zerg Mercs + item_names.INFESTED_MEDICS: ItemData(600 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mercenary, 0, SC2Race.ZERG, classification=ItemClassification.progression_skip_balancing), + item_names.INFESTED_SIEGE_BREAKERS: ItemData(601 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mercenary, 1, SC2Race.ZERG, classification=ItemClassification.progression_skip_balancing), + item_names.INFESTED_DUSK_WINGS: ItemData(602 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mercenary, 2, SC2Race.ZERG, classification=ItemClassification.progression_skip_balancing), + item_names.DEVOURING_ONES: ItemData(603 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mercenary, 3, SC2Race.ZERG, classification=ItemClassification.progression_skip_balancing), + item_names.HUNTER_KILLERS: ItemData(604 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mercenary, 4, SC2Race.ZERG, classification=ItemClassification.progression_skip_balancing), + item_names.TORRASQUE_MERC: ItemData(605 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mercenary, 5, SC2Race.ZERG, classification=ItemClassification.progression_skip_balancing), + item_names.HUNTERLING: ItemData(606 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mercenary, 6, SC2Race.ZERG, classification=ItemClassification.progression_skip_balancing), + item_names.YGGDRASIL: ItemData(607 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mercenary, 7, SC2Race.ZERG, classification=ItemClassification.progression_skip_balancing), + item_names.CAUSTIC_HORRORS: ItemData(608 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Mercenary, 8, SC2Race.ZERG, classification=ItemClassification.progression_skip_balancing), + + + # Misc Upgrades + item_names.OVERLORD_VENTRAL_SACS: ItemData(700 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Evolution_Pit, 6, SC2Race.ZERG, classification=ItemClassification.progression_skip_balancing), + item_names.OVERLORD_GENERATE_CREEP: ItemData(701 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Evolution_Pit, 7, SC2Race.ZERG, classification=ItemClassification.progression_skip_balancing), + item_names.OVERLORD_ANTENNAE: ItemData(702 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Evolution_Pit, 8, SC2Race.ZERG), + item_names.OVERLORD_PNEUMATIZED_CARAPACE: ItemData(703 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Evolution_Pit, 9, SC2Race.ZERG), + item_names.ZERG_EXCAVATING_CLAWS: ItemData(704 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Evolution_Pit, 11, SC2Race.ZERG, parent=parent_names.ZERG_UPROOTABLE_BUILDINGS), + item_names.ZERG_CREEP_STOMACH: ItemData(705 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Evolution_Pit, 10, SC2Race.ZERG), + item_names.HIVE_CLUSTER_MATURATION: ItemData(706 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Evolution_Pit, 12, SC2Race.ZERG), + item_names.MACROSCOPIC_RECUPERATION: ItemData(707 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Evolution_Pit, 13, SC2Race.ZERG), + item_names.BIOMECHANICAL_STOCKPILING: ItemData(708 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Evolution_Pit, 14, SC2Race.ZERG, parent=parent_names.INFESTED_FACTORY_OR_STARPORT), + item_names.BROODLING_SPORE_SATURATION: ItemData(709 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Evolution_Pit, 15, SC2Race.ZERG), + item_names.CELL_DIVISION: ItemData(710 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Evolution_Pit, 16, SC2Race.ZERG, classification=ItemClassification.progression, parent=parent_names.ZERG_MERCENARIES), + item_names.SELF_SUFFICIENT: ItemData(711 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Evolution_Pit, 17, SC2Race.ZERG, classification=ItemClassification.progression, parent=parent_names.ZERG_MERCENARIES), + item_names.UNRESTRICTED_MUTATION: ItemData(712 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Evolution_Pit, 18, SC2Race.ZERG, classification=ItemClassification.progression, parent=parent_names.ZERG_MERCENARIES), + item_names.EVOLUTIONARY_LEAP: ItemData(713 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Evolution_Pit, 19, SC2Race.ZERG, classification=ItemClassification.progression, parent=parent_names.ZERG_MERCENARIES), + + # Morphs + item_names.MUTALISK_CORRUPTOR_GUARDIAN_ASPECT: ItemData(800 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Morph, 6, SC2Race.ZERG, classification=ItemClassification.progression, parent=parent_names.MORPH_SOURCE_AIR), + item_names.MUTALISK_CORRUPTOR_DEVOURER_ASPECT: ItemData(801 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Morph, 7, SC2Race.ZERG, classification=ItemClassification.progression, parent=parent_names.MORPH_SOURCE_AIR), + item_names.ROACH_RAVAGER_ASPECT: ItemData(802 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Morph, 8, SC2Race.ZERG, classification=ItemClassification.progression, parent=parent_names.MORPH_SOURCE_ROACH), + item_names.OVERLORD_OVERSEER_ASPECT: ItemData(803 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Morph, 4, SC2Race.ZERG, classification=ItemClassification.progression), + item_names.ROACH_PRIMAL_IGNITER_ASPECT: ItemData(804 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Morph, 9, SC2Race.ZERG, classification=ItemClassification.progression, parent=parent_names.MORPH_SOURCE_ROACH), + item_names.ULTRALISK_TYRANNOZOR_ASPECT: ItemData(805 + SC2HOTS_ITEM_ID_OFFSET, ZergItemType.Morph, 10, SC2Race.ZERG, classification=ItemClassification.progression, parent=parent_names.MORPH_SOURCE_ULTRALISK), + + # Protoss Units + # The first several are in SC2WOL offset for historical reasons (show up in prophecy) + item_names.ZEALOT: + ItemData(700 + SC2WOL_ITEM_ID_OFFSET, ProtossItemType.Unit, 0, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.STALKER: + ItemData(701 + SC2WOL_ITEM_ID_OFFSET, ProtossItemType.Unit, 1, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.HIGH_TEMPLAR: + ItemData(702 + SC2WOL_ITEM_ID_OFFSET, ProtossItemType.Unit, 2, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.DARK_TEMPLAR: + ItemData(703 + SC2WOL_ITEM_ID_OFFSET, ProtossItemType.Unit, 3, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.IMMORTAL: + ItemData(704 + SC2WOL_ITEM_ID_OFFSET, ProtossItemType.Unit, 4, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.COLOSSUS: + ItemData(705 + SC2WOL_ITEM_ID_OFFSET, ProtossItemType.Unit, 5, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.PHOENIX: + ItemData(706 + SC2WOL_ITEM_ID_OFFSET, ProtossItemType.Unit, 6, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.VOID_RAY: + ItemData(707 + SC2WOL_ITEM_ID_OFFSET, ProtossItemType.Unit, 7, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.CARRIER: + ItemData(708 + SC2WOL_ITEM_ID_OFFSET, ProtossItemType.Unit, 8, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.OBSERVER: + ItemData(0 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit, 9, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.CENTURION: + ItemData(1 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit, 10, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.SENTINEL: + ItemData(2 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit, 11, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.SUPPLICANT: + ItemData(3 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit, 12, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.INSTIGATOR: + ItemData(4 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit, 13, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.SLAYER: + ItemData(5 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit, 14, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.SENTRY: + ItemData(6 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit, 15, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.ENERGIZER: + ItemData(7 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit, 16, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.HAVOC: + ItemData(8 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit, 17, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.SIGNIFIER: + ItemData(9 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit, 18, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.ASCENDANT: + ItemData(10 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit, 19, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.AVENGER: + ItemData(11 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit, 20, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.BLOOD_HUNTER: + ItemData(12 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit, 21, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.DRAGOON: + ItemData(13 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit, 22, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.DARK_ARCHON: + ItemData(14 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit, 23, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.ADEPT: + ItemData(15 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit, 24, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.WARP_PRISM: + ItemData(16 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit, 25, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.ANNIHILATOR: + ItemData(17 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit, 26, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.VANGUARD: + ItemData(18 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit, 27, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.WRATHWALKER: + ItemData(19 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit, 28, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.REAVER: + ItemData(20 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit, 29, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.DISRUPTOR: + ItemData(21 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit_2, 0, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.MIRAGE: + ItemData(22 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit_2, 1, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.CORSAIR: + ItemData(23 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit_2, 2, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.DESTROYER: + ItemData(24 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit_2, 3, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.SCOUT: + ItemData(25 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit_2, 4, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.TEMPEST: + ItemData(26 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit_2, 5, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.MOTHERSHIP: + ItemData(27 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit_2, 6, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.ARBITER: + ItemData(28 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit_2, 7, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.ORACLE: + ItemData(29 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit_2, 8, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.STALWART: + ItemData(30 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit_2, 9, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.PULSAR: + ItemData(31 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit_2, 10, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.DAWNBRINGER: + ItemData(32 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit_2, 11, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.SKYLORD: + ItemData(33 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit_2, 12, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.TRIREME: + ItemData(34 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit_2, 13, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.SKIRMISHER: + ItemData(35 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit_2, 14, SC2Race.PROTOSS, + classification=ItemClassification.progression), + # 36, 37 reserved for Mothership + item_names.OPPRESSOR: + ItemData(38 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit_2, 17, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.CALADRIUS: + ItemData(39 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit_2, 18, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.MISTWING: + ItemData(40 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Unit_2, 19, SC2Race.PROTOSS, + classification=ItemClassification.progression), + + # Protoss Upgrades + item_names.PROGRESSIVE_PROTOSS_GROUND_WEAPON: ItemData(100 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Upgrade, 0, SC2Race.PROTOSS, classification=ItemClassification.progression, quantity=WEAPON_ARMOR_UPGRADE_MAX_LEVEL), + item_names.PROGRESSIVE_PROTOSS_GROUND_ARMOR: ItemData(101 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Upgrade, 4, SC2Race.PROTOSS, classification=ItemClassification.progression, quantity=WEAPON_ARMOR_UPGRADE_MAX_LEVEL), + item_names.PROGRESSIVE_PROTOSS_SHIELDS: ItemData(102 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Upgrade, 8, SC2Race.PROTOSS, classification=ItemClassification.progression, quantity=WEAPON_ARMOR_UPGRADE_MAX_LEVEL), + item_names.PROGRESSIVE_PROTOSS_AIR_WEAPON: ItemData(103 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Upgrade, 12, SC2Race.PROTOSS, classification=ItemClassification.progression, quantity=WEAPON_ARMOR_UPGRADE_MAX_LEVEL), + item_names.PROGRESSIVE_PROTOSS_AIR_ARMOR: ItemData(104 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Upgrade, 16, SC2Race.PROTOSS, classification=ItemClassification.progression, quantity=WEAPON_ARMOR_UPGRADE_MAX_LEVEL), + # Bundles + item_names.PROGRESSIVE_PROTOSS_WEAPON_UPGRADE: ItemData(105 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Upgrade, -1, SC2Race.PROTOSS, classification=ItemClassification.progression, quantity=WEAPON_ARMOR_UPGRADE_MAX_LEVEL), + item_names.PROGRESSIVE_PROTOSS_ARMOR_UPGRADE: ItemData(106 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Upgrade, -1, SC2Race.PROTOSS, classification=ItemClassification.progression, quantity=WEAPON_ARMOR_UPGRADE_MAX_LEVEL), + item_names.PROGRESSIVE_PROTOSS_GROUND_UPGRADE: ItemData(107 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Upgrade, -1, SC2Race.PROTOSS, classification=ItemClassification.progression, quantity=WEAPON_ARMOR_UPGRADE_MAX_LEVEL), + item_names.PROGRESSIVE_PROTOSS_AIR_UPGRADE: ItemData(108 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Upgrade, -1, SC2Race.PROTOSS, classification=ItemClassification.progression, quantity=WEAPON_ARMOR_UPGRADE_MAX_LEVEL), + item_names.PROGRESSIVE_PROTOSS_WEAPON_ARMOR_UPGRADE: ItemData(109 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Upgrade, -1, SC2Race.PROTOSS, classification=ItemClassification.progression, quantity=WEAPON_ARMOR_UPGRADE_MAX_LEVEL), + + # Protoss Buildings + item_names.PHOTON_CANNON: ItemData(200 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Building, 0, SC2Race.PROTOSS, classification=ItemClassification.progression), + item_names.KHAYDARIN_MONOLITH: ItemData(201 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Building, 1, SC2Race.PROTOSS, classification=ItemClassification.progression), + item_names.SHIELD_BATTERY: ItemData(202 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Building, 2, SC2Race.PROTOSS, classification=ItemClassification.progression), + + # Protoss Unit Upgrades + item_names.SUPPLICANT_BLOOD_SHIELD: ItemData(300 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_1, 0, SC2Race.PROTOSS, parent=item_names.SUPPLICANT), + item_names.SUPPLICANT_SOUL_AUGMENTATION: ItemData(301 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_1, 1, SC2Race.PROTOSS, parent=item_names.SUPPLICANT), + item_names.SUPPLICANT_ENDLESS_SERVITUDE: ItemData(302 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_1, 2, SC2Race.PROTOSS, parent=item_names.SUPPLICANT), + item_names.ADEPT_SHOCKWAVE: ItemData(303 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_1, 3, SC2Race.PROTOSS, parent=item_names.ADEPT), + item_names.ADEPT_RESONATING_GLAIVES: ItemData(304 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_1, 4, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.ADEPT), + item_names.ADEPT_PHASE_BULWARK: ItemData(305 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_1, 5, SC2Race.PROTOSS, parent=item_names.ADEPT), + item_names.STALKER_INSTIGATOR_SLAYER_DISINTEGRATING_PARTICLES: ItemData(306 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_1, 6, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=parent_names.STALKER_CLASS), + item_names.STALKER_INSTIGATOR_SLAYER_PARTICLE_REFLECTION: ItemData(307 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_1, 7, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=parent_names.STALKER_CLASS), + item_names.DRAGOON_CONCENTRATED_ANTIMATTER: ItemData(308 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_1, 8, SC2Race.PROTOSS, parent=item_names.DRAGOON), + item_names.DRAGOON_TRILLIC_COMPRESSION_SYSTEM: ItemData(309 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_1, 9, SC2Race.PROTOSS, parent=item_names.DRAGOON), + item_names.DRAGOON_SINGULARITY_CHARGE: ItemData(310 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_1, 10, SC2Race.PROTOSS, parent=item_names.DRAGOON), + item_names.DRAGOON_ENHANCED_STRIDER_SERVOS: ItemData(311 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_1, 11, SC2Race.PROTOSS, parent=item_names.DRAGOON), + item_names.SCOUT_COMBAT_SENSOR_ARRAY: ItemData(312 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_1, 12, SC2Race.PROTOSS, parent=parent_names.SCOUT_CLASS), + item_names.SCOUT_APIAL_SENSORS: ItemData(313 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_1, 13, SC2Race.PROTOSS, parent=item_names.SCOUT), + item_names.SCOUT_GRAVITIC_THRUSTERS: ItemData(314 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_1, 14, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=parent_names.SCOUT_CLASS), + item_names.SCOUT_ADVANCED_PHOTON_BLASTERS: ItemData(315 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_1, 15, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=parent_names.SCOUT_OR_OPPRESSOR_OR_MISTWING), + item_names.TEMPEST_TECTONIC_DESTABILIZERS: ItemData(316 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_1, 16, SC2Race.PROTOSS, parent=item_names.TEMPEST), + item_names.TEMPEST_QUANTIC_REACTOR: ItemData(317 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_1, 17, SC2Race.PROTOSS, parent=item_names.TEMPEST), + item_names.TEMPEST_GRAVITY_SLING: ItemData(318 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_1, 18, SC2Race.PROTOSS, parent=item_names.TEMPEST), + item_names.PHOENIX_CLASS_IONIC_WAVELENGTH_FLUX: ItemData(319 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_1, 19, SC2Race.PROTOSS, parent=parent_names.PHOENIX_CLASS), + item_names.PHOENIX_CLASS_ANION_PULSE_CRYSTALS: ItemData(320 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_1, 20, SC2Race.PROTOSS, parent=parent_names.PHOENIX_CLASS), + item_names.CORSAIR_STEALTH_DRIVE: ItemData(321 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_1, 21, SC2Race.PROTOSS, parent=item_names.CORSAIR), + item_names.CORSAIR_ARGUS_JEWEL: ItemData(322 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_1, 22, SC2Race.PROTOSS, parent=item_names.CORSAIR), + item_names.CORSAIR_SUSTAINING_DISRUPTION: ItemData(323 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_1, 23, SC2Race.PROTOSS, parent=item_names.CORSAIR), + item_names.CORSAIR_NEUTRON_SHIELDS: ItemData(324 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_1, 24, SC2Race.PROTOSS, parent=item_names.CORSAIR), + item_names.ORACLE_STEALTH_DRIVE: ItemData(325 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_1, 25, SC2Race.PROTOSS, parent=item_names.ORACLE), + item_names.ORACLE_SKYWARD_CHRONOANOMALY: ItemData(544 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_1, 26, SC2Race.PROTOSS, parent=item_names.ORACLE), + item_names.ORACLE_TEMPORAL_ACCELERATION_BEAM: ItemData(327 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_1, 27, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.ORACLE), + item_names.ARBITER_CHRONOSTATIC_REINFORCEMENT: ItemData(328 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_1, 28, SC2Race.PROTOSS, parent=item_names.ARBITER), + item_names.ARBITER_KHAYDARIN_CORE: ItemData(329 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_1, 29, SC2Race.PROTOSS, parent=item_names.ARBITER), + item_names.ARBITER_SPACETIME_ANCHOR: ItemData(330 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_2, 0, SC2Race.PROTOSS, parent=item_names.ARBITER), + item_names.ARBITER_RESOURCE_EFFICIENCY: ItemData(331 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_2, 1, SC2Race.PROTOSS, parent=item_names.ARBITER), + item_names.ARBITER_JUDICATORS_VEIL: ItemData(332 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_2, 2, SC2Race.PROTOSS, parent=item_names.ARBITER), + item_names.CARRIER_TRIREME_GRAVITON_CATAPULT: + ItemData(333 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_2, 3, SC2Race.PROTOSS, parent=parent_names.CARRIER_OR_TRIREME), + item_names.CARRIER_SKYLORD_TRIREME_HULL_OF_PAST_GLORIES: + ItemData(334 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_2, 4, SC2Race.PROTOSS, parent=parent_names.CARRIER_CLASS), + item_names.VOID_RAY_DESTROYER_PULSAR_DAWNBRINGER_FLUX_VANES: + ItemData(335 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_2, 5, SC2Race.PROTOSS, parent=parent_names.VOID_RAY_CLASS), + item_names.DESTROYER_RESOURCE_EFFICIENCY: ItemData(535 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_2, 6, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.DESTROYER), + item_names.WARP_PRISM_GRAVITIC_DRIVE: + ItemData(337 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_2, 7, SC2Race.PROTOSS, parent=item_names.WARP_PRISM), + item_names.WARP_PRISM_PHASE_BLASTER: + ItemData(338 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_2, 8, SC2Race.PROTOSS, + classification=ItemClassification.progression, parent=item_names.WARP_PRISM), + item_names.WARP_PRISM_WAR_CONFIGURATION: ItemData(339 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_2, 9, SC2Race.PROTOSS, parent=item_names.WARP_PRISM), + item_names.OBSERVER_GRAVITIC_BOOSTERS: ItemData(340 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_2, 10, SC2Race.PROTOSS, parent=item_names.OBSERVER), + item_names.OBSERVER_SENSOR_ARRAY: ItemData(341 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_2, 11, SC2Race.PROTOSS, parent=item_names.OBSERVER), + item_names.REAVER_SCARAB_DAMAGE: ItemData(342 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_2, 12, SC2Race.PROTOSS, parent=item_names.REAVER), + item_names.REAVER_SOLARITE_PAYLOAD: ItemData(343 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_2, 13, SC2Race.PROTOSS, parent=item_names.REAVER), + item_names.REAVER_REAVER_CAPACITY: ItemData(344 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_2, 14, SC2Race.PROTOSS, parent=item_names.REAVER), + item_names.REAVER_RESOURCE_EFFICIENCY: ItemData(345 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_2, 15, SC2Race.PROTOSS, parent=item_names.REAVER), + item_names.VANGUARD_AGONY_LAUNCHERS: ItemData(346 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_2, 16, SC2Race.PROTOSS, parent=item_names.VANGUARD), + item_names.VANGUARD_MATTER_DISPERSION: ItemData(347 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_2, 17, SC2Race.PROTOSS, parent=item_names.VANGUARD), + item_names.IMMORTAL_ANNIHILATOR_SINGULARITY_CHARGE: ItemData(348 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_2, 18, SC2Race.PROTOSS, parent=parent_names.IMMORTAL_OR_ANNIHILATOR), + item_names.IMMORTAL_ANNIHILATOR_ADVANCED_TARGETING: ItemData(349 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_2, 19, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=parent_names.IMMORTAL_OR_ANNIHILATOR), + item_names.COLOSSUS_PACIFICATION_PROTOCOL: ItemData(350 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_2, 20, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.COLOSSUS), + item_names.WRATHWALKER_RAPID_POWER_CYCLING: ItemData(351 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_2, 21, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.WRATHWALKER), + item_names.WRATHWALKER_EYE_OF_WRATH: ItemData(352 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_2, 22, SC2Race.PROTOSS, parent=item_names.WRATHWALKER), + item_names.DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_SHROUD_OF_ADUN: ItemData(353 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_2, 23, SC2Race.PROTOSS, parent=parent_names.DARK_TEMPLAR_CLASS), + item_names.DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_SHADOW_GUARD_TRAINING: ItemData(354 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_2, 24, SC2Race.PROTOSS, parent=parent_names.DARK_TEMPLAR_CLASS), + item_names.DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_BLINK: ItemData(355 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_2, 25, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=parent_names.DARK_TEMPLAR_CLASS), + item_names.DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_RESOURCE_EFFICIENCY: ItemData(356 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_2, 26, SC2Race.PROTOSS, parent=parent_names.DARK_TEMPLAR_CLASS), + item_names.DARK_TEMPLAR_DARK_ARCHON_MELD: ItemData(357 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_2, 27, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.DARK_TEMPLAR), + item_names.HIGH_TEMPLAR_SIGNIFIER_UNSHACKLED_PSIONIC_STORM: ItemData(358 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_2, 28, SC2Race.PROTOSS, parent=parent_names.STORM_CASTER), + item_names.HIGH_TEMPLAR_SIGNIFIER_HALLUCINATION: ItemData(359 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_2, 29, SC2Race.PROTOSS, parent=parent_names.STORM_CASTER), + item_names.HIGH_TEMPLAR_SIGNIFIER_KHAYDARIN_AMULET: ItemData(360 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_3, 0, SC2Race.PROTOSS, parent=parent_names.STORM_CASTER), + item_names.ARCHON_HIGH_ARCHON: ItemData(361 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_3, 1, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=parent_names.ARCHON_SOURCE), + item_names.DARK_ARCHON_FEEDBACK: ItemData(362 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_3, 2, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=parent_names.DARK_ARCHON_SOURCE), + item_names.DARK_ARCHON_MAELSTROM: ItemData(363 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_3, 3, SC2Race.PROTOSS, parent=parent_names.DARK_ARCHON_SOURCE), + item_names.DARK_ARCHON_ARGUS_TALISMAN: ItemData(364 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_3, 4, SC2Race.PROTOSS, parent=parent_names.DARK_ARCHON_SOURCE), + item_names.ASCENDANT_POWER_OVERWHELMING: ItemData(365 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_3, 5, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=parent_names.SUPPLICANT_AND_ASCENDANT), + item_names.ASCENDANT_CHAOTIC_ATTUNEMENT: ItemData(366 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_3, 6, SC2Race.PROTOSS, parent=item_names.ASCENDANT), + item_names.ASCENDANT_BLOOD_AMULET: ItemData(367 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_3, 7, SC2Race.PROTOSS, parent=item_names.ASCENDANT), + item_names.SENTRY_ENERGIZER_HAVOC_CLOAKING_MODULE: ItemData(368 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_3, 8, SC2Race.PROTOSS, parent=parent_names.SENTRY_CLASS), + item_names.SENTRY_ENERGIZER_HAVOC_SHIELD_BATTERY_RAPID_RECHARGING: ItemData(369 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_3, 9, SC2Race.PROTOSS, parent=parent_names.SENTRY_CLASS_OR_SHIELD_BATTERY), + item_names.SENTRY_FORCE_FIELD: ItemData(370 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_3, 10, SC2Race.PROTOSS, parent=item_names.SENTRY), + item_names.SENTRY_HALLUCINATION: ItemData(371 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_3, 11, SC2Race.PROTOSS, parent=item_names.SENTRY), + item_names.ENERGIZER_RECLAMATION: ItemData(372 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_3, 12, SC2Race.PROTOSS, parent=item_names.ENERGIZER), + item_names.ENERGIZER_FORGED_CHASSIS: ItemData(373 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_3, 13, SC2Race.PROTOSS, parent=item_names.ENERGIZER), + item_names.HAVOC_DETECT_WEAKNESS: ItemData(374 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_3, 14, SC2Race.PROTOSS, parent=item_names.HAVOC), + item_names.HAVOC_BLOODSHARD_RESONANCE: ItemData(375 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_3, 15, SC2Race.PROTOSS, parent=item_names.HAVOC), + item_names.ZEALOT_SENTINEL_CENTURION_LEG_ENHANCEMENTS: ItemData(376 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_3, 16, SC2Race.PROTOSS, parent=parent_names.ZEALOT_OR_SENTINEL_OR_CENTURION), + item_names.ZEALOT_SENTINEL_CENTURION_SHIELD_CAPACITY: ItemData(377 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_3, 17, SC2Race.PROTOSS, classification=ItemClassification.progression_skip_balancing, parent=parent_names.ZEALOT_OR_SENTINEL_OR_CENTURION), + item_names.ORACLE_BOSONIC_CORE: ItemData(378 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_3, 18, SC2Race.PROTOSS, parent=item_names.ORACLE), + item_names.SCOUT_RESOURCE_EFFICIENCY: ItemData(379 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_3, 19, SC2Race.PROTOSS, parent=item_names.SCOUT), + item_names.IMMORTAL_ANNIHILATOR_DISRUPTOR_DISPERSION: ItemData(380 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_3, 20, SC2Race.PROTOSS, parent=parent_names.IMMORTAL_OR_ANNIHILATOR), + item_names.DISRUPTOR_CLOAKING_MODULE: ItemData(381 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_3, 21, SC2Race.PROTOSS, parent=item_names.DISRUPTOR), + item_names.DISRUPTOR_PERFECTED_POWER: ItemData(382 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_3, 22, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.DISRUPTOR), + item_names.DISRUPTOR_RESTRAINED_DESTRUCTION: ItemData(383 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_3, 23, SC2Race.PROTOSS, parent=item_names.DISRUPTOR), + item_names.TEMPEST_INTERPLANETARY_RANGE: ItemData(384 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_3, 24, SC2Race.PROTOSS, parent=item_names.TEMPEST), + item_names.DAWNBRINGER_ANTI_SURFACE_COUNTERMEASURES: ItemData(385 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_3, 25, SC2Race.PROTOSS, parent=item_names.DAWNBRINGER), + item_names.DAWNBRINGER_ENHANCED_SHIELD_GENERATOR: ItemData(386 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_3, 26, SC2Race.PROTOSS, parent=item_names.DAWNBRINGER), + item_names.STALWART_HIGH_VOLTAGE_CAPACITORS: ItemData(387 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_3, 27, SC2Race.PROTOSS, parent=item_names.STALWART), + item_names.STALWART_REINTEGRATED_FRAMEWORK: ItemData(388 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_3, 28, SC2Race.PROTOSS, parent=item_names.STALWART), + item_names.STALWART_STABILIZED_ELECTRODES: ItemData(389 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_3, 29, SC2Race.PROTOSS, parent=item_names.STALWART), + item_names.STALWART_LATTICED_SHIELDING: ItemData(390 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_4, 0, SC2Race.PROTOSS, parent=item_names.STALWART), + item_names.ARCHON_TRANSCENDENCE: ItemData(391 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_4, 1, SC2Race.PROTOSS, parent=parent_names.ARCHON_SOURCE), + item_names.ARCHON_POWER_SIPHON: ItemData(392 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_4, 2, SC2Race.PROTOSS, parent=parent_names.ARCHON_SOURCE), + item_names.ARCHON_ERADICATE: ItemData(393 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_4, 3, SC2Race.PROTOSS, parent=parent_names.ARCHON_SOURCE), + item_names.ARCHON_OBLITERATE: ItemData(394 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_4, 4, SC2Race.PROTOSS, parent=parent_names.ARCHON_SOURCE), + item_names.SUPPLICANT_ZENITH_PITCH: ItemData(395 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_4, 5, SC2Race.PROTOSS, classification=ItemClassification.progression_skip_balancing, parent=item_names.SUPPLICANT), + item_names.PULSAR_CHRONOCLYSM: ItemData(396 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_4, 6, SC2Race.PROTOSS, parent=item_names.PULSAR), + item_names.PULSAR_ENTROPIC_REVERSAL: ItemData(397 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_4, 7, SC2Race.PROTOSS, parent=item_names.PULSAR), + # 398-407 reserved for Mothership + item_names.OPPRESSOR_ACCELERATED_WARP: ItemData(408 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_4, 18, SC2Race.PROTOSS, parent=item_names.OPPRESSOR), + item_names.OPPRESSOR_ARMOR_MELTING_BLASTERS: ItemData(409 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_4, 19, SC2Race.PROTOSS, parent=item_names.OPPRESSOR), + item_names.CALADRIUS_SIDE_MISSILES: ItemData(410 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_4, 20, SC2Race.PROTOSS, parent=item_names.CALADRIUS), + item_names.CALADRIUS_STRUCTURE_TARGETING: ItemData(411 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_4, 21, SC2Race.PROTOSS, parent=item_names.CALADRIUS), + item_names.CALADRIUS_SOLARITE_REACTOR: ItemData(412 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_4, 22, SC2Race.PROTOSS, parent=item_names.CALADRIUS), + item_names.MISTWING_NULL_SHROUD: ItemData(413 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_4, 23, SC2Race.PROTOSS, parent=item_names.MISTWING), + item_names.MISTWING_PILOT: ItemData(414 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_4, 24, SC2Race.PROTOSS, classification=ItemClassification.progression_skip_balancing, parent=item_names.MISTWING), + item_names.INSTIGATOR_BLINK_OVERDRIVE: ItemData(415 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_4, 25, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.INSTIGATOR), + item_names.INSTIGATOR_RECONSTRUCTION: ItemData(416 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_4, 26, SC2Race.PROTOSS, parent=item_names.INSTIGATOR), + item_names.DARK_TEMPLAR_ARCHON_MERGE: ItemData(417 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_4, 27, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.DARK_TEMPLAR), + item_names.ASCENDANT_ARCHON_MERGE: ItemData(418 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_4, 28, SC2Race.PROTOSS, classification=ItemClassification.progression_skip_balancing, parent=item_names.ASCENDANT), + item_names.SCOUT_SUPPLY_EFFICIENCY: ItemData(419 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_4, 29, SC2Race.PROTOSS, parent=item_names.SCOUT), + item_names.REAVER_BARGAIN_BIN_PRICES: ItemData(420 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Forge_5, 0, SC2Race.PROTOSS, parent=item_names.SCOUT), + + + # War Council + item_names.ZEALOT_WHIRLWIND: ItemData(500 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 0, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.ZEALOT), + item_names.CENTURION_RESOURCE_EFFICIENCY: ItemData(501 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 1, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.CENTURION), + item_names.SENTINEL_RESOURCE_EFFICIENCY: ItemData(502 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 2, SC2Race.PROTOSS, parent=item_names.SENTINEL), + item_names.STALKER_PHASE_REACTOR: ItemData(503 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 3, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.STALKER), + item_names.DRAGOON_PHALANX_SUIT: ItemData(504 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 4, SC2Race.PROTOSS, parent=item_names.DRAGOON), + item_names.INSTIGATOR_MODERNIZED_SERVOS: ItemData(505 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 5, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.INSTIGATOR), + item_names.ADEPT_DISRUPTIVE_TRANSFER: ItemData(506 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 6, SC2Race.PROTOSS, parent=item_names.ADEPT), + item_names.SLAYER_PHASE_BLINK: ItemData(507 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 7, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.SLAYER), + item_names.AVENGER_KRYHAS_CLOAK: ItemData(508 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 8, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.AVENGER), + item_names.DARK_TEMPLAR_LESSER_SHADOW_FURY: ItemData(509 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 9, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.DARK_TEMPLAR), + item_names.DARK_TEMPLAR_GREATER_SHADOW_FURY: ItemData(510 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 10, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.DARK_TEMPLAR), + item_names.BLOOD_HUNTER_BRUTAL_EFFICIENCY: ItemData(511 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 11, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.BLOOD_HUNTER), + item_names.SENTRY_DOUBLE_SHIELD_RECHARGE: ItemData(512 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 12, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.SENTRY), + item_names.ENERGIZER_MOBILE_CHRONO_BEAM: ItemData(513 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 13, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.ENERGIZER), + item_names.HAVOC_ENDURING_SIGHT: ItemData(514 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 14, SC2Race.PROTOSS, parent=item_names.HAVOC), + item_names.HIGH_TEMPLAR_PLASMA_SURGE: ItemData(515 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 15, SC2Race.PROTOSS, parent=item_names.HIGH_TEMPLAR), + item_names.SIGNIFIER_FEEDBACK: ItemData(516 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 16, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.SIGNIFIER), + item_names.ASCENDANT_BREATH_OF_CREATION: ItemData(517 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 17, SC2Race.PROTOSS, parent=item_names.ASCENDANT), + item_names.DARK_ARCHON_INDOMITABLE_WILL: ItemData(518 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 18, SC2Race.PROTOSS, parent=parent_names.DARK_ARCHON_SOURCE), + item_names.IMMORTAL_IMPROVED_BARRIER: ItemData(519 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 19, SC2Race.PROTOSS, parent=item_names.IMMORTAL), + item_names.VANGUARD_RAPIDFIRE_CANNON: ItemData(520 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 20, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.VANGUARD), + item_names.VANGUARD_FUSION_MORTARS: ItemData(521 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 21, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.VANGUARD), + item_names.ANNIHILATOR_TWILIGHT_CHASSIS: ItemData(522 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 22, SC2Race.PROTOSS, parent=item_names.ANNIHILATOR), + item_names.STALWART_ARC_INDUCERS: ItemData(523 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 23, SC2Race.PROTOSS, parent=item_names.STALWART), + item_names.COLOSSUS_FIRE_LANCE: ItemData(524 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 24, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.COLOSSUS), + item_names.WRATHWALKER_AERIAL_TRACKING: ItemData(525 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 25, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.WRATHWALKER), + item_names.REAVER_KHALAI_REPLICATORS: ItemData(526 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 26, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.REAVER), + item_names.DISRUPTOR_MOBILITY_PROTOCOLS: ItemData(527 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 27, SC2Race.PROTOSS, parent=item_names.DISRUPTOR), + item_names.WARP_PRISM_WARP_REFRACTION: ItemData(528 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 28, SC2Race.PROTOSS, parent=item_names.WARP_PRISM), + item_names.OBSERVER_INDUCE_SCOPOPHOBIA: ItemData(529 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council, 29, SC2Race.PROTOSS, parent=item_names.OBSERVER), + item_names.PHOENIX_DOUBLE_GRAVITON_BEAM: ItemData(530 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council_2, 0, SC2Race.PROTOSS, parent=item_names.PHOENIX), + item_names.CORSAIR_NETWORK_DISRUPTION: ItemData(531 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council_2, 1, SC2Race.PROTOSS, parent=item_names.CORSAIR), + item_names.MIRAGE_GRAVITON_BEAM: ItemData(532 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council_2, 2, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.MIRAGE), + item_names.SKIRMISHER_PEER_CONTEMPT: ItemData(533 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council_2, 3, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.SKIRMISHER), + item_names.VOID_RAY_PRISMATIC_RANGE: ItemData(534 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council_2, 4, SC2Race.PROTOSS, parent=item_names.VOID_RAY), + item_names.DESTROYER_REFORGED_BLOODSHARD_CORE: ItemData(336 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council_2, 5, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.DESTROYER), + item_names.PULSAR_CHRONO_SHEAR: ItemData(536 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council_2, 6, SC2Race.PROTOSS, parent=item_names.PULSAR), + item_names.DAWNBRINGER_SOLARITE_LENS: ItemData(537 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council_2, 7, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.DAWNBRINGER), + item_names.CARRIER_REPAIR_DRONES: ItemData(538 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council_2, 8, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.CARRIER), + item_names.SKYLORD_JUMP: ItemData(539 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council_2, 9, SC2Race.PROTOSS, parent=item_names.SKYLORD), + item_names.TRIREME_SOLAR_BEAM: ItemData(540 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council_2, 10, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.TRIREME), + item_names.TEMPEST_DISINTEGRATION: ItemData(541 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council_2, 11, SC2Race.PROTOSS, parent=item_names.TEMPEST), + item_names.SCOUT_EXPEDITIONARY_HULL: ItemData(542 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council_2, 12, SC2Race.PROTOSS, parent=item_names.SCOUT), + item_names.ARBITER_VESSEL_OF_THE_CONCLAVE: ItemData(543 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council_2, 13, SC2Race.PROTOSS, parent=item_names.ARBITER), + item_names.ORACLE_STASIS_CALIBRATION: ItemData(326 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council_2, 14, SC2Race.PROTOSS, parent=item_names.ORACLE), + item_names.MOTHERSHIP_INTEGRATED_POWER: ItemData(545 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council_2, 15, SC2Race.PROTOSS, parent=item_names.MOTHERSHIP), + # 546-549 reserved for Mothership + item_names.OPPRESSOR_VULCAN_BLASTER: ItemData(550 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council_2, 20, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.OPPRESSOR), + item_names.CALADRIUS_CORONA_BEAM: ItemData(551 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council_2, 21, SC2Race.PROTOSS, classification=ItemClassification.progression, parent=item_names.CALADRIUS), + item_names.MISTWING_PHANTOM_DASH: ItemData(552 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council_2, 22, SC2Race.PROTOSS, parent=item_names.MISTWING), + item_names.SUPPLICANT_SACRIFICE: ItemData(553 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.War_Council_2, 23, SC2Race.PROTOSS, parent=item_names.SUPPLICANT), + + # SoA Calldown powers + item_names.SOA_CHRONO_SURGE: ItemData(700 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Spear_Of_Adun, 0, SC2Race.PROTOSS), + item_names.SOA_PROGRESSIVE_PROXY_PYLON: ItemData(701 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Progressive, 0, SC2Race.PROTOSS, quantity=2, classification=ItemClassification.progression), + item_names.SOA_PYLON_OVERCHARGE: ItemData(702 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Spear_Of_Adun, 1, SC2Race.PROTOSS, classification=ItemClassification.progression), + item_names.SOA_ORBITAL_STRIKE: ItemData(703 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Spear_Of_Adun, 2, SC2Race.PROTOSS, classification=ItemClassification.progression), + item_names.SOA_TEMPORAL_FIELD: ItemData(704 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Spear_Of_Adun, 3, SC2Race.PROTOSS), + item_names.SOA_SOLAR_LANCE: ItemData(705 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Spear_Of_Adun, 4, SC2Race.PROTOSS, classification=ItemClassification.progression), + item_names.SOA_MASS_RECALL: ItemData(706 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Spear_Of_Adun, 5, SC2Race.PROTOSS), + item_names.SOA_SHIELD_OVERCHARGE: ItemData(707 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Spear_Of_Adun, 6, SC2Race.PROTOSS, classification=ItemClassification.progression), + item_names.SOA_DEPLOY_FENIX: ItemData(708 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Spear_Of_Adun, 7, SC2Race.PROTOSS, classification=ItemClassification.progression), + item_names.SOA_PURIFIER_BEAM: ItemData(709 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Spear_Of_Adun, 8, SC2Race.PROTOSS, classification=ItemClassification.progression), + item_names.SOA_TIME_STOP: ItemData(710 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Spear_Of_Adun, 9, SC2Race.PROTOSS, classification=ItemClassification.progression), + item_names.SOA_SOLAR_BOMBARDMENT: ItemData(711 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Spear_Of_Adun, 10, SC2Race.PROTOSS, classification=ItemClassification.progression), + + # Generic Protoss Upgrades + item_names.MATRIX_OVERLOAD: + ItemData(800 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Solarite_Core, 0, SC2Race.PROTOSS, classification=ItemClassification.progression), + item_names.QUATRO: + ItemData(801 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Solarite_Core, 1, SC2Race.PROTOSS, classification=ItemClassification.progression), + item_names.NEXUS_OVERCHARGE: + ItemData(802 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Solarite_Core, 2, SC2Race.PROTOSS, + classification=ItemClassification.progression, important_for_filtering=True), + item_names.ORBITAL_ASSIMILATORS: + ItemData(803 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Solarite_Core, 3, SC2Race.PROTOSS, classification=ItemClassification.progression), + item_names.WARP_HARMONIZATION: + ItemData(804 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Solarite_Core, 4, SC2Race.PROTOSS, classification=ItemClassification.progression_skip_balancing), + item_names.GUARDIAN_SHELL: + ItemData(805 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Solarite_Core, 5, SC2Race.PROTOSS, classification=ItemClassification.progression), + item_names.RECONSTRUCTION_BEAM: + ItemData(806 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Solarite_Core, 6, SC2Race.PROTOSS, + classification=ItemClassification.progression), + item_names.OVERWATCH: + ItemData(807 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Solarite_Core, 7, SC2Race.PROTOSS, classification=ItemClassification.progression), + item_names.SUPERIOR_WARP_GATES: + ItemData(808 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Solarite_Core, 8, SC2Race.PROTOSS), + item_names.ENHANCED_TARGETING: + ItemData(809 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Solarite_Core, 9, SC2Race.PROTOSS, parent=parent_names.PROTOSS_STATIC_DEFENSE), + item_names.OPTIMIZED_ORDNANCE: + ItemData(810 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Solarite_Core, 10, SC2Race.PROTOSS, parent=parent_names.PROTOSS_ATTACKING_BUILDING), + item_names.KHALAI_INGENUITY: + ItemData(811 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Solarite_Core, 11, SC2Race.PROTOSS, classification=ItemClassification.progression), + item_names.AMPLIFIED_ASSIMILATORS: + ItemData(812 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Solarite_Core, 12, SC2Race.PROTOSS, classification=ItemClassification.progression), + item_names.PROGRESSIVE_WARP_RELOCATE: + ItemData(813 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Progressive, 2, SC2Race.PROTOSS, quantity=2, + classification=ItemClassification.progression), + item_names.PROBE_WARPIN: + ItemData(814 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Solarite_Core, 13, SC2Race.PROTOSS, classification=ItemClassification.progression), + item_names.ELDER_PROBES: + ItemData(815 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Solarite_Core, 14, SC2Race.PROTOSS, classification=ItemClassification.progression), +} + +# Add keys to item table +# Mission keys (key offset + 0-999) +# Mission IDs start at 1 so the item IDs are moved down a space +mission_key_item_table = { + item_names._TEMPLATE_MISSION_KEY.format(mission.mission_name): + ItemData(mission.id - 1 + SC2_KEY_ITEM_ID_OFFSET, FactionlessItemType.Keys, 0, SC2Race.ANY, + classification=ItemClassification.progression, quantity=0) + for mission in SC2Mission +} +# Numbered layout keys (key offset + 1000 - 1999) +numbered_layout_key_item_table = { + item_names._TEMPLATE_NUMBERED_LAYOUT_KEY.format(number + 1): + ItemData(number + SC2_KEY_ITEM_ID_OFFSET + SC2_KEY_ITEM_SECTION_SIZE, FactionlessItemType.Keys, 0, SC2Race.ANY, + classification=ItemClassification.progression, quantity=0) + for number in range(len(SC2Mission)) +} +# Numbered campaign keys (key offset + 2000 - 2999) +numbered_campaign_key_item_table = { + item_names._TEMPLATE_NUMBERED_CAMPAIGN_KEY.format(number + 1): + ItemData(number + SC2_KEY_ITEM_ID_OFFSET + SC2_KEY_ITEM_SECTION_SIZE * 2, FactionlessItemType.Keys, 0, SC2Race.ANY, + classification=ItemClassification.progression, quantity=0) + for number in range(len(SC2Mission)) +} +# Flavor keys (key offset + 3000 - 3999) +flavor_key_item_table = { + item_names._TEMPLATE_FLAVOR_KEY.format(name): + ItemData(i + SC2_KEY_ITEM_ID_OFFSET + SC2_KEY_ITEM_SECTION_SIZE * 3, FactionlessItemType.Keys, 0, SC2Race.ANY, + classification=ItemClassification.progression, quantity=0) + for (i, name) in enumerate(item_names._flavor_key_names) +} +# Named layout keys (key offset + 4000 - 4999) +campaign_to_layout_names = get_used_layout_names() +named_layout_key_item_table = { + item_names._TEMPLATE_NAMED_LAYOUT_KEY.format(layout_name, campaign.campaign_name): + ItemData(layout_start + i + SC2_KEY_ITEM_ID_OFFSET + SC2_KEY_ITEM_SECTION_SIZE * 4, FactionlessItemType.Keys, 0, SC2Race.ANY, + classification=ItemClassification.progression, quantity=0) + for (campaign, (layout_start, layout_names)) in campaign_to_layout_names.items() for (i, layout_name) in enumerate(layout_names) +} +# Named campaign keys (key offset + 5000 - 5999) +campaign_names = [campaign.campaign_name for campaign in SC2Campaign if campaign != SC2Campaign.GLOBAL] +named_campaign_key_item_table = { + item_names._TEMPLATE_NAMED_CAMPAIGN_KEY.format(campaign_name): + ItemData(i + SC2_KEY_ITEM_ID_OFFSET + SC2_KEY_ITEM_SECTION_SIZE * 5, FactionlessItemType.Keys, 0, SC2Race.ANY, + classification=ItemClassification.progression, quantity=0) + for (i, campaign_name) in enumerate(campaign_names) +} +# Numbered progressive keys (key offset + 6000 - 6999) +numbered_progressive_keys = { + item_names._TEMPLATE_PROGRESSIVE_KEY.format(number + 1): + ItemData(number + SC2_KEY_ITEM_ID_OFFSET + SC2_KEY_ITEM_SECTION_SIZE * 6, FactionlessItemType.Keys, 0, SC2Race.ANY, + classification=ItemClassification.progression, quantity=0) + for number in range(len(SC2Mission)) +} +# Special keys (key offset + 7000 - 7999) +special_keys = { + item_names.PROGRESSIVE_MISSION_KEY: + ItemData(0 + SC2_KEY_ITEM_ID_OFFSET + SC2_KEY_ITEM_SECTION_SIZE * 7, FactionlessItemType.Keys, 0, SC2Race.ANY, + classification=ItemClassification.progression, quantity=0), + item_names.PROGRESSIVE_QUESTLINE_KEY: + ItemData(1 + SC2_KEY_ITEM_ID_OFFSET + SC2_KEY_ITEM_SECTION_SIZE * 7, FactionlessItemType.Keys, 0, SC2Race.ANY, + classification=ItemClassification.progression, quantity=0), +} +key_item_table = {} +key_item_table.update(mission_key_item_table) +key_item_table.update(numbered_layout_key_item_table) +key_item_table.update(numbered_campaign_key_item_table) +key_item_table.update(flavor_key_item_table) +key_item_table.update(named_layout_key_item_table) +key_item_table.update(named_campaign_key_item_table) +key_item_table.update(numbered_progressive_keys) +key_item_table.update(special_keys) +item_table.update(key_item_table) + +def get_item_table(): + return item_table + + +basic_units = { + SC2Race.TERRAN: { + item_names.MARINE, + item_names.MARAUDER, + item_names.DOMINION_TROOPER, + item_names.GOLIATH, + item_names.HELLION, + item_names.VULTURE, + item_names.WARHOUND, + }, + SC2Race.ZERG: { + item_names.SWARM_QUEEN, + item_names.ROACH, + item_names.HYDRALISK, + }, + SC2Race.PROTOSS: { + item_names.ZEALOT, + item_names.CENTURION, + item_names.SENTINEL, + item_names.STALKER, + item_names.INSTIGATOR, + item_names.SLAYER, + item_names.ADEPT, + } +} + +advanced_basic_units = { + SC2Race.TERRAN: basic_units[SC2Race.TERRAN].union({ + item_names.REAPER, + item_names.DIAMONDBACK, + item_names.VIKING, + item_names.SIEGE_TANK, + item_names.BANSHEE, + item_names.THOR, + item_names.BATTLECRUISER, + item_names.CYCLONE + }), + SC2Race.ZERG: basic_units[SC2Race.ZERG].union({ + item_names.INFESTED_BANSHEE, + item_names.INFESTED_DIAMONDBACK, + item_names.INFESTOR, + item_names.ABERRATION, + }), + SC2Race.PROTOSS: basic_units[SC2Race.PROTOSS].union({ + item_names.DARK_TEMPLAR, + item_names.DRAGOON, + item_names.AVENGER, + item_names.IMMORTAL, + item_names.ANNIHILATOR, + item_names.VANGUARD, + item_names.SKIRMISHER, + }) +} + +no_logic_basic_units = { + SC2Race.TERRAN: advanced_basic_units[SC2Race.TERRAN].union({ + item_names.FIREBAT, + item_names.GHOST, + item_names.SPECTRE, + item_names.WRAITH, + item_names.RAVEN, + item_names.PREDATOR, + item_names.LIBERATOR, + item_names.HERC, + }), + SC2Race.ZERG: advanced_basic_units[SC2Race.ZERG].union({ + item_names.ZERGLING, + item_names.PYGALISK, + item_names.INFESTED_SIEGE_TANK, + item_names.ULTRALISK, + item_names.SWARM_HOST + }), + SC2Race.PROTOSS: advanced_basic_units[SC2Race.PROTOSS].union({ + item_names.BLOOD_HUNTER, + item_names.STALWART, + item_names.CARRIER, + item_names.SKYLORD, + item_names.TRIREME, + item_names.TEMPEST, + item_names.VOID_RAY, + item_names.DESTROYER, + item_names.PULSAR, + item_names.DAWNBRINGER, + item_names.COLOSSUS, + item_names.WRATHWALKER, + item_names.SCOUT, + item_names.OPPRESSOR, + item_names.MISTWING, + item_names.HIGH_TEMPLAR, + item_names.SIGNIFIER, + item_names.ASCENDANT, + item_names.DARK_ARCHON, + item_names.SUPPLICANT, + }) +} + +not_balanced_starting_units = { + item_names.SIEGE_TANK, + item_names.THOR, + item_names.BANSHEE, + item_names.BATTLECRUISER, + item_names.ULTRALISK, + item_names.CARRIER, + item_names.TEMPEST, +} + + +# Defense rating table +# Commented defense ratings are handled in LogicMixin +tvx_defense_ratings = { + item_names.SIEGE_TANK: 5, + # "Graduating Range": 1, + item_names.PLANETARY_FORTRESS: 3, + # Bunker w/ Marine/Marauder: 3, + item_names.PERDITION_TURRET: 2, + item_names.DEVASTATOR_TURRET: 2, + item_names.VULTURE: 1, + item_names.BANSHEE: 1, + item_names.BATTLECRUISER: 1, + item_names.LIBERATOR: 4, + item_names.WIDOW_MINE: 1, + # "Concealment (Widow Mine)": 1 +} +tvz_defense_ratings = { + item_names.PERDITION_TURRET: 2, + # Bunker w/ Firebat: 2, + item_names.LIBERATOR: -2, + item_names.HIVE_MIND_EMULATOR: 3, + item_names.PSI_DISRUPTER: 3, +} +tvx_air_defense_ratings = { + item_names.MISSILE_TURRET: 2, +} +zvx_defense_ratings = { + # Note that this doesn't include Kerrigan because this is just for race swaps, which doesn't involve her (for now) + item_names.SPINE_CRAWLER: 3, + # w/ Twin Drones: 1 + item_names.SWARM_QUEEN: 1, + item_names.SWARM_HOST: 1, + # impaler: 3 + # "Hardened Tentacle Spines (Impaler)": 2 + # lurker: 1 + # "Seismic Spines (Lurker)": 2 + # "Adapted Spines (Lurker)": 1 + # brood lord : 2 + # corpser roach: 1 + # creep tumors (swarm queen or overseer): 1 + # w/ malignant creep: 1 + # tanks with ammo: 5 + item_names.INFESTED_BUNKER: 3, + item_names.BILE_LAUNCHER: 2, +} +# zvz_defense_ratings = { + # corpser roach: 1 + # primal igniter: 2 + # lurker: 1 + # w/ adapted spines: -1 + # impaler: -1 +# } +zvx_air_defense_ratings = { + item_names.SPORE_CRAWLER: 2, + # w/ Twin Drones: 1 + item_names.INFESTED_MISSILE_TURRET: 2, +} +pvx_defense_ratings = { + item_names.PHOTON_CANNON: 2, + item_names.KHAYDARIN_MONOLITH: 3, + item_names.SHIELD_BATTERY: 1, + item_names.NEXUS_OVERCHARGE: 2, + item_names.SKYLORD: 1, + item_names.MATRIX_OVERLOAD: 1, + item_names.COLOSSUS: 1, + item_names.VANGUARD: 1, + item_names.REAVER: 1, +} +pvz_defense_ratings = { + item_names.KHAYDARIN_MONOLITH: -2, + item_names.COLOSSUS: 1, +} + +terran_passive_ratings = { + item_names.AUTOMATED_REFINERY: 4, + item_names.COMMAND_CENTER_MULE: 4, + item_names.ORBITAL_DEPOTS: 2, + item_names.COMMAND_CENTER_COMMAND_CENTER_REACTOR: 2, + item_names.COMMAND_CENTER_EXTRA_SUPPLIES: 2, + item_names.MICRO_FILTERING: 2, + item_names.TECH_REACTOR: 2 +} + +zerg_passive_ratings = { + item_names.TWIN_DRONES: 7, + item_names.AUTOMATED_EXTRACTORS: 4, + item_names.VESPENE_EFFICIENCY: 3, + item_names.OVERLORD_IMPROVED_OVERLORDS: 4, + item_names.MALIGNANT_CREEP: 2 +} + +protoss_passive_ratings = { + item_names.QUATRO: 4, + item_names.ORBITAL_ASSIMILATORS: 4, + item_names.AMPLIFIED_ASSIMILATORS: 3, + item_names.PROBE_WARPIN: 2, + item_names.ELDER_PROBES: 2, + item_names.MATRIX_OVERLOAD: 2 +} + +soa_energy_ratings = { + item_names.SOA_SOLAR_LANCE: 8, + item_names.SOA_DEPLOY_FENIX: 7, + item_names.SOA_TEMPORAL_FIELD: 6, + item_names.SOA_PROGRESSIVE_PROXY_PYLON: 5, # Requires Lvl 2 (Warp in Reinforcements) + item_names.SOA_SHIELD_OVERCHARGE: 5, + item_names.SOA_ORBITAL_STRIKE: 4 +} + +soa_passive_ratings = { + item_names.GUARDIAN_SHELL: 4, + item_names.OVERWATCH: 2 +} + +soa_ultimate_ratings = { + item_names.SOA_TIME_STOP: 4, + item_names.SOA_PURIFIER_BEAM: 3, + item_names.SOA_SOLAR_BOMBARDMENT: 3 +} + +kerrigan_levels = [ + item_name for item_name, item_data in item_table.items() + if item_data.type == ZergItemType.Level and item_data.race == SC2Race.ZERG +] + + +spear_of_adun_calldowns = { + item_names.SOA_CHRONO_SURGE, + item_names.SOA_PROGRESSIVE_PROXY_PYLON, + item_names.SOA_PYLON_OVERCHARGE, + item_names.SOA_ORBITAL_STRIKE, + item_names.SOA_TEMPORAL_FIELD, + item_names.SOA_SOLAR_LANCE, + item_names.SOA_MASS_RECALL, + item_names.SOA_SHIELD_OVERCHARGE, + item_names.SOA_DEPLOY_FENIX, + item_names.SOA_PURIFIER_BEAM, + item_names.SOA_TIME_STOP, + item_names.SOA_SOLAR_BOMBARDMENT +} + +spear_of_adun_castable_passives = { + item_names.RECONSTRUCTION_BEAM, + item_names.OVERWATCH, + item_names.GUARDIAN_SHELL, +} + +nova_equipment = { + *[item_name for item_name, item_data in get_full_item_list().items() + if item_data.type == TerranItemType.Nova_Gear], + item_names.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE +} + +upgrade_bundles: Dict[str, List[str]] = { + # Terran + item_names.PROGRESSIVE_TERRAN_WEAPON_UPGRADE: + [ + item_names.PROGRESSIVE_TERRAN_INFANTRY_WEAPON, + item_names.PROGRESSIVE_TERRAN_VEHICLE_WEAPON, + item_names.PROGRESSIVE_TERRAN_SHIP_WEAPON + ], + item_names.PROGRESSIVE_TERRAN_ARMOR_UPGRADE: + [ + item_names.PROGRESSIVE_TERRAN_INFANTRY_ARMOR, + item_names.PROGRESSIVE_TERRAN_VEHICLE_ARMOR, + item_names.PROGRESSIVE_TERRAN_SHIP_ARMOR + ], + item_names.PROGRESSIVE_TERRAN_INFANTRY_UPGRADE: + [ + item_names.PROGRESSIVE_TERRAN_INFANTRY_WEAPON, item_names.PROGRESSIVE_TERRAN_INFANTRY_ARMOR + ], + item_names.PROGRESSIVE_TERRAN_VEHICLE_UPGRADE: + [ + item_names.PROGRESSIVE_TERRAN_VEHICLE_WEAPON, item_names.PROGRESSIVE_TERRAN_VEHICLE_ARMOR + ], + item_names.PROGRESSIVE_TERRAN_SHIP_UPGRADE: + [ + item_names.PROGRESSIVE_TERRAN_SHIP_WEAPON, item_names.PROGRESSIVE_TERRAN_SHIP_ARMOR + ], + item_names.PROGRESSIVE_TERRAN_WEAPON_ARMOR_UPGRADE: + [ + item_names.PROGRESSIVE_TERRAN_INFANTRY_WEAPON, item_names.PROGRESSIVE_TERRAN_INFANTRY_ARMOR, + item_names.PROGRESSIVE_TERRAN_VEHICLE_WEAPON, item_names.PROGRESSIVE_TERRAN_VEHICLE_ARMOR, + item_names.PROGRESSIVE_TERRAN_SHIP_WEAPON, item_names.PROGRESSIVE_TERRAN_SHIP_ARMOR + ], + # Zerg + item_names.PROGRESSIVE_ZERG_WEAPON_UPGRADE: + [ + item_names.PROGRESSIVE_ZERG_MELEE_ATTACK, + item_names.PROGRESSIVE_ZERG_MISSILE_ATTACK, + item_names.PROGRESSIVE_ZERG_FLYER_ATTACK + ], + item_names.PROGRESSIVE_ZERG_ARMOR_UPGRADE: + [ + item_names.PROGRESSIVE_ZERG_GROUND_CARAPACE, item_names.PROGRESSIVE_ZERG_FLYER_CARAPACE + ], + item_names.PROGRESSIVE_ZERG_GROUND_UPGRADE: + [ + item_names.PROGRESSIVE_ZERG_MELEE_ATTACK, + item_names.PROGRESSIVE_ZERG_MISSILE_ATTACK, + item_names.PROGRESSIVE_ZERG_GROUND_CARAPACE + ], + item_names.PROGRESSIVE_ZERG_FLYER_UPGRADE: + [ + item_names.PROGRESSIVE_ZERG_FLYER_ATTACK, item_names.PROGRESSIVE_ZERG_FLYER_CARAPACE + ], + item_names.PROGRESSIVE_ZERG_WEAPON_ARMOR_UPGRADE: + [ + item_names.PROGRESSIVE_ZERG_MELEE_ATTACK, + item_names.PROGRESSIVE_ZERG_MISSILE_ATTACK, + item_names.PROGRESSIVE_ZERG_GROUND_CARAPACE, + item_names.PROGRESSIVE_ZERG_FLYER_ATTACK, + item_names.PROGRESSIVE_ZERG_FLYER_CARAPACE + ], + # Protoss + item_names.PROGRESSIVE_PROTOSS_WEAPON_UPGRADE: + [ + item_names.PROGRESSIVE_PROTOSS_GROUND_WEAPON, item_names.PROGRESSIVE_PROTOSS_AIR_WEAPON + ], + item_names.PROGRESSIVE_PROTOSS_ARMOR_UPGRADE: + [ + item_names.PROGRESSIVE_PROTOSS_GROUND_ARMOR, item_names.PROGRESSIVE_PROTOSS_AIR_ARMOR, + item_names.PROGRESSIVE_PROTOSS_SHIELDS + ], + item_names.PROGRESSIVE_PROTOSS_GROUND_UPGRADE: + [ + item_names.PROGRESSIVE_PROTOSS_GROUND_WEAPON, item_names.PROGRESSIVE_PROTOSS_GROUND_ARMOR, + item_names.PROGRESSIVE_PROTOSS_SHIELDS + ], + item_names.PROGRESSIVE_PROTOSS_AIR_UPGRADE: + [ + item_names.PROGRESSIVE_PROTOSS_AIR_WEAPON, item_names.PROGRESSIVE_PROTOSS_AIR_ARMOR, + item_names.PROGRESSIVE_PROTOSS_SHIELDS + ], + item_names.PROGRESSIVE_PROTOSS_WEAPON_ARMOR_UPGRADE: + [ + item_names.PROGRESSIVE_PROTOSS_GROUND_WEAPON, item_names.PROGRESSIVE_PROTOSS_GROUND_ARMOR, + item_names.PROGRESSIVE_PROTOSS_AIR_WEAPON, item_names.PROGRESSIVE_PROTOSS_AIR_ARMOR, + item_names.PROGRESSIVE_PROTOSS_SHIELDS + ], +} + +# Used for logic +upgrade_bundle_inverted_lookup: Dict[str, List[str]] = dict() +for key, values in upgrade_bundles.items(): + for value in values: + if upgrade_bundle_inverted_lookup.get(value) is None: + upgrade_bundle_inverted_lookup[value] = list() + if (value != item_names.PROGRESSIVE_PROTOSS_SHIELDS + or key not in [ + item_names.PROGRESSIVE_PROTOSS_GROUND_UPGRADE, + item_names.PROGRESSIVE_PROTOSS_AIR_UPGRADE + ] + ): + # Shield handling is trickier as it's max of Ground/Air group, not their sum + upgrade_bundle_inverted_lookup[value].append(key) + +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} + +upgrade_item_types = (TerranItemType.Upgrade, ZergItemType.Upgrade, ProtossItemType.Upgrade) diff --git a/worlds/sc2/item/parent_names.py b/worlds/sc2/item/parent_names.py new file mode 100644 index 00000000..8bf33bec --- /dev/null +++ b/worlds/sc2/item/parent_names.py @@ -0,0 +1,57 @@ +""" +Identifiers for complex item parent structures. +Defined separately from item_parents to avoid a circular import +item_names -> item_parent_names -> item_tables -> item_parents +""" + +# Terran +DOMINION_TROOPER_WEAPONS = "Dominion Trooper Weapons" +INFANTRY_UNITS = "Infantry Units" +INFANTRY_WEAPON_UNITS = "Infantry Weapon Units" +ORBITAL_COMMAND_AND_PLANETARY = "Orbital Command Abilities + Planetary Fortress" # MULE | Scan | Supply Drop +SIEGE_TANK_AND_TRANSPORT = "Siege Tank + Transport" +SIEGE_TANK_AND_MEDIVAC = "Siege Tank + Medivac" +SPIDER_MINE_SOURCE = "Spider Mine Source" +STARSHIP_UNITS = "Starship Units" +STARSHIP_WEAPON_UNITS = "Starship Weapon Units" +VEHICLE_UNITS = "Vehicle Units" +VEHICLE_WEAPON_UNITS = "Vehicle Weapon Units" +TERRAN_MERCENARIES = "Terran Mercenaries" + +# Zerg +ANY_NYDUS_WORM = "Any Nydus Worm" +BANELING_SOURCE = "Any Baneling Source" # Baneling aspect | Kerrigan Spawn Banelings +INFESTED_UNITS = "Infested Units" +INFESTED_FACTORY_OR_STARPORT = "Infested Factory or Starport" +MORPH_SOURCE_AIR = "Air Morph Source" # Morphling | Mutalisk | Corruptor +MORPH_SOURCE_ROACH = "Roach Morph Source" # Morphling | Roach +MORPH_SOURCE_ZERGLING = "Zergling Morph Source" # Morphling | Zergling +MORPH_SOURCE_HYDRALISK = "Hydralisk Morph Source" # Morphling | Hydralisk +MORPH_SOURCE_ULTRALISK = "Ultralisk Morph Source" # Morphling | Ultralisk +ZERG_UPROOTABLE_BUILDINGS = "Zerg Uprootable Buildings" +ZERG_MELEE_ATTACKER = "Zerg Melee Attacker" +ZERG_MISSILE_ATTACKER = "Zerg Missile Attacker" +ZERG_CARAPACE_UNIT = "Zerg Carapace Unit" +ZERG_FLYING_UNIT = "Zerg Flying Unit" +ZERG_MERCENARIES = "Zerg Mercenaries" +ZERG_OUROBOUROS_CONDITION = "Zerg Ourobouros Condition" + +# Protoss +ARCHON_SOURCE = "Any Archon Source" +CARRIER_CLASS = "Carrier Class" +CARRIER_OR_TRIREME = "Carrier | Trireme" +DARK_ARCHON_SOURCE = "Dark Archon Source" +DARK_TEMPLAR_CLASS = "Dark Templar Class" +STORM_CASTER = "Storm Caster" +IMMORTAL_OR_ANNIHILATOR = "Immortal | Annihilator" +PHOENIX_CLASS = "Phoenix Class" +SENTRY_CLASS = "Sentry Class" +SENTRY_CLASS_OR_SHIELD_BATTERY = "Sentry Class | Shield Battery" +STALKER_CLASS = "Stalker Class" +SUPPLICANT_AND_ASCENDANT = "Supplicant + Ascendant" +VOID_RAY_CLASS = "Void Ray Class" +ZEALOT_OR_SENTINEL_OR_CENTURION = "Zealot | Sentinel | Centurion" +PROTOSS_STATIC_DEFENSE = "Protoss Static Defense" +PROTOSS_ATTACKING_BUILDING = "Protoss Attacking Structure" +SCOUT_CLASS = "Scout Class" +SCOUT_OR_OPPRESSOR_OR_MISTWING = "Scout | Oppressor | Mist Wing" diff --git a/worlds/sc2/location_groups.py b/worlds/sc2/location_groups.py new file mode 100644 index 00000000..c353558f --- /dev/null +++ b/worlds/sc2/location_groups.py @@ -0,0 +1,40 @@ +""" +Location group definitions +""" + +from typing import Dict, Set, Iterable +from .locations import DEFAULT_LOCATION_LIST, LocationData +from .mission_tables import lookup_name_to_mission, MissionFlag + +def get_location_groups() -> Dict[str, Set[str]]: + result: Dict[str, Set[str]] = {} + locations: Iterable[LocationData] = DEFAULT_LOCATION_LIST + + for location in locations: + if location.code is None: + # Beat events + continue + mission = lookup_name_to_mission.get(location.region) + if mission is None: + continue + + if (MissionFlag.HasRaceSwap|MissionFlag.RaceSwap) & mission.flags: + # Location group including race-swapped variants of a location + agnostic_location_name = ( + location.name + .replace(' (Terran)', '') + .replace(' (Protoss)', '') + .replace(' (Zerg)', '') + ) + result.setdefault(agnostic_location_name, set()).add(location.name) + + # Location group including all locations in all raceswaps + result.setdefault(mission.mission_name[:mission.mission_name.find(' (')], set()).add(location.name) + + # Location group including all locations in a mission + result.setdefault(mission.mission_name, set()).add(location.name) + + # Location group by location category + result.setdefault(location.type.name.title(), set()).add(location.name) + + return result diff --git a/worlds/sc2/locations.py b/worlds/sc2/locations.py new file mode 100644 index 00000000..0e00f4d7 --- /dev/null +++ b/worlds/sc2/locations.py @@ -0,0 +1,14175 @@ +import enum +from typing import List, Tuple, Optional, Callable, NamedTuple, Set, TYPE_CHECKING +from .item import item_names +from .item.item_groups import kerrigan_logic_ultimates +from .options import ( + get_option_value, + RequiredTactics, + LocationInclusion, + KerriganPresence, + GrantStoryTech, + get_enabled_campaigns, +) +from .mission_tables import SC2Mission, SC2Campaign + +from BaseClasses import Location +from worlds.AutoWorld import World + +if TYPE_CHECKING: + from BaseClasses import CollectionState + from . import SC2World + +SC2WOL_LOC_ID_OFFSET = 1000 +SC2HOTS_LOC_ID_OFFSET = 20000000 # Avoid clashes with The Legend of Zelda +SC2LOTV_LOC_ID_OFFSET = SC2HOTS_LOC_ID_OFFSET + 2000 +SC2NCO_LOC_ID_OFFSET = SC2LOTV_LOC_ID_OFFSET + 2500 +SC2_RACESWAP_LOC_ID_OFFSET = SC2NCO_LOC_ID_OFFSET + 900 +VICTORY_CACHE_OFFSET = 90 + + +class SC2Location(Location): + game: str = "Starcraft2" + + +class LocationType(enum.IntEnum): + VICTORY = 0 # Winning a mission + VANILLA = 1 # Objectives that provided metaprogression in the original campaign, along with a few other locations for a balanced experience + EXTRA = 2 # Additional locations based on mission progression, collecting in-mission rewards, etc. that do not significantly increase the challenge. + CHALLENGE = 3 # Challenging objectives, often harder than just completing a mission, and often associated with Achievements + MASTERY = 4 # Extremely challenging objectives often associated with Masteries and Feats of Strength in the original campaign + VICTORY_CACHE = 5 # Bonus locations for beating a mission + + +class LocationFlag(enum.IntFlag): + NONE = 0 + BASEBUST = enum.auto() + """Locations about killing challenging bases""" + SPEEDRUN = enum.auto() + """Locations that are about doing something fast""" + PREVENTATIVE = enum.auto() + """Locations that are about preventing something from happening""" + + def values(self): + """Hacky iterator for backwards-compatibility with Python <= 3.10. Not necessary on Python 3.11+""" + return tuple( + val + for val in ( + LocationFlag.SPEEDRUN, + LocationFlag.PREVENTATIVE, + ) + if val in self + ) + + +class LocationData(NamedTuple): + region: str + name: str + code: int + type: LocationType + rule: Callable[["CollectionState"], bool] = Location.access_rule + flags: LocationFlag = LocationFlag.NONE + hard_rule: Optional[Callable[["CollectionState"], bool]] = None + + +def make_location_data( + region: str, + name: str, + code: int, + type: LocationType, + rule: Callable[["CollectionState"], bool] = Location.access_rule, + flags: LocationFlag = LocationFlag.NONE, + hard_rule: Optional[Callable[["CollectionState"], bool]] = None, +) -> LocationData: + return LocationData(region, f"{region}: {name}", code, type, rule, flags, hard_rule) + + +def get_location_types(world: "SC2World", inclusion_type: int) -> Set[LocationType]: + """ + :param world: The starcraft 2 world object + :param inclusion_type: Level of inclusion to check for + :return: A list of location types that match the inclusion type + """ + exclusion_options = [ + ("vanilla_locations", LocationType.VANILLA), + ("extra_locations", LocationType.EXTRA), + ("challenge_locations", LocationType.CHALLENGE), + ("mastery_locations", LocationType.MASTERY), + ] + excluded_location_types = set() + for option_name, location_type in exclusion_options: + if get_option_value(world, option_name) is inclusion_type: + excluded_location_types.add(location_type) + return excluded_location_types + + +def get_location_flags(world: "SC2World", inclusion_type: int) -> LocationFlag: + """ + :param world: The starcraft 2 world object + :param inclusion_type: Level of inclusion to check for + :return: A list of location types that match the inclusion type + """ + matching_location_flags = LocationFlag.NONE + if world.options.basebust_locations.value == inclusion_type: + matching_location_flags |= LocationFlag.BASEBUST + if world.options.speedrun_locations.value == inclusion_type: + matching_location_flags |= LocationFlag.SPEEDRUN + if world.options.preventative_locations.value == inclusion_type: + matching_location_flags |= LocationFlag.PREVENTATIVE + return matching_location_flags + + +def get_plando_locations(world: World) -> List[str]: + """ + :param multiworld: + :param player: + :return: A list of locations affected by a plando in a world + """ + if world is None: + return [] + plando_locations = [] + for plando_setting in world.options.plando_items: + plando_locations += plando_setting.locations + + return plando_locations + + +def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: + # Note: rules which are ended with or True are rules identified as needed later when restricted units is an option + if world is None: + logic_level = int(RequiredTactics.default) + kerriganless = False + else: + logic_level = world.options.required_tactics.value + kerriganless = ( + world.options.kerrigan_presence.value != KerriganPresence.option_vanilla + or SC2Campaign.HOTS not in get_enabled_campaigns(world) + ) + adv_tactics = logic_level != RequiredTactics.option_standard + if world is not None and world.logic is not None: + logic = world.logic + else: + from .rules import SC2Logic + + logic = SC2Logic(world) + player = 1 if world is None else world.player + location_table: List[LocationData] = [ + # WoL + make_location_data( + SC2Mission.LIBERATION_DAY.mission_name, + "Victory", + SC2WOL_LOC_ID_OFFSET + 100, + LocationType.VICTORY, + ), + make_location_data( + SC2Mission.LIBERATION_DAY.mission_name, + "First Statue", + SC2WOL_LOC_ID_OFFSET + 101, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.LIBERATION_DAY.mission_name, + "Second Statue", + SC2WOL_LOC_ID_OFFSET + 102, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.LIBERATION_DAY.mission_name, + "Third Statue", + SC2WOL_LOC_ID_OFFSET + 103, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.LIBERATION_DAY.mission_name, + "Fourth Statue", + SC2WOL_LOC_ID_OFFSET + 104, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.LIBERATION_DAY.mission_name, + "Fifth Statue", + SC2WOL_LOC_ID_OFFSET + 105, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.LIBERATION_DAY.mission_name, + "Sixth Statue", + SC2WOL_LOC_ID_OFFSET + 106, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.LIBERATION_DAY.mission_name, + "Special Delivery", + SC2WOL_LOC_ID_OFFSET + 107, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.LIBERATION_DAY.mission_name, + "Transport", + SC2WOL_LOC_ID_OFFSET + 108, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.THE_OUTLAWS.mission_name, + "Victory", + SC2WOL_LOC_ID_OFFSET + 200, + LocationType.VICTORY, + logic.terran_early_tech, + ), + make_location_data( + SC2Mission.THE_OUTLAWS.mission_name, + "Rebel Base", + SC2WOL_LOC_ID_OFFSET + 201, + LocationType.VANILLA, + logic.terran_early_tech, + ), + make_location_data( + SC2Mission.THE_OUTLAWS.mission_name, + "North Resource Pickups", + SC2WOL_LOC_ID_OFFSET + 202, + LocationType.EXTRA, + logic.terran_early_tech, + ), + make_location_data( + SC2Mission.THE_OUTLAWS.mission_name, + "Bunker", + SC2WOL_LOC_ID_OFFSET + 203, + LocationType.VANILLA, + logic.terran_early_tech, + ), + make_location_data( + SC2Mission.THE_OUTLAWS.mission_name, + "Close Resource Pickups", + SC2WOL_LOC_ID_OFFSET + 204, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.ZERO_HOUR.mission_name, + "Victory", + SC2WOL_LOC_ID_OFFSET + 300, + LocationType.VICTORY, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_defense_rating(state, True) >= 2 + and (adv_tactics or logic.terran_basic_anti_air(state)) + ), + ), + make_location_data( + SC2Mission.ZERO_HOUR.mission_name, + "First Group Rescued", + SC2WOL_LOC_ID_OFFSET + 301, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.ZERO_HOUR.mission_name, + "Second Group Rescued", + SC2WOL_LOC_ID_OFFSET + 302, + LocationType.VANILLA, + logic.terran_common_unit, + ), + make_location_data( + SC2Mission.ZERO_HOUR.mission_name, + "Third Group Rescued", + SC2WOL_LOC_ID_OFFSET + 303, + LocationType.VANILLA, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_defense_rating(state, True) >= 2 + ), + ), + make_location_data( + SC2Mission.ZERO_HOUR.mission_name, + "First Hatchery", + SC2WOL_LOC_ID_OFFSET + 304, + LocationType.CHALLENGE, + logic.terran_competent_comp, + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.ZERO_HOUR.mission_name, + "Second Hatchery", + SC2WOL_LOC_ID_OFFSET + 305, + LocationType.CHALLENGE, + logic.terran_competent_comp, + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.ZERO_HOUR.mission_name, + "Third Hatchery", + SC2WOL_LOC_ID_OFFSET + 306, + LocationType.CHALLENGE, + logic.terran_competent_comp, + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.ZERO_HOUR.mission_name, + "Fourth Hatchery", + SC2WOL_LOC_ID_OFFSET + 307, + LocationType.CHALLENGE, + logic.terran_competent_comp, + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.ZERO_HOUR.mission_name, + "Ride's on its Way", + SC2WOL_LOC_ID_OFFSET + 308, + LocationType.EXTRA, + logic.terran_common_unit, + ), + make_location_data( + SC2Mission.ZERO_HOUR.mission_name, + "Hold Just a Little Longer", + SC2WOL_LOC_ID_OFFSET + 309, + LocationType.EXTRA, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_defense_rating(state, True) >= 2 + ), + ), + make_location_data( + SC2Mission.ZERO_HOUR.mission_name, + "Cavalry's on the Way", + SC2WOL_LOC_ID_OFFSET + 310, + LocationType.EXTRA, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_defense_rating(state, True) >= 2 + ), + ), + make_location_data( + SC2Mission.EVACUATION.mission_name, + "Victory", + SC2WOL_LOC_ID_OFFSET + 400, + LocationType.VICTORY, + lambda state: ( + logic.terran_early_tech(state) + and ( + (adv_tactics and logic.terran_basic_anti_air(state)) + or logic.terran_competent_anti_air(state) + ) + ), + ), + make_location_data( + SC2Mission.EVACUATION.mission_name, + "North Chrysalis", + SC2WOL_LOC_ID_OFFSET + 401, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.EVACUATION.mission_name, + "West Chrysalis", + SC2WOL_LOC_ID_OFFSET + 402, + LocationType.VANILLA, + logic.terran_early_tech, + ), + make_location_data( + SC2Mission.EVACUATION.mission_name, + "East Chrysalis", + SC2WOL_LOC_ID_OFFSET + 403, + LocationType.VANILLA, + logic.terran_early_tech, + ), + make_location_data( + SC2Mission.EVACUATION.mission_name, + "Reach Hanson", + SC2WOL_LOC_ID_OFFSET + 404, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.EVACUATION.mission_name, + "Secret Resource Stash", + SC2WOL_LOC_ID_OFFSET + 405, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.EVACUATION.mission_name, + "Flawless", + SC2WOL_LOC_ID_OFFSET + 406, + LocationType.CHALLENGE, + lambda state: ( + logic.terran_early_tech(state) + and logic.terran_defense_rating(state, True, False) >= 2 + and ( + (adv_tactics and logic.terran_basic_anti_air(state)) + or logic.terran_competent_anti_air(state) + ) + ), + flags=LocationFlag.PREVENTATIVE, + ), + make_location_data( + SC2Mission.EVACUATION.mission_name, + "Western Zerg Base", + SC2WOL_LOC_ID_OFFSET + 407, + LocationType.MASTERY, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_base_trasher(state) + and logic.terran_competent_anti_air(state) + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.EVACUATION.mission_name, + "Eastern Zerg Base", + SC2WOL_LOC_ID_OFFSET + 408, + LocationType.MASTERY, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_base_trasher(state) + and logic.terran_competent_anti_air(state) + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.OUTBREAK.mission_name, + "Victory", + SC2WOL_LOC_ID_OFFSET + 500, + LocationType.VICTORY, + logic.terran_outbreak_requirement, + ), + make_location_data( + SC2Mission.OUTBREAK.mission_name, + "Left Infestor", + SC2WOL_LOC_ID_OFFSET + 501, + LocationType.VANILLA, + logic.terran_outbreak_requirement, + ), + make_location_data( + SC2Mission.OUTBREAK.mission_name, + "Right Infestor", + SC2WOL_LOC_ID_OFFSET + 502, + LocationType.VANILLA, + logic.terran_outbreak_requirement, + ), + make_location_data( + SC2Mission.OUTBREAK.mission_name, + "North Infested Command Center", + SC2WOL_LOC_ID_OFFSET + 503, + LocationType.EXTRA, + logic.terran_outbreak_requirement, + ), + make_location_data( + SC2Mission.OUTBREAK.mission_name, + "South Infested Command Center", + SC2WOL_LOC_ID_OFFSET + 504, + LocationType.EXTRA, + logic.terran_outbreak_requirement, + ), + make_location_data( + SC2Mission.OUTBREAK.mission_name, + "Northwest Bar", + SC2WOL_LOC_ID_OFFSET + 505, + LocationType.EXTRA, + logic.terran_outbreak_requirement, + ), + make_location_data( + SC2Mission.OUTBREAK.mission_name, + "North Bar", + SC2WOL_LOC_ID_OFFSET + 506, + LocationType.EXTRA, + logic.terran_outbreak_requirement, + ), + make_location_data( + SC2Mission.OUTBREAK.mission_name, + "South Bar", + SC2WOL_LOC_ID_OFFSET + 507, + LocationType.EXTRA, + logic.terran_outbreak_requirement, + ), + make_location_data( + SC2Mission.SAFE_HAVEN.mission_name, + "Victory", + SC2WOL_LOC_ID_OFFSET + 600, + LocationType.VICTORY, + logic.terran_safe_haven_requirement, + hard_rule=logic.terran_any_anti_air, + ), + make_location_data( + SC2Mission.SAFE_HAVEN.mission_name, + "North Nexus", + SC2WOL_LOC_ID_OFFSET + 601, + LocationType.EXTRA, + logic.terran_safe_haven_requirement, + ), + make_location_data( + SC2Mission.SAFE_HAVEN.mission_name, + "East Nexus", + SC2WOL_LOC_ID_OFFSET + 602, + LocationType.EXTRA, + logic.terran_safe_haven_requirement, + ), + make_location_data( + SC2Mission.SAFE_HAVEN.mission_name, + "South Nexus", + SC2WOL_LOC_ID_OFFSET + 603, + LocationType.EXTRA, + logic.terran_safe_haven_requirement, + ), + make_location_data( + SC2Mission.SAFE_HAVEN.mission_name, + "First Terror Fleet", + SC2WOL_LOC_ID_OFFSET + 604, + LocationType.VANILLA, + logic.terran_safe_haven_requirement, + hard_rule=logic.terran_any_anti_air, + ), + make_location_data( + SC2Mission.SAFE_HAVEN.mission_name, + "Second Terror Fleet", + SC2WOL_LOC_ID_OFFSET + 605, + LocationType.VANILLA, + logic.terran_safe_haven_requirement, + hard_rule=logic.terran_any_anti_air, + ), + make_location_data( + SC2Mission.SAFE_HAVEN.mission_name, + "Third Terror Fleet", + SC2WOL_LOC_ID_OFFSET + 606, + LocationType.VANILLA, + logic.terran_safe_haven_requirement, + hard_rule=logic.terran_any_anti_air, + ), + make_location_data( + SC2Mission.HAVENS_FALL.mission_name, + "Victory", + SC2WOL_LOC_ID_OFFSET + 700, + LocationType.VICTORY, + logic.terran_havens_fall_requirement, + hard_rule=logic.terran_any_anti_air_or_science_vessels, + ), + make_location_data( + SC2Mission.HAVENS_FALL.mission_name, + "North Hive", + SC2WOL_LOC_ID_OFFSET + 701, + LocationType.VANILLA, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.HAVENS_FALL.mission_name, + "East Hive", + SC2WOL_LOC_ID_OFFSET + 702, + LocationType.VANILLA, + logic.terran_havens_fall_requirement, + ), + make_location_data( + SC2Mission.HAVENS_FALL.mission_name, + "South Hive", + SC2WOL_LOC_ID_OFFSET + 703, + LocationType.VANILLA, + logic.terran_havens_fall_requirement, + ), + make_location_data( + SC2Mission.HAVENS_FALL.mission_name, + "Northeast Colony Base", + SC2WOL_LOC_ID_OFFSET + 704, + LocationType.CHALLENGE, + logic.terran_respond_to_colony_infestations, + hard_rule=logic.terran_any_anti_air_or_science_vessels, + ), + make_location_data( + SC2Mission.HAVENS_FALL.mission_name, + "East Colony Base", + SC2WOL_LOC_ID_OFFSET + 705, + LocationType.CHALLENGE, + logic.terran_respond_to_colony_infestations, + hard_rule=logic.terran_any_anti_air_or_science_vessels, + ), + make_location_data( + SC2Mission.HAVENS_FALL.mission_name, + "Middle Colony Base", + SC2WOL_LOC_ID_OFFSET + 706, + LocationType.CHALLENGE, + logic.terran_respond_to_colony_infestations, + hard_rule=logic.terran_any_anti_air_or_science_vessels, + ), + make_location_data( + SC2Mission.HAVENS_FALL.mission_name, + "Southeast Colony Base", + SC2WOL_LOC_ID_OFFSET + 707, + LocationType.CHALLENGE, + logic.terran_respond_to_colony_infestations, + hard_rule=logic.terran_any_anti_air_or_science_vessels, + ), + make_location_data( + SC2Mission.HAVENS_FALL.mission_name, + "Southwest Colony Base", + SC2WOL_LOC_ID_OFFSET + 708, + LocationType.CHALLENGE, + logic.terran_respond_to_colony_infestations, + hard_rule=logic.terran_any_anti_air_or_science_vessels, + ), + make_location_data( + SC2Mission.HAVENS_FALL.mission_name, + "Southwest Gas Pickups", + SC2WOL_LOC_ID_OFFSET + 709, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.HAVENS_FALL.mission_name, + "East Gas Pickups", + SC2WOL_LOC_ID_OFFSET + 710, + LocationType.EXTRA, + logic.terran_havens_fall_requirement, + ), + make_location_data( + SC2Mission.HAVENS_FALL.mission_name, + "Southeast Gas Pickups", + SC2WOL_LOC_ID_OFFSET + 711, + LocationType.EXTRA, + logic.terran_havens_fall_requirement, + ), + make_location_data( + SC2Mission.SMASH_AND_GRAB.mission_name, + "Victory", + SC2WOL_LOC_ID_OFFSET + 800, + LocationType.VICTORY, + lambda state: ( + logic.terran_common_unit(state) + and ( + adv_tactics + and logic.terran_moderate_anti_air(state) + or logic.terran_competent_anti_air(state) + ) + ), + ), + make_location_data( + SC2Mission.SMASH_AND_GRAB.mission_name, + "First Relic", + SC2WOL_LOC_ID_OFFSET + 801, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.SMASH_AND_GRAB.mission_name, + "Second Relic", + SC2WOL_LOC_ID_OFFSET + 802, + LocationType.VANILLA, + lambda state: (adv_tactics or logic.terran_common_unit(state)), + ), + make_location_data( + SC2Mission.SMASH_AND_GRAB.mission_name, + "Third Relic", + SC2WOL_LOC_ID_OFFSET + 803, + LocationType.VANILLA, + lambda state: ( + logic.terran_common_unit(state) + and ( + adv_tactics + and logic.terran_moderate_anti_air(state) + or logic.terran_competent_anti_air(state) + ) + ), + ), + make_location_data( + SC2Mission.SMASH_AND_GRAB.mission_name, + "Fourth Relic", + SC2WOL_LOC_ID_OFFSET + 804, + LocationType.VANILLA, + lambda state: ( + logic.terran_common_unit(state) + and ( + adv_tactics + and logic.terran_moderate_anti_air(state) + or logic.terran_competent_anti_air(state) + ) + ), + ), + make_location_data( + SC2Mission.SMASH_AND_GRAB.mission_name, + "First Forcefield Area Busted", + SC2WOL_LOC_ID_OFFSET + 805, + LocationType.EXTRA, + lambda state: ( + logic.terran_common_unit(state) + and ( + adv_tactics + and logic.terran_moderate_anti_air(state) + or logic.terran_competent_anti_air(state) + ) + ), + ), + make_location_data( + SC2Mission.SMASH_AND_GRAB.mission_name, + "Second Forcefield Area Busted", + SC2WOL_LOC_ID_OFFSET + 806, + LocationType.EXTRA, + lambda state: ( + logic.terran_common_unit(state) + and ( + adv_tactics + and logic.terran_moderate_anti_air(state) + or logic.terran_competent_anti_air(state) + ) + ), + ), + make_location_data( + SC2Mission.SMASH_AND_GRAB.mission_name, + "Defeat Kerrigan", + SC2WOL_LOC_ID_OFFSET + 807, + LocationType.MASTERY, + lambda state: ( + logic.terran_common_unit(state) and logic.terran_base_trasher(state) + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.THE_DIG.mission_name, + "Victory", + SC2WOL_LOC_ID_OFFSET + 900, + LocationType.VICTORY, + lambda state: ( + ( + logic.terran_competent_anti_air(state) + or adv_tactics + and logic.terran_moderate_anti_air(state) + ) + and logic.terran_defense_rating(state, False, True) >= 8 + and logic.terran_common_unit(state) + and (logic.marine_medic_upgrade(state) or adv_tactics) + ), + ), + make_location_data( + SC2Mission.THE_DIG.mission_name, + "Left Relic", + SC2WOL_LOC_ID_OFFSET + 901, + LocationType.VANILLA, + lambda state: ( + logic.terran_defense_rating(state, False, False) >= 6 + and logic.terran_common_unit(state) + and (logic.marine_medic_upgrade(state) or adv_tactics) + ), + ), + make_location_data( + SC2Mission.THE_DIG.mission_name, + "Right Ground Relic", + SC2WOL_LOC_ID_OFFSET + 902, + LocationType.VANILLA, + lambda state: ( + logic.terran_defense_rating(state, False, False) >= 6 + and logic.terran_common_unit(state) + and (logic.marine_medic_upgrade(state) or adv_tactics) + ), + ), + make_location_data( + SC2Mission.THE_DIG.mission_name, + "Right Cliff Relic", + SC2WOL_LOC_ID_OFFSET + 903, + LocationType.VANILLA, + lambda state: ( + logic.terran_defense_rating(state, False, False) >= 6 + and logic.terran_common_unit(state) + and (logic.marine_medic_upgrade(state) or adv_tactics) + ), + ), + make_location_data( + SC2Mission.THE_DIG.mission_name, + "Moebius Base", + SC2WOL_LOC_ID_OFFSET + 904, + LocationType.EXTRA, + lambda state: logic.marine_medic_upgrade(state) or adv_tactics, + ), + make_location_data( + SC2Mission.THE_DIG.mission_name, + "Door Outer Layer", + SC2WOL_LOC_ID_OFFSET + 905, + LocationType.EXTRA, + lambda state: ( + logic.terran_defense_rating(state, False, False) >= 6 + and logic.terran_common_unit(state) + and (logic.marine_medic_upgrade(state) or adv_tactics) + ), + ), + make_location_data( + SC2Mission.THE_DIG.mission_name, + "Door Thermal Barrier", + SC2WOL_LOC_ID_OFFSET + 906, + LocationType.EXTRA, + lambda state: ( + ( + logic.terran_competent_anti_air(state) + or adv_tactics + and logic.terran_moderate_anti_air(state) + ) + and logic.terran_defense_rating(state, False, True) >= 8 + and logic.terran_common_unit(state) + and (logic.marine_medic_upgrade(state) or adv_tactics) + ), + ), + make_location_data( + SC2Mission.THE_DIG.mission_name, + "Cutting Through the Core", + SC2WOL_LOC_ID_OFFSET + 907, + LocationType.EXTRA, + lambda state: ( + ( + logic.terran_competent_anti_air(state) + or adv_tactics + and logic.terran_moderate_anti_air(state) + ) + and logic.terran_defense_rating(state, False, True) >= 8 + and logic.terran_common_unit(state) + and (logic.marine_medic_upgrade(state) or adv_tactics) + ), + ), + make_location_data( + SC2Mission.THE_DIG.mission_name, + "Structure Access Imminent", + SC2WOL_LOC_ID_OFFSET + 908, + LocationType.EXTRA, + lambda state: ( + ( + logic.terran_competent_anti_air(state) + or adv_tactics + and logic.terran_moderate_anti_air(state) + ) + and logic.terran_defense_rating(state, False, True) >= 8 + and logic.terran_common_unit(state) + and (logic.marine_medic_upgrade(state) or adv_tactics) + ), + ), + make_location_data( + SC2Mission.THE_DIG.mission_name, + "Northwestern Protoss Base", + SC2WOL_LOC_ID_OFFSET + 909, + LocationType.MASTERY, + lambda state: ( + logic.terran_beats_protoss_deathball(state) + and logic.terran_defense_rating(state, False, True) >= 8 + and logic.terran_common_unit(state) + and (logic.marine_medic_upgrade(state) or adv_tactics) + and logic.terran_base_trasher(state) + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.THE_DIG.mission_name, + "Northeastern Protoss Base", + SC2WOL_LOC_ID_OFFSET + 910, + LocationType.MASTERY, + lambda state: ( + logic.terran_beats_protoss_deathball(state) + and logic.terran_defense_rating(state, False, True) >= 8 + and logic.terran_common_unit(state) + and (logic.marine_medic_upgrade(state) or adv_tactics) + and logic.terran_base_trasher(state) + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.THE_DIG.mission_name, + "Eastern Protoss Base", + SC2WOL_LOC_ID_OFFSET + 911, + LocationType.MASTERY, + lambda state: ( + logic.terran_beats_protoss_deathball(state) + and logic.terran_defense_rating(state, False, True) >= 8 + and logic.terran_common_unit(state) + and logic.terran_base_trasher(state) + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.THE_MOEBIUS_FACTOR.mission_name, + "Victory", + SC2WOL_LOC_ID_OFFSET + 1000, + LocationType.VICTORY, + lambda state: ( + ( + logic.terran_moderate_anti_air(state) + and state.has_any({item_names.MEDIVAC, item_names.HERCULES}, player) + or logic.terran_air_anti_air(state) + ) + and ( + logic.terran_air(state) + or state.has_any({item_names.MEDIVAC, item_names.HERCULES}, player) + and logic.terran_common_unit(state) + ) + ), + ), + make_location_data( + SC2Mission.THE_MOEBIUS_FACTOR.mission_name, + "1st Data Core", + SC2WOL_LOC_ID_OFFSET + 1001, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.THE_MOEBIUS_FACTOR.mission_name, + "2nd Data Core", + SC2WOL_LOC_ID_OFFSET + 1002, + LocationType.VANILLA, + lambda state: ( + logic.terran_air(state) + or ( + state.has_any({item_names.MEDIVAC, item_names.HERCULES}, player) + and logic.terran_common_unit(state) + ) + ), + ), + make_location_data( + SC2Mission.THE_MOEBIUS_FACTOR.mission_name, + "South Rescue", + SC2WOL_LOC_ID_OFFSET + 1003, + LocationType.EXTRA, + logic.terran_can_rescue, + ), + make_location_data( + SC2Mission.THE_MOEBIUS_FACTOR.mission_name, + "Wall Rescue", + SC2WOL_LOC_ID_OFFSET + 1004, + LocationType.EXTRA, + logic.terran_can_rescue, + ), + make_location_data( + SC2Mission.THE_MOEBIUS_FACTOR.mission_name, + "Mid Rescue", + SC2WOL_LOC_ID_OFFSET + 1005, + LocationType.EXTRA, + logic.terran_can_rescue, + ), + make_location_data( + SC2Mission.THE_MOEBIUS_FACTOR.mission_name, + "Nydus Roof Rescue", + SC2WOL_LOC_ID_OFFSET + 1006, + LocationType.EXTRA, + logic.terran_can_rescue, + ), + make_location_data( + SC2Mission.THE_MOEBIUS_FACTOR.mission_name, + "Alive Inside Rescue", + SC2WOL_LOC_ID_OFFSET + 1007, + LocationType.EXTRA, + logic.terran_can_rescue, + ), + make_location_data( + SC2Mission.THE_MOEBIUS_FACTOR.mission_name, + "Brutalisk", + SC2WOL_LOC_ID_OFFSET + 1008, + LocationType.VANILLA, + lambda state: ( + ( + logic.terran_moderate_anti_air(state) + and state.has_any({item_names.MEDIVAC, item_names.HERCULES}, player) + or logic.terran_air_anti_air(state) + ) + and ( + logic.terran_air(state) + or state.has_any({item_names.MEDIVAC, item_names.HERCULES}, player) + and logic.terran_common_unit(state) + ) + ), + ), + make_location_data( + SC2Mission.THE_MOEBIUS_FACTOR.mission_name, + "3rd Data Core", + SC2WOL_LOC_ID_OFFSET + 1009, + LocationType.VANILLA, + lambda state: ( + ( + logic.terran_moderate_anti_air(state) + and state.has_any({item_names.MEDIVAC, item_names.HERCULES}, player) + or logic.terran_air_anti_air(state) + ) + and ( + logic.terran_air(state) + or state.has_any({item_names.MEDIVAC, item_names.HERCULES}, player) + and logic.terran_common_unit(state) + ) + ), + ), + make_location_data( + SC2Mission.SUPERNOVA.mission_name, + "Victory", + SC2WOL_LOC_ID_OFFSET + 1100, + LocationType.VICTORY, + logic.terran_supernova_requirement, + ), + make_location_data( + SC2Mission.SUPERNOVA.mission_name, + "West Relic", + SC2WOL_LOC_ID_OFFSET + 1101, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.SUPERNOVA.mission_name, + "North Relic", + SC2WOL_LOC_ID_OFFSET + 1102, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.SUPERNOVA.mission_name, + "South Relic", + SC2WOL_LOC_ID_OFFSET + 1103, + LocationType.VANILLA, + logic.terran_supernova_requirement, + ), + make_location_data( + SC2Mission.SUPERNOVA.mission_name, + "East Relic", + SC2WOL_LOC_ID_OFFSET + 1104, + LocationType.VANILLA, + logic.terran_supernova_requirement, + ), + make_location_data( + SC2Mission.SUPERNOVA.mission_name, + "Landing Zone Cleared", + SC2WOL_LOC_ID_OFFSET + 1105, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.SUPERNOVA.mission_name, + "Middle Base", + SC2WOL_LOC_ID_OFFSET + 1106, + LocationType.EXTRA, + logic.terran_supernova_requirement, + ), + make_location_data( + SC2Mission.SUPERNOVA.mission_name, + "Southeast Base", + SC2WOL_LOC_ID_OFFSET + 1107, + LocationType.EXTRA, + logic.terran_supernova_requirement, + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID.mission_name, + "Victory", + SC2WOL_LOC_ID_OFFSET + 1200, + LocationType.VICTORY, + logic.terran_maw_requirement, + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID.mission_name, + "Landing Zone Cleared", + SC2WOL_LOC_ID_OFFSET + 1201, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID.mission_name, + "Expansion Prisoners", + SC2WOL_LOC_ID_OFFSET + 1202, + LocationType.VANILLA, + lambda state: adv_tactics or logic.terran_maw_requirement(state), + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID.mission_name, + "South Close Prisoners", + SC2WOL_LOC_ID_OFFSET + 1203, + LocationType.VANILLA, + lambda state: adv_tactics or logic.terran_maw_requirement(state), + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID.mission_name, + "South Far Prisoners", + SC2WOL_LOC_ID_OFFSET + 1204, + LocationType.VANILLA, + logic.terran_maw_requirement, + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID.mission_name, + "North Prisoners", + SC2WOL_LOC_ID_OFFSET + 1205, + LocationType.VANILLA, + logic.terran_maw_requirement, + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID.mission_name, + "Mothership", + SC2WOL_LOC_ID_OFFSET + 1206, + LocationType.EXTRA, + logic.terran_maw_requirement, + hard_rule=logic.terran_any_anti_air, + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID.mission_name, + "Expansion Rip Field Generator", + SC2WOL_LOC_ID_OFFSET + 1207, + LocationType.EXTRA, + lambda state: adv_tactics or logic.terran_maw_requirement(state), + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID.mission_name, + "Middle Rip Field Generator", + SC2WOL_LOC_ID_OFFSET + 1208, + LocationType.EXTRA, + logic.terran_maw_requirement, + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID.mission_name, + "Southeast Rip Field Generator", + SC2WOL_LOC_ID_OFFSET + 1209, + LocationType.EXTRA, + logic.terran_maw_requirement, + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID.mission_name, + "Stargate Rip Field Generator", + SC2WOL_LOC_ID_OFFSET + 1210, + LocationType.EXTRA, + logic.terran_maw_requirement, + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID.mission_name, + "Northwest Rip Field Generator", + SC2WOL_LOC_ID_OFFSET + 1211, + LocationType.CHALLENGE, + logic.terran_maw_requirement, + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID.mission_name, + "West Rip Field Generator", + SC2WOL_LOC_ID_OFFSET + 1212, + LocationType.CHALLENGE, + logic.terran_maw_requirement, + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID.mission_name, + "Southwest Rip Field Generator", + SC2WOL_LOC_ID_OFFSET + 1213, + LocationType.CHALLENGE, + logic.terran_maw_requirement, + ), + make_location_data( + SC2Mission.DEVILS_PLAYGROUND.mission_name, + "Victory", + SC2WOL_LOC_ID_OFFSET + 1300, + LocationType.VICTORY, + lambda state: ( + adv_tactics + or logic.terran_moderate_anti_air(state) + and ( + logic.terran_common_unit(state) + or state.has(item_names.REAPER, player) + ) + ), + ), + make_location_data( + SC2Mission.DEVILS_PLAYGROUND.mission_name, + "Tosh's Miners", + SC2WOL_LOC_ID_OFFSET + 1301, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.DEVILS_PLAYGROUND.mission_name, + "Brutalisk", + SC2WOL_LOC_ID_OFFSET + 1302, + LocationType.VANILLA, + lambda state: adv_tactics + or logic.terran_common_unit(state) + or state.has(item_names.REAPER, player), + ), + make_location_data( + SC2Mission.DEVILS_PLAYGROUND.mission_name, + "North Reapers", + SC2WOL_LOC_ID_OFFSET + 1303, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.DEVILS_PLAYGROUND.mission_name, + "Middle Reapers", + SC2WOL_LOC_ID_OFFSET + 1304, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.DEVILS_PLAYGROUND.mission_name, + "Southwest Reapers", + SC2WOL_LOC_ID_OFFSET + 1305, + LocationType.EXTRA, + lambda state: adv_tactics + or logic.terran_common_unit(state) + or state.has(item_names.REAPER, player), + ), + make_location_data( + SC2Mission.DEVILS_PLAYGROUND.mission_name, + "Southeast Reapers", + SC2WOL_LOC_ID_OFFSET + 1306, + LocationType.EXTRA, + lambda state: ( + adv_tactics + or logic.terran_moderate_anti_air(state) + and ( + logic.terran_common_unit(state) + or state.has(item_names.REAPER, player) + ) + ), + ), + make_location_data( + SC2Mission.DEVILS_PLAYGROUND.mission_name, + "East Reapers", + SC2WOL_LOC_ID_OFFSET + 1307, + LocationType.EXTRA, + lambda state: ( + logic.terran_moderate_anti_air(state) + and ( + adv_tactics + or logic.terran_common_unit(state) + or state.has(item_names.REAPER, player) + ) + ), + ), + make_location_data( + SC2Mission.DEVILS_PLAYGROUND.mission_name, + "Zerg Cleared", + SC2WOL_LOC_ID_OFFSET + 1308, + LocationType.CHALLENGE, + lambda state: ( + logic.terran_competent_anti_air(state) + and ( + logic.terran_common_unit(state) + or state.has(item_names.REAPER, player) + ) + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.WELCOME_TO_THE_JUNGLE.mission_name, + "Victory", + SC2WOL_LOC_ID_OFFSET + 1400, + LocationType.VICTORY, + logic.terran_welcome_to_the_jungle_requirement, + ), + make_location_data( + SC2Mission.WELCOME_TO_THE_JUNGLE.mission_name, + "Close Relic", + SC2WOL_LOC_ID_OFFSET + 1401, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.WELCOME_TO_THE_JUNGLE.mission_name, + "West Relic", + SC2WOL_LOC_ID_OFFSET + 1402, + LocationType.VANILLA, + logic.terran_welcome_to_the_jungle_requirement, + ), + make_location_data( + SC2Mission.WELCOME_TO_THE_JUNGLE.mission_name, + "North-East Relic", + SC2WOL_LOC_ID_OFFSET + 1403, + LocationType.VANILLA, + logic.terran_welcome_to_the_jungle_requirement, + ), + make_location_data( + SC2Mission.WELCOME_TO_THE_JUNGLE.mission_name, + "Middle Base", + SC2WOL_LOC_ID_OFFSET + 1404, + LocationType.EXTRA, + logic.terran_welcome_to_the_jungle_requirement, + ), + make_location_data( + SC2Mission.WELCOME_TO_THE_JUNGLE.mission_name, + "Protoss Cleared", + SC2WOL_LOC_ID_OFFSET + 1405, + LocationType.MASTERY, + lambda state: ( + logic.terran_welcome_to_the_jungle_requirement(state) + and logic.terran_beats_protoss_deathball(state) + and logic.terran_base_trasher(state) + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.WELCOME_TO_THE_JUNGLE.mission_name, + "No Terrazine Nodes Sealed", + SC2WOL_LOC_ID_OFFSET + 1406, + LocationType.CHALLENGE, + lambda state: ( + logic.terran_welcome_to_the_jungle_requirement(state) + and logic.terran_competent_ground_to_air(state) + and logic.terran_beats_protoss_deathball(state) + ), + flags=LocationFlag.PREVENTATIVE, + ), + make_location_data( + SC2Mission.WELCOME_TO_THE_JUNGLE.mission_name, + "Up to 1 Terrazine Node Sealed", + SC2WOL_LOC_ID_OFFSET + 1407, + LocationType.CHALLENGE, + lambda state: ( + logic.terran_welcome_to_the_jungle_requirement(state) + and logic.terran_competent_ground_to_air(state) + and logic.terran_beats_protoss_deathball(state) + ), + flags=LocationFlag.PREVENTATIVE, + ), + make_location_data( + SC2Mission.WELCOME_TO_THE_JUNGLE.mission_name, + "Up to 2 Terrazine Nodes Sealed", + SC2WOL_LOC_ID_OFFSET + 1408, + LocationType.CHALLENGE, + lambda state: ( + logic.terran_welcome_to_the_jungle_requirement(state) + and logic.terran_beats_protoss_deathball(state) + ), + flags=LocationFlag.PREVENTATIVE, + ), + make_location_data( + SC2Mission.WELCOME_TO_THE_JUNGLE.mission_name, + "Up to 3 Terrazine Nodes Sealed", + SC2WOL_LOC_ID_OFFSET + 1409, + LocationType.CHALLENGE, + lambda state: ( + logic.terran_welcome_to_the_jungle_requirement(state) + and logic.terran_competent_comp(state) + ), + flags=LocationFlag.PREVENTATIVE, + ), + make_location_data( + SC2Mission.WELCOME_TO_THE_JUNGLE.mission_name, + "Up to 4 Terrazine Nodes Sealed", + SC2WOL_LOC_ID_OFFSET + 1410, + LocationType.EXTRA, + logic.terran_welcome_to_the_jungle_requirement, + flags=LocationFlag.PREVENTATIVE, + ), + make_location_data( + SC2Mission.WELCOME_TO_THE_JUNGLE.mission_name, + "Up to 5 Terrazine Nodes Sealed", + SC2WOL_LOC_ID_OFFSET + 1411, + LocationType.EXTRA, + logic.terran_welcome_to_the_jungle_requirement, + flags=LocationFlag.PREVENTATIVE, + ), + make_location_data( + SC2Mission.BREAKOUT.mission_name, + "Victory", + SC2WOL_LOC_ID_OFFSET + 1500, + LocationType.VICTORY, + ), + make_location_data( + SC2Mission.BREAKOUT.mission_name, + "Diamondback Prison", + SC2WOL_LOC_ID_OFFSET + 1501, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.BREAKOUT.mission_name, + "Siege Tank Prison", + SC2WOL_LOC_ID_OFFSET + 1502, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.BREAKOUT.mission_name, + "First Checkpoint", + SC2WOL_LOC_ID_OFFSET + 1503, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.BREAKOUT.mission_name, + "Second Checkpoint", + SC2WOL_LOC_ID_OFFSET + 1504, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.GHOST_OF_A_CHANCE.mission_name, + "Victory", + SC2WOL_LOC_ID_OFFSET + 1600, + LocationType.VICTORY, + logic.ghost_of_a_chance_requirement, + hard_rule=logic.ghost_of_a_chance_requirement, + ), + make_location_data( + SC2Mission.GHOST_OF_A_CHANCE.mission_name, + "Terrazine Tank", + SC2WOL_LOC_ID_OFFSET + 1601, + LocationType.EXTRA, + logic.ghost_of_a_chance_requirement, + hard_rule=logic.ghost_of_a_chance_requirement, + ), + make_location_data( + SC2Mission.GHOST_OF_A_CHANCE.mission_name, + "Jorium Stockpile", + SC2WOL_LOC_ID_OFFSET + 1602, + LocationType.EXTRA, + logic.ghost_of_a_chance_requirement, + hard_rule=logic.ghost_of_a_chance_requirement, + ), + make_location_data( + SC2Mission.GHOST_OF_A_CHANCE.mission_name, + "First Island Spectres", + SC2WOL_LOC_ID_OFFSET + 1603, + LocationType.VANILLA, + logic.ghost_of_a_chance_requirement, + hard_rule=logic.ghost_of_a_chance_requirement, + ), + make_location_data( + SC2Mission.GHOST_OF_A_CHANCE.mission_name, + "Second Island Spectres", + SC2WOL_LOC_ID_OFFSET + 1604, + LocationType.VANILLA, + logic.ghost_of_a_chance_requirement, + hard_rule=logic.ghost_of_a_chance_requirement, + ), + make_location_data( + SC2Mission.GHOST_OF_A_CHANCE.mission_name, + "Third Island Spectres", + SC2WOL_LOC_ID_OFFSET + 1605, + LocationType.VANILLA, + logic.ghost_of_a_chance_requirement, + hard_rule=logic.ghost_of_a_chance_requirement, + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY.mission_name, + "Victory", + SC2WOL_LOC_ID_OFFSET + 1700, + LocationType.VICTORY, + lambda state: ( + logic.terran_great_train_robbery_train_stopper(state) + and logic.terran_basic_anti_air(state) + ), + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY.mission_name, + "North Defiler", + SC2WOL_LOC_ID_OFFSET + 1701, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY.mission_name, + "Mid Defiler", + SC2WOL_LOC_ID_OFFSET + 1702, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY.mission_name, + "South Defiler", + SC2WOL_LOC_ID_OFFSET + 1703, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY.mission_name, + "Close Diamondback", + SC2WOL_LOC_ID_OFFSET + 1704, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY.mission_name, + "Northwest Diamondback", + SC2WOL_LOC_ID_OFFSET + 1705, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY.mission_name, + "North Diamondback", + SC2WOL_LOC_ID_OFFSET + 1706, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY.mission_name, + "Northeast Diamondback", + SC2WOL_LOC_ID_OFFSET + 1707, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY.mission_name, + "Southwest Diamondback", + SC2WOL_LOC_ID_OFFSET + 1708, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY.mission_name, + "Southeast Diamondback", + SC2WOL_LOC_ID_OFFSET + 1709, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY.mission_name, + "Kill Team", + SC2WOL_LOC_ID_OFFSET + 1710, + LocationType.CHALLENGE, + lambda state: ( + (adv_tactics or logic.terran_common_unit(state)) + and logic.terran_great_train_robbery_train_stopper(state) + and logic.terran_basic_anti_air(state) + ), + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY.mission_name, + "Flawless", + SC2WOL_LOC_ID_OFFSET + 1711, + LocationType.CHALLENGE, + lambda state: ( + logic.terran_great_train_robbery_train_stopper(state) + and logic.terran_basic_anti_air(state) + ), + flags=LocationFlag.PREVENTATIVE, + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY.mission_name, + "2 Trains Destroyed", + SC2WOL_LOC_ID_OFFSET + 1712, + LocationType.EXTRA, + logic.terran_great_train_robbery_train_stopper, + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY.mission_name, + "4 Trains Destroyed", + SC2WOL_LOC_ID_OFFSET + 1713, + LocationType.EXTRA, + lambda state: ( + logic.terran_great_train_robbery_train_stopper(state) + and logic.terran_basic_anti_air(state) + ), + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY.mission_name, + "6 Trains Destroyed", + SC2WOL_LOC_ID_OFFSET + 1714, + LocationType.EXTRA, + lambda state: ( + logic.terran_great_train_robbery_train_stopper(state) + and logic.terran_basic_anti_air(state) + ), + ), + make_location_data( + SC2Mission.CUTTHROAT.mission_name, + "Victory", + SC2WOL_LOC_ID_OFFSET + 1800, + LocationType.VICTORY, + lambda state: ( + logic.terran_common_unit(state) + and (adv_tactics or logic.terran_moderate_anti_air(state)) + ), + ), + make_location_data( + SC2Mission.CUTTHROAT.mission_name, + "Mira Han", + SC2WOL_LOC_ID_OFFSET + 1801, + LocationType.EXTRA, + logic.terran_common_unit, + ), + make_location_data( + SC2Mission.CUTTHROAT.mission_name, + "North Relic", + SC2WOL_LOC_ID_OFFSET + 1802, + LocationType.VANILLA, + logic.terran_common_unit, + ), + make_location_data( + SC2Mission.CUTTHROAT.mission_name, + "Mid Relic", + SC2WOL_LOC_ID_OFFSET + 1803, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.CUTTHROAT.mission_name, + "Southwest Relic", + SC2WOL_LOC_ID_OFFSET + 1804, + LocationType.VANILLA, + logic.terran_common_unit, + ), + make_location_data( + SC2Mission.CUTTHROAT.mission_name, + "North Command Center", + SC2WOL_LOC_ID_OFFSET + 1805, + LocationType.EXTRA, + logic.terran_common_unit, + ), + make_location_data( + SC2Mission.CUTTHROAT.mission_name, + "South Command Center", + SC2WOL_LOC_ID_OFFSET + 1806, + LocationType.EXTRA, + logic.terran_common_unit, + ), + make_location_data( + SC2Mission.CUTTHROAT.mission_name, + "West Command Center", + SC2WOL_LOC_ID_OFFSET + 1807, + LocationType.EXTRA, + logic.terran_common_unit, + ), + make_location_data( + SC2Mission.ENGINE_OF_DESTRUCTION.mission_name, + "Victory", + SC2WOL_LOC_ID_OFFSET + 1900, + LocationType.VICTORY, + logic.terran_engine_of_destruction_requirement, + ), + make_location_data( + SC2Mission.ENGINE_OF_DESTRUCTION.mission_name, + "Odin", + SC2WOL_LOC_ID_OFFSET + 1901, + LocationType.EXTRA, + logic.marine_medic_upgrade, + ), + make_location_data( + SC2Mission.ENGINE_OF_DESTRUCTION.mission_name, + "Loki", + SC2WOL_LOC_ID_OFFSET + 1902, + LocationType.CHALLENGE, + logic.terran_engine_of_destruction_requirement, + ), + make_location_data( + SC2Mission.ENGINE_OF_DESTRUCTION.mission_name, + "Lab Devourer", + SC2WOL_LOC_ID_OFFSET + 1903, + LocationType.VANILLA, + logic.marine_medic_upgrade, + ), + make_location_data( + SC2Mission.ENGINE_OF_DESTRUCTION.mission_name, + "North Devourer", + SC2WOL_LOC_ID_OFFSET + 1904, + LocationType.VANILLA, + logic.terran_engine_of_destruction_requirement, + ), + make_location_data( + SC2Mission.ENGINE_OF_DESTRUCTION.mission_name, + "Southeast Devourer", + SC2WOL_LOC_ID_OFFSET + 1905, + LocationType.VANILLA, + logic.terran_engine_of_destruction_requirement, + ), + make_location_data( + SC2Mission.ENGINE_OF_DESTRUCTION.mission_name, + "West Base", + SC2WOL_LOC_ID_OFFSET + 1906, + LocationType.EXTRA, + logic.terran_engine_of_destruction_requirement, + ), + make_location_data( + SC2Mission.ENGINE_OF_DESTRUCTION.mission_name, + "Northwest Base", + SC2WOL_LOC_ID_OFFSET + 1907, + LocationType.EXTRA, + logic.terran_engine_of_destruction_requirement, + ), + make_location_data( + SC2Mission.ENGINE_OF_DESTRUCTION.mission_name, + "Northeast Base", + SC2WOL_LOC_ID_OFFSET + 1908, + LocationType.EXTRA, + logic.terran_engine_of_destruction_requirement, + ), + make_location_data( + SC2Mission.ENGINE_OF_DESTRUCTION.mission_name, + "Southeast Base", + SC2WOL_LOC_ID_OFFSET + 1909, + LocationType.EXTRA, + logic.terran_engine_of_destruction_requirement, + ), + make_location_data( + SC2Mission.MEDIA_BLITZ.mission_name, + "Victory", + SC2WOL_LOC_ID_OFFSET + 2000, + LocationType.VICTORY, + logic.terran_competent_comp, + ), + make_location_data( + SC2Mission.MEDIA_BLITZ.mission_name, + "Tower 1", + SC2WOL_LOC_ID_OFFSET + 2001, + LocationType.VANILLA, + logic.terran_competent_comp, + ), + make_location_data( + SC2Mission.MEDIA_BLITZ.mission_name, + "Tower 2", + SC2WOL_LOC_ID_OFFSET + 2002, + LocationType.VANILLA, + logic.terran_competent_comp, + ), + make_location_data( + SC2Mission.MEDIA_BLITZ.mission_name, + "Tower 3", + SC2WOL_LOC_ID_OFFSET + 2003, + LocationType.VANILLA, + logic.terran_competent_comp, + ), + make_location_data( + SC2Mission.MEDIA_BLITZ.mission_name, + "Science Facility", + SC2WOL_LOC_ID_OFFSET + 2004, + LocationType.VANILLA, + lambda state: adv_tactics or logic.terran_competent_comp(state), + ), + make_location_data( + SC2Mission.MEDIA_BLITZ.mission_name, + "All Barracks", + SC2WOL_LOC_ID_OFFSET + 2005, + LocationType.EXTRA, + logic.terran_competent_comp, + ), + make_location_data( + SC2Mission.MEDIA_BLITZ.mission_name, + "All Factories", + SC2WOL_LOC_ID_OFFSET + 2006, + LocationType.EXTRA, + logic.terran_competent_comp, + ), + make_location_data( + SC2Mission.MEDIA_BLITZ.mission_name, + "All Starports", + SC2WOL_LOC_ID_OFFSET + 2007, + LocationType.EXTRA, + lambda state: adv_tactics or logic.terran_competent_comp(state), + ), + make_location_data( + SC2Mission.MEDIA_BLITZ.mission_name, + "Odin Not Trashed", + SC2WOL_LOC_ID_OFFSET + 2008, + LocationType.CHALLENGE, + logic.terran_competent_comp, + ), + make_location_data( + SC2Mission.MEDIA_BLITZ.mission_name, + "Surprise Attack Ends", + SC2WOL_LOC_ID_OFFSET + 2009, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.PIERCING_OF_THE_SHROUD.mission_name, + "Victory", + SC2WOL_LOC_ID_OFFSET + 2100, + LocationType.VICTORY, + logic.marine_medic_upgrade, + ), + make_location_data( + SC2Mission.PIERCING_OF_THE_SHROUD.mission_name, + "Holding Cell Relic", + SC2WOL_LOC_ID_OFFSET + 2101, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.PIERCING_OF_THE_SHROUD.mission_name, + "Brutalisk Relic", + SC2WOL_LOC_ID_OFFSET + 2102, + LocationType.VANILLA, + logic.marine_medic_upgrade, + ), + make_location_data( + SC2Mission.PIERCING_OF_THE_SHROUD.mission_name, + "First Escape Relic", + SC2WOL_LOC_ID_OFFSET + 2103, + LocationType.VANILLA, + logic.marine_medic_upgrade, + ), + make_location_data( + SC2Mission.PIERCING_OF_THE_SHROUD.mission_name, + "Second Escape Relic", + SC2WOL_LOC_ID_OFFSET + 2104, + LocationType.VANILLA, + logic.marine_medic_upgrade, + ), + make_location_data( + SC2Mission.PIERCING_OF_THE_SHROUD.mission_name, + "Brutalisk", + SC2WOL_LOC_ID_OFFSET + 2105, + LocationType.VANILLA, + logic.marine_medic_upgrade, + ), + make_location_data( + SC2Mission.PIERCING_OF_THE_SHROUD.mission_name, + "Fusion Reactor", + SC2WOL_LOC_ID_OFFSET + 2106, + LocationType.EXTRA, + logic.marine_medic_upgrade, + ), + make_location_data( + SC2Mission.PIERCING_OF_THE_SHROUD.mission_name, + "Entrance Holding Pen", + SC2WOL_LOC_ID_OFFSET + 2107, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.PIERCING_OF_THE_SHROUD.mission_name, + "Cargo Bay Warbot", + SC2WOL_LOC_ID_OFFSET + 2108, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.PIERCING_OF_THE_SHROUD.mission_name, + "Escape Warbot", + SC2WOL_LOC_ID_OFFSET + 2109, + LocationType.EXTRA, + logic.marine_medic_upgrade, + ), + make_location_data( + SC2Mission.WHISPERS_OF_DOOM.mission_name, + "Victory", + SC2WOL_LOC_ID_OFFSET + 2200, + LocationType.VICTORY, + ), + make_location_data( + SC2Mission.WHISPERS_OF_DOOM.mission_name, + "First Hatchery", + SC2WOL_LOC_ID_OFFSET + 2201, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.WHISPERS_OF_DOOM.mission_name, + "Second Hatchery", + SC2WOL_LOC_ID_OFFSET + 2202, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.WHISPERS_OF_DOOM.mission_name, + "Third Hatchery", + SC2WOL_LOC_ID_OFFSET + 2203, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.WHISPERS_OF_DOOM.mission_name, + "First Prophecy Fragment", + SC2WOL_LOC_ID_OFFSET + 2204, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.WHISPERS_OF_DOOM.mission_name, + "Second Prophecy Fragment", + SC2WOL_LOC_ID_OFFSET + 2205, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.WHISPERS_OF_DOOM.mission_name, + "Third Prophecy Fragment", + SC2WOL_LOC_ID_OFFSET + 2206, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.A_SINISTER_TURN.mission_name, + "Victory", + SC2WOL_LOC_ID_OFFSET + 2300, + LocationType.VICTORY, + lambda state: logic.protoss_common_unit(state) + and logic.protoss_competent_anti_air(state), + ), + make_location_data( + SC2Mission.A_SINISTER_TURN.mission_name, + "Robotics Facility", + SC2WOL_LOC_ID_OFFSET + 2301, + LocationType.VANILLA, + lambda state: adv_tactics or logic.protoss_common_unit(state), + ), + make_location_data( + SC2Mission.A_SINISTER_TURN.mission_name, + "Dark Shrine", + SC2WOL_LOC_ID_OFFSET + 2302, + LocationType.VANILLA, + lambda state: adv_tactics or logic.protoss_common_unit(state), + ), + make_location_data( + SC2Mission.A_SINISTER_TURN.mission_name, + "Templar Archives", + SC2WOL_LOC_ID_OFFSET + 2303, + LocationType.VANILLA, + lambda state: logic.protoss_common_unit(state) + and logic.protoss_competent_anti_air(state), + ), + make_location_data( + SC2Mission.A_SINISTER_TURN.mission_name, + "Northeast Base", + SC2WOL_LOC_ID_OFFSET + 2304, + LocationType.EXTRA, + lambda state: logic.protoss_common_unit(state) + and logic.protoss_competent_anti_air(state), + ), + make_location_data( + SC2Mission.A_SINISTER_TURN.mission_name, + "Southwest Base", + SC2WOL_LOC_ID_OFFSET + 2305, + LocationType.CHALLENGE, + lambda state: logic.protoss_common_unit(state) + and logic.protoss_competent_anti_air(state), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.A_SINISTER_TURN.mission_name, + "Maar", + SC2WOL_LOC_ID_OFFSET + 2306, + LocationType.EXTRA, + logic.protoss_common_unit, + ), + make_location_data( + SC2Mission.A_SINISTER_TURN.mission_name, + "Northwest Preserver", + SC2WOL_LOC_ID_OFFSET + 2307, + LocationType.EXTRA, + lambda state: logic.protoss_common_unit(state) + and logic.protoss_competent_anti_air(state), + ), + make_location_data( + SC2Mission.A_SINISTER_TURN.mission_name, + "Southwest Preserver", + SC2WOL_LOC_ID_OFFSET + 2308, + LocationType.EXTRA, + lambda state: logic.protoss_common_unit(state) + and logic.protoss_competent_anti_air(state), + ), + make_location_data( + SC2Mission.A_SINISTER_TURN.mission_name, + "East Preserver", + SC2WOL_LOC_ID_OFFSET + 2309, + LocationType.EXTRA, + lambda state: logic.protoss_common_unit(state) + and logic.protoss_competent_anti_air(state), + ), + make_location_data( + SC2Mission.ECHOES_OF_THE_FUTURE.mission_name, + "Victory", + SC2WOL_LOC_ID_OFFSET + 2400, + LocationType.VICTORY, + lambda state: ( + (adv_tactics and logic.protoss_static_defense(state)) + or ( + logic.protoss_common_unit(state) + and logic.protoss_competent_anti_air(state) + ) + ), + ), + make_location_data( + SC2Mission.ECHOES_OF_THE_FUTURE.mission_name, + "Close Obelisk", + SC2WOL_LOC_ID_OFFSET + 2401, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.ECHOES_OF_THE_FUTURE.mission_name, + "West Obelisk", + SC2WOL_LOC_ID_OFFSET + 2402, + LocationType.VANILLA, + lambda state: adv_tactics or logic.protoss_common_unit(state), + ), + make_location_data( + SC2Mission.ECHOES_OF_THE_FUTURE.mission_name, + "Base", + SC2WOL_LOC_ID_OFFSET + 2403, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.ECHOES_OF_THE_FUTURE.mission_name, + "Southwest Tendril", + SC2WOL_LOC_ID_OFFSET + 2404, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.ECHOES_OF_THE_FUTURE.mission_name, + "Southeast Tendril", + SC2WOL_LOC_ID_OFFSET + 2405, + LocationType.EXTRA, + lambda state: adv_tactics + and logic.protoss_static_defense(state) + or logic.protoss_common_unit(state), + ), + make_location_data( + SC2Mission.ECHOES_OF_THE_FUTURE.mission_name, + "Northeast Tendril", + SC2WOL_LOC_ID_OFFSET + 2406, + LocationType.EXTRA, + lambda state: adv_tactics + and logic.protoss_static_defense(state) + or logic.protoss_common_unit(state), + ), + make_location_data( + SC2Mission.ECHOES_OF_THE_FUTURE.mission_name, + "Northwest Tendril", + SC2WOL_LOC_ID_OFFSET + 2407, + LocationType.EXTRA, + lambda state: adv_tactics + and logic.protoss_static_defense(state) + or logic.protoss_common_unit(state), + ), + make_location_data( + SC2Mission.IN_UTTER_DARKNESS.mission_name, + "Defeat", + SC2WOL_LOC_ID_OFFSET + 2500, + LocationType.VICTORY, + ), + make_location_data( + SC2Mission.IN_UTTER_DARKNESS.mission_name, + "Protoss Archive", + SC2WOL_LOC_ID_OFFSET + 2501, + LocationType.VANILLA, + logic.protoss_in_utter_darkness_requirement, + ), + make_location_data( + SC2Mission.IN_UTTER_DARKNESS.mission_name, + "Kills", + SC2WOL_LOC_ID_OFFSET + 2502, + LocationType.VANILLA, + logic.protoss_in_utter_darkness_requirement, + ), + make_location_data( + SC2Mission.IN_UTTER_DARKNESS.mission_name, + "Urun", + SC2WOL_LOC_ID_OFFSET + 2503, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.IN_UTTER_DARKNESS.mission_name, + "Mohandar", + SC2WOL_LOC_ID_OFFSET + 2504, + LocationType.EXTRA, + logic.protoss_in_utter_darkness_requirement, + ), + make_location_data( + SC2Mission.IN_UTTER_DARKNESS.mission_name, + "Selendis", + SC2WOL_LOC_ID_OFFSET + 2505, + LocationType.EXTRA, + logic.protoss_in_utter_darkness_requirement, + ), + make_location_data( + SC2Mission.IN_UTTER_DARKNESS.mission_name, + "Artanis", + SC2WOL_LOC_ID_OFFSET + 2506, + LocationType.EXTRA, + logic.protoss_in_utter_darkness_requirement, + ), + make_location_data( + SC2Mission.GATES_OF_HELL.mission_name, + "Victory", + SC2WOL_LOC_ID_OFFSET + 2600, + LocationType.VICTORY, + logic.terran_gates_of_hell_requirement, + ), + make_location_data( + SC2Mission.GATES_OF_HELL.mission_name, + "Large Army", + SC2WOL_LOC_ID_OFFSET + 2601, + LocationType.VANILLA, + logic.terran_gates_of_hell_requirement, + ), + make_location_data( + SC2Mission.GATES_OF_HELL.mission_name, + "2 Drop Pods", + SC2WOL_LOC_ID_OFFSET + 2602, + LocationType.VANILLA, + logic.terran_gates_of_hell_requirement, + ), + make_location_data( + SC2Mission.GATES_OF_HELL.mission_name, + "4 Drop Pods", + SC2WOL_LOC_ID_OFFSET + 2603, + LocationType.VANILLA, + logic.terran_gates_of_hell_requirement, + ), + make_location_data( + SC2Mission.GATES_OF_HELL.mission_name, + "6 Drop Pods", + SC2WOL_LOC_ID_OFFSET + 2604, + LocationType.EXTRA, + logic.terran_gates_of_hell_requirement, + ), + make_location_data( + SC2Mission.GATES_OF_HELL.mission_name, + "8 Drop Pods", + SC2WOL_LOC_ID_OFFSET + 2605, + LocationType.CHALLENGE, + logic.terran_gates_of_hell_requirement, + ), + make_location_data( + SC2Mission.GATES_OF_HELL.mission_name, + "Southwest Spore Cannon", + SC2WOL_LOC_ID_OFFSET + 2606, + LocationType.EXTRA, + logic.terran_gates_of_hell_requirement, + ), + make_location_data( + SC2Mission.GATES_OF_HELL.mission_name, + "Northwest Spore Cannon", + SC2WOL_LOC_ID_OFFSET + 2607, + LocationType.EXTRA, + logic.terran_gates_of_hell_requirement, + ), + make_location_data( + SC2Mission.GATES_OF_HELL.mission_name, + "Northeast Spore Cannon", + SC2WOL_LOC_ID_OFFSET + 2608, + LocationType.EXTRA, + logic.terran_gates_of_hell_requirement, + ), + make_location_data( + SC2Mission.GATES_OF_HELL.mission_name, + "East Spore Cannon", + SC2WOL_LOC_ID_OFFSET + 2609, + LocationType.EXTRA, + logic.terran_gates_of_hell_requirement, + ), + make_location_data( + SC2Mission.GATES_OF_HELL.mission_name, + "Southeast Spore Cannon", + SC2WOL_LOC_ID_OFFSET + 2610, + LocationType.EXTRA, + logic.terran_gates_of_hell_requirement, + ), + make_location_data( + SC2Mission.GATES_OF_HELL.mission_name, + "Expansion Spore Cannon", + SC2WOL_LOC_ID_OFFSET + 2611, + LocationType.EXTRA, + logic.terran_gates_of_hell_requirement, + ), + make_location_data( + SC2Mission.BELLY_OF_THE_BEAST.mission_name, + "Victory", + SC2WOL_LOC_ID_OFFSET + 2700, + LocationType.VICTORY, + lambda state: adv_tactics or logic.marine_medic_firebat_upgrade(state), + ), + make_location_data( + SC2Mission.BELLY_OF_THE_BEAST.mission_name, + "First Charge", + SC2WOL_LOC_ID_OFFSET + 2701, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.BELLY_OF_THE_BEAST.mission_name, + "Second Charge", + SC2WOL_LOC_ID_OFFSET + 2702, + LocationType.EXTRA, + lambda state: adv_tactics or logic.marine_medic_firebat_upgrade(state), + ), + make_location_data( + SC2Mission.BELLY_OF_THE_BEAST.mission_name, + "Third Charge", + SC2WOL_LOC_ID_OFFSET + 2703, + LocationType.EXTRA, + lambda state: adv_tactics or logic.marine_medic_firebat_upgrade(state), + ), + make_location_data( + SC2Mission.BELLY_OF_THE_BEAST.mission_name, + "First Group Rescued", + SC2WOL_LOC_ID_OFFSET + 2704, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.BELLY_OF_THE_BEAST.mission_name, + "Second Group Rescued", + SC2WOL_LOC_ID_OFFSET + 2705, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.BELLY_OF_THE_BEAST.mission_name, + "Third Group Rescued", + SC2WOL_LOC_ID_OFFSET + 2706, + LocationType.VANILLA, + lambda state: adv_tactics or logic.marine_medic_firebat_upgrade(state), + ), + make_location_data( + SC2Mission.SHATTER_THE_SKY.mission_name, + "Victory", + SC2WOL_LOC_ID_OFFSET + 2800, + LocationType.VICTORY, + lambda state: logic.terran_competent_comp(state) + and logic.terran_army_weapon_armor_upgrade_min_level(state) >= 2, + ), + make_location_data( + SC2Mission.SHATTER_THE_SKY.mission_name, + "Close Coolant Tower", + SC2WOL_LOC_ID_OFFSET + 2801, + LocationType.VANILLA, + logic.terran_competent_comp, + ), + make_location_data( + SC2Mission.SHATTER_THE_SKY.mission_name, + "Northwest Coolant Tower", + SC2WOL_LOC_ID_OFFSET + 2802, + LocationType.VANILLA, + logic.terran_competent_comp, + ), + make_location_data( + SC2Mission.SHATTER_THE_SKY.mission_name, + "Southeast Coolant Tower", + SC2WOL_LOC_ID_OFFSET + 2803, + LocationType.VANILLA, + lambda state: logic.terran_competent_comp(state) + and logic.terran_army_weapon_armor_upgrade_min_level(state) >= 2, + ), + make_location_data( + SC2Mission.SHATTER_THE_SKY.mission_name, + "Southwest Coolant Tower", + SC2WOL_LOC_ID_OFFSET + 2804, + LocationType.VANILLA, + lambda state: logic.terran_competent_comp(state) + and logic.terran_army_weapon_armor_upgrade_min_level(state) >= 2, + ), + make_location_data( + SC2Mission.SHATTER_THE_SKY.mission_name, + "Leviathan", + SC2WOL_LOC_ID_OFFSET + 2805, + LocationType.VANILLA, + lambda state: logic.terran_competent_comp(state) + and logic.terran_army_weapon_armor_upgrade_min_level(state) >= 2, + hard_rule=logic.terran_any_anti_air, + ), + make_location_data( + SC2Mission.SHATTER_THE_SKY.mission_name, + "East Hatchery", + SC2WOL_LOC_ID_OFFSET + 2806, + LocationType.EXTRA, + logic.terran_competent_comp, + ), + make_location_data( + SC2Mission.SHATTER_THE_SKY.mission_name, + "North Hatchery", + SC2WOL_LOC_ID_OFFSET + 2807, + LocationType.EXTRA, + logic.terran_competent_comp, + ), + make_location_data( + SC2Mission.SHATTER_THE_SKY.mission_name, + "Mid Hatchery", + SC2WOL_LOC_ID_OFFSET + 2808, + LocationType.EXTRA, + logic.terran_competent_comp, + ), + make_location_data( + SC2Mission.ALL_IN.mission_name, + "Victory", + SC2WOL_LOC_ID_OFFSET + 2900, + LocationType.VICTORY, + logic.terran_all_in_requirement, + ), + make_location_data( + SC2Mission.ALL_IN.mission_name, + "First Kerrigan Attack", + SC2WOL_LOC_ID_OFFSET + 2901, + LocationType.EXTRA, + logic.terran_all_in_requirement, + ), + make_location_data( + SC2Mission.ALL_IN.mission_name, + "Second Kerrigan Attack", + SC2WOL_LOC_ID_OFFSET + 2902, + LocationType.EXTRA, + logic.terran_all_in_requirement, + ), + make_location_data( + SC2Mission.ALL_IN.mission_name, + "Third Kerrigan Attack", + SC2WOL_LOC_ID_OFFSET + 2903, + LocationType.EXTRA, + logic.terran_all_in_requirement, + ), + make_location_data( + SC2Mission.ALL_IN.mission_name, + "Fourth Kerrigan Attack", + SC2WOL_LOC_ID_OFFSET + 2904, + LocationType.EXTRA, + logic.terran_all_in_requirement, + ), + make_location_data( + SC2Mission.ALL_IN.mission_name, + "Fifth Kerrigan Attack", + SC2WOL_LOC_ID_OFFSET + 2905, + LocationType.EXTRA, + logic.terran_all_in_requirement, + ), + # HotS + make_location_data( + SC2Mission.LAB_RAT.mission_name, + "Victory", + SC2HOTS_LOC_ID_OFFSET + 100, + LocationType.VICTORY, + lambda state: ( + logic.zerg_common_unit + or state.has_any((item_names.ZERGLING, item_names.PYGALISK), player) + ), + ), + make_location_data( + SC2Mission.LAB_RAT.mission_name, + "Gather Minerals", + SC2HOTS_LOC_ID_OFFSET + 101, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.LAB_RAT.mission_name, + "South Zergling Group", + SC2HOTS_LOC_ID_OFFSET + 102, + LocationType.VANILLA, + lambda state: adv_tactics + or ( + logic.zerg_common_unit + or state.has_any((item_names.ZERGLING, item_names.PYGALISK), player) + ), + ), + make_location_data( + SC2Mission.LAB_RAT.mission_name, + "East Zergling Group", + SC2HOTS_LOC_ID_OFFSET + 103, + LocationType.VANILLA, + lambda state: adv_tactics + or ( + logic.zerg_common_unit + or state.has_any((item_names.ZERGLING, item_names.PYGALISK), player) + ), + ), + make_location_data( + SC2Mission.LAB_RAT.mission_name, + "West Zergling Group", + SC2HOTS_LOC_ID_OFFSET + 104, + LocationType.VANILLA, + lambda state: adv_tactics + or ( + logic.zerg_common_unit + or state.has_any((item_names.ZERGLING, item_names.PYGALISK), player) + ), + ), + make_location_data( + SC2Mission.LAB_RAT.mission_name, + "Hatchery", + SC2HOTS_LOC_ID_OFFSET + 105, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.LAB_RAT.mission_name, + "Overlord", + SC2HOTS_LOC_ID_OFFSET + 106, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.LAB_RAT.mission_name, + "Gas Turrets", + SC2HOTS_LOC_ID_OFFSET + 107, + LocationType.EXTRA, + lambda state: adv_tactics + or ( + logic.zerg_common_unit + or state.has_any((item_names.ZERGLING, item_names.PYGALISK), player) + ), + ), + make_location_data( + SC2Mission.LAB_RAT.mission_name, + "Win In Under 10 Minutes", + SC2HOTS_LOC_ID_OFFSET + 108, + LocationType.CHALLENGE, + lambda state: ( + logic.zerg_common_unit + or state.has_any((item_names.ZERGLING, item_names.PYGALISK), player) + ), + flags=LocationFlag.SPEEDRUN, + ), + make_location_data( + SC2Mission.BACK_IN_THE_SADDLE.mission_name, + "Victory", + SC2HOTS_LOC_ID_OFFSET + 200, + LocationType.VICTORY, + lambda state: logic.basic_kerrigan(state) + or kerriganless + or logic.grant_story_tech == GrantStoryTech.option_grant, + hard_rule=logic.zerg_any_units_back_in_the_saddle_requirement, + ), + make_location_data( + SC2Mission.BACK_IN_THE_SADDLE.mission_name, + "Defend the Tram", + SC2HOTS_LOC_ID_OFFSET + 201, + LocationType.EXTRA, + lambda state: logic.basic_kerrigan(state) + or kerriganless + or logic.grant_story_tech == GrantStoryTech.option_grant, + hard_rule=logic.zerg_any_units_back_in_the_saddle_requirement, + ), + make_location_data( + SC2Mission.BACK_IN_THE_SADDLE.mission_name, + "Kinetic Blast", + SC2HOTS_LOC_ID_OFFSET + 202, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.BACK_IN_THE_SADDLE.mission_name, + "Crushing Grip", + SC2HOTS_LOC_ID_OFFSET + 203, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.BACK_IN_THE_SADDLE.mission_name, + "Reach the Sublevel", + SC2HOTS_LOC_ID_OFFSET + 204, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.BACK_IN_THE_SADDLE.mission_name, + "Door Section Cleared", + SC2HOTS_LOC_ID_OFFSET + 205, + LocationType.EXTRA, + lambda state: logic.basic_kerrigan(state) + or kerriganless + or logic.grant_story_tech == GrantStoryTech.option_grant, + hard_rule=logic.zerg_any_units_back_in_the_saddle_requirement, + ), + make_location_data( + SC2Mission.RENDEZVOUS.mission_name, + "Victory", + SC2HOTS_LOC_ID_OFFSET + 300, + LocationType.VICTORY, + lambda state: ( + logic.zerg_common_unit(state) + and logic.zerg_basic_anti_air(state) + and logic.zerg_defense_rating(state, False, False) >= 3 + ), + ), + make_location_data( + SC2Mission.RENDEZVOUS.mission_name, + "Right Queen", + SC2HOTS_LOC_ID_OFFSET + 301, + LocationType.VANILLA, + lambda state: ( + logic.zerg_common_unit(state) + and logic.zerg_basic_anti_air(state) + and logic.zerg_defense_rating(state, False, False) >= 3 + ), + ), + make_location_data( + SC2Mission.RENDEZVOUS.mission_name, + "Center Queen", + SC2HOTS_LOC_ID_OFFSET + 302, + LocationType.VANILLA, + lambda state: ( + logic.zerg_common_unit(state) + and logic.zerg_basic_anti_air(state) + and logic.zerg_defense_rating(state, False, False) >= 3 + ), + ), + make_location_data( + SC2Mission.RENDEZVOUS.mission_name, + "Left Queen", + SC2HOTS_LOC_ID_OFFSET + 303, + LocationType.VANILLA, + lambda state: ( + logic.zerg_common_unit(state) + and logic.zerg_basic_anti_air(state) + and logic.zerg_defense_rating(state, False, False) >= 3 + ), + ), + make_location_data( + SC2Mission.RENDEZVOUS.mission_name, + "Hold Out Finished", + SC2HOTS_LOC_ID_OFFSET + 304, + LocationType.EXTRA, + lambda state: ( + logic.zerg_common_unit(state) + and logic.zerg_basic_anti_air(state) + and logic.zerg_defense_rating(state, False, False) >= 3 + ), + ), + make_location_data( + SC2Mission.RENDEZVOUS.mission_name, + "Kill All Buildings Before Reinforcements", + SC2HOTS_LOC_ID_OFFSET + 305, + LocationType.MASTERY, + lambda state: ( + logic.zerg_competent_comp(state) + and logic.zerg_competent_anti_air(state) + and (logic.basic_kerrigan(state) or kerriganless) + and logic.zerg_defense_rating(state, False, False) >= 3 + and logic.zerg_power_rating(state) >= 5 + ), + flags=LocationFlag.SPEEDRUN, + ), + make_location_data( + SC2Mission.HARVEST_OF_SCREAMS.mission_name, + "Victory", + SC2HOTS_LOC_ID_OFFSET + 400, + LocationType.VICTORY, + lambda state: ( + logic.zerg_common_unit(state) and logic.zerg_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.HARVEST_OF_SCREAMS.mission_name, + "First Ursadon Matriarch", + SC2HOTS_LOC_ID_OFFSET + 401, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.HARVEST_OF_SCREAMS.mission_name, + "North Ursadon Matriarch", + SC2HOTS_LOC_ID_OFFSET + 402, + LocationType.VANILLA, + logic.zerg_common_unit, + ), + make_location_data( + SC2Mission.HARVEST_OF_SCREAMS.mission_name, + "West Ursadon Matriarch", + SC2HOTS_LOC_ID_OFFSET + 403, + LocationType.VANILLA, + logic.zerg_common_unit, + ), + make_location_data( + SC2Mission.HARVEST_OF_SCREAMS.mission_name, + "Lost Brood", + SC2HOTS_LOC_ID_OFFSET + 404, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.HARVEST_OF_SCREAMS.mission_name, + "Northeast Psi-link Spire", + SC2HOTS_LOC_ID_OFFSET + 405, + LocationType.EXTRA, + logic.zerg_common_unit, + ), + make_location_data( + SC2Mission.HARVEST_OF_SCREAMS.mission_name, + "Northwest Psi-link Spire", + SC2HOTS_LOC_ID_OFFSET + 406, + LocationType.EXTRA, + lambda state: ( + logic.zerg_common_unit(state) and logic.zerg_moderate_anti_air(state) + ), + ), + make_location_data( + SC2Mission.HARVEST_OF_SCREAMS.mission_name, + "Southwest Psi-link Spire", + SC2HOTS_LOC_ID_OFFSET + 407, + LocationType.EXTRA, + lambda state: ( + logic.zerg_common_unit(state) and logic.zerg_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.HARVEST_OF_SCREAMS.mission_name, + "Nafash", + SC2HOTS_LOC_ID_OFFSET + 408, + LocationType.EXTRA, + lambda state: ( + logic.zerg_common_unit(state) and logic.zerg_moderate_anti_air(state) + ), + ), + make_location_data( + SC2Mission.HARVEST_OF_SCREAMS.mission_name, + "20 Unfrozen Structures", + SC2HOTS_LOC_ID_OFFSET + 409, + LocationType.CHALLENGE, + lambda state: ( + logic.zerg_common_unit(state) and logic.zerg_moderate_anti_air(state) + ), + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER.mission_name, + "Victory", + SC2HOTS_LOC_ID_OFFSET + 500, + LocationType.VICTORY, + lambda state: ( + logic.zerg_common_unit(state) and logic.zerg_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER.mission_name, + "East Stasis Chamber", + SC2HOTS_LOC_ID_OFFSET + 501, + LocationType.VANILLA, + lambda state: ( + logic.zerg_common_unit(state) and logic.zerg_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER.mission_name, + "Center Stasis Chamber", + SC2HOTS_LOC_ID_OFFSET + 502, + LocationType.VANILLA, + lambda state: logic.zerg_common_unit(state) or adv_tactics, + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER.mission_name, + "West Stasis Chamber", + SC2HOTS_LOC_ID_OFFSET + 503, + LocationType.VANILLA, + lambda state: ( + logic.zerg_common_unit(state) and logic.zerg_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER.mission_name, + "Destroy 4 Shuttles", + SC2HOTS_LOC_ID_OFFSET + 504, + LocationType.EXTRA, + lambda state: ( + logic.zerg_common_unit(state) and logic.zerg_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER.mission_name, + "Frozen Expansion", + SC2HOTS_LOC_ID_OFFSET + 505, + LocationType.EXTRA, + logic.zerg_common_unit, + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER.mission_name, + "Southwest Frozen Zerg", + SC2HOTS_LOC_ID_OFFSET + 506, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER.mission_name, + "Southeast Frozen Zerg", + SC2HOTS_LOC_ID_OFFSET + 507, + LocationType.EXTRA, + lambda state: logic.zerg_common_unit(state) or adv_tactics, + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER.mission_name, + "West Frozen Zerg", + SC2HOTS_LOC_ID_OFFSET + 508, + LocationType.EXTRA, + logic.zerg_common_unit_competent_aa, + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER.mission_name, + "East Frozen Zerg", + SC2HOTS_LOC_ID_OFFSET + 509, + LocationType.EXTRA, + logic.zerg_common_unit_competent_aa, + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER.mission_name, + "West Launch Bay", + SC2HOTS_LOC_ID_OFFSET + 510, + LocationType.CHALLENGE, + logic.zerg_competent_comp_competent_aa, + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER.mission_name, + "Center Launch Bay", + SC2HOTS_LOC_ID_OFFSET + 511, + LocationType.CHALLENGE, + logic.zerg_competent_comp_competent_aa, + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER.mission_name, + "East Launch Bay", + SC2HOTS_LOC_ID_OFFSET + 512, + LocationType.CHALLENGE, + logic.zerg_competent_comp_competent_aa, + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.ENEMY_WITHIN.mission_name, + "Victory", + SC2HOTS_LOC_ID_OFFSET + 600, + LocationType.VICTORY, + lambda state: ( + logic.zerg_pass_vents(state) + and ( + logic.grant_story_tech == GrantStoryTech.option_grant + or state.has_any( + { + item_names.ZERGLING_RAPTOR_STRAIN, + item_names.ROACH, + item_names.HYDRALISK, + item_names.INFESTOR, + }, + player, + ) + ) + ), + hard_rule=logic.zerg_pass_vents, + ), + make_location_data( + SC2Mission.ENEMY_WITHIN.mission_name, + "Infest Giant Ursadon", + SC2HOTS_LOC_ID_OFFSET + 601, + LocationType.VANILLA, + logic.zerg_pass_vents, + hard_rule=logic.zerg_pass_vents, + ), + make_location_data( + SC2Mission.ENEMY_WITHIN.mission_name, + "First Niadra Evolution", + SC2HOTS_LOC_ID_OFFSET + 602, + LocationType.VANILLA, + logic.zerg_pass_vents, + ), + make_location_data( + SC2Mission.ENEMY_WITHIN.mission_name, + "Second Niadra Evolution", + SC2HOTS_LOC_ID_OFFSET + 603, + LocationType.VANILLA, + logic.zerg_pass_vents, + hard_rule=logic.zerg_pass_vents, + ), + make_location_data( + SC2Mission.ENEMY_WITHIN.mission_name, + "Third Niadra Evolution", + SC2HOTS_LOC_ID_OFFSET + 604, + LocationType.VANILLA, + logic.zerg_pass_vents, + hard_rule=logic.zerg_pass_vents, + ), + make_location_data( + SC2Mission.ENEMY_WITHIN.mission_name, + "Warp Drive", + SC2HOTS_LOC_ID_OFFSET + 605, + LocationType.EXTRA, + logic.zerg_pass_vents, + hard_rule=logic.zerg_pass_vents, + ), + make_location_data( + SC2Mission.ENEMY_WITHIN.mission_name, + "Stasis Quadrant", + SC2HOTS_LOC_ID_OFFSET + 606, + LocationType.EXTRA, + logic.zerg_pass_vents, + hard_rule=logic.zerg_pass_vents, + ), + make_location_data( + SC2Mission.DOMINATION.mission_name, + "Victory", + SC2HOTS_LOC_ID_OFFSET + 700, + LocationType.VICTORY, + logic.zerg_common_unit_basic_aa, + ), + make_location_data( + SC2Mission.DOMINATION.mission_name, + "Center Infested Command Center", + SC2HOTS_LOC_ID_OFFSET + 701, + LocationType.VANILLA, + logic.zerg_common_unit, + ), + make_location_data( + SC2Mission.DOMINATION.mission_name, + "North Infested Command Center", + SC2HOTS_LOC_ID_OFFSET + 702, + LocationType.VANILLA, + logic.zerg_common_unit, + ), + make_location_data( + SC2Mission.DOMINATION.mission_name, + "Repel Zagara", + SC2HOTS_LOC_ID_OFFSET + 703, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.DOMINATION.mission_name, + "Close Baneling Nest", + SC2HOTS_LOC_ID_OFFSET + 704, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.DOMINATION.mission_name, + "South Baneling Nest", + SC2HOTS_LOC_ID_OFFSET + 705, + LocationType.EXTRA, + lambda state: adv_tactics or logic.zerg_common_unit(state), + ), + make_location_data( + SC2Mission.DOMINATION.mission_name, + "Southwest Baneling Nest", + SC2HOTS_LOC_ID_OFFSET + 706, + LocationType.EXTRA, + logic.zerg_common_unit, + ), + make_location_data( + SC2Mission.DOMINATION.mission_name, + "Southeast Baneling Nest", + SC2HOTS_LOC_ID_OFFSET + 707, + LocationType.EXTRA, + logic.zerg_common_unit_basic_aa, + ), + make_location_data( + SC2Mission.DOMINATION.mission_name, + "North Baneling Nest", + SC2HOTS_LOC_ID_OFFSET + 708, + LocationType.EXTRA, + logic.zerg_common_unit, + ), + make_location_data( + SC2Mission.DOMINATION.mission_name, + "Northeast Baneling Nest", + SC2HOTS_LOC_ID_OFFSET + 709, + LocationType.EXTRA, + logic.zerg_common_unit_basic_aa, + ), + make_location_data( + SC2Mission.DOMINATION.mission_name, + "Win Without 100 Eggs", + SC2HOTS_LOC_ID_OFFSET + 710, + LocationType.CHALLENGE, + logic.zerg_competent_comp_competent_aa, + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY.mission_name, + "Victory", + SC2HOTS_LOC_ID_OFFSET + 800, + LocationType.VICTORY, + lambda state: ( + logic.zerg_competent_comp(state) + and logic.zerg_moderate_anti_air(state) + and logic.spread_creep(state) + ), + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY.mission_name, + "West Biomass", + SC2HOTS_LOC_ID_OFFSET + 801, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY.mission_name, + "North Biomass", + SC2HOTS_LOC_ID_OFFSET + 802, + LocationType.VANILLA, + lambda state: ( + logic.zerg_competent_comp(state) + and logic.zerg_moderate_anti_air(state) + and logic.spread_creep(state) + ), + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY.mission_name, + "South Biomass", + SC2HOTS_LOC_ID_OFFSET + 803, + LocationType.VANILLA, + lambda state: ( + logic.zerg_competent_comp(state) + and logic.zerg_moderate_anti_air(state) + and logic.spread_creep(state) + ), + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY.mission_name, + "Destroy 3 Gorgons", + SC2HOTS_LOC_ID_OFFSET + 804, + LocationType.EXTRA, + lambda state: ( + logic.zerg_competent_comp(state) + and logic.zerg_moderate_anti_air(state) + and logic.spread_creep(state) + ), + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY.mission_name, + "Close Zerg Rescue", + SC2HOTS_LOC_ID_OFFSET + 805, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY.mission_name, + "South Zerg Rescue", + SC2HOTS_LOC_ID_OFFSET + 806, + LocationType.EXTRA, + logic.zerg_common_unit, + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY.mission_name, + "North Zerg Rescue", + SC2HOTS_LOC_ID_OFFSET + 807, + LocationType.EXTRA, + lambda state: ( + logic.zerg_competent_comp(state) + and logic.zerg_moderate_anti_air(state) + and logic.spread_creep(state) + ), + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY.mission_name, + "West Queen Rescue", + SC2HOTS_LOC_ID_OFFSET + 808, + LocationType.EXTRA, + lambda state: ( + logic.zerg_competent_comp(state) + and logic.zerg_moderate_anti_air(state) + and logic.spread_creep(state) + ), + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY.mission_name, + "East Queen Rescue", + SC2HOTS_LOC_ID_OFFSET + 809, + LocationType.EXTRA, + lambda state: ( + logic.zerg_competent_comp(state) + and logic.zerg_moderate_anti_air(state) + and logic.spread_creep(state) + ), + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY.mission_name, + "South Orbital Command Center", + SC2HOTS_LOC_ID_OFFSET + 810, + LocationType.CHALLENGE, + lambda state: ( + logic.zerg_competent_comp(state) and logic.zerg_moderate_anti_air(state) + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY.mission_name, + "Northwest Orbital Command Center", + SC2HOTS_LOC_ID_OFFSET + 811, + LocationType.CHALLENGE, + lambda state: ( + logic.zerg_competent_comp(state) and logic.zerg_moderate_anti_air(state) + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY.mission_name, + "Southeast Orbital Command Center", + SC2HOTS_LOC_ID_OFFSET + 812, + LocationType.CHALLENGE, + lambda state: ( + logic.zerg_competent_comp(state) and logic.zerg_moderate_anti_air(state) + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.OLD_SOLDIERS.mission_name, + "Victory", + SC2HOTS_LOC_ID_OFFSET + 900, + LocationType.VICTORY, + logic.zerg_competent_comp_competent_aa, + ), + make_location_data( + SC2Mission.OLD_SOLDIERS.mission_name, + "East Science Lab", + SC2HOTS_LOC_ID_OFFSET + 901, + LocationType.VANILLA, + logic.zerg_competent_comp_competent_aa, + ), + make_location_data( + SC2Mission.OLD_SOLDIERS.mission_name, + "North Science Lab", + SC2HOTS_LOC_ID_OFFSET + 902, + LocationType.VANILLA, + logic.zerg_competent_comp_competent_aa, + ), + make_location_data( + SC2Mission.OLD_SOLDIERS.mission_name, + "Get Nuked", + SC2HOTS_LOC_ID_OFFSET + 903, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.OLD_SOLDIERS.mission_name, + "Entrance Gate", + SC2HOTS_LOC_ID_OFFSET + 904, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.OLD_SOLDIERS.mission_name, + "Citadel Gate", + SC2HOTS_LOC_ID_OFFSET + 905, + LocationType.EXTRA, + logic.zerg_competent_comp_competent_aa, + ), + make_location_data( + SC2Mission.OLD_SOLDIERS.mission_name, + "South Expansion", + SC2HOTS_LOC_ID_OFFSET + 906, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.OLD_SOLDIERS.mission_name, + "Rich Mineral Expansion", + SC2HOTS_LOC_ID_OFFSET + 907, + LocationType.EXTRA, + logic.zerg_competent_comp_competent_aa, + ), + make_location_data( + SC2Mission.WAKING_THE_ANCIENT.mission_name, + "Victory", + SC2HOTS_LOC_ID_OFFSET + 1000, + LocationType.VICTORY, + logic.zerg_competent_comp_competent_aa, + hard_rule=logic.zerg_kerrigan_or_any_anti_air, + ), + make_location_data( + SC2Mission.WAKING_THE_ANCIENT.mission_name, + "Center Essence Pool", + SC2HOTS_LOC_ID_OFFSET + 1001, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.WAKING_THE_ANCIENT.mission_name, + "East Essence Pool", + SC2HOTS_LOC_ID_OFFSET + 1002, + LocationType.VANILLA, + lambda state: ( + logic.zerg_common_unit(state) + and ( + adv_tactics + and logic.zerg_basic_anti_air(state) + or logic.zerg_competent_anti_air(state) + ) + ), + ), + make_location_data( + SC2Mission.WAKING_THE_ANCIENT.mission_name, + "South Essence Pool", + SC2HOTS_LOC_ID_OFFSET + 1003, + LocationType.VANILLA, + lambda state: ( + logic.zerg_common_unit(state) + and ( + adv_tactics + and logic.zerg_basic_anti_air(state) + or logic.zerg_competent_anti_air(state) + ) + ), + ), + make_location_data( + SC2Mission.WAKING_THE_ANCIENT.mission_name, + "Finish Feeding", + SC2HOTS_LOC_ID_OFFSET + 1004, + LocationType.EXTRA, + logic.zerg_competent_comp_competent_aa, + hard_rule=logic.zerg_kerrigan_or_any_anti_air, + ), + make_location_data( + SC2Mission.WAKING_THE_ANCIENT.mission_name, + "South Proxy Primal Hive", + SC2HOTS_LOC_ID_OFFSET + 1005, + LocationType.CHALLENGE, + logic.zerg_competent_comp_competent_aa, + ), + make_location_data( + SC2Mission.WAKING_THE_ANCIENT.mission_name, + "East Proxy Primal Hive", + SC2HOTS_LOC_ID_OFFSET + 1006, + LocationType.CHALLENGE, + logic.zerg_competent_comp_competent_aa, + ), + make_location_data( + SC2Mission.WAKING_THE_ANCIENT.mission_name, + "South Main Primal Hive", + SC2HOTS_LOC_ID_OFFSET + 1007, + LocationType.CHALLENGE, + logic.zerg_competent_comp_competent_aa, + flags=LocationFlag.BASEBUST, + hard_rule=logic.zerg_kerrigan_or_any_anti_air, + ), + make_location_data( + SC2Mission.WAKING_THE_ANCIENT.mission_name, + "East Main Primal Hive", + SC2HOTS_LOC_ID_OFFSET + 1008, + LocationType.CHALLENGE, + logic.zerg_competent_comp_competent_aa, + flags=LocationFlag.BASEBUST, + hard_rule=logic.zerg_kerrigan_or_any_anti_air, + ), + make_location_data( + SC2Mission.WAKING_THE_ANCIENT.mission_name, + "Flawless", + SC2HOTS_LOC_ID_OFFSET + 1009, + LocationType.CHALLENGE, + logic.zerg_competent_comp_competent_aa, + flags=LocationFlag.PREVENTATIVE, + hard_rule=logic.zerg_kerrigan_or_any_anti_air, + ), + make_location_data( + SC2Mission.THE_CRUCIBLE.mission_name, + "Victory", + SC2HOTS_LOC_ID_OFFSET + 1100, + LocationType.VICTORY, + lambda state: ( + logic.zerg_common_unit(state) + and logic.zerg_defense_rating(state, True, True) >= 7 + and logic.zerg_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.THE_CRUCIBLE.mission_name, + "Tyrannozor", + SC2HOTS_LOC_ID_OFFSET + 1101, + LocationType.VANILLA, + lambda state: ( + logic.zerg_common_unit(state) + and logic.zerg_defense_rating(state, True, True) >= 7 + and logic.zerg_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.THE_CRUCIBLE.mission_name, + "Reach the Pool", + SC2HOTS_LOC_ID_OFFSET + 1102, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.THE_CRUCIBLE.mission_name, + "15 Minutes Remaining", + SC2HOTS_LOC_ID_OFFSET + 1103, + LocationType.EXTRA, + lambda state: ( + logic.zerg_common_unit(state) + and logic.zerg_defense_rating(state, True, True) >= 7 + and logic.zerg_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.THE_CRUCIBLE.mission_name, + "5 Minutes Remaining", + SC2HOTS_LOC_ID_OFFSET + 1104, + LocationType.EXTRA, + lambda state: ( + logic.zerg_common_unit(state) + and logic.zerg_defense_rating(state, True, True) >= 7 + and logic.zerg_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.THE_CRUCIBLE.mission_name, + "Pincer Attack", + SC2HOTS_LOC_ID_OFFSET + 1105, + LocationType.EXTRA, + lambda state: ( + logic.zerg_common_unit(state) + and logic.zerg_defense_rating(state, True, True) >= 7 + and logic.zerg_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.THE_CRUCIBLE.mission_name, + "Yagdra Claims Brakk's Pack", + SC2HOTS_LOC_ID_OFFSET + 1106, + LocationType.EXTRA, + lambda state: ( + logic.zerg_common_unit(state) + and logic.zerg_defense_rating(state, True, True) >= 7 + and logic.zerg_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.SUPREME.mission_name, + "Victory", + SC2HOTS_LOC_ID_OFFSET + 1200, + LocationType.VICTORY, + logic.supreme_requirement, + hard_rule=logic.supreme_requirement, + ), + make_location_data( + SC2Mission.SUPREME.mission_name, + "First Relic", + SC2HOTS_LOC_ID_OFFSET + 1201, + LocationType.VANILLA, + logic.supreme_requirement, + hard_rule=logic.supreme_requirement, + ), + make_location_data( + SC2Mission.SUPREME.mission_name, + "Second Relic", + SC2HOTS_LOC_ID_OFFSET + 1202, + LocationType.VANILLA, + logic.supreme_requirement, + hard_rule=logic.supreme_requirement, + ), + make_location_data( + SC2Mission.SUPREME.mission_name, + "Third Relic", + SC2HOTS_LOC_ID_OFFSET + 1203, + LocationType.VANILLA, + logic.supreme_requirement, + hard_rule=logic.supreme_requirement, + ), + make_location_data( + SC2Mission.SUPREME.mission_name, + "Fourth Relic", + SC2HOTS_LOC_ID_OFFSET + 1204, + LocationType.VANILLA, + logic.supreme_requirement, + hard_rule=logic.supreme_requirement, + ), + make_location_data( + SC2Mission.SUPREME.mission_name, + "Yagdra", + SC2HOTS_LOC_ID_OFFSET + 1205, + LocationType.EXTRA, + logic.supreme_requirement, + hard_rule=logic.supreme_requirement, + ), + make_location_data( + SC2Mission.SUPREME.mission_name, + "Kraith", + SC2HOTS_LOC_ID_OFFSET + 1206, + LocationType.EXTRA, + logic.supreme_requirement, + hard_rule=logic.supreme_requirement, + ), + make_location_data( + SC2Mission.SUPREME.mission_name, + "Slivan", + SC2HOTS_LOC_ID_OFFSET + 1207, + LocationType.EXTRA, + logic.supreme_requirement, + hard_rule=logic.supreme_requirement, + ), + make_location_data( + SC2Mission.INFESTED.mission_name, + "Victory", + SC2HOTS_LOC_ID_OFFSET + 1300, + LocationType.VICTORY, + lambda state: ( + logic.zerg_common_unit(state) + and ( + ( + logic.zerg_competent_anti_air(state) + and state.has(item_names.INFESTOR, player) + ) + or (adv_tactics and logic.zerg_moderate_anti_air(state)) + ) + ), + hard_rule=logic.zerg_kerrigan_or_any_anti_air, + ), + make_location_data( + SC2Mission.INFESTED.mission_name, + "East Science Facility", + SC2HOTS_LOC_ID_OFFSET + 1301, + LocationType.VANILLA, + lambda state: ( + logic.zerg_common_unit(state) + and logic.zerg_moderate_anti_air(state) + and logic.spread_creep(state) + ), + ), + make_location_data( + SC2Mission.INFESTED.mission_name, + "Center Science Facility", + SC2HOTS_LOC_ID_OFFSET + 1302, + LocationType.VANILLA, + lambda state: ( + logic.zerg_common_unit(state) + and logic.zerg_moderate_anti_air(state) + and logic.spread_creep(state) + ), + ), + make_location_data( + SC2Mission.INFESTED.mission_name, + "West Science Facility", + SC2HOTS_LOC_ID_OFFSET + 1303, + LocationType.VANILLA, + lambda state: ( + logic.zerg_common_unit(state) + and logic.zerg_moderate_anti_air(state) + and logic.spread_creep(state) + ), + ), + make_location_data( + SC2Mission.INFESTED.mission_name, + "First Intro Garrison", + SC2HOTS_LOC_ID_OFFSET + 1304, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.INFESTED.mission_name, + "Second Intro Garrison", + SC2HOTS_LOC_ID_OFFSET + 1305, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.INFESTED.mission_name, + "Base Garrison", + SC2HOTS_LOC_ID_OFFSET + 1306, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.INFESTED.mission_name, + "East Garrison", + SC2HOTS_LOC_ID_OFFSET + 1307, + LocationType.EXTRA, + lambda state: ( + logic.zerg_common_unit(state) + and logic.zerg_moderate_anti_air(state) + and (adv_tactics or state.has(item_names.INFESTOR, player)) + ), + ), + make_location_data( + SC2Mission.INFESTED.mission_name, + "Mid Garrison", + SC2HOTS_LOC_ID_OFFSET + 1308, + LocationType.EXTRA, + lambda state: ( + logic.zerg_common_unit(state) + and logic.zerg_moderate_anti_air(state) + and (adv_tactics or state.has(item_names.INFESTOR, player)) + ), + ), + make_location_data( + SC2Mission.INFESTED.mission_name, + "North Garrison", + SC2HOTS_LOC_ID_OFFSET + 1309, + LocationType.EXTRA, + lambda state: ( + logic.zerg_common_unit(state) + and logic.zerg_moderate_anti_air(state) + and (adv_tactics or state.has(item_names.INFESTOR, player)) + ), + ), + make_location_data( + SC2Mission.INFESTED.mission_name, + "Close Southwest Garrison", + SC2HOTS_LOC_ID_OFFSET + 1310, + LocationType.EXTRA, + lambda state: ( + logic.zerg_common_unit(state) + and logic.zerg_moderate_anti_air(state) + and (adv_tactics or state.has(item_names.INFESTOR, player)) + ), + ), + make_location_data( + SC2Mission.INFESTED.mission_name, + "Far Southwest Garrison", + SC2HOTS_LOC_ID_OFFSET + 1311, + LocationType.EXTRA, + lambda state: ( + logic.zerg_common_unit(state) + and logic.zerg_moderate_anti_air(state) + and (adv_tactics or state.has(item_names.INFESTOR, player)) + ), + ), + make_location_data( + SC2Mission.HAND_OF_DARKNESS.mission_name, + "Victory", + SC2HOTS_LOC_ID_OFFSET + 1400, + LocationType.VICTORY, + logic.zerg_hand_of_darkness_requirement, + ), + make_location_data( + SC2Mission.HAND_OF_DARKNESS.mission_name, + "North Brutalisk", + SC2HOTS_LOC_ID_OFFSET + 1401, + LocationType.VANILLA, + logic.zerg_hand_of_darkness_requirement, + ), + make_location_data( + SC2Mission.HAND_OF_DARKNESS.mission_name, + "South Brutalisk", + SC2HOTS_LOC_ID_OFFSET + 1402, + LocationType.VANILLA, + logic.zerg_hand_of_darkness_requirement, + ), + make_location_data( + SC2Mission.HAND_OF_DARKNESS.mission_name, + "Kill 1 Hybrid", + SC2HOTS_LOC_ID_OFFSET + 1403, + LocationType.EXTRA, + logic.zerg_hand_of_darkness_requirement, + ), + make_location_data( + SC2Mission.HAND_OF_DARKNESS.mission_name, + "Kill 2 Hybrid", + SC2HOTS_LOC_ID_OFFSET + 1404, + LocationType.EXTRA, + logic.zerg_hand_of_darkness_requirement, + ), + make_location_data( + SC2Mission.HAND_OF_DARKNESS.mission_name, + "Kill 3 Hybrid", + SC2HOTS_LOC_ID_OFFSET + 1405, + LocationType.EXTRA, + logic.zerg_hand_of_darkness_requirement, + ), + make_location_data( + SC2Mission.HAND_OF_DARKNESS.mission_name, + "Kill 4 Hybrid", + SC2HOTS_LOC_ID_OFFSET + 1406, + LocationType.EXTRA, + logic.zerg_hand_of_darkness_requirement, + ), + make_location_data( + SC2Mission.HAND_OF_DARKNESS.mission_name, + "Kill 5 Hybrid", + SC2HOTS_LOC_ID_OFFSET + 1407, + LocationType.EXTRA, + logic.zerg_hand_of_darkness_requirement, + ), + make_location_data( + SC2Mission.HAND_OF_DARKNESS.mission_name, + "Kill 6 Hybrid", + SC2HOTS_LOC_ID_OFFSET + 1408, + LocationType.EXTRA, + logic.zerg_hand_of_darkness_requirement, + ), + make_location_data( + SC2Mission.HAND_OF_DARKNESS.mission_name, + "Kill 7 Hybrid", + SC2HOTS_LOC_ID_OFFSET + 1409, + LocationType.EXTRA, + logic.zerg_hand_of_darkness_requirement, + ), + make_location_data( + SC2Mission.PHANTOMS_OF_THE_VOID.mission_name, + "Victory", + SC2HOTS_LOC_ID_OFFSET + 1500, + LocationType.VICTORY, + lambda state: ( + logic.zerg_competent_comp(state) + and ( + logic.zerg_competent_anti_air(state) + or (adv_tactics and logic.zerg_moderate_anti_air(state)) + ) + ), + ), + make_location_data( + SC2Mission.PHANTOMS_OF_THE_VOID.mission_name, + "Northwest Crystal", + SC2HOTS_LOC_ID_OFFSET + 1501, + LocationType.VANILLA, + lambda state: ( + logic.zerg_competent_comp(state) + and ( + logic.zerg_competent_anti_air(state) + or (adv_tactics and logic.zerg_moderate_anti_air(state)) + ) + ), + ), + make_location_data( + SC2Mission.PHANTOMS_OF_THE_VOID.mission_name, + "Northeast Crystal", + SC2HOTS_LOC_ID_OFFSET + 1502, + LocationType.VANILLA, + lambda state: ( + logic.zerg_competent_comp(state) + and ( + logic.zerg_competent_anti_air(state) + or (adv_tactics and logic.zerg_moderate_anti_air(state)) + ) + ), + ), + make_location_data( + SC2Mission.PHANTOMS_OF_THE_VOID.mission_name, + "South Crystal", + SC2HOTS_LOC_ID_OFFSET + 1503, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.PHANTOMS_OF_THE_VOID.mission_name, + "Base Established", + SC2HOTS_LOC_ID_OFFSET + 1504, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.PHANTOMS_OF_THE_VOID.mission_name, + "Close Temple", + SC2HOTS_LOC_ID_OFFSET + 1505, + LocationType.EXTRA, + lambda state: ( + logic.zerg_competent_comp(state) + and ( + logic.zerg_competent_anti_air(state) + or (adv_tactics and logic.zerg_moderate_anti_air(state)) + ) + ), + ), + make_location_data( + SC2Mission.PHANTOMS_OF_THE_VOID.mission_name, + "Mid Temple", + SC2HOTS_LOC_ID_OFFSET + 1506, + LocationType.EXTRA, + lambda state: ( + logic.zerg_competent_comp(state) + and ( + logic.zerg_competent_anti_air(state) + or (adv_tactics and logic.zerg_moderate_anti_air(state)) + ) + ), + ), + make_location_data( + SC2Mission.PHANTOMS_OF_THE_VOID.mission_name, + "Southeast Temple", + SC2HOTS_LOC_ID_OFFSET + 1507, + LocationType.EXTRA, + lambda state: ( + logic.zerg_competent_comp(state) + and ( + logic.zerg_competent_anti_air(state) + or (adv_tactics and logic.zerg_moderate_anti_air(state)) + ) + ), + ), + make_location_data( + SC2Mission.PHANTOMS_OF_THE_VOID.mission_name, + "Northeast Temple", + SC2HOTS_LOC_ID_OFFSET + 1508, + LocationType.EXTRA, + lambda state: ( + logic.zerg_competent_comp(state) + and ( + logic.zerg_competent_anti_air(state) + or (adv_tactics and logic.zerg_moderate_anti_air(state)) + ) + ), + ), + make_location_data( + SC2Mission.PHANTOMS_OF_THE_VOID.mission_name, + "Northwest Temple", + SC2HOTS_LOC_ID_OFFSET + 1509, + LocationType.EXTRA, + lambda state: ( + logic.zerg_competent_comp(state) + and ( + logic.zerg_competent_anti_air(state) + or (adv_tactics and logic.zerg_moderate_anti_air(state)) + ) + ), + ), + make_location_data( + SC2Mission.WITH_FRIENDS_LIKE_THESE.mission_name, + "Victory", + SC2HOTS_LOC_ID_OFFSET + 1600, + LocationType.VICTORY, + ), + make_location_data( + SC2Mission.WITH_FRIENDS_LIKE_THESE.mission_name, + "Pirate Capital Ship", + SC2HOTS_LOC_ID_OFFSET + 1601, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.WITH_FRIENDS_LIKE_THESE.mission_name, + "First Mineral Patch", + SC2HOTS_LOC_ID_OFFSET + 1602, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.WITH_FRIENDS_LIKE_THESE.mission_name, + "Second Mineral Patch", + SC2HOTS_LOC_ID_OFFSET + 1603, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.WITH_FRIENDS_LIKE_THESE.mission_name, + "Third Mineral Patch", + SC2HOTS_LOC_ID_OFFSET + 1604, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.CONVICTION.mission_name, + "Victory", + SC2HOTS_LOC_ID_OFFSET + 1700, + LocationType.VICTORY, + lambda state: ( + kerriganless + or ( + logic.two_kerrigan_actives(state) + and (logic.basic_kerrigan(state) or logic.grant_story_tech == GrantStoryTech.option_grant) + and logic.kerrigan_levels(state, 25) + ) + ), + ), + make_location_data( + SC2Mission.CONVICTION.mission_name, + "First Secret Documents", + SC2HOTS_LOC_ID_OFFSET + 1701, + LocationType.VANILLA, + lambda state: ( + logic.two_kerrigan_actives(state) and logic.kerrigan_levels(state, 25) + ) + or kerriganless, + ), + make_location_data( + SC2Mission.CONVICTION.mission_name, + "Second Secret Documents", + SC2HOTS_LOC_ID_OFFSET + 1702, + LocationType.VANILLA, + lambda state: ( + kerriganless + or ( + logic.two_kerrigan_actives(state) + and (logic.basic_kerrigan(state) or logic.grant_story_tech == GrantStoryTech.option_grant) + and logic.kerrigan_levels(state, 25) + ) + ), + ), + make_location_data( + SC2Mission.CONVICTION.mission_name, + "Power Coupling", + SC2HOTS_LOC_ID_OFFSET + 1703, + LocationType.EXTRA, + lambda state: ( + logic.two_kerrigan_actives(state) and logic.kerrigan_levels(state, 25) + ) + or kerriganless, + ), + make_location_data( + SC2Mission.CONVICTION.mission_name, + "Door Blasted", + SC2HOTS_LOC_ID_OFFSET + 1704, + LocationType.EXTRA, + lambda state: ( + logic.two_kerrigan_actives(state) and logic.kerrigan_levels(state, 25) + ) + or kerriganless, + ), + make_location_data( + SC2Mission.PLANETFALL.mission_name, + "Victory", + SC2HOTS_LOC_ID_OFFSET + 1800, + LocationType.VICTORY, + logic.zerg_planetfall_requirement, + hard_rule=logic.zerg_kerrigan_or_any_anti_air, + ), + make_location_data( + SC2Mission.PLANETFALL.mission_name, + "East Gate", + SC2HOTS_LOC_ID_OFFSET + 1801, + LocationType.VANILLA, + logic.zerg_planetfall_requirement, + ), + make_location_data( + SC2Mission.PLANETFALL.mission_name, + "Northwest Gate", + SC2HOTS_LOC_ID_OFFSET + 1802, + LocationType.VANILLA, + logic.zerg_planetfall_requirement, + ), + make_location_data( + SC2Mission.PLANETFALL.mission_name, + "North Gate", + SC2HOTS_LOC_ID_OFFSET + 1803, + LocationType.VANILLA, + logic.zerg_planetfall_requirement, + ), + make_location_data( + SC2Mission.PLANETFALL.mission_name, + "1 Bile Launcher Deployed", + SC2HOTS_LOC_ID_OFFSET + 1804, + LocationType.EXTRA, + logic.zerg_planetfall_requirement, + ), + make_location_data( + SC2Mission.PLANETFALL.mission_name, + "2 Bile Launchers Deployed", + SC2HOTS_LOC_ID_OFFSET + 1805, + LocationType.EXTRA, + logic.zerg_planetfall_requirement, + ), + make_location_data( + SC2Mission.PLANETFALL.mission_name, + "3 Bile Launchers Deployed", + SC2HOTS_LOC_ID_OFFSET + 1806, + LocationType.EXTRA, + logic.zerg_planetfall_requirement, + ), + make_location_data( + SC2Mission.PLANETFALL.mission_name, + "4 Bile Launchers Deployed", + SC2HOTS_LOC_ID_OFFSET + 1807, + LocationType.EXTRA, + logic.zerg_planetfall_requirement, + hard_rule=logic.zerg_kerrigan_or_any_anti_air, + ), + make_location_data( + SC2Mission.PLANETFALL.mission_name, + "5 Bile Launchers Deployed", + SC2HOTS_LOC_ID_OFFSET + 1808, + LocationType.EXTRA, + logic.zerg_planetfall_requirement, + hard_rule=logic.zerg_kerrigan_or_any_anti_air, + ), + make_location_data( + SC2Mission.PLANETFALL.mission_name, + "Sons of Korhal", + SC2HOTS_LOC_ID_OFFSET + 1809, + LocationType.EXTRA, + logic.zerg_planetfall_requirement, + ), + make_location_data( + SC2Mission.PLANETFALL.mission_name, + "Night Wolves", + SC2HOTS_LOC_ID_OFFSET + 1810, + LocationType.EXTRA, + logic.zerg_planetfall_requirement, + hard_rule=logic.zerg_kerrigan_or_any_anti_air, + ), + make_location_data( + SC2Mission.PLANETFALL.mission_name, + "West Expansion", + SC2HOTS_LOC_ID_OFFSET + 1811, + LocationType.EXTRA, + logic.zerg_planetfall_requirement, + hard_rule=logic.zerg_kerrigan_or_any_anti_air, + ), + make_location_data( + SC2Mission.PLANETFALL.mission_name, + "Mid Expansion", + SC2HOTS_LOC_ID_OFFSET + 1812, + LocationType.EXTRA, + logic.zerg_planetfall_requirement, + hard_rule=logic.zerg_kerrigan_or_any_anti_air, + ), + make_location_data( + SC2Mission.DEATH_FROM_ABOVE.mission_name, + "Victory", + SC2HOTS_LOC_ID_OFFSET + 1900, + LocationType.VICTORY, + lambda state: logic.zerg_competent_comp_competent_aa(state) + and (adv_tactics or logic.zerg_base_buster(state)), + ), + make_location_data( + SC2Mission.DEATH_FROM_ABOVE.mission_name, + "First Power Link", + SC2HOTS_LOC_ID_OFFSET + 1901, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.DEATH_FROM_ABOVE.mission_name, + "Second Power Link", + SC2HOTS_LOC_ID_OFFSET + 1902, + LocationType.VANILLA, + logic.zerg_competent_comp_competent_aa, + ), + make_location_data( + SC2Mission.DEATH_FROM_ABOVE.mission_name, + "Third Power Link", + SC2HOTS_LOC_ID_OFFSET + 1903, + LocationType.VANILLA, + logic.zerg_competent_comp_competent_aa, + ), + make_location_data( + SC2Mission.DEATH_FROM_ABOVE.mission_name, + "Expansion Command Center", + SC2HOTS_LOC_ID_OFFSET + 1904, + LocationType.EXTRA, + logic.zerg_competent_comp_competent_aa, + ), + make_location_data( + SC2Mission.DEATH_FROM_ABOVE.mission_name, + "Main Path Command Center", + SC2HOTS_LOC_ID_OFFSET + 1905, + LocationType.EXTRA, + lambda state: logic.zerg_competent_comp_competent_aa(state) + and (adv_tactics or logic.zerg_base_buster(state)), + ), + make_location_data( + SC2Mission.THE_RECKONING.mission_name, + "Victory", + SC2HOTS_LOC_ID_OFFSET + 2000, + LocationType.VICTORY, + logic.zerg_the_reckoning_requirement, + ), + make_location_data( + SC2Mission.THE_RECKONING.mission_name, + "South Lane", + SC2HOTS_LOC_ID_OFFSET + 2001, + LocationType.VANILLA, + logic.zerg_the_reckoning_requirement, + ), + make_location_data( + SC2Mission.THE_RECKONING.mission_name, + "North Lane", + SC2HOTS_LOC_ID_OFFSET + 2002, + LocationType.VANILLA, + logic.zerg_the_reckoning_requirement, + ), + make_location_data( + SC2Mission.THE_RECKONING.mission_name, + "East Lane", + SC2HOTS_LOC_ID_OFFSET + 2003, + LocationType.VANILLA, + logic.zerg_the_reckoning_requirement, + ), + make_location_data( + SC2Mission.THE_RECKONING.mission_name, + "Odin", + SC2HOTS_LOC_ID_OFFSET + 2004, + LocationType.EXTRA, + logic.zerg_the_reckoning_requirement, + ), + make_location_data( + SC2Mission.THE_RECKONING.mission_name, + "Trash the Odin Early", + SC2HOTS_LOC_ID_OFFSET + 2005, + LocationType.MASTERY, + lambda state: ( + logic.zerg_the_reckoning_requirement(state) + and ( + kerriganless + or ( + logic.kerrigan_levels(state, 50, False) + and state.has_any(kerrigan_logic_ultimates, player) + ) + ) + and logic.zerg_power_rating(state) >= 10 + ), + flags=LocationFlag.SPEEDRUN, + ), + # LotV Prologue + make_location_data( + SC2Mission.DARK_WHISPERS.mission_name, + "Victory", + SC2LOTV_LOC_ID_OFFSET + 100, + LocationType.VICTORY, + logic.protoss_common_unit_basic_aa, + ), + make_location_data( + SC2Mission.DARK_WHISPERS.mission_name, + "First Prisoner Group", + SC2LOTV_LOC_ID_OFFSET + 101, + LocationType.VANILLA, + logic.protoss_common_unit_basic_aa, + ), + make_location_data( + SC2Mission.DARK_WHISPERS.mission_name, + "Second Prisoner Group", + SC2LOTV_LOC_ID_OFFSET + 102, + LocationType.VANILLA, + logic.protoss_common_unit_basic_aa, + ), + make_location_data( + SC2Mission.DARK_WHISPERS.mission_name, + "First Pylon", + SC2LOTV_LOC_ID_OFFSET + 103, + LocationType.VANILLA, + logic.protoss_common_unit_basic_aa, + ), + make_location_data( + SC2Mission.DARK_WHISPERS.mission_name, + "Second Pylon", + SC2LOTV_LOC_ID_OFFSET + 104, + LocationType.VANILLA, + logic.protoss_common_unit_basic_aa, + ), + make_location_data( + SC2Mission.DARK_WHISPERS.mission_name, + "Zerg Base", + SC2LOTV_LOC_ID_OFFSET + 105, + LocationType.MASTERY, + lambda state: logic.protoss_deathball(state) + and logic.protoss_power_rating(state) >= 6, + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.GHOSTS_IN_THE_FOG.mission_name, + "Victory", + SC2LOTV_LOC_ID_OFFSET + 200, + LocationType.VICTORY, + lambda state: logic.protoss_competent_comp(state) + and logic.protoss_mineral_dump(state), + ), + make_location_data( + SC2Mission.GHOSTS_IN_THE_FOG.mission_name, + "South Rock Formation", + SC2LOTV_LOC_ID_OFFSET + 201, + LocationType.VANILLA, + lambda state: logic.protoss_competent_comp(state) + and logic.protoss_mineral_dump(state), + ), + make_location_data( + SC2Mission.GHOSTS_IN_THE_FOG.mission_name, + "West Rock Formation", + SC2LOTV_LOC_ID_OFFSET + 202, + LocationType.VANILLA, + lambda state: logic.protoss_competent_comp(state) + and logic.protoss_mineral_dump(state), + ), + make_location_data( + SC2Mission.GHOSTS_IN_THE_FOG.mission_name, + "East Rock Formation", + SC2LOTV_LOC_ID_OFFSET + 203, + LocationType.VANILLA, + lambda state: ( + logic.protoss_competent_comp(state) + and logic.protoss_mineral_dump(state) + and logic.protoss_can_attack_behind_chasm(state) + ), + ), + make_location_data( + SC2Mission.EVIL_AWOKEN.mission_name, + "Victory", + SC2LOTV_LOC_ID_OFFSET + 300, + LocationType.VICTORY, + lambda state: adv_tactics + or state.count_from_list( + ( + item_names.STALKER_PHASE_REACTOR, + item_names.STALKER_INSTIGATOR_SLAYER_DISINTEGRATING_PARTICLES, + item_names.STALKER_INSTIGATOR_SLAYER_PARTICLE_REFLECTION, + ), + player, + ) + >= 2, + ), + make_location_data( + SC2Mission.EVIL_AWOKEN.mission_name, + "Temple Investigated", + SC2LOTV_LOC_ID_OFFSET + 301, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.EVIL_AWOKEN.mission_name, + "Void Catalyst", + SC2LOTV_LOC_ID_OFFSET + 302, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.EVIL_AWOKEN.mission_name, + "First Particle Cannon", + SC2LOTV_LOC_ID_OFFSET + 303, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.EVIL_AWOKEN.mission_name, + "Second Particle Cannon", + SC2LOTV_LOC_ID_OFFSET + 304, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.EVIL_AWOKEN.mission_name, + "Third Particle Cannon", + SC2LOTV_LOC_ID_OFFSET + 305, + LocationType.VANILLA, + ), + # LotV + make_location_data( + SC2Mission.FOR_AIUR.mission_name, + "Victory", + SC2LOTV_LOC_ID_OFFSET + 400, + LocationType.VICTORY, + ), + make_location_data( + SC2Mission.FOR_AIUR.mission_name, + "Southwest Hive", + SC2LOTV_LOC_ID_OFFSET + 401, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.FOR_AIUR.mission_name, + "Northwest Hive", + SC2LOTV_LOC_ID_OFFSET + 402, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.FOR_AIUR.mission_name, + "Northeast Hive", + SC2LOTV_LOC_ID_OFFSET + 403, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.FOR_AIUR.mission_name, + "East Hive", + SC2LOTV_LOC_ID_OFFSET + 404, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.FOR_AIUR.mission_name, + "West Conduit", + SC2LOTV_LOC_ID_OFFSET + 405, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.FOR_AIUR.mission_name, + "Middle Conduit", + SC2LOTV_LOC_ID_OFFSET + 406, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.FOR_AIUR.mission_name, + "Northeast Conduit", + SC2LOTV_LOC_ID_OFFSET + 407, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.THE_GROWING_SHADOW.mission_name, + "Victory", + SC2LOTV_LOC_ID_OFFSET + 500, + LocationType.VICTORY, + lambda state: logic.protoss_common_unit(state) + and (adv_tactics or logic.protoss_moderate_anti_air(state)), + ), + make_location_data( + SC2Mission.THE_GROWING_SHADOW.mission_name, + "Close Pylon", + SC2LOTV_LOC_ID_OFFSET + 501, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.THE_GROWING_SHADOW.mission_name, + "East Pylon", + SC2LOTV_LOC_ID_OFFSET + 502, + LocationType.VANILLA, + lambda state: logic.protoss_common_unit(state) + and (adv_tactics or logic.protoss_moderate_anti_air(state)), + ), + make_location_data( + SC2Mission.THE_GROWING_SHADOW.mission_name, + "West Pylon", + SC2LOTV_LOC_ID_OFFSET + 503, + LocationType.VANILLA, + lambda state: logic.protoss_common_unit(state) + and (adv_tactics or logic.protoss_moderate_anti_air(state)), + ), + make_location_data( + SC2Mission.THE_GROWING_SHADOW.mission_name, + "Nexus", + SC2LOTV_LOC_ID_OFFSET + 504, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.THE_GROWING_SHADOW.mission_name, + "Templar Base", + SC2LOTV_LOC_ID_OFFSET + 505, + LocationType.EXTRA, + lambda state: logic.protoss_common_unit(state) + and (adv_tactics or logic.protoss_moderate_anti_air(state)), + ), + make_location_data( + SC2Mission.THE_SPEAR_OF_ADUN.mission_name, + "Victory", + SC2LOTV_LOC_ID_OFFSET + 600, + LocationType.VICTORY, + logic.protoss_spear_of_adun_requirement, + hard_rule=logic.protoss_any_anti_air_unit_or_soa, + ), + make_location_data( + SC2Mission.THE_SPEAR_OF_ADUN.mission_name, + "Close Warp Gate", + SC2LOTV_LOC_ID_OFFSET + 601, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.THE_SPEAR_OF_ADUN.mission_name, + "West Warp Gate", + SC2LOTV_LOC_ID_OFFSET + 602, + LocationType.VANILLA, + logic.protoss_spear_of_adun_requirement, + ), + make_location_data( + SC2Mission.THE_SPEAR_OF_ADUN.mission_name, + "North Warp Gate", + SC2LOTV_LOC_ID_OFFSET + 603, + LocationType.VANILLA, + logic.protoss_spear_of_adun_requirement, + ), + make_location_data( + SC2Mission.THE_SPEAR_OF_ADUN.mission_name, + "North Power Cell", + SC2LOTV_LOC_ID_OFFSET + 604, + LocationType.EXTRA, + logic.protoss_spear_of_adun_requirement, + ), + make_location_data( + SC2Mission.THE_SPEAR_OF_ADUN.mission_name, + "East Power Cell", + SC2LOTV_LOC_ID_OFFSET + 605, + LocationType.EXTRA, + logic.protoss_spear_of_adun_requirement, + ), + make_location_data( + SC2Mission.THE_SPEAR_OF_ADUN.mission_name, + "South Power Cell", + SC2LOTV_LOC_ID_OFFSET + 606, + LocationType.EXTRA, + logic.protoss_spear_of_adun_requirement, + ), + make_location_data( + SC2Mission.THE_SPEAR_OF_ADUN.mission_name, + "Southeast Power Cell", + SC2LOTV_LOC_ID_OFFSET + 607, + LocationType.EXTRA, + logic.protoss_spear_of_adun_requirement, + ), + make_location_data( + SC2Mission.SKY_SHIELD.mission_name, + "Victory", + SC2LOTV_LOC_ID_OFFSET + 700, + LocationType.VICTORY, + logic.protoss_sky_shield_requirement, + ), + make_location_data( + SC2Mission.SKY_SHIELD.mission_name, + "Mid EMP Scrambler", + SC2LOTV_LOC_ID_OFFSET + 701, + LocationType.VANILLA, + logic.protoss_sky_shield_requirement, + ), + make_location_data( + SC2Mission.SKY_SHIELD.mission_name, + "Southeast EMP Scrambler", + SC2LOTV_LOC_ID_OFFSET + 702, + LocationType.VANILLA, + logic.protoss_sky_shield_requirement, + ), + make_location_data( + SC2Mission.SKY_SHIELD.mission_name, + "North EMP Scrambler", + SC2LOTV_LOC_ID_OFFSET + 703, + LocationType.VANILLA, + logic.protoss_sky_shield_requirement, + ), + make_location_data( + SC2Mission.SKY_SHIELD.mission_name, + "Mid Stabilizer", + SC2LOTV_LOC_ID_OFFSET + 704, + LocationType.EXTRA, + logic.protoss_common_unit, + ), + make_location_data( + SC2Mission.SKY_SHIELD.mission_name, + "Southwest Stabilizer", + SC2LOTV_LOC_ID_OFFSET + 705, + LocationType.EXTRA, + logic.protoss_sky_shield_requirement, + ), + make_location_data( + SC2Mission.SKY_SHIELD.mission_name, + "Northwest Stabilizer", + SC2LOTV_LOC_ID_OFFSET + 706, + LocationType.EXTRA, + logic.protoss_sky_shield_requirement, + ), + make_location_data( + SC2Mission.SKY_SHIELD.mission_name, + "Northeast Stabilizer", + SC2LOTV_LOC_ID_OFFSET + 707, + LocationType.EXTRA, + logic.protoss_sky_shield_requirement, + ), + make_location_data( + SC2Mission.SKY_SHIELD.mission_name, + "Southeast Stabilizer", + SC2LOTV_LOC_ID_OFFSET + 708, + LocationType.EXTRA, + logic.protoss_sky_shield_requirement, + ), + make_location_data( + SC2Mission.SKY_SHIELD.mission_name, + "West Raynor Base", + SC2LOTV_LOC_ID_OFFSET + 709, + LocationType.EXTRA, + logic.protoss_sky_shield_requirement, + ), + make_location_data( + SC2Mission.SKY_SHIELD.mission_name, + "East Raynor Base", + SC2LOTV_LOC_ID_OFFSET + 710, + LocationType.EXTRA, + logic.protoss_sky_shield_requirement, + ), + make_location_data( + SC2Mission.BROTHERS_IN_ARMS.mission_name, + "Victory", + SC2LOTV_LOC_ID_OFFSET + 800, + LocationType.VICTORY, + logic.protoss_brothers_in_arms_requirement, + ), + make_location_data( + SC2Mission.BROTHERS_IN_ARMS.mission_name, + "Mid Science Facility", + SC2LOTV_LOC_ID_OFFSET + 801, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.BROTHERS_IN_ARMS.mission_name, + "North Science Facility", + SC2LOTV_LOC_ID_OFFSET + 802, + LocationType.VANILLA, + lambda state: ( + logic.protoss_brothers_in_arms_requirement(state) + or ( + logic.take_over_ai_allies + and logic.advanced_tactics + and ( + logic.terran_common_unit(state) + or logic.protoss_common_unit(state) + ) + ) + ), + ), + make_location_data( + SC2Mission.BROTHERS_IN_ARMS.mission_name, + "South Science Facility", + SC2LOTV_LOC_ID_OFFSET + 803, + LocationType.VANILLA, + logic.protoss_brothers_in_arms_requirement, + ), + make_location_data( + SC2Mission.BROTHERS_IN_ARMS.mission_name, + "Raynor Forward Positions", + SC2LOTV_LOC_ID_OFFSET + 804, + LocationType.EXTRA, + logic.protoss_brothers_in_arms_requirement, + ), + make_location_data( + SC2Mission.BROTHERS_IN_ARMS.mission_name, + "Valerian Forward Positions", + SC2LOTV_LOC_ID_OFFSET + 805, + LocationType.EXTRA, + logic.protoss_brothers_in_arms_requirement, + ), + make_location_data( + SC2Mission.BROTHERS_IN_ARMS.mission_name, + "Win in under 15 minutes", + SC2LOTV_LOC_ID_OFFSET + 806, + LocationType.CHALLENGE, + lambda state: ( + logic.protoss_brothers_in_arms_requirement(state) + and logic.protoss_deathball(state) + and logic.protoss_power_rating(state) >= 8 + ), + flags=LocationFlag.SPEEDRUN, + ), + make_location_data( + SC2Mission.AMON_S_REACH.mission_name, + "Victory", + SC2LOTV_LOC_ID_OFFSET + 900, + LocationType.VICTORY, + logic.protoss_common_unit_anti_light_air, + ), + make_location_data( + SC2Mission.AMON_S_REACH.mission_name, + "Close Solarite Reserve", + SC2LOTV_LOC_ID_OFFSET + 901, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.AMON_S_REACH.mission_name, + "North Solarite Reserve", + SC2LOTV_LOC_ID_OFFSET + 902, + LocationType.VANILLA, + logic.protoss_common_unit_anti_light_air, + ), + make_location_data( + SC2Mission.AMON_S_REACH.mission_name, + "East Solarite Reserve", + SC2LOTV_LOC_ID_OFFSET + 903, + LocationType.VANILLA, + logic.protoss_common_unit_anti_light_air, + ), + make_location_data( + SC2Mission.AMON_S_REACH.mission_name, + "West Launch Bay", + SC2LOTV_LOC_ID_OFFSET + 904, + LocationType.EXTRA, + logic.protoss_common_unit_anti_light_air, + ), + make_location_data( + SC2Mission.AMON_S_REACH.mission_name, + "South Launch Bay", + SC2LOTV_LOC_ID_OFFSET + 905, + LocationType.EXTRA, + logic.protoss_common_unit_anti_light_air, + ), + make_location_data( + SC2Mission.AMON_S_REACH.mission_name, + "Northwest Launch Bay", + SC2LOTV_LOC_ID_OFFSET + 906, + LocationType.EXTRA, + logic.protoss_common_unit_anti_light_air, + ), + make_location_data( + SC2Mission.AMON_S_REACH.mission_name, + "East Launch Bay", + SC2LOTV_LOC_ID_OFFSET + 907, + LocationType.EXTRA, + logic.protoss_common_unit_anti_light_air, + ), + make_location_data( + SC2Mission.LAST_STAND.mission_name, + "Victory", + SC2LOTV_LOC_ID_OFFSET + 1000, + LocationType.VICTORY, + logic.protoss_last_stand_requirement, + ), + make_location_data( + SC2Mission.LAST_STAND.mission_name, + "West Zenith Stone", + SC2LOTV_LOC_ID_OFFSET + 1001, + LocationType.VANILLA, + logic.protoss_last_stand_requirement, + ), + make_location_data( + SC2Mission.LAST_STAND.mission_name, + "North Zenith Stone", + SC2LOTV_LOC_ID_OFFSET + 1002, + LocationType.VANILLA, + logic.protoss_last_stand_requirement, + ), + make_location_data( + SC2Mission.LAST_STAND.mission_name, + "East Zenith Stone", + SC2LOTV_LOC_ID_OFFSET + 1003, + LocationType.VANILLA, + logic.protoss_last_stand_requirement, + ), + make_location_data( + SC2Mission.LAST_STAND.mission_name, + "1 Billion Zerg", + SC2LOTV_LOC_ID_OFFSET + 1004, + LocationType.EXTRA, + logic.protoss_last_stand_requirement, + ), + make_location_data( + SC2Mission.LAST_STAND.mission_name, + "1.5 Billion Zerg", + SC2LOTV_LOC_ID_OFFSET + 1005, + LocationType.VANILLA, + lambda state: ( + logic.protoss_last_stand_requirement(state) + and ( + state.has_all( + { + item_names.KHAYDARIN_MONOLITH, + item_names.PHOTON_CANNON, + item_names.SHIELD_BATTERY, + }, + player, + ) + or state.has_any( + {item_names.SOA_SOLAR_LANCE, item_names.SOA_DEPLOY_FENIX}, + player, + ) + ) + and logic.protoss_defense_rating(state, False) >= 13 + ), + ), + make_location_data( + SC2Mission.FORBIDDEN_WEAPON.mission_name, + "Victory", + SC2LOTV_LOC_ID_OFFSET + 1100, + LocationType.VICTORY, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.FORBIDDEN_WEAPON.mission_name, + "South Solarite", + SC2LOTV_LOC_ID_OFFSET + 1101, + LocationType.VANILLA, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.FORBIDDEN_WEAPON.mission_name, + "North Solarite", + SC2LOTV_LOC_ID_OFFSET + 1102, + LocationType.VANILLA, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.FORBIDDEN_WEAPON.mission_name, + "Northwest Solarite", + SC2LOTV_LOC_ID_OFFSET + 1103, + LocationType.VANILLA, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.FORBIDDEN_WEAPON.mission_name, + "Rescue Sentries", + SC2LOTV_LOC_ID_OFFSET + 1104, + LocationType.EXTRA, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.FORBIDDEN_WEAPON.mission_name, + "Destroy Gateways", + SC2LOTV_LOC_ID_OFFSET + 1105, + LocationType.CHALLENGE, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.TEMPLE_OF_UNIFICATION.mission_name, + "Victory", + SC2LOTV_LOC_ID_OFFSET + 1200, + LocationType.VICTORY, + logic.protoss_temple_of_unification_requirement, + ), + make_location_data( + SC2Mission.TEMPLE_OF_UNIFICATION.mission_name, + "Mid Celestial Lock", + SC2LOTV_LOC_ID_OFFSET + 1201, + LocationType.EXTRA, + logic.protoss_temple_of_unification_requirement, + ), + make_location_data( + SC2Mission.TEMPLE_OF_UNIFICATION.mission_name, + "West Celestial Lock", + SC2LOTV_LOC_ID_OFFSET + 1202, + LocationType.EXTRA, + logic.protoss_temple_of_unification_requirement, + ), + make_location_data( + SC2Mission.TEMPLE_OF_UNIFICATION.mission_name, + "South Celestial Lock", + SC2LOTV_LOC_ID_OFFSET + 1203, + LocationType.EXTRA, + logic.protoss_temple_of_unification_requirement, + ), + make_location_data( + SC2Mission.TEMPLE_OF_UNIFICATION.mission_name, + "East Celestial Lock", + SC2LOTV_LOC_ID_OFFSET + 1204, + LocationType.EXTRA, + logic.protoss_temple_of_unification_requirement, + ), + make_location_data( + SC2Mission.TEMPLE_OF_UNIFICATION.mission_name, + "North Celestial Lock", + SC2LOTV_LOC_ID_OFFSET + 1205, + LocationType.EXTRA, + logic.protoss_temple_of_unification_requirement, + ), + make_location_data( + SC2Mission.TEMPLE_OF_UNIFICATION.mission_name, + "Titanic Warp Prism", + SC2LOTV_LOC_ID_OFFSET + 1206, + LocationType.VANILLA, + logic.protoss_temple_of_unification_requirement, + hard_rule=logic.protoss_any_anti_air_unit_or_soa, + ), + make_location_data( + SC2Mission.TEMPLE_OF_UNIFICATION.mission_name, + "Terran Main Base", + SC2LOTV_LOC_ID_OFFSET + 1207, + LocationType.MASTERY, + lambda state: logic.protoss_temple_of_unification_requirement(state) + and logic.protoss_deathball(state), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.TEMPLE_OF_UNIFICATION.mission_name, + "Protoss Main Base", + SC2LOTV_LOC_ID_OFFSET + 1208, + LocationType.MASTERY, + lambda state: logic.protoss_temple_of_unification_requirement(state) + and logic.protoss_deathball(state), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.THE_INFINITE_CYCLE.mission_name, + "Victory", + SC2LOTV_LOC_ID_OFFSET + 1300, + LocationType.VICTORY, + logic.the_infinite_cycle_requirement, + ), + make_location_data( + SC2Mission.THE_INFINITE_CYCLE.mission_name, + "First Hall of Revelation", + SC2LOTV_LOC_ID_OFFSET + 1301, + LocationType.EXTRA, + logic.the_infinite_cycle_requirement, + ), + make_location_data( + SC2Mission.THE_INFINITE_CYCLE.mission_name, + "Second Hall of Revelation", + SC2LOTV_LOC_ID_OFFSET + 1302, + LocationType.EXTRA, + logic.the_infinite_cycle_requirement, + ), + make_location_data( + SC2Mission.THE_INFINITE_CYCLE.mission_name, + "First Xel'Naga Device", + SC2LOTV_LOC_ID_OFFSET + 1303, + LocationType.VANILLA, + logic.the_infinite_cycle_requirement, + ), + make_location_data( + SC2Mission.THE_INFINITE_CYCLE.mission_name, + "Second Xel'Naga Device", + SC2LOTV_LOC_ID_OFFSET + 1304, + LocationType.VANILLA, + logic.the_infinite_cycle_requirement, + ), + make_location_data( + SC2Mission.THE_INFINITE_CYCLE.mission_name, + "Third Xel'Naga Device", + SC2LOTV_LOC_ID_OFFSET + 1305, + LocationType.VANILLA, + logic.the_infinite_cycle_requirement, + ), + make_location_data( + SC2Mission.HARBINGER_OF_OBLIVION.mission_name, + "Victory", + SC2LOTV_LOC_ID_OFFSET + 1400, + LocationType.VICTORY, + logic.protoss_harbinger_of_oblivion_requirement, + ), + make_location_data( + SC2Mission.HARBINGER_OF_OBLIVION.mission_name, + "Artanis", + SC2LOTV_LOC_ID_OFFSET + 1401, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.HARBINGER_OF_OBLIVION.mission_name, + "Northwest Void Crystal", + SC2LOTV_LOC_ID_OFFSET + 1402, + LocationType.EXTRA, + logic.protoss_harbinger_of_oblivion_requirement, + ), + make_location_data( + SC2Mission.HARBINGER_OF_OBLIVION.mission_name, + "Northeast Void Crystal", + SC2LOTV_LOC_ID_OFFSET + 1403, + LocationType.EXTRA, + logic.protoss_harbinger_of_oblivion_requirement, + ), + make_location_data( + SC2Mission.HARBINGER_OF_OBLIVION.mission_name, + "Southwest Void Crystal", + SC2LOTV_LOC_ID_OFFSET + 1404, + LocationType.EXTRA, + logic.protoss_harbinger_of_oblivion_requirement, + ), + make_location_data( + SC2Mission.HARBINGER_OF_OBLIVION.mission_name, + "Southeast Void Crystal", + SC2LOTV_LOC_ID_OFFSET + 1405, + LocationType.EXTRA, + logic.protoss_harbinger_of_oblivion_requirement, + ), + make_location_data( + SC2Mission.HARBINGER_OF_OBLIVION.mission_name, + "South Xel'Naga Vessel", + SC2LOTV_LOC_ID_OFFSET + 1406, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.HARBINGER_OF_OBLIVION.mission_name, + "Mid Xel'Naga Vessel", + SC2LOTV_LOC_ID_OFFSET + 1407, + LocationType.VANILLA, + logic.protoss_harbinger_of_oblivion_requirement, + ), + make_location_data( + SC2Mission.HARBINGER_OF_OBLIVION.mission_name, + "North Xel'Naga Vessel", + SC2LOTV_LOC_ID_OFFSET + 1408, + LocationType.VANILLA, + logic.protoss_harbinger_of_oblivion_requirement, + ), + make_location_data( + SC2Mission.UNSEALING_THE_PAST.mission_name, + "Victory", + SC2LOTV_LOC_ID_OFFSET + 1500, + LocationType.VICTORY, + lambda state: ( + logic.protoss_deathball(state) + and logic.protoss_power_rating(state) >= 6 + ), + ), + make_location_data( + SC2Mission.UNSEALING_THE_PAST.mission_name, + "Zerg Cleared", + SC2LOTV_LOC_ID_OFFSET + 1501, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.UNSEALING_THE_PAST.mission_name, + "First Stasis Lock", + SC2LOTV_LOC_ID_OFFSET + 1502, + LocationType.EXTRA, + lambda state: ( + logic.protoss_deathball(state) + and logic.protoss_power_rating(state) >= 6 + ), + ), + make_location_data( + SC2Mission.UNSEALING_THE_PAST.mission_name, + "Second Stasis Lock", + SC2LOTV_LOC_ID_OFFSET + 1503, + LocationType.EXTRA, + lambda state: ( + logic.protoss_deathball(state) + and logic.protoss_power_rating(state) >= 6 + ), + ), + make_location_data( + SC2Mission.UNSEALING_THE_PAST.mission_name, + "Third Stasis Lock", + SC2LOTV_LOC_ID_OFFSET + 1504, + LocationType.EXTRA, + lambda state: ( + logic.protoss_deathball(state) + and logic.protoss_power_rating(state) >= 6 + ), + ), + make_location_data( + SC2Mission.UNSEALING_THE_PAST.mission_name, + "Fourth Stasis Lock", + SC2LOTV_LOC_ID_OFFSET + 1505, + LocationType.EXTRA, + lambda state: ( + logic.protoss_deathball(state) + and logic.protoss_power_rating(state) >= 6 + ), + ), + make_location_data( + SC2Mission.UNSEALING_THE_PAST.mission_name, + "South Power Core", + SC2LOTV_LOC_ID_OFFSET + 1506, + LocationType.VANILLA, + lambda state: ( + logic.protoss_deathball(state) + and logic.protoss_power_rating(state) >= 6 + and (adv_tactics or logic.protoss_fleet(state)) + ), + ), + make_location_data( + SC2Mission.UNSEALING_THE_PAST.mission_name, + "East Power Core", + SC2LOTV_LOC_ID_OFFSET + 1507, + LocationType.VANILLA, + lambda state: ( + logic.protoss_deathball(state) + and logic.protoss_power_rating(state) >= 6 + and (adv_tactics or logic.protoss_fleet(state)) + ), + ), + make_location_data( + SC2Mission.PURIFICATION.mission_name, + "Victory", + SC2LOTV_LOC_ID_OFFSET + 1600, + LocationType.VICTORY, + logic.protoss_deathball, + ), + make_location_data( + SC2Mission.PURIFICATION.mission_name, + "North Sector: West Null Circuit", + SC2LOTV_LOC_ID_OFFSET + 1601, + LocationType.VANILLA, + logic.protoss_deathball, + ), + make_location_data( + SC2Mission.PURIFICATION.mission_name, + "North Sector: Northeast Null Circuit", + SC2LOTV_LOC_ID_OFFSET + 1602, + LocationType.EXTRA, + logic.protoss_deathball, + ), + make_location_data( + SC2Mission.PURIFICATION.mission_name, + "North Sector: Southeast Null Circuit", + SC2LOTV_LOC_ID_OFFSET + 1603, + LocationType.EXTRA, + logic.protoss_deathball, + ), + make_location_data( + SC2Mission.PURIFICATION.mission_name, + "South Sector: West Null Circuit", + SC2LOTV_LOC_ID_OFFSET + 1604, + LocationType.VANILLA, + logic.protoss_deathball, + ), + make_location_data( + SC2Mission.PURIFICATION.mission_name, + "South Sector: North Null Circuit", + SC2LOTV_LOC_ID_OFFSET + 1605, + LocationType.EXTRA, + logic.protoss_deathball, + ), + make_location_data( + SC2Mission.PURIFICATION.mission_name, + "South Sector: East Null Circuit", + SC2LOTV_LOC_ID_OFFSET + 1606, + LocationType.EXTRA, + logic.protoss_deathball, + ), + make_location_data( + SC2Mission.PURIFICATION.mission_name, + "West Sector: West Null Circuit", + SC2LOTV_LOC_ID_OFFSET + 1607, + LocationType.VANILLA, + logic.protoss_deathball, + ), + make_location_data( + SC2Mission.PURIFICATION.mission_name, + "West Sector: Mid Null Circuit", + SC2LOTV_LOC_ID_OFFSET + 1608, + LocationType.EXTRA, + logic.protoss_deathball, + ), + make_location_data( + SC2Mission.PURIFICATION.mission_name, + "West Sector: East Null Circuit", + SC2LOTV_LOC_ID_OFFSET + 1609, + LocationType.EXTRA, + logic.protoss_deathball, + ), + make_location_data( + SC2Mission.PURIFICATION.mission_name, + "East Sector: North Null Circuit", + SC2LOTV_LOC_ID_OFFSET + 1610, + LocationType.VANILLA, + logic.protoss_deathball, + ), + make_location_data( + SC2Mission.PURIFICATION.mission_name, + "East Sector: West Null Circuit", + SC2LOTV_LOC_ID_OFFSET + 1611, + LocationType.EXTRA, + logic.protoss_deathball, + ), + make_location_data( + SC2Mission.PURIFICATION.mission_name, + "East Sector: South Null Circuit", + SC2LOTV_LOC_ID_OFFSET + 1612, + LocationType.EXTRA, + logic.protoss_deathball, + ), + make_location_data( + SC2Mission.PURIFICATION.mission_name, + "Purifier Warden", + SC2LOTV_LOC_ID_OFFSET + 1613, + LocationType.VANILLA, + logic.protoss_deathball, + ), + make_location_data( + SC2Mission.STEPS_OF_THE_RITE.mission_name, + "Victory", + SC2LOTV_LOC_ID_OFFSET + 1700, + LocationType.VICTORY, + logic.protoss_steps_of_the_rite_requirement, + ), + make_location_data( + SC2Mission.STEPS_OF_THE_RITE.mission_name, + "First Terrazine Fog", + SC2LOTV_LOC_ID_OFFSET + 1701, + LocationType.EXTRA, + logic.protoss_steps_of_the_rite_requirement, + ), + make_location_data( + SC2Mission.STEPS_OF_THE_RITE.mission_name, + "Southwest Guardian", + SC2LOTV_LOC_ID_OFFSET + 1702, + LocationType.EXTRA, + logic.protoss_steps_of_the_rite_requirement, + ), + make_location_data( + SC2Mission.STEPS_OF_THE_RITE.mission_name, + "West Guardian", + SC2LOTV_LOC_ID_OFFSET + 1703, + LocationType.EXTRA, + logic.protoss_steps_of_the_rite_requirement, + ), + make_location_data( + SC2Mission.STEPS_OF_THE_RITE.mission_name, + "Northwest Guardian", + SC2LOTV_LOC_ID_OFFSET + 1704, + LocationType.EXTRA, + logic.protoss_steps_of_the_rite_requirement, + ), + make_location_data( + SC2Mission.STEPS_OF_THE_RITE.mission_name, + "Northeast Guardian", + SC2LOTV_LOC_ID_OFFSET + 1705, + LocationType.EXTRA, + logic.protoss_steps_of_the_rite_requirement, + ), + make_location_data( + SC2Mission.STEPS_OF_THE_RITE.mission_name, + "North Mothership", + SC2LOTV_LOC_ID_OFFSET + 1706, + LocationType.VANILLA, + logic.protoss_steps_of_the_rite_requirement, + hard_rule=logic.protoss_any_anti_air_unit_or_soa, + ), + make_location_data( + SC2Mission.STEPS_OF_THE_RITE.mission_name, + "South Mothership", + SC2LOTV_LOC_ID_OFFSET + 1707, + LocationType.VANILLA, + logic.protoss_steps_of_the_rite_requirement, + hard_rule=logic.protoss_any_anti_air_unit_or_soa, + ), + make_location_data( + SC2Mission.RAK_SHIR.mission_name, + "Victory", + SC2LOTV_LOC_ID_OFFSET + 1800, + LocationType.VICTORY, + logic.protoss_rak_shir_requirement, + ), + make_location_data( + SC2Mission.RAK_SHIR.mission_name, + "North Slayn Elemental", + SC2LOTV_LOC_ID_OFFSET + 1801, + LocationType.VANILLA, + logic.protoss_rak_shir_requirement, + hard_rule=logic.protoss_any_anti_air_unit_or_soa, + ), + make_location_data( + SC2Mission.RAK_SHIR.mission_name, + "Southwest Slayn Elemental", + SC2LOTV_LOC_ID_OFFSET + 1802, + LocationType.VANILLA, + logic.protoss_rak_shir_requirement, + hard_rule=logic.protoss_any_anti_air_unit_or_soa, + ), + make_location_data( + SC2Mission.RAK_SHIR.mission_name, + "East Slayn Elemental", + SC2LOTV_LOC_ID_OFFSET + 1803, + LocationType.VANILLA, + logic.protoss_rak_shir_requirement, + hard_rule=logic.protoss_any_anti_air_unit_or_soa, + ), + make_location_data( + SC2Mission.RAK_SHIR.mission_name, + "Resource Pickups", + SC2LOTV_LOC_ID_OFFSET + 1804, + LocationType.EXTRA, + logic.protoss_rak_shir_requirement, + ), + make_location_data( + SC2Mission.RAK_SHIR.mission_name, + "Destroy Nexuses", + SC2LOTV_LOC_ID_OFFSET + 1805, + LocationType.CHALLENGE, + logic.protoss_rak_shir_requirement, + ), + make_location_data( + SC2Mission.RAK_SHIR.mission_name, + "Win in under 15 minutes", + SC2LOTV_LOC_ID_OFFSET + 1806, + LocationType.MASTERY, + logic.protoss_rak_shir_requirement, + flags=LocationFlag.SPEEDRUN, + ), + make_location_data( + SC2Mission.TEMPLAR_S_CHARGE.mission_name, + "Victory", + SC2LOTV_LOC_ID_OFFSET + 1900, + LocationType.VICTORY, + logic.protoss_templars_charge_requirement, + ), + make_location_data( + SC2Mission.TEMPLAR_S_CHARGE.mission_name, + "Northwest Power Core", + SC2LOTV_LOC_ID_OFFSET + 1901, + LocationType.EXTRA, + logic.protoss_templars_charge_requirement, + ), + make_location_data( + SC2Mission.TEMPLAR_S_CHARGE.mission_name, + "Northeast Power Core", + SC2LOTV_LOC_ID_OFFSET + 1902, + LocationType.EXTRA, + logic.protoss_templars_charge_requirement, + ), + make_location_data( + SC2Mission.TEMPLAR_S_CHARGE.mission_name, + "Southeast Power Core", + SC2LOTV_LOC_ID_OFFSET + 1903, + LocationType.EXTRA, + logic.protoss_templars_charge_requirement, + ), + make_location_data( + SC2Mission.TEMPLAR_S_CHARGE.mission_name, + "West Hybrid Stasis Chamber", + SC2LOTV_LOC_ID_OFFSET + 1904, + LocationType.VANILLA, + logic.protoss_templars_charge_requirement, + ), + make_location_data( + SC2Mission.TEMPLAR_S_CHARGE.mission_name, + "Southeast Hybrid Stasis Chamber", + SC2LOTV_LOC_ID_OFFSET + 1905, + LocationType.VANILLA, + logic.protoss_fleet, + ), + make_location_data( + SC2Mission.TEMPLAR_S_RETURN.mission_name, + "Victory", + SC2LOTV_LOC_ID_OFFSET + 2000, + LocationType.VICTORY, + logic.templars_return_phase_3_reach_dts_requirement, + ), + make_location_data( + SC2Mission.TEMPLAR_S_RETURN.mission_name, + "Citadel: First Gate", + SC2LOTV_LOC_ID_OFFSET + 2001, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.TEMPLAR_S_RETURN.mission_name, + "Citadel: Second Gate", + SC2LOTV_LOC_ID_OFFSET + 2002, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.TEMPLAR_S_RETURN.mission_name, + "Citadel: Power Structure", + SC2LOTV_LOC_ID_OFFSET + 2003, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.TEMPLAR_S_RETURN.mission_name, + "Temple Grounds: Gather Army", + SC2LOTV_LOC_ID_OFFSET + 2004, + LocationType.VANILLA, + logic.templars_return_phase_2_requirement, + ), + make_location_data( + SC2Mission.TEMPLAR_S_RETURN.mission_name, + "Temple Grounds: Power Structure", + SC2LOTV_LOC_ID_OFFSET + 2005, + LocationType.VANILLA, + logic.templars_return_phase_2_requirement, + ), + make_location_data( + SC2Mission.TEMPLAR_S_RETURN.mission_name, + "Caverns: Purifier", + SC2LOTV_LOC_ID_OFFSET + 2006, + LocationType.EXTRA, + logic.templars_return_phase_3_reach_colossus_requirement, + ), + make_location_data( + SC2Mission.TEMPLAR_S_RETURN.mission_name, + "Caverns: Dark Templar", + SC2LOTV_LOC_ID_OFFSET + 2007, + LocationType.EXTRA, + logic.templars_return_phase_3_reach_dts_requirement, + ), + make_location_data( + SC2Mission.THE_HOST.mission_name, + "Victory", + SC2LOTV_LOC_ID_OFFSET + 2100, + LocationType.VICTORY, + logic.protoss_the_host_requirement, + ), + make_location_data( + SC2Mission.THE_HOST.mission_name, + "Southeast Void Shard", + SC2LOTV_LOC_ID_OFFSET + 2101, + LocationType.EXTRA, + logic.protoss_the_host_requirement, + ), + make_location_data( + SC2Mission.THE_HOST.mission_name, + "South Void Shard", + SC2LOTV_LOC_ID_OFFSET + 2102, + LocationType.EXTRA, + logic.protoss_the_host_requirement, + ), + make_location_data( + SC2Mission.THE_HOST.mission_name, + "Southwest Void Shard", + SC2LOTV_LOC_ID_OFFSET + 2103, + LocationType.EXTRA, + logic.protoss_the_host_requirement, + ), + make_location_data( + SC2Mission.THE_HOST.mission_name, + "North Void Shard", + SC2LOTV_LOC_ID_OFFSET + 2104, + LocationType.EXTRA, + logic.protoss_the_host_requirement, + ), + make_location_data( + SC2Mission.THE_HOST.mission_name, + "Northwest Void Shard", + SC2LOTV_LOC_ID_OFFSET + 2105, + LocationType.EXTRA, + logic.protoss_the_host_requirement, + ), + make_location_data( + SC2Mission.THE_HOST.mission_name, + "Nerazim Warp in Zone", + SC2LOTV_LOC_ID_OFFSET + 2106, + LocationType.VANILLA, + logic.protoss_the_host_requirement, + ), + make_location_data( + SC2Mission.THE_HOST.mission_name, + "Tal'darim Warp in Zone", + SC2LOTV_LOC_ID_OFFSET + 2107, + LocationType.VANILLA, + logic.protoss_the_host_requirement, + ), + make_location_data( + SC2Mission.THE_HOST.mission_name, + "Purifier Warp in Zone", + SC2LOTV_LOC_ID_OFFSET + 2108, + LocationType.VANILLA, + logic.protoss_the_host_requirement, + ), + make_location_data( + SC2Mission.SALVATION.mission_name, + "Victory", + SC2LOTV_LOC_ID_OFFSET + 2200, + LocationType.VICTORY, + logic.protoss_salvation_requirement, + ), + make_location_data( + SC2Mission.SALVATION.mission_name, + "Fabrication Matrix", + SC2LOTV_LOC_ID_OFFSET + 2201, + LocationType.EXTRA, + logic.protoss_salvation_requirement, + ), + make_location_data( + SC2Mission.SALVATION.mission_name, + "Assault Cluster", + SC2LOTV_LOC_ID_OFFSET + 2202, + LocationType.EXTRA, + logic.protoss_salvation_requirement, + ), + make_location_data( + SC2Mission.SALVATION.mission_name, + "Hull Breach", + SC2LOTV_LOC_ID_OFFSET + 2203, + LocationType.EXTRA, + logic.protoss_salvation_requirement, + ), + make_location_data( + SC2Mission.SALVATION.mission_name, + "Core Critical", + SC2LOTV_LOC_ID_OFFSET + 2204, + LocationType.EXTRA, + logic.protoss_salvation_requirement, + ), + make_location_data( + SC2Mission.SALVATION.mission_name, + "Kill Brutalisk", + SC2LOTV_LOC_ID_OFFSET + 2205, + LocationType.MASTERY, + logic.protoss_salvation_requirement, + ), + # Epilogue + make_location_data( + SC2Mission.INTO_THE_VOID.mission_name, + "Victory", + SC2LOTV_LOC_ID_OFFSET + 2300, + LocationType.VICTORY, + logic.into_the_void_requirement, + ), + make_location_data( + SC2Mission.INTO_THE_VOID.mission_name, + "Corruption Source", + SC2LOTV_LOC_ID_OFFSET + 2301, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.INTO_THE_VOID.mission_name, + "Southwest Forward Position", + SC2LOTV_LOC_ID_OFFSET + 2302, + LocationType.VANILLA, + logic.into_the_void_requirement, + ), + make_location_data( + SC2Mission.INTO_THE_VOID.mission_name, + "Northwest Forward Position", + SC2LOTV_LOC_ID_OFFSET + 2303, + LocationType.VANILLA, + logic.into_the_void_requirement, + ), + make_location_data( + SC2Mission.INTO_THE_VOID.mission_name, + "Southeast Forward Position", + SC2LOTV_LOC_ID_OFFSET + 2304, + LocationType.VANILLA, + logic.into_the_void_requirement, + ), + make_location_data( + SC2Mission.INTO_THE_VOID.mission_name, + "Northeast Forward Position", + SC2LOTV_LOC_ID_OFFSET + 2305, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.THE_ESSENCE_OF_ETERNITY.mission_name, + "Victory", + SC2LOTV_LOC_ID_OFFSET + 2400, + LocationType.VICTORY, + logic.essence_of_eternity_requirement, + ), + make_location_data( + SC2Mission.THE_ESSENCE_OF_ETERNITY.mission_name, + "Initial Void Thrashers", + SC2LOTV_LOC_ID_OFFSET + 2401, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.THE_ESSENCE_OF_ETERNITY.mission_name, + "Void Thrasher Wave 1", + SC2LOTV_LOC_ID_OFFSET + 2402, + LocationType.EXTRA, + logic.essence_of_eternity_requirement, + ), + make_location_data( + SC2Mission.THE_ESSENCE_OF_ETERNITY.mission_name, + "Void Thrasher Wave 2", + SC2LOTV_LOC_ID_OFFSET + 2403, + LocationType.EXTRA, + logic.essence_of_eternity_requirement, + ), + make_location_data( + SC2Mission.THE_ESSENCE_OF_ETERNITY.mission_name, + "Void Thrasher Wave 3", + SC2LOTV_LOC_ID_OFFSET + 2404, + LocationType.EXTRA, + logic.essence_of_eternity_requirement, + ), + make_location_data( + SC2Mission.THE_ESSENCE_OF_ETERNITY.mission_name, + "Void Thrasher Wave 4", + SC2LOTV_LOC_ID_OFFSET + 2405, + LocationType.EXTRA, + logic.essence_of_eternity_requirement, + ), + make_location_data( + SC2Mission.THE_ESSENCE_OF_ETERNITY.mission_name, + "No more than 15 Kerrigan Kills", + SC2LOTV_LOC_ID_OFFSET + 2406, + LocationType.MASTERY, + logic.essence_of_eternity_requirement, + flags=LocationFlag.PREVENTATIVE, + ), + make_location_data( + SC2Mission.AMON_S_FALL.mission_name, + "Victory", + SC2LOTV_LOC_ID_OFFSET + 2500, + LocationType.VICTORY, + logic.amons_fall_requirement, + ), + make_location_data( + SC2Mission.AMON_S_FALL.mission_name, + "Destroy 1 Crystal", + SC2LOTV_LOC_ID_OFFSET + 2501, + LocationType.EXTRA, + logic.amons_fall_requirement, + ), + make_location_data( + SC2Mission.AMON_S_FALL.mission_name, + "Destroy 2 Crystals", + SC2LOTV_LOC_ID_OFFSET + 2502, + LocationType.EXTRA, + logic.amons_fall_requirement, + ), + make_location_data( + SC2Mission.AMON_S_FALL.mission_name, + "Destroy 3 Crystals", + SC2LOTV_LOC_ID_OFFSET + 2503, + LocationType.EXTRA, + logic.amons_fall_requirement, + ), + make_location_data( + SC2Mission.AMON_S_FALL.mission_name, + "Destroy 4 Crystals", + SC2LOTV_LOC_ID_OFFSET + 2504, + LocationType.EXTRA, + logic.amons_fall_requirement, + ), + make_location_data( + SC2Mission.AMON_S_FALL.mission_name, + "Destroy 5 Crystals", + SC2LOTV_LOC_ID_OFFSET + 2505, + LocationType.EXTRA, + logic.amons_fall_requirement, + ), + make_location_data( + SC2Mission.AMON_S_FALL.mission_name, + "Destroy 6 Crystals", + SC2LOTV_LOC_ID_OFFSET + 2506, + LocationType.EXTRA, + logic.amons_fall_requirement, + ), + make_location_data( + SC2Mission.AMON_S_FALL.mission_name, + "Clear Void Chasms", + SC2LOTV_LOC_ID_OFFSET + 2507, + LocationType.MASTERY, + lambda state: logic.amons_fall_requirement(state) + and logic.spread_creep(state, False) + and logic.zerg_big_monsters(state), + ), + # Nova Covert Ops + make_location_data( + SC2Mission.THE_ESCAPE.mission_name, + "Victory", + SC2NCO_LOC_ID_OFFSET + 100, + LocationType.VICTORY, + logic.the_escape_requirement, + hard_rule=logic.nova_any_nobuild_damage, + ), + make_location_data( + SC2Mission.THE_ESCAPE.mission_name, + "Rifle", + SC2NCO_LOC_ID_OFFSET + 101, + LocationType.VANILLA, + logic.the_escape_first_stage_requirement, + ), + make_location_data( + SC2Mission.THE_ESCAPE.mission_name, + "Grenades", + SC2NCO_LOC_ID_OFFSET + 102, + LocationType.VANILLA, + logic.the_escape_first_stage_requirement, + hard_rule=logic.nova_any_nobuild_damage, + ), + make_location_data( + SC2Mission.THE_ESCAPE.mission_name, + "Agent Delta", + SC2NCO_LOC_ID_OFFSET + 103, + LocationType.VANILLA, + logic.the_escape_requirement, + hard_rule=logic.nova_any_nobuild_damage, + ), + make_location_data( + SC2Mission.THE_ESCAPE.mission_name, + "Agent Pierce", + SC2NCO_LOC_ID_OFFSET + 104, + LocationType.VANILLA, + logic.the_escape_requirement, + hard_rule=logic.nova_any_nobuild_damage, + ), + make_location_data( + SC2Mission.THE_ESCAPE.mission_name, + "Agent Stone", + SC2NCO_LOC_ID_OFFSET + 105, + LocationType.VANILLA, + logic.the_escape_requirement, + hard_rule=logic.nova_any_nobuild_damage, + ), + make_location_data( + SC2Mission.SUDDEN_STRIKE.mission_name, + "Victory", + SC2NCO_LOC_ID_OFFSET + 200, + LocationType.VICTORY, + logic.sudden_strike_requirement, + ), + make_location_data( + SC2Mission.SUDDEN_STRIKE.mission_name, + "Research Center", + SC2NCO_LOC_ID_OFFSET + 201, + LocationType.VANILLA, + logic.sudden_strike_requirement, + ), + make_location_data( + SC2Mission.SUDDEN_STRIKE.mission_name, + "Weaponry Labs", + SC2NCO_LOC_ID_OFFSET + 202, + LocationType.VANILLA, + logic.sudden_strike_requirement, + ), + make_location_data( + SC2Mission.SUDDEN_STRIKE.mission_name, + "Brutalisk", + SC2NCO_LOC_ID_OFFSET + 203, + LocationType.EXTRA, + logic.sudden_strike_requirement, + ), + make_location_data( + SC2Mission.SUDDEN_STRIKE.mission_name, + "Gas Pickups", + SC2NCO_LOC_ID_OFFSET + 204, + LocationType.EXTRA, + lambda state: ( + logic.advanced_tactics or logic.sudden_strike_requirement(state) + ), + ), + make_location_data( + SC2Mission.SUDDEN_STRIKE.mission_name, + "Protect Buildings", + SC2NCO_LOC_ID_OFFSET + 205, + LocationType.CHALLENGE, + logic.sudden_strike_requirement, + flags=LocationFlag.PREVENTATIVE, + ), + make_location_data( + SC2Mission.SUDDEN_STRIKE.mission_name, + "Zerg Base", + SC2NCO_LOC_ID_OFFSET + 206, + LocationType.MASTERY, + lambda state: ( + logic.sudden_strike_requirement(state) + and logic.terran_competent_comp(state) + and logic.terran_base_trasher(state) + and logic.terran_power_rating(state) >= 8 + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.ENEMY_INTELLIGENCE.mission_name, + "Victory", + SC2NCO_LOC_ID_OFFSET + 300, + LocationType.VICTORY, + logic.enemy_intelligence_third_stage_requirement, + hard_rule=logic.enemy_intelligence_cliff_garrison_and_nova_mobility, + ), + make_location_data( + SC2Mission.ENEMY_INTELLIGENCE.mission_name, + "West Garrison", + SC2NCO_LOC_ID_OFFSET + 301, + LocationType.EXTRA, + logic.enemy_intelligence_first_stage_requirement, + hard_rule=logic.enemy_intelligence_garrisonable_unit, + ), + make_location_data( + SC2Mission.ENEMY_INTELLIGENCE.mission_name, + "Close Garrison", + SC2NCO_LOC_ID_OFFSET + 302, + LocationType.EXTRA, + logic.enemy_intelligence_first_stage_requirement, + hard_rule=logic.enemy_intelligence_garrisonable_unit, + ), + make_location_data( + SC2Mission.ENEMY_INTELLIGENCE.mission_name, + "Northeast Garrison", + SC2NCO_LOC_ID_OFFSET + 303, + LocationType.EXTRA, + logic.enemy_intelligence_first_stage_requirement, + hard_rule=logic.enemy_intelligence_garrisonable_unit, + ), + make_location_data( + SC2Mission.ENEMY_INTELLIGENCE.mission_name, + "Southeast Garrison", + SC2NCO_LOC_ID_OFFSET + 304, + LocationType.EXTRA, + lambda state: ( + logic.enemy_intelligence_first_stage_requirement(state) + and logic.enemy_intelligence_cliff_garrison(state) + ), + hard_rule=logic.enemy_intelligence_cliff_garrison, + ), + make_location_data( + SC2Mission.ENEMY_INTELLIGENCE.mission_name, + "South Garrison", + SC2NCO_LOC_ID_OFFSET + 305, + LocationType.EXTRA, + logic.enemy_intelligence_first_stage_requirement, + hard_rule=logic.enemy_intelligence_garrisonable_unit, + ), + make_location_data( + SC2Mission.ENEMY_INTELLIGENCE.mission_name, + "All Garrisons", + SC2NCO_LOC_ID_OFFSET + 306, + LocationType.VANILLA, + lambda state: ( + logic.enemy_intelligence_first_stage_requirement(state) + and logic.enemy_intelligence_cliff_garrison(state) + ), + hard_rule=logic.enemy_intelligence_cliff_garrison, + ), + make_location_data( + SC2Mission.ENEMY_INTELLIGENCE.mission_name, + "Forces Rescued", + SC2NCO_LOC_ID_OFFSET + 307, + LocationType.VANILLA, + logic.enemy_intelligence_first_stage_requirement, + ), + make_location_data( + SC2Mission.ENEMY_INTELLIGENCE.mission_name, + "Communications Hub", + SC2NCO_LOC_ID_OFFSET + 308, + LocationType.VANILLA, + logic.enemy_intelligence_second_stage_requirement, + hard_rule=logic.enemy_intelligence_cliff_garrison_and_nova_mobility, + ), + make_location_data( + SC2Mission.TROUBLE_IN_PARADISE.mission_name, + "Victory", + SC2NCO_LOC_ID_OFFSET + 400, + LocationType.VICTORY, + logic.trouble_in_paradise_requirement, + ), + make_location_data( + SC2Mission.TROUBLE_IN_PARADISE.mission_name, + "North Base: West Hatchery", + SC2NCO_LOC_ID_OFFSET + 401, + LocationType.VANILLA, + logic.trouble_in_paradise_requirement, + ), + make_location_data( + SC2Mission.TROUBLE_IN_PARADISE.mission_name, + "North Base: North Hatchery", + SC2NCO_LOC_ID_OFFSET + 402, + LocationType.VANILLA, + logic.trouble_in_paradise_requirement, + ), + make_location_data( + SC2Mission.TROUBLE_IN_PARADISE.mission_name, + "North Base: East Hatchery", + SC2NCO_LOC_ID_OFFSET + 403, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.TROUBLE_IN_PARADISE.mission_name, + "South Base: Northwest Hatchery", + SC2NCO_LOC_ID_OFFSET + 404, + LocationType.VANILLA, + logic.trouble_in_paradise_requirement, + ), + make_location_data( + SC2Mission.TROUBLE_IN_PARADISE.mission_name, + "South Base: Southwest Hatchery", + SC2NCO_LOC_ID_OFFSET + 405, + LocationType.VANILLA, + logic.trouble_in_paradise_requirement, + ), + make_location_data( + SC2Mission.TROUBLE_IN_PARADISE.mission_name, + "South Base: East Hatchery", + SC2NCO_LOC_ID_OFFSET + 406, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.TROUBLE_IN_PARADISE.mission_name, + "North Shield Projector", + SC2NCO_LOC_ID_OFFSET + 407, + LocationType.EXTRA, + logic.trouble_in_paradise_requirement, + ), + make_location_data( + SC2Mission.TROUBLE_IN_PARADISE.mission_name, + "East Shield Projector", + SC2NCO_LOC_ID_OFFSET + 408, + LocationType.EXTRA, + logic.trouble_in_paradise_requirement, + ), + make_location_data( + SC2Mission.TROUBLE_IN_PARADISE.mission_name, + "South Shield Projector", + SC2NCO_LOC_ID_OFFSET + 409, + LocationType.EXTRA, + logic.trouble_in_paradise_requirement, + ), + make_location_data( + SC2Mission.TROUBLE_IN_PARADISE.mission_name, + "West Shield Projector", + SC2NCO_LOC_ID_OFFSET + 410, + LocationType.EXTRA, + logic.trouble_in_paradise_requirement, + ), + make_location_data( + SC2Mission.TROUBLE_IN_PARADISE.mission_name, + "Fleet Beacon", + SC2NCO_LOC_ID_OFFSET + 411, + LocationType.VANILLA, + logic.trouble_in_paradise_requirement, + ), + make_location_data( + SC2Mission.NIGHT_TERRORS.mission_name, + "Victory", + SC2NCO_LOC_ID_OFFSET + 500, + LocationType.VICTORY, + logic.night_terrors_requirement, + ), + make_location_data( + SC2Mission.NIGHT_TERRORS.mission_name, + "1 Terrazine Node Collected", + SC2NCO_LOC_ID_OFFSET + 501, + LocationType.EXTRA, + logic.night_terrors_requirement, + ), + make_location_data( + SC2Mission.NIGHT_TERRORS.mission_name, + "2 Terrazine Nodes Collected", + SC2NCO_LOC_ID_OFFSET + 502, + LocationType.EXTRA, + logic.night_terrors_requirement, + ), + make_location_data( + SC2Mission.NIGHT_TERRORS.mission_name, + "3 Terrazine Nodes Collected", + SC2NCO_LOC_ID_OFFSET + 503, + LocationType.EXTRA, + logic.night_terrors_requirement, + ), + make_location_data( + SC2Mission.NIGHT_TERRORS.mission_name, + "4 Terrazine Nodes Collected", + SC2NCO_LOC_ID_OFFSET + 504, + LocationType.EXTRA, + logic.night_terrors_requirement, + ), + make_location_data( + SC2Mission.NIGHT_TERRORS.mission_name, + "5 Terrazine Nodes Collected", + SC2NCO_LOC_ID_OFFSET + 505, + LocationType.EXTRA, + logic.night_terrors_requirement, + ), + make_location_data( + SC2Mission.NIGHT_TERRORS.mission_name, + "HERC Outpost", + SC2NCO_LOC_ID_OFFSET + 506, + LocationType.VANILLA, + logic.night_terrors_requirement, + ), + make_location_data( + SC2Mission.NIGHT_TERRORS.mission_name, + "Umojan Mine", + SC2NCO_LOC_ID_OFFSET + 507, + LocationType.EXTRA, + logic.night_terrors_requirement, + ), + make_location_data( + SC2Mission.NIGHT_TERRORS.mission_name, + "Blightbringer", + SC2NCO_LOC_ID_OFFSET + 508, + LocationType.VANILLA, + lambda state: ( + logic.night_terrors_requirement(state) + and logic.nova_ranged_weapon(state) + and state.has_any( + { + item_names.NOVA_HELLFIRE_SHOTGUN, + item_names.NOVA_PULSE_GRENADES, + item_names.NOVA_STIM_INFUSION, + item_names.NOVA_HOLO_DECOY, + }, + player, + ) + ), + ), + make_location_data( + SC2Mission.NIGHT_TERRORS.mission_name, + "Science Facility", + SC2NCO_LOC_ID_OFFSET + 509, + LocationType.EXTRA, + logic.night_terrors_requirement, + ), + make_location_data( + SC2Mission.NIGHT_TERRORS.mission_name, + "Eradicators", + SC2NCO_LOC_ID_OFFSET + 510, + LocationType.VANILLA, + lambda state: ( + logic.night_terrors_requirement(state) and logic.nova_any_weapon(state) + ), + ), + make_location_data( + SC2Mission.FLASHPOINT.mission_name, + "Victory", + SC2NCO_LOC_ID_OFFSET + 600, + LocationType.VICTORY, + logic.flashpoint_far_requirement, + ), + make_location_data( + SC2Mission.FLASHPOINT.mission_name, + "Close North Evidence Coordinates", + SC2NCO_LOC_ID_OFFSET + 601, + LocationType.EXTRA, + lambda state: ( + state.has_any( + { + item_names.LIBERATOR_RAID_ARTILLERY, + item_names.RAVEN_HUNTER_SEEKER_WEAPON, + }, + player, + ) + or logic.terran_common_unit(state) + ), + ), + make_location_data( + SC2Mission.FLASHPOINT.mission_name, + "Close East Evidence Coordinates", + SC2NCO_LOC_ID_OFFSET + 602, + LocationType.EXTRA, + lambda state: ( + state.has_any( + { + item_names.LIBERATOR_RAID_ARTILLERY, + item_names.RAVEN_HUNTER_SEEKER_WEAPON, + }, + player, + ) + or logic.terran_common_unit(state) + ), + ), + make_location_data( + SC2Mission.FLASHPOINT.mission_name, + "Far North Evidence Coordinates", + SC2NCO_LOC_ID_OFFSET + 603, + LocationType.EXTRA, + logic.flashpoint_far_requirement, + ), + make_location_data( + SC2Mission.FLASHPOINT.mission_name, + "Far East Evidence Coordinates", + SC2NCO_LOC_ID_OFFSET + 604, + LocationType.EXTRA, + logic.flashpoint_far_requirement, + ), + make_location_data( + SC2Mission.FLASHPOINT.mission_name, + "Experimental Weapon", + SC2NCO_LOC_ID_OFFSET + 605, + LocationType.VANILLA, + logic.flashpoint_far_requirement, + ), + make_location_data( + SC2Mission.FLASHPOINT.mission_name, + "Northwest Subway Entrance", + SC2NCO_LOC_ID_OFFSET + 606, + LocationType.VANILLA, + lambda state: ( + state.has_any( + { + item_names.LIBERATOR_RAID_ARTILLERY, + item_names.RAVEN_HUNTER_SEEKER_WEAPON, + }, + player, + ) + and logic.terran_common_unit(state) + or logic.flashpoint_far_requirement(state) + ), + ), + make_location_data( + SC2Mission.FLASHPOINT.mission_name, + "Southeast Subway Entrance", + SC2NCO_LOC_ID_OFFSET + 607, + LocationType.VANILLA, + lambda state: state.has_any( + { + item_names.LIBERATOR_RAID_ARTILLERY, + item_names.RAVEN_HUNTER_SEEKER_WEAPON, + }, + player, + ) + and logic.terran_common_unit(state) + or logic.flashpoint_far_requirement(state), + ), + make_location_data( + SC2Mission.FLASHPOINT.mission_name, + "Northeast Subway Entrance", + SC2NCO_LOC_ID_OFFSET + 608, + LocationType.VANILLA, + logic.flashpoint_far_requirement, + ), + make_location_data( + SC2Mission.FLASHPOINT.mission_name, + "Expansion Hatchery", + SC2NCO_LOC_ID_OFFSET + 609, + LocationType.EXTRA, + lambda state: state.has(item_names.LIBERATOR_RAID_ARTILLERY, player) + and logic.terran_common_unit(state) + or logic.flashpoint_far_requirement(state), + ), + make_location_data( + SC2Mission.FLASHPOINT.mission_name, + "Baneling Spawns", + SC2NCO_LOC_ID_OFFSET + 610, + LocationType.EXTRA, + logic.flashpoint_far_requirement, + ), + make_location_data( + SC2Mission.FLASHPOINT.mission_name, + "Mutalisk Spawns", + SC2NCO_LOC_ID_OFFSET + 611, + LocationType.EXTRA, + logic.flashpoint_far_requirement, + ), + make_location_data( + SC2Mission.FLASHPOINT.mission_name, + "Nydus Worm Spawns", + SC2NCO_LOC_ID_OFFSET + 612, + LocationType.EXTRA, + logic.flashpoint_far_requirement, + ), + make_location_data( + SC2Mission.FLASHPOINT.mission_name, + "Lurker Spawns", + SC2NCO_LOC_ID_OFFSET + 613, + LocationType.EXTRA, + logic.flashpoint_far_requirement, + ), + make_location_data( + SC2Mission.FLASHPOINT.mission_name, + "Brood Lord Spawns", + SC2NCO_LOC_ID_OFFSET + 614, + LocationType.EXTRA, + logic.flashpoint_far_requirement, + ), + make_location_data( + SC2Mission.FLASHPOINT.mission_name, + "Ultralisk Spawns", + SC2NCO_LOC_ID_OFFSET + 615, + LocationType.EXTRA, + logic.flashpoint_far_requirement, + ), + make_location_data( + SC2Mission.IN_THE_ENEMY_S_SHADOW.mission_name, + "Victory", + SC2NCO_LOC_ID_OFFSET + 700, + LocationType.VICTORY, + logic.enemy_shadow_victory, + hard_rule=lambda state: logic.nova_beat_stone(state) + and logic.enemy_shadow_door_unlocks_tool(state), + ), + make_location_data( + SC2Mission.IN_THE_ENEMY_S_SHADOW.mission_name, + "Sewers: Domination Visor", + SC2NCO_LOC_ID_OFFSET + 701, + LocationType.VANILLA, + logic.enemy_shadow_domination, + hard_rule=logic.nova_any_nobuild_damage, + ), + make_location_data( + SC2Mission.IN_THE_ENEMY_S_SHADOW.mission_name, + "Sewers: Resupply Crate", + SC2NCO_LOC_ID_OFFSET + 702, + LocationType.EXTRA, + logic.enemy_shadow_first_stage, + hard_rule=logic.nova_any_nobuild_damage, + ), + make_location_data( + SC2Mission.IN_THE_ENEMY_S_SHADOW.mission_name, + "Sewers: Facility Access", + SC2NCO_LOC_ID_OFFSET + 703, + LocationType.VANILLA, + logic.enemy_shadow_first_stage, + hard_rule=logic.nova_any_nobuild_damage, + ), + make_location_data( + SC2Mission.IN_THE_ENEMY_S_SHADOW.mission_name, + "Facility: Northwest Door Lock", + SC2NCO_LOC_ID_OFFSET + 704, + LocationType.VANILLA, + logic.enemy_shadow_door_controls, + hard_rule=lambda state: logic.nova_any_nobuild_damage(state) + and logic.enemy_shadow_door_unlocks_tool(state), + ), + make_location_data( + SC2Mission.IN_THE_ENEMY_S_SHADOW.mission_name, + "Facility: Southeast Door Lock", + SC2NCO_LOC_ID_OFFSET + 705, + LocationType.VANILLA, + logic.enemy_shadow_door_controls, + hard_rule=lambda state: logic.nova_any_nobuild_damage(state) + and logic.enemy_shadow_door_unlocks_tool(state), + ), + make_location_data( + SC2Mission.IN_THE_ENEMY_S_SHADOW.mission_name, + "Facility: Blazefire Gunblade", + SC2NCO_LOC_ID_OFFSET + 706, + LocationType.VANILLA, + lambda state: ( + logic.enemy_shadow_second_stage(state) + and ( + logic.grant_story_tech == GrantStoryTech.option_grant + or state.has(item_names.NOVA_BLINK, player) + or ( + adv_tactics + and state.has_all( + { + item_names.NOVA_DOMINATION, + item_names.NOVA_HOLO_DECOY, + item_names.NOVA_JUMP_SUIT_MODULE, + }, + player, + ) + ) + ) + ), + hard_rule=logic.enemy_shadow_nova_damage_and_blazefire_unlock, + ), + make_location_data( + SC2Mission.IN_THE_ENEMY_S_SHADOW.mission_name, + "Facility: Blink Suit", + SC2NCO_LOC_ID_OFFSET + 707, + LocationType.VANILLA, + logic.enemy_shadow_second_stage, + hard_rule=logic.nova_any_nobuild_damage, + ), + make_location_data( + SC2Mission.IN_THE_ENEMY_S_SHADOW.mission_name, + "Facility: Advanced Weaponry", + SC2NCO_LOC_ID_OFFSET + 708, + LocationType.VANILLA, + logic.enemy_shadow_second_stage, + hard_rule=logic.nova_any_nobuild_damage, + ), + make_location_data( + SC2Mission.IN_THE_ENEMY_S_SHADOW.mission_name, + "Facility: Entrance Resupply Crate", + SC2NCO_LOC_ID_OFFSET + 709, + LocationType.EXTRA, + logic.enemy_shadow_first_stage, + hard_rule=logic.nova_any_nobuild_damage, + ), + make_location_data( + SC2Mission.IN_THE_ENEMY_S_SHADOW.mission_name, + "Facility: West Resupply Crate", + SC2NCO_LOC_ID_OFFSET + 710, + LocationType.EXTRA, + logic.enemy_shadow_second_stage, + hard_rule=logic.nova_any_nobuild_damage, + ), + make_location_data( + SC2Mission.IN_THE_ENEMY_S_SHADOW.mission_name, + "Facility: North Resupply Crate", + SC2NCO_LOC_ID_OFFSET + 711, + LocationType.EXTRA, + logic.enemy_shadow_second_stage, + hard_rule=logic.nova_any_nobuild_damage, + ), + make_location_data( + SC2Mission.IN_THE_ENEMY_S_SHADOW.mission_name, + "Facility: East Resupply Crate", + SC2NCO_LOC_ID_OFFSET + 712, + LocationType.EXTRA, + logic.enemy_shadow_second_stage, + hard_rule=logic.nova_any_nobuild_damage, + ), + make_location_data( + SC2Mission.IN_THE_ENEMY_S_SHADOW.mission_name, + "Facility: South Resupply Crate", + SC2NCO_LOC_ID_OFFSET + 713, + LocationType.EXTRA, + logic.enemy_shadow_second_stage, + hard_rule=logic.nova_any_nobuild_damage, + ), + make_location_data( + SC2Mission.DARK_SKIES.mission_name, + "Victory", + SC2NCO_LOC_ID_OFFSET + 800, + LocationType.VICTORY, + logic.dark_skies_requirement, + ), + make_location_data( + SC2Mission.DARK_SKIES.mission_name, + "First Squadron of Dominion Fleet", + SC2NCO_LOC_ID_OFFSET + 801, + LocationType.EXTRA, + logic.dark_skies_requirement, + ), + make_location_data( + SC2Mission.DARK_SKIES.mission_name, + "Remainder of Dominion Fleet", + SC2NCO_LOC_ID_OFFSET + 802, + LocationType.EXTRA, + logic.dark_skies_requirement, + ), + make_location_data( + SC2Mission.DARK_SKIES.mission_name, + "Ji'nara", + SC2NCO_LOC_ID_OFFSET + 803, + LocationType.EXTRA, + logic.dark_skies_requirement, + ), + make_location_data( + SC2Mission.DARK_SKIES.mission_name, + "Science Facility", + SC2NCO_LOC_ID_OFFSET + 804, + LocationType.VANILLA, + logic.dark_skies_requirement, + ), + make_location_data( + SC2Mission.END_GAME.mission_name, + "Victory", + SC2NCO_LOC_ID_OFFSET + 900, + LocationType.VICTORY, + lambda state: logic.end_game_requirement(state) + and logic.nova_any_weapon(state), + ), + make_location_data( + SC2Mission.END_GAME.mission_name, + "Destroy the Xanthos", + SC2NCO_LOC_ID_OFFSET + 901, + LocationType.VANILLA, + logic.end_game_requirement, + ), + make_location_data( + SC2Mission.END_GAME.mission_name, + "Disable Xanthos Railgun", + SC2NCO_LOC_ID_OFFSET + 902, + LocationType.EXTRA, + logic.end_game_requirement, + ), + make_location_data( + SC2Mission.END_GAME.mission_name, + "Disable Xanthos Flamethrower", + SC2NCO_LOC_ID_OFFSET + 903, + LocationType.EXTRA, + logic.end_game_requirement, + ), + make_location_data( + SC2Mission.END_GAME.mission_name, + "Disable Xanthos Fighter Bay", + SC2NCO_LOC_ID_OFFSET + 904, + LocationType.EXTRA, + logic.end_game_requirement, + ), + make_location_data( + SC2Mission.END_GAME.mission_name, + "Disable Xanthos Missile Pods", + SC2NCO_LOC_ID_OFFSET + 905, + LocationType.EXTRA, + logic.end_game_requirement, + ), + make_location_data( + SC2Mission.END_GAME.mission_name, + "Protect Hyperion", + SC2NCO_LOC_ID_OFFSET + 906, + LocationType.CHALLENGE, + logic.end_game_requirement, + ), + make_location_data( + SC2Mission.END_GAME.mission_name, + "Destroy Orbital Commands", + SC2NCO_LOC_ID_OFFSET + 907, + LocationType.CHALLENGE, + logic.end_game_requirement, + flags=LocationFlag.BASEBUST, + ), + # Mission Variants + # 10X/20X - Liberation Day + make_location_data( + SC2Mission.THE_OUTLAWS_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 300, + LocationType.VICTORY, + logic.zerg_common_unit, + ), + make_location_data( + SC2Mission.THE_OUTLAWS_Z.mission_name, + "Rebel Base", + SC2_RACESWAP_LOC_ID_OFFSET + 301, + LocationType.VANILLA, + logic.zerg_common_unit, + ), + make_location_data( + SC2Mission.THE_OUTLAWS_Z.mission_name, + "North Resource Pickups", + SC2_RACESWAP_LOC_ID_OFFSET + 302, + LocationType.EXTRA, + logic.zerg_common_unit, + ), + make_location_data( + SC2Mission.THE_OUTLAWS_Z.mission_name, + "Bunker", + SC2_RACESWAP_LOC_ID_OFFSET + 303, + LocationType.VANILLA, + logic.zerg_common_unit, + ), + make_location_data( + SC2Mission.THE_OUTLAWS_Z.mission_name, + "Close Resource Pickups", + SC2_RACESWAP_LOC_ID_OFFSET + 304, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.THE_OUTLAWS_P.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 400, + LocationType.VICTORY, + logic.protoss_common_unit, + ), + make_location_data( + SC2Mission.THE_OUTLAWS_P.mission_name, + "Rebel Base", + SC2_RACESWAP_LOC_ID_OFFSET + 401, + LocationType.VANILLA, + logic.protoss_common_unit, + ), + make_location_data( + SC2Mission.THE_OUTLAWS_P.mission_name, + "North Resource Pickups", + SC2_RACESWAP_LOC_ID_OFFSET + 402, + LocationType.EXTRA, + logic.protoss_common_unit, + ), + make_location_data( + SC2Mission.THE_OUTLAWS_P.mission_name, + "Bunker", + SC2_RACESWAP_LOC_ID_OFFSET + 403, + LocationType.VANILLA, + logic.protoss_common_unit, + ), + make_location_data( + SC2Mission.THE_OUTLAWS_P.mission_name, + "Close Resource Pickups", + SC2_RACESWAP_LOC_ID_OFFSET + 404, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.ZERO_HOUR_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 500, + LocationType.VICTORY, + lambda state: ( + logic.zerg_common_unit(state) + and logic.zerg_defense_rating(state, True, True) >= 5 + and logic.zerg_basic_kerriganless_anti_air(state) + ), + ), + make_location_data( + SC2Mission.ZERO_HOUR_Z.mission_name, + "First Group Rescued", + SC2_RACESWAP_LOC_ID_OFFSET + 501, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.ZERO_HOUR_Z.mission_name, + "Second Group Rescued", + SC2_RACESWAP_LOC_ID_OFFSET + 502, + LocationType.VANILLA, + logic.zerg_common_unit, + ), + make_location_data( + SC2Mission.ZERO_HOUR_Z.mission_name, + "Third Group Rescued", + SC2_RACESWAP_LOC_ID_OFFSET + 503, + LocationType.VANILLA, + lambda state: ( + logic.zerg_common_unit(state) + and logic.zerg_defense_rating(state, True, True) >= 5 + and logic.zerg_basic_kerriganless_anti_air(state) + ), + ), + make_location_data( + SC2Mission.ZERO_HOUR_Z.mission_name, + "First Hatchery", + SC2_RACESWAP_LOC_ID_OFFSET + 504, + LocationType.CHALLENGE, + logic.zerg_competent_comp_competent_aa, + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.ZERO_HOUR_Z.mission_name, + "Second Hatchery", + SC2_RACESWAP_LOC_ID_OFFSET + 505, + LocationType.CHALLENGE, + logic.zerg_competent_comp_competent_aa, + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.ZERO_HOUR_Z.mission_name, + "Third Hatchery", + SC2_RACESWAP_LOC_ID_OFFSET + 506, + LocationType.CHALLENGE, + logic.zerg_competent_comp_competent_aa, + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.ZERO_HOUR_Z.mission_name, + "Fourth Hatchery", + SC2_RACESWAP_LOC_ID_OFFSET + 507, + LocationType.CHALLENGE, + logic.zerg_competent_comp_competent_aa, + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.ZERO_HOUR_Z.mission_name, + "Ride's on its Way", + SC2_RACESWAP_LOC_ID_OFFSET + 508, + LocationType.EXTRA, + lambda state: ( + logic.zerg_common_unit(state) + and logic.zerg_defense_rating(state, True, True) >= 5 + and logic.zerg_basic_kerriganless_anti_air(state) + ), + ), + make_location_data( + SC2Mission.ZERO_HOUR_Z.mission_name, + "Hold Just a Little Longer", + SC2_RACESWAP_LOC_ID_OFFSET + 509, + LocationType.EXTRA, + lambda state: ( + logic.zerg_common_unit(state) + and logic.zerg_defense_rating(state, True, True) >= 5 + and logic.zerg_basic_kerriganless_anti_air(state) + ), + ), + make_location_data( + SC2Mission.ZERO_HOUR_Z.mission_name, + "Cavalry's on the Way", + SC2_RACESWAP_LOC_ID_OFFSET + 510, + LocationType.EXTRA, + lambda state: ( + logic.zerg_common_unit(state) + and logic.zerg_defense_rating(state, True, True) >= 5 + and logic.zerg_basic_kerriganless_anti_air(state) + ), + ), + make_location_data( + SC2Mission.ZERO_HOUR_P.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 600, + LocationType.VICTORY, + lambda state: ( + logic.protoss_common_unit(state) + and logic.protoss_anti_light_anti_air(state) + and ( + state.has(item_names.PHOTON_CANNON, player) + or logic.protoss_basic_splash(state) + ) + ), + ), + make_location_data( + SC2Mission.ZERO_HOUR_P.mission_name, + "First Group Rescued", + SC2_RACESWAP_LOC_ID_OFFSET + 601, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.ZERO_HOUR_P.mission_name, + "Second Group Rescued", + SC2_RACESWAP_LOC_ID_OFFSET + 602, + LocationType.VANILLA, + logic.protoss_common_unit, + ), + make_location_data( + SC2Mission.ZERO_HOUR_P.mission_name, + "Third Group Rescued", + SC2_RACESWAP_LOC_ID_OFFSET + 603, + LocationType.VANILLA, + lambda state: ( + logic.protoss_common_unit(state) + and logic.protoss_anti_light_anti_air(state) + and ( + state.has(item_names.PHOTON_CANNON, player) + or logic.protoss_basic_splash(state) + ) + ), + ), + make_location_data( + SC2Mission.ZERO_HOUR_P.mission_name, + "First Hatchery", + SC2_RACESWAP_LOC_ID_OFFSET + 604, + LocationType.CHALLENGE, + logic.protoss_competent_comp, + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.ZERO_HOUR_P.mission_name, + "Second Hatchery", + SC2_RACESWAP_LOC_ID_OFFSET + 605, + LocationType.CHALLENGE, + logic.protoss_competent_comp, + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.ZERO_HOUR_P.mission_name, + "Third Hatchery", + SC2_RACESWAP_LOC_ID_OFFSET + 606, + LocationType.CHALLENGE, + logic.protoss_competent_comp, + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.ZERO_HOUR_P.mission_name, + "Fourth Hatchery", + SC2_RACESWAP_LOC_ID_OFFSET + 607, + LocationType.CHALLENGE, + logic.protoss_competent_comp, + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.ZERO_HOUR_P.mission_name, + "Ride's on its Way", + SC2_RACESWAP_LOC_ID_OFFSET + 608, + LocationType.EXTRA, + lambda state: ( + logic.protoss_common_unit(state) + and logic.protoss_anti_light_anti_air(state) + and ( + state.has(item_names.PHOTON_CANNON, player) + or logic.protoss_basic_splash(state) + ) + ), + ), + make_location_data( + SC2Mission.ZERO_HOUR_P.mission_name, + "Hold Just a Little Longer", + SC2_RACESWAP_LOC_ID_OFFSET + 609, + LocationType.EXTRA, + lambda state: ( + logic.protoss_common_unit(state) + and logic.protoss_anti_light_anti_air(state) + and ( + state.has(item_names.PHOTON_CANNON, player) + or logic.protoss_basic_splash(state) + ) + ), + ), + make_location_data( + SC2Mission.ZERO_HOUR_P.mission_name, + "Cavalry's on the Way", + SC2_RACESWAP_LOC_ID_OFFSET + 610, + LocationType.EXTRA, + lambda state: ( + logic.protoss_common_unit(state) + and logic.protoss_anti_light_anti_air(state) + and ( + state.has(item_names.PHOTON_CANNON, player) + or logic.protoss_basic_splash(state) + ) + ), + ), + make_location_data( + SC2Mission.EVACUATION_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 700, + LocationType.VICTORY, + lambda state: ( + logic.zerg_common_unit(state) + and ( + logic.zerg_competent_anti_air(state) + or (adv_tactics and logic.zerg_basic_kerriganless_anti_air(state)) + ) + ), + ), + make_location_data( + SC2Mission.EVACUATION_Z.mission_name, + "North Chrysalis", + SC2_RACESWAP_LOC_ID_OFFSET + 701, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.EVACUATION_Z.mission_name, + "West Chrysalis", + SC2_RACESWAP_LOC_ID_OFFSET + 702, + LocationType.VANILLA, + logic.zerg_common_unit, + ), + make_location_data( + SC2Mission.EVACUATION_Z.mission_name, + "East Chrysalis", + SC2_RACESWAP_LOC_ID_OFFSET + 703, + LocationType.VANILLA, + logic.zerg_common_unit, + ), + make_location_data( + SC2Mission.EVACUATION_Z.mission_name, + "Reach Hanson", + SC2_RACESWAP_LOC_ID_OFFSET + 704, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.EVACUATION_Z.mission_name, + "Secret Resource Stash", + SC2_RACESWAP_LOC_ID_OFFSET + 705, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.EVACUATION_Z.mission_name, + "Flawless", + SC2_RACESWAP_LOC_ID_OFFSET + 706, + LocationType.CHALLENGE, + lambda state: ( + logic.zerg_common_unit(state) + and logic.zerg_defense_rating(state, True, False) >= 5 + and ( + (adv_tactics and logic.zerg_basic_kerriganless_anti_air(state)) + or logic.zerg_competent_anti_air(state) + ) + ), + flags=LocationFlag.PREVENTATIVE, + ), + make_location_data( + SC2Mission.EVACUATION_Z.mission_name, + "Western Zerg Base", + SC2_RACESWAP_LOC_ID_OFFSET + 707, + LocationType.MASTERY, + lambda state: ( + logic.zerg_common_unit_competent_aa(state) + and logic.zerg_base_buster(state) + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.EVACUATION_Z.mission_name, + "Eastern Zerg Base", + SC2_RACESWAP_LOC_ID_OFFSET + 708, + LocationType.MASTERY, + lambda state: ( + logic.zerg_common_unit_competent_aa(state) + and logic.zerg_base_buster(state) + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.EVACUATION_P.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 800, + LocationType.VICTORY, + lambda state: ( + logic.protoss_common_unit(state) + and ( + (adv_tactics and logic.protoss_basic_anti_air(state)) + or logic.protoss_anti_light_anti_air(state) + ) + ), + ), + make_location_data( + SC2Mission.EVACUATION_P.mission_name, + "North Chrysalis", + SC2_RACESWAP_LOC_ID_OFFSET + 801, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.EVACUATION_P.mission_name, + "West Chrysalis", + SC2_RACESWAP_LOC_ID_OFFSET + 802, + LocationType.VANILLA, + logic.protoss_common_unit, + ), + make_location_data( + SC2Mission.EVACUATION_P.mission_name, + "East Chrysalis", + SC2_RACESWAP_LOC_ID_OFFSET + 803, + LocationType.VANILLA, + logic.protoss_common_unit, + ), + make_location_data( + SC2Mission.EVACUATION_P.mission_name, + "Reach Hanson", + SC2_RACESWAP_LOC_ID_OFFSET + 804, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.EVACUATION_P.mission_name, + "Secret Resource Stash", + SC2_RACESWAP_LOC_ID_OFFSET + 805, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.EVACUATION_P.mission_name, + "Flawless", + SC2_RACESWAP_LOC_ID_OFFSET + 806, + LocationType.CHALLENGE, + lambda state: ( + logic.protoss_defense_rating(state, True) >= 2 + and logic.protoss_common_unit(state) + and ( + (adv_tactics and logic.protoss_basic_anti_air(state)) + or logic.protoss_anti_light_anti_air(state) + ) + ), + flags=LocationFlag.PREVENTATIVE, + ), + make_location_data( + SC2Mission.EVACUATION_P.mission_name, + "Western Zerg Base", + SC2_RACESWAP_LOC_ID_OFFSET + 807, + LocationType.MASTERY, + logic.protoss_competent_comp, + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.EVACUATION_P.mission_name, + "Eastern Zerg Base", + SC2_RACESWAP_LOC_ID_OFFSET + 808, + LocationType.MASTERY, + logic.protoss_competent_comp, + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.OUTBREAK_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 900, + LocationType.VICTORY, + logic.zerg_outbreak_requirement, + ), + make_location_data( + SC2Mission.OUTBREAK_Z.mission_name, + "Left Infestor", + SC2_RACESWAP_LOC_ID_OFFSET + 901, + LocationType.VANILLA, + logic.zerg_outbreak_requirement, + ), + make_location_data( + SC2Mission.OUTBREAK_Z.mission_name, + "Right Infestor", + SC2_RACESWAP_LOC_ID_OFFSET + 902, + LocationType.VANILLA, + logic.zerg_outbreak_requirement, + ), + make_location_data( + SC2Mission.OUTBREAK_Z.mission_name, + "North Infested Command Center", + SC2_RACESWAP_LOC_ID_OFFSET + 903, + LocationType.EXTRA, + logic.zerg_outbreak_requirement, + ), + make_location_data( + SC2Mission.OUTBREAK_Z.mission_name, + "South Infested Command Center", + SC2_RACESWAP_LOC_ID_OFFSET + 904, + LocationType.EXTRA, + logic.zerg_outbreak_requirement, + ), + make_location_data( + SC2Mission.OUTBREAK_Z.mission_name, + "Northwest Bar", + SC2_RACESWAP_LOC_ID_OFFSET + 905, + LocationType.EXTRA, + logic.zerg_outbreak_requirement, + ), + make_location_data( + SC2Mission.OUTBREAK_Z.mission_name, + "North Bar", + SC2_RACESWAP_LOC_ID_OFFSET + 906, + LocationType.EXTRA, + logic.zerg_outbreak_requirement, + ), + make_location_data( + SC2Mission.OUTBREAK_Z.mission_name, + "South Bar", + SC2_RACESWAP_LOC_ID_OFFSET + 907, + LocationType.EXTRA, + logic.zerg_outbreak_requirement, + ), + make_location_data( + SC2Mission.OUTBREAK_P.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 1000, + LocationType.VICTORY, + logic.protoss_outbreak_requirement, + ), + make_location_data( + SC2Mission.OUTBREAK_P.mission_name, + "Left Infestor", + SC2_RACESWAP_LOC_ID_OFFSET + 1001, + LocationType.VANILLA, + logic.protoss_outbreak_requirement, + ), + make_location_data( + SC2Mission.OUTBREAK_P.mission_name, + "Right Infestor", + SC2_RACESWAP_LOC_ID_OFFSET + 1002, + LocationType.VANILLA, + logic.protoss_outbreak_requirement, + ), + make_location_data( + SC2Mission.OUTBREAK_P.mission_name, + "North Infested Command Center", + SC2_RACESWAP_LOC_ID_OFFSET + 1003, + LocationType.EXTRA, + logic.protoss_outbreak_requirement, + ), + make_location_data( + SC2Mission.OUTBREAK_P.mission_name, + "South Infested Command Center", + SC2_RACESWAP_LOC_ID_OFFSET + 1004, + LocationType.EXTRA, + logic.protoss_outbreak_requirement, + ), + make_location_data( + SC2Mission.OUTBREAK_P.mission_name, + "Northwest Bar", + SC2_RACESWAP_LOC_ID_OFFSET + 1005, + LocationType.EXTRA, + logic.protoss_outbreak_requirement, + ), + make_location_data( + SC2Mission.OUTBREAK_P.mission_name, + "North Bar", + SC2_RACESWAP_LOC_ID_OFFSET + 1006, + LocationType.EXTRA, + logic.protoss_outbreak_requirement, + ), + make_location_data( + SC2Mission.OUTBREAK_P.mission_name, + "South Bar", + SC2_RACESWAP_LOC_ID_OFFSET + 1007, + LocationType.EXTRA, + logic.protoss_outbreak_requirement, + ), + make_location_data( + SC2Mission.SAFE_HAVEN_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 1100, + LocationType.VICTORY, + lambda state: logic.zerg_common_unit(state) + and logic.zerg_competent_anti_air(state), + hard_rule=logic.zerg_any_anti_air, + ), + make_location_data( + SC2Mission.SAFE_HAVEN_Z.mission_name, + "North Nexus", + SC2_RACESWAP_LOC_ID_OFFSET + 1101, + LocationType.EXTRA, + lambda state: logic.zerg_common_unit(state) + and logic.zerg_competent_anti_air(state), + ), + make_location_data( + SC2Mission.SAFE_HAVEN_Z.mission_name, + "East Nexus", + SC2_RACESWAP_LOC_ID_OFFSET + 1102, + LocationType.EXTRA, + lambda state: logic.zerg_common_unit(state) + and logic.zerg_competent_anti_air(state), + ), + make_location_data( + SC2Mission.SAFE_HAVEN_Z.mission_name, + "South Nexus", + SC2_RACESWAP_LOC_ID_OFFSET + 1103, + LocationType.EXTRA, + lambda state: logic.zerg_common_unit(state) + and logic.zerg_competent_anti_air(state), + ), + make_location_data( + SC2Mission.SAFE_HAVEN_Z.mission_name, + "First Terror Fleet", + SC2_RACESWAP_LOC_ID_OFFSET + 1104, + LocationType.VANILLA, + lambda state: logic.zerg_common_unit(state) + and logic.zerg_competent_anti_air(state), + hard_rule=logic.zerg_any_anti_air, + ), + make_location_data( + SC2Mission.SAFE_HAVEN_Z.mission_name, + "Second Terror Fleet", + SC2_RACESWAP_LOC_ID_OFFSET + 1105, + LocationType.VANILLA, + lambda state: logic.zerg_common_unit(state) + and logic.zerg_competent_anti_air(state), + hard_rule=logic.zerg_any_anti_air, + ), + make_location_data( + SC2Mission.SAFE_HAVEN_Z.mission_name, + "Third Terror Fleet", + SC2_RACESWAP_LOC_ID_OFFSET + 1106, + LocationType.VANILLA, + lambda state: logic.zerg_common_unit(state) + and logic.zerg_competent_anti_air(state), + hard_rule=logic.zerg_any_anti_air, + ), + make_location_data( + SC2Mission.SAFE_HAVEN_P.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 1200, + LocationType.VICTORY, + lambda state: logic.protoss_common_unit(state) + and logic.protoss_anti_armor_anti_air(state), + hard_rule=logic.protoss_any_anti_air_unit_or_soa_any_protoss, + ), + make_location_data( + SC2Mission.SAFE_HAVEN_P.mission_name, + "North Nexus", + SC2_RACESWAP_LOC_ID_OFFSET + 1201, + LocationType.EXTRA, + lambda state: logic.protoss_common_unit(state) + and logic.protoss_anti_armor_anti_air(state), + ), + make_location_data( + SC2Mission.SAFE_HAVEN_P.mission_name, + "East Nexus", + SC2_RACESWAP_LOC_ID_OFFSET + 1202, + LocationType.EXTRA, + lambda state: logic.protoss_common_unit(state) + and logic.protoss_anti_armor_anti_air(state), + ), + make_location_data( + SC2Mission.SAFE_HAVEN_P.mission_name, + "South Nexus", + SC2_RACESWAP_LOC_ID_OFFSET + 1203, + LocationType.EXTRA, + lambda state: logic.protoss_common_unit(state) + and logic.protoss_anti_armor_anti_air(state), + ), + make_location_data( + SC2Mission.SAFE_HAVEN_P.mission_name, + "First Terror Fleet", + SC2_RACESWAP_LOC_ID_OFFSET + 1204, + LocationType.VANILLA, + lambda state: logic.protoss_common_unit(state) + and logic.protoss_anti_armor_anti_air(state), + hard_rule=logic.protoss_any_anti_air_unit_or_soa_any_protoss, + ), + make_location_data( + SC2Mission.SAFE_HAVEN_P.mission_name, + "Second Terror Fleet", + SC2_RACESWAP_LOC_ID_OFFSET + 1205, + LocationType.VANILLA, + lambda state: logic.protoss_common_unit(state) + and logic.protoss_anti_armor_anti_air(state), + hard_rule=logic.protoss_any_anti_air_unit_or_soa_any_protoss, + ), + make_location_data( + SC2Mission.SAFE_HAVEN_P.mission_name, + "Third Terror Fleet", + SC2_RACESWAP_LOC_ID_OFFSET + 1206, + LocationType.VANILLA, + lambda state: logic.protoss_common_unit(state) + and logic.protoss_anti_armor_anti_air(state), + hard_rule=logic.protoss_any_anti_air_unit_or_soa_any_protoss, + ), + make_location_data( + SC2Mission.HAVENS_FALL_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 1300, + LocationType.VICTORY, + logic.zerg_havens_fall_requirement, + hard_rule=logic.zerg_any_anti_air, + ), + make_location_data( + SC2Mission.HAVENS_FALL_Z.mission_name, + "North Hive", + SC2_RACESWAP_LOC_ID_OFFSET + 1301, + LocationType.VANILLA, + lambda state: ( + logic.zerg_common_unit(state) and logic.zerg_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.HAVENS_FALL_Z.mission_name, + "East Hive", + SC2_RACESWAP_LOC_ID_OFFSET + 1302, + LocationType.VANILLA, + logic.zerg_havens_fall_requirement, + ), + make_location_data( + SC2Mission.HAVENS_FALL_Z.mission_name, + "South Hive", + SC2_RACESWAP_LOC_ID_OFFSET + 1303, + LocationType.VANILLA, + logic.zerg_havens_fall_requirement, + ), + make_location_data( + SC2Mission.HAVENS_FALL_Z.mission_name, + "Northeast Colony Base", + SC2_RACESWAP_LOC_ID_OFFSET + 1304, + LocationType.CHALLENGE, + logic.zerg_havens_fall_requirement, + ), + make_location_data( + SC2Mission.HAVENS_FALL_Z.mission_name, + "East Colony Base", + SC2_RACESWAP_LOC_ID_OFFSET + 1305, + LocationType.CHALLENGE, + logic.zerg_respond_to_colony_infestations, + hard_rule=logic.zerg_any_anti_air, + ), + make_location_data( + SC2Mission.HAVENS_FALL_Z.mission_name, + "Middle Colony Base", + SC2_RACESWAP_LOC_ID_OFFSET + 1306, + LocationType.CHALLENGE, + logic.zerg_respond_to_colony_infestations, + hard_rule=logic.zerg_any_anti_air, + ), + make_location_data( + SC2Mission.HAVENS_FALL_Z.mission_name, + "Southeast Colony Base", + SC2_RACESWAP_LOC_ID_OFFSET + 1307, + LocationType.CHALLENGE, + logic.zerg_respond_to_colony_infestations, + hard_rule=logic.zerg_any_anti_air, + ), + make_location_data( + SC2Mission.HAVENS_FALL_Z.mission_name, + "Southwest Colony Base", + SC2_RACESWAP_LOC_ID_OFFSET + 1308, + LocationType.CHALLENGE, + logic.zerg_respond_to_colony_infestations, + hard_rule=logic.zerg_any_anti_air, + ), + make_location_data( + SC2Mission.HAVENS_FALL_Z.mission_name, + "Southwest Gas Pickups", + SC2_RACESWAP_LOC_ID_OFFSET + 1309, + LocationType.EXTRA, + lambda state: state.has_any( + (item_names.OVERLORD_VENTRAL_SACS, item_names.YGGDRASIL), player + ) + or adv_tactics + and state.has_all( + ( + item_names.INFESTED_BANSHEE, + item_names.INFESTED_BANSHEE_RAPID_HIBERNATION, + ), + player, + ), + hard_rule=logic.zerg_can_collect_pickup_across_gap, + ), + make_location_data( + SC2Mission.HAVENS_FALL_Z.mission_name, + "East Gas Pickups", + SC2_RACESWAP_LOC_ID_OFFSET + 1310, + LocationType.EXTRA, + lambda state: ( + logic.zerg_havens_fall_requirement(state) + and ( + state.has(item_names.OVERLORD_VENTRAL_SACS, player) + or adv_tactics + and ( + state.has_all( + ( + item_names.INFESTED_BANSHEE, + item_names.INFESTED_BANSHEE_RAPID_HIBERNATION, + ), + player, + ) + or state.has(item_names.YGGDRASIL, player) + or logic.morph_viper(state) + ) + ) + ), + ), + make_location_data( + SC2Mission.HAVENS_FALL_Z.mission_name, + "Southeast Gas Pickups", + SC2_RACESWAP_LOC_ID_OFFSET + 1311, + LocationType.EXTRA, + lambda state: ( + logic.zerg_havens_fall_requirement(state) + and ( + state.has(item_names.OVERLORD_VENTRAL_SACS, player) + or adv_tactics + and ( + state.has_all( + ( + item_names.INFESTED_BANSHEE, + item_names.INFESTED_BANSHEE_RAPID_HIBERNATION, + ), + player, + ) + or state.has(item_names.YGGDRASIL, player) + ) + ) + ), + ), + make_location_data( + SC2Mission.HAVENS_FALL_P.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 1400, + LocationType.VICTORY, + logic.protoss_havens_fall_requirement, + hard_rule=logic.protoss_any_anti_air_unit_or_soa_any_protoss, + ), + make_location_data( + SC2Mission.HAVENS_FALL_P.mission_name, + "North Hive", + SC2_RACESWAP_LOC_ID_OFFSET + 1401, + LocationType.VANILLA, + lambda state: ( + logic.protoss_common_unit(state) + and logic.protoss_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.HAVENS_FALL_P.mission_name, + "East Hive", + SC2_RACESWAP_LOC_ID_OFFSET + 1402, + LocationType.VANILLA, + logic.protoss_havens_fall_requirement, + ), + make_location_data( + SC2Mission.HAVENS_FALL_P.mission_name, + "South Hive", + SC2_RACESWAP_LOC_ID_OFFSET + 1403, + LocationType.VANILLA, + logic.protoss_havens_fall_requirement, + ), + make_location_data( + SC2Mission.HAVENS_FALL_P.mission_name, + "Northeast Colony Base", + SC2_RACESWAP_LOC_ID_OFFSET + 1404, + LocationType.CHALLENGE, + logic.protoss_respond_to_colony_infestations, + hard_rule=logic.protoss_any_anti_air_unit_or_soa_any_protoss, + ), + make_location_data( + SC2Mission.HAVENS_FALL_P.mission_name, + "East Colony Base", + SC2_RACESWAP_LOC_ID_OFFSET + 1405, + LocationType.CHALLENGE, + logic.protoss_respond_to_colony_infestations, + hard_rule=logic.protoss_any_anti_air_unit_or_soa_any_protoss, + ), + make_location_data( + SC2Mission.HAVENS_FALL_P.mission_name, + "Middle Colony Base", + SC2_RACESWAP_LOC_ID_OFFSET + 1406, + LocationType.CHALLENGE, + logic.protoss_respond_to_colony_infestations, + hard_rule=logic.protoss_any_anti_air_unit_or_soa_any_protoss, + ), + make_location_data( + SC2Mission.HAVENS_FALL_P.mission_name, + "Southeast Colony Base", + SC2_RACESWAP_LOC_ID_OFFSET + 1407, + LocationType.CHALLENGE, + logic.protoss_respond_to_colony_infestations, + hard_rule=logic.protoss_any_anti_air_unit_or_soa_any_protoss, + ), + make_location_data( + SC2Mission.HAVENS_FALL_P.mission_name, + "Southwest Colony Base", + SC2_RACESWAP_LOC_ID_OFFSET + 1408, + LocationType.CHALLENGE, + logic.protoss_respond_to_colony_infestations, + hard_rule=logic.protoss_any_anti_air_unit_or_soa_any_protoss, + ), + make_location_data( + SC2Mission.HAVENS_FALL_P.mission_name, + "Southwest Gas Pickups", + SC2_RACESWAP_LOC_ID_OFFSET + 1409, + LocationType.EXTRA, + lambda state: ( + state.has(item_names.WARP_PRISM, player) + or adv_tactics + and ( + state.has_all( + (item_names.MISTWING, item_names.MISTWING_PILOT), player + ) + or state.has(item_names.ARBITER, player) + ) + ), + hard_rule=logic.protoss_any_gap_transport, + ), + make_location_data( + SC2Mission.HAVENS_FALL_P.mission_name, + "East Gas Pickups", + SC2_RACESWAP_LOC_ID_OFFSET + 1410, + LocationType.EXTRA, + lambda state: ( + logic.protoss_havens_fall_requirement(state) + and ( + state.has(item_names.WARP_PRISM, player) + or adv_tactics + and ( + state.has_all( + (item_names.MISTWING, item_names.MISTWING_PILOT), player + ) + or state.has(item_names.ARBITER, player) + ) + ) + ), + hard_rule=logic.protoss_any_gap_transport, + ), + make_location_data( + SC2Mission.HAVENS_FALL_P.mission_name, + "Southeast Gas Pickups", + SC2_RACESWAP_LOC_ID_OFFSET + 1411, + LocationType.EXTRA, + lambda state: ( + logic.protoss_havens_fall_requirement(state) + and ( + state.has(item_names.WARP_PRISM, player) + or adv_tactics + and ( + state.has_all( + (item_names.MISTWING, item_names.MISTWING_PILOT), player + ) + or state.has(item_names.ARBITER, player) + ) + ) + ), + hard_rule=logic.protoss_any_gap_transport, + ), + make_location_data( + SC2Mission.SMASH_AND_GRAB_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 1500, + LocationType.VICTORY, + lambda state: ( + logic.zerg_common_unit(state) + and ( + (adv_tactics and logic.zerg_moderate_anti_air(state)) + or logic.zerg_competent_anti_air(state) + ) + ), + ), + make_location_data( + SC2Mission.SMASH_AND_GRAB_Z.mission_name, + "First Relic", + SC2_RACESWAP_LOC_ID_OFFSET + 1501, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.SMASH_AND_GRAB_Z.mission_name, + "Second Relic", + SC2_RACESWAP_LOC_ID_OFFSET + 1502, + LocationType.VANILLA, + lambda state: logic.zerg_common_unit(state) + or state.has(item_names.OVERLORD_VENTRAL_SACS, player), + ), + make_location_data( + SC2Mission.SMASH_AND_GRAB_Z.mission_name, + "Third Relic", + SC2_RACESWAP_LOC_ID_OFFSET + 1503, + LocationType.VANILLA, + lambda state: ( + logic.zerg_common_unit(state) + and ( + (adv_tactics and logic.zerg_moderate_anti_air(state)) + or logic.zerg_competent_anti_air(state) + ) + ), + ), + make_location_data( + SC2Mission.SMASH_AND_GRAB_Z.mission_name, + "Fourth Relic", + SC2_RACESWAP_LOC_ID_OFFSET + 1504, + LocationType.VANILLA, + lambda state: ( + logic.zerg_common_unit(state) + and ( + (adv_tactics and logic.zerg_moderate_anti_air(state)) + or logic.zerg_competent_anti_air(state) + ) + ), + ), + make_location_data( + SC2Mission.SMASH_AND_GRAB_Z.mission_name, + "First Forcefield Area Busted", + SC2_RACESWAP_LOC_ID_OFFSET + 1505, + LocationType.EXTRA, + lambda state: ( + logic.zerg_common_unit(state) + and ( + (adv_tactics and logic.zerg_moderate_anti_air(state)) + or logic.zerg_competent_anti_air(state) + ) + ), + ), + make_location_data( + SC2Mission.SMASH_AND_GRAB_Z.mission_name, + "Second Forcefield Area Busted", + SC2_RACESWAP_LOC_ID_OFFSET + 1506, + LocationType.EXTRA, + lambda state: ( + logic.zerg_common_unit(state) + and ( + (adv_tactics and logic.zerg_moderate_anti_air(state)) + or logic.zerg_competent_anti_air(state) + ) + ), + ), + make_location_data( + SC2Mission.SMASH_AND_GRAB_Z.mission_name, + "Defeat Kerrigan", + SC2_RACESWAP_LOC_ID_OFFSET + 1507, + LocationType.MASTERY, + lambda state: ( + logic.zerg_common_unit_competent_aa(state) + and logic.zerg_base_buster(state) + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.SMASH_AND_GRAB_P.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 1600, + LocationType.VICTORY, + lambda state: ( + logic.protoss_common_unit(state) + and ( + (adv_tactics and logic.protoss_basic_anti_air(state)) + or logic.protoss_anti_light_anti_air(state) + ) + ), + ), + make_location_data( + SC2Mission.SMASH_AND_GRAB_P.mission_name, + "First Relic", + SC2_RACESWAP_LOC_ID_OFFSET + 1601, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.SMASH_AND_GRAB_P.mission_name, + "Second Relic", + SC2_RACESWAP_LOC_ID_OFFSET + 1602, + LocationType.VANILLA, + lambda state: adv_tactics or logic.protoss_common_unit(state), + ), + make_location_data( + SC2Mission.SMASH_AND_GRAB_P.mission_name, + "Third Relic", + SC2_RACESWAP_LOC_ID_OFFSET + 1603, + LocationType.VANILLA, + lambda state: ( + logic.protoss_common_unit(state) + and ( + (adv_tactics and logic.protoss_basic_anti_air(state)) + or logic.protoss_anti_light_anti_air(state) + ) + ), + ), + make_location_data( + SC2Mission.SMASH_AND_GRAB_P.mission_name, + "Fourth Relic", + SC2_RACESWAP_LOC_ID_OFFSET + 1604, + LocationType.VANILLA, + lambda state: ( + logic.protoss_common_unit(state) + and ( + (adv_tactics and logic.protoss_basic_anti_air(state)) + or logic.protoss_anti_light_anti_air(state) + ) + ), + ), + make_location_data( + SC2Mission.SMASH_AND_GRAB_P.mission_name, + "First Forcefield Area Busted", + SC2_RACESWAP_LOC_ID_OFFSET + 1605, + LocationType.EXTRA, + lambda state: ( + logic.protoss_common_unit(state) + and ( + (adv_tactics and logic.protoss_basic_anti_air(state)) + or logic.protoss_anti_light_anti_air(state) + ) + ), + ), + make_location_data( + SC2Mission.SMASH_AND_GRAB_P.mission_name, + "Second Forcefield Area Busted", + SC2_RACESWAP_LOC_ID_OFFSET + 1606, + LocationType.EXTRA, + lambda state: ( + logic.protoss_common_unit(state) + and ( + (adv_tactics and logic.protoss_basic_anti_air(state)) + or logic.protoss_anti_light_anti_air(state) + ) + ), + ), + make_location_data( + SC2Mission.SMASH_AND_GRAB_P.mission_name, + "Defeat Kerrigan", + SC2_RACESWAP_LOC_ID_OFFSET + 1607, + LocationType.MASTERY, + logic.protoss_deathball, + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.THE_DIG_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 1700, + LocationType.VICTORY, + lambda state: ( + ( + logic.zerg_competent_anti_air(state) + or adv_tactics + and logic.zerg_moderate_anti_air(state) + ) + and logic.zerg_defense_rating(state, False, True) >= 8 + and logic.zerg_common_unit(state) + ), + ), + make_location_data( + SC2Mission.THE_DIG_Z.mission_name, + "Left Relic", + SC2_RACESWAP_LOC_ID_OFFSET + 1701, + LocationType.VANILLA, + lambda state: ( + logic.zerg_defense_rating(state, False, False) >= 6 + and logic.zerg_common_unit(state) + ), + ), + make_location_data( + SC2Mission.THE_DIG_Z.mission_name, + "Right Ground Relic", + SC2_RACESWAP_LOC_ID_OFFSET + 1702, + LocationType.VANILLA, + lambda state: ( + logic.zerg_defense_rating(state, False, False) >= 6 + and logic.zerg_common_unit(state) + ), + ), + make_location_data( + SC2Mission.THE_DIG_Z.mission_name, + "Right Cliff Relic", + SC2_RACESWAP_LOC_ID_OFFSET + 1703, + LocationType.VANILLA, + lambda state: ( + logic.zerg_defense_rating(state, False, False) >= 6 + and logic.zerg_common_unit(state) + ), + ), + make_location_data( + SC2Mission.THE_DIG_Z.mission_name, + "Moebius Base", + SC2_RACESWAP_LOC_ID_OFFSET + 1704, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.THE_DIG_Z.mission_name, + "Door Outer Layer", + SC2_RACESWAP_LOC_ID_OFFSET + 1705, + LocationType.EXTRA, + lambda state: ( + logic.zerg_defense_rating(state, False, False) >= 6 + and logic.zerg_common_unit(state) + ), + ), + make_location_data( + SC2Mission.THE_DIG_Z.mission_name, + "Door Thermal Barrier", + SC2_RACESWAP_LOC_ID_OFFSET + 1706, + LocationType.EXTRA, + lambda state: ( + ( + logic.zerg_competent_anti_air(state) + or adv_tactics + and logic.zerg_moderate_anti_air(state) + ) + and logic.zerg_defense_rating(state, False, True) >= 8 + and logic.zerg_common_unit(state) + ), + ), + make_location_data( + SC2Mission.THE_DIG_Z.mission_name, + "Cutting Through the Core", + SC2_RACESWAP_LOC_ID_OFFSET + 1707, + LocationType.EXTRA, + lambda state: ( + ( + logic.zerg_competent_anti_air(state) + or adv_tactics + and logic.zerg_moderate_anti_air(state) + ) + and logic.zerg_defense_rating(state, False, True) >= 8 + and logic.zerg_common_unit(state) + ), + ), + make_location_data( + SC2Mission.THE_DIG_Z.mission_name, + "Structure Access Imminent", + SC2_RACESWAP_LOC_ID_OFFSET + 1708, + LocationType.EXTRA, + lambda state: ( + ( + logic.zerg_competent_anti_air(state) + or adv_tactics + and logic.zerg_moderate_anti_air(state) + ) + and logic.zerg_defense_rating(state, False, True) >= 8 + and logic.zerg_common_unit(state) + ), + ), + make_location_data( + SC2Mission.THE_DIG_Z.mission_name, + "Northwestern Protoss Base", + SC2_RACESWAP_LOC_ID_OFFSET + 1709, + LocationType.MASTERY, + lambda state: ( + logic.zerg_competent_anti_air(state) + and logic.zerg_defense_rating(state, False, True) >= 8 + and logic.zerg_common_unit(state) + and logic.zerg_base_buster(state) + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.THE_DIG_Z.mission_name, + "Northeastern Protoss Base", + SC2_RACESWAP_LOC_ID_OFFSET + 1710, + LocationType.MASTERY, + lambda state: ( + logic.zerg_competent_anti_air(state) + and logic.zerg_defense_rating(state, False, True) >= 8 + and logic.zerg_common_unit(state) + and logic.zerg_base_buster(state) + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.THE_DIG_Z.mission_name, + "Eastern Protoss Base", + SC2_RACESWAP_LOC_ID_OFFSET + 1711, + LocationType.MASTERY, + lambda state: ( + logic.zerg_competent_anti_air(state) + and logic.zerg_defense_rating(state, False, True) >= 8 + and logic.zerg_common_unit(state) + and logic.zerg_base_buster(state) + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.THE_DIG_P.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 1800, + LocationType.VICTORY, + lambda state: ( + ( + logic.protoss_anti_armor_anti_air(state) + or adv_tactics + and logic.protoss_moderate_anti_air(state) + ) + and logic.protoss_defense_rating(state, False) >= 6 + and logic.protoss_common_unit(state) + ), + ), + make_location_data( + SC2Mission.THE_DIG_P.mission_name, + "Left Relic", + SC2_RACESWAP_LOC_ID_OFFSET + 1801, + LocationType.VANILLA, + lambda state: ( + logic.protoss_defense_rating(state, False) >= 6 + and logic.protoss_common_unit(state) + ), + ), + make_location_data( + SC2Mission.THE_DIG_P.mission_name, + "Right Ground Relic", + SC2_RACESWAP_LOC_ID_OFFSET + 1802, + LocationType.VANILLA, + lambda state: ( + logic.protoss_defense_rating(state, False) >= 6 + and logic.protoss_common_unit(state) + ), + ), + make_location_data( + SC2Mission.THE_DIG_P.mission_name, + "Right Cliff Relic", + SC2_RACESWAP_LOC_ID_OFFSET + 1803, + LocationType.VANILLA, + lambda state: ( + logic.protoss_defense_rating(state, False) >= 6 + and logic.protoss_common_unit(state) + ), + ), + make_location_data( + SC2Mission.THE_DIG_P.mission_name, + "Moebius Base", + SC2_RACESWAP_LOC_ID_OFFSET + 1804, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.THE_DIG_P.mission_name, + "Door Outer Layer", + SC2_RACESWAP_LOC_ID_OFFSET + 1805, + LocationType.EXTRA, + lambda state: ( + logic.protoss_defense_rating(state, False) >= 6 + and logic.protoss_common_unit(state) + ), + ), + make_location_data( + SC2Mission.THE_DIG_P.mission_name, + "Door Thermal Barrier", + SC2_RACESWAP_LOC_ID_OFFSET + 1806, + LocationType.EXTRA, + lambda state: ( + ( + logic.protoss_anti_armor_anti_air(state) + or adv_tactics + and logic.protoss_moderate_anti_air(state) + ) + and logic.protoss_defense_rating(state, False) >= 6 + and logic.protoss_common_unit(state) + ), + ), + make_location_data( + SC2Mission.THE_DIG_P.mission_name, + "Cutting Through the Core", + SC2_RACESWAP_LOC_ID_OFFSET + 1807, + LocationType.EXTRA, + lambda state: ( + ( + logic.protoss_anti_armor_anti_air(state) + or adv_tactics + and logic.protoss_moderate_anti_air(state) + ) + and logic.protoss_defense_rating(state, False) >= 6 + and logic.protoss_common_unit(state) + ), + ), + make_location_data( + SC2Mission.THE_DIG_P.mission_name, + "Structure Access Imminent", + SC2_RACESWAP_LOC_ID_OFFSET + 1808, + LocationType.EXTRA, + lambda state: ( + ( + logic.protoss_anti_armor_anti_air(state) + or adv_tactics + and logic.protoss_moderate_anti_air(state) + ) + and logic.protoss_defense_rating(state, False) >= 6 + and logic.protoss_common_unit(state) + ), + ), + make_location_data( + SC2Mission.THE_DIG_P.mission_name, + "Northwestern Protoss Base", + SC2_RACESWAP_LOC_ID_OFFSET + 1809, + LocationType.MASTERY, + lambda state: ( + logic.protoss_anti_armor_anti_air + and logic.protoss_defense_rating(state, False) >= 6 + and logic.protoss_common_unit(state) + and logic.protoss_deathball(state) + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.THE_DIG_P.mission_name, + "Northeastern Protoss Base", + SC2_RACESWAP_LOC_ID_OFFSET + 1810, + LocationType.MASTERY, + lambda state: ( + logic.protoss_anti_armor_anti_air(state) + and logic.protoss_defense_rating(state, False) >= 6 + and logic.protoss_common_unit(state) + and logic.protoss_deathball(state) + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.THE_DIG_P.mission_name, + "Eastern Protoss Base", + SC2_RACESWAP_LOC_ID_OFFSET + 1811, + LocationType.MASTERY, + lambda state: ( + logic.protoss_anti_armor_anti_air(state) + and logic.protoss_defense_rating(state, False) >= 6 + and logic.protoss_common_unit(state) + and logic.protoss_deathball(state) + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.THE_MOEBIUS_FACTOR_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 1900, + LocationType.VICTORY, + lambda state: ( + logic.zerg_moderate_anti_air(state) + and ( + logic.zerg_versatile_air(state) + or state.has_any( + { + item_names.YGGDRASIL, + item_names.OVERLORD_VENTRAL_SACS, + item_names.NYDUS_WORM, + item_names.BULLFROG, + }, + player, + ) + and logic.zerg_common_unit(state) + ) + ), + ), + make_location_data( + SC2Mission.THE_MOEBIUS_FACTOR_Z.mission_name, + "1st Data Core", + SC2_RACESWAP_LOC_ID_OFFSET + 1901, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.THE_MOEBIUS_FACTOR_Z.mission_name, + "2nd Data Core", + SC2_RACESWAP_LOC_ID_OFFSET + 1902, + LocationType.VANILLA, + lambda state: ( + logic.zerg_versatile_air(state) + or state.has_any( + { + item_names.YGGDRASIL, + item_names.OVERLORD_VENTRAL_SACS, + item_names.NYDUS_WORM, + item_names.BULLFROG, + }, + player, + ) + and logic.zerg_common_unit(state) + ), + ), + make_location_data( + SC2Mission.THE_MOEBIUS_FACTOR_Z.mission_name, + "South Rescue", + SC2_RACESWAP_LOC_ID_OFFSET + 1903, + LocationType.EXTRA, + lambda state: state.has_any( + { + item_names.YGGDRASIL, + item_names.OVERLORD_VENTRAL_SACS, + item_names.NYDUS_WORM, + item_names.BULLFROG, + }, + player, + ), + ), + make_location_data( + SC2Mission.THE_MOEBIUS_FACTOR_Z.mission_name, + "Wall Rescue", + SC2_RACESWAP_LOC_ID_OFFSET + 1904, + LocationType.EXTRA, + lambda state: state.has_any( + { + item_names.YGGDRASIL, + item_names.OVERLORD_VENTRAL_SACS, + item_names.NYDUS_WORM, + item_names.BULLFROG, + }, + player, + ), + ), + make_location_data( + SC2Mission.THE_MOEBIUS_FACTOR_Z.mission_name, + "Mid Rescue", + SC2_RACESWAP_LOC_ID_OFFSET + 1905, + LocationType.EXTRA, + lambda state: state.has_any( + { + item_names.YGGDRASIL, + item_names.OVERLORD_VENTRAL_SACS, + item_names.NYDUS_WORM, + item_names.BULLFROG, + }, + player, + ), + ), + make_location_data( + SC2Mission.THE_MOEBIUS_FACTOR_Z.mission_name, + "Nydus Roof Rescue", + SC2_RACESWAP_LOC_ID_OFFSET + 1906, + LocationType.EXTRA, + lambda state: state.has_any( + { + item_names.YGGDRASIL, + item_names.OVERLORD_VENTRAL_SACS, + item_names.NYDUS_WORM, + item_names.BULLFROG, + }, + player, + ), + ), + make_location_data( + SC2Mission.THE_MOEBIUS_FACTOR_Z.mission_name, + "Alive Inside Rescue", + SC2_RACESWAP_LOC_ID_OFFSET + 1907, + LocationType.EXTRA, + lambda state: state.has_any( + { + item_names.YGGDRASIL, + item_names.OVERLORD_VENTRAL_SACS, + item_names.NYDUS_WORM, + item_names.BULLFROG, + }, + player, + ), + ), + make_location_data( + SC2Mission.THE_MOEBIUS_FACTOR_Z.mission_name, + "Brutalisk", + SC2_RACESWAP_LOC_ID_OFFSET + 1908, + LocationType.VANILLA, + lambda state: ( + logic.zerg_moderate_anti_air(state) + and ( + logic.zerg_versatile_air(state) + or state.has_any( + { + item_names.YGGDRASIL, + item_names.OVERLORD_VENTRAL_SACS, + item_names.NYDUS_WORM, + item_names.BULLFROG, + }, + player, + ) + and logic.zerg_common_unit(state) + ) + ), + ), + make_location_data( + SC2Mission.THE_MOEBIUS_FACTOR_Z.mission_name, + "3rd Data Core", + SC2_RACESWAP_LOC_ID_OFFSET + 1909, + LocationType.VANILLA, + lambda state: ( + logic.zerg_moderate_anti_air(state) + and ( + logic.zerg_versatile_air(state) + or state.has_any( + { + item_names.YGGDRASIL, + item_names.OVERLORD_VENTRAL_SACS, + item_names.NYDUS_WORM, + item_names.BULLFROG, + }, + player, + ) + and logic.zerg_common_unit(state) + ) + ), + ), + make_location_data( + SC2Mission.THE_MOEBIUS_FACTOR_P.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 2000, + LocationType.VICTORY, + lambda state: ( + logic.protoss_moderate_anti_air(state) + and ( + logic.protoss_fleet(state) + or state.has(item_names.WARP_PRISM, player) + and logic.protoss_common_unit(state) + ) + ), + ), + make_location_data( + SC2Mission.THE_MOEBIUS_FACTOR_P.mission_name, + "1st Data Core", + SC2_RACESWAP_LOC_ID_OFFSET + 2001, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.THE_MOEBIUS_FACTOR_P.mission_name, + "2nd Data Core", + SC2_RACESWAP_LOC_ID_OFFSET + 2002, + LocationType.VANILLA, + lambda state: ( + logic.protoss_fleet(state) + or ( + state.has(item_names.WARP_PRISM, player) + and logic.protoss_common_unit(state) + ) + ), + ), + make_location_data( + SC2Mission.THE_MOEBIUS_FACTOR_P.mission_name, + "South Rescue", + SC2_RACESWAP_LOC_ID_OFFSET + 2003, + LocationType.EXTRA, + lambda state: adv_tactics or state.has(item_names.WARP_PRISM, player), + ), + make_location_data( + SC2Mission.THE_MOEBIUS_FACTOR_P.mission_name, + "Wall Rescue", + SC2_RACESWAP_LOC_ID_OFFSET + 2004, + LocationType.EXTRA, + lambda state: adv_tactics or state.has(item_names.WARP_PRISM, player), + ), + make_location_data( + SC2Mission.THE_MOEBIUS_FACTOR_P.mission_name, + "Mid Rescue", + SC2_RACESWAP_LOC_ID_OFFSET + 2005, + LocationType.EXTRA, + lambda state: adv_tactics or state.has(item_names.WARP_PRISM, player), + ), + make_location_data( + SC2Mission.THE_MOEBIUS_FACTOR_P.mission_name, + "Nydus Roof Rescue", + SC2_RACESWAP_LOC_ID_OFFSET + 2006, + LocationType.EXTRA, + lambda state: adv_tactics or state.has(item_names.WARP_PRISM, player), + ), + make_location_data( + SC2Mission.THE_MOEBIUS_FACTOR_P.mission_name, + "Alive Inside Rescue", + SC2_RACESWAP_LOC_ID_OFFSET + 2007, + LocationType.EXTRA, + lambda state: adv_tactics or state.has(item_names.WARP_PRISM, player), + ), + make_location_data( + SC2Mission.THE_MOEBIUS_FACTOR_P.mission_name, + "Brutalisk", + SC2_RACESWAP_LOC_ID_OFFSET + 2008, + LocationType.VANILLA, + lambda state: ( + logic.protoss_moderate_anti_air(state) + and ( + logic.protoss_fleet(state) + or state.has(item_names.WARP_PRISM, player) + and logic.protoss_common_unit(state) + ) + ), + ), + make_location_data( + SC2Mission.THE_MOEBIUS_FACTOR_P.mission_name, + "3rd Data Core", + SC2_RACESWAP_LOC_ID_OFFSET + 2009, + LocationType.VANILLA, + lambda state: ( + logic.protoss_moderate_anti_air(state) + and ( + logic.protoss_fleet(state) + or state.has(item_names.WARP_PRISM, player) + and logic.protoss_common_unit(state) + ) + ), + ), + make_location_data( + SC2Mission.SUPERNOVA_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 2100, + LocationType.VICTORY, + logic.zerg_supernova_requirement, + ), + make_location_data( + SC2Mission.SUPERNOVA_Z.mission_name, + "West Relic", + SC2_RACESWAP_LOC_ID_OFFSET + 2101, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.SUPERNOVA_Z.mission_name, + "North Relic", + SC2_RACESWAP_LOC_ID_OFFSET + 2102, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.SUPERNOVA_Z.mission_name, + "South Relic", + SC2_RACESWAP_LOC_ID_OFFSET + 2103, + LocationType.VANILLA, + logic.zerg_supernova_requirement, + ), + make_location_data( + SC2Mission.SUPERNOVA_Z.mission_name, + "East Relic", + SC2_RACESWAP_LOC_ID_OFFSET + 2104, + LocationType.VANILLA, + logic.zerg_supernova_requirement, + ), + make_location_data( + SC2Mission.SUPERNOVA_Z.mission_name, + "Landing Zone Cleared", + SC2_RACESWAP_LOC_ID_OFFSET + 2105, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.SUPERNOVA_Z.mission_name, + "Middle Base", + SC2_RACESWAP_LOC_ID_OFFSET + 2106, + LocationType.EXTRA, + logic.zerg_supernova_requirement, + ), + make_location_data( + SC2Mission.SUPERNOVA_Z.mission_name, + "Southeast Base", + SC2_RACESWAP_LOC_ID_OFFSET + 2107, + LocationType.EXTRA, + logic.zerg_supernova_requirement, + ), + make_location_data( + SC2Mission.SUPERNOVA_P.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 2200, + LocationType.VICTORY, + logic.protoss_supernova_requirement, + ), + make_location_data( + SC2Mission.SUPERNOVA_P.mission_name, + "West Relic", + SC2_RACESWAP_LOC_ID_OFFSET + 2201, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.SUPERNOVA_P.mission_name, + "North Relic", + SC2_RACESWAP_LOC_ID_OFFSET + 2202, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.SUPERNOVA_P.mission_name, + "South Relic", + SC2_RACESWAP_LOC_ID_OFFSET + 2203, + LocationType.VANILLA, + logic.protoss_supernova_requirement, + ), + make_location_data( + SC2Mission.SUPERNOVA_P.mission_name, + "East Relic", + SC2_RACESWAP_LOC_ID_OFFSET + 2204, + LocationType.VANILLA, + logic.protoss_supernova_requirement, + ), + make_location_data( + SC2Mission.SUPERNOVA_P.mission_name, + "Landing Zone Cleared", + SC2_RACESWAP_LOC_ID_OFFSET + 2205, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.SUPERNOVA_P.mission_name, + "Middle Base", + SC2_RACESWAP_LOC_ID_OFFSET + 2206, + LocationType.EXTRA, + logic.protoss_supernova_requirement, + ), + make_location_data( + SC2Mission.SUPERNOVA_P.mission_name, + "Southeast Base", + SC2_RACESWAP_LOC_ID_OFFSET + 2207, + LocationType.EXTRA, + logic.protoss_supernova_requirement, + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 2300, + LocationType.VICTORY, + logic.zerg_maw_requirement, + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID_Z.mission_name, + "Landing Zone Cleared", + SC2_RACESWAP_LOC_ID_OFFSET + 2301, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID_Z.mission_name, + "Expansion Prisoners", + SC2_RACESWAP_LOC_ID_OFFSET + 2302, + LocationType.VANILLA, + logic.zerg_maw_requirement, + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID_Z.mission_name, + "South Close Prisoners", + SC2_RACESWAP_LOC_ID_OFFSET + 2303, + LocationType.VANILLA, + logic.zerg_maw_requirement, + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID_Z.mission_name, + "South Far Prisoners", + SC2_RACESWAP_LOC_ID_OFFSET + 2304, + LocationType.VANILLA, + logic.zerg_maw_requirement, + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID_Z.mission_name, + "North Prisoners", + SC2_RACESWAP_LOC_ID_OFFSET + 2305, + LocationType.VANILLA, + logic.zerg_maw_requirement, + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID_Z.mission_name, + "Mothership", + SC2_RACESWAP_LOC_ID_OFFSET + 2306, + LocationType.EXTRA, + logic.zerg_maw_requirement, + hard_rule=logic.zerg_any_anti_air, + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID_Z.mission_name, + "Expansion Rip Field Generator", + SC2_RACESWAP_LOC_ID_OFFSET + 2307, + LocationType.EXTRA, + logic.zerg_maw_requirement, + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID_Z.mission_name, + "Middle Rip Field Generator", + SC2_RACESWAP_LOC_ID_OFFSET + 2308, + LocationType.EXTRA, + logic.zerg_maw_requirement, + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID_Z.mission_name, + "Southeast Rip Field Generator", + SC2_RACESWAP_LOC_ID_OFFSET + 2309, + LocationType.EXTRA, + logic.zerg_maw_requirement, + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID_Z.mission_name, + "Stargate Rip Field Generator", + SC2_RACESWAP_LOC_ID_OFFSET + 2310, + LocationType.EXTRA, + logic.zerg_maw_requirement, + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID_Z.mission_name, + "Northwest Rip Field Generator", + SC2_RACESWAP_LOC_ID_OFFSET + 2311, + LocationType.CHALLENGE, + logic.zerg_maw_requirement, + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID_Z.mission_name, + "West Rip Field Generator", + SC2_RACESWAP_LOC_ID_OFFSET + 2312, + LocationType.CHALLENGE, + logic.zerg_maw_requirement, + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID_Z.mission_name, + "Southwest Rip Field Generator", + SC2_RACESWAP_LOC_ID_OFFSET + 2313, + LocationType.CHALLENGE, + logic.zerg_maw_requirement, + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID_P.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 2400, + LocationType.VICTORY, + logic.protoss_maw_requirement, + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID_P.mission_name, + "Landing Zone Cleared", + SC2_RACESWAP_LOC_ID_OFFSET + 2401, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID_P.mission_name, + "Expansion Prisoners", + SC2_RACESWAP_LOC_ID_OFFSET + 2402, + LocationType.VANILLA, + lambda state: adv_tactics or logic.protoss_maw_requirement(state), + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID_P.mission_name, + "South Close Prisoners", + SC2_RACESWAP_LOC_ID_OFFSET + 2403, + LocationType.VANILLA, + lambda state: adv_tactics or logic.protoss_maw_requirement(state), + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID_P.mission_name, + "South Far Prisoners", + SC2_RACESWAP_LOC_ID_OFFSET + 2404, + LocationType.VANILLA, + logic.protoss_maw_requirement, + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID_P.mission_name, + "North Prisoners", + SC2_RACESWAP_LOC_ID_OFFSET + 2405, + LocationType.VANILLA, + logic.protoss_maw_requirement, + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID_P.mission_name, + "Mothership", + SC2_RACESWAP_LOC_ID_OFFSET + 2406, + LocationType.EXTRA, + logic.protoss_maw_requirement, + hard_rule=logic.protoss_any_anti_air_unit_or_soa_any_protoss, + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID_P.mission_name, + "Expansion Rip Field Generator", + SC2_RACESWAP_LOC_ID_OFFSET + 2407, + LocationType.EXTRA, + lambda state: adv_tactics or logic.protoss_maw_requirement(state), + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID_P.mission_name, + "Middle Rip Field Generator", + SC2_RACESWAP_LOC_ID_OFFSET + 2408, + LocationType.EXTRA, + logic.protoss_maw_requirement, + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID_P.mission_name, + "Southeast Rip Field Generator", + SC2_RACESWAP_LOC_ID_OFFSET + 2409, + LocationType.EXTRA, + logic.protoss_maw_requirement, + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID_P.mission_name, + "Stargate Rip Field Generator", + SC2_RACESWAP_LOC_ID_OFFSET + 2410, + LocationType.EXTRA, + logic.protoss_maw_requirement, + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID_P.mission_name, + "Northwest Rip Field Generator", + SC2_RACESWAP_LOC_ID_OFFSET + 2411, + LocationType.CHALLENGE, + logic.protoss_maw_requirement, + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID_P.mission_name, + "West Rip Field Generator", + SC2_RACESWAP_LOC_ID_OFFSET + 2412, + LocationType.CHALLENGE, + logic.protoss_maw_requirement, + ), + make_location_data( + SC2Mission.MAW_OF_THE_VOID_P.mission_name, + "Southwest Rip Field Generator", + SC2_RACESWAP_LOC_ID_OFFSET + 2413, + LocationType.CHALLENGE, + logic.protoss_maw_requirement, + ), + make_location_data( + SC2Mission.DEVILS_PLAYGROUND_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 2500, + LocationType.VICTORY, + lambda state: ( + logic.zerg_moderate_anti_air(state) + and logic.zerg_common_unit(state) + ), + ), + make_location_data( + SC2Mission.DEVILS_PLAYGROUND_Z.mission_name, + "Tosh's Miners", + SC2_RACESWAP_LOC_ID_OFFSET + 2501, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.DEVILS_PLAYGROUND_Z.mission_name, + "Brutalisk", + SC2_RACESWAP_LOC_ID_OFFSET + 2502, + LocationType.VANILLA, + lambda state: logic.zerg_common_unit(state), + ), + make_location_data( + SC2Mission.DEVILS_PLAYGROUND_Z.mission_name, + "North Reinforcements", + SC2_RACESWAP_LOC_ID_OFFSET + 2503, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.DEVILS_PLAYGROUND_Z.mission_name, + "Middle Reinforcements", + SC2_RACESWAP_LOC_ID_OFFSET + 2504, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.DEVILS_PLAYGROUND_Z.mission_name, + "Southwest Reinforcements", + SC2_RACESWAP_LOC_ID_OFFSET + 2505, + LocationType.EXTRA, + lambda state: logic.zerg_common_unit(state), + ), + make_location_data( + SC2Mission.DEVILS_PLAYGROUND_Z.mission_name, + "Southeast Reinforcements", + SC2_RACESWAP_LOC_ID_OFFSET + 2506, + LocationType.EXTRA, + lambda state: ( + logic.zerg_moderate_anti_air(state) + and logic.zerg_common_unit(state) + ), + ), + make_location_data( + SC2Mission.DEVILS_PLAYGROUND_Z.mission_name, + "East Reinforcements", + SC2_RACESWAP_LOC_ID_OFFSET + 2507, + LocationType.EXTRA, + lambda state: ( + logic.zerg_moderate_anti_air(state) + and logic.zerg_common_unit(state) + ), + ), + make_location_data( + SC2Mission.DEVILS_PLAYGROUND_Z.mission_name, + "Zerg Cleared", + SC2_RACESWAP_LOC_ID_OFFSET + 2508, + LocationType.CHALLENGE, + lambda state: ( + logic.zerg_competent_anti_air(state) + and logic.zerg_common_unit(state) + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.DEVILS_PLAYGROUND_P.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 2600, + LocationType.VICTORY, + lambda state: ( + adv_tactics + or logic.protoss_basic_anti_air(state) + and logic.protoss_common_unit(state) + ), + ), + make_location_data( + SC2Mission.DEVILS_PLAYGROUND_P.mission_name, + "Tosh's Miners", + SC2_RACESWAP_LOC_ID_OFFSET + 2601, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.DEVILS_PLAYGROUND_P.mission_name, + "Brutalisk", + SC2_RACESWAP_LOC_ID_OFFSET + 2602, + LocationType.VANILLA, + lambda state: adv_tactics or logic.protoss_common_unit(state), + ), + make_location_data( + SC2Mission.DEVILS_PLAYGROUND_P.mission_name, + "North Reinforcements", + SC2_RACESWAP_LOC_ID_OFFSET + 2603, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.DEVILS_PLAYGROUND_P.mission_name, + "Middle Reinforcements", + SC2_RACESWAP_LOC_ID_OFFSET + 2604, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.DEVILS_PLAYGROUND_P.mission_name, + "Southwest Reinforcements", + SC2_RACESWAP_LOC_ID_OFFSET + 2605, + LocationType.EXTRA, + lambda state: adv_tactics or logic.protoss_common_unit(state), + ), + make_location_data( + SC2Mission.DEVILS_PLAYGROUND_P.mission_name, + "Southeast Reinforcements", + SC2_RACESWAP_LOC_ID_OFFSET + 2606, + LocationType.EXTRA, + lambda state: ( + adv_tactics + or logic.protoss_basic_anti_air(state) + and logic.protoss_common_unit(state) + ), + ), + make_location_data( + SC2Mission.DEVILS_PLAYGROUND_P.mission_name, + "East Reinforcements", + SC2_RACESWAP_LOC_ID_OFFSET + 2607, + LocationType.EXTRA, + lambda state: ( + adv_tactics + or logic.protoss_basic_anti_air(state) + and logic.protoss_common_unit(state) + ), + ), + make_location_data( + SC2Mission.DEVILS_PLAYGROUND_P.mission_name, + "Zerg Cleared", + SC2_RACESWAP_LOC_ID_OFFSET + 2608, + LocationType.CHALLENGE, + lambda state: ( + logic.protoss_competent_anti_air(state) + and (logic.protoss_common_unit(state)) + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.WELCOME_TO_THE_JUNGLE_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 2700, + LocationType.VICTORY, + logic.zerg_welcome_to_the_jungle_requirement, + ), + make_location_data( + SC2Mission.WELCOME_TO_THE_JUNGLE_Z.mission_name, + "Close Relic", + SC2_RACESWAP_LOC_ID_OFFSET + 2701, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.WELCOME_TO_THE_JUNGLE_Z.mission_name, + "West Relic", + SC2_RACESWAP_LOC_ID_OFFSET + 2702, + LocationType.VANILLA, + logic.zerg_welcome_to_the_jungle_requirement, + ), + make_location_data( + SC2Mission.WELCOME_TO_THE_JUNGLE_Z.mission_name, + "North-East Relic", + SC2_RACESWAP_LOC_ID_OFFSET + 2703, + LocationType.VANILLA, + logic.zerg_welcome_to_the_jungle_requirement, + ), + make_location_data( + SC2Mission.WELCOME_TO_THE_JUNGLE_Z.mission_name, + "Middle Base", + SC2_RACESWAP_LOC_ID_OFFSET + 2704, + LocationType.EXTRA, + logic.zerg_welcome_to_the_jungle_requirement, + ), + make_location_data( + SC2Mission.WELCOME_TO_THE_JUNGLE_Z.mission_name, + "Protoss Cleared", + SC2_RACESWAP_LOC_ID_OFFSET + 2705, + LocationType.MASTERY, + lambda state: ( + logic.zerg_welcome_to_the_jungle_requirement(state) + and logic.zerg_competent_anti_air(state) + and logic.zerg_base_buster(state) + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.WELCOME_TO_THE_JUNGLE_Z.mission_name, + "No Terrazine Nodes Sealed", + SC2_RACESWAP_LOC_ID_OFFSET + 2706, + LocationType.CHALLENGE, + lambda state: ( + logic.zerg_welcome_to_the_jungle_requirement(state) + and logic.zerg_competent_anti_air(state) + and logic.zerg_big_monsters(state) + ), + flags=LocationFlag.PREVENTATIVE, + ), + make_location_data( + SC2Mission.WELCOME_TO_THE_JUNGLE_Z.mission_name, + "Up to 1 Terrazine Node Sealed", + SC2_RACESWAP_LOC_ID_OFFSET + 2707, + LocationType.CHALLENGE, + lambda state: ( + logic.zerg_welcome_to_the_jungle_requirement(state) + and logic.zerg_competent_anti_air(state) + ), + flags=LocationFlag.PREVENTATIVE, + ), + make_location_data( + SC2Mission.WELCOME_TO_THE_JUNGLE_Z.mission_name, + "Up to 2 Terrazine Nodes Sealed", + SC2_RACESWAP_LOC_ID_OFFSET + 2708, + LocationType.CHALLENGE, + lambda state: ( + logic.zerg_welcome_to_the_jungle_requirement(state) + and logic.zerg_competent_anti_air(state) + ), + flags=LocationFlag.PREVENTATIVE, + ), + make_location_data( + SC2Mission.WELCOME_TO_THE_JUNGLE_Z.mission_name, + "Up to 3 Terrazine Nodes Sealed", + SC2_RACESWAP_LOC_ID_OFFSET + 2709, + LocationType.CHALLENGE, + lambda state: ( + logic.zerg_welcome_to_the_jungle_requirement(state) + and logic.zerg_competent_anti_air(state) + ), + flags=LocationFlag.PREVENTATIVE, + ), + make_location_data( + SC2Mission.WELCOME_TO_THE_JUNGLE_Z.mission_name, + "Up to 4 Terrazine Nodes Sealed", + SC2_RACESWAP_LOC_ID_OFFSET + 2710, + LocationType.EXTRA, + logic.zerg_welcome_to_the_jungle_requirement, + flags=LocationFlag.PREVENTATIVE, + ), + make_location_data( + SC2Mission.WELCOME_TO_THE_JUNGLE_Z.mission_name, + "Up to 5 Terrazine Nodes Sealed", + SC2_RACESWAP_LOC_ID_OFFSET + 2711, + LocationType.EXTRA, + logic.zerg_welcome_to_the_jungle_requirement, + flags=LocationFlag.PREVENTATIVE, + ), + make_location_data( + SC2Mission.WELCOME_TO_THE_JUNGLE_P.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 2800, + LocationType.VICTORY, + logic.protoss_welcome_to_the_jungle_requirement, + ), + make_location_data( + SC2Mission.WELCOME_TO_THE_JUNGLE_P.mission_name, + "Close Relic", + SC2_RACESWAP_LOC_ID_OFFSET + 2801, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.WELCOME_TO_THE_JUNGLE_P.mission_name, + "West Relic", + SC2_RACESWAP_LOC_ID_OFFSET + 2802, + LocationType.VANILLA, + logic.protoss_welcome_to_the_jungle_requirement, + ), + make_location_data( + SC2Mission.WELCOME_TO_THE_JUNGLE_P.mission_name, + "North-East Relic", + SC2_RACESWAP_LOC_ID_OFFSET + 2803, + LocationType.VANILLA, + logic.protoss_welcome_to_the_jungle_requirement, + ), + make_location_data( + SC2Mission.WELCOME_TO_THE_JUNGLE_P.mission_name, + "Middle Base", + SC2_RACESWAP_LOC_ID_OFFSET + 2804, + LocationType.EXTRA, + logic.protoss_welcome_to_the_jungle_requirement, + ), + make_location_data( + SC2Mission.WELCOME_TO_THE_JUNGLE_P.mission_name, + "Protoss Cleared", + SC2_RACESWAP_LOC_ID_OFFSET + 2805, + LocationType.MASTERY, + lambda state: ( + logic.protoss_welcome_to_the_jungle_requirement(state) + and logic.protoss_competent_comp(state) + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.WELCOME_TO_THE_JUNGLE_P.mission_name, + "No Terrazine Nodes Sealed", + SC2_RACESWAP_LOC_ID_OFFSET + 2806, + LocationType.CHALLENGE, + lambda state: ( + logic.protoss_welcome_to_the_jungle_requirement(state) + and logic.protoss_competent_comp(state) + ), + flags=LocationFlag.PREVENTATIVE, + ), + make_location_data( + SC2Mission.WELCOME_TO_THE_JUNGLE_P.mission_name, + "Up to 1 Terrazine Node Sealed", + SC2_RACESWAP_LOC_ID_OFFSET + 2807, + LocationType.CHALLENGE, + lambda state: ( + logic.protoss_welcome_to_the_jungle_requirement(state) + and logic.protoss_competent_comp(state) + ), + flags=LocationFlag.PREVENTATIVE, + ), + make_location_data( + SC2Mission.WELCOME_TO_THE_JUNGLE_P.mission_name, + "Up to 2 Terrazine Nodes Sealed", + SC2_RACESWAP_LOC_ID_OFFSET + 2808, + LocationType.CHALLENGE, + lambda state: ( + logic.protoss_welcome_to_the_jungle_requirement(state) + and logic.protoss_basic_splash(state) + ), + flags=LocationFlag.PREVENTATIVE, + ), + make_location_data( + SC2Mission.WELCOME_TO_THE_JUNGLE_P.mission_name, + "Up to 3 Terrazine Nodes Sealed", + SC2_RACESWAP_LOC_ID_OFFSET + 2809, + LocationType.CHALLENGE, + lambda state: ( + logic.protoss_welcome_to_the_jungle_requirement(state) + and logic.protoss_basic_splash(state) + ), + flags=LocationFlag.PREVENTATIVE, + ), + make_location_data( + SC2Mission.WELCOME_TO_THE_JUNGLE_P.mission_name, + "Up to 4 Terrazine Nodes Sealed", + SC2_RACESWAP_LOC_ID_OFFSET + 2810, + LocationType.EXTRA, + logic.protoss_welcome_to_the_jungle_requirement, + flags=LocationFlag.PREVENTATIVE, + ), + make_location_data( + SC2Mission.WELCOME_TO_THE_JUNGLE_P.mission_name, + "Up to 5 Terrazine Nodes Sealed", + SC2_RACESWAP_LOC_ID_OFFSET + 2811, + LocationType.EXTRA, + logic.protoss_welcome_to_the_jungle_requirement, + flags=LocationFlag.PREVENTATIVE, + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 3300, + LocationType.VICTORY, + lambda state: ( + logic.zerg_great_train_robbery_train_stopper(state) + and logic.zerg_basic_kerriganless_anti_air(state) + ), + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY_Z.mission_name, + "North Defiler", + SC2_RACESWAP_LOC_ID_OFFSET + 3301, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY_Z.mission_name, + "Mid Defiler", + SC2_RACESWAP_LOC_ID_OFFSET + 3302, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY_Z.mission_name, + "South Defiler", + SC2_RACESWAP_LOC_ID_OFFSET + 3303, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY_Z.mission_name, + "Close Infested Diamondback", + SC2_RACESWAP_LOC_ID_OFFSET + 3304, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY_Z.mission_name, + "Northwest Infested Diamondback", + SC2_RACESWAP_LOC_ID_OFFSET + 3305, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY_Z.mission_name, + "North Infested Diamondback", + SC2_RACESWAP_LOC_ID_OFFSET + 3306, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY_Z.mission_name, + "Northeast Infested Diamondback", + SC2_RACESWAP_LOC_ID_OFFSET + 3307, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY_Z.mission_name, + "Southwest Infested Diamondback", + SC2_RACESWAP_LOC_ID_OFFSET + 3308, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY_Z.mission_name, + "Southeast Infested Diamondback", + SC2_RACESWAP_LOC_ID_OFFSET + 3309, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY_Z.mission_name, + "Kill Team", + SC2_RACESWAP_LOC_ID_OFFSET + 3310, + LocationType.CHALLENGE, + lambda state: ( + (adv_tactics or logic.zerg_common_unit(state)) + and logic.zerg_great_train_robbery_train_stopper(state) + and logic.zerg_basic_kerriganless_anti_air(state) + ), + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY_Z.mission_name, + "Flawless", + SC2_RACESWAP_LOC_ID_OFFSET + 3311, + LocationType.CHALLENGE, + lambda state: ( + logic.zerg_great_train_robbery_train_stopper(state) + and logic.zerg_basic_kerriganless_anti_air(state) + ), + flags=LocationFlag.PREVENTATIVE, + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY_Z.mission_name, + "2 Trains Destroyed", + SC2_RACESWAP_LOC_ID_OFFSET + 3312, + LocationType.EXTRA, + logic.zerg_great_train_robbery_train_stopper, + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY_Z.mission_name, + "4 Trains Destroyed", + SC2_RACESWAP_LOC_ID_OFFSET + 3313, + LocationType.EXTRA, + lambda state: ( + logic.zerg_great_train_robbery_train_stopper(state) + and logic.zerg_basic_kerriganless_anti_air(state) + ), + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY_Z.mission_name, + "6 Trains Destroyed", + SC2_RACESWAP_LOC_ID_OFFSET + 3314, + LocationType.EXTRA, + lambda state: ( + logic.zerg_great_train_robbery_train_stopper(state) + and logic.zerg_basic_kerriganless_anti_air(state) + ), + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY_P.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 3400, + LocationType.VICTORY, + lambda state: ( + logic.protoss_great_train_robbery_train_stopper(state) + and logic.protoss_basic_anti_air(state) + ), + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY_P.mission_name, + "North Defiler", + SC2_RACESWAP_LOC_ID_OFFSET + 3401, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY_P.mission_name, + "Mid Defiler", + SC2_RACESWAP_LOC_ID_OFFSET + 3402, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY_P.mission_name, + "South Defiler", + SC2_RACESWAP_LOC_ID_OFFSET + 3403, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY_P.mission_name, + "Close Immortal", + SC2_RACESWAP_LOC_ID_OFFSET + 3404, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY_P.mission_name, + "Northwest Immortal", + SC2_RACESWAP_LOC_ID_OFFSET + 3405, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY_P.mission_name, + "North Instigator", + SC2_RACESWAP_LOC_ID_OFFSET + 3406, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY_P.mission_name, + "Northeast Instigator", + SC2_RACESWAP_LOC_ID_OFFSET + 3407, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY_P.mission_name, + "Southwest Instigator", + SC2_RACESWAP_LOC_ID_OFFSET + 3408, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY_P.mission_name, + "Southeast Immortal", + SC2_RACESWAP_LOC_ID_OFFSET + 3409, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY_P.mission_name, + "Kill Team", + SC2_RACESWAP_LOC_ID_OFFSET + 3410, + LocationType.CHALLENGE, + lambda state: ( + (adv_tactics or logic.protoss_common_unit(state)) + and logic.protoss_great_train_robbery_train_stopper(state) + and logic.protoss_basic_anti_air(state) + ), + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY_P.mission_name, + "Flawless", + SC2_RACESWAP_LOC_ID_OFFSET + 3411, + LocationType.CHALLENGE, + lambda state: ( + logic.protoss_great_train_robbery_train_stopper(state) + and logic.protoss_basic_anti_air(state) + ), + flags=LocationFlag.PREVENTATIVE, + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY_P.mission_name, + "2 Trains Destroyed", + SC2_RACESWAP_LOC_ID_OFFSET + 3412, + LocationType.EXTRA, + logic.protoss_great_train_robbery_train_stopper, + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY_P.mission_name, + "4 Trains Destroyed", + SC2_RACESWAP_LOC_ID_OFFSET + 3413, + LocationType.EXTRA, + lambda state: ( + logic.protoss_great_train_robbery_train_stopper(state) + and logic.protoss_basic_anti_air(state) + ), + ), + make_location_data( + SC2Mission.THE_GREAT_TRAIN_ROBBERY_P.mission_name, + "6 Trains Destroyed", + SC2_RACESWAP_LOC_ID_OFFSET + 3414, + LocationType.EXTRA, + lambda state: ( + logic.protoss_great_train_robbery_train_stopper(state) + and logic.protoss_basic_anti_air(state) + ), + ), + make_location_data( + SC2Mission.CUTTHROAT_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 3500, + LocationType.VICTORY, + lambda state: ( + logic.zerg_common_unit(state) + and (adv_tactics or logic.zerg_moderate_anti_air(state)) + ), + ), + make_location_data( + SC2Mission.CUTTHROAT_Z.mission_name, + "Mira Han", + SC2_RACESWAP_LOC_ID_OFFSET + 3501, + LocationType.EXTRA, + logic.zerg_common_unit, + ), + make_location_data( + SC2Mission.CUTTHROAT_Z.mission_name, + "North Relic", + SC2_RACESWAP_LOC_ID_OFFSET + 3502, + LocationType.VANILLA, + logic.zerg_common_unit, + ), + make_location_data( + SC2Mission.CUTTHROAT_Z.mission_name, + "Mid Relic", + SC2_RACESWAP_LOC_ID_OFFSET + 3503, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.CUTTHROAT_Z.mission_name, + "Southwest Relic", + SC2_RACESWAP_LOC_ID_OFFSET + 3504, + LocationType.VANILLA, + logic.zerg_common_unit, + ), + make_location_data( + SC2Mission.CUTTHROAT_Z.mission_name, + "North Command Center", + SC2_RACESWAP_LOC_ID_OFFSET + 3505, + LocationType.EXTRA, + logic.zerg_common_unit, + ), + make_location_data( + SC2Mission.CUTTHROAT_Z.mission_name, + "South Command Center", + SC2_RACESWAP_LOC_ID_OFFSET + 3506, + LocationType.EXTRA, + logic.zerg_common_unit, + ), + make_location_data( + SC2Mission.CUTTHROAT_Z.mission_name, + "West Command Center", + SC2_RACESWAP_LOC_ID_OFFSET + 3507, + LocationType.EXTRA, + logic.zerg_common_unit, + ), + make_location_data( + SC2Mission.CUTTHROAT_P.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 3600, + LocationType.VICTORY, + lambda state: ( + logic.protoss_common_unit(state) + and (adv_tactics or logic.protoss_basic_anti_air(state)) + ), + ), + make_location_data( + SC2Mission.CUTTHROAT_P.mission_name, + "Mira Han", + SC2_RACESWAP_LOC_ID_OFFSET + 3601, + LocationType.EXTRA, + logic.protoss_common_unit, + ), + make_location_data( + SC2Mission.CUTTHROAT_P.mission_name, + "North Relic", + SC2_RACESWAP_LOC_ID_OFFSET + 3602, + LocationType.VANILLA, + logic.protoss_common_unit, + ), + make_location_data( + SC2Mission.CUTTHROAT_P.mission_name, + "Mid Relic", + SC2_RACESWAP_LOC_ID_OFFSET + 3603, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.CUTTHROAT_P.mission_name, + "Southwest Relic", + SC2_RACESWAP_LOC_ID_OFFSET + 3604, + LocationType.VANILLA, + logic.protoss_common_unit, + ), + make_location_data( + SC2Mission.CUTTHROAT_P.mission_name, + "North Command Center", + SC2_RACESWAP_LOC_ID_OFFSET + 3605, + LocationType.EXTRA, + logic.protoss_common_unit, + ), + make_location_data( + SC2Mission.CUTTHROAT_P.mission_name, + "South Command Center", + SC2_RACESWAP_LOC_ID_OFFSET + 3606, + LocationType.EXTRA, + logic.protoss_common_unit, + ), + make_location_data( + SC2Mission.CUTTHROAT_P.mission_name, + "West Command Center", + SC2_RACESWAP_LOC_ID_OFFSET + 3607, + LocationType.EXTRA, + logic.protoss_common_unit, + ), + make_location_data( + SC2Mission.ENGINE_OF_DESTRUCTION_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 3700, + LocationType.VICTORY, + logic.zerg_engine_of_destruction_requirement, + ), + make_location_data( + SC2Mission.ENGINE_OF_DESTRUCTION_Z.mission_name, + "Odin", + SC2_RACESWAP_LOC_ID_OFFSET + 3701, + LocationType.EXTRA, + logic.zergling_hydra_roach_start, + ), + make_location_data( + SC2Mission.ENGINE_OF_DESTRUCTION_Z.mission_name, + "Loki", + SC2_RACESWAP_LOC_ID_OFFSET + 3702, + LocationType.CHALLENGE, + logic.zerg_engine_of_destruction_requirement, + ), + make_location_data( + SC2Mission.ENGINE_OF_DESTRUCTION_Z.mission_name, + "Lab Devourer", + SC2_RACESWAP_LOC_ID_OFFSET + 3703, + LocationType.VANILLA, + logic.zergling_hydra_roach_start, + ), + make_location_data( + SC2Mission.ENGINE_OF_DESTRUCTION_Z.mission_name, + "North Devourer", + SC2_RACESWAP_LOC_ID_OFFSET + 3704, + LocationType.VANILLA, + logic.zerg_engine_of_destruction_requirement, + ), + make_location_data( + SC2Mission.ENGINE_OF_DESTRUCTION_Z.mission_name, + "Southeast Devourer", + SC2_RACESWAP_LOC_ID_OFFSET + 3705, + LocationType.VANILLA, + logic.zerg_engine_of_destruction_requirement, + ), + make_location_data( + SC2Mission.ENGINE_OF_DESTRUCTION_Z.mission_name, + "West Base", + SC2_RACESWAP_LOC_ID_OFFSET + 3706, + LocationType.EXTRA, + logic.zerg_engine_of_destruction_requirement, + ), + make_location_data( + SC2Mission.ENGINE_OF_DESTRUCTION_Z.mission_name, + "Northwest Base", + SC2_RACESWAP_LOC_ID_OFFSET + 3707, + LocationType.EXTRA, + logic.zerg_engine_of_destruction_requirement, + ), + make_location_data( + SC2Mission.ENGINE_OF_DESTRUCTION_Z.mission_name, + "Northeast Base", + SC2_RACESWAP_LOC_ID_OFFSET + 3708, + LocationType.EXTRA, + logic.zerg_engine_of_destruction_requirement, + ), + make_location_data( + SC2Mission.ENGINE_OF_DESTRUCTION_Z.mission_name, + "Southeast Base", + SC2_RACESWAP_LOC_ID_OFFSET + 3709, + LocationType.EXTRA, + logic.zerg_engine_of_destruction_requirement, + ), + make_location_data( + SC2Mission.ENGINE_OF_DESTRUCTION_P.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 3800, + LocationType.VICTORY, + logic.protoss_engine_of_destruction_requirement, + ), + make_location_data( + SC2Mission.ENGINE_OF_DESTRUCTION_P.mission_name, + "Odin", + SC2_RACESWAP_LOC_ID_OFFSET + 3801, + LocationType.EXTRA, + logic.zealot_sentry_slayer_start, + ), + make_location_data( + SC2Mission.ENGINE_OF_DESTRUCTION_P.mission_name, + "Loki", + SC2_RACESWAP_LOC_ID_OFFSET + 3802, + LocationType.CHALLENGE, + logic.protoss_engine_of_destruction_requirement, + ), + make_location_data( + SC2Mission.ENGINE_OF_DESTRUCTION_P.mission_name, + "Lab Devourer", + SC2_RACESWAP_LOC_ID_OFFSET + 3803, + LocationType.VANILLA, + logic.zealot_sentry_slayer_start, + ), + make_location_data( + SC2Mission.ENGINE_OF_DESTRUCTION_P.mission_name, + "North Devourer", + SC2_RACESWAP_LOC_ID_OFFSET + 3804, + LocationType.VANILLA, + logic.protoss_engine_of_destruction_requirement, + ), + make_location_data( + SC2Mission.ENGINE_OF_DESTRUCTION_P.mission_name, + "Southeast Devourer", + SC2_RACESWAP_LOC_ID_OFFSET + 3805, + LocationType.VANILLA, + logic.protoss_engine_of_destruction_requirement, + ), + make_location_data( + SC2Mission.ENGINE_OF_DESTRUCTION_P.mission_name, + "West Base", + SC2_RACESWAP_LOC_ID_OFFSET + 3806, + LocationType.EXTRA, + logic.protoss_engine_of_destruction_requirement, + ), + make_location_data( + SC2Mission.ENGINE_OF_DESTRUCTION_P.mission_name, + "Northwest Base", + SC2_RACESWAP_LOC_ID_OFFSET + 3807, + LocationType.EXTRA, + logic.protoss_engine_of_destruction_requirement, + ), + make_location_data( + SC2Mission.ENGINE_OF_DESTRUCTION_P.mission_name, + "Northeast Base", + SC2_RACESWAP_LOC_ID_OFFSET + 3808, + LocationType.EXTRA, + logic.protoss_engine_of_destruction_requirement, + ), + make_location_data( + SC2Mission.ENGINE_OF_DESTRUCTION_P.mission_name, + "Southeast Base", + SC2_RACESWAP_LOC_ID_OFFSET + 3809, + LocationType.EXTRA, + logic.protoss_engine_of_destruction_requirement, + ), + make_location_data( + SC2Mission.MEDIA_BLITZ_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 3900, + LocationType.VICTORY, + logic.zerg_competent_comp_competent_aa, + ), + make_location_data( + SC2Mission.MEDIA_BLITZ_Z.mission_name, + "Tower 1", + SC2_RACESWAP_LOC_ID_OFFSET + 3901, + LocationType.VANILLA, + logic.zerg_competent_comp_competent_aa, + ), + make_location_data( + SC2Mission.MEDIA_BLITZ_Z.mission_name, + "Tower 2", + SC2_RACESWAP_LOC_ID_OFFSET + 3902, + LocationType.VANILLA, + logic.zerg_competent_comp_competent_aa, + ), + make_location_data( + SC2Mission.MEDIA_BLITZ_Z.mission_name, + "Tower 3", + SC2_RACESWAP_LOC_ID_OFFSET + 3903, + LocationType.VANILLA, + logic.zerg_competent_comp_competent_aa, + ), + make_location_data( + SC2Mission.MEDIA_BLITZ_Z.mission_name, + "Science Facility", + SC2_RACESWAP_LOC_ID_OFFSET + 3904, + LocationType.VANILLA, + lambda state: ( + logic.advanced_tactics or logic.zerg_competent_comp_competent_aa(state) + ), + ), + make_location_data( + SC2Mission.MEDIA_BLITZ_Z.mission_name, + "All Barracks", + SC2_RACESWAP_LOC_ID_OFFSET + 3905, + LocationType.EXTRA, + logic.zerg_competent_comp_competent_aa, + ), + make_location_data( + SC2Mission.MEDIA_BLITZ_Z.mission_name, + "All Factories", + SC2_RACESWAP_LOC_ID_OFFSET + 3906, + LocationType.EXTRA, + logic.zerg_competent_comp_competent_aa, + ), + make_location_data( + SC2Mission.MEDIA_BLITZ_Z.mission_name, + "All Starports", + SC2_RACESWAP_LOC_ID_OFFSET + 3907, + LocationType.EXTRA, + logic.zerg_competent_comp_competent_aa, + ), + make_location_data( + SC2Mission.MEDIA_BLITZ_Z.mission_name, + "Odin Not Trashed", + SC2_RACESWAP_LOC_ID_OFFSET + 3908, + LocationType.CHALLENGE, + lambda state: ( + logic.zerg_competent_comp_competent_aa(state) + and logic.zerg_repair_odin(state) + ), + ), + make_location_data( + SC2Mission.MEDIA_BLITZ_Z.mission_name, + "Surprise Attack Ends", + SC2_RACESWAP_LOC_ID_OFFSET + 3909, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.MEDIA_BLITZ_P.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 4000, + LocationType.VICTORY, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.MEDIA_BLITZ_P.mission_name, + "Tower 1", + SC2_RACESWAP_LOC_ID_OFFSET + 4001, + LocationType.VANILLA, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.MEDIA_BLITZ_P.mission_name, + "Tower 2", + SC2_RACESWAP_LOC_ID_OFFSET + 4002, + LocationType.VANILLA, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.MEDIA_BLITZ_P.mission_name, + "Tower 3", + SC2_RACESWAP_LOC_ID_OFFSET + 4003, + LocationType.VANILLA, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.MEDIA_BLITZ_P.mission_name, + "Science Facility", + SC2_RACESWAP_LOC_ID_OFFSET + 4004, + LocationType.VANILLA, + lambda state: adv_tactics or logic.protoss_competent_comp(state), + ), + make_location_data( + SC2Mission.MEDIA_BLITZ_P.mission_name, + "All Barracks", + SC2_RACESWAP_LOC_ID_OFFSET + 4005, + LocationType.EXTRA, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.MEDIA_BLITZ_P.mission_name, + "All Factories", + SC2_RACESWAP_LOC_ID_OFFSET + 4006, + LocationType.EXTRA, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.MEDIA_BLITZ_P.mission_name, + "All Starports", + SC2_RACESWAP_LOC_ID_OFFSET + 4007, + LocationType.EXTRA, + lambda state: adv_tactics or logic.protoss_competent_comp(state), + ), + make_location_data( + SC2Mission.MEDIA_BLITZ_P.mission_name, + "Odin Not Trashed", + SC2_RACESWAP_LOC_ID_OFFSET + 4008, + LocationType.CHALLENGE, + lambda state: ( + logic.protoss_competent_comp(state) and logic.protoss_repair_odin(state) + ), + ), + make_location_data( + SC2Mission.MEDIA_BLITZ_P.mission_name, + "Surprise Attack Ends", + SC2_RACESWAP_LOC_ID_OFFSET + 4009, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.A_SINISTER_TURN_T.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 4500, + LocationType.VICTORY, + lambda state: ( + logic.terran_competent_comp(state) + and logic.terran_common_unit(state) + and logic.terran_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.A_SINISTER_TURN_T.mission_name, + "Factory", + SC2_RACESWAP_LOC_ID_OFFSET + 4501, + LocationType.VANILLA, + lambda state: adv_tactics or logic.terran_common_unit(state), + ), + make_location_data( + SC2Mission.A_SINISTER_TURN_T.mission_name, + "Armory", + SC2_RACESWAP_LOC_ID_OFFSET + 4502, + LocationType.VANILLA, + lambda state: adv_tactics or logic.terran_common_unit(state), + ), + make_location_data( + SC2Mission.A_SINISTER_TURN_T.mission_name, + "Shadow Ops", + SC2_RACESWAP_LOC_ID_OFFSET + 4503, + LocationType.VANILLA, + lambda state: logic.terran_common_unit(state) + and logic.terran_competent_anti_air(state), + ), + make_location_data( + SC2Mission.A_SINISTER_TURN_T.mission_name, + "Northeast Base", + SC2_RACESWAP_LOC_ID_OFFSET + 4504, + LocationType.EXTRA, + lambda state: logic.terran_common_unit(state) + and logic.terran_competent_anti_air(state), + ), + make_location_data( + SC2Mission.A_SINISTER_TURN_T.mission_name, + "Southwest Base", + SC2_RACESWAP_LOC_ID_OFFSET + 4505, + LocationType.CHALLENGE, + lambda state: ( + logic.terran_competent_comp(state) + and logic.terran_common_unit(state) + and logic.terran_competent_anti_air(state) + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.A_SINISTER_TURN_T.mission_name, + "Maar", + SC2_RACESWAP_LOC_ID_OFFSET + 4506, + LocationType.EXTRA, + logic.terran_common_unit, + ), + make_location_data( + SC2Mission.A_SINISTER_TURN_T.mission_name, + "Northwest Preserver", + SC2_RACESWAP_LOC_ID_OFFSET + 4507, + LocationType.EXTRA, + lambda state: ( + logic.terran_competent_comp(state) + and logic.terran_common_unit(state) + and logic.terran_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.A_SINISTER_TURN_T.mission_name, + "Southwest Preserver", + SC2_RACESWAP_LOC_ID_OFFSET + 4508, + LocationType.EXTRA, + lambda state: ( + logic.terran_competent_comp(state) + and logic.terran_common_unit(state) + and logic.terran_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.A_SINISTER_TURN_T.mission_name, + "East Preserver", + SC2_RACESWAP_LOC_ID_OFFSET + 4509, + LocationType.EXTRA, + lambda state: ( + logic.terran_competent_comp(state) + and logic.terran_common_unit(state) + and logic.terran_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.A_SINISTER_TURN_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 4600, + LocationType.VICTORY, + lambda state: logic.zerg_competent_comp(state) + and logic.zerg_competent_anti_air(state), + ), + make_location_data( + SC2Mission.A_SINISTER_TURN_Z.mission_name, + "Ultralisk Cavern", + SC2_RACESWAP_LOC_ID_OFFSET + 4601, + LocationType.VANILLA, + lambda state: (adv_tactics or logic.zerg_common_unit(state)) + and logic.spread_creep(state), + ), + make_location_data( + SC2Mission.A_SINISTER_TURN_Z.mission_name, + "Hydralisk Den", + SC2_RACESWAP_LOC_ID_OFFSET + 4602, + LocationType.VANILLA, + lambda state: (adv_tactics or logic.zerg_common_unit(state)) + and logic.spread_creep(state), + ), + make_location_data( + SC2Mission.A_SINISTER_TURN_Z.mission_name, + "Infestation Pit", + SC2_RACESWAP_LOC_ID_OFFSET + 4603, + LocationType.VANILLA, + lambda state: logic.zerg_common_unit(state) + and logic.zerg_competent_anti_air(state) + and logic.spread_creep(state), + ), + make_location_data( + SC2Mission.A_SINISTER_TURN_Z.mission_name, + "Northeast Base", + SC2_RACESWAP_LOC_ID_OFFSET + 4604, + LocationType.EXTRA, + lambda state: logic.zerg_common_unit(state) + and logic.zerg_competent_anti_air(state), + ), + make_location_data( + SC2Mission.A_SINISTER_TURN_Z.mission_name, + "Southwest Base", + SC2_RACESWAP_LOC_ID_OFFSET + 4605, + LocationType.CHALLENGE, + lambda state: logic.zerg_common_unit(state) + and logic.zerg_competent_anti_air(state), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.A_SINISTER_TURN_Z.mission_name, + "Maar", + SC2_RACESWAP_LOC_ID_OFFSET + 4606, + LocationType.EXTRA, + logic.zerg_common_unit, + ), + make_location_data( + SC2Mission.A_SINISTER_TURN_Z.mission_name, + "Northwest Preserver", + SC2_RACESWAP_LOC_ID_OFFSET + 4607, + LocationType.EXTRA, + lambda state: logic.zerg_competent_comp(state) + and logic.zerg_competent_anti_air(state), + ), + make_location_data( + SC2Mission.A_SINISTER_TURN_Z.mission_name, + "Southwest Preserver", + SC2_RACESWAP_LOC_ID_OFFSET + 4608, + LocationType.EXTRA, + lambda state: logic.zerg_competent_comp(state) + and logic.zerg_competent_anti_air(state), + ), + make_location_data( + SC2Mission.A_SINISTER_TURN_Z.mission_name, + "East Preserver", + SC2_RACESWAP_LOC_ID_OFFSET + 4609, + LocationType.EXTRA, + lambda state: logic.zerg_competent_comp(state) + and logic.zerg_competent_anti_air(state), + ), + make_location_data( + SC2Mission.ECHOES_OF_THE_FUTURE_T.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 4700, + LocationType.VICTORY, + lambda state: logic.terran_common_unit(state) + and logic.terran_competent_anti_air(state), + ), + make_location_data( + SC2Mission.ECHOES_OF_THE_FUTURE_T.mission_name, + "Close Obelisk", + SC2_RACESWAP_LOC_ID_OFFSET + 4701, + LocationType.VANILLA, + lambda state: adv_tactics or logic.terran_common_unit(state), + ), + make_location_data( + SC2Mission.ECHOES_OF_THE_FUTURE_T.mission_name, + "West Obelisk", + SC2_RACESWAP_LOC_ID_OFFSET + 4702, + LocationType.VANILLA, + lambda state: adv_tactics + or (logic.terran_common_unit(state) and logic.terran_basic_anti_air(state)), + ), + make_location_data( + SC2Mission.ECHOES_OF_THE_FUTURE_T.mission_name, + "Base", + SC2_RACESWAP_LOC_ID_OFFSET + 4703, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.ECHOES_OF_THE_FUTURE_T.mission_name, + "Southwest Tendril", + SC2_RACESWAP_LOC_ID_OFFSET + 4704, + LocationType.EXTRA, + logic.terran_common_unit, + ), + make_location_data( + SC2Mission.ECHOES_OF_THE_FUTURE_T.mission_name, + "Southeast Tendril", + SC2_RACESWAP_LOC_ID_OFFSET + 4705, + LocationType.EXTRA, + logic.terran_common_unit, + ), + make_location_data( + SC2Mission.ECHOES_OF_THE_FUTURE_T.mission_name, + "Northeast Tendril", + SC2_RACESWAP_LOC_ID_OFFSET + 4706, + LocationType.EXTRA, + lambda state: logic.terran_common_unit(state) + and logic.terran_competent_anti_air(state), + ), + make_location_data( + SC2Mission.ECHOES_OF_THE_FUTURE_T.mission_name, + "Northwest Tendril", + SC2_RACESWAP_LOC_ID_OFFSET + 4707, + LocationType.EXTRA, + lambda state: logic.terran_common_unit(state) + and logic.terran_competent_anti_air(state), + ), + make_location_data( + SC2Mission.ECHOES_OF_THE_FUTURE_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 4800, + LocationType.VICTORY, + lambda state: logic.zerg_common_unit(state) + and logic.zerg_competent_anti_air(state), + ), + make_location_data( + SC2Mission.ECHOES_OF_THE_FUTURE_Z.mission_name, + "Close Obelisk", + SC2_RACESWAP_LOC_ID_OFFSET + 4801, + LocationType.VANILLA, + lambda state: adv_tactics or logic.zerg_common_unit, + ), + make_location_data( + SC2Mission.ECHOES_OF_THE_FUTURE_Z.mission_name, + "West Obelisk", + SC2_RACESWAP_LOC_ID_OFFSET + 4802, + LocationType.VANILLA, + lambda state: ( + adv_tactics + or ( + logic.zerg_common_unit(state) + and logic.zerg_basic_kerriganless_anti_air(state) + and logic.spread_creep(state) + ) + ), + ), + make_location_data( + SC2Mission.ECHOES_OF_THE_FUTURE_Z.mission_name, + "Base", + SC2_RACESWAP_LOC_ID_OFFSET + 4803, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.ECHOES_OF_THE_FUTURE_Z.mission_name, + "Southwest Tendril", + SC2_RACESWAP_LOC_ID_OFFSET + 4804, + LocationType.EXTRA, + logic.zerg_common_unit, + ), + make_location_data( + SC2Mission.ECHOES_OF_THE_FUTURE_Z.mission_name, + "Southeast Tendril", + SC2_RACESWAP_LOC_ID_OFFSET + 4805, + LocationType.EXTRA, + logic.zerg_common_unit, + ), + make_location_data( + SC2Mission.ECHOES_OF_THE_FUTURE_Z.mission_name, + "Northeast Tendril", + SC2_RACESWAP_LOC_ID_OFFSET + 4806, + LocationType.EXTRA, + lambda state: logic.zerg_common_unit(state) + and logic.zerg_competent_anti_air(state), + ), + make_location_data( + SC2Mission.ECHOES_OF_THE_FUTURE_Z.mission_name, + "Northwest Tendril", + SC2_RACESWAP_LOC_ID_OFFSET + 4807, + LocationType.EXTRA, + lambda state: logic.zerg_common_unit(state) + and logic.zerg_competent_anti_air(state), + ), + make_location_data( + SC2Mission.IN_UTTER_DARKNESS_T.mission_name, + "Defeat", + SC2_RACESWAP_LOC_ID_OFFSET + 4900, + LocationType.VICTORY, + ), + make_location_data( + SC2Mission.IN_UTTER_DARKNESS_T.mission_name, + "Protoss Archive", + SC2_RACESWAP_LOC_ID_OFFSET + 4901, + LocationType.VANILLA, + logic.terran_in_utter_darkness_requirement, + ), + make_location_data( + SC2Mission.IN_UTTER_DARKNESS_T.mission_name, + "Kills", + SC2_RACESWAP_LOC_ID_OFFSET + 4902, + LocationType.VANILLA, + logic.terran_in_utter_darkness_requirement, + ), + make_location_data( + SC2Mission.IN_UTTER_DARKNESS_T.mission_name, + "Urun", + SC2_RACESWAP_LOC_ID_OFFSET + 4903, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.IN_UTTER_DARKNESS_T.mission_name, + "Mohandar", + SC2_RACESWAP_LOC_ID_OFFSET + 4904, + LocationType.EXTRA, + logic.terran_in_utter_darkness_requirement, + ), + make_location_data( + SC2Mission.IN_UTTER_DARKNESS_T.mission_name, + "Selendis", + SC2_RACESWAP_LOC_ID_OFFSET + 4905, + LocationType.EXTRA, + logic.terran_in_utter_darkness_requirement, + ), + make_location_data( + SC2Mission.IN_UTTER_DARKNESS_T.mission_name, + "Artanis", + SC2_RACESWAP_LOC_ID_OFFSET + 4906, + LocationType.EXTRA, + logic.terran_in_utter_darkness_requirement, + ), + make_location_data( + SC2Mission.IN_UTTER_DARKNESS_Z.mission_name, + "Defeat", + SC2_RACESWAP_LOC_ID_OFFSET + 5000, + LocationType.VICTORY, + ), + make_location_data( + SC2Mission.IN_UTTER_DARKNESS_Z.mission_name, + "Protoss Archive", + SC2_RACESWAP_LOC_ID_OFFSET + 5001, + LocationType.VANILLA, + logic.zerg_in_utter_darkness_requirement, + ), + make_location_data( + SC2Mission.IN_UTTER_DARKNESS_Z.mission_name, + "Kills", + SC2_RACESWAP_LOC_ID_OFFSET + 5002, + LocationType.VANILLA, + logic.zerg_in_utter_darkness_requirement, + ), + make_location_data( + SC2Mission.IN_UTTER_DARKNESS_Z.mission_name, + "Urun", + SC2_RACESWAP_LOC_ID_OFFSET + 5003, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.IN_UTTER_DARKNESS_Z.mission_name, + "Mohandar", + SC2_RACESWAP_LOC_ID_OFFSET + 5004, + LocationType.EXTRA, + logic.zerg_in_utter_darkness_requirement, + ), + make_location_data( + SC2Mission.IN_UTTER_DARKNESS_Z.mission_name, + "Selendis", + SC2_RACESWAP_LOC_ID_OFFSET + 5005, + LocationType.EXTRA, + logic.zerg_in_utter_darkness_requirement, + ), + make_location_data( + SC2Mission.IN_UTTER_DARKNESS_Z.mission_name, + "Artanis", + SC2_RACESWAP_LOC_ID_OFFSET + 5006, + LocationType.EXTRA, + logic.zerg_in_utter_darkness_requirement, + ), + make_location_data( + SC2Mission.GATES_OF_HELL_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 5100, + LocationType.VICTORY, + logic.zerg_gates_of_hell_requirement, + ), + make_location_data( + SC2Mission.GATES_OF_HELL_Z.mission_name, + "Large Army", + SC2_RACESWAP_LOC_ID_OFFSET + 5101, + LocationType.VANILLA, + logic.zerg_gates_of_hell_requirement, + ), + make_location_data( + SC2Mission.GATES_OF_HELL_Z.mission_name, + "2 Drop Pods", + SC2_RACESWAP_LOC_ID_OFFSET + 5102, + LocationType.VANILLA, + logic.zerg_gates_of_hell_requirement, + ), + make_location_data( + SC2Mission.GATES_OF_HELL_Z.mission_name, + "4 Drop Pods", + SC2_RACESWAP_LOC_ID_OFFSET + 5103, + LocationType.VANILLA, + logic.zerg_gates_of_hell_requirement, + ), + make_location_data( + SC2Mission.GATES_OF_HELL_Z.mission_name, + "6 Drop Pods", + SC2_RACESWAP_LOC_ID_OFFSET + 5104, + LocationType.EXTRA, + logic.zerg_gates_of_hell_requirement, + ), + make_location_data( + SC2Mission.GATES_OF_HELL_Z.mission_name, + "8 Drop Pods", + SC2_RACESWAP_LOC_ID_OFFSET + 5105, + LocationType.CHALLENGE, + logic.zerg_gates_of_hell_requirement, + ), + make_location_data( + SC2Mission.GATES_OF_HELL_Z.mission_name, + "Southwest Spore Cannon", + SC2_RACESWAP_LOC_ID_OFFSET + 5106, + LocationType.EXTRA, + logic.zerg_gates_of_hell_requirement, + ), + make_location_data( + SC2Mission.GATES_OF_HELL_Z.mission_name, + "Northwest Spore Cannon", + SC2_RACESWAP_LOC_ID_OFFSET + 5107, + LocationType.EXTRA, + logic.zerg_gates_of_hell_requirement, + ), + make_location_data( + SC2Mission.GATES_OF_HELL_Z.mission_name, + "Northeast Spore Cannon", + SC2_RACESWAP_LOC_ID_OFFSET + 5108, + LocationType.EXTRA, + logic.zerg_gates_of_hell_requirement, + ), + make_location_data( + SC2Mission.GATES_OF_HELL_Z.mission_name, + "East Spore Cannon", + SC2_RACESWAP_LOC_ID_OFFSET + 5109, + LocationType.EXTRA, + logic.zerg_gates_of_hell_requirement, + ), + make_location_data( + SC2Mission.GATES_OF_HELL_Z.mission_name, + "Southeast Spore Cannon", + SC2_RACESWAP_LOC_ID_OFFSET + 5110, + LocationType.EXTRA, + logic.zerg_gates_of_hell_requirement, + ), + make_location_data( + SC2Mission.GATES_OF_HELL_Z.mission_name, + "Expansion Spore Cannon", + SC2_RACESWAP_LOC_ID_OFFSET + 5111, + LocationType.EXTRA, + logic.zerg_gates_of_hell_requirement, + ), + make_location_data( + SC2Mission.GATES_OF_HELL_P.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 5200, + LocationType.VICTORY, + logic.protoss_gates_of_hell_requirement, + ), + make_location_data( + SC2Mission.GATES_OF_HELL_P.mission_name, + "Large Army", + SC2_RACESWAP_LOC_ID_OFFSET + 5201, + LocationType.VANILLA, + logic.protoss_gates_of_hell_requirement, + ), + make_location_data( + SC2Mission.GATES_OF_HELL_P.mission_name, + "2 Drop Pods", + SC2_RACESWAP_LOC_ID_OFFSET + 5202, + LocationType.VANILLA, + logic.protoss_gates_of_hell_requirement, + ), + make_location_data( + SC2Mission.GATES_OF_HELL_P.mission_name, + "4 Drop Pods", + SC2_RACESWAP_LOC_ID_OFFSET + 5203, + LocationType.VANILLA, + logic.protoss_gates_of_hell_requirement, + ), + make_location_data( + SC2Mission.GATES_OF_HELL_P.mission_name, + "6 Drop Pods", + SC2_RACESWAP_LOC_ID_OFFSET + 5204, + LocationType.EXTRA, + logic.protoss_gates_of_hell_requirement, + ), + make_location_data( + SC2Mission.GATES_OF_HELL_P.mission_name, + "8 Drop Pods", + SC2_RACESWAP_LOC_ID_OFFSET + 5205, + LocationType.CHALLENGE, + logic.protoss_gates_of_hell_requirement, + ), + make_location_data( + SC2Mission.GATES_OF_HELL_P.mission_name, + "Southwest Spore Cannon", + SC2_RACESWAP_LOC_ID_OFFSET + 5206, + LocationType.EXTRA, + logic.protoss_gates_of_hell_requirement, + ), + make_location_data( + SC2Mission.GATES_OF_HELL_P.mission_name, + "Northwest Spore Cannon", + SC2_RACESWAP_LOC_ID_OFFSET + 5207, + LocationType.EXTRA, + logic.protoss_gates_of_hell_requirement, + ), + make_location_data( + SC2Mission.GATES_OF_HELL_P.mission_name, + "Northeast Spore Cannon", + SC2_RACESWAP_LOC_ID_OFFSET + 5208, + LocationType.EXTRA, + logic.protoss_gates_of_hell_requirement, + ), + make_location_data( + SC2Mission.GATES_OF_HELL_P.mission_name, + "East Spore Cannon", + SC2_RACESWAP_LOC_ID_OFFSET + 5209, + LocationType.EXTRA, + logic.protoss_gates_of_hell_requirement, + ), + make_location_data( + SC2Mission.GATES_OF_HELL_P.mission_name, + "Southeast Spore Cannon", + SC2_RACESWAP_LOC_ID_OFFSET + 5210, + LocationType.EXTRA, + logic.protoss_gates_of_hell_requirement, + ), + make_location_data( + SC2Mission.GATES_OF_HELL_P.mission_name, + "Expansion Spore Cannon", + SC2_RACESWAP_LOC_ID_OFFSET + 5211, + LocationType.EXTRA, + logic.protoss_gates_of_hell_requirement, + ), + make_location_data( + SC2Mission.SHATTER_THE_SKY_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 5500, + LocationType.VICTORY, + logic.zerg_competent_comp_competent_aa, + ), + make_location_data( + SC2Mission.SHATTER_THE_SKY_Z.mission_name, + "Close Coolant Tower", + SC2_RACESWAP_LOC_ID_OFFSET + 5501, + LocationType.VANILLA, + logic.zerg_competent_comp_competent_aa, + ), + make_location_data( + SC2Mission.SHATTER_THE_SKY_Z.mission_name, + "Northwest Coolant Tower", + SC2_RACESWAP_LOC_ID_OFFSET + 5502, + LocationType.VANILLA, + logic.zerg_competent_comp_competent_aa, + ), + make_location_data( + SC2Mission.SHATTER_THE_SKY_Z.mission_name, + "Southeast Coolant Tower", + SC2_RACESWAP_LOC_ID_OFFSET + 5503, + LocationType.VANILLA, + logic.zerg_competent_comp_competent_aa, + ), + make_location_data( + SC2Mission.SHATTER_THE_SKY_Z.mission_name, + "Southwest Coolant Tower", + SC2_RACESWAP_LOC_ID_OFFSET + 5504, + LocationType.VANILLA, + logic.zerg_competent_comp_competent_aa, + ), + make_location_data( + SC2Mission.SHATTER_THE_SKY_Z.mission_name, + "Leviathan", + SC2_RACESWAP_LOC_ID_OFFSET + 5505, + LocationType.VANILLA, + logic.zerg_competent_comp_competent_aa, + hard_rule=logic.zerg_any_anti_air, + ), + make_location_data( + SC2Mission.SHATTER_THE_SKY_Z.mission_name, + "East Hatchery", + SC2_RACESWAP_LOC_ID_OFFSET + 5506, + LocationType.EXTRA, + logic.zerg_competent_comp_competent_aa, + ), + make_location_data( + SC2Mission.SHATTER_THE_SKY_Z.mission_name, + "North Hatchery", + SC2_RACESWAP_LOC_ID_OFFSET + 5507, + LocationType.EXTRA, + logic.zerg_competent_comp_competent_aa, + ), + make_location_data( + SC2Mission.SHATTER_THE_SKY_Z.mission_name, + "Mid Hatchery", + SC2_RACESWAP_LOC_ID_OFFSET + 5508, + LocationType.EXTRA, + logic.zerg_competent_comp_competent_aa, + ), + make_location_data( + SC2Mission.SHATTER_THE_SKY_P.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 5600, + LocationType.VICTORY, + lambda state: logic.protoss_competent_comp(state) + and logic.protoss_army_weapon_armor_upgrade_min_level(state) >= 2, + ), + make_location_data( + SC2Mission.SHATTER_THE_SKY_P.mission_name, + "Close Coolant Tower", + SC2_RACESWAP_LOC_ID_OFFSET + 5601, + LocationType.VANILLA, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.SHATTER_THE_SKY_P.mission_name, + "Northwest Coolant Tower", + SC2_RACESWAP_LOC_ID_OFFSET + 5602, + LocationType.VANILLA, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.SHATTER_THE_SKY_P.mission_name, + "Southeast Coolant Tower", + SC2_RACESWAP_LOC_ID_OFFSET + 5603, + LocationType.VANILLA, + lambda state: logic.protoss_competent_comp(state) + and logic.protoss_army_weapon_armor_upgrade_min_level(state) >= 2, + ), + make_location_data( + SC2Mission.SHATTER_THE_SKY_P.mission_name, + "Southwest Coolant Tower", + SC2_RACESWAP_LOC_ID_OFFSET + 5604, + LocationType.VANILLA, + lambda state: logic.protoss_competent_comp(state) + and logic.protoss_army_weapon_armor_upgrade_min_level(state) >= 2, + ), + make_location_data( + SC2Mission.SHATTER_THE_SKY_P.mission_name, + "Leviathan", + SC2_RACESWAP_LOC_ID_OFFSET + 5605, + LocationType.VANILLA, + lambda state: logic.protoss_competent_comp(state) + and logic.protoss_army_weapon_armor_upgrade_min_level(state) >= 2, + hard_rule=logic.protoss_any_anti_air_unit_or_soa_any_protoss, + ), + make_location_data( + SC2Mission.SHATTER_THE_SKY_P.mission_name, + "East Hatchery", + SC2_RACESWAP_LOC_ID_OFFSET + 5606, + LocationType.EXTRA, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.SHATTER_THE_SKY_P.mission_name, + "North Hatchery", + SC2_RACESWAP_LOC_ID_OFFSET + 5607, + LocationType.EXTRA, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.SHATTER_THE_SKY_P.mission_name, + "Mid Hatchery", + SC2_RACESWAP_LOC_ID_OFFSET + 5608, + LocationType.EXTRA, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.ALL_IN_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 5700, + LocationType.VICTORY, + logic.zerg_all_in_requirement, + ), + make_location_data( + SC2Mission.ALL_IN_Z.mission_name, + "First Kerrigan Attack", + SC2_RACESWAP_LOC_ID_OFFSET + 5701, + LocationType.EXTRA, + logic.zerg_all_in_requirement, + ), + make_location_data( + SC2Mission.ALL_IN_Z.mission_name, + "Second Kerrigan Attack", + SC2_RACESWAP_LOC_ID_OFFSET + 5702, + LocationType.EXTRA, + logic.zerg_all_in_requirement, + ), + make_location_data( + SC2Mission.ALL_IN_Z.mission_name, + "Third Kerrigan Attack", + SC2_RACESWAP_LOC_ID_OFFSET + 5703, + LocationType.EXTRA, + logic.zerg_all_in_requirement, + ), + make_location_data( + SC2Mission.ALL_IN_Z.mission_name, + "Fourth Kerrigan Attack", + SC2_RACESWAP_LOC_ID_OFFSET + 5704, + LocationType.EXTRA, + logic.zerg_all_in_requirement, + ), + make_location_data( + SC2Mission.ALL_IN_Z.mission_name, + "Fifth Kerrigan Attack", + SC2_RACESWAP_LOC_ID_OFFSET + 5705, + LocationType.EXTRA, + logic.zerg_all_in_requirement, + ), + make_location_data( + SC2Mission.ALL_IN_P.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 5800, + LocationType.VICTORY, + logic.protoss_all_in_requirement, + ), + make_location_data( + SC2Mission.ALL_IN_P.mission_name, + "First Kerrigan Attack", + SC2_RACESWAP_LOC_ID_OFFSET + 5801, + LocationType.EXTRA, + logic.protoss_all_in_requirement, + ), + make_location_data( + SC2Mission.ALL_IN_P.mission_name, + "Second Kerrigan Attack", + SC2_RACESWAP_LOC_ID_OFFSET + 5802, + LocationType.EXTRA, + logic.protoss_all_in_requirement, + ), + make_location_data( + SC2Mission.ALL_IN_P.mission_name, + "Third Kerrigan Attack", + SC2_RACESWAP_LOC_ID_OFFSET + 5803, + LocationType.EXTRA, + logic.protoss_all_in_requirement, + ), + make_location_data( + SC2Mission.ALL_IN_P.mission_name, + "Fourth Kerrigan Attack", + SC2_RACESWAP_LOC_ID_OFFSET + 5804, + LocationType.EXTRA, + logic.protoss_all_in_requirement, + ), + make_location_data( + SC2Mission.ALL_IN_P.mission_name, + "Fifth Kerrigan Attack", + SC2_RACESWAP_LOC_ID_OFFSET + 5805, + LocationType.EXTRA, + logic.protoss_all_in_requirement, + ), + make_location_data( + SC2Mission.LAB_RAT_T.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 5900, + LocationType.VICTORY, + logic.terran_common_unit, + ), + make_location_data( + SC2Mission.LAB_RAT_T.mission_name, + "Gather Minerals", + SC2_RACESWAP_LOC_ID_OFFSET + 5901, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.LAB_RAT_T.mission_name, + "South Marine Group", + SC2_RACESWAP_LOC_ID_OFFSET + 5902, + LocationType.VANILLA, + lambda state: adv_tactics or logic.terran_common_unit(state), + ), + make_location_data( + SC2Mission.LAB_RAT_T.mission_name, + "East Marine Group", + SC2_RACESWAP_LOC_ID_OFFSET + 5903, + LocationType.VANILLA, + lambda state: adv_tactics or logic.terran_common_unit(state), + ), + make_location_data( + SC2Mission.LAB_RAT_T.mission_name, + "West Marine Group", + SC2_RACESWAP_LOC_ID_OFFSET + 5904, + LocationType.VANILLA, + lambda state: adv_tactics or logic.terran_common_unit(state), + ), + make_location_data( + SC2Mission.LAB_RAT_T.mission_name, + "Command Center", + SC2_RACESWAP_LOC_ID_OFFSET + 5905, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.LAB_RAT_T.mission_name, + "Supply Depot", + SC2_RACESWAP_LOC_ID_OFFSET + 5906, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.LAB_RAT_T.mission_name, + "Gas Turrets", + SC2_RACESWAP_LOC_ID_OFFSET + 5907, + LocationType.EXTRA, + lambda state: adv_tactics or logic.terran_common_unit(state), + ), + make_location_data( + SC2Mission.LAB_RAT_T.mission_name, + "Win In Under 10 Minutes", + SC2_RACESWAP_LOC_ID_OFFSET + 5908, + LocationType.CHALLENGE, + lambda state: logic.terran_common_unit(state) + and logic.terran_early_tech(state), + flags=LocationFlag.SPEEDRUN, + ), + make_location_data( + SC2Mission.LAB_RAT_P.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 6000, + LocationType.VICTORY, + logic.protoss_common_unit, + ), + make_location_data( + SC2Mission.LAB_RAT_P.mission_name, + "Gather Minerals", + SC2_RACESWAP_LOC_ID_OFFSET + 6001, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.LAB_RAT_P.mission_name, + "South Zealot Group", + SC2_RACESWAP_LOC_ID_OFFSET + 6002, + LocationType.VANILLA, + lambda state: adv_tactics or logic.protoss_common_unit(state), + ), + make_location_data( + SC2Mission.LAB_RAT_P.mission_name, + "East Zealot Group", + SC2_RACESWAP_LOC_ID_OFFSET + 6003, + LocationType.VANILLA, + lambda state: adv_tactics or logic.protoss_common_unit(state), + ), + make_location_data( + SC2Mission.LAB_RAT_P.mission_name, + "West Zealot Group", + SC2_RACESWAP_LOC_ID_OFFSET + 6004, + LocationType.VANILLA, + lambda state: adv_tactics or logic.protoss_common_unit(state), + ), + make_location_data( + SC2Mission.LAB_RAT_P.mission_name, + "Nexus", + SC2_RACESWAP_LOC_ID_OFFSET + 6005, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.LAB_RAT_P.mission_name, + "Pylon", + SC2_RACESWAP_LOC_ID_OFFSET + 6006, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.LAB_RAT_P.mission_name, + "Gas Turrets", + SC2_RACESWAP_LOC_ID_OFFSET + 6007, + LocationType.EXTRA, + lambda state: adv_tactics or logic.protoss_common_unit(state), + ), + make_location_data( + SC2Mission.LAB_RAT_P.mission_name, + "Win In Under 10 Minutes", + SC2_RACESWAP_LOC_ID_OFFSET + 6008, + LocationType.CHALLENGE, + logic.protoss_common_unit, + flags=LocationFlag.SPEEDRUN, + ), + make_location_data( + SC2Mission.RENDEZVOUS_T.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 6300, + LocationType.VICTORY, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_basic_anti_air(state) + and logic.terran_defense_rating(state, False, False) >= 3 + ), + ), + make_location_data( + SC2Mission.RENDEZVOUS_T.mission_name, + "Right Group", + SC2_RACESWAP_LOC_ID_OFFSET + 6301, + LocationType.VANILLA, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_basic_anti_air(state) + and logic.terran_defense_rating(state, False, False) >= 3 + ), + ), + make_location_data( + SC2Mission.RENDEZVOUS_T.mission_name, + "Center Group", + SC2_RACESWAP_LOC_ID_OFFSET + 6302, + LocationType.VANILLA, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_basic_anti_air(state) + and logic.terran_defense_rating(state, False, False) >= 3 + ), + ), + make_location_data( + SC2Mission.RENDEZVOUS_T.mission_name, + "Left Group", + SC2_RACESWAP_LOC_ID_OFFSET + 6303, + LocationType.VANILLA, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_basic_anti_air(state) + and logic.terran_defense_rating(state, False, False) >= 3 + ), + ), + make_location_data( + SC2Mission.RENDEZVOUS_T.mission_name, + "Hold Out Finished", + SC2_RACESWAP_LOC_ID_OFFSET + 6304, + LocationType.EXTRA, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_basic_anti_air(state) + and logic.terran_defense_rating(state, False, False) >= 3 + ), + ), + make_location_data( + SC2Mission.RENDEZVOUS_T.mission_name, + "Kill All Buildings Before Reinforcements", + SC2_RACESWAP_LOC_ID_OFFSET + 6305, + LocationType.MASTERY, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_competent_comp(state) + and logic.terran_defense_rating(state, False, False) >= 3 + and logic.terran_power_rating(state) >= 5 + ), + flags=LocationFlag.SPEEDRUN, + ), + make_location_data( + SC2Mission.RENDEZVOUS_P.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 6400, + LocationType.VICTORY, + lambda state: ( + logic.protoss_common_unit(state) + and logic.protoss_basic_anti_air(state) + and logic.protoss_defense_rating(state, False) >= 3 + ), + ), + make_location_data( + SC2Mission.RENDEZVOUS_P.mission_name, + "Right Group", + SC2_RACESWAP_LOC_ID_OFFSET + 6401, + LocationType.VANILLA, + lambda state: ( + logic.protoss_common_unit(state) + and logic.protoss_basic_anti_air(state) + and logic.protoss_defense_rating(state, False) >= 3 + ), + ), + make_location_data( + SC2Mission.RENDEZVOUS_P.mission_name, + "Center Group", + SC2_RACESWAP_LOC_ID_OFFSET + 6402, + LocationType.VANILLA, + lambda state: ( + logic.protoss_common_unit(state) + and logic.protoss_basic_anti_air(state) + and logic.protoss_defense_rating(state, False) >= 3 + ), + ), + make_location_data( + SC2Mission.RENDEZVOUS_P.mission_name, + "Left Group", + SC2_RACESWAP_LOC_ID_OFFSET + 6403, + LocationType.VANILLA, + lambda state: ( + logic.protoss_common_unit(state) + and logic.protoss_basic_anti_air(state) + and logic.protoss_defense_rating(state, False) >= 3 + ), + ), + make_location_data( + SC2Mission.RENDEZVOUS_P.mission_name, + "Hold Out Finished", + SC2_RACESWAP_LOC_ID_OFFSET + 6404, + LocationType.EXTRA, + lambda state: ( + logic.protoss_common_unit(state) + and logic.protoss_basic_anti_air(state) + and logic.protoss_defense_rating(state, False) >= 3 + ), + ), + make_location_data( + SC2Mission.RENDEZVOUS_P.mission_name, + "Kill All Buildings Before Reinforcements", + SC2_RACESWAP_LOC_ID_OFFSET + 6405, + LocationType.MASTERY, + lambda state: ( + logic.protoss_competent_comp(state) + and logic.protoss_defense_rating(state, False) >= 3 + and logic.protoss_power_rating(state) >= 5 + ), + flags=LocationFlag.SPEEDRUN, + ), + make_location_data( + SC2Mission.HARVEST_OF_SCREAMS_T.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 6500, + LocationType.VICTORY, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.HARVEST_OF_SCREAMS_T.mission_name, + "First Ursadon Matriarch", + SC2_RACESWAP_LOC_ID_OFFSET + 6501, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.HARVEST_OF_SCREAMS_T.mission_name, + "North Ursadon Matriarch", + SC2_RACESWAP_LOC_ID_OFFSET + 6502, + LocationType.VANILLA, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_moderate_anti_air(state) + ), + ), + make_location_data( + SC2Mission.HARVEST_OF_SCREAMS_T.mission_name, + "West Ursadon Matriarch", + SC2_RACESWAP_LOC_ID_OFFSET + 6503, + LocationType.VANILLA, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_moderate_anti_air(state) + ), + ), + make_location_data( + SC2Mission.HARVEST_OF_SCREAMS_T.mission_name, + "Lost Base", + SC2_RACESWAP_LOC_ID_OFFSET + 6504, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.HARVEST_OF_SCREAMS_T.mission_name, + "Northeast Psi-link Spire", + SC2_RACESWAP_LOC_ID_OFFSET + 6505, + LocationType.EXTRA, + lambda state: logic.terran_common_unit(state) or adv_tactics, + ), + make_location_data( + SC2Mission.HARVEST_OF_SCREAMS_T.mission_name, + "Northwest Psi-link Spire", + SC2_RACESWAP_LOC_ID_OFFSET + 6506, + LocationType.EXTRA, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_moderate_anti_air(state) + ), + ), + make_location_data( + SC2Mission.HARVEST_OF_SCREAMS_T.mission_name, + "Southwest Psi-link Spire", + SC2_RACESWAP_LOC_ID_OFFSET + 6507, + LocationType.EXTRA, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.HARVEST_OF_SCREAMS_T.mission_name, + "Nafash", + SC2_RACESWAP_LOC_ID_OFFSET + 6508, + LocationType.EXTRA, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_moderate_anti_air(state) + ), + ), + make_location_data( + SC2Mission.HARVEST_OF_SCREAMS_T.mission_name, + "20 Unfrozen Structures", + SC2_RACESWAP_LOC_ID_OFFSET + 6509, + LocationType.CHALLENGE, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.HARVEST_OF_SCREAMS_P.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 6600, + LocationType.VICTORY, + lambda state: ( + logic.protoss_common_unit(state) + and logic.protoss_anti_armor_anti_air(state) + ), + ), + make_location_data( + SC2Mission.HARVEST_OF_SCREAMS_P.mission_name, + "First Ursadon Matriarch", + SC2_RACESWAP_LOC_ID_OFFSET + 6601, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.HARVEST_OF_SCREAMS_P.mission_name, + "North Ursadon Matriarch", + SC2_RACESWAP_LOC_ID_OFFSET + 6602, + LocationType.VANILLA, + logic.protoss_common_unit_basic_aa, + ), + make_location_data( + SC2Mission.HARVEST_OF_SCREAMS_P.mission_name, + "West Ursadon Matriarch", + SC2_RACESWAP_LOC_ID_OFFSET + 6603, + LocationType.VANILLA, + logic.protoss_common_unit_basic_aa, + ), + make_location_data( + SC2Mission.HARVEST_OF_SCREAMS_P.mission_name, + "Lost Base", + SC2_RACESWAP_LOC_ID_OFFSET + 6604, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.HARVEST_OF_SCREAMS_P.mission_name, + "Northeast Psi-link Spire", + SC2_RACESWAP_LOC_ID_OFFSET + 6605, + LocationType.EXTRA, + lambda state: logic.protoss_common_unit(state) or adv_tactics, + ), + make_location_data( + SC2Mission.HARVEST_OF_SCREAMS_P.mission_name, + "Northwest Psi-link Spire", + SC2_RACESWAP_LOC_ID_OFFSET + 6606, + LocationType.EXTRA, + lambda state: ( + logic.protoss_common_unit(state) and logic.protoss_basic_anti_air(state) + ), + ), + make_location_data( + SC2Mission.HARVEST_OF_SCREAMS_P.mission_name, + "Southwest Psi-link Spire", + SC2_RACESWAP_LOC_ID_OFFSET + 6607, + LocationType.EXTRA, + lambda state: ( + logic.protoss_common_unit(state) + and logic.protoss_anti_armor_anti_air(state) + ), + ), + make_location_data( + SC2Mission.HARVEST_OF_SCREAMS_P.mission_name, + "Nafash", + SC2_RACESWAP_LOC_ID_OFFSET + 6608, + LocationType.EXTRA, + lambda state: ( + logic.protoss_common_unit(state) and logic.protoss_basic_anti_air(state) + ), + ), + make_location_data( + SC2Mission.HARVEST_OF_SCREAMS_P.mission_name, + "20 Unfrozen Structures", + SC2_RACESWAP_LOC_ID_OFFSET + 6609, + LocationType.CHALLENGE, + lambda state: ( + logic.protoss_common_unit(state) and logic.protoss_basic_anti_air(state) + ), + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER_T.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 6700, + LocationType.VICTORY, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER_T.mission_name, + "East Stasis Chamber", + SC2_RACESWAP_LOC_ID_OFFSET + 6701, + LocationType.VANILLA, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER_T.mission_name, + "Center Stasis Chamber", + SC2_RACESWAP_LOC_ID_OFFSET + 6702, + LocationType.VANILLA, + lambda state: logic.terran_common_unit(state) or adv_tactics, + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER_T.mission_name, + "West Stasis Chamber", + SC2_RACESWAP_LOC_ID_OFFSET + 6703, + LocationType.VANILLA, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER_T.mission_name, + "Destroy 4 Shuttles", + SC2_RACESWAP_LOC_ID_OFFSET + 6704, + LocationType.EXTRA, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER_T.mission_name, + "Frozen Expansion", + SC2_RACESWAP_LOC_ID_OFFSET + 6705, + LocationType.EXTRA, + logic.terran_common_unit, + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER_T.mission_name, + "Southwest Frozen Group", + SC2_RACESWAP_LOC_ID_OFFSET + 6706, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER_T.mission_name, + "Southeast Frozen Group", + SC2_RACESWAP_LOC_ID_OFFSET + 6707, + LocationType.EXTRA, + lambda state: logic.terran_common_unit(state) or adv_tactics, + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER_T.mission_name, + "West Frozen Group", + SC2_RACESWAP_LOC_ID_OFFSET + 6708, + LocationType.EXTRA, + lambda state: logic.terran_common_unit(state) + and logic.terran_competent_anti_air(state), + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER_T.mission_name, + "East Frozen Group", + SC2_RACESWAP_LOC_ID_OFFSET + 6709, + LocationType.EXTRA, + lambda state: logic.terran_common_unit(state) + and logic.terran_competent_anti_air(state), + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER_T.mission_name, + "West Launch Bay", + SC2_RACESWAP_LOC_ID_OFFSET + 6710, + LocationType.CHALLENGE, + lambda state: logic.terran_beats_protoss_deathball(state) + and logic.terran_common_unit(state), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER_T.mission_name, + "Center Launch Bay", + SC2_RACESWAP_LOC_ID_OFFSET + 6711, + LocationType.CHALLENGE, + lambda state: logic.terran_beats_protoss_deathball(state) + and logic.terran_competent_ground_to_air(state) + and logic.terran_common_unit(state), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER_T.mission_name, + "East Launch Bay", + SC2_RACESWAP_LOC_ID_OFFSET + 6712, + LocationType.CHALLENGE, + lambda state: logic.terran_beats_protoss_deathball(state) + and logic.terran_competent_ground_to_air(state) + and logic.terran_common_unit(state), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER_P.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 6800, + LocationType.VICTORY, + lambda state: ( + logic.protoss_common_unit(state) + and logic.protoss_anti_armor_anti_air(state) + ), + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER_P.mission_name, + "East Stasis Chamber", + SC2_RACESWAP_LOC_ID_OFFSET + 6801, + LocationType.VANILLA, + lambda state: ( + logic.protoss_common_unit(state) + and logic.protoss_anti_armor_anti_air(state) + ), + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER_P.mission_name, + "Center Stasis Chamber", + SC2_RACESWAP_LOC_ID_OFFSET + 6802, + LocationType.VANILLA, + lambda state: logic.protoss_common_unit(state) or adv_tactics, + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER_P.mission_name, + "West Stasis Chamber", + SC2_RACESWAP_LOC_ID_OFFSET + 6803, + LocationType.VANILLA, + lambda state: ( + logic.protoss_common_unit(state) + and logic.protoss_anti_armor_anti_air(state) + ), + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER_P.mission_name, + "Destroy 4 Shuttles", + SC2_RACESWAP_LOC_ID_OFFSET + 6804, + LocationType.EXTRA, + lambda state: ( + logic.protoss_common_unit(state) + and logic.protoss_anti_armor_anti_air(state) + ), + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER_P.mission_name, + "Frozen Expansion", + SC2_RACESWAP_LOC_ID_OFFSET + 6805, + LocationType.EXTRA, + logic.protoss_common_unit, + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER_P.mission_name, + "Southwest Frozen Group", + SC2_RACESWAP_LOC_ID_OFFSET + 6806, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER_P.mission_name, + "Southeast Frozen Group", + SC2_RACESWAP_LOC_ID_OFFSET + 6807, + LocationType.EXTRA, + lambda state: logic.protoss_common_unit(state) or adv_tactics, + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER_P.mission_name, + "West Frozen Group", + SC2_RACESWAP_LOC_ID_OFFSET + 6808, + LocationType.EXTRA, + lambda state: ( + logic.protoss_common_unit(state) + and logic.protoss_anti_armor_anti_air(state) + ), + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER_P.mission_name, + "East Frozen Group", + SC2_RACESWAP_LOC_ID_OFFSET + 6809, + LocationType.EXTRA, + lambda state: ( + logic.protoss_common_unit(state) + and logic.protoss_anti_armor_anti_air(state) + ), + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER_P.mission_name, + "West Launch Bay", + SC2_RACESWAP_LOC_ID_OFFSET + 6810, + LocationType.CHALLENGE, + logic.protoss_competent_comp, + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER_P.mission_name, + "Center Launch Bay", + SC2_RACESWAP_LOC_ID_OFFSET + 6811, + LocationType.CHALLENGE, + logic.protoss_competent_comp, + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.SHOOT_THE_MESSENGER_P.mission_name, + "East Launch Bay", + SC2_RACESWAP_LOC_ID_OFFSET + 6812, + LocationType.CHALLENGE, + logic.protoss_competent_comp, + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.DOMINATION_T.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 7100, + LocationType.VICTORY, + lambda state: logic.terran_common_unit(state) + and (logic.terran_basic_anti_air(state) or adv_tactics), + ), + make_location_data( + SC2Mission.DOMINATION_T.mission_name, + "Center Infested Command Center", + SC2_RACESWAP_LOC_ID_OFFSET + 7101, + LocationType.VANILLA, + logic.terran_common_unit, + ), + make_location_data( + SC2Mission.DOMINATION_T.mission_name, + "North Infested Command Center", + SC2_RACESWAP_LOC_ID_OFFSET + 7102, + LocationType.VANILLA, + logic.terran_common_unit, + ), + make_location_data( + SC2Mission.DOMINATION_T.mission_name, + "Repel Zagara", + SC2_RACESWAP_LOC_ID_OFFSET + 7103, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.DOMINATION_T.mission_name, + "Close Bunker", + SC2_RACESWAP_LOC_ID_OFFSET + 7104, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.DOMINATION_T.mission_name, + "South Bunker", + SC2_RACESWAP_LOC_ID_OFFSET + 7105, + LocationType.EXTRA, + lambda state: adv_tactics or logic.terran_common_unit(state), + ), + make_location_data( + SC2Mission.DOMINATION_T.mission_name, + "Southwest Bunker", + SC2_RACESWAP_LOC_ID_OFFSET + 7106, + LocationType.EXTRA, + logic.terran_common_unit, + ), + make_location_data( + SC2Mission.DOMINATION_T.mission_name, + "Southeast Bunker", + SC2_RACESWAP_LOC_ID_OFFSET + 7107, + LocationType.EXTRA, + lambda state: logic.terran_common_unit(state) + and (logic.terran_basic_anti_air(state) or adv_tactics), + ), + make_location_data( + SC2Mission.DOMINATION_T.mission_name, + "North Bunker", + SC2_RACESWAP_LOC_ID_OFFSET + 7108, + LocationType.EXTRA, + logic.terran_common_unit, + ), + make_location_data( + SC2Mission.DOMINATION_T.mission_name, + "Northeast Bunker", + SC2_RACESWAP_LOC_ID_OFFSET + 7109, + LocationType.EXTRA, + lambda state: logic.terran_common_unit(state) + and (logic.terran_basic_anti_air(state) or adv_tactics), + ), + make_location_data( + SC2Mission.DOMINATION_T.mission_name, + "Win Without 100 Eggs", + SC2_RACESWAP_LOC_ID_OFFSET + 7110, + LocationType.CHALLENGE, + logic.terran_competent_comp, + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.DOMINATION_P.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 7200, + LocationType.VICTORY, + lambda state: logic.protoss_common_unit(state) + and (adv_tactics or logic.protoss_basic_anti_air(state)), + ), + make_location_data( + SC2Mission.DOMINATION_P.mission_name, + "Center Infested Command Center", + SC2_RACESWAP_LOC_ID_OFFSET + 7201, + LocationType.VANILLA, + logic.protoss_common_unit, + ), + make_location_data( + SC2Mission.DOMINATION_P.mission_name, + "North Infested Command Center", + SC2_RACESWAP_LOC_ID_OFFSET + 7202, + LocationType.VANILLA, + logic.protoss_common_unit, + ), + make_location_data( + SC2Mission.DOMINATION_P.mission_name, + "Repel Zagara", + SC2_RACESWAP_LOC_ID_OFFSET + 7203, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.DOMINATION_P.mission_name, + "Close Templar", + SC2_RACESWAP_LOC_ID_OFFSET + 7204, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.DOMINATION_P.mission_name, + "South Templar", + SC2_RACESWAP_LOC_ID_OFFSET + 7205, + LocationType.EXTRA, + lambda state: adv_tactics or logic.protoss_common_unit(state), + ), + make_location_data( + SC2Mission.DOMINATION_P.mission_name, + "Southwest Templar", + SC2_RACESWAP_LOC_ID_OFFSET + 7206, + LocationType.EXTRA, + logic.protoss_common_unit, + ), + make_location_data( + SC2Mission.DOMINATION_P.mission_name, + "Southeast Templar", + SC2_RACESWAP_LOC_ID_OFFSET + 7207, + LocationType.EXTRA, + lambda state: logic.protoss_common_unit(state) + and (adv_tactics or logic.protoss_basic_anti_air(state)), + ), + make_location_data( + SC2Mission.DOMINATION_P.mission_name, + "North Templar", + SC2_RACESWAP_LOC_ID_OFFSET + 7208, + LocationType.EXTRA, + logic.protoss_common_unit, + ), + make_location_data( + SC2Mission.DOMINATION_P.mission_name, + "Northeast Templar", + SC2_RACESWAP_LOC_ID_OFFSET + 7209, + LocationType.EXTRA, + lambda state: logic.protoss_common_unit(state) + and (adv_tactics or logic.protoss_basic_anti_air(state)), + ), + make_location_data( + SC2Mission.DOMINATION_P.mission_name, + "Win Without 100 Eggs", + SC2_RACESWAP_LOC_ID_OFFSET + 7210, + LocationType.CHALLENGE, + logic.protoss_competent_comp, + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY_T.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 7300, + LocationType.VICTORY, + lambda state: ( + logic.terran_common_unit(state) and logic.terran_competent_comp(state) + ), + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY_T.mission_name, + "West Biomass", + SC2_RACESWAP_LOC_ID_OFFSET + 7301, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY_T.mission_name, + "North Biomass", + SC2_RACESWAP_LOC_ID_OFFSET + 7302, + LocationType.VANILLA, + lambda state: ( + logic.terran_common_unit(state) and logic.terran_competent_comp(state) + ), + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY_T.mission_name, + "South Biomass", + SC2_RACESWAP_LOC_ID_OFFSET + 7303, + LocationType.VANILLA, + lambda state: ( + logic.terran_common_unit(state) and logic.terran_competent_comp(state) + ), + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY_T.mission_name, + "Destroy 3 Gorgons", + SC2_RACESWAP_LOC_ID_OFFSET + 7304, + LocationType.EXTRA, + lambda state: ( + logic.terran_common_unit(state) and logic.terran_competent_comp(state) + ), + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY_T.mission_name, + "Close Rescue", + SC2_RACESWAP_LOC_ID_OFFSET + 7305, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY_T.mission_name, + "South Rescue", + SC2_RACESWAP_LOC_ID_OFFSET + 7306, + LocationType.EXTRA, + logic.terran_common_unit, + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY_T.mission_name, + "North Rescue", + SC2_RACESWAP_LOC_ID_OFFSET + 7307, + LocationType.EXTRA, + lambda state: ( + logic.terran_common_unit(state) and logic.terran_competent_comp(state) + ), + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY_T.mission_name, + "West Medic Rescue", + SC2_RACESWAP_LOC_ID_OFFSET + 7308, + LocationType.EXTRA, + lambda state: ( + logic.terran_common_unit(state) and logic.terran_competent_comp(state) + ), + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY_T.mission_name, + "East Medic Rescue", + SC2_RACESWAP_LOC_ID_OFFSET + 7309, + LocationType.EXTRA, + lambda state: ( + logic.terran_common_unit(state) and logic.terran_competent_comp(state) + ), + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY_T.mission_name, + "South Orbital Command Center", + SC2_RACESWAP_LOC_ID_OFFSET + 7310, + LocationType.CHALLENGE, + lambda state: ( + logic.terran_common_unit(state) and logic.terran_competent_comp(state) + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY_T.mission_name, + "Northwest Orbital Command Center", + SC2_RACESWAP_LOC_ID_OFFSET + 7311, + LocationType.CHALLENGE, + lambda state: ( + logic.terran_common_unit(state) and logic.terran_competent_comp(state) + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY_T.mission_name, + "Southeast Orbital Command Center", + SC2_RACESWAP_LOC_ID_OFFSET + 7312, + LocationType.CHALLENGE, + lambda state: ( + logic.terran_common_unit(state) and logic.terran_competent_comp(state) + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY_P.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 7400, + LocationType.VICTORY, + lambda state: ( + logic.protoss_common_unit(state) and logic.protoss_competent_comp(state) + ), + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY_P.mission_name, + "West Biomass", + SC2_RACESWAP_LOC_ID_OFFSET + 7401, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY_P.mission_name, + "North Biomass", + SC2_RACESWAP_LOC_ID_OFFSET + 7402, + LocationType.VANILLA, + lambda state: ( + logic.protoss_common_unit(state) and logic.protoss_competent_comp(state) + ), + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY_P.mission_name, + "South Biomass", + SC2_RACESWAP_LOC_ID_OFFSET + 7403, + LocationType.VANILLA, + lambda state: ( + logic.protoss_common_unit(state) and logic.protoss_competent_comp(state) + ), + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY_P.mission_name, + "Destroy 3 Gorgons", + SC2_RACESWAP_LOC_ID_OFFSET + 7404, + LocationType.EXTRA, + lambda state: ( + logic.protoss_common_unit(state) and logic.protoss_competent_comp(state) + ), + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY_P.mission_name, + "Close Rescue", + SC2_RACESWAP_LOC_ID_OFFSET + 7405, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY_P.mission_name, + "South Rescue", + SC2_RACESWAP_LOC_ID_OFFSET + 7406, + LocationType.EXTRA, + logic.protoss_common_unit, + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY_P.mission_name, + "North Rescue", + SC2_RACESWAP_LOC_ID_OFFSET + 7407, + LocationType.EXTRA, + lambda state: ( + logic.protoss_common_unit(state) and logic.protoss_competent_comp(state) + ), + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY_P.mission_name, + "West Energizer Rescue", + SC2_RACESWAP_LOC_ID_OFFSET + 7408, + LocationType.EXTRA, + lambda state: ( + logic.protoss_common_unit(state) and logic.protoss_competent_comp(state) + ), + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY_P.mission_name, + "East Energizer Rescue", + SC2_RACESWAP_LOC_ID_OFFSET + 7409, + LocationType.EXTRA, + lambda state: ( + logic.protoss_common_unit(state) and logic.protoss_competent_comp(state) + ), + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY_P.mission_name, + "South Orbital Command Center", + SC2_RACESWAP_LOC_ID_OFFSET + 7410, + LocationType.CHALLENGE, + lambda state: ( + logic.protoss_common_unit(state) and logic.protoss_competent_comp(state) + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY_P.mission_name, + "Northwest Orbital Command Center", + SC2_RACESWAP_LOC_ID_OFFSET + 7411, + LocationType.CHALLENGE, + lambda state: ( + logic.protoss_common_unit(state) and logic.protoss_competent_comp(state) + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.FIRE_IN_THE_SKY_P.mission_name, + "Southeast Orbital Command Center", + SC2_RACESWAP_LOC_ID_OFFSET + 7412, + LocationType.CHALLENGE, + lambda state: ( + logic.protoss_common_unit(state) and logic.protoss_competent_comp(state) + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.OLD_SOLDIERS_T.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 7500, + LocationType.VICTORY, + logic.terran_competent_comp, + ), + make_location_data( + SC2Mission.OLD_SOLDIERS_T.mission_name, + "East Science Lab", + SC2_RACESWAP_LOC_ID_OFFSET + 7501, + LocationType.VANILLA, + logic.terran_competent_comp, + ), + make_location_data( + SC2Mission.OLD_SOLDIERS_T.mission_name, + "North Science Lab", + SC2_RACESWAP_LOC_ID_OFFSET + 7502, + LocationType.VANILLA, + logic.terran_competent_comp, + ), + make_location_data( + SC2Mission.OLD_SOLDIERS_T.mission_name, + "Get Nuked", + SC2_RACESWAP_LOC_ID_OFFSET + 7503, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.OLD_SOLDIERS_T.mission_name, + "Entrance Gate", + SC2_RACESWAP_LOC_ID_OFFSET + 7504, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.OLD_SOLDIERS_T.mission_name, + "Citadel Gate", + SC2_RACESWAP_LOC_ID_OFFSET + 7505, + LocationType.EXTRA, + logic.terran_competent_comp, + ), + make_location_data( + SC2Mission.OLD_SOLDIERS_T.mission_name, + "South Expansion", + SC2_RACESWAP_LOC_ID_OFFSET + 7506, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.OLD_SOLDIERS_T.mission_name, + "Rich Mineral Expansion", + SC2_RACESWAP_LOC_ID_OFFSET + 7507, + LocationType.EXTRA, + logic.terran_competent_comp, + ), + make_location_data( + SC2Mission.OLD_SOLDIERS_P.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 7600, + LocationType.VICTORY, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.OLD_SOLDIERS_P.mission_name, + "East Science Lab", + SC2_RACESWAP_LOC_ID_OFFSET + 7601, + LocationType.VANILLA, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.OLD_SOLDIERS_P.mission_name, + "North Science Lab", + SC2_RACESWAP_LOC_ID_OFFSET + 7602, + LocationType.VANILLA, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.OLD_SOLDIERS_P.mission_name, + "Get Nuked", + SC2_RACESWAP_LOC_ID_OFFSET + 7603, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.OLD_SOLDIERS_P.mission_name, + "Entrance Gate", + SC2_RACESWAP_LOC_ID_OFFSET + 7604, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.OLD_SOLDIERS_P.mission_name, + "Citadel Gate", + SC2_RACESWAP_LOC_ID_OFFSET + 7605, + LocationType.EXTRA, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.OLD_SOLDIERS_P.mission_name, + "South Expansion", + SC2_RACESWAP_LOC_ID_OFFSET + 7606, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.OLD_SOLDIERS_P.mission_name, + "Rich Mineral Expansion", + SC2_RACESWAP_LOC_ID_OFFSET + 7607, + LocationType.EXTRA, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.WAKING_THE_ANCIENT_T.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 7700, + LocationType.VICTORY, + lambda state: ( + logic.terran_competent_comp(state) and logic.terran_common_unit(state) + ), + hard_rule=logic.terran_any_anti_air, + ), + make_location_data( + SC2Mission.WAKING_THE_ANCIENT_T.mission_name, + "Center Essence Pool", + SC2_RACESWAP_LOC_ID_OFFSET + 7701, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.WAKING_THE_ANCIENT_T.mission_name, + "East Essence Pool", + SC2_RACESWAP_LOC_ID_OFFSET + 7702, + LocationType.VANILLA, + lambda state: ( + logic.terran_common_unit(state) + and ( + adv_tactics + and logic.terran_basic_anti_air(state) + or logic.terran_competent_anti_air(state) + ) + ), + ), + make_location_data( + SC2Mission.WAKING_THE_ANCIENT_T.mission_name, + "South Essence Pool", + SC2_RACESWAP_LOC_ID_OFFSET + 7703, + LocationType.VANILLA, + lambda state: ( + logic.terran_common_unit(state) + and ( + adv_tactics + and logic.terran_basic_anti_air(state) + or logic.terran_competent_anti_air(state) + ) + ), + ), + make_location_data( + SC2Mission.WAKING_THE_ANCIENT_T.mission_name, + "Finish Feeding", + SC2_RACESWAP_LOC_ID_OFFSET + 7704, + LocationType.EXTRA, + lambda state: ( + logic.terran_competent_comp(state) and logic.terran_common_unit(state) + ), + hard_rule=logic.terran_any_anti_air, + ), + make_location_data( + SC2Mission.WAKING_THE_ANCIENT_T.mission_name, + "South Proxy Primal Hive", + SC2_RACESWAP_LOC_ID_OFFSET + 7705, + LocationType.CHALLENGE, + lambda state: ( + logic.terran_competent_comp(state) and logic.terran_common_unit(state) + ), + ), + make_location_data( + SC2Mission.WAKING_THE_ANCIENT_T.mission_name, + "East Proxy Primal Hive", + SC2_RACESWAP_LOC_ID_OFFSET + 7706, + LocationType.CHALLENGE, + lambda state: ( + logic.terran_competent_comp(state) and logic.terran_common_unit(state) + ), + ), + make_location_data( + SC2Mission.WAKING_THE_ANCIENT_T.mission_name, + "South Main Primal Hive", + SC2_RACESWAP_LOC_ID_OFFSET + 7707, + LocationType.CHALLENGE, + lambda state: ( + logic.terran_competent_comp(state) and logic.terran_common_unit(state) + ), + flags=LocationFlag.BASEBUST, + hard_rule=logic.terran_any_anti_air, + ), + make_location_data( + SC2Mission.WAKING_THE_ANCIENT_T.mission_name, + "East Main Primal Hive", + SC2_RACESWAP_LOC_ID_OFFSET + 7708, + LocationType.CHALLENGE, + lambda state: ( + logic.terran_competent_comp(state) and logic.terran_common_unit(state) + ), + flags=LocationFlag.BASEBUST, + hard_rule=logic.terran_any_anti_air, + ), + make_location_data( + SC2Mission.WAKING_THE_ANCIENT_T.mission_name, + "Flawless", + SC2_RACESWAP_LOC_ID_OFFSET + 7709, + LocationType.CHALLENGE, + lambda state: ( + logic.terran_competent_comp(state) + and logic.terran_common_unit(state) + and ( + # Fast unit + state.has_any( + ( + item_names.BANSHEE, + item_names.VULTURE, + item_names.DIAMONDBACK, + item_names.WARHOUND, + item_names.CYCLONE, + ), + player, + ) + or state.has_all( + (item_names.VALKYRIE, item_names.VALKYRIE_FLECHETTE_MISSILES), + player, + ) + or state.has_all( + ( + item_names.WRAITH, + item_names.WRAITH_ADVANCED_LASER_TECHNOLOGY, + ), + player, + ) + ) + ), + flags=LocationFlag.PREVENTATIVE, + hard_rule=logic.terran_any_anti_air, + ), + make_location_data( + SC2Mission.WAKING_THE_ANCIENT_P.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 7800, + LocationType.VICTORY, + logic.protoss_competent_comp, + hard_rule=logic.protoss_any_anti_air_unit, + ), + make_location_data( + SC2Mission.WAKING_THE_ANCIENT_P.mission_name, + "Center Essence Pool", + SC2_RACESWAP_LOC_ID_OFFSET + 7801, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.WAKING_THE_ANCIENT_P.mission_name, + "East Essence Pool", + SC2_RACESWAP_LOC_ID_OFFSET + 7802, + LocationType.VANILLA, + lambda state: ( + logic.protoss_common_unit(state) + and logic.protoss_anti_light_anti_air(state) + ), + ), + make_location_data( + SC2Mission.WAKING_THE_ANCIENT_P.mission_name, + "South Essence Pool", + SC2_RACESWAP_LOC_ID_OFFSET + 7803, + LocationType.VANILLA, + lambda state: ( + logic.protoss_common_unit(state) + and logic.protoss_anti_light_anti_air(state) + ), + ), + make_location_data( + SC2Mission.WAKING_THE_ANCIENT_P.mission_name, + "Finish Feeding", + SC2_RACESWAP_LOC_ID_OFFSET + 7804, + LocationType.EXTRA, + logic.protoss_competent_comp, + hard_rule=logic.protoss_any_anti_air_unit, + ), + make_location_data( + SC2Mission.WAKING_THE_ANCIENT_P.mission_name, + "South Proxy Primal Hive", + SC2_RACESWAP_LOC_ID_OFFSET + 7805, + LocationType.CHALLENGE, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.WAKING_THE_ANCIENT_P.mission_name, + "East Proxy Primal Hive", + SC2_RACESWAP_LOC_ID_OFFSET + 7806, + LocationType.CHALLENGE, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.WAKING_THE_ANCIENT_P.mission_name, + "South Main Primal Hive", + SC2_RACESWAP_LOC_ID_OFFSET + 7807, + LocationType.CHALLENGE, + logic.protoss_competent_comp, + flags=LocationFlag.BASEBUST, + hard_rule=logic.protoss_any_anti_air_unit, + ), + make_location_data( + SC2Mission.WAKING_THE_ANCIENT_P.mission_name, + "East Main Primal Hive", + SC2_RACESWAP_LOC_ID_OFFSET + 7808, + LocationType.CHALLENGE, + logic.protoss_competent_comp, + flags=LocationFlag.BASEBUST, + hard_rule=logic.protoss_any_anti_air_unit, + ), + make_location_data( + SC2Mission.WAKING_THE_ANCIENT_P.mission_name, + "Flawless", + SC2_RACESWAP_LOC_ID_OFFSET + 7809, + LocationType.CHALLENGE, + logic.protoss_competent_comp, + flags=LocationFlag.PREVENTATIVE, + hard_rule=logic.protoss_any_anti_air_unit, + ), + make_location_data( + SC2Mission.THE_CRUCIBLE_T.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 7900, + LocationType.VICTORY, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_defense_rating(state, True, True) >= 7 + and logic.terran_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.THE_CRUCIBLE_T.mission_name, + "Tyrannozor", + SC2_RACESWAP_LOC_ID_OFFSET + 7901, + LocationType.VANILLA, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_defense_rating(state, True, True) >= 7 + and logic.terran_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.THE_CRUCIBLE_T.mission_name, + "Reach the Pool", + SC2_RACESWAP_LOC_ID_OFFSET + 7902, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.THE_CRUCIBLE_T.mission_name, + "15 Minutes Remaining", + SC2_RACESWAP_LOC_ID_OFFSET + 7903, + LocationType.EXTRA, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_defense_rating(state, True, True) >= 7 + and logic.terran_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.THE_CRUCIBLE_T.mission_name, + "5 Minutes Remaining", + SC2_RACESWAP_LOC_ID_OFFSET + 7904, + LocationType.EXTRA, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_defense_rating(state, True, True) >= 7 + and logic.terran_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.THE_CRUCIBLE_T.mission_name, + "Pincer Attack", + SC2_RACESWAP_LOC_ID_OFFSET + 7905, + LocationType.EXTRA, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_defense_rating(state, True, True) >= 7 + and logic.terran_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.THE_CRUCIBLE_T.mission_name, + "Yagdra Claims Brakk's Pack", + SC2_RACESWAP_LOC_ID_OFFSET + 7906, + LocationType.EXTRA, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_defense_rating(state, True, True) >= 7 + and logic.terran_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.THE_CRUCIBLE_P.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 8000, + LocationType.VICTORY, + lambda state: ( + logic.protoss_common_unit(state) + and logic.protoss_defense_rating(state, True) >= 7 + and logic.protoss_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.THE_CRUCIBLE_P.mission_name, + "Tyrannozor", + SC2_RACESWAP_LOC_ID_OFFSET + 8001, + LocationType.VANILLA, + lambda state: ( + logic.protoss_common_unit(state) + and logic.protoss_defense_rating(state, True) >= 7 + and logic.protoss_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.THE_CRUCIBLE_P.mission_name, + "Reach the Pool", + SC2_RACESWAP_LOC_ID_OFFSET + 8002, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.THE_CRUCIBLE_P.mission_name, + "15 Minutes Remaining", + SC2_RACESWAP_LOC_ID_OFFSET + 8003, + LocationType.EXTRA, + lambda state: ( + logic.protoss_common_unit(state) + and logic.protoss_defense_rating(state, True) >= 7 + and logic.protoss_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.THE_CRUCIBLE_P.mission_name, + "5 Minutes Remaining", + SC2_RACESWAP_LOC_ID_OFFSET + 8004, + LocationType.EXTRA, + lambda state: ( + logic.protoss_common_unit(state) + and logic.protoss_defense_rating(state, True) >= 7 + and logic.protoss_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.THE_CRUCIBLE_P.mission_name, + "Pincer Attack", + SC2_RACESWAP_LOC_ID_OFFSET + 8005, + LocationType.EXTRA, + lambda state: ( + logic.protoss_common_unit(state) + and logic.protoss_defense_rating(state, True) >= 7 + and logic.protoss_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.THE_CRUCIBLE_P.mission_name, + "Yagdra Claims Brakk's Pack", + SC2_RACESWAP_LOC_ID_OFFSET + 8006, + LocationType.EXTRA, + lambda state: ( + logic.protoss_common_unit(state) + and logic.protoss_defense_rating(state, True) >= 7 + and logic.protoss_competent_anti_air(state) + ), + ), + make_location_data( + SC2Mission.INFESTED_T.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 8300, + LocationType.VICTORY, + lambda state: ( + logic.terran_common_unit(state) and logic.terran_competent_comp(state) + ), + hard_rule=logic.terran_any_anti_air, + ), + make_location_data( + SC2Mission.INFESTED_T.mission_name, + "East Science Facility", + SC2_RACESWAP_LOC_ID_OFFSET + 8301, + LocationType.VANILLA, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_moderate_anti_air(state) + ), + ), + make_location_data( + SC2Mission.INFESTED_T.mission_name, + "Center Science Facility", + SC2_RACESWAP_LOC_ID_OFFSET + 8302, + LocationType.VANILLA, + lambda state: ( + logic.terran_common_unit(state) and logic.terran_competent_comp(state) + ), + ), + make_location_data( + SC2Mission.INFESTED_T.mission_name, + "West Science Facility", + SC2_RACESWAP_LOC_ID_OFFSET + 8303, + LocationType.VANILLA, + lambda state: ( + logic.terran_common_unit(state) and logic.terran_competent_comp(state) + ), + ), + make_location_data( + SC2Mission.INFESTED_T.mission_name, + "First Intro Garrison", + SC2_RACESWAP_LOC_ID_OFFSET + 8304, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.INFESTED_T.mission_name, + "Second Intro Garrison", + SC2_RACESWAP_LOC_ID_OFFSET + 8305, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.INFESTED_T.mission_name, + "Base Garrison", + SC2_RACESWAP_LOC_ID_OFFSET + 8306, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.INFESTED_T.mission_name, + "East Garrison", + SC2_RACESWAP_LOC_ID_OFFSET + 8307, + LocationType.EXTRA, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_moderate_anti_air(state) + and (adv_tactics or logic.terran_infested_garrison_claimer(state)) + ), + ), + make_location_data( + SC2Mission.INFESTED_T.mission_name, + "Mid Garrison", + SC2_RACESWAP_LOC_ID_OFFSET + 8308, + LocationType.EXTRA, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_moderate_anti_air(state) + and (adv_tactics or logic.terran_infested_garrison_claimer(state)) + ), + ), + make_location_data( + SC2Mission.INFESTED_T.mission_name, + "North Garrison", + SC2_RACESWAP_LOC_ID_OFFSET + 8309, + LocationType.EXTRA, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_competent_comp(state) + and (adv_tactics or logic.terran_infested_garrison_claimer(state)) + ), + ), + make_location_data( + SC2Mission.INFESTED_T.mission_name, + "Close Southwest Garrison", + SC2_RACESWAP_LOC_ID_OFFSET + 8310, + LocationType.EXTRA, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_competent_comp(state) + and (adv_tactics or logic.terran_infested_garrison_claimer(state)) + ), + ), + make_location_data( + SC2Mission.INFESTED_T.mission_name, + "Far Southwest Garrison", + SC2_RACESWAP_LOC_ID_OFFSET + 8311, + LocationType.EXTRA, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_competent_comp(state) + and (adv_tactics or logic.terran_infested_garrison_claimer(state)) + ), + ), + make_location_data( + SC2Mission.INFESTED_P.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 8400, + LocationType.VICTORY, + logic.protoss_competent_comp, + hard_rule=logic.protoss_any_anti_air_unit_or_soa_any_protoss, + ), + make_location_data( + SC2Mission.INFESTED_P.mission_name, + "East Science Facility", + SC2_RACESWAP_LOC_ID_OFFSET + 8401, + LocationType.VANILLA, + lambda state: ( + logic.protoss_common_unit(state) and logic.protoss_basic_anti_air(state) + ), + ), + make_location_data( + SC2Mission.INFESTED_P.mission_name, + "Center Science Facility", + SC2_RACESWAP_LOC_ID_OFFSET + 8402, + LocationType.VANILLA, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.INFESTED_P.mission_name, + "West Science Facility", + SC2_RACESWAP_LOC_ID_OFFSET + 8403, + LocationType.VANILLA, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.INFESTED_P.mission_name, + "First Intro Garrison", + SC2_RACESWAP_LOC_ID_OFFSET + 8404, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.INFESTED_P.mission_name, + "Second Intro Garrison", + SC2_RACESWAP_LOC_ID_OFFSET + 8405, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.INFESTED_P.mission_name, + "Base Garrison", + SC2_RACESWAP_LOC_ID_OFFSET + 8406, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.INFESTED_P.mission_name, + "East Garrison", + SC2_RACESWAP_LOC_ID_OFFSET + 8407, + LocationType.EXTRA, + lambda state: ( + logic.protoss_common_unit(state) + and logic.protoss_basic_anti_air(state) + and (adv_tactics or logic.protoss_infested_garrison_claimer(state)) + ), + ), + make_location_data( + SC2Mission.INFESTED_P.mission_name, + "Mid Garrison", + SC2_RACESWAP_LOC_ID_OFFSET + 8408, + LocationType.EXTRA, + lambda state: ( + logic.protoss_common_unit(state) + and logic.protoss_basic_anti_air(state) + and (adv_tactics or logic.protoss_infested_garrison_claimer(state)) + ), + ), + make_location_data( + SC2Mission.INFESTED_P.mission_name, + "North Garrison", + SC2_RACESWAP_LOC_ID_OFFSET + 8409, + LocationType.EXTRA, + lambda state: ( + logic.protoss_common_unit(state) + and logic.protoss_competent_anti_air(state) + and (adv_tactics or logic.protoss_infested_garrison_claimer(state)) + ), + ), + make_location_data( + SC2Mission.INFESTED_P.mission_name, + "Close Southwest Garrison", + SC2_RACESWAP_LOC_ID_OFFSET + 8410, + LocationType.EXTRA, + lambda state: ( + logic.protoss_competent_comp(state) + and (adv_tactics or logic.protoss_infested_garrison_claimer(state)) + ), + ), + make_location_data( + SC2Mission.INFESTED_P.mission_name, + "Far Southwest Garrison", + SC2_RACESWAP_LOC_ID_OFFSET + 8411, + LocationType.EXTRA, + lambda state: ( + logic.protoss_competent_comp(state) + and (adv_tactics or logic.protoss_infested_garrison_claimer(state)) + ), + ), + make_location_data( + SC2Mission.HAND_OF_DARKNESS_T.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 8500, + LocationType.VICTORY, + logic.terran_hand_of_darkness_requirement, + ), + make_location_data( + SC2Mission.HAND_OF_DARKNESS_T.mission_name, + "North War Bot", + SC2_RACESWAP_LOC_ID_OFFSET + 8501, + LocationType.VANILLA, + logic.terran_hand_of_darkness_requirement, + ), + make_location_data( + SC2Mission.HAND_OF_DARKNESS_T.mission_name, + "South War Bot", + SC2_RACESWAP_LOC_ID_OFFSET + 8502, + LocationType.VANILLA, + logic.terran_hand_of_darkness_requirement, + ), + make_location_data( + SC2Mission.HAND_OF_DARKNESS_T.mission_name, + "Kill 1 Hybrid", + SC2_RACESWAP_LOC_ID_OFFSET + 8503, + LocationType.EXTRA, + logic.terran_hand_of_darkness_requirement, + ), + make_location_data( + SC2Mission.HAND_OF_DARKNESS_T.mission_name, + "Kill 2 Hybrid", + SC2_RACESWAP_LOC_ID_OFFSET + 8504, + LocationType.EXTRA, + logic.terran_hand_of_darkness_requirement, + ), + make_location_data( + SC2Mission.HAND_OF_DARKNESS_T.mission_name, + "Kill 3 Hybrid", + SC2_RACESWAP_LOC_ID_OFFSET + 8505, + LocationType.EXTRA, + logic.terran_hand_of_darkness_requirement, + ), + make_location_data( + SC2Mission.HAND_OF_DARKNESS_T.mission_name, + "Kill 4 Hybrid", + SC2_RACESWAP_LOC_ID_OFFSET + 8506, + LocationType.EXTRA, + logic.terran_hand_of_darkness_requirement, + ), + make_location_data( + SC2Mission.HAND_OF_DARKNESS_T.mission_name, + "Kill 5 Hybrid", + SC2_RACESWAP_LOC_ID_OFFSET + 8507, + LocationType.EXTRA, + logic.terran_hand_of_darkness_requirement, + ), + make_location_data( + SC2Mission.HAND_OF_DARKNESS_T.mission_name, + "Kill 6 Hybrid", + SC2_RACESWAP_LOC_ID_OFFSET + 8508, + LocationType.EXTRA, + logic.terran_hand_of_darkness_requirement, + ), + make_location_data( + SC2Mission.HAND_OF_DARKNESS_T.mission_name, + "Kill 7 Hybrid", + SC2_RACESWAP_LOC_ID_OFFSET + 8509, + LocationType.EXTRA, + logic.terran_hand_of_darkness_requirement, + ), + make_location_data( + SC2Mission.HAND_OF_DARKNESS_P.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 8600, + LocationType.VICTORY, + logic.protoss_hand_of_darkness_requirement, + ), + make_location_data( + SC2Mission.HAND_OF_DARKNESS_P.mission_name, + "North Stone Zealot", + SC2_RACESWAP_LOC_ID_OFFSET + 8601, + LocationType.VANILLA, + logic.protoss_hand_of_darkness_requirement, + ), + make_location_data( + SC2Mission.HAND_OF_DARKNESS_P.mission_name, + "South Stone Zealot", + SC2_RACESWAP_LOC_ID_OFFSET + 8602, + LocationType.VANILLA, + logic.protoss_hand_of_darkness_requirement, + ), + make_location_data( + SC2Mission.HAND_OF_DARKNESS_P.mission_name, + "Kill 1 Hybrid", + SC2_RACESWAP_LOC_ID_OFFSET + 8603, + LocationType.EXTRA, + logic.protoss_hand_of_darkness_requirement, + ), + make_location_data( + SC2Mission.HAND_OF_DARKNESS_P.mission_name, + "Kill 2 Hybrid", + SC2_RACESWAP_LOC_ID_OFFSET + 8604, + LocationType.EXTRA, + logic.protoss_hand_of_darkness_requirement, + ), + make_location_data( + SC2Mission.HAND_OF_DARKNESS_P.mission_name, + "Kill 3 Hybrid", + SC2_RACESWAP_LOC_ID_OFFSET + 8605, + LocationType.EXTRA, + logic.protoss_hand_of_darkness_requirement, + ), + make_location_data( + SC2Mission.HAND_OF_DARKNESS_P.mission_name, + "Kill 4 Hybrid", + SC2_RACESWAP_LOC_ID_OFFSET + 8606, + LocationType.EXTRA, + logic.protoss_hand_of_darkness_requirement, + ), + make_location_data( + SC2Mission.HAND_OF_DARKNESS_P.mission_name, + "Kill 5 Hybrid", + SC2_RACESWAP_LOC_ID_OFFSET + 8607, + LocationType.EXTRA, + logic.protoss_hand_of_darkness_requirement, + ), + make_location_data( + SC2Mission.HAND_OF_DARKNESS_P.mission_name, + "Kill 6 Hybrid", + SC2_RACESWAP_LOC_ID_OFFSET + 8608, + LocationType.EXTRA, + logic.protoss_hand_of_darkness_requirement, + ), + make_location_data( + SC2Mission.HAND_OF_DARKNESS_P.mission_name, + "Kill 7 Hybrid", + SC2_RACESWAP_LOC_ID_OFFSET + 8609, + LocationType.EXTRA, + logic.protoss_hand_of_darkness_requirement, + ), + make_location_data( + SC2Mission.PHANTOMS_OF_THE_VOID_T.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 8700, + LocationType.VICTORY, + logic.terran_competent_comp, + ), + make_location_data( + SC2Mission.PHANTOMS_OF_THE_VOID_T.mission_name, + "Northwest Crystal", + SC2_RACESWAP_LOC_ID_OFFSET + 8701, + LocationType.VANILLA, + logic.terran_competent_comp, + ), + make_location_data( + SC2Mission.PHANTOMS_OF_THE_VOID_T.mission_name, + "Northeast Crystal", + SC2_RACESWAP_LOC_ID_OFFSET + 8702, + LocationType.VANILLA, + logic.terran_competent_comp, + ), + make_location_data( + SC2Mission.PHANTOMS_OF_THE_VOID_T.mission_name, + "South Crystal", + SC2_RACESWAP_LOC_ID_OFFSET + 8703, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.PHANTOMS_OF_THE_VOID_T.mission_name, + "Base Established", + SC2_RACESWAP_LOC_ID_OFFSET + 8704, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.PHANTOMS_OF_THE_VOID_T.mission_name, + "Close Temple", + SC2_RACESWAP_LOC_ID_OFFSET + 8705, + LocationType.EXTRA, + logic.terran_competent_comp, + ), + make_location_data( + SC2Mission.PHANTOMS_OF_THE_VOID_T.mission_name, + "Mid Temple", + SC2_RACESWAP_LOC_ID_OFFSET + 8706, + LocationType.EXTRA, + logic.terran_competent_comp, + ), + make_location_data( + SC2Mission.PHANTOMS_OF_THE_VOID_T.mission_name, + "Southeast Temple", + SC2_RACESWAP_LOC_ID_OFFSET + 8707, + LocationType.EXTRA, + logic.terran_competent_comp, + ), + make_location_data( + SC2Mission.PHANTOMS_OF_THE_VOID_T.mission_name, + "Northeast Temple", + SC2_RACESWAP_LOC_ID_OFFSET + 8708, + LocationType.EXTRA, + logic.terran_competent_comp, + ), + make_location_data( + SC2Mission.PHANTOMS_OF_THE_VOID_T.mission_name, + "Northwest Temple", + SC2_RACESWAP_LOC_ID_OFFSET + 8709, + LocationType.EXTRA, + logic.terran_competent_comp, + ), + make_location_data( + SC2Mission.PHANTOMS_OF_THE_VOID_P.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 8800, + LocationType.VICTORY, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.PHANTOMS_OF_THE_VOID_P.mission_name, + "Northwest Crystal", + SC2_RACESWAP_LOC_ID_OFFSET + 8801, + LocationType.VANILLA, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.PHANTOMS_OF_THE_VOID_P.mission_name, + "Northeast Crystal", + SC2_RACESWAP_LOC_ID_OFFSET + 8802, + LocationType.VANILLA, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.PHANTOMS_OF_THE_VOID_P.mission_name, + "South Crystal", + SC2_RACESWAP_LOC_ID_OFFSET + 8803, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.PHANTOMS_OF_THE_VOID_P.mission_name, + "Base Established", + SC2_RACESWAP_LOC_ID_OFFSET + 8804, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.PHANTOMS_OF_THE_VOID_P.mission_name, + "Close Temple", + SC2_RACESWAP_LOC_ID_OFFSET + 8805, + LocationType.EXTRA, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.PHANTOMS_OF_THE_VOID_P.mission_name, + "Mid Temple", + SC2_RACESWAP_LOC_ID_OFFSET + 8806, + LocationType.EXTRA, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.PHANTOMS_OF_THE_VOID_P.mission_name, + "Southeast Temple", + SC2_RACESWAP_LOC_ID_OFFSET + 8807, + LocationType.EXTRA, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.PHANTOMS_OF_THE_VOID_P.mission_name, + "Northeast Temple", + SC2_RACESWAP_LOC_ID_OFFSET + 8808, + LocationType.EXTRA, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.PHANTOMS_OF_THE_VOID_P.mission_name, + "Northwest Temple", + SC2_RACESWAP_LOC_ID_OFFSET + 8809, + LocationType.EXTRA, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.PLANETFALL_T.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 9300, + LocationType.VICTORY, + logic.terran_planetfall_requirement, + hard_rule=logic.terran_any_anti_air, + ), + make_location_data( + SC2Mission.PLANETFALL_T.mission_name, + "East Gate", + SC2_RACESWAP_LOC_ID_OFFSET + 9301, + LocationType.VANILLA, + logic.terran_planetfall_requirement, + ), + make_location_data( + SC2Mission.PLANETFALL_T.mission_name, + "Northwest Gate", + SC2_RACESWAP_LOC_ID_OFFSET + 9302, + LocationType.VANILLA, + logic.terran_planetfall_requirement, + ), + make_location_data( + SC2Mission.PLANETFALL_T.mission_name, + "North Gate", + SC2_RACESWAP_LOC_ID_OFFSET + 9303, + LocationType.VANILLA, + logic.terran_planetfall_requirement, + ), + make_location_data( + SC2Mission.PLANETFALL_T.mission_name, + "1 Laser Drill Deployed", + SC2_RACESWAP_LOC_ID_OFFSET + 9304, + LocationType.EXTRA, + logic.terran_planetfall_requirement, + ), + make_location_data( + SC2Mission.PLANETFALL_T.mission_name, + "2 Laser Drills Deployed", + SC2_RACESWAP_LOC_ID_OFFSET + 9305, + LocationType.EXTRA, + logic.terran_planetfall_requirement, + ), + make_location_data( + SC2Mission.PLANETFALL_T.mission_name, + "3 Laser Drills Deployed", + SC2_RACESWAP_LOC_ID_OFFSET + 9306, + LocationType.EXTRA, + logic.terran_planetfall_requirement, + ), + make_location_data( + SC2Mission.PLANETFALL_T.mission_name, + "4 Laser Drills Deployed", + SC2_RACESWAP_LOC_ID_OFFSET + 9307, + LocationType.EXTRA, + logic.terran_planetfall_requirement, + hard_rule=logic.terran_any_anti_air, + ), + make_location_data( + SC2Mission.PLANETFALL_T.mission_name, + "5 Laser Drills Deployed", + SC2_RACESWAP_LOC_ID_OFFSET + 9308, + LocationType.EXTRA, + logic.terran_planetfall_requirement, + hard_rule=logic.terran_any_anti_air, + ), + make_location_data( + SC2Mission.PLANETFALL_T.mission_name, + "Sons of Korhal", + SC2_RACESWAP_LOC_ID_OFFSET + 9309, + LocationType.EXTRA, + logic.terran_planetfall_requirement, + ), + make_location_data( + SC2Mission.PLANETFALL_T.mission_name, + "Night Wolves", + SC2_RACESWAP_LOC_ID_OFFSET + 9310, + LocationType.EXTRA, + logic.terran_planetfall_requirement, + hard_rule=logic.terran_any_anti_air, + ), + make_location_data( + SC2Mission.PLANETFALL_T.mission_name, + "West Expansion", + SC2_RACESWAP_LOC_ID_OFFSET + 9311, + LocationType.EXTRA, + logic.terran_planetfall_requirement, + hard_rule=logic.terran_any_anti_air, + ), + make_location_data( + SC2Mission.PLANETFALL_T.mission_name, + "Mid Expansion", + SC2_RACESWAP_LOC_ID_OFFSET + 9312, + LocationType.EXTRA, + logic.terran_planetfall_requirement, + hard_rule=logic.terran_any_anti_air, + ), + make_location_data( + SC2Mission.PLANETFALL_P.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 9400, + LocationType.VICTORY, + logic.protoss_planetfall_requirement, + hard_rule=logic.protoss_any_anti_air_unit_or_soa_any_protoss, + ), + make_location_data( + SC2Mission.PLANETFALL_P.mission_name, + "East Gate", + SC2_RACESWAP_LOC_ID_OFFSET + 9401, + LocationType.VANILLA, + logic.protoss_planetfall_requirement, + ), + make_location_data( + SC2Mission.PLANETFALL_P.mission_name, + "Northwest Gate", + SC2_RACESWAP_LOC_ID_OFFSET + 9402, + LocationType.VANILLA, + logic.protoss_planetfall_requirement, + ), + make_location_data( + SC2Mission.PLANETFALL_P.mission_name, + "North Gate", + SC2_RACESWAP_LOC_ID_OFFSET + 9403, + LocationType.VANILLA, + logic.protoss_planetfall_requirement, + ), + make_location_data( + SC2Mission.PLANETFALL_P.mission_name, + "1 Particle Cannon Deployed", + SC2_RACESWAP_LOC_ID_OFFSET + 9404, + LocationType.EXTRA, + logic.protoss_planetfall_requirement, + ), + make_location_data( + SC2Mission.PLANETFALL_P.mission_name, + "2 Particle Cannons Deployed", + SC2_RACESWAP_LOC_ID_OFFSET + 9405, + LocationType.EXTRA, + logic.protoss_planetfall_requirement, + ), + make_location_data( + SC2Mission.PLANETFALL_P.mission_name, + "3 Particle Cannons Deployed", + SC2_RACESWAP_LOC_ID_OFFSET + 9406, + LocationType.EXTRA, + logic.protoss_planetfall_requirement, + ), + make_location_data( + SC2Mission.PLANETFALL_P.mission_name, + "4 Particle Cannons Deployed", + SC2_RACESWAP_LOC_ID_OFFSET + 9407, + LocationType.EXTRA, + logic.protoss_planetfall_requirement, + hard_rule=logic.protoss_any_anti_air_unit_or_soa_any_protoss, + ), + make_location_data( + SC2Mission.PLANETFALL_P.mission_name, + "5 Particle Cannons Deployed", + SC2_RACESWAP_LOC_ID_OFFSET + 9408, + LocationType.EXTRA, + logic.protoss_planetfall_requirement, + hard_rule=logic.protoss_any_anti_air_unit_or_soa_any_protoss, + ), + make_location_data( + SC2Mission.PLANETFALL_P.mission_name, + "Sons of Korhal", + SC2_RACESWAP_LOC_ID_OFFSET + 9409, + LocationType.EXTRA, + logic.protoss_planetfall_requirement, + ), + make_location_data( + SC2Mission.PLANETFALL_P.mission_name, + "Night Wolves", + SC2_RACESWAP_LOC_ID_OFFSET + 9410, + LocationType.EXTRA, + logic.protoss_planetfall_requirement, + hard_rule=logic.protoss_any_anti_air_unit_or_soa_any_protoss, + ), + make_location_data( + SC2Mission.PLANETFALL_P.mission_name, + "West Expansion", + SC2_RACESWAP_LOC_ID_OFFSET + 9411, + LocationType.EXTRA, + logic.protoss_planetfall_requirement, + hard_rule=logic.protoss_any_anti_air_unit_or_soa_any_protoss, + ), + make_location_data( + SC2Mission.PLANETFALL_P.mission_name, + "Mid Expansion", + SC2_RACESWAP_LOC_ID_OFFSET + 9412, + LocationType.EXTRA, + logic.protoss_planetfall_requirement, + hard_rule=logic.protoss_any_anti_air_unit_or_soa_any_protoss, + ), + make_location_data( + SC2Mission.DEATH_FROM_ABOVE_T.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 9500, + LocationType.VICTORY, + logic.terran_base_trasher, + ), + make_location_data( + SC2Mission.DEATH_FROM_ABOVE_T.mission_name, + "First Power Link", + SC2_RACESWAP_LOC_ID_OFFSET + 9501, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.DEATH_FROM_ABOVE_T.mission_name, + "Second Power Link", + SC2_RACESWAP_LOC_ID_OFFSET + 9502, + LocationType.VANILLA, + logic.terran_competent_comp, + ), + make_location_data( + SC2Mission.DEATH_FROM_ABOVE_T.mission_name, + "Third Power Link", + SC2_RACESWAP_LOC_ID_OFFSET + 9503, + LocationType.VANILLA, + logic.terran_competent_comp, + ), + make_location_data( + SC2Mission.DEATH_FROM_ABOVE_T.mission_name, + "Expansion Command Center", + SC2_RACESWAP_LOC_ID_OFFSET + 9504, + LocationType.EXTRA, + logic.terran_competent_comp, + ), + make_location_data( + SC2Mission.DEATH_FROM_ABOVE_T.mission_name, + "Main Path Command Center", + SC2_RACESWAP_LOC_ID_OFFSET + 9505, + LocationType.EXTRA, + logic.terran_base_trasher, + ), + make_location_data( + SC2Mission.DEATH_FROM_ABOVE_P.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 9600, + LocationType.VICTORY, + lambda state: logic.protoss_deathball + or (adv_tactics and logic.protoss_competent_comp(state)), + ), + make_location_data( + SC2Mission.DEATH_FROM_ABOVE_P.mission_name, + "First Power Link", + SC2_RACESWAP_LOC_ID_OFFSET + 9601, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.DEATH_FROM_ABOVE_P.mission_name, + "Second Power Link", + SC2_RACESWAP_LOC_ID_OFFSET + 9602, + LocationType.VANILLA, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.DEATH_FROM_ABOVE_P.mission_name, + "Third Power Link", + SC2_RACESWAP_LOC_ID_OFFSET + 9603, + LocationType.VANILLA, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.DEATH_FROM_ABOVE_P.mission_name, + "Expansion Command Center", + SC2_RACESWAP_LOC_ID_OFFSET + 9604, + LocationType.EXTRA, + logic.protoss_competent_comp, + ), + make_location_data( + SC2Mission.DEATH_FROM_ABOVE_P.mission_name, + "Main Path Command Center", + SC2_RACESWAP_LOC_ID_OFFSET + 9605, + LocationType.EXTRA, + lambda state: logic.protoss_deathball + or (adv_tactics and logic.protoss_competent_comp(state)), + ), + make_location_data( + SC2Mission.THE_RECKONING_T.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 9700, + LocationType.VICTORY, + logic.terran_the_reckoning_requirement, + ), + make_location_data( + SC2Mission.THE_RECKONING_T.mission_name, + "South Lane", + SC2_RACESWAP_LOC_ID_OFFSET + 9701, + LocationType.VANILLA, + logic.terran_the_reckoning_requirement, + ), + make_location_data( + SC2Mission.THE_RECKONING_T.mission_name, + "North Lane", + SC2_RACESWAP_LOC_ID_OFFSET + 9702, + LocationType.VANILLA, + logic.terran_the_reckoning_requirement, + ), + make_location_data( + SC2Mission.THE_RECKONING_T.mission_name, + "East Lane", + SC2_RACESWAP_LOC_ID_OFFSET + 9703, + LocationType.VANILLA, + logic.terran_the_reckoning_requirement, + ), + make_location_data( + SC2Mission.THE_RECKONING_T.mission_name, + "Odin", + SC2_RACESWAP_LOC_ID_OFFSET + 9704, + LocationType.EXTRA, + logic.terran_the_reckoning_requirement, + ), + make_location_data( + SC2Mission.THE_RECKONING_T.mission_name, + "Trash the Odin Early", + SC2_RACESWAP_LOC_ID_OFFSET + 9705, + LocationType.MASTERY, + lambda state: ( + logic.terran_the_reckoning_requirement(state) + and logic.terran_power_rating(state) >= 10 + ), + flags=LocationFlag.SPEEDRUN, + ), + make_location_data( + SC2Mission.THE_RECKONING_P.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 9800, + LocationType.VICTORY, + logic.protoss_the_reckoning_requirement, + ), + make_location_data( + SC2Mission.THE_RECKONING_P.mission_name, + "South Lane", + SC2_RACESWAP_LOC_ID_OFFSET + 9801, + LocationType.VANILLA, + logic.protoss_the_reckoning_requirement, + ), + make_location_data( + SC2Mission.THE_RECKONING_P.mission_name, + "North Lane", + SC2_RACESWAP_LOC_ID_OFFSET + 9802, + LocationType.VANILLA, + logic.protoss_the_reckoning_requirement, + ), + make_location_data( + SC2Mission.THE_RECKONING_P.mission_name, + "East Lane", + SC2_RACESWAP_LOC_ID_OFFSET + 9803, + LocationType.VANILLA, + logic.protoss_the_reckoning_requirement, + ), + make_location_data( + SC2Mission.THE_RECKONING_P.mission_name, + "Odin", + SC2_RACESWAP_LOC_ID_OFFSET + 9804, + LocationType.EXTRA, + logic.protoss_the_reckoning_requirement, + ), + make_location_data( + SC2Mission.THE_RECKONING_P.mission_name, + "Trash the Odin Early", + SC2_RACESWAP_LOC_ID_OFFSET + 9805, + LocationType.MASTERY, + lambda state: ( + logic.protoss_the_reckoning_requirement(state) + and ( + logic.protoss_fleet(state) + or logic.protoss_power_rating(state) >= 10 + ) + ), + flags=LocationFlag.SPEEDRUN, + ), + make_location_data( + SC2Mission.DARK_WHISPERS_T.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 9900, + LocationType.VICTORY, + logic.terran_competent_comp, + ), + make_location_data( + SC2Mission.DARK_WHISPERS_T.mission_name, + "First Prisoner Group", + SC2_RACESWAP_LOC_ID_OFFSET + 9901, + LocationType.VANILLA, + logic.terran_competent_comp, + ), + make_location_data( + SC2Mission.DARK_WHISPERS_T.mission_name, + "Second Prisoner Group", + SC2_RACESWAP_LOC_ID_OFFSET + 9902, + LocationType.VANILLA, + logic.terran_competent_comp, + ), + make_location_data( + SC2Mission.DARK_WHISPERS_T.mission_name, + "First Pylon", + SC2_RACESWAP_LOC_ID_OFFSET + 9903, + LocationType.VANILLA, + logic.terran_competent_comp, + ), + make_location_data( + SC2Mission.DARK_WHISPERS_T.mission_name, + "Second Pylon", + SC2_RACESWAP_LOC_ID_OFFSET + 9904, + LocationType.VANILLA, + logic.terran_competent_comp, + ), + make_location_data( + SC2Mission.DARK_WHISPERS_T.mission_name, + "Zerg Base", + SC2_RACESWAP_LOC_ID_OFFSET + 9905, + LocationType.MASTERY, + lambda state: ( + logic.terran_competent_comp(state) + and logic.terran_base_trasher(state) + and logic.terran_power_rating(state) >= 6 + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.DARK_WHISPERS_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 10000, + LocationType.VICTORY, + lambda state: logic.zerg_competent_comp + and logic.zerg_moderate_anti_air(state), + ), + make_location_data( + SC2Mission.DARK_WHISPERS_Z.mission_name, + "First Prisoner Group", + SC2_RACESWAP_LOC_ID_OFFSET + 10001, + LocationType.VANILLA, + lambda state: logic.zerg_competent_comp + and logic.zerg_moderate_anti_air(state), + ), + make_location_data( + SC2Mission.DARK_WHISPERS_Z.mission_name, + "Second Prisoner Group", + SC2_RACESWAP_LOC_ID_OFFSET + 10002, + LocationType.VANILLA, + lambda state: logic.zerg_competent_comp + and logic.zerg_moderate_anti_air(state), + ), + make_location_data( + SC2Mission.DARK_WHISPERS_Z.mission_name, + "First Pylon", + SC2_RACESWAP_LOC_ID_OFFSET + 10003, + LocationType.VANILLA, + lambda state: logic.zerg_competent_comp + and logic.zerg_moderate_anti_air(state), + ), + make_location_data( + SC2Mission.DARK_WHISPERS_Z.mission_name, + "Second Pylon", + SC2_RACESWAP_LOC_ID_OFFSET + 10004, + LocationType.VANILLA, + lambda state: logic.zerg_competent_comp + and logic.zerg_moderate_anti_air(state), + ), + make_location_data( + SC2Mission.DARK_WHISPERS_Z.mission_name, + "Zerg Base", + SC2_RACESWAP_LOC_ID_OFFSET + 10005, + LocationType.MASTERY, + lambda state: ( + logic.zerg_competent_comp(state) + and logic.zerg_moderate_anti_air(state) + and logic.zerg_base_buster(state) + and logic.zerg_power_rating(state) >= 6 + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.GHOSTS_IN_THE_FOG_T.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 10100, + LocationType.VICTORY, + lambda state: ( + logic.terran_beats_protoss_deathball(state) + and logic.terran_mineral_dump(state) + ), + ), + make_location_data( + SC2Mission.GHOSTS_IN_THE_FOG_T.mission_name, + "South Rock Formation", + SC2_RACESWAP_LOC_ID_OFFSET + 10101, + LocationType.VANILLA, + lambda state: ( + logic.terran_beats_protoss_deathball(state) + and logic.terran_mineral_dump(state) + ), + ), + make_location_data( + SC2Mission.GHOSTS_IN_THE_FOG_T.mission_name, + "West Rock Formation", + SC2_RACESWAP_LOC_ID_OFFSET + 10102, + LocationType.VANILLA, + lambda state: ( + logic.terran_beats_protoss_deathball(state) + and logic.terran_mineral_dump(state) + ), + ), + make_location_data( + SC2Mission.GHOSTS_IN_THE_FOG_T.mission_name, + "East Rock Formation", + SC2_RACESWAP_LOC_ID_OFFSET + 10103, + LocationType.VANILLA, + lambda state: ( + logic.terran_beats_protoss_deathball(state) + and logic.terran_mineral_dump(state) + and logic.terran_can_grab_ghosts_in_the_fog_east_rock_formation(state) + ), + ), + make_location_data( + SC2Mission.GHOSTS_IN_THE_FOG_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 10200, + LocationType.VICTORY, + lambda state: ( + logic.zerg_competent_comp(state) + and logic.zerg_competent_anti_air(state) + and logic.zerg_mineral_dump(state) + ), + ), + make_location_data( + SC2Mission.GHOSTS_IN_THE_FOG_Z.mission_name, + "South Rock Formation", + SC2_RACESWAP_LOC_ID_OFFSET + 10201, + LocationType.VANILLA, + lambda state: ( + logic.zerg_competent_comp(state) + and logic.zerg_competent_anti_air(state) + and logic.zerg_mineral_dump(state) + ), + ), + make_location_data( + SC2Mission.GHOSTS_IN_THE_FOG_Z.mission_name, + "West Rock Formation", + SC2_RACESWAP_LOC_ID_OFFSET + 10202, + LocationType.VANILLA, + lambda state: ( + logic.zerg_competent_comp(state) + and logic.zerg_competent_anti_air(state) + and logic.zerg_mineral_dump(state) + ), + ), + make_location_data( + SC2Mission.GHOSTS_IN_THE_FOG_Z.mission_name, + "East Rock Formation", + SC2_RACESWAP_LOC_ID_OFFSET + 10203, + LocationType.VANILLA, + lambda state: ( + logic.zerg_competent_comp(state) + and logic.zerg_competent_anti_air(state) + and logic.zerg_mineral_dump(state) + and logic.zerg_can_grab_ghosts_in_the_fog_east_rock_formation(state) + ), + ), + make_location_data( + SC2Mission.THE_GROWING_SHADOW_T.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 10700, + LocationType.VICTORY, + lambda state: ( + logic.terran_common_unit(state) + and (adv_tactics or logic.terran_moderate_anti_air(state)) + ), + ), + make_location_data( + SC2Mission.THE_GROWING_SHADOW_T.mission_name, + "Close Pylon", + SC2_RACESWAP_LOC_ID_OFFSET + 10701, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.THE_GROWING_SHADOW_T.mission_name, + "East Pylon", + SC2_RACESWAP_LOC_ID_OFFSET + 10702, + LocationType.VANILLA, + lambda state: ( + logic.terran_common_unit(state) + and ( + adv_tactics + or ( + logic.terran_moderate_anti_air(state) + and logic.terran_any_air_unit(state) + ) + ) + ), + ), + make_location_data( + SC2Mission.THE_GROWING_SHADOW_T.mission_name, + "West Pylon", + SC2_RACESWAP_LOC_ID_OFFSET + 10703, + LocationType.VANILLA, + lambda state: ( + logic.terran_common_unit(state) + and (adv_tactics or logic.terran_moderate_anti_air(state)) + ), + ), + make_location_data( + SC2Mission.THE_GROWING_SHADOW_T.mission_name, + "Base", + SC2_RACESWAP_LOC_ID_OFFSET + 10704, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.THE_GROWING_SHADOW_T.mission_name, + "Templar Base", + SC2_RACESWAP_LOC_ID_OFFSET + 10705, + LocationType.EXTRA, + lambda state: ( + logic.terran_common_unit(state) + and (adv_tactics or logic.terran_moderate_anti_air(state)) + ), + ), + make_location_data( + SC2Mission.THE_GROWING_SHADOW_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 10800, + LocationType.VICTORY, + lambda state: ( + logic.zerg_common_unit(state) and logic.zerg_moderate_anti_air(state) + ), + ), + make_location_data( + SC2Mission.THE_GROWING_SHADOW_Z.mission_name, + "Close Pylon", + SC2_RACESWAP_LOC_ID_OFFSET + 10801, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.THE_GROWING_SHADOW_Z.mission_name, + "East Pylon", + SC2_RACESWAP_LOC_ID_OFFSET + 10802, + LocationType.VANILLA, + lambda state: ( + logic.zerg_common_unit(state) and logic.zerg_moderate_anti_air(state) + ), + ), + make_location_data( + SC2Mission.THE_GROWING_SHADOW_Z.mission_name, + "West Pylon", + SC2_RACESWAP_LOC_ID_OFFSET + 10803, + LocationType.VANILLA, + lambda state: ( + logic.zerg_common_unit(state) and logic.zerg_moderate_anti_air(state) + ), + ), + make_location_data( + SC2Mission.THE_GROWING_SHADOW_Z.mission_name, + "Base", + SC2_RACESWAP_LOC_ID_OFFSET + 10804, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.THE_GROWING_SHADOW_Z.mission_name, + "Templar Base", + SC2_RACESWAP_LOC_ID_OFFSET + 10805, + LocationType.EXTRA, + lambda state: ( + logic.zerg_common_unit(state) and logic.zerg_moderate_anti_air(state) + ), + ), + make_location_data( + SC2Mission.THE_SPEAR_OF_ADUN_T.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 10900, + LocationType.VICTORY, + logic.terran_spear_of_adun_requirement, + hard_rule=logic.terran_any_anti_air, + ), + make_location_data( + SC2Mission.THE_SPEAR_OF_ADUN_T.mission_name, + "Factory", + SC2_RACESWAP_LOC_ID_OFFSET + 10901, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.THE_SPEAR_OF_ADUN_T.mission_name, + "Armory", + SC2_RACESWAP_LOC_ID_OFFSET + 10902, + LocationType.VANILLA, + logic.terran_spear_of_adun_requirement, + ), + make_location_data( + SC2Mission.THE_SPEAR_OF_ADUN_T.mission_name, + "Starport", + SC2_RACESWAP_LOC_ID_OFFSET + 10903, + LocationType.VANILLA, + logic.terran_spear_of_adun_requirement, + ), + make_location_data( + SC2Mission.THE_SPEAR_OF_ADUN_T.mission_name, + "North Power Cell", + SC2_RACESWAP_LOC_ID_OFFSET + 10904, + LocationType.EXTRA, + logic.terran_spear_of_adun_requirement, + ), + make_location_data( + SC2Mission.THE_SPEAR_OF_ADUN_T.mission_name, + "East Power Cell", + SC2_RACESWAP_LOC_ID_OFFSET + 10905, + LocationType.EXTRA, + logic.terran_spear_of_adun_requirement, + ), + make_location_data( + SC2Mission.THE_SPEAR_OF_ADUN_T.mission_name, + "South Power Cell", + SC2_RACESWAP_LOC_ID_OFFSET + 10906, + LocationType.EXTRA, + logic.terran_spear_of_adun_requirement, + ), + make_location_data( + SC2Mission.THE_SPEAR_OF_ADUN_T.mission_name, + "Southeast Power Cell", + SC2_RACESWAP_LOC_ID_OFFSET + 10907, + LocationType.EXTRA, + logic.terran_spear_of_adun_requirement, + ), + make_location_data( + SC2Mission.THE_SPEAR_OF_ADUN_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 11000, + LocationType.VICTORY, + logic.zerg_competent_comp_competent_aa, + hard_rule=logic.zerg_any_anti_air, + ), + make_location_data( + SC2Mission.THE_SPEAR_OF_ADUN_Z.mission_name, + "Baneling Nest", + SC2_RACESWAP_LOC_ID_OFFSET + 11001, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.THE_SPEAR_OF_ADUN_Z.mission_name, + "Roach Warren", + SC2_RACESWAP_LOC_ID_OFFSET + 11002, + LocationType.VANILLA, + lambda state: ( + logic.zerg_spear_of_adun_requirement(state) + and logic.spread_creep(state) + ), + ), + make_location_data( + SC2Mission.THE_SPEAR_OF_ADUN_Z.mission_name, + "Infestation Pit", + SC2_RACESWAP_LOC_ID_OFFSET + 11003, + LocationType.VANILLA, + lambda state: ( + logic.zerg_spear_of_adun_requirement(state) + and logic.spread_creep(state) + ), + ), + make_location_data( + SC2Mission.THE_SPEAR_OF_ADUN_Z.mission_name, + "North Power Cell", + SC2_RACESWAP_LOC_ID_OFFSET + 11004, + LocationType.EXTRA, + logic.zerg_spear_of_adun_requirement, + ), + make_location_data( + SC2Mission.THE_SPEAR_OF_ADUN_Z.mission_name, + "East Power Cell", + SC2_RACESWAP_LOC_ID_OFFSET + 11005, + LocationType.EXTRA, + logic.zerg_spear_of_adun_requirement, + ), + make_location_data( + SC2Mission.THE_SPEAR_OF_ADUN_Z.mission_name, + "South Power Cell", + SC2_RACESWAP_LOC_ID_OFFSET + 11006, + LocationType.EXTRA, + logic.zerg_spear_of_adun_requirement, + ), + make_location_data( + SC2Mission.THE_SPEAR_OF_ADUN_Z.mission_name, + "Southeast Power Cell", + SC2_RACESWAP_LOC_ID_OFFSET + 11007, + LocationType.EXTRA, + logic.zerg_spear_of_adun_requirement, + ), + make_location_data( + SC2Mission.SKY_SHIELD_T.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 11100, + LocationType.VICTORY, + logic.terran_sky_shield_requirement, + ), + make_location_data( + SC2Mission.SKY_SHIELD_T.mission_name, + "Mid EMP Scrambler", + SC2_RACESWAP_LOC_ID_OFFSET + 11101, + LocationType.VANILLA, + logic.terran_sky_shield_requirement, + ), + make_location_data( + SC2Mission.SKY_SHIELD_T.mission_name, + "Southeast EMP Scrambler", + SC2_RACESWAP_LOC_ID_OFFSET + 11102, + LocationType.VANILLA, + logic.terran_sky_shield_requirement, + ), + make_location_data( + SC2Mission.SKY_SHIELD_T.mission_name, + "North EMP Scrambler", + SC2_RACESWAP_LOC_ID_OFFSET + 11103, + LocationType.VANILLA, + logic.terran_sky_shield_requirement, + ), + make_location_data( + SC2Mission.SKY_SHIELD_T.mission_name, + "Mid Stabilizer", + SC2_RACESWAP_LOC_ID_OFFSET + 11104, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.SKY_SHIELD_T.mission_name, + "Southwest Stabilizer", + SC2_RACESWAP_LOC_ID_OFFSET + 11105, + LocationType.EXTRA, + logic.terran_sky_shield_requirement, + ), + make_location_data( + SC2Mission.SKY_SHIELD_T.mission_name, + "Northwest Stabilizer", + SC2_RACESWAP_LOC_ID_OFFSET + 11106, + LocationType.EXTRA, + logic.terran_sky_shield_requirement, + ), + make_location_data( + SC2Mission.SKY_SHIELD_T.mission_name, + "Northeast Stabilizer", + SC2_RACESWAP_LOC_ID_OFFSET + 11107, + LocationType.EXTRA, + logic.terran_sky_shield_requirement, + ), + make_location_data( + SC2Mission.SKY_SHIELD_T.mission_name, + "Southeast Stabilizer", + SC2_RACESWAP_LOC_ID_OFFSET + 11108, + LocationType.EXTRA, + logic.terran_sky_shield_requirement, + ), + make_location_data( + SC2Mission.SKY_SHIELD_T.mission_name, + "West Raynor Base", + SC2_RACESWAP_LOC_ID_OFFSET + 11109, + LocationType.EXTRA, + logic.terran_sky_shield_requirement, + ), + make_location_data( + SC2Mission.SKY_SHIELD_T.mission_name, + "East Raynor Base", + SC2_RACESWAP_LOC_ID_OFFSET + 11110, + LocationType.EXTRA, + logic.terran_sky_shield_requirement, + ), + make_location_data( + SC2Mission.SKY_SHIELD_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 11200, + LocationType.VICTORY, + logic.zerg_sky_shield_requirement, + ), + make_location_data( + SC2Mission.SKY_SHIELD_Z.mission_name, + "Mid EMP Scrambler", + SC2_RACESWAP_LOC_ID_OFFSET + 11201, + LocationType.VANILLA, + logic.zerg_sky_shield_requirement, + ), + make_location_data( + SC2Mission.SKY_SHIELD_Z.mission_name, + "Southeast EMP Scrambler", + SC2_RACESWAP_LOC_ID_OFFSET + 11202, + LocationType.VANILLA, + logic.zerg_sky_shield_requirement, + ), + make_location_data( + SC2Mission.SKY_SHIELD_Z.mission_name, + "North EMP Scrambler", + SC2_RACESWAP_LOC_ID_OFFSET + 11203, + LocationType.VANILLA, + logic.zerg_sky_shield_requirement, + ), + make_location_data( + SC2Mission.SKY_SHIELD_Z.mission_name, + "Mid Stabilizer", + SC2_RACESWAP_LOC_ID_OFFSET + 11204, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.SKY_SHIELD_Z.mission_name, + "Southwest Stabilizer", + SC2_RACESWAP_LOC_ID_OFFSET + 11205, + LocationType.EXTRA, + logic.zerg_sky_shield_requirement, + ), + make_location_data( + SC2Mission.SKY_SHIELD_Z.mission_name, + "Northwest Stabilizer", + SC2_RACESWAP_LOC_ID_OFFSET + 11206, + LocationType.EXTRA, + logic.zerg_sky_shield_requirement, + ), + make_location_data( + SC2Mission.SKY_SHIELD_Z.mission_name, + "Northeast Stabilizer", + SC2_RACESWAP_LOC_ID_OFFSET + 11207, + LocationType.EXTRA, + logic.zerg_sky_shield_requirement, + ), + make_location_data( + SC2Mission.SKY_SHIELD_Z.mission_name, + "Southeast Stabilizer", + SC2_RACESWAP_LOC_ID_OFFSET + 11208, + LocationType.EXTRA, + logic.zerg_sky_shield_requirement, + ), + make_location_data( + SC2Mission.SKY_SHIELD_Z.mission_name, + "West Raynor Base", + SC2_RACESWAP_LOC_ID_OFFSET + 11209, + LocationType.EXTRA, + logic.zerg_sky_shield_requirement, + ), + make_location_data( + SC2Mission.SKY_SHIELD_Z.mission_name, + "East Raynor Base", + SC2_RACESWAP_LOC_ID_OFFSET + 11210, + LocationType.EXTRA, + logic.zerg_sky_shield_requirement, + ), + make_location_data( + SC2Mission.BROTHERS_IN_ARMS_T.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 11300, + LocationType.VICTORY, + lambda state: ( + logic.terran_common_unit(state) and logic.terran_competent_comp(state) + ), + ), + make_location_data( + SC2Mission.BROTHERS_IN_ARMS_T.mission_name, + "Mid Science Facility", + SC2_RACESWAP_LOC_ID_OFFSET + 11301, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.BROTHERS_IN_ARMS_T.mission_name, + "North Science Facility", + SC2_RACESWAP_LOC_ID_OFFSET + 11302, + LocationType.VANILLA, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_competent_comp(state) + or ( + logic.take_over_ai_allies + and logic.advanced_tactics + and logic.terran_common_unit(state) + ) + ), + ), + make_location_data( + SC2Mission.BROTHERS_IN_ARMS_T.mission_name, + "South Science Facility", + SC2_RACESWAP_LOC_ID_OFFSET + 11303, + LocationType.VANILLA, + lambda state: ( + logic.terran_common_unit(state) and logic.terran_competent_comp(state) + ), + ), + make_location_data( + SC2Mission.BROTHERS_IN_ARMS_T.mission_name, + "Raynor Forward Positions", + SC2_RACESWAP_LOC_ID_OFFSET + 11304, + LocationType.EXTRA, + lambda state: ( + logic.terran_common_unit(state) and logic.terran_competent_comp(state) + ), + ), + make_location_data( + SC2Mission.BROTHERS_IN_ARMS_T.mission_name, + "Valerian Forward Positions", + SC2_RACESWAP_LOC_ID_OFFSET + 11305, + LocationType.EXTRA, + lambda state: ( + logic.terran_common_unit(state) and logic.terran_competent_comp(state) + ), + ), + make_location_data( + SC2Mission.BROTHERS_IN_ARMS_T.mission_name, + "Win in under 15 Minutes", + SC2_RACESWAP_LOC_ID_OFFSET + 11306, + LocationType.CHALLENGE, + lambda state: ( + logic.terran_common_unit(state) + and logic.terran_base_trasher(state) + and logic.terran_power_rating(state) >= 8 + ), + flags=LocationFlag.SPEEDRUN, + ), + make_location_data( + SC2Mission.BROTHERS_IN_ARMS_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 11400, + LocationType.VICTORY, + logic.zerg_brothers_in_arms_requirement, + ), + make_location_data( + SC2Mission.BROTHERS_IN_ARMS_Z.mission_name, + "Mid Science Facility", + SC2_RACESWAP_LOC_ID_OFFSET + 11401, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.BROTHERS_IN_ARMS_Z.mission_name, + "North Science Facility", + SC2_RACESWAP_LOC_ID_OFFSET + 11402, + LocationType.VANILLA, + lambda state: ( + logic.zerg_brothers_in_arms_requirement(state) + or ( + logic.take_over_ai_allies + and logic.advanced_tactics + and ( + logic.zerg_common_unit(state) or logic.terran_common_unit(state) + ) + ) + ), + ), + make_location_data( + SC2Mission.BROTHERS_IN_ARMS_Z.mission_name, + "South Science Facility", + SC2_RACESWAP_LOC_ID_OFFSET + 11403, + LocationType.VANILLA, + logic.zerg_brothers_in_arms_requirement, + ), + make_location_data( + SC2Mission.BROTHERS_IN_ARMS_Z.mission_name, + "Raynor Forward Positions", + SC2_RACESWAP_LOC_ID_OFFSET + 11404, + LocationType.EXTRA, + logic.zerg_brothers_in_arms_requirement, + ), + make_location_data( + SC2Mission.BROTHERS_IN_ARMS_Z.mission_name, + "Valerian Forward Positions", + SC2_RACESWAP_LOC_ID_OFFSET + 11405, + LocationType.EXTRA, + logic.zerg_brothers_in_arms_requirement, + ), + make_location_data( + SC2Mission.BROTHERS_IN_ARMS_Z.mission_name, + "Win in under 15 Minutes", + SC2_RACESWAP_LOC_ID_OFFSET + 11406, + LocationType.CHALLENGE, + lambda state: ( + logic.zerg_brothers_in_arms_requirement + and logic.zerg_base_buster(state) + and logic.zerg_power_rating(state) >= 8 + ), + flags=LocationFlag.SPEEDRUN, + ), + make_location_data( + SC2Mission.AMON_S_REACH_T.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 11500, + LocationType.VICTORY, + lambda state: (logic.terran_competent_comp(state)), + ), + make_location_data( + SC2Mission.AMON_S_REACH_T.mission_name, + "Close Solarite Reserve", + SC2_RACESWAP_LOC_ID_OFFSET + 11501, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.AMON_S_REACH_T.mission_name, + "North Solarite Reserve", + SC2_RACESWAP_LOC_ID_OFFSET + 11502, + LocationType.VANILLA, + lambda state: (logic.terran_competent_comp(state)), + ), + make_location_data( + SC2Mission.AMON_S_REACH_T.mission_name, + "East Solarite Reserve", + SC2_RACESWAP_LOC_ID_OFFSET + 11503, + LocationType.VANILLA, + lambda state: (logic.terran_competent_comp(state)), + ), + make_location_data( + SC2Mission.AMON_S_REACH_T.mission_name, + "West Launch Bay", + SC2_RACESWAP_LOC_ID_OFFSET + 11504, + LocationType.EXTRA, + lambda state: (logic.terran_competent_comp(state)), + ), + make_location_data( + SC2Mission.AMON_S_REACH_T.mission_name, + "South Launch Bay", + SC2_RACESWAP_LOC_ID_OFFSET + 11505, + LocationType.EXTRA, + lambda state: (logic.terran_competent_comp(state)), + ), + make_location_data( + SC2Mission.AMON_S_REACH_T.mission_name, + "Northwest Launch Bay", + SC2_RACESWAP_LOC_ID_OFFSET + 11506, + LocationType.EXTRA, + lambda state: (logic.terran_competent_comp(state)), + ), + make_location_data( + SC2Mission.AMON_S_REACH_T.mission_name, + "East Launch Bay", + SC2_RACESWAP_LOC_ID_OFFSET + 11507, + LocationType.EXTRA, + lambda state: (logic.terran_competent_comp(state)), + ), + make_location_data( + SC2Mission.AMON_S_REACH_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 11600, + LocationType.VICTORY, + lambda state: ( + logic.zerg_competent_comp(state) + and logic.zerg_basic_kerriganless_anti_air(state) + ), + ), + make_location_data( + SC2Mission.AMON_S_REACH_Z.mission_name, + "Close Solarite Reserve", + SC2_RACESWAP_LOC_ID_OFFSET + 11601, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.AMON_S_REACH_Z.mission_name, + "North Solarite Reserve", + SC2_RACESWAP_LOC_ID_OFFSET + 11602, + LocationType.VANILLA, + lambda state: ( + logic.zerg_competent_comp(state) + and logic.zerg_basic_kerriganless_anti_air(state) + ), + ), + make_location_data( + SC2Mission.AMON_S_REACH_Z.mission_name, + "East Solarite Reserve", + SC2_RACESWAP_LOC_ID_OFFSET + 11603, + LocationType.VANILLA, + lambda state: ( + logic.zerg_competent_comp(state) + and logic.zerg_basic_kerriganless_anti_air(state) + ), + ), + make_location_data( + SC2Mission.AMON_S_REACH_Z.mission_name, + "West Launch Bay", + SC2_RACESWAP_LOC_ID_OFFSET + 11604, + LocationType.EXTRA, + lambda state: ( + logic.zerg_competent_comp(state) + and logic.zerg_basic_kerriganless_anti_air(state) + ), + ), + make_location_data( + SC2Mission.AMON_S_REACH_Z.mission_name, + "South Launch Bay", + SC2_RACESWAP_LOC_ID_OFFSET + 11605, + LocationType.EXTRA, + lambda state: ( + logic.zerg_competent_comp(state) + and logic.zerg_basic_kerriganless_anti_air(state) + ), + ), + make_location_data( + SC2Mission.AMON_S_REACH_Z.mission_name, + "Northwest Launch Bay", + SC2_RACESWAP_LOC_ID_OFFSET + 11606, + LocationType.EXTRA, + lambda state: ( + logic.zerg_competent_comp(state) + and logic.zerg_basic_kerriganless_anti_air(state) + ), + ), + make_location_data( + SC2Mission.AMON_S_REACH_Z.mission_name, + "East Launch Bay", + SC2_RACESWAP_LOC_ID_OFFSET + 11607, + LocationType.EXTRA, + lambda state: ( + logic.zerg_competent_comp(state) + and logic.zerg_basic_kerriganless_anti_air(state) + ), + ), + make_location_data( + SC2Mission.LAST_STAND_T.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 11700, + LocationType.VICTORY, + logic.terran_last_stand_requirement, + ), + make_location_data( + SC2Mission.LAST_STAND_T.mission_name, + "West Zenith Stone", + SC2_RACESWAP_LOC_ID_OFFSET + 11701, + LocationType.VANILLA, + logic.terran_last_stand_requirement, + ), + make_location_data( + SC2Mission.LAST_STAND_T.mission_name, + "North Zenith Stone", + SC2_RACESWAP_LOC_ID_OFFSET + 11702, + LocationType.VANILLA, + logic.terran_last_stand_requirement, + ), + make_location_data( + SC2Mission.LAST_STAND_T.mission_name, + "East Zenith Stone", + SC2_RACESWAP_LOC_ID_OFFSET + 11703, + LocationType.VANILLA, + logic.terran_last_stand_requirement, + ), + make_location_data( + SC2Mission.LAST_STAND_T.mission_name, + "1 Billion Zerg", + SC2_RACESWAP_LOC_ID_OFFSET + 11704, + LocationType.EXTRA, + logic.terran_last_stand_requirement, + ), + make_location_data( + SC2Mission.LAST_STAND_T.mission_name, + "1.5 Billion Zerg", + SC2_RACESWAP_LOC_ID_OFFSET + 11705, + LocationType.VANILLA, + lambda state: logic.terran_last_stand_requirement(state) + and logic.terran_defense_rating(state, True, True) >= 13, + ), + make_location_data( + SC2Mission.LAST_STAND_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 11800, + LocationType.VICTORY, + logic.zerg_last_stand_requirement, + ), + make_location_data( + SC2Mission.LAST_STAND_Z.mission_name, + "West Zenith Stone", + SC2_RACESWAP_LOC_ID_OFFSET + 11801, + LocationType.VANILLA, + logic.zerg_last_stand_requirement, + ), + make_location_data( + SC2Mission.LAST_STAND_Z.mission_name, + "North Zenith Stone", + SC2_RACESWAP_LOC_ID_OFFSET + 11802, + LocationType.VANILLA, + logic.zerg_last_stand_requirement, + ), + make_location_data( + SC2Mission.LAST_STAND_Z.mission_name, + "East Zenith Stone", + SC2_RACESWAP_LOC_ID_OFFSET + 11803, + LocationType.VANILLA, + logic.zerg_last_stand_requirement, + ), + make_location_data( + SC2Mission.LAST_STAND_Z.mission_name, + "1 Billion Zerg", + SC2_RACESWAP_LOC_ID_OFFSET + 11804, + LocationType.EXTRA, + logic.zerg_last_stand_requirement, + ), + make_location_data( + SC2Mission.LAST_STAND_Z.mission_name, + "1.5 Billion Zerg", + SC2_RACESWAP_LOC_ID_OFFSET + 11805, + LocationType.VANILLA, + logic.zerg_last_stand_requirement, + ), + make_location_data( + SC2Mission.FORBIDDEN_WEAPON_T.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 11900, + LocationType.VICTORY, + logic.terran_beats_protoss_deathball, + ), + make_location_data( + SC2Mission.FORBIDDEN_WEAPON_T.mission_name, + "South Solarite", + SC2_RACESWAP_LOC_ID_OFFSET + 11901, + LocationType.VANILLA, + logic.terran_beats_protoss_deathball, + ), + make_location_data( + SC2Mission.FORBIDDEN_WEAPON_T.mission_name, + "North Solarite", + SC2_RACESWAP_LOC_ID_OFFSET + 11902, + LocationType.VANILLA, + logic.terran_beats_protoss_deathball, + ), + make_location_data( + SC2Mission.FORBIDDEN_WEAPON_T.mission_name, + "Northwest Solarite", + SC2_RACESWAP_LOC_ID_OFFSET + 11903, + LocationType.VANILLA, + logic.terran_beats_protoss_deathball, + ), + make_location_data( + SC2Mission.FORBIDDEN_WEAPON_T.mission_name, + "Rescue Medics", + SC2_RACESWAP_LOC_ID_OFFSET + 11904, + LocationType.EXTRA, + logic.terran_beats_protoss_deathball, + ), + make_location_data( + SC2Mission.FORBIDDEN_WEAPON_T.mission_name, + "Destroy Gateways", + SC2_RACESWAP_LOC_ID_OFFSET + 11905, + LocationType.CHALLENGE, + logic.terran_beats_protoss_deathball, + ), + make_location_data( + SC2Mission.FORBIDDEN_WEAPON_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 12000, + LocationType.VICTORY, + logic.zerg_competent_comp_competent_aa, + ), + make_location_data( + SC2Mission.FORBIDDEN_WEAPON_Z.mission_name, + "South Solarite", + SC2_RACESWAP_LOC_ID_OFFSET + 12001, + LocationType.VANILLA, + logic.zerg_competent_comp_competent_aa, + ), + make_location_data( + SC2Mission.FORBIDDEN_WEAPON_Z.mission_name, + "North Solarite", + SC2_RACESWAP_LOC_ID_OFFSET + 12002, + LocationType.VANILLA, + logic.zerg_competent_comp_competent_aa, + ), + make_location_data( + SC2Mission.FORBIDDEN_WEAPON_Z.mission_name, + "Northwest Solarite", + SC2_RACESWAP_LOC_ID_OFFSET + 12003, + LocationType.VANILLA, + logic.zerg_competent_comp_competent_aa, + ), + make_location_data( + SC2Mission.FORBIDDEN_WEAPON_Z.mission_name, + "Rescue Infested Medics", + SC2_RACESWAP_LOC_ID_OFFSET + 12004, + LocationType.EXTRA, + logic.zerg_competent_comp_competent_aa, + ), + make_location_data( + SC2Mission.FORBIDDEN_WEAPON_Z.mission_name, + "Destroy Gateways", + SC2_RACESWAP_LOC_ID_OFFSET + 12005, + LocationType.CHALLENGE, + logic.zerg_competent_comp_competent_aa, + ), + make_location_data( + SC2Mission.TEMPLE_OF_UNIFICATION_T.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 12100, + LocationType.VICTORY, + logic.terran_temple_of_unification_requirement, + ), + make_location_data( + SC2Mission.TEMPLE_OF_UNIFICATION_T.mission_name, + "Mid Celestial Lock", + SC2_RACESWAP_LOC_ID_OFFSET + 12101, + LocationType.EXTRA, + logic.terran_temple_of_unification_requirement, + ), + make_location_data( + SC2Mission.TEMPLE_OF_UNIFICATION_T.mission_name, + "West Celestial Lock", + SC2_RACESWAP_LOC_ID_OFFSET + 12102, + LocationType.EXTRA, + logic.terran_temple_of_unification_requirement, + ), + make_location_data( + SC2Mission.TEMPLE_OF_UNIFICATION_T.mission_name, + "South Celestial Lock", + SC2_RACESWAP_LOC_ID_OFFSET + 12103, + LocationType.EXTRA, + logic.terran_temple_of_unification_requirement, + ), + make_location_data( + SC2Mission.TEMPLE_OF_UNIFICATION_T.mission_name, + "East Celestial Lock", + SC2_RACESWAP_LOC_ID_OFFSET + 12104, + LocationType.EXTRA, + logic.terran_temple_of_unification_requirement, + ), + make_location_data( + SC2Mission.TEMPLE_OF_UNIFICATION_T.mission_name, + "North Celestial Lock", + SC2_RACESWAP_LOC_ID_OFFSET + 12105, + LocationType.EXTRA, + logic.terran_temple_of_unification_requirement, + ), + make_location_data( + SC2Mission.TEMPLE_OF_UNIFICATION_T.mission_name, + "Titanic Warp Prism", + SC2_RACESWAP_LOC_ID_OFFSET + 12106, + LocationType.VANILLA, + logic.terran_temple_of_unification_requirement, + hard_rule=logic.terran_any_anti_air, + ), + make_location_data( + SC2Mission.TEMPLE_OF_UNIFICATION_T.mission_name, + "Terran Main Base", + SC2_RACESWAP_LOC_ID_OFFSET + 12107, + LocationType.MASTERY, + lambda state: ( + logic.terran_temple_of_unification_requirement(state) + and logic.terran_base_trasher(state) + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.TEMPLE_OF_UNIFICATION_T.mission_name, + "Protoss Main Base", + SC2_RACESWAP_LOC_ID_OFFSET + 12108, + LocationType.MASTERY, + lambda state: ( + logic.terran_temple_of_unification_requirement(state) + and logic.terran_base_trasher(state) + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.TEMPLE_OF_UNIFICATION_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 12200, + LocationType.VICTORY, + logic.zerg_temple_of_unification_requirement, + ), + make_location_data( + SC2Mission.TEMPLE_OF_UNIFICATION_Z.mission_name, + "Mid Celestial Lock", + SC2_RACESWAP_LOC_ID_OFFSET + 12201, + LocationType.EXTRA, + logic.zerg_temple_of_unification_requirement, + ), + make_location_data( + SC2Mission.TEMPLE_OF_UNIFICATION_Z.mission_name, + "West Celestial Lock", + SC2_RACESWAP_LOC_ID_OFFSET + 12202, + LocationType.EXTRA, + logic.zerg_temple_of_unification_requirement, + ), + make_location_data( + SC2Mission.TEMPLE_OF_UNIFICATION_Z.mission_name, + "South Celestial Lock", + SC2_RACESWAP_LOC_ID_OFFSET + 12203, + LocationType.EXTRA, + logic.zerg_temple_of_unification_requirement, + ), + make_location_data( + SC2Mission.TEMPLE_OF_UNIFICATION_Z.mission_name, + "East Celestial Lock", + SC2_RACESWAP_LOC_ID_OFFSET + 12204, + LocationType.EXTRA, + logic.zerg_temple_of_unification_requirement, + ), + make_location_data( + SC2Mission.TEMPLE_OF_UNIFICATION_Z.mission_name, + "North Celestial Lock", + SC2_RACESWAP_LOC_ID_OFFSET + 12205, + LocationType.EXTRA, + logic.zerg_temple_of_unification_requirement, + ), + make_location_data( + SC2Mission.TEMPLE_OF_UNIFICATION_Z.mission_name, + "Titanic Warp Prism", + SC2_RACESWAP_LOC_ID_OFFSET + 12206, + LocationType.VANILLA, + logic.zerg_temple_of_unification_requirement, + hard_rule=logic.zerg_any_anti_air, + ), + make_location_data( + SC2Mission.TEMPLE_OF_UNIFICATION_Z.mission_name, + "Terran Main Base", + SC2_RACESWAP_LOC_ID_OFFSET + 12207, + LocationType.MASTERY, + lambda state: ( + logic.zerg_temple_of_unification_requirement(state) + and logic.zerg_base_buster(state) + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.TEMPLE_OF_UNIFICATION_Z.mission_name, + "Protoss Main Base", + SC2_RACESWAP_LOC_ID_OFFSET + 12208, + LocationType.MASTERY, + lambda state: ( + logic.zerg_temple_of_unification_requirement(state) + and logic.zerg_base_buster(state) + ), + flags=LocationFlag.BASEBUST, + ), + make_location_data( + SC2Mission.HARBINGER_OF_OBLIVION_T.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 12500, + LocationType.VICTORY, + logic.terran_harbinger_of_oblivion_requirement, + ), + make_location_data( + SC2Mission.HARBINGER_OF_OBLIVION_T.mission_name, + "Artanis", + SC2_RACESWAP_LOC_ID_OFFSET + 12501, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.HARBINGER_OF_OBLIVION_T.mission_name, + "Northwest Void Crystal", + SC2_RACESWAP_LOC_ID_OFFSET + 12502, + LocationType.EXTRA, + logic.terran_harbinger_of_oblivion_requirement, + ), + make_location_data( + SC2Mission.HARBINGER_OF_OBLIVION_T.mission_name, + "Northeast Void Crystal", + SC2_RACESWAP_LOC_ID_OFFSET + 12503, + LocationType.EXTRA, + logic.terran_harbinger_of_oblivion_requirement, + ), + make_location_data( + SC2Mission.HARBINGER_OF_OBLIVION_T.mission_name, + "Southwest Void Crystal", + SC2_RACESWAP_LOC_ID_OFFSET + 12504, + LocationType.EXTRA, + logic.terran_harbinger_of_oblivion_requirement, + ), + make_location_data( + SC2Mission.HARBINGER_OF_OBLIVION_T.mission_name, + "Southeast Void Crystal", + SC2_RACESWAP_LOC_ID_OFFSET + 12505, + LocationType.EXTRA, + logic.terran_harbinger_of_oblivion_requirement, + ), + make_location_data( + SC2Mission.HARBINGER_OF_OBLIVION_T.mission_name, + "South Xel'Naga Vessel", + SC2_RACESWAP_LOC_ID_OFFSET + 12506, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.HARBINGER_OF_OBLIVION_T.mission_name, + "Mid Xel'Naga Vessel", + SC2_RACESWAP_LOC_ID_OFFSET + 12507, + LocationType.VANILLA, + logic.terran_harbinger_of_oblivion_requirement, + ), + make_location_data( + SC2Mission.HARBINGER_OF_OBLIVION_T.mission_name, + "North Xel'Naga Vessel", + SC2_RACESWAP_LOC_ID_OFFSET + 12508, + LocationType.VANILLA, + logic.terran_harbinger_of_oblivion_requirement, + ), + make_location_data( + SC2Mission.HARBINGER_OF_OBLIVION_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 12600, + LocationType.VICTORY, + logic.zerg_harbinger_of_oblivion_requirement, + ), + make_location_data( + SC2Mission.HARBINGER_OF_OBLIVION_Z.mission_name, + "Artanis", + SC2_RACESWAP_LOC_ID_OFFSET + 12601, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.HARBINGER_OF_OBLIVION_Z.mission_name, + "Northwest Void Crystal", + SC2_RACESWAP_LOC_ID_OFFSET + 12602, + LocationType.EXTRA, + logic.zerg_harbinger_of_oblivion_requirement, + ), + make_location_data( + SC2Mission.HARBINGER_OF_OBLIVION_Z.mission_name, + "Northeast Void Crystal", + SC2_RACESWAP_LOC_ID_OFFSET + 12603, + LocationType.EXTRA, + logic.zerg_harbinger_of_oblivion_requirement, + ), + make_location_data( + SC2Mission.HARBINGER_OF_OBLIVION_Z.mission_name, + "Southwest Void Crystal", + SC2_RACESWAP_LOC_ID_OFFSET + 12604, + LocationType.EXTRA, + logic.zerg_harbinger_of_oblivion_requirement, + ), + make_location_data( + SC2Mission.HARBINGER_OF_OBLIVION_Z.mission_name, + "Southeast Void Crystal", + SC2_RACESWAP_LOC_ID_OFFSET + 12605, + LocationType.EXTRA, + logic.zerg_harbinger_of_oblivion_requirement, + ), + make_location_data( + SC2Mission.HARBINGER_OF_OBLIVION_Z.mission_name, + "South Xel'Naga Vessel", + SC2_RACESWAP_LOC_ID_OFFSET + 12606, + LocationType.VANILLA, + ), + make_location_data( + SC2Mission.HARBINGER_OF_OBLIVION_Z.mission_name, + "Mid Xel'Naga Vessel", + SC2_RACESWAP_LOC_ID_OFFSET + 12607, + LocationType.VANILLA, + logic.zerg_harbinger_of_oblivion_requirement, + ), + make_location_data( + SC2Mission.HARBINGER_OF_OBLIVION_Z.mission_name, + "North Xel'Naga Vessel", + SC2_RACESWAP_LOC_ID_OFFSET + 12608, + LocationType.VANILLA, + logic.zerg_harbinger_of_oblivion_requirement, + ), + make_location_data( + SC2Mission.UNSEALING_THE_PAST_T.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 12700, + LocationType.VICTORY, + logic.terran_unsealing_the_past_requirement, + ), + make_location_data( + SC2Mission.UNSEALING_THE_PAST_T.mission_name, + "Zerg Cleared", + SC2_RACESWAP_LOC_ID_OFFSET + 12701, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.UNSEALING_THE_PAST_T.mission_name, + "First Stasis Lock", + SC2_RACESWAP_LOC_ID_OFFSET + 12702, + LocationType.EXTRA, + lambda state: ( + logic.advanced_tactics + or logic.terran_unsealing_the_past_requirement(state) + ), + ), + make_location_data( + SC2Mission.UNSEALING_THE_PAST_T.mission_name, + "Second Stasis Lock", + SC2_RACESWAP_LOC_ID_OFFSET + 12703, + LocationType.EXTRA, + logic.terran_unsealing_the_past_requirement, + ), + make_location_data( + SC2Mission.UNSEALING_THE_PAST_T.mission_name, + "Third Stasis Lock", + SC2_RACESWAP_LOC_ID_OFFSET + 12704, + LocationType.EXTRA, + logic.terran_unsealing_the_past_requirement, + ), + make_location_data( + SC2Mission.UNSEALING_THE_PAST_T.mission_name, + "Fourth Stasis Lock", + SC2_RACESWAP_LOC_ID_OFFSET + 12705, + LocationType.EXTRA, + logic.terran_unsealing_the_past_requirement, + ), + make_location_data( + SC2Mission.UNSEALING_THE_PAST_T.mission_name, + "South Power Core", + SC2_RACESWAP_LOC_ID_OFFSET + 12706, + LocationType.VANILLA, + lambda state: ( + logic.terran_unsealing_the_past_requirement(state) + and ( + adv_tactics + or logic.terran_air(state) + or state.has_all( + {item_names.GOLIATH, item_names.GOLIATH_JUMP_JETS}, player + ) + ) + ), + ), + make_location_data( + SC2Mission.UNSEALING_THE_PAST_T.mission_name, + "East Power Core", + SC2_RACESWAP_LOC_ID_OFFSET + 12707, + LocationType.VANILLA, + lambda state: ( + logic.terran_unsealing_the_past_requirement(state) + and ( + adv_tactics + or logic.terran_air(state) + or state.has_all( + {item_names.GOLIATH, item_names.GOLIATH_JUMP_JETS}, player + ) + ) + ), + ), + make_location_data( + SC2Mission.UNSEALING_THE_PAST_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 12800, + LocationType.VICTORY, + logic.zerg_unsealing_the_past_requirement, + ), + make_location_data( + SC2Mission.UNSEALING_THE_PAST_Z.mission_name, + "Zerg Cleared", + SC2_RACESWAP_LOC_ID_OFFSET + 12801, + LocationType.EXTRA, + ), + make_location_data( + SC2Mission.UNSEALING_THE_PAST_Z.mission_name, + "First Stasis Lock", + SC2_RACESWAP_LOC_ID_OFFSET + 12802, + LocationType.EXTRA, + lambda state: ( + logic.advanced_tactics + or logic.zerg_unsealing_the_past_requirement(state) + ), + ), + make_location_data( + SC2Mission.UNSEALING_THE_PAST_Z.mission_name, + "Second Stasis Lock", + SC2_RACESWAP_LOC_ID_OFFSET + 12803, + LocationType.EXTRA, + logic.zerg_unsealing_the_past_requirement, + ), + make_location_data( + SC2Mission.UNSEALING_THE_PAST_Z.mission_name, + "Third Stasis Lock", + SC2_RACESWAP_LOC_ID_OFFSET + 12804, + LocationType.EXTRA, + logic.zerg_unsealing_the_past_requirement, + ), + make_location_data( + SC2Mission.UNSEALING_THE_PAST_Z.mission_name, + "Fourth Stasis Lock", + SC2_RACESWAP_LOC_ID_OFFSET + 12805, + LocationType.EXTRA, + logic.zerg_unsealing_the_past_requirement, + ), + make_location_data( + SC2Mission.UNSEALING_THE_PAST_Z.mission_name, + "South Power Core", + SC2_RACESWAP_LOC_ID_OFFSET + 12806, + LocationType.VANILLA, + lambda state: ( + logic.zerg_unsealing_the_past_requirement(state) + and ( + adv_tactics + or ( + state.has(item_names.MUTALISK, player) + or logic.morph_brood_lord(state) + or logic.morph_guardian(state) + ) + ) + ), + ), + make_location_data( + SC2Mission.UNSEALING_THE_PAST_Z.mission_name, + "East Power Core", + SC2_RACESWAP_LOC_ID_OFFSET + 12807, + LocationType.VANILLA, + lambda state: ( + logic.zerg_unsealing_the_past_requirement(state) + and ( + adv_tactics + or ( + state.has(item_names.MUTALISK, player) + or logic.morph_brood_lord(state) + or logic.morph_guardian(state) + ) + ) + ), + ), + make_location_data( + SC2Mission.PURIFICATION_T.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 12900, + LocationType.VICTORY, + logic.terran_purification_requirement, + ), + make_location_data( + SC2Mission.PURIFICATION_T.mission_name, + "North Sector: West Null Circuit", + SC2_RACESWAP_LOC_ID_OFFSET + 12901, + LocationType.VANILLA, + logic.terran_purification_requirement, + ), + make_location_data( + SC2Mission.PURIFICATION_T.mission_name, + "North Sector: Northeast Null Circuit", + SC2_RACESWAP_LOC_ID_OFFSET + 12902, + LocationType.EXTRA, + logic.terran_purification_requirement, + ), + make_location_data( + SC2Mission.PURIFICATION_T.mission_name, + "North Sector: Southeast Null Circuit", + SC2_RACESWAP_LOC_ID_OFFSET + 12903, + LocationType.EXTRA, + logic.terran_purification_requirement, + ), + make_location_data( + SC2Mission.PURIFICATION_T.mission_name, + "South Sector: West Null Circuit", + SC2_RACESWAP_LOC_ID_OFFSET + 12904, + LocationType.VANILLA, + logic.terran_purification_requirement, + ), + make_location_data( + SC2Mission.PURIFICATION_T.mission_name, + "South Sector: North Null Circuit", + SC2_RACESWAP_LOC_ID_OFFSET + 12905, + LocationType.EXTRA, + logic.terran_purification_requirement, + ), + make_location_data( + SC2Mission.PURIFICATION_T.mission_name, + "South Sector: East Null Circuit", + SC2_RACESWAP_LOC_ID_OFFSET + 12906, + LocationType.EXTRA, + logic.terran_purification_requirement, + ), + make_location_data( + SC2Mission.PURIFICATION_T.mission_name, + "West Sector: West Null Circuit", + SC2_RACESWAP_LOC_ID_OFFSET + 12907, + LocationType.VANILLA, + logic.terran_purification_requirement, + ), + make_location_data( + SC2Mission.PURIFICATION_T.mission_name, + "West Sector: Mid Null Circuit", + SC2_RACESWAP_LOC_ID_OFFSET + 12908, + LocationType.EXTRA, + logic.terran_purification_requirement, + ), + make_location_data( + SC2Mission.PURIFICATION_T.mission_name, + "West Sector: East Null Circuit", + SC2_RACESWAP_LOC_ID_OFFSET + 12909, + LocationType.EXTRA, + logic.terran_purification_requirement, + ), + make_location_data( + SC2Mission.PURIFICATION_T.mission_name, + "East Sector: North Null Circuit", + SC2_RACESWAP_LOC_ID_OFFSET + 12910, + LocationType.VANILLA, + logic.terran_purification_requirement, + ), + make_location_data( + SC2Mission.PURIFICATION_T.mission_name, + "East Sector: West Null Circuit", + SC2_RACESWAP_LOC_ID_OFFSET + 12911, + LocationType.EXTRA, + logic.terran_purification_requirement, + ), + make_location_data( + SC2Mission.PURIFICATION_T.mission_name, + "East Sector: South Null Circuit", + SC2_RACESWAP_LOC_ID_OFFSET + 12912, + LocationType.EXTRA, + logic.terran_purification_requirement, + ), + make_location_data( + SC2Mission.PURIFICATION_T.mission_name, + "Purifier Warden", + SC2_RACESWAP_LOC_ID_OFFSET + 12913, + LocationType.VANILLA, + logic.terran_purification_requirement, + ), + make_location_data( + SC2Mission.PURIFICATION_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 13000, + LocationType.VICTORY, + logic.zerg_purification_requirement, + ), + make_location_data( + SC2Mission.PURIFICATION_Z.mission_name, + "North Sector: West Null Circuit", + SC2_RACESWAP_LOC_ID_OFFSET + 13001, + LocationType.VANILLA, + logic.zerg_purification_requirement, + ), + make_location_data( + SC2Mission.PURIFICATION_Z.mission_name, + "North Sector: Northeast Null Circuit", + SC2_RACESWAP_LOC_ID_OFFSET + 13002, + LocationType.EXTRA, + logic.zerg_purification_requirement, + ), + make_location_data( + SC2Mission.PURIFICATION_Z.mission_name, + "North Sector: Southeast Null Circuit", + SC2_RACESWAP_LOC_ID_OFFSET + 13003, + LocationType.EXTRA, + logic.zerg_purification_requirement, + ), + make_location_data( + SC2Mission.PURIFICATION_Z.mission_name, + "South Sector: West Null Circuit", + SC2_RACESWAP_LOC_ID_OFFSET + 13004, + LocationType.VANILLA, + logic.zerg_purification_requirement, + ), + make_location_data( + SC2Mission.PURIFICATION_Z.mission_name, + "South Sector: North Null Circuit", + SC2_RACESWAP_LOC_ID_OFFSET + 13005, + LocationType.EXTRA, + logic.zerg_purification_requirement, + ), + make_location_data( + SC2Mission.PURIFICATION_Z.mission_name, + "South Sector: East Null Circuit", + SC2_RACESWAP_LOC_ID_OFFSET + 13006, + LocationType.EXTRA, + logic.zerg_purification_requirement, + ), + make_location_data( + SC2Mission.PURIFICATION_Z.mission_name, + "West Sector: West Null Circuit", + SC2_RACESWAP_LOC_ID_OFFSET + 13007, + LocationType.VANILLA, + logic.zerg_purification_requirement, + ), + make_location_data( + SC2Mission.PURIFICATION_Z.mission_name, + "West Sector: Mid Null Circuit", + SC2_RACESWAP_LOC_ID_OFFSET + 13008, + LocationType.EXTRA, + logic.zerg_purification_requirement, + ), + make_location_data( + SC2Mission.PURIFICATION_Z.mission_name, + "West Sector: East Null Circuit", + SC2_RACESWAP_LOC_ID_OFFSET + 13009, + LocationType.EXTRA, + logic.zerg_purification_requirement, + ), + make_location_data( + SC2Mission.PURIFICATION_Z.mission_name, + "East Sector: North Null Circuit", + SC2_RACESWAP_LOC_ID_OFFSET + 13010, + LocationType.VANILLA, + logic.zerg_purification_requirement, + ), + make_location_data( + SC2Mission.PURIFICATION_Z.mission_name, + "East Sector: West Null Circuit", + SC2_RACESWAP_LOC_ID_OFFSET + 13011, + LocationType.EXTRA, + logic.zerg_purification_requirement, + ), + make_location_data( + SC2Mission.PURIFICATION_Z.mission_name, + "East Sector: South Null Circuit", + SC2_RACESWAP_LOC_ID_OFFSET + 13012, + LocationType.EXTRA, + logic.zerg_purification_requirement, + ), + make_location_data( + SC2Mission.PURIFICATION_Z.mission_name, + "Purifier Warden", + SC2_RACESWAP_LOC_ID_OFFSET + 13013, + LocationType.VANILLA, + logic.zerg_purification_requirement, + ), + make_location_data( + SC2Mission.STEPS_OF_THE_RITE_T.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 13100, + LocationType.VICTORY, + logic.terran_steps_of_the_rite_requirement, + ), + make_location_data( + SC2Mission.STEPS_OF_THE_RITE_T.mission_name, + "First Terrazine Fog", + SC2_RACESWAP_LOC_ID_OFFSET + 13101, + LocationType.EXTRA, + logic.terran_steps_of_the_rite_requirement, + ), + make_location_data( + SC2Mission.STEPS_OF_THE_RITE_T.mission_name, + "Southwest Guardian", + SC2_RACESWAP_LOC_ID_OFFSET + 13102, + LocationType.EXTRA, + logic.terran_steps_of_the_rite_requirement, + ), + make_location_data( + SC2Mission.STEPS_OF_THE_RITE_T.mission_name, + "West Guardian", + SC2_RACESWAP_LOC_ID_OFFSET + 13103, + LocationType.EXTRA, + logic.terran_steps_of_the_rite_requirement, + ), + make_location_data( + SC2Mission.STEPS_OF_THE_RITE_T.mission_name, + "Northwest Guardian", + SC2_RACESWAP_LOC_ID_OFFSET + 13104, + LocationType.EXTRA, + logic.terran_steps_of_the_rite_requirement, + ), + make_location_data( + SC2Mission.STEPS_OF_THE_RITE_T.mission_name, + "Northeast Guardian", + SC2_RACESWAP_LOC_ID_OFFSET + 13105, + LocationType.EXTRA, + logic.terran_steps_of_the_rite_requirement, + ), + make_location_data( + SC2Mission.STEPS_OF_THE_RITE_T.mission_name, + "North Mothership", + SC2_RACESWAP_LOC_ID_OFFSET + 13106, + LocationType.VANILLA, + logic.terran_steps_of_the_rite_requirement, + hard_rule=logic.terran_any_anti_air, + ), + make_location_data( + SC2Mission.STEPS_OF_THE_RITE_T.mission_name, + "South Mothership", + SC2_RACESWAP_LOC_ID_OFFSET + 13107, + LocationType.VANILLA, + logic.terran_steps_of_the_rite_requirement, + hard_rule=logic.terran_any_anti_air, + ), + make_location_data( + SC2Mission.STEPS_OF_THE_RITE_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 13200, + LocationType.VICTORY, + logic.zerg_steps_of_the_rite_requirement, + ), + make_location_data( + SC2Mission.STEPS_OF_THE_RITE_Z.mission_name, + "First Terrazine Fog", + SC2_RACESWAP_LOC_ID_OFFSET + 13201, + LocationType.EXTRA, + logic.zerg_steps_of_the_rite_requirement, + ), + make_location_data( + SC2Mission.STEPS_OF_THE_RITE_Z.mission_name, + "Southwest Guardian", + SC2_RACESWAP_LOC_ID_OFFSET + 13202, + LocationType.EXTRA, + logic.zerg_steps_of_the_rite_requirement, + ), + make_location_data( + SC2Mission.STEPS_OF_THE_RITE_Z.mission_name, + "West Guardian", + SC2_RACESWAP_LOC_ID_OFFSET + 13203, + LocationType.EXTRA, + logic.zerg_steps_of_the_rite_requirement, + ), + make_location_data( + SC2Mission.STEPS_OF_THE_RITE_Z.mission_name, + "Northwest Guardian", + SC2_RACESWAP_LOC_ID_OFFSET + 13204, + LocationType.EXTRA, + logic.zerg_steps_of_the_rite_requirement, + ), + make_location_data( + SC2Mission.STEPS_OF_THE_RITE_Z.mission_name, + "Northeast Guardian", + SC2_RACESWAP_LOC_ID_OFFSET + 13205, + LocationType.EXTRA, + logic.zerg_steps_of_the_rite_requirement, + ), + make_location_data( + SC2Mission.STEPS_OF_THE_RITE_Z.mission_name, + "North Mothership", + SC2_RACESWAP_LOC_ID_OFFSET + 13206, + LocationType.VANILLA, + logic.zerg_steps_of_the_rite_requirement, + hard_rule=logic.zerg_any_anti_air, + ), + make_location_data( + SC2Mission.STEPS_OF_THE_RITE_Z.mission_name, + "South Mothership", + SC2_RACESWAP_LOC_ID_OFFSET + 13207, + LocationType.VANILLA, + logic.zerg_steps_of_the_rite_requirement, + hard_rule=logic.zerg_any_anti_air, + ), + make_location_data( + SC2Mission.RAK_SHIR_T.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 13300, + LocationType.VICTORY, + logic.terran_rak_shir_requirement, + ), + make_location_data( + SC2Mission.RAK_SHIR_T.mission_name, + "North Slayn Elemental", + SC2_RACESWAP_LOC_ID_OFFSET + 13301, + LocationType.VANILLA, + logic.terran_rak_shir_requirement, + hard_rule=logic.terran_any_anti_air, + ), + make_location_data( + SC2Mission.RAK_SHIR_T.mission_name, + "Southwest Slayn Elemental", + SC2_RACESWAP_LOC_ID_OFFSET + 13302, + LocationType.VANILLA, + logic.terran_rak_shir_requirement, + hard_rule=logic.terran_any_anti_air, + ), + make_location_data( + SC2Mission.RAK_SHIR_T.mission_name, + "East Slayn Elemental", + SC2_RACESWAP_LOC_ID_OFFSET + 13303, + LocationType.VANILLA, + logic.terran_rak_shir_requirement, + hard_rule=logic.terran_any_anti_air, + ), + make_location_data( + SC2Mission.RAK_SHIR_T.mission_name, + "Resource Pickups", + SC2_RACESWAP_LOC_ID_OFFSET + 13304, + LocationType.EXTRA, + logic.terran_rak_shir_requirement, + ), + make_location_data( + SC2Mission.RAK_SHIR_T.mission_name, + "Destroy Nexuses", + SC2_RACESWAP_LOC_ID_OFFSET + 13305, + LocationType.CHALLENGE, + logic.terran_rak_shir_requirement, + ), + make_location_data( + SC2Mission.RAK_SHIR_T.mission_name, + "Win in under 15 minutes", + SC2_RACESWAP_LOC_ID_OFFSET + 13306, + LocationType.MASTERY, + logic.terran_rak_shir_requirement, + flags=LocationFlag.SPEEDRUN, + ), + make_location_data( + SC2Mission.RAK_SHIR_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 13400, + LocationType.VICTORY, + logic.zerg_rak_shir_requirement, + ), + make_location_data( + SC2Mission.RAK_SHIR_Z.mission_name, + "North Slayn Elemental", + SC2_RACESWAP_LOC_ID_OFFSET + 13401, + LocationType.VANILLA, + logic.zerg_rak_shir_requirement, + hard_rule=logic.zerg_any_anti_air, + ), + make_location_data( + SC2Mission.RAK_SHIR_Z.mission_name, + "Southwest Slayn Elemental", + SC2_RACESWAP_LOC_ID_OFFSET + 13402, + LocationType.VANILLA, + logic.zerg_rak_shir_requirement, + hard_rule=logic.zerg_any_anti_air, + ), + make_location_data( + SC2Mission.RAK_SHIR_Z.mission_name, + "East Slayn Elemental", + SC2_RACESWAP_LOC_ID_OFFSET + 13403, + LocationType.VANILLA, + logic.zerg_rak_shir_requirement, + hard_rule=logic.zerg_any_anti_air, + ), + make_location_data( + SC2Mission.RAK_SHIR_Z.mission_name, + "Resource Pickups", + SC2_RACESWAP_LOC_ID_OFFSET + 13404, + LocationType.EXTRA, + logic.zerg_rak_shir_requirement, + ), + make_location_data( + SC2Mission.RAK_SHIR_Z.mission_name, + "Destroy Nexuses", + SC2_RACESWAP_LOC_ID_OFFSET + 13405, + LocationType.CHALLENGE, + logic.zerg_rak_shir_requirement, + ), + make_location_data( + SC2Mission.RAK_SHIR_Z.mission_name, + "Win in under 15 minutes", + SC2_RACESWAP_LOC_ID_OFFSET + 13406, + LocationType.MASTERY, + logic.zerg_rak_shir_requirement, + flags=LocationFlag.SPEEDRUN, + ), + make_location_data( + SC2Mission.TEMPLAR_S_CHARGE_T.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 13500, + LocationType.VICTORY, + logic.terran_templars_charge_requirement, + ), + make_location_data( + SC2Mission.TEMPLAR_S_CHARGE_T.mission_name, + "Northwest Power Core", + SC2_RACESWAP_LOC_ID_OFFSET + 13501, + LocationType.EXTRA, + logic.terran_templars_charge_requirement, + ), + make_location_data( + SC2Mission.TEMPLAR_S_CHARGE_T.mission_name, + "Northeast Power Core", + SC2_RACESWAP_LOC_ID_OFFSET + 13502, + LocationType.EXTRA, + logic.terran_templars_charge_requirement, + ), + make_location_data( + SC2Mission.TEMPLAR_S_CHARGE_T.mission_name, + "Southeast Power Core", + SC2_RACESWAP_LOC_ID_OFFSET + 13503, + LocationType.EXTRA, + logic.terran_templars_charge_requirement, + ), + make_location_data( + SC2Mission.TEMPLAR_S_CHARGE_T.mission_name, + "West Hybrid Stasis Chamber", + SC2_RACESWAP_LOC_ID_OFFSET + 13504, + LocationType.VANILLA, + logic.terran_templars_charge_requirement, + ), + make_location_data( + SC2Mission.TEMPLAR_S_CHARGE_T.mission_name, + "Southeast Hybrid Stasis Chamber", + SC2_RACESWAP_LOC_ID_OFFSET + 13505, + LocationType.VANILLA, + lambda state: ( + logic.terran_templars_charge_requirement(state) + and logic.terran_air(state) + ), + ), + make_location_data( + SC2Mission.TEMPLAR_S_CHARGE_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 13600, + LocationType.VICTORY, + logic.zerg_templars_charge_requirement, + ), + make_location_data( + SC2Mission.TEMPLAR_S_CHARGE_Z.mission_name, + "Northwest Power Core", + SC2_RACESWAP_LOC_ID_OFFSET + 13601, + LocationType.EXTRA, + logic.zerg_templars_charge_requirement, + ), + make_location_data( + SC2Mission.TEMPLAR_S_CHARGE_Z.mission_name, + "Northeast Power Core", + SC2_RACESWAP_LOC_ID_OFFSET + 13602, + LocationType.EXTRA, + logic.zerg_templars_charge_requirement, + ), + make_location_data( + SC2Mission.TEMPLAR_S_CHARGE_Z.mission_name, + "Southeast Power Core", + SC2_RACESWAP_LOC_ID_OFFSET + 13603, + LocationType.EXTRA, + logic.zerg_templars_charge_requirement, + ), + make_location_data( + SC2Mission.TEMPLAR_S_CHARGE_Z.mission_name, + "West Hybrid Stasis Chamber", + SC2_RACESWAP_LOC_ID_OFFSET + 13604, + LocationType.VANILLA, + logic.zerg_templars_charge_requirement, + ), + make_location_data( + SC2Mission.TEMPLAR_S_CHARGE_Z.mission_name, + "Southeast Hybrid Stasis Chamber", + SC2_RACESWAP_LOC_ID_OFFSET + 13605, + LocationType.VANILLA, + logic.zerg_templars_charge_requirement, + ), + make_location_data( + SC2Mission.THE_HOST_T.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 13900, + LocationType.VICTORY, + logic.terran_the_host_requirement, + ), + make_location_data( + SC2Mission.THE_HOST_T.mission_name, + "Southeast Void Shard", + SC2_RACESWAP_LOC_ID_OFFSET + 13901, + LocationType.EXTRA, + logic.terran_the_host_requirement, + ), + make_location_data( + SC2Mission.THE_HOST_T.mission_name, + "South Void Shard", + SC2_RACESWAP_LOC_ID_OFFSET + 13902, + LocationType.EXTRA, + logic.terran_the_host_requirement, + ), + make_location_data( + SC2Mission.THE_HOST_T.mission_name, + "Southwest Void Shard", + SC2_RACESWAP_LOC_ID_OFFSET + 13903, + LocationType.EXTRA, + logic.terran_the_host_requirement, + ), + make_location_data( + SC2Mission.THE_HOST_T.mission_name, + "North Void Shard", + SC2_RACESWAP_LOC_ID_OFFSET + 13904, + LocationType.EXTRA, + logic.terran_the_host_requirement, + ), + make_location_data( + SC2Mission.THE_HOST_T.mission_name, + "Northwest Void Shard", + SC2_RACESWAP_LOC_ID_OFFSET + 13905, + LocationType.EXTRA, + logic.terran_the_host_requirement, + ), + make_location_data( + SC2Mission.THE_HOST_T.mission_name, + "Nerazim Warp in Zone", + SC2_RACESWAP_LOC_ID_OFFSET + 13906, + LocationType.VANILLA, + logic.terran_the_host_requirement, + ), + make_location_data( + SC2Mission.THE_HOST_T.mission_name, + "Tal'darim Warp in Zone", + SC2_RACESWAP_LOC_ID_OFFSET + 13907, + LocationType.VANILLA, + logic.terran_the_host_requirement, + ), + make_location_data( + SC2Mission.THE_HOST_T.mission_name, + "Purifier Warp in Zone", + SC2_RACESWAP_LOC_ID_OFFSET + 13908, + LocationType.VANILLA, + logic.terran_the_host_requirement, + ), + make_location_data( + SC2Mission.THE_HOST_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 14000, + LocationType.VICTORY, + logic.zerg_the_host_requirement, + ), + make_location_data( + SC2Mission.THE_HOST_Z.mission_name, + "Southeast Void Shard", + SC2_RACESWAP_LOC_ID_OFFSET + 14001, + LocationType.EXTRA, + logic.zerg_the_host_requirement, + ), + make_location_data( + SC2Mission.THE_HOST_Z.mission_name, + "South Void Shard", + SC2_RACESWAP_LOC_ID_OFFSET + 14002, + LocationType.EXTRA, + logic.zerg_the_host_requirement, + ), + make_location_data( + SC2Mission.THE_HOST_Z.mission_name, + "Southwest Void Shard", + SC2_RACESWAP_LOC_ID_OFFSET + 14003, + LocationType.EXTRA, + logic.zerg_the_host_requirement, + ), + make_location_data( + SC2Mission.THE_HOST_Z.mission_name, + "North Void Shard", + SC2_RACESWAP_LOC_ID_OFFSET + 14004, + LocationType.EXTRA, + logic.zerg_the_host_requirement, + ), + make_location_data( + SC2Mission.THE_HOST_Z.mission_name, + "Northwest Void Shard", + SC2_RACESWAP_LOC_ID_OFFSET + 14005, + LocationType.EXTRA, + logic.zerg_the_host_requirement, + ), + make_location_data( + SC2Mission.THE_HOST_Z.mission_name, + "Nerazim Warp in Zone", + SC2_RACESWAP_LOC_ID_OFFSET + 14006, + LocationType.VANILLA, + logic.zerg_the_host_requirement, + ), + make_location_data( + SC2Mission.THE_HOST_Z.mission_name, + "Tal'darim Warp in Zone", + SC2_RACESWAP_LOC_ID_OFFSET + 14007, + LocationType.VANILLA, + logic.zerg_the_host_requirement, + ), + make_location_data( + SC2Mission.THE_HOST_Z.mission_name, + "Purifier Warp in Zone", + SC2_RACESWAP_LOC_ID_OFFSET + 14008, + LocationType.VANILLA, + logic.zerg_the_host_requirement, + ), + make_location_data( + SC2Mission.SALVATION_T.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 14100, + LocationType.VICTORY, + logic.terran_salvation_requirement, + ), + make_location_data( + SC2Mission.SALVATION_T.mission_name, + "Fabrication Matrix", + SC2_RACESWAP_LOC_ID_OFFSET + 14101, + LocationType.EXTRA, + logic.terran_salvation_requirement, + ), + make_location_data( + SC2Mission.SALVATION_T.mission_name, + "Assault Cluster", + SC2_RACESWAP_LOC_ID_OFFSET + 14102, + LocationType.EXTRA, + logic.terran_salvation_requirement, + ), + make_location_data( + SC2Mission.SALVATION_T.mission_name, + "Hull Breach", + SC2_RACESWAP_LOC_ID_OFFSET + 14103, + LocationType.EXTRA, + logic.terran_salvation_requirement, + ), + make_location_data( + SC2Mission.SALVATION_T.mission_name, + "Core Critical", + SC2_RACESWAP_LOC_ID_OFFSET + 14104, + LocationType.EXTRA, + logic.terran_salvation_requirement, + ), + make_location_data( + SC2Mission.SALVATION_T.mission_name, + "Kill Brutalisk", + SC2_RACESWAP_LOC_ID_OFFSET + 14105, + LocationType.MASTERY, + logic.terran_salvation_requirement, + ), + make_location_data( + SC2Mission.SALVATION_Z.mission_name, + "Victory", + SC2_RACESWAP_LOC_ID_OFFSET + 14200, + LocationType.VICTORY, + logic.zerg_salvation_requirement, + ), + make_location_data( + SC2Mission.SALVATION_Z.mission_name, + "Fabrication Matrix", + SC2_RACESWAP_LOC_ID_OFFSET + 14201, + LocationType.EXTRA, + logic.zerg_salvation_requirement, + ), + make_location_data( + SC2Mission.SALVATION_Z.mission_name, + "Assault Cluster", + SC2_RACESWAP_LOC_ID_OFFSET + 14202, + LocationType.EXTRA, + logic.zerg_salvation_requirement, + ), + make_location_data( + SC2Mission.SALVATION_Z.mission_name, + "Hull Breach", + SC2_RACESWAP_LOC_ID_OFFSET + 14203, + LocationType.EXTRA, + logic.zerg_salvation_requirement, + ), + make_location_data( + SC2Mission.SALVATION_Z.mission_name, + "Core Critical", + SC2_RACESWAP_LOC_ID_OFFSET + 14204, + LocationType.EXTRA, + logic.zerg_salvation_requirement, + ), + make_location_data( + SC2Mission.SALVATION_Z.mission_name, + "Kill Brutalisk", + SC2_RACESWAP_LOC_ID_OFFSET + 14205, + LocationType.MASTERY, + logic.zerg_salvation_requirement, + ), + ] + + # Filtering out excluded locations + if world is not None: + excluded_location_types = get_location_types( + world, LocationInclusion.option_disabled + ) + excluded_location_flags = get_location_flags( + world, LocationInclusion.option_disabled + ) + chance_location_types = get_location_types( + world, LocationInclusion.option_half_chance + ) + chance_location_flags = get_location_flags( + world, LocationInclusion.option_half_chance + ) + plando_locations = get_plando_locations(world) + exclude_locations = world.options.exclude_locations.value + + def include_location(location: LocationData) -> bool: + if location.type is LocationType.VICTORY: + return True + if location.name in plando_locations: + return True + if location.name in exclude_locations: + return False + if location.flags & excluded_location_flags: + return False + if location.type in excluded_location_types: + return False + if location.flags & chance_location_flags: + if world.random.random() < 0.5: + return False + if location.type in chance_location_types: + if world.random.random() < 0.5: + return False + return True + + location_table = [ + location for location in location_table if include_location(location) + ] + beat_events: List[LocationData] = [] + victory_caches: List[LocationData] = [] + VICTORY_CACHE_SIZE = 10 + for location_data in location_table: + # Generating Beat event and Victory Cache locations + if location_data.type == LocationType.VICTORY: + beat_events.append( + location_data._replace(name="Beat " + location_data.region, code=None) # type: ignore + ) + for v in range(VICTORY_CACHE_SIZE): + victory_caches.append( + location_data._replace( + name=location_data.name + f" Cache ({v + 1})", + code=location_data.code + VICTORY_CACHE_OFFSET + v, + type=LocationType.VICTORY_CACHE, + ) + ) + + return tuple(location_table + beat_events + victory_caches) + + +DEFAULT_LOCATION_LIST = get_locations(None) +"""A location table with `None` as the input world; does not contain logic rules""" + +lookup_location_id_to_type = { + loc.code: loc.type for loc in DEFAULT_LOCATION_LIST if loc.code is not None +} +lookup_location_id_to_flags = { + loc.code: loc.flags for loc in DEFAULT_LOCATION_LIST if loc.code is not None +} diff --git a/worlds/sc2/mission_groups.py b/worlds/sc2/mission_groups.py new file mode 100644 index 00000000..de6e7d34 --- /dev/null +++ b/worlds/sc2/mission_groups.py @@ -0,0 +1,194 @@ +""" +Mission group aliases for use in yaml options. +""" + +from typing import Dict, List, Set +from .mission_tables import SC2Mission, MissionFlag, SC2Campaign + + +class MissionGroupNames: + ALL_MISSIONS = "All Missions" + WOL_MISSIONS = "WoL Missions" + HOTS_MISSIONS = "HotS Missions" + LOTV_MISSIONS = "LotV Missions" + NCO_MISSIONS = "NCO Missions" + PROPHECY_MISSIONS = "Prophecy Missions" + PROLOGUE_MISSIONS = "Prologue Missions" + EPILOGUE_MISSIONS = "Epilogue Missions" + + TERRAN_MISSIONS = "Terran Missions" + ZERG_MISSIONS = "Zerg Missions" + PROTOSS_MISSIONS = "Protoss Missions" + NOBUILD_MISSIONS = "No-Build Missions" + DEFENSE_MISSIONS = "Defense Missions" + AUTO_SCROLLER_MISSIONS = "Auto-Scroller Missions" + COUNTDOWN_MISSIONS = "Countdown Missions" + KERRIGAN_MISSIONS = "Kerrigan Missions" + VANILLA_SOA_MISSIONS = "Vanilla SOA Missions" + TERRAN_ALLY_MISSIONS = "Controllable Terran Ally Missions" + ZERG_ALLY_MISSIONS = "Controllable Zerg Ally Missions" + PROTOSS_ALLY_MISSIONS = "Controllable Protoss Ally Missions" + VS_TERRAN_MISSIONS = "Vs Terran Missions" + VS_ZERG_MISSIONS = "Vs Zerg Missions" + VS_PROTOSS_MISSIONS = "Vs Protoss Missions" + RACESWAP_MISSIONS = "Raceswap Missions" + + # By planet + PLANET_MAR_SARA_MISSIONS = "Planet Mar Sara" + PLANET_CHAR_MISSIONS = "Planet Char" + PLANET_KORHAL_MISSIONS = "Planet Korhal" + PLANET_AIUR_MISSIONS = "Planet Aiur" + + # By quest chain + WOL_MAR_SARA_MISSIONS = "WoL Mar Sara" + WOL_COLONIST_MISSIONS = "WoL Colonist" + WOL_ARTIFACT_MISSIONS = "WoL Artifact" + WOL_COVERT_MISSIONS = "WoL Covert" + WOL_REBELLION_MISSIONS = "WoL Rebellion" + WOL_CHAR_MISSIONS = "WoL Char" + + HOTS_UMOJA_MISSIONS = "HotS Umoja" + HOTS_KALDIR_MISSIONS = "HotS Kaldir" + HOTS_CHAR_MISSIONS = "HotS Char" + HOTS_ZERUS_MISSIONS = "HotS Zerus" + HOTS_SKYGEIRR_MISSIONS = "HotS Skygeirr Station" + HOTS_DOMINION_SPACE_MISSIONS = "HotS Dominion Space" + HOTS_KORHAL_MISSIONS = "HotS Korhal" + + LOTV_AIUR_MISSIONS = "LotV Aiur" + LOTV_KORHAL_MISSIONS = "LotV Korhal" + LOTV_SHAKURAS_MISSIONS = "LotV Shakuras" + LOTV_ULNAR_MISSIONS = "LotV Ulnar" + LOTV_PURIFIER_MISSIONS = "LotV Purifier" + LOTV_TALDARIM_MISSIONS = "LotV Tal'darim" + LOTV_MOEBIUS_MISSIONS = "LotV Moebius" + LOTV_RETURN_TO_AIUR_MISSIONS = "LotV Return to Aiur" + + NCO_MISSION_PACK_1 = "NCO Mission Pack 1" + NCO_MISSION_PACK_2 = "NCO Mission Pack 2" + NCO_MISSION_PACK_3 = "NCO Mission Pack 3" + + @classmethod + def get_all_group_names(cls) -> Set[str]: + return { + name + for identifier, name in cls.__dict__.items() + if not identifier.startswith("_") and not identifier.startswith("get_") + } + + +mission_groups: Dict[str, List[str]] = {} + +mission_groups[MissionGroupNames.ALL_MISSIONS] = [mission.mission_name for mission in SC2Mission] +for group_name, campaign in ( + (MissionGroupNames.WOL_MISSIONS, SC2Campaign.WOL), + (MissionGroupNames.HOTS_MISSIONS, SC2Campaign.HOTS), + (MissionGroupNames.LOTV_MISSIONS, SC2Campaign.LOTV), + (MissionGroupNames.NCO_MISSIONS, SC2Campaign.NCO), + (MissionGroupNames.PROPHECY_MISSIONS, SC2Campaign.PROPHECY), + (MissionGroupNames.PROLOGUE_MISSIONS, SC2Campaign.PROLOGUE), + (MissionGroupNames.EPILOGUE_MISSIONS, SC2Campaign.EPILOGUE), +): + mission_groups[group_name] = [mission.mission_name for mission in SC2Mission if mission.campaign == campaign] + +for group_name, flags in ( + (MissionGroupNames.TERRAN_MISSIONS, MissionFlag.Terran), + (MissionGroupNames.ZERG_MISSIONS, MissionFlag.Zerg), + (MissionGroupNames.PROTOSS_MISSIONS, MissionFlag.Protoss), + (MissionGroupNames.NOBUILD_MISSIONS, MissionFlag.NoBuild), + (MissionGroupNames.DEFENSE_MISSIONS, MissionFlag.Defense), + (MissionGroupNames.AUTO_SCROLLER_MISSIONS, MissionFlag.AutoScroller), + (MissionGroupNames.COUNTDOWN_MISSIONS, MissionFlag.Countdown), + (MissionGroupNames.KERRIGAN_MISSIONS, MissionFlag.Kerrigan), + (MissionGroupNames.VANILLA_SOA_MISSIONS, MissionFlag.VanillaSoa), + (MissionGroupNames.TERRAN_ALLY_MISSIONS, MissionFlag.AiTerranAlly), + (MissionGroupNames.ZERG_ALLY_MISSIONS, MissionFlag.AiZergAlly), + (MissionGroupNames.PROTOSS_ALLY_MISSIONS, MissionFlag.AiProtossAlly), + (MissionGroupNames.VS_TERRAN_MISSIONS, MissionFlag.VsTerran), + (MissionGroupNames.VS_ZERG_MISSIONS, MissionFlag.VsZerg), + (MissionGroupNames.VS_PROTOSS_MISSIONS, MissionFlag.VsProtoss), + (MissionGroupNames.RACESWAP_MISSIONS, MissionFlag.RaceSwap), +): + mission_groups[group_name] = [mission.mission_name for mission in SC2Mission if flags in mission.flags] + +for group_name, campaign, chain_name in ( + (MissionGroupNames.WOL_MAR_SARA_MISSIONS, SC2Campaign.WOL, "Mar Sara"), + (MissionGroupNames.WOL_COLONIST_MISSIONS, SC2Campaign.WOL, "Colonist"), + (MissionGroupNames.WOL_ARTIFACT_MISSIONS, SC2Campaign.WOL, "Artifact"), + (MissionGroupNames.WOL_COVERT_MISSIONS, SC2Campaign.WOL, "Covert"), + (MissionGroupNames.WOL_REBELLION_MISSIONS, SC2Campaign.WOL, "Rebellion"), + (MissionGroupNames.WOL_CHAR_MISSIONS, SC2Campaign.WOL, "Char"), + (MissionGroupNames.HOTS_UMOJA_MISSIONS, SC2Campaign.HOTS, "Umoja"), + (MissionGroupNames.HOTS_KALDIR_MISSIONS, SC2Campaign.HOTS, "Kaldir"), + (MissionGroupNames.HOTS_CHAR_MISSIONS, SC2Campaign.HOTS, "Char"), + (MissionGroupNames.HOTS_ZERUS_MISSIONS, SC2Campaign.HOTS, "Zerus"), + (MissionGroupNames.HOTS_SKYGEIRR_MISSIONS, SC2Campaign.HOTS, "Skygeirr Station"), + (MissionGroupNames.HOTS_DOMINION_SPACE_MISSIONS, SC2Campaign.HOTS, "Dominion Space"), + (MissionGroupNames.HOTS_KORHAL_MISSIONS, SC2Campaign.HOTS, "Korhal"), + (MissionGroupNames.LOTV_AIUR_MISSIONS, SC2Campaign.LOTV, "Aiur"), + (MissionGroupNames.LOTV_KORHAL_MISSIONS, SC2Campaign.LOTV, "Korhal"), + (MissionGroupNames.LOTV_SHAKURAS_MISSIONS, SC2Campaign.LOTV, "Shakuras"), + (MissionGroupNames.LOTV_ULNAR_MISSIONS, SC2Campaign.LOTV, "Ulnar"), + (MissionGroupNames.LOTV_PURIFIER_MISSIONS, SC2Campaign.LOTV, "Purifier"), + (MissionGroupNames.LOTV_TALDARIM_MISSIONS, SC2Campaign.LOTV, "Tal'darim"), + (MissionGroupNames.LOTV_MOEBIUS_MISSIONS, SC2Campaign.LOTV, "Moebius"), + (MissionGroupNames.LOTV_RETURN_TO_AIUR_MISSIONS, SC2Campaign.LOTV, "Return to Aiur"), +): + mission_groups[group_name] = [ + mission.mission_name for mission in SC2Mission if mission.campaign == campaign and mission.area == chain_name + ] + +mission_groups[MissionGroupNames.NCO_MISSION_PACK_1] = [ + SC2Mission.THE_ESCAPE.mission_name, + SC2Mission.SUDDEN_STRIKE.mission_name, + SC2Mission.ENEMY_INTELLIGENCE.mission_name, +] +mission_groups[MissionGroupNames.NCO_MISSION_PACK_2] = [ + SC2Mission.TROUBLE_IN_PARADISE.mission_name, + SC2Mission.NIGHT_TERRORS.mission_name, + SC2Mission.FLASHPOINT.mission_name, +] +mission_groups[MissionGroupNames.NCO_MISSION_PACK_3] = [ + SC2Mission.IN_THE_ENEMY_S_SHADOW.mission_name, + SC2Mission.DARK_SKIES.mission_name, + SC2Mission.END_GAME.mission_name, +] + +mission_groups[MissionGroupNames.PLANET_MAR_SARA_MISSIONS] = [ + SC2Mission.LIBERATION_DAY.mission_name, + SC2Mission.THE_OUTLAWS.mission_name, + SC2Mission.ZERO_HOUR.mission_name, +] +mission_groups[MissionGroupNames.PLANET_CHAR_MISSIONS] = [ + SC2Mission.GATES_OF_HELL.mission_name, + SC2Mission.BELLY_OF_THE_BEAST.mission_name, + SC2Mission.SHATTER_THE_SKY.mission_name, + SC2Mission.ALL_IN.mission_name, + SC2Mission.DOMINATION.mission_name, + SC2Mission.FIRE_IN_THE_SKY.mission_name, + SC2Mission.OLD_SOLDIERS.mission_name, +] +mission_groups[MissionGroupNames.PLANET_KORHAL_MISSIONS] = [ + SC2Mission.MEDIA_BLITZ.mission_name, + SC2Mission.PLANETFALL.mission_name, + SC2Mission.DEATH_FROM_ABOVE.mission_name, + SC2Mission.THE_RECKONING.mission_name, + SC2Mission.SKY_SHIELD.mission_name, + SC2Mission.BROTHERS_IN_ARMS.mission_name, +] +mission_groups[MissionGroupNames.PLANET_AIUR_MISSIONS] = [ + SC2Mission.ECHOES_OF_THE_FUTURE.mission_name, + SC2Mission.FOR_AIUR.mission_name, + SC2Mission.THE_GROWING_SHADOW.mission_name, + SC2Mission.THE_SPEAR_OF_ADUN.mission_name, + SC2Mission.TEMPLAR_S_RETURN.mission_name, + SC2Mission.THE_HOST.mission_name, + SC2Mission.SALVATION.mission_name, +] + +for mission in SC2Mission: + if mission.flags & MissionFlag.HasRaceSwap: + short_name = mission.get_short_name() + mission_groups[short_name] = [ + mission_var.mission_name for mission_var in SC2Mission if short_name in mission_var.mission_name + ] diff --git a/worlds/sc2/mission_order/__init__.py b/worlds/sc2/mission_order/__init__.py new file mode 100644 index 00000000..ec533ccd --- /dev/null +++ b/worlds/sc2/mission_order/__init__.py @@ -0,0 +1,66 @@ +from typing import List, Dict, Any, Callable, TYPE_CHECKING + +from BaseClasses import CollectionState +from ..mission_tables import SC2Mission, MissionFlag, get_goal_location +from .mission_pools import SC2MOGenMissionPools + +if TYPE_CHECKING: + from .nodes import SC2MOGenMissionOrder, SC2MOGenMission + +class SC2MissionOrder: + """ + Wrapper class for a generated mission order. Contains helper functions for getting data about generated missions. + """ + + def __init__(self, mission_order_node: 'SC2MOGenMissionOrder', mission_pools: SC2MOGenMissionPools): + self.mission_order_node: 'SC2MOGenMissionOrder' = mission_order_node + """Root node of the mission order structure.""" + self.mission_pools: SC2MOGenMissionPools = mission_pools + """Manager for missions in the mission order.""" + + def get_used_flags(self) -> Dict[MissionFlag, int]: + """Returns a dictionary of all used flags and their appearance count within the mission order. + Flags that don't appear in the mission order also don't appear in this dictionary.""" + return self.mission_pools.get_used_flags() + + def get_used_missions(self) -> List[SC2Mission]: + """Returns a list of all missions used in the mission order.""" + return self.mission_pools.get_used_missions() + + def get_mission_count(self) -> int: + """Returns the amount of missions in the mission order.""" + return sum( + len([mission for mission in layout.missions if not mission.option_empty]) + for campaign in self.mission_order_node.campaigns for layout in campaign.layouts + ) + + def get_starting_missions(self) -> List[SC2Mission]: + """Returns a list containing all the missions that are accessible without beating any other missions.""" + return [ + slot.mission + for campaign in self.mission_order_node.campaigns if campaign.is_always_unlocked() + for layout in campaign.layouts if layout.is_always_unlocked() + for slot in layout.missions if slot.is_always_unlocked() and not slot.option_empty + ] + + def get_completion_condition(self, player: int) -> Callable[[CollectionState], bool]: + """Returns a lambda to determine whether a state has beaten the mission order's required campaigns.""" + final_locations = [get_goal_location(mission.mission) for mission in self.get_final_missions()] + return lambda state, final_locations=final_locations: all(state.can_reach_location(loc, player) for loc in final_locations) + + def get_final_mission_ids(self) -> List[int]: + """Returns the IDs of all missions that are required to beat the mission order.""" + return [mission.mission.id for mission in self.get_final_missions()] + + def get_final_missions(self) -> List['SC2MOGenMission']: + """Returns the slots of all missions that are required to beat the mission order.""" + return self.mission_order_node.goal_missions + + def get_items_to_lock(self) -> Dict[str, int]: + """Returns a dict of item names and amounts that are required by Item entry rules.""" + return self.mission_order_node.items_to_lock + + def get_slot_data(self) -> List[Dict[str, Any]]: + """Parses the mission order into a format usable for slot data.""" + return self.mission_order_node.get_slot_data() + diff --git a/worlds/sc2/mission_order/entry_rules.py b/worlds/sc2/mission_order/entry_rules.py new file mode 100644 index 00000000..cb3afb37 --- /dev/null +++ b/worlds/sc2/mission_order/entry_rules.py @@ -0,0 +1,389 @@ +from __future__ import annotations +from typing import Set, Callable, Dict, List, Union, TYPE_CHECKING, Any, NamedTuple +from abc import ABC, abstractmethod +from dataclasses import dataclass + +from ..mission_tables import SC2Mission +from ..item.item_tables import item_table +from BaseClasses import CollectionState + +if TYPE_CHECKING: + from .nodes import SC2MOGenMission + + +class EntryRule(ABC): + buffer_fulfilled: bool + buffer_depth: int + + def __init__(self) -> None: + self.buffer_fulfilled = False + self.buffer_depth = -1 + + def is_always_fulfilled(self, in_region_creation: bool = False) -> bool: + return self.is_fulfilled(set(), in_region_creation) + + @abstractmethod + def _is_fulfilled(self, beaten_missions: Set[SC2MOGenMission], in_region_creation: bool) -> bool: + """Used during region creation to ensure a beatable mission order. + + `in_region_creation` should determine whether rules that cannot be handled during region creation (like Item rules) + report themselves as fulfilled or unfulfilled.""" + return False + + def is_fulfilled(self, beaten_missions: Set[SC2MOGenMission], in_region_creation: bool) -> bool: + if len(beaten_missions) == 0: + # Special-cased to avoid the buffer + # This is used to determine starting missions + return self._is_fulfilled(beaten_missions, in_region_creation) + self.buffer_fulfilled = self.buffer_fulfilled or self._is_fulfilled(beaten_missions, in_region_creation) + return self.buffer_fulfilled + + @abstractmethod + def _get_depth(self, beaten_missions: Set[SC2MOGenMission]) -> int: + """Used during region creation to determine the minimum depth this entry rule can be cleared at.""" + return -1 + + def get_depth(self, beaten_missions: Set[SC2MOGenMission]) -> int: + if not self.is_fulfilled(beaten_missions, in_region_creation = True): + return -1 + if self.buffer_depth == -1: + self.buffer_depth = self._get_depth(beaten_missions) + return self.buffer_depth + + @abstractmethod + def to_lambda(self, player: int) -> Callable[[CollectionState], bool]: + """Passed to Archipelago for use during item placement.""" + return lambda _: False + + @abstractmethod + def to_slot_data(self) -> RuleData: + """Used in the client to determine accessibility while playing and to populate tooltips.""" + pass + + +@dataclass +class RuleData(ABC): + @abstractmethod + def tooltip(self, indents: int, missions: Dict[int, SC2Mission], done_color: str, not_done_color: str) -> str: + return "" + + @abstractmethod + def shows_single_rule(self) -> bool: + return False + + @abstractmethod + def is_accessible( + self, beaten_missions: Set[int], received_items: Dict[int, int] + ) -> bool: + return False + + +class BeatMissionsEntryRule(EntryRule): + missions_to_beat: List[SC2MOGenMission] + visual_reqs: List[Union[str, SC2MOGenMission]] + + def __init__(self, missions_to_beat: List[SC2MOGenMission], visual_reqs: List[Union[str, SC2MOGenMission]]): + super().__init__() + self.missions_to_beat = missions_to_beat + self.visual_reqs = visual_reqs + + def _is_fulfilled(self, beaten_missions: Set[SC2MOGenMission], in_region_check: bool) -> bool: + return beaten_missions.issuperset(self.missions_to_beat) + + def _get_depth(self, beaten_missions: Set[SC2MOGenMission]) -> int: + return max(mission.min_depth for mission in self.missions_to_beat) + + def to_lambda(self, player: int) -> Callable[[CollectionState], bool]: + return lambda state: state.has_all([mission.beat_item() for mission in self.missions_to_beat], player) + + def to_slot_data(self) -> RuleData: + resolved_reqs: List[Union[str, int]] = [req if isinstance(req, str) else req.mission.id for req in self.visual_reqs] + mission_ids = [mission.mission.id for mission in self.missions_to_beat] + return BeatMissionsRuleData( + mission_ids, + resolved_reqs + ) + + +@dataclass +class BeatMissionsRuleData(RuleData): + mission_ids: List[int] + visual_reqs: List[Union[str, int]] + + def tooltip(self, indents: int, missions: Dict[int, SC2Mission], done_color: str, not_done_color: str) -> str: + indent = " ".join("" for _ in range(indents)) + if len(self.visual_reqs) == 1: + req = self.visual_reqs[0] + return f"Beat {missions[req].mission_name if isinstance(req, int) else req}" + tooltip = f"Beat all of these:\n{indent}- " + reqs = [missions[req].mission_name if isinstance(req, int) else req for req in self.visual_reqs] + tooltip += f"\n{indent}- ".join(req for req in reqs) + return tooltip + + def shows_single_rule(self) -> bool: + return len(self.visual_reqs) == 1 + + def is_accessible( + self, beaten_missions: Set[int], received_items: Dict[int, int] + ) -> bool: + # Beat rules are accessible if all their missions are beaten and accessible + if not beaten_missions.issuperset(self.mission_ids): + return False + return True + + +class CountMissionsEntryRule(EntryRule): + missions_to_count: List[SC2MOGenMission] + target_amount: int + visual_reqs: List[Union[str, SC2MOGenMission]] + + def __init__(self, missions_to_count: List[SC2MOGenMission], target_amount: int, visual_reqs: List[Union[str, SC2MOGenMission]]): + super().__init__() + self.missions_to_count = missions_to_count + if target_amount == -1 or target_amount > len(missions_to_count): + self.target_amount = len(missions_to_count) + else: + self.target_amount = target_amount + self.visual_reqs = visual_reqs + + def _is_fulfilled(self, beaten_missions: Set[SC2MOGenMission], in_region_check: bool) -> bool: + return self.target_amount <= len(beaten_missions.intersection(self.missions_to_count)) + + def _get_depth(self, beaten_missions: Set[SC2MOGenMission]) -> int: + sorted_missions = sorted(beaten_missions.intersection(self.missions_to_count), key = lambda mission: mission.min_depth) + mission_depth = max(mission.min_depth for mission in sorted_missions[:self.target_amount]) + return max(mission_depth, self.target_amount - 1) # -1 because depth is zero-based but amount is one-based + + def to_lambda(self, player: int) -> Callable[[CollectionState], bool]: + return lambda state: self.target_amount <= sum(state.has(mission.beat_item(), player) for mission in self.missions_to_count) + + def to_slot_data(self) -> RuleData: + resolved_reqs: List[Union[str, int]] = [req if isinstance(req, str) else req.mission.id for req in self.visual_reqs] + mission_ids = [mission.mission.id for mission in sorted(self.missions_to_count, key = lambda mission: mission.min_depth)] + return CountMissionsRuleData( + mission_ids, + self.target_amount, + resolved_reqs + ) + + +@dataclass +class CountMissionsRuleData(RuleData): + mission_ids: List[int] + amount: int + visual_reqs: List[Union[str, int]] + + def tooltip(self, indents: int, missions: Dict[int, SC2Mission], done_color: str, not_done_color: str) -> str: + indent = " ".join("" for _ in range(indents)) + if self.amount == len(self.mission_ids): + amount = "all" + else: + amount = str(self.amount) + if len(self.visual_reqs) == 1: + req = self.visual_reqs[0] + req_str = missions[req].mission_name if isinstance(req, int) else req + if self.amount == 1: + if type(req) == int: + return f"Beat {req_str}" + return f"Beat any mission from {req_str}" + return f"Beat {amount} missions from {req_str}" + if self.amount == 1: + tooltip = f"Beat any mission from:\n{indent}- " + else: + tooltip = f"Beat {amount} missions from:\n{indent}- " + reqs = [missions[req].mission_name if isinstance(req, int) else req for req in self.visual_reqs] + tooltip += f"\n{indent}- ".join(req for req in reqs) + return tooltip + + def shows_single_rule(self) -> bool: + return len(self.visual_reqs) == 1 + + def is_accessible( + self, beaten_missions: Set[int], received_items: Dict[int, int] + ) -> bool: + # Count rules are accessible if enough of their missions are beaten and accessible + return len([mission_id for mission_id in self.mission_ids if mission_id in beaten_missions]) >= self.amount + + +class SubRuleEntryRule(EntryRule): + rule_id: int + rules_to_check: List[EntryRule] + target_amount: int + min_depth: int + + def __init__(self, rules_to_check: List[EntryRule], target_amount: int, rule_id: int): + super().__init__() + self.rule_id = rule_id + self.rules_to_check = rules_to_check + self.min_depth = -1 + if target_amount == -1 or target_amount > len(rules_to_check): + self.target_amount = len(rules_to_check) + else: + self.target_amount = target_amount + + def _is_fulfilled(self, beaten_missions: Set[SC2MOGenMission], in_region_check: bool) -> bool: + return self.target_amount <= sum(rule.is_fulfilled(beaten_missions, in_region_check) for rule in self.rules_to_check) + + def _get_depth(self, beaten_missions: Set[SC2MOGenMission]) -> int: + if len(self.rules_to_check) == 0: + return self.min_depth + # It should be guaranteed by is_fulfilled that enough rules have a valid depth because they are fulfilled + filtered_rules = [rule for rule in self.rules_to_check if rule.get_depth(beaten_missions) > -1] + sorted_rules = sorted(filtered_rules, key = lambda rule: rule.get_depth(beaten_missions)) + required_depth = max(rule.get_depth(beaten_missions) for rule in sorted_rules[:self.target_amount]) + return max(required_depth, self.min_depth) + + def to_lambda(self, player: int) -> Callable[[CollectionState], bool]: + sub_lambdas = [rule.to_lambda(player) for rule in self.rules_to_check] + return lambda state, sub_lambdas=sub_lambdas: self.target_amount <= sum(sub_lambda(state) for sub_lambda in sub_lambdas) + + def to_slot_data(self) -> SubRuleRuleData: + sub_rules = [rule.to_slot_data() for rule in self.rules_to_check] + return SubRuleRuleData( + self.rule_id, + sub_rules, + self.target_amount + ) + + +@dataclass +class SubRuleRuleData(RuleData): + rule_id: int + sub_rules: List[RuleData] + amount: int + + @staticmethod + def parse_from_dict(data: Dict[str, Any]) -> SubRuleRuleData: + amount = data["amount"] + rule_id = data["rule_id"] + sub_rules: List[RuleData] = [] + for rule_data in data["sub_rules"]: + if "sub_rules" in rule_data: + rule: RuleData = SubRuleRuleData.parse_from_dict(rule_data) + elif "item_ids" in rule_data: + # Slot data converts Dict[int, int] to Dict[str, int] for some reason + item_ids = {int(item): item_amount for (item, item_amount) in rule_data["item_ids"].items()} + rule = ItemRuleData( + item_ids, + rule_data["visual_reqs"] + ) + elif "amount" in rule_data: + rule = CountMissionsRuleData( + **{field: value for field, value in rule_data.items()} + ) + else: + rule = BeatMissionsRuleData( + **{field: value for field, value in rule_data.items()} + ) + sub_rules.append(rule) + rule = SubRuleRuleData( + rule_id, + sub_rules, + amount + ) + return rule + + @staticmethod + def empty() -> SubRuleRuleData: + return SubRuleRuleData(-1, [], 0) + + def tooltip(self, indents: int, missions: Dict[int, SC2Mission], done_color: str, not_done_color: str) -> str: + indent = " ".join("" for _ in range(indents)) + if self.amount == len(self.sub_rules): + if self.amount == 1: + return self.sub_rules[0].tooltip(indents, missions, done_color, not_done_color) + amount = "all" + elif self.amount == 1: + amount = "any" + else: + amount = str(self.amount) + tooltip = f"Fulfill {amount} of these conditions:\n{indent}- " + subrule_tooltips: List[str] = [] + for rule in self.sub_rules: + sub_tooltip = rule.tooltip(indents + 4, missions, done_color, not_done_color) + if getattr(rule, "was_accessible", False): + subrule_tooltips.append(f"[color={done_color}]{sub_tooltip}[/color]") + else: + subrule_tooltips.append(f"[color={not_done_color}]{sub_tooltip}[/color]") + tooltip += f"\n{indent}- ".join(sub_tooltip for sub_tooltip in subrule_tooltips) + return tooltip + + def shows_single_rule(self) -> bool: + return self.amount == len(self.sub_rules) == 1 and self.sub_rules[0].shows_single_rule() + + def is_accessible( + self, beaten_missions: Set[int], received_items: Dict[int, int] + ) -> bool: + # Sub-rule rules are accessible if enough of their child rules are accessible + accessible_count = 0 + success = accessible_count >= self.amount + if self.amount > 0: + for rule in self.sub_rules: + if rule.is_accessible(beaten_missions, received_items): + rule.was_accessible = True + accessible_count += 1 + if accessible_count >= self.amount: + success = True + break + else: + rule.was_accessible = False + + return success + +class MissionEntryRules(NamedTuple): + mission_rule: SubRuleRuleData + layout_rule: SubRuleRuleData + campaign_rule: SubRuleRuleData + + +class ItemEntryRule(EntryRule): + items_to_check: Dict[str, int] + + def __init__(self, items_to_check: Dict[str, int]) -> None: + super().__init__() + self.items_to_check = items_to_check + + def _is_fulfilled(self, beaten_missions: Set[SC2MOGenMission], in_region_check: bool) -> bool: + # Region creation should assume items can be placed, + # but later uses (eg. starter missions) should respect that this locks a mission + return in_region_check + + def _get_depth(self, beaten_missions: Set[SC2MOGenMission]) -> int: + # Depth 0 means this rule requires 0 prior beaten missions + return 0 + + def to_lambda(self, player: int) -> Callable[[CollectionState], bool]: + return lambda state: state.has_all_counts(self.items_to_check, player) + + def to_slot_data(self) -> RuleData: + item_ids = {item_table[item].code: amount for (item, amount) in self.items_to_check.items()} + visual_reqs = [item if amount == 1 else str(amount) + "x " + item for (item, amount) in self.items_to_check.items()] + return ItemRuleData( + item_ids, + visual_reqs + ) + + +@dataclass +class ItemRuleData(RuleData): + item_ids: Dict[int, int] + visual_reqs: List[str] + + def tooltip(self, indents: int, missions: Dict[int, SC2Mission], done_color: str, not_done_color: str) -> str: + indent = " ".join("" for _ in range(indents)) + if len(self.visual_reqs) == 1: + return f"Find {self.visual_reqs[0]}" + tooltip = f"Find all of these:\n{indent}- " + tooltip += f"\n{indent}- ".join(req for req in self.visual_reqs) + return tooltip + + def shows_single_rule(self) -> bool: + return len(self.visual_reqs) == 1 + + def is_accessible( + self, beaten_missions: Set[int], received_items: Dict[int, int] + ) -> bool: + return all( + item in received_items and received_items[item] >= amount + for (item, amount) in self.item_ids.items() + ) diff --git a/worlds/sc2/mission_order/generation.py b/worlds/sc2/mission_order/generation.py new file mode 100644 index 00000000..5582d7c3 --- /dev/null +++ b/worlds/sc2/mission_order/generation.py @@ -0,0 +1,702 @@ +""" +Contains the complex data manipulation functions for mission order generation and Archipelago region creation. +Incoming data is validated to match specifications in .options.py. +The functions here are called from ..regions.py. +""" + +from typing import Set, Dict, Any, List, Tuple, Union, Optional, Callable, TYPE_CHECKING +import logging + +from BaseClasses import Location, Region, Entrance +from ..mission_tables import SC2Mission, MissionFlag, lookup_name_to_mission, lookup_id_to_mission +from ..item.item_tables import named_layout_key_item_table, named_campaign_key_item_table +from ..item import item_names +from .nodes import MissionOrderNode, SC2MOGenMissionOrder, SC2MOGenCampaign, SC2MOGenLayout, SC2MOGenMission +from .entry_rules import EntryRule, SubRuleEntryRule, ItemEntryRule, CountMissionsEntryRule, BeatMissionsEntryRule +from .mission_pools import ( + SC2MOGenMissionPools, Difficulty, modified_difficulty_thresholds, STANDARD_DIFFICULTY_FILL_ORDER +) +from .options import GENERIC_KEY_NAME, GENERIC_PROGRESSIVE_KEY_NAME + +if TYPE_CHECKING: + from ..locations import LocationData + from .. import SC2World + +def resolve_unlocks(mission_order: SC2MOGenMissionOrder): + """Parses a mission order's entry rule dicts into entry rule objects.""" + rolling_rule_id = 0 + for campaign in mission_order.campaigns: + entry_rule = { + "rules": campaign.option_entry_rules, + "amount": -1 + } + campaign.entry_rule = dict_to_entry_rule(mission_order, entry_rule, campaign, rolling_rule_id) + rolling_rule_id += 1 + for layout in campaign.layouts: + entry_rule = { + "rules": layout.option_entry_rules, + "amount": -1 + } + layout.entry_rule = dict_to_entry_rule(mission_order, entry_rule, layout, rolling_rule_id) + rolling_rule_id += 1 + for mission in layout.missions: + entry_rule = { + "rules": mission.option_entry_rules, + "amount": -1 + } + mission.entry_rule = dict_to_entry_rule(mission_order, entry_rule, mission, rolling_rule_id) + rolling_rule_id += 1 + # Manually make a rule for prev missions + if len(mission.prev) > 0: + mission.entry_rule.target_amount += 1 + mission.entry_rule.rules_to_check.append(CountMissionsEntryRule(mission.prev, 1, mission.prev)) + + +def dict_to_entry_rule(mission_order: SC2MOGenMissionOrder, data: Dict[str, Any], start_node: MissionOrderNode, rule_id: int = -1) -> EntryRule: + """Tries to create an entry rule object from an entry rule dict. The structure of these dicts is validated in .options.py.""" + if "items" in data: + items: Dict[str, int] = data["items"] + has_generic_key = False + for (item, amount) in items.items(): + if item.casefold() == GENERIC_KEY_NAME or item.casefold().startswith(GENERIC_PROGRESSIVE_KEY_NAME): + has_generic_key = True + continue # Don't try to lock the generic key + if item in mission_order.items_to_lock: + # Lock the greatest required amount of each item + mission_order.items_to_lock[item] = max(mission_order.items_to_lock[item], amount) + else: + mission_order.items_to_lock[item] = amount + rule = ItemEntryRule(items) + if has_generic_key: + mission_order.keys_to_resolve.setdefault(start_node, []).append(rule) + return rule + if "rules" in data: + rules = [dict_to_entry_rule(mission_order, subrule, start_node) for subrule in data["rules"]] + return SubRuleEntryRule(rules, data["amount"], rule_id) + if "scope" in data: + objects: List[Tuple[MissionOrderNode, str]] = [] + for address in data["scope"]: + resolved = resolve_address(mission_order, address, start_node) + objects.extend((obj, address) for obj in resolved) + visual_reqs = [obj.get_visual_requirement(start_node) for (obj, _) in objects] + missions: List[SC2MOGenMission] + if "amount" in data: + missions = [mission for (obj, _) in objects for mission in obj.get_missions() if not mission.option_empty] + if len(missions) == 0: + raise ValueError(f"Count rule did not find any missions at scopes: {data['scope']}") + return CountMissionsEntryRule(missions, data["amount"], visual_reqs) + missions = [] + for (obj, address) in objects: + obj.important_beat_event = True + exits = obj.get_exits() + if len(exits) == 0: + raise ValueError( + f"Address \"{address}\" found an unbeatable object. " + "This should mean the address contains \"..\" too often." + ) + missions.extend(exits) + return BeatMissionsEntryRule(missions, visual_reqs) + raise ValueError(f"Invalid data for entry rule: {data}") + + +def resolve_address(mission_order: SC2MOGenMissionOrder, address: str, start_node: MissionOrderNode) -> List[MissionOrderNode]: + """Tries to find a node in the mission order by following the given address.""" + if address.startswith("../") or address == "..": + # Relative address, starts from searching object + cursor = start_node + else: + # Absolute address, starts from the top + cursor = mission_order + address_so_far = "" + for term in address.split("/"): + if len(address_so_far) > 0: + address_so_far += "/" + address_so_far += term + if term == "..": + cursor = cursor.get_parent(address_so_far, address) + else: + result = cursor.search(term) + if result is None: + raise ValueError(f"Address \"{address_so_far}\" (from \"{address}\") tried to find a child for a mission.") + if len(result) == 0: + raise ValueError(f"Address \"{address_so_far}\" (from \"{address}\") could not find a {cursor.child_type_name()}.") + if len(result) > 1: + # Layouts are allowed to end with multiple missions via an index function + if type(result[0]) == SC2MOGenMission and address_so_far == address: + return result + raise ValueError((f"Address \"{address_so_far}\" (from \"{address}\") found more than one {cursor.child_type_name()}.")) + cursor = result[0] + if cursor == start_node: + raise ValueError( + f"Address \"{address_so_far}\" (from \"{address}\") returned to original object. " + "This is not allowed to avoid circular requirements." + ) + return [cursor] + + +######################## + + +def fill_depths(mission_order: SC2MOGenMissionOrder) -> None: + """ + Flood-fills the mission order by following its entry rules to determine the depth of all nodes. + This also ensures theoretical total accessibility of all nodes, but this is allowed to be violated by item placement and the accessibility setting. + """ + accessible_campaigns: Set[SC2MOGenCampaign] = {campaign for campaign in mission_order.campaigns if campaign.is_always_unlocked(in_region_creation=True)} + next_campaigns: Set[SC2MOGenCampaign] = set(mission_order.campaigns).difference(accessible_campaigns) + + accessible_layouts: Set[SC2MOGenLayout] = { + layout + for campaign in accessible_campaigns for layout in campaign.layouts + if layout.is_always_unlocked(in_region_creation=True) + } + next_layouts: Set[SC2MOGenLayout] = {layout for campaign in accessible_campaigns for layout in campaign.layouts}.difference(accessible_layouts) + + next_missions: Set[SC2MOGenMission] = {mission for layout in accessible_layouts for mission in layout.entrances} + beaten_missions: Set[SC2MOGenMission] = set() + + # Sanity check: Can any missions be accessed? + if len(next_missions) == 0: + raise Exception("Mission order has no possibly accessible missions") + + iterations = 0 + while len(next_missions) > 0: + # Check for accessible missions + cur_missions: Set[SC2MOGenMission] = { + mission for mission in next_missions + if mission.is_unlocked(beaten_missions, in_region_creation=True) + } + if len(cur_missions) == 0: + raise Exception(f"Mission order ran out of accessible missions during iteration {iterations}") + next_missions.difference_update(cur_missions) + # Set the depth counters of all currently accessible missions + new_beaten_missions: Set[SC2MOGenMission] = set() + while len(cur_missions) > 0: + mission = cur_missions.pop() + new_beaten_missions.add(mission) + # If the beaten missions at depth X unlock a mission, said mission can be beaten at depth X+1 + mission.min_depth = mission.entry_rule.get_depth(beaten_missions) + 1 + new_next = [ + next_mission for next_mission in mission.next if not ( + next_mission in cur_missions + or next_mission in beaten_missions + or next_mission in new_beaten_missions + ) + ] + next_missions.update(new_next) + + # Any campaigns/layouts/missions added after this point will be seen in the next iteration at the earliest + iterations += 1 + beaten_missions.update(new_beaten_missions) + + # Check for newly accessible campaigns & layouts + new_campaigns: Set[SC2MOGenCampaign] = set() + for campaign in next_campaigns: + if campaign.is_unlocked(beaten_missions, in_region_creation=True): + new_campaigns.add(campaign) + for campaign in new_campaigns: + accessible_campaigns.add(campaign) + next_layouts.update(campaign.layouts) + next_campaigns.remove(campaign) + for layout in campaign.layouts: + layout.entry_rule.min_depth = campaign.entry_rule.get_depth(beaten_missions) + new_layouts: Set[SC2MOGenLayout] = set() + for layout in next_layouts: + if layout.is_unlocked(beaten_missions, in_region_creation=True): + new_layouts.add(layout) + for layout in new_layouts: + accessible_layouts.add(layout) + next_missions.update(layout.entrances) + next_layouts.remove(layout) + for mission in layout.entrances: + mission.entry_rule.min_depth = layout.entry_rule.get_depth(beaten_missions) + + # Make sure we didn't miss anything + assert len(accessible_campaigns) == len(mission_order.campaigns) + assert len(accessible_layouts) == sum(len(campaign.layouts) for campaign in mission_order.campaigns) + total_missions = sum( + len([mission for mission in layout.missions if not mission.option_empty]) + for campaign in mission_order.campaigns for layout in campaign.layouts + ) + assert len(beaten_missions) == total_missions, f'Can only access {len(beaten_missions)} missions out of {total_missions}' + + # Fill campaign/layout depth values as min/max of their children + for campaign in mission_order.campaigns: + for layout in campaign.layouts: + depths = [mission.min_depth for mission in layout.missions if not mission.option_empty] + layout.min_depth = min(depths) + layout.max_depth = max(depths) + campaign.min_depth = min(layout.min_depth for layout in campaign.layouts) + campaign.max_depth = max(layout.max_depth for layout in campaign.layouts) + mission_order.max_depth = max(campaign.max_depth for campaign in mission_order.campaigns) + + +######################## + + +def resolve_difficulties(mission_order: SC2MOGenMissionOrder) -> None: + """Determines the concrete difficulty of all mission slots.""" + for campaign in mission_order.campaigns: + for layout in campaign.layouts: + if layout.option_min_difficulty == Difficulty.RELATIVE: + min_diff = campaign.option_min_difficulty + if min_diff == Difficulty.RELATIVE: + min_depth = 0 + else: + min_depth = campaign.min_depth + else: + min_diff = layout.option_min_difficulty + min_depth = layout.min_depth + + if layout.option_max_difficulty == Difficulty.RELATIVE: + max_diff = campaign.option_max_difficulty + if max_diff == Difficulty.RELATIVE: + max_depth = mission_order.max_depth + else: + max_depth = campaign.max_depth + else: + max_diff = layout.option_max_difficulty + max_depth = layout.max_depth + + depth_range = max_depth - min_depth + if depth_range == 0: + # This can happen if layout size is 1 or layout is all entrances + # Use minimum difficulty in this case + depth_range = 1 + # If min/max aren't relative, assume the limits are meant to show up + layout_thresholds = modified_difficulty_thresholds(min_diff, max_diff) + thresholds = sorted(layout_thresholds.keys()) + + for mission in layout.missions: + if mission.option_empty: + continue + if len(mission.option_mission_pool) == 1: + mission_order.fixed_missions.append(mission) + continue + if mission.option_difficulty == Difficulty.RELATIVE: + mission_thresh = int((mission.min_depth - min_depth) * 100 / depth_range) + for i in range(len(thresholds)): + if thresholds[i] > mission_thresh: + mission.option_difficulty = layout_thresholds[thresholds[i - 1]] + break + mission.option_difficulty = layout_thresholds[thresholds[-1]] + mission_order.sorted_missions[mission.option_difficulty].append(mission) + + +######################## + + +def fill_missions( + mission_order: SC2MOGenMissionOrder, mission_pools: SC2MOGenMissionPools, + world: 'SC2World', locked_missions: List[str], locations: Tuple['LocationData', ...], location_cache: List[Location] +) -> None: + """Places missions in all non-empty mission slots. Also responsible for creating Archipelago regions & locations for placed missions.""" + locations_per_region = get_locations_per_region(locations) + regions: List[Region] = [create_region(world, locations_per_region, location_cache, "Menu")] + locked_ids = [lookup_name_to_mission[mission].id for mission in locked_missions] + prefer_close_difficulty = world.options.difficulty_curve.value == world.options.difficulty_curve.option_standard + + def set_mission_in_slot(slot: SC2MOGenMission, mission: SC2Mission): + slot.mission = mission + slot.region = create_region(world, locations_per_region, location_cache, + mission.mission_name, slot) + + # Resolve slots with set mission names + for mission_slot in mission_order.fixed_missions: + mission_id = mission_slot.option_mission_pool.pop() + # Remove set mission from locked missions + locked_ids = [locked for locked in locked_ids if locked != mission_id] + mission = lookup_id_to_mission[mission_id] + if mission in mission_pools.get_used_missions(): + raise ValueError(f"Mission slot at address \"{mission_slot.get_address_to_node()}\" tried to plando an already plando'd mission.") + mission_pools.pull_specific_mission(mission) + set_mission_in_slot(mission_slot, mission) + regions.append(mission_slot.region) + + # Shuffle & sort all slots to pick from smallest to biggest pool with tie-breaks by difficulty (lowest to highest), then randomly + # Additionally sort goals by difficulty (highest to lowest) with random tie-breaks + sorted_goals: List[SC2MOGenMission] = [] + for difficulty in sorted(mission_order.sorted_missions.keys()): + world.random.shuffle(mission_order.sorted_missions[difficulty]) + sorted_goals.extend(mission for mission in mission_order.sorted_missions[difficulty] if mission in mission_order.goal_missions) + # Sort slots by difficulty, with difficulties sorted by fill order + # standard curve/close difficulty fills difficulties out->in, uneven fills easy->hard + if prefer_close_difficulty: + all_slots = [slot for diff in STANDARD_DIFFICULTY_FILL_ORDER for slot in mission_order.sorted_missions[diff]] + else: + all_slots = [slot for diff in sorted(mission_order.sorted_missions.keys()) for slot in mission_order.sorted_missions[diff]] + # Pick slots with a constrained mission pool first + all_slots.sort(key = lambda slot: len(slot.option_mission_pool.intersection(mission_pools.master_list))) + sorted_goals.reverse() + + # Randomly assign locked missions to appropriate difficulties + slots_for_locked: Dict[int, List[SC2MOGenMission]] = {locked: [] for locked in locked_ids} + for mission_slot in all_slots: + allowed_locked = mission_slot.option_mission_pool.intersection(locked_ids) + for locked in allowed_locked: + slots_for_locked[locked].append(mission_slot) + for (locked, allowed_slots) in slots_for_locked.items(): + locked_mission = lookup_id_to_mission[locked] + allowed_slots = [slot for slot in allowed_slots if slot in all_slots] + if len(allowed_slots) == 0: + logging.warning(f"SC2: Locked mission \"{locked_mission.mission_name}\" is not allowed in any remaining spot and will not be placed.") + continue + # This inherits the earlier sorting, but is now sorted again by relative difficulty + # The result is a sorting in order of nearest difficulty (preferring lower), then by smallest pool, then randomly + allowed_slots.sort(key = lambda slot: abs(slot.option_difficulty - locked_mission.pool + 1)) + # The first slot should be most appropriate + mission_slot = allowed_slots[0] + mission_pools.pull_specific_mission(locked_mission) + set_mission_in_slot(mission_slot, locked_mission) + regions.append(mission_slot.region) + all_slots.remove(mission_slot) + if mission_slot in sorted_goals: + sorted_goals.remove(mission_slot) + + # Pick goal missions first with stricter difficulty matching, and starting with harder goals + for goal_slot in sorted_goals: + try: + mission = mission_pools.pull_random_mission(world, goal_slot, prefer_close_difficulty=True) + set_mission_in_slot(goal_slot, mission) + regions.append(goal_slot.region) + all_slots.remove(goal_slot) + except IndexError: + raise IndexError( + f"Slot at address \"{goal_slot.get_address_to_node()}\" ran out of possible missions to place " + f"with {len(all_slots)} empty slots remaining." + ) + + # Pick random missions + remaining_count = len(all_slots) + for mission_slot in all_slots: + try: + mission = mission_pools.pull_random_mission(world, mission_slot, prefer_close_difficulty=prefer_close_difficulty) + set_mission_in_slot(mission_slot, mission) + regions.append(mission_slot.region) + remaining_count -= 1 + except IndexError: + raise IndexError( + f"Slot at address \"{mission_slot.get_address_to_node()}\" ran out of possible missions to place " + f"with {remaining_count} empty slots remaining." + ) + + world.multiworld.regions += regions + + +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 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_minimal_logic_location( + world: 'SC2World', location_data: 'LocationData', region: Region, location_cache: List[Location], unit_count: int = 0, +) -> Location: + location = Location(world.player, location_data.name, location_data.code, region) + mission = lookup_name_to_mission.get(region.name) + if mission is None: + pass + elif location_data.hard_rule: + assert world.logic + unit_rule = world.logic.has_race_units(unit_count, mission.race) + location.access_rule = lambda state: unit_rule(state) and location_data.hard_rule(state) + else: + assert world.logic + location.access_rule = world.logic.has_race_units(unit_count, mission.race) + location_cache.append(location) + return location + + +def create_region( + world: 'SC2World', + locations_per_region: Dict[str, List['LocationData']], + location_cache: List[Location], + name: str, + slot: Optional[SC2MOGenMission] = None, +) -> Region: + MAX_UNIT_REQUIREMENT = 5 + region = Region(name, world.player, world.multiworld) + + from ..locations import LocationType + if slot is None: + target_victory_cache_locations = 0 + else: + target_victory_cache_locations = slot.option_victory_cache + victory_cache_locations = 0 + + # If the first mission is a build mission, + # require a unit everywhere except one location in the easiest category + mission_needs_unit = False + unit_given = False + easiest_category = LocationType.MASTERY + if slot is not None and slot.min_depth == 0: + mission = lookup_name_to_mission.get(region.name) + if mission is not None and MissionFlag.NoBuild not in mission.flags: + mission_needs_unit = True + for location_data in locations_per_region.get(name, ()): + if location_data.type == LocationType.VICTORY: + pass + elif location_data.type < easiest_category: + easiest_category = location_data.type + if easiest_category >= LocationType.CHALLENGE: + easiest_category = LocationType.VICTORY + + for location_data in locations_per_region.get(name, ()): + assert slot is not None + if location_data.type == LocationType.VICTORY_CACHE: + if victory_cache_locations >= target_victory_cache_locations: + continue + victory_cache_locations += 1 + if world.options.required_tactics.value == world.options.required_tactics.option_any_units: + if mission_needs_unit and not unit_given and location_data.type == easiest_category: + # Ensure there is at least one no-logic location if the first mission is a build mission + location = create_minimal_logic_location(world, location_data, region, location_cache, 0) + unit_given = True + elif location_data.type == LocationType.MASTERY: + # Mastery locations always require max units regardless of position in the ramp + location = create_minimal_logic_location(world, location_data, region, location_cache, MAX_UNIT_REQUIREMENT) + else: + # Required number of units = mission depth; +1 if it's a starting build mission; +1 if it's a challenge location + location = create_minimal_logic_location(world, location_data, region, location_cache, min( + slot.min_depth + mission_needs_unit + (location_data.type == LocationType.CHALLENGE), + MAX_UNIT_REQUIREMENT + )) + else: + location = create_location(world.player, location_data, region, location_cache) + region.locations.append(location) + + return region + + +######################## + + +def make_connections(mission_order: SC2MOGenMissionOrder, world: 'SC2World'): + """Creates Archipelago entrances between missions and creates access rules for the generator from entry rule objects.""" + names: Dict[str, int] = {} + player = world.player + for campaign in mission_order.campaigns: + for layout in campaign.layouts: + for mission in layout.missions: + if not mission.option_empty: + mission_rule = mission.entry_rule.to_lambda(player) + # Only layout entrances need to consider campaign & layout prerequisites + if mission.option_entrance: + campaign_rule = mission.parent().parent().entry_rule.to_lambda(player) + layout_rule = mission.parent().entry_rule.to_lambda(player) + unlock_rule = lambda state, campaign_rule=campaign_rule, layout_rule=layout_rule, mission_rule=mission_rule: \ + campaign_rule(state) and layout_rule(state) and mission_rule(state) + else: + unlock_rule = mission_rule + # Individually connect to previous missions + for prev_mission in mission.prev: + connect(world, names, prev_mission.mission.mission_name, mission.mission.mission_name, + lambda state, unlock_rule=unlock_rule: unlock_rule(state)) + # If there are no previous missions, connect to Menu instead + if len(mission.prev) == 0: + connect(world, names, "Menu", mission.mission.mission_name, + lambda state, unlock_rule=unlock_rule: unlock_rule(state)) + + +def connect(world: 'SC2World', 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 resolve_generic_keys(mission_order: SC2MOGenMissionOrder) -> None: + """ + Replaces placeholder keys in Item entry rules with their concrete counterparts. + Specifically this handles placing named keys into missions and vanilla campaigns/layouts, + and assigning correct progression tracks to progressive keys. + """ + layout_numbered_keys = 1 + campaign_numbered_keys = 1 + progression_tracks: Dict[int, List[Tuple[MissionOrderNode, ItemEntryRule]]] = {} + for (node, item_rules) in mission_order.keys_to_resolve.items(): + key_name = node.get_key_name() + # Generic keys in mission slots should always resolve to an existing key + # Layouts and campaigns may need to be switched for numbered keys + if isinstance(node, SC2MOGenLayout) and key_name not in named_layout_key_item_table: + key_name = item_names._TEMPLATE_NUMBERED_LAYOUT_KEY.format(layout_numbered_keys) + layout_numbered_keys += 1 + elif isinstance(node, SC2MOGenCampaign) and key_name not in named_campaign_key_item_table: + key_name = item_names._TEMPLATE_NUMBERED_CAMPAIGN_KEY.format(campaign_numbered_keys) + campaign_numbered_keys += 1 + + for item_rule in item_rules: + # Swap regular generic key names for the node's proper key name + item_rule.items_to_check = { + key_name if item_name.casefold() == GENERIC_KEY_NAME else item_name: amount + for (item_name, amount) in item_rule.items_to_check.items() + } + # Only lock the key if it was actually placed in this rule + if key_name in item_rule.items_to_check: + mission_order.items_to_lock[key_name] = max(item_rule.items_to_check[key_name], mission_order.items_to_lock.get(key_name, 0)) + + # Sort progressive keys by their given track + for (item_name, amount) in item_rule.items_to_check.items(): + if item_name.casefold() == GENERIC_PROGRESSIVE_KEY_NAME: + progression_tracks.setdefault(amount, []).append((node, item_rule)) + elif item_name.casefold().startswith(GENERIC_PROGRESSIVE_KEY_NAME): + track_string = item_name.split()[-1] + try: + track = int(track_string) + progression_tracks.setdefault(track, []).append((node, item_rule)) + except ValueError: + raise ValueError( + f"Progression track \"{track_string}\" for progressive key \"{item_name}: {amount}\" is not a valid number. " + "Valid formats are:\n" + f"- {GENERIC_PROGRESSIVE_KEY_NAME.title()}: X\n" + f"- {GENERIC_PROGRESSIVE_KEY_NAME.title()} X: 1" + ) + + def find_progressive_keys(item_rule: ItemEntryRule, track_to_find: int) -> List[str]: + return [ + item_name for (item_name, amount) in item_rule.items_to_check.items() + if (item_name.casefold() == GENERIC_PROGRESSIVE_KEY_NAME and amount == track_to_find) or ( + item_name.casefold().startswith(GENERIC_PROGRESSIVE_KEY_NAME) and + item_name.split()[-1] == str(track_to_find) + ) + ] + + def replace_progressive_keys(item_rule: ItemEntryRule, track_to_replace: int, new_key_name: str, new_key_amount: int): + keys_to_replace = find_progressive_keys(item_rule, track_to_replace) + new_items_to_check: Dict[str, int] = {} + for (item_name, amount) in item_rule.items_to_check.items(): + if item_name in keys_to_replace: + new_items_to_check[new_key_name] = new_key_amount + else: + new_items_to_check[item_name] = amount + item_rule.items_to_check = new_items_to_check + + # Change progressive keys to be unique for missions and layouts that request it + want_unique: Dict[MissionOrderNode, List[Tuple[MissionOrderNode, ItemEntryRule]]] = {} + empty_tracks: List[int] = [] + for track in progression_tracks: + # Sort keys to change by layout + new_unique_tracks: Dict[MissionOrderNode, List[Tuple[MissionOrderNode, ItemEntryRule]]] = {} + for (node, item_rule) in progression_tracks[track]: + if isinstance(node, SC2MOGenMission): + # Unique tracks for layouts take priority over campaigns + if node.parent().option_unique_progression_track == track: + new_unique_tracks.setdefault(node.parent(), []).append((node, item_rule)) + elif node.parent().parent().option_unique_progression_track == track: + new_unique_tracks.setdefault(node.parent().parent(), []).append((node, item_rule)) + elif isinstance(node, SC2MOGenLayout) and node.parent().option_unique_progression_track == track: + new_unique_tracks.setdefault(node.parent(), []).append((node, item_rule)) + # Remove found keys from their original progression track + for (container_node, rule_list) in new_unique_tracks.items(): + for node_and_rule in rule_list: + progression_tracks[track].remove(node_and_rule) + want_unique.setdefault(container_node, []).extend(rule_list) + if len(progression_tracks[track]) == 0: + empty_tracks.append(track) + for track in empty_tracks: + progression_tracks.pop(track) + + # Make sure all tracks that can't have keys have been taken care of + invalid_tracks: List[int] = [track for track in progression_tracks if track < 1 or track > len(SC2Mission)] + if len(invalid_tracks) > 0: + affected_key_list: Dict[MissionOrderNode, List[str]] = {} + for track in invalid_tracks: + for (node, item_rule) in progression_tracks[track]: + affected_key_list.setdefault(node, []).extend( + f"{key}: {item_rule.items_to_check[key]}" for key in find_progressive_keys(item_rule, track) + ) + affected_key_list_string = "\n- " + "\n- ".join( + f"{node.get_address_to_node()}: {affected_keys}" + for (node, affected_keys) in affected_key_list.items() + ) + raise ValueError( + "Some item rules contain progressive keys with invalid tracks:" + + affected_key_list_string + + f"\nPossible solutions are changing the tracks of affected keys to be in the range from 1 to {len(SC2Mission)}, " + "or changing the unique_progression_track of containing campaigns/layouts to match the invalid tracks." + ) + + # Assign new free progression tracks to nodes in definition order + next_free = 1 + nodes_to_assign = list(want_unique.keys()) + while len(want_unique) > 0: + while next_free in progression_tracks: + next_free += 1 + container_node = nodes_to_assign.pop(0) + progression_tracks[next_free] = want_unique.pop(container_node) + # Replace the affected keys in nodes with their correct counterparts + key_name = f"{GENERIC_PROGRESSIVE_KEY_NAME} {next_free}" + for (node, item_rule) in progression_tracks[next_free]: + # It's guaranteed by the sorting above that the container is either a layout or a campaign + replace_progressive_keys(item_rule, container_node.option_unique_progression_track, key_name, 1) + + # Give progressive keys a more fitting name if there's only one track and they all apply to the same type of node + progressive_flavor_name: Union[str, None] = None + if len(progression_tracks) == 1: + if all(isinstance(node, SC2MOGenLayout) for rule_list in progression_tracks.values() for (node, _) in rule_list): + progressive_flavor_name = item_names.PROGRESSIVE_QUESTLINE_KEY + elif all(isinstance(node, SC2MOGenMission) for rule_list in progression_tracks.values() for (node, _) in rule_list): + progressive_flavor_name = item_names.PROGRESSIVE_MISSION_KEY + + for (track, rule_list) in progression_tracks.items(): + key_name = item_names._TEMPLATE_PROGRESSIVE_KEY.format(track) if progressive_flavor_name is None else progressive_flavor_name + # Determine order in which the rules should unlock + ordered_item_rules: List[List[ItemEntryRule]] = [] + if not any(isinstance(node, SC2MOGenMission) for (node, _) in rule_list): + # No rule on this track belongs to a mission, so the rules can be kept in definition order + ordered_item_rules = [[item_rule] for (_, item_rule) in rule_list] + else: + # At least one rule belongs to a mission + # Sort rules by the depth of their nodes, ties get the same amount of keys + depth_to_rules: Dict[int, List[ItemEntryRule]] = {} + for (node, item_rule) in rule_list: + depth_to_rules.setdefault(node.get_min_depth(), []).append(item_rule) + ordered_item_rules = [depth_to_rules[depth] for depth in sorted(depth_to_rules.keys())] + + # Assign correct progressive keys to each rule + for (position, item_rules) in enumerate(ordered_item_rules): + for item_rule in item_rules: + keys_to_replace = [ + item_name for (item_name, amount) in item_rule.items_to_check.items() + if (item_name.casefold() == GENERIC_PROGRESSIVE_KEY_NAME and amount == track) or ( + item_name.casefold().startswith(GENERIC_PROGRESSIVE_KEY_NAME) and + item_name.split()[-1] == str(track) + ) + ] + new_items_to_check: Dict[str, int] = {} + for (item_name, amount) in item_rule.items_to_check.items(): + if item_name in keys_to_replace: + new_items_to_check[key_name] = position + 1 + else: + new_items_to_check[item_name] = amount + item_rule.items_to_check = new_items_to_check + mission_order.items_to_lock[key_name] = len(ordered_item_rules) diff --git a/worlds/sc2/mission_order/layout_types.py b/worlds/sc2/mission_order/layout_types.py new file mode 100644 index 00000000..7581ac64 --- /dev/null +++ b/worlds/sc2/mission_order/layout_types.py @@ -0,0 +1,620 @@ +from __future__ import annotations +from typing import List, Callable, Set, Tuple, Union, TYPE_CHECKING, Dict, Any +import math +from abc import ABC, abstractmethod + +if TYPE_CHECKING: + from .nodes import SC2MOGenMission + +class LayoutType(ABC): + size: int + index_functions: List[str] = [] + """Names of available functions for mission indices. For list member `"my_fn"`, function should be called `idx_my_fn`.""" + + def __init__(self, size: int): + self.size = size + + def set_options(self, options: Dict[str, Any]) -> Dict[str, Any]: + """Get type-specific options from the provided dict. Should return unused values.""" + return options + + @abstractmethod + def make_slots(self, mission_factory: Callable[[], SC2MOGenMission]) -> List[SC2MOGenMission]: + """Use the provided `Callable` to create a one-dimensional list of mission slots and set up initial settings and connections. + + This should include at least one entrance and exit.""" + return [] + + def final_setup(self, missions: List[SC2MOGenMission]): + """Called after user changes to the layout are applied to make any final checks and changes. + + Implementers should make changes with caution, since it runs after a user's explicit commands are implemented.""" + return + + def parse_index(self, term: str) -> Union[Set[int], None]: + """From the given term, determine a list of desired target indices. The term is guaranteed to not be "entrances", "exits", or "all". + + If the term cannot be parsed, either raise an exception or return `None`.""" + return self.parse_index_as_function(term) + + def parse_index_as_function(self, term: str) -> Union[Set[int], None]: + """Helper function to interpret the term as a function call on the layout type, if it is declared in `self.index_functions`. + + Returns the function's return value if `term` is a valid function call, `None` otherwise.""" + left = term.find('(') + right = term.find(')') + if left == -1 and right == -1: + # Assume no args are desired + fn_name = term.strip() + fn_args = [] + elif left == -1 or right == -1: + return None + else: + fn_name = term[:left].strip() + fn_args_str = term[left + 1:right] + fn_args = [arg.strip() for arg in fn_args_str.split(',')] + + if fn_name in self.index_functions: + try: + return getattr(self, "idx_" + fn_name)(*fn_args) + except: + return None + else: + return None + + @abstractmethod + def get_visual_layout(self) -> List[List[int]]: + """Organize the mission slots into a list of columns from left to right and top to bottom. + The list should contain indices into the list created by `make_slots`. Intentionally empty spots should contain -1. + + The resulting 2D list should be rectangular.""" + pass + +class Column(LayoutType): + """Linear layout. Default entrance is index 0 at the top, default exit is index `size - 1` at the bottom.""" + + # 0 + # 1 + # 2 + + def make_slots(self, mission_factory: Callable[[], SC2MOGenMission]) -> List[SC2MOGenMission]: + missions = [mission_factory() for _ in range(self.size)] + missions[0].option_entrance = True + missions[-1].option_exit = True + for i in range(self.size - 1): + missions[i].next.append(missions[i + 1]) + return missions + + def get_visual_layout(self) -> List[List[int]]: + return [list(range(self.size))] + +class Grid(LayoutType): + """Rectangular grid. Default entrance is index 0 in the top left, default exit is index `size - 1` in the bottom right.""" + width: int + height: int + num_corners_to_remove: int + two_start_positions: bool + + index_functions = [ + "point", "rect" + ] + + # 0 1 2 + # 3 4 5 + # 6 7 8 + + def set_options(self, options: Dict[str, Any]) -> Dict[str, Any]: + self.two_start_positions = options.pop("two_start_positions", False) and self.size >= 2 + if self.two_start_positions: + self.size += 1 + width: int = options.pop("width", 0) + if width < 1: + self.width, self.height, self.num_corners_to_remove = Grid.get_grid_dimensions(self.size) + else: + self.width = width + self.height = math.ceil(self.size / self.width) + self.num_corners_to_remove = self.height * width - self.size + return options + + @staticmethod + 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 + + @staticmethod + 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]] = [(*Grid.get_factors(size + x), x) for x in (2, 1, 0)] + best_dimension = min(dimension_candidates, key=sum) + return best_dimension + + @staticmethod + def manhattan_distance(point1: Tuple[int, int], point2: Tuple[int, int]) -> int: + return abs(point1[0] - point2[0]) + abs(point1[1] - point2[1]) + + @staticmethod + def euclidean_distance_squared(point1: Tuple[int, int], point2: Tuple[int, int]) -> int: + return (point1[0] - point2[0]) ** 2 + (point1[1] - point2[1]) ** 2 + + @staticmethod + def euclidean_distance(point1: Tuple[int, int], point2: Tuple[int, int]) -> float: + return math.sqrt(Grid.euclidean_distance_squared(point1, point2)) + + def get_grid_coordinates(self, idx: int) -> Tuple[int, int]: + return (idx % self.width), (idx // self.width) + + def get_grid_index(self, x: int, y: int) -> int: + return y * self.width + x + + def is_valid_coordinates(self, x: int, y: int) -> bool: + return ( + 0 <= x < self.width and + 0 <= y < self.height + ) + + def make_slots(self, mission_factory: Callable[[], SC2MOGenMission]) -> List[SC2MOGenMission]: + missions = [mission_factory() for _ in range(self.width * self.height)] + if self.two_start_positions: + missions[0].option_empty = True + missions[1].option_entrance = True + missions[self.get_grid_index(0, 1)].option_entrance = True + else: + missions[0].option_entrance = True + missions[-1].option_exit = True + + for x in range(self.width): + left = x - 1 + right = x + 1 + for y in range(self.height): + up = y - 1 + down = y + 1 + idx = self.get_grid_index(x, y) + neighbours = [ + self.get_grid_index(nb_x, nb_y) + for (nb_x, nb_y) in [(left, y), (right, y), (x, up), (x, down)] + if self.is_valid_coordinates(nb_x, nb_y) + ] + missions[idx].next = [missions[nb] for nb in neighbours] + + # Empty corners + top_corners = math.floor(self.num_corners_to_remove / 2) + bottom_corners = math.ceil(self.num_corners_to_remove / 2) + + # Bottom left corners + y = self.height - 1 + x = 0 + leading_x = 0 + placed = 0 + while placed < bottom_corners: + if x == -1 or y == 0: + leading_x += 1 + x = leading_x + y = self.height - 1 + missions[self.get_grid_index(x, y)].option_empty = True + placed += 1 + x -= 1 + y -= 1 + + # Top right corners + y = 0 + x = self.width - 1 + leading_x = self.width - 1 + placed = 0 + while placed < top_corners: + if x == self.width or y == self.height - 1: + leading_x -= 1 + x = leading_x + y = 0 + missions[self.get_grid_index(x, y)].option_empty = True + placed += 1 + x += 1 + y += 1 + + return missions + + def get_visual_layout(self) -> List[List[int]]: + columns = [ + [self.get_grid_index(x, y) for y in range(self.height)] + for x in range(self.width) + ] + return columns + + def idx_point(self, x: str, y: str) -> Union[Set[int], None]: + try: + x = int(x) + y = int(y) + except: + return None + if self.is_valid_coordinates(x, y): + return {self.get_grid_index(x, y)} + return None + + def idx_rect(self, x: str, y: str, width: str, height: str) -> Union[Set[int], None]: + try: + x = int(x) + y = int(y) + width = int(width) + height = int(height) + except: + return None + indices = { + self.get_grid_index(pt_x, pt_y) + for pt_y in range(y, y + height) + for pt_x in range(x, x + width) + if self.is_valid_coordinates(pt_x, pt_y) + } + return indices + + +class Canvas(Grid): + """Rectangular grid that determines size and filled slots based on special canvas option.""" + canvas: List[str] + groups: Dict[str, List[int]] + jump_distance_orthogonal: int + jump_distance_diagonal: int + + jumps_orthogonal = [(-1, 0), (0, 1), (1, 0), (0, -1)] + jumps_diagonal = [(-1, -1), (-1, 1), (1, 1), (1, -1)] + + index_functions = Grid.index_functions + ["group"] + + def set_options(self, options: Dict[str, Any]) -> Dict[str, Any]: + self.width = options.pop("width") # Should be guaranteed by the option parser + self.height = math.ceil(self.size / self.width) + self.num_corners_to_remove = 0 + self.two_start_positions = False + self.jump_distance_orthogonal = max(options.pop("jump_distance_orthogonal", 1), 1) + self.jump_distance_diagonal = max(options.pop("jump_distance_diagonal", 1), 0) + + if "canvas" not in options: + raise KeyError("Canvas layout is missing required canvas option. Either create it or change type to Grid.") + self.canvas = options.pop("canvas") + # Pad short lines with spaces + longest_line = max(len(line) for line in self.canvas) + for idx in range(len(self.canvas)): + padding = ' ' * (longest_line - len(self.canvas[idx])) + self.canvas[idx] += padding + + self.groups = {} + for (line_idx, line) in enumerate(self.canvas): + for (char_idx, char) in enumerate(line): + self.groups.setdefault(char, []).append(self.get_grid_index(char_idx, line_idx)) + + return options + + def make_slots(self, mission_factory: Callable[[], SC2MOGenMission]) -> List[SC2MOGenMission]: + missions = super().make_slots(mission_factory) + missions[0].option_entrance = False + missions[-1].option_exit = False + + # Canvas spaces become empty slots + for idx in self.groups.get(" ", []): + missions[idx].option_empty = True + + # Raycast into jump directions to find nearest empty space + def jump(point: Tuple[int, int], direction: Tuple[int, int], distance: int) -> Tuple[int, int]: + return ( + point[0] + direction[0] * distance, + point[1] + direction[1] * distance + ) + + def raycast(point: Tuple[int, int], direction: Tuple[int, int], max_distance: int) -> Union[Tuple[int, SC2MOGenMission], None]: + for distance in range(1, max_distance + 1): + target = jump(point, direction, distance) + if self.is_valid_coordinates(*target): + target_mission = missions[self.get_grid_index(*target)] + if not target_mission.option_empty: + return (distance, target_mission) + else: + # Out of bounds + return None + return None + + for (idx, mission) in enumerate(missions): + if mission.option_empty: + continue + point = self.get_grid_coordinates(idx) + if self.jump_distance_orthogonal > 1: + for direction in Canvas.jumps_orthogonal: + target = raycast(point, direction, self.jump_distance_orthogonal) + if target is not None: + (distance, target_mission) = target + if distance > 1: + # Distance 1 orthogonal jumps already come from the base grid + mission.next.append(target[1]) + if self.jump_distance_diagonal > 0: + for direction in Canvas.jumps_diagonal: + target = raycast(point, direction, self.jump_distance_diagonal) + if target is not None: + (distance, target_mission) = target + if distance == 1: + # Keep distance 1 diagonal slots only if the orthogonal neighbours are empty + x_neighbour = jump(point, (direction[0], 0), 1) + y_neighbour = jump(point, (0, direction[1]), 1) + if ( + missions[self.get_grid_index(*x_neighbour)].option_empty and + missions[self.get_grid_index(*y_neighbour)].option_empty + ): + mission.next.append(target_mission) + else: + mission.next.append(target_mission) + + return missions + + def final_setup(self, missions: List[SC2MOGenMission]): + # Pick missions near the original start and end to set as default entrance/exit + # if the user didn't set one themselves + def distance_lambda(point: Tuple[int, int]) -> Callable[[Tuple[int, SC2MOGenMission]], int]: + return lambda idx_mission: Grid.euclidean_distance_squared(self.get_grid_coordinates(idx_mission[0]), point) + + if not any(mission.option_entrance for mission in missions): + top_left = self.get_grid_coordinates(0) + closest_to_top_left = sorted( + ((idx, mission) for (idx, mission) in enumerate(missions) if not mission.option_empty), + key = distance_lambda(top_left) + ) + closest_to_top_left[0][1].option_entrance = True + + if not any(mission.option_exit for mission in missions): + bottom_right = self.get_grid_coordinates(len(missions) - 1) + closest_to_bottom_right = sorted( + ((idx, mission) for (idx, mission) in enumerate(missions) if not mission.option_empty), + key = distance_lambda(bottom_right) + ) + closest_to_bottom_right[0][1].option_exit = True + + def idx_group(self, group: str) -> Union[Set[int], None]: + if group not in self.groups: + return None + return set(self.groups[group]) + + +class Hopscotch(LayoutType): + """Alternating between one and two available missions. + Default entrance is index 0 in the top left, default exit is index `size - 1` in the bottom right.""" + width: int + spacer: int + two_start_positions: bool + + index_functions = [ + "top", "bottom", "middle", "corner" + ] + + # 0 2 + # 1 3 5 + # 4 6 + # 7 + + def set_options(self, options: Dict[str, Any]) -> Dict[str, Any]: + self.two_start_positions = options.pop("two_start_positions", False) and self.size >= 2 + if self.two_start_positions: + self.size += 1 + width: int = options.pop("width", 7) + self.width = max(width, 4) + spacer: int = options.pop("spacer", 2) + self.spacer = max(spacer, 1) + return options + + def make_slots(self, mission_factory: Callable[[], SC2MOGenMission]) -> List[SC2MOGenMission]: + slots = [mission_factory() for _ in range(self.size)] + if self.two_start_positions: + slots[0].option_empty = True + slots[1].option_entrance = True + slots[2].option_entrance = True + else: + slots[0].option_entrance = True + slots[-1].option_exit = True + + cycle = 0 + for idx in range(self.size): + if cycle == 0: + indices = [idx + 1, idx + 2] + cycle = 2 + elif cycle == 1: + indices = [idx + 1] + cycle -= 1 + else: + indices = [idx + 2] + cycle -= 1 + for next_idx in indices: + if next_idx < self.size: + slots[idx].next.append(slots[next_idx]) + + return slots + + @staticmethod + def space_at_column(idx: int) -> List[int]: + # -1 0 1 2 3 4 5 + amount = idx - 1 + if amount > 0: + return [-1 for _ in range(amount)] + else: + return [] + + def get_visual_layout(self) -> List[List[int]]: + # size offset by 1 to account for first column of two slots + cols: List[List[int]] = [] + col: List[int] = [] + col_size = 1 + for idx in range(self.size): + if col_size == 3: + col_size = 1 + cols.append(col) + col = [idx] + else: + col_size += 1 + col.append(idx) + if len(col) > 0: + cols.append(col) + + final_cols: List[List[int]] = [Hopscotch.space_at_column(idx) for idx in range(min(len(cols), self.width))] + for (col_idx, col) in enumerate(cols): + if col_idx >= self.width: + final_cols[col_idx % self.width].extend([-1 for _ in range(self.spacer)]) + final_cols[col_idx % self.width].extend(col) + + fill_to_longest(final_cols) + + return final_cols + + def idx_bottom(self) -> Set[int]: + corners = math.ceil(self.size / 3) + indices = [num * 3 + 1 for num in range(corners)] + return { + idx for idx in indices if idx < self.size + } + + def idx_top(self) -> Set[int]: + corners = math.ceil(self.size / 3) + indices = [num * 3 + 2 for num in range(corners)] + return { + idx for idx in indices if idx < self.size + } + + def idx_middle(self) -> Set[int]: + corners = math.ceil(self.size / 3) + indices = [num * 3 for num in range(corners)] + return { + idx for idx in indices if idx < self.size + } + + def idx_corner(self, number: str) -> Union[Set[int], None]: + try: + number = int(number) + except: + return None + corners = math.ceil(self.size / 3) + if number >= corners: + return None + indices = [number * 3 + n for n in range(3)] + return { + idx for idx in indices if idx < self.size + } + + +class Gauntlet(LayoutType): + """Long, linear layout. Goes horizontally and wraps around. + Default entrance is index 0 in the top left, default exit is index `size - 1` in the bottom right.""" + width: int + + # 0 1 2 3 + # + # 4 5 6 7 + + def set_options(self, options: Dict[str, Any]) -> Dict[str, Any]: + width: int = options.pop("width", 7) + self.width = min(max(width, 4), self.size) + return options + + def make_slots(self, mission_factory: Callable[[], SC2MOGenMission]) -> List[SC2MOGenMission]: + missions = [mission_factory() for _ in range(self.size)] + missions[0].option_entrance = True + missions[-1].option_exit = True + for i in range(self.size - 1): + missions[i].next.append(missions[i + 1]) + return missions + + def get_visual_layout(self) -> List[List[int]]: + columns = [[] for _ in range(self.width)] + for idx in range(self.size): + if idx >= self.width: + columns[idx % self.width].append(-1) + columns[idx % self.width].append(idx) + + fill_to_longest(columns) + + return columns + +class Blitz(LayoutType): + """Rows of missions, one mission per row required. + Default entrances are every mission in the top row, default exit is a central mission in the bottom row.""" + width: int + + index_functions = [ + "row" + ] + + # 0 1 2 3 + # 4 5 6 7 + + def set_options(self, options: Dict[str, Any]) -> Dict[str, Any]: + width = options.pop("width", 0) + if width < 1: + min_width, max_width = 2, 5 + mission_divisor = 5 + self.width = min(max(self.size // mission_divisor, min_width), max_width) + else: + self.width = min(self.size, width) + return options + + def make_slots(self, mission_factory: Callable[[], SC2MOGenMission]) -> List[SC2MOGenMission]: + slots = [mission_factory() for _ in range(self.size)] + for idx in range(self.width): + slots[idx].option_entrance = True + + # TODO: this is copied from the original mission order and works, but I'm not sure on the intent + # middle_column = self.width // 2 + # if self.size % self.width > middle_column: + # final_row = self.width * (self.size // self.width) + # final_mission = final_row + middle_column + # else: + # final_mission = self.size - 1 + # slots[final_mission].option_exit = True + + rows = self.size // self.width + for row in range(rows): + for top in range(self.width): + idx = row * self.width + top + for bot in range(self.width): + other = (row + 1) * self.width + bot + if other < self.size: + slots[idx].next.append(slots[other]) + if row == rows-1: + slots[idx].option_exit = True + + return slots + + def get_visual_layout(self) -> List[List[int]]: + columns = [[] for _ in range(self.width)] + for idx in range(self.size): + columns[idx % self.width].append(idx) + + fill_to_longest(columns) + + return columns + + def idx_row(self, row: str) -> Union[Set[int], None]: + try: + row = int(row) + except: + return None + rows = math.ceil(self.size / self.width) + if row >= rows: + return None + indices = [row * self.width + col for col in range(self.width)] + return { + idx for idx in indices if idx < self.size + } + +def fill_to_longest(columns: List[List[int]]): + longest = max(len(col) for col in columns) + for idx in range(len(columns)): + length = len(columns[idx]) + if length < longest: + columns[idx].extend([-1 for _ in range(longest - length)]) \ No newline at end of file diff --git a/worlds/sc2/mission_order/mission_pools.py b/worlds/sc2/mission_order/mission_pools.py new file mode 100644 index 00000000..a3ab99f4 --- /dev/null +++ b/worlds/sc2/mission_order/mission_pools.py @@ -0,0 +1,251 @@ +from enum import IntEnum +from typing import TYPE_CHECKING, Dict, Set, List, Iterable + +from Options import OptionError +from ..mission_tables import SC2Mission, lookup_id_to_mission, MissionFlag, SC2Campaign +from worlds.AutoWorld import World + +if TYPE_CHECKING: + from .nodes import SC2MOGenMission + +class Difficulty(IntEnum): + RELATIVE = 0 + STARTER = 1 + EASY = 2 + MEDIUM = 3 + HARD = 4 + VERY_HARD = 5 + +# TODO figure out an organic way to get these +DEFAULT_DIFFICULTY_THRESHOLDS = { + Difficulty.STARTER: 0, + Difficulty.EASY: 10, + Difficulty.MEDIUM: 35, + Difficulty.HARD: 65, + Difficulty.VERY_HARD: 90, + Difficulty.VERY_HARD + 1: 100 +} + +STANDARD_DIFFICULTY_FILL_ORDER = ( + Difficulty.VERY_HARD, + Difficulty.STARTER, + Difficulty.HARD, + Difficulty.EASY, + Difficulty.MEDIUM, +) +"""Fill mission slots outer->inner difficulties, +so if multiple pools get exhausted, they will tend to overflow towards the middle.""" + +def modified_difficulty_thresholds(min_difficulty: Difficulty, max_difficulty: Difficulty) -> Dict[int, Difficulty]: + if min_difficulty == Difficulty.RELATIVE: + min_difficulty = Difficulty.STARTER + if max_difficulty == Difficulty.RELATIVE: + max_difficulty = Difficulty.VERY_HARD + thresholds: Dict[int, Difficulty] = {} + min_thresh = DEFAULT_DIFFICULTY_THRESHOLDS[min_difficulty] + total_thresh = DEFAULT_DIFFICULTY_THRESHOLDS[max_difficulty + 1] - min_thresh + for difficulty in range(min_difficulty, max_difficulty + 1): + threshold = DEFAULT_DIFFICULTY_THRESHOLDS[difficulty] - min_thresh + threshold *= 100 // total_thresh + thresholds[threshold] = Difficulty(difficulty) + return thresholds + +class SC2MOGenMissionPools: + """ + Manages available and used missions for a mission order. + """ + master_list: Set[int] + difficulty_pools: Dict[Difficulty, Set[int]] + _used_flags: Dict[MissionFlag, int] + _used_missions: List[SC2Mission] + _updated_difficulties: Dict[int, Difficulty] + _flag_ratios: Dict[MissionFlag, float] + _flag_weights: Dict[MissionFlag, int] + + def __init__(self) -> None: + self.master_list = {mission.id for mission in SC2Mission} + self.difficulty_pools = { + diff: {mission.id for mission in SC2Mission if mission.pool + 1 == diff} + for diff in Difficulty if diff != Difficulty.RELATIVE + } + self._used_flags = {} + self._used_missions = [] + self._updated_difficulties = {} + self._flag_ratios = {} + self._flag_weights = {} + + def set_exclusions(self, excluded: Iterable[SC2Mission], unexcluded: Iterable[SC2Mission]) -> None: + """Prevents all the missions that appear in the `excluded` list, but not in the `unexcluded` list, + from appearing in the mission order.""" + total_exclusions = [mission.id for mission in excluded if mission not in unexcluded] + self.master_list.difference_update(total_exclusions) + + def get_allowed_mission_count(self) -> int: + return len(self.master_list) + + def count_allowed_missions(self, campaign: SC2Campaign) -> int: + allowed_missions = [ + mission_id + for mission_id in self.master_list + if lookup_id_to_mission[mission_id].campaign == campaign + ] + return len(allowed_missions) + + def move_mission(self, mission: SC2Mission, old_diff: Difficulty, new_diff: Difficulty) -> None: + """Changes the difficulty of the given `mission`. Does nothing if the mission is not allowed to appear + or if it isn't set to the `old_diff` difficulty.""" + if mission.id in self.master_list and mission.id in self.difficulty_pools[old_diff]: + self.difficulty_pools[old_diff].remove(mission.id) + self.difficulty_pools[new_diff].add(mission.id) + self._updated_difficulties[mission.id] = new_diff + + def get_modified_mission_difficulty(self, mission: SC2Mission) -> Difficulty: + if mission.id in self._updated_difficulties: + return self._updated_difficulties[mission.id] + return Difficulty(mission.pool + 1) + + def get_pool_size(self, diff: Difficulty) -> int: + """Returns the amount of missions of the given difficulty that are allowed to appear.""" + return len(self.difficulty_pools[diff]) + + def get_used_flags(self) -> Dict[MissionFlag, int]: + """Returns a dictionary of all used flags and their appearance count within the mission order. + Flags that don't appear in the mission order also don't appear in this dictionary.""" + return self._used_flags + + def get_used_missions(self) -> List[SC2Mission]: + """Returns a set of all missions used in the mission order.""" + return self._used_missions + + def set_flag_balances(self, flag_ratios: Dict[MissionFlag, int], flag_weights: Dict[MissionFlag, int]): + # Ensure the ratios are percentages + ratio_sum = sum(ratio for ratio in flag_ratios.values()) + self._flag_ratios = {flag: ratio / ratio_sum for flag, ratio in flag_ratios.items()} + self._flag_weights = flag_weights + + def pick_balanced_mission(self, world: World, pool: List[int]) -> int: + """Applies ratio-based and weight-based balancing to pick a preferred mission from a given mission pool.""" + # Currently only used for race balancing + # Untested for flags that may overlap or not be present at all, but should at least generate + balanced_pool = pool + if len(self._flag_ratios) > 0: + relevant_used_flag_count = max(sum(self._used_flags.get(flag, 0) for flag in self._flag_ratios), 1) + current_ratios = { + flag: self._used_flags.get(flag, 0) / relevant_used_flag_count + for flag in self._flag_ratios + } + # Desirability of missions is the difference between target and current ratios for relevant flags + flag_scores = { + flag: self._flag_ratios[flag] - current_ratios[flag] + for flag in self._flag_ratios + } + mission_scores = [ + sum( + flag_scores[flag] for flag in self._flag_ratios + if flag in lookup_id_to_mission[mission].flags + ) + for mission in balanced_pool + ] + # Only keep the missions that create the best balance + best_score = max(mission_scores) + balanced_pool = [mission for idx, mission in enumerate(balanced_pool) if mission_scores[idx] == best_score] + + balanced_weights = [1.0 for _ in balanced_pool] + if len(self._flag_weights) > 0: + relevant_used_flag_count = max(sum(self._used_flags.get(flag, 0) for flag in self._flag_weights), 1) + # Higher usage rate of relevant flags means lower desirability + flag_scores = { + flag: (relevant_used_flag_count - self._used_flags.get(flag, 0)) * self._flag_weights[flag] + for flag in self._flag_weights + } + # Mission scores are averaged across the mission's flags, + # else flags that aren't always present will inflate weights + mission_scores = [ + sum( + flag_scores[flag] for flag in self._flag_weights + if flag in lookup_id_to_mission[mission].flags + ) / sum(flag in lookup_id_to_mission[mission].flags for flag in self._flag_weights) + for mission in balanced_pool + ] + balanced_weights = mission_scores + + if sum(balanced_weights) == 0.0: + balanced_weights = [1.0 for _ in balanced_weights] + return world.random.choices(balanced_pool, balanced_weights, k=1)[0] + + def pull_specific_mission(self, mission: SC2Mission) -> None: + """Marks the given mission as present in the mission order.""" + # Remove the mission from the master list and whichever difficulty pool it is in + if mission.id in self.master_list: + self.master_list.remove(mission.id) + for diff in self.difficulty_pools: + if mission.id in self.difficulty_pools[diff]: + self.difficulty_pools[diff].remove(mission.id) + break + self._add_mission_stats(mission) + + def _add_mission_stats(self, mission: SC2Mission) -> None: + # Update used flag counts & missions + # Done weirdly for Python <= 3.10 compatibility + flag: MissionFlag + for flag in iter(MissionFlag): # type: ignore + if flag & mission.flags == flag: + self._used_flags.setdefault(flag, 0) + self._used_flags[flag] += 1 + self._used_missions.append(mission) + + def pull_random_mission(self, world: World, slot: 'SC2MOGenMission', *, prefer_close_difficulty: bool = False) -> SC2Mission: + """Picks a random mission from the mission pool of the given slot and marks it as present in the mission order. + + With `prefer_close_difficulty = True` the mission is picked to be as close to the slot's desired difficulty as possible.""" + pool = slot.option_mission_pool.intersection(self.master_list) + + difficulty_pools: Dict[int, List[int]] = { + diff: sorted(pool.intersection(self.difficulty_pools[diff])) + for diff in Difficulty if diff != Difficulty.RELATIVE + } + + if len(pool) == 0: + raise OptionError(f"No available mission to be picked for slot {slot.get_address_to_node()}.") + + desired_difficulty = slot.option_difficulty + if prefer_close_difficulty: + # Iteratively look up and down around the slot's desired difficulty + # Either a difficulty with valid missions is found, or an error is raised + difficulty_offset = 0 + final_pool = difficulty_pools[desired_difficulty] + while len(final_pool) == 0: + higher_diff = min(desired_difficulty + difficulty_offset + 1, Difficulty.VERY_HARD) + final_pool = difficulty_pools[higher_diff] + if len(final_pool) > 0: + break + lower_diff = max(desired_difficulty - difficulty_offset, Difficulty.STARTER) + final_pool = difficulty_pools[lower_diff] + if len(final_pool) > 0: + break + if lower_diff == Difficulty.STARTER and higher_diff == Difficulty.VERY_HARD: + raise IndexError() + difficulty_offset += 1 + + else: + # Consider missions from all lower difficulties as well the desired difficulty + # Only take from higher difficulties if no lower difficulty is possible + final_pool = [ + mission + for difficulty in range(Difficulty.STARTER, desired_difficulty + 1) + for mission in difficulty_pools[difficulty] + ] + difficulty_offset = 1 + while len(final_pool) == 0: + higher_difficulty = desired_difficulty + difficulty_offset + if higher_difficulty > Difficulty.VERY_HARD: + raise IndexError() + final_pool = difficulty_pools[higher_difficulty] + difficulty_offset += 1 + + # Remove the mission from the master list + mission = lookup_id_to_mission[self.pick_balanced_mission(world, final_pool)] + self.master_list.remove(mission.id) + self.difficulty_pools[self.get_modified_mission_difficulty(mission)].remove(mission.id) + self._add_mission_stats(mission) + return mission diff --git a/worlds/sc2/mission_order/nodes.py b/worlds/sc2/mission_order/nodes.py new file mode 100644 index 00000000..a18a433c --- /dev/null +++ b/worlds/sc2/mission_order/nodes.py @@ -0,0 +1,606 @@ +""" +Contains the data structures that make up a mission order. +Data in these structures is validated in .options.py and manipulated by .generation.py. +""" + +from __future__ import annotations +from typing import Dict, Set, Callable, List, Any, Type, Optional, Union, TYPE_CHECKING +from weakref import ref, ReferenceType +from dataclasses import asdict +from abc import ABC, abstractmethod +import logging + +from BaseClasses import Region, CollectionState +from ..mission_tables import SC2Mission +from ..item import item_names +from .layout_types import LayoutType +from .entry_rules import SubRuleEntryRule, ItemEntryRule +from .mission_pools import Difficulty +from .slot_data import CampaignSlotData, LayoutSlotData, MissionSlotData + +if TYPE_CHECKING: + from .. import SC2World + +class MissionOrderNode(ABC): + parent: Optional[ReferenceType[MissionOrderNode]] + important_beat_event: bool + + def get_parent(self, address_so_far: str, full_address: str) -> MissionOrderNode: + if self.parent is None: + raise ValueError( + f"Address \"{address_so_far}\" (from \"{full_address}\") could not find a parent object. " + "This should mean the address contains \"..\" too often." + ) + return self.parent() + + @abstractmethod + def search(self, term: str) -> Union[List[MissionOrderNode], None]: + raise NotImplementedError + + @abstractmethod + def child_type_name(self) -> str: + raise NotImplementedError + + @abstractmethod + def get_missions(self) -> List[SC2MOGenMission]: + raise NotImplementedError + + @abstractmethod + def get_exits(self) -> List[SC2MOGenMission]: + raise NotImplementedError + + @abstractmethod + def get_visual_requirement(self, start_node: MissionOrderNode) -> Union[str, SC2MOGenMission]: + raise NotImplementedError + + @abstractmethod + def get_key_name(self) -> str: + raise NotImplementedError + + @abstractmethod + def get_min_depth(self) -> int: + raise NotImplementedError + + @abstractmethod + def get_address_to_node(self) -> str: + raise NotImplementedError + + +class SC2MOGenMissionOrder(MissionOrderNode): + """ + The top-level data structure for mission orders. + """ + campaigns: List[SC2MOGenCampaign] + sorted_missions: Dict[Difficulty, List[SC2MOGenMission]] + """All mission slots in the mission order sorted by their difficulty, but not their depth.""" + fixed_missions: List[SC2MOGenMission] + """All mission slots that have a plando'd mission.""" + items_to_lock: Dict[str, int] + keys_to_resolve: Dict[MissionOrderNode, List[ItemEntryRule]] + goal_missions: List[SC2MOGenMission] + max_depth: int + + def __init__(self, world: 'SC2World', data: Dict[str, Any]): + self.campaigns = [] + self.sorted_missions = {diff: [] for diff in Difficulty if diff != Difficulty.RELATIVE} + self.fixed_missions = [] + self.items_to_lock = {} + self.keys_to_resolve = {} + self.goal_missions = [] + self.parent = None + + for (campaign_name, campaign_data) in data.items(): + campaign = SC2MOGenCampaign(world, ref(self), campaign_name, campaign_data) + self.campaigns.append(campaign) + + # Check that the mission order actually has a goal + for campaign in self.campaigns: + if campaign.option_goal: + self.goal_missions.extend(mission for mission in campaign.exits) + for layout in campaign.layouts: + if layout.option_goal: + self.goal_missions.extend(layout.exits) + for mission in layout.missions: + if mission.option_goal and not mission.option_empty: + self.goal_missions.append(mission) + # Remove duplicates + for goal in self.goal_missions: + while self.goal_missions.count(goal) > 1: + self.goal_missions.remove(goal) + + # If not, set the last defined campaign as goal + if len(self.goal_missions) == 0: + self.campaigns[-1].option_goal = True + self.goal_missions.extend(mission for mission in self.campaigns[-1].exits) + + # Apply victory cache option wherever the value has not yet been defined; must happen after goal missions are decided + for mission in self.get_missions(): + if mission.option_victory_cache != -1: + # Already set + continue + if mission in self.goal_missions: + mission.option_victory_cache = 0 + else: + mission.option_victory_cache = world.options.victory_cache.value + + # Resolve names + used_names: Set[str] = set() + for campaign in self.campaigns: + names = [campaign.option_name] if len(campaign.option_display_name) == 0 else campaign.option_display_name + if campaign.option_unique_name: + names = [name for name in names if name not in used_names] + campaign.display_name = world.random.choice(names) + used_names.add(campaign.display_name) + for layout in campaign.layouts: + names = [layout.option_name] if len(layout.option_display_name) == 0 else layout.option_display_name + if layout.option_unique_name: + names = [name for name in names if name not in used_names] + layout.display_name = world.random.choice(names) + used_names.add(layout.display_name) + + def get_slot_data(self) -> List[Dict[str, Any]]: + # [(campaign data, [(layout data, [[(mission data)]] )] )] + return [asdict(campaign.get_slot_data()) for campaign in self.campaigns] + + def search(self, term: str) -> Union[List[MissionOrderNode], None]: + return [ + campaign.layouts[0] if campaign.option_single_layout_campaign else campaign + for campaign in self.campaigns + if campaign.option_name.casefold() == term.casefold() + ] + + def child_type_name(self) -> str: + return "Campaign" + + def get_missions(self) -> List[SC2MOGenMission]: + return [mission for campaign in self.campaigns for layout in campaign.layouts for mission in layout.missions] + + def get_exits(self) -> List[SC2MOGenMission]: + return [] + + def get_visual_requirement(self, _start_node: MissionOrderNode) -> Union[str, SC2MOGenMission]: + return "All Missions" + + def get_key_name(self) -> str: + return super().get_key_name() # type: ignore + + def get_min_depth(self) -> int: + return super().get_min_depth() # type: ignore + + def get_address_to_node(self): + return self.campaigns[0].get_address_to_node() + "/.." + + +class SC2MOGenCampaign(MissionOrderNode): + option_name: str # name of this campaign + option_display_name: List[str] + option_unique_name: bool + option_entry_rules: List[Dict[str, Any]] + option_unique_progression_track: int # progressive keys under this campaign and on this track will be changed to a unique track + option_goal: bool # whether this campaign is required to beat the game + # minimum difficulty of this campaign + # 'relative': based on the median distance of the first mission + option_min_difficulty: Difficulty + # maximum difficulty of this campaign + # 'relative': based on the median distance of the last mission + option_max_difficulty: Difficulty + option_single_layout_campaign: bool + + # layouts of this campaign in correct order + layouts: List[SC2MOGenLayout] + exits: List[SC2MOGenMission] # missions required to beat this campaign (missions marked "exit" in layouts marked "exit") + entry_rule: SubRuleEntryRule + display_name: str + + min_depth: int + max_depth: int + + def __init__(self, world: 'SC2World', parent: ReferenceType[SC2MOGenMissionOrder], name: str, data: Dict[str, Any]): + self.parent = parent + self.important_beat_event = False + self.option_name = name + self.option_display_name = data["display_name"] + self.option_unique_name = data["unique_name"] + self.option_goal = data["goal"] + self.option_entry_rules = data["entry_rules"] + self.option_unique_progression_track = data["unique_progression_track"] + self.option_min_difficulty = Difficulty(data["min_difficulty"]) + self.option_max_difficulty = Difficulty(data["max_difficulty"]) + self.option_single_layout_campaign = data["single_layout_campaign"] + self.layouts = [] + self.exits = [] + + for (layout_name, layout_data) in data.items(): + if type(layout_data) == dict: + layout = SC2MOGenLayout(world, ref(self), layout_name, layout_data) + self.layouts.append(layout) + + # Collect required missions (marked layouts' exits) + if layout.option_exit: + self.exits.extend(layout.exits) + + # If no exits are set, use the last defined layout + if len(self.exits) == 0: + self.layouts[-1].option_exit = True + self.exits.extend(self.layouts[-1].exits) + + def is_beaten(self, beaten_missions: Set[SC2MOGenMission]) -> bool: + return beaten_missions.issuperset(self.exits) + + def is_always_unlocked(self, in_region_creation = False) -> bool: + return self.entry_rule.is_always_fulfilled(in_region_creation) + + def is_unlocked(self, beaten_missions: Set[SC2MOGenMission], in_region_creation = False) -> bool: + return self.entry_rule.is_fulfilled(beaten_missions, in_region_creation) + + def search(self, term: str) -> Union[List[MissionOrderNode], None]: + return [ + layout + for layout in self.layouts + if layout.option_name.casefold() == term.casefold() + ] + + def child_type_name(self) -> str: + return "Layout" + + def get_missions(self) -> List[SC2MOGenMission]: + return [mission for layout in self.layouts for mission in layout.missions] + + def get_exits(self) -> List[SC2MOGenMission]: + return self.exits + + def get_visual_requirement(self, start_node: MissionOrderNode) -> Union[str, SC2MOGenMission]: + visual_name = self.get_visual_name() + # Needs special handling for double-parent, which is valid for missions but errors for campaigns + first_parent = start_node.get_parent("", "") + if ( + first_parent is self or ( + first_parent.parent is not None and first_parent.get_parent("", "") is self + ) + ) and visual_name == "": + return "this campaign" + return visual_name + + def get_visual_name(self) -> str: + return self.display_name + + def get_key_name(self) -> str: + return item_names._TEMPLATE_NAMED_CAMPAIGN_KEY.format(self.get_visual_name()) + + def get_min_depth(self) -> int: + return self.min_depth + + def get_address_to_node(self) -> str: + return f"{self.option_name}" + + def get_slot_data(self) -> CampaignSlotData: + if self.important_beat_event: + exits = [slot.mission.id for slot in self.exits] + else: + exits = [] + + return CampaignSlotData( + self.get_visual_name(), + asdict(self.entry_rule.to_slot_data()), + exits, + [asdict(layout.get_slot_data()) for layout in self.layouts] + ) + + +class SC2MOGenLayout(MissionOrderNode): + option_name: str # name of this layout + option_display_name: List[str] # visual name of this layout + option_unique_name: bool + option_type: Type[LayoutType] # type of this layout + option_size: int # amount of missions in this layout + option_goal: bool # whether this layout is required to beat the game + option_exit: bool # whether this layout is required to beat its parent campaign + option_mission_pool: List[int] # IDs of valid missions for this layout + option_missions: List[Dict[str, Any]] + + option_entry_rules: List[Dict[str, Any]] + option_unique_progression_track: int # progressive keys under this layout and on this track will be changed to a unique track + + # minimum difficulty of this layout + # 'relative': based on the median distance of the first mission + option_min_difficulty: Difficulty + # maximum difficulty of this layout + # 'relative': based on the median distance of the last mission + option_max_difficulty: Difficulty + + missions: List[SC2MOGenMission] + layout_type: LayoutType + entrances: List[SC2MOGenMission] + exits: List[SC2MOGenMission] + entry_rule: SubRuleEntryRule + display_name: str + + min_depth: int + max_depth: int + + def __init__(self, world: 'SC2World', parent: ReferenceType[SC2MOGenCampaign], name: str, data: Dict): + self.parent: ReferenceType[SC2MOGenCampaign] = parent + self.important_beat_event = False + self.option_name = name + self.option_display_name = data.pop("display_name") + self.option_unique_name = data.pop("unique_name") + self.option_type = data.pop("type") + self.option_size = data.pop("size") + self.option_goal = data.pop("goal") + self.option_exit = data.pop("exit") + self.option_mission_pool = data.pop("mission_pool") + self.option_missions = data.pop("missions") + self.option_entry_rules = data.pop("entry_rules") + self.option_unique_progression_track = data.pop("unique_progression_track") + self.option_min_difficulty = Difficulty(data.pop("min_difficulty")) + self.option_max_difficulty = Difficulty(data.pop("max_difficulty")) + self.missions = [] + self.entrances = [] + self.exits = [] + + # Check for positive size now instead of during YAML validation to actively error with default size + if self.option_size == 0: + raise ValueError(f"Layout \"{self.option_name}\" has a size of 0.") + + # Build base layout + from . import layout_types + self.layout_type: LayoutType = getattr(layout_types, self.option_type)(self.option_size) + unused = self.layout_type.set_options(data) + if len(unused) > 0: + logging.warning(f"SC2 ({world.player_name}): Layout \"{self.option_name}\" has unknown options: {list(unused.keys())}") + mission_factory = lambda: SC2MOGenMission(ref(self), set(self.option_mission_pool)) + self.missions = self.layout_type.make_slots(mission_factory) + + # Update missions with user data + for mission_data in self.option_missions: + indices: Set[int] = set() + index_terms: List[Union[int, str]] = mission_data["index"] + for term in index_terms: + result = self.resolve_index_term(term) + indices.update(result) + for idx in indices: + self.missions[idx].update_with_data(mission_data) + + # Let layout respond to user changes + self.layout_type.final_setup(self.missions) + + for mission in self.missions: + if mission.option_entrance: + self.entrances.append(mission) + if mission.option_exit: + self.exits.append(mission) + if mission.option_next is not None: + mission.next = [self.missions[idx] for term in mission.option_next for idx in sorted(self.resolve_index_term(term))] + + # Set up missions' prev data + for mission in self.missions: + for next_mission in mission.next: + next_mission.prev.append(mission) + + # Remove empty missions from access data + for mission in self.missions: + if mission.option_empty: + for next_mission in mission.next: + next_mission.prev.remove(mission) + mission.next.clear() + for prev_mission in mission.prev: + prev_mission.next.remove(mission) + mission.prev.clear() + + # Clean up data and options + all_empty = True + for mission in self.missions: + if mission.option_empty: + # Empty missions cannot be entrances, exits, or required + # This is done now instead of earlier to make "set all default entrances to empty" not fail + if mission in self.entrances: + self.entrances.remove(mission) + mission.option_entrance = False + if mission in self.exits: + self.exits.remove(mission) + mission.option_exit = False + mission.option_goal = False + # Empty missions are also not allowed to cause secondary effects via entry rules (eg. create key items) + mission.option_entry_rules = [] + else: + all_empty = False + # Establish the following invariant: + # A non-empty mission has no prev missions <=> A non-empty mission is an entrance + # This is mandatory to guarantee the entire layout is accessible via consecutive .nexts + # Note that the opposite is not enforced for exits to allow fully optional layouts + if len(mission.prev) == 0: + mission.option_entrance = True + self.entrances.append(mission) + elif mission.option_entrance: + for prev_mission in mission.prev: + prev_mission.next.remove(mission) + mission.prev.clear() + if all_empty: + raise Exception(f"Layout \"{self.option_name}\" only contains empty mission slots.") + + def is_beaten(self, beaten_missions: Set[SC2MOGenMission]) -> bool: + return beaten_missions.issuperset(self.exits) + + def is_always_unlocked(self, in_region_creation = False) -> bool: + return self.entry_rule.is_always_fulfilled(in_region_creation) + + def is_unlocked(self, beaten_missions: Set[SC2MOGenMission], in_region_creation = False) -> bool: + return self.entry_rule.is_fulfilled(beaten_missions, in_region_creation) + + def resolve_index_term(self, term: Union[str, int], *, ignore_out_of_bounds: bool = True, reject_none: bool = True) -> Union[Set[int], None]: + try: + result = {int(term)} + except ValueError: + if term == "entrances": + result = {idx for idx in range(len(self.missions)) if self.missions[idx].option_entrance} + elif term == "exits": + result = {idx for idx in range(len(self.missions)) if self.missions[idx].option_exit} + elif term == "all": + result = {idx for idx in range(len(self.missions))} + else: + result = self.layout_type.parse_index(term) + if result is None and reject_none: + raise ValueError(f"Layout \"{self.option_name}\" could not resolve mission index term \"{term}\".") + if ignore_out_of_bounds: + result = [index for index in result if index >= 0 and index < len(self.missions)] + return result + + def get_parent(self, _address_so_far: str, _full_address: str) -> MissionOrderNode: + if self.parent().option_single_layout_campaign: + parent = self.parent().parent + else: + parent = self.parent + return parent() + + def search(self, term: str) -> Union[List[MissionOrderNode], None]: + indices = self.resolve_index_term(term, reject_none=False) + if indices is None: + # Let the address parser handle the fail case + return [] + missions = [self.missions[index] for index in sorted(indices)] + return missions + + def child_type_name(self) -> str: + return "Mission" + + def get_missions(self) -> List[SC2MOGenMission]: + return [mission for mission in self.missions] + + def get_exits(self) -> List[SC2MOGenMission]: + return self.exits + + def get_visual_requirement(self, start_node: MissionOrderNode) -> Union[str, SC2MOGenMission]: + visual_name = self.get_visual_name() + if start_node.get_parent("", "") is self and visual_name == "": + return "this questline" + return visual_name + + def get_visual_name(self) -> str: + return self.display_name + + def get_key_name(self) -> str: + return item_names._TEMPLATE_NAMED_LAYOUT_KEY.format(self.get_visual_name(), self.parent().get_visual_name()) + + def get_min_depth(self) -> int: + return self.min_depth + + def get_address_to_node(self) -> str: + campaign = self.parent() + if campaign.option_single_layout_campaign: + return f"{self.option_name}" + return self.parent().get_address_to_node() + f"/{self.option_name}" + + def get_slot_data(self) -> LayoutSlotData: + mission_slots = [ + [ + asdict(self.missions[idx].get_slot_data() if (idx >= 0 and not self.missions[idx].option_empty) else MissionSlotData.empty()) + for idx in column + ] + for column in self.layout_type.get_visual_layout() + ] + if self.important_beat_event: + exits = [slot.mission.id for slot in self.exits] + else: + exits = [] + + return LayoutSlotData( + self.get_visual_name(), + asdict(self.entry_rule.to_slot_data()), + exits, + mission_slots + ) + + +class SC2MOGenMission(MissionOrderNode): + option_goal: bool # whether this mission is required to beat the game + option_entrance: bool # whether this mission is unlocked when the layout is unlocked + option_exit: bool # whether this mission is required to beat its parent layout + option_empty: bool # whether this slot contains a mission at all + option_next: Union[None, List[Union[int, str]]] # indices of internally connected missions + option_entry_rules: List[Dict[str, Any]] + option_difficulty: Difficulty # difficulty pool this mission pulls from + option_mission_pool: Set[int] # Allowed mission IDs for this slot + option_victory_cache: int # Number of victory cache locations tied to the mission name + + entry_rule: SubRuleEntryRule + min_depth: int # Smallest amount of missions to beat before this slot is accessible + + mission: SC2Mission + region: Region + + next: List[SC2MOGenMission] + prev: List[SC2MOGenMission] + + def __init__(self, parent: ReferenceType[SC2MOGenLayout], parent_mission_pool: Set[int]): + self.parent: ReferenceType[SC2MOGenLayout] = parent + self.important_beat_event = False + self.option_mission_pool = parent_mission_pool + self.option_goal = False + self.option_entrance = False + self.option_exit = False + self.option_empty = False + self.option_next = None + self.option_entry_rules = [] + self.option_difficulty = Difficulty.RELATIVE + self.next = [] + self.prev = [] + self.min_depth = -1 + self.option_victory_cache = -1 + + def update_with_data(self, data: Dict): + self.option_goal = data.get("goal", self.option_goal) + self.option_entrance = data.get("entrance", self.option_entrance) + self.option_exit = data.get("exit", self.option_exit) + self.option_empty = data.get("empty", self.option_empty) + self.option_next = data.get("next", self.option_next) + self.option_entry_rules = data.get("entry_rules", self.option_entry_rules) + self.option_difficulty = data.get("difficulty", self.option_difficulty) + self.option_mission_pool = data.get("mission_pool", self.option_mission_pool) + self.option_victory_cache = data.get("victory_cache", -1) + + def is_always_unlocked(self, in_region_creation = False) -> bool: + return self.entry_rule.is_always_fulfilled(in_region_creation) + + def is_unlocked(self, beaten_missions: Set[SC2MOGenMission], in_region_creation = False) -> bool: + return self.entry_rule.is_fulfilled(beaten_missions, in_region_creation) + + def beat_item(self) -> str: + return f"Beat {self.mission.mission_name}" + + def beat_rule(self, player) -> Callable[[CollectionState], bool]: + return lambda state: state.has(self.beat_item(), player) + + def search(self, term: str) -> Union[List[MissionOrderNode], None]: + return None + + def child_type_name(self) -> str: + return "" + + def get_missions(self) -> List[SC2MOGenMission]: + return [self] + + def get_exits(self) -> List[SC2MOGenMission]: + return [self] + + def get_visual_requirement(self, _start_node: MissionOrderNode) -> Union[str, SC2MOGenMission]: + return self + + def get_key_name(self) -> str: + return item_names._TEMPLATE_MISSION_KEY.format(self.mission.mission_name) + + def get_min_depth(self) -> int: + return self.min_depth + + def get_address_to_node(self) -> str: + layout = self.parent() + assert layout is not None + index = layout.missions.index(self) + return layout.get_address_to_node() + f"/{index}" + + def get_slot_data(self) -> MissionSlotData: + return MissionSlotData( + self.mission.id, + [mission.mission.id for mission in self.prev], + self.entry_rule.to_slot_data(), + self.option_victory_cache, + ) diff --git a/worlds/sc2/mission_order/options.py b/worlds/sc2/mission_order/options.py new file mode 100644 index 00000000..84630ba1 --- /dev/null +++ b/worlds/sc2/mission_order/options.py @@ -0,0 +1,472 @@ +""" +Contains the Custom Mission Order option. Also validates the option value, so generation can assume the data matches the specification. +""" + +from __future__ import annotations +import random + +from Options import OptionDict, Visibility +from schema import Schema, Optional, And, Or +import typing +from typing import Any, Union, Dict, Set, List +import copy + +from ..mission_tables import lookup_name_to_mission +from ..mission_groups import mission_groups +from ..item.item_tables import item_table +from ..item.item_groups import item_name_groups +from . import layout_types +from .layout_types import LayoutType, Column, Grid, Hopscotch, Gauntlet, Blitz, Canvas +from .mission_pools import Difficulty +from .presets_static import ( + static_preset, preset_mini_wol_with_prophecy, preset_mini_wol, preset_mini_hots, preset_mini_prophecy, + preset_mini_lotv_prologue, preset_mini_lotv, preset_mini_lotv_epilogue, preset_mini_nco, + preset_wol_with_prophecy, preset_wol, preset_prophecy, preset_hots, preset_lotv_prologue, + preset_lotv_epilogue, preset_lotv, preset_nco +) +from .presets_scripted import make_golden_path + +GENERIC_KEY_NAME = "Key".casefold() +GENERIC_PROGRESSIVE_KEY_NAME = "Progressive Key".casefold() + +STR_OPTION_VALUES: Dict[str, Dict[str, Any]] = { + "type": { + "column": Column.__name__, "grid": Grid.__name__, "hopscotch": Hopscotch.__name__, "gauntlet": Gauntlet.__name__, "blitz": Blitz.__name__, + "canvas": Canvas.__name__, + }, + "difficulty": { + "relative": Difficulty.RELATIVE.value, "starter": Difficulty.STARTER.value, "easy": Difficulty.EASY.value, + "medium": Difficulty.MEDIUM.value, "hard": Difficulty.HARD.value, "very hard": Difficulty.VERY_HARD.value + }, + "preset": { + "none": lambda _: {}, + "wol + prophecy": static_preset(preset_wol_with_prophecy), + "wol": static_preset(preset_wol), + "prophecy": static_preset(preset_prophecy), + "hots": static_preset(preset_hots), + "prologue": static_preset(preset_lotv_prologue), + "lotv prologue": static_preset(preset_lotv_prologue), + "lotv": static_preset(preset_lotv), + "epilogue": static_preset(preset_lotv_epilogue), + "lotv epilogue": static_preset(preset_lotv_epilogue), + "nco": static_preset(preset_nco), + "mini wol + prophecy": static_preset(preset_mini_wol_with_prophecy), + "mini wol": static_preset(preset_mini_wol), + "mini prophecy": static_preset(preset_mini_prophecy), + "mini hots": static_preset(preset_mini_hots), + "mini prologue": static_preset(preset_mini_lotv_prologue), + "mini lotv prologue": static_preset(preset_mini_lotv_prologue), + "mini lotv": static_preset(preset_mini_lotv), + "mini epilogue": static_preset(preset_mini_lotv_epilogue), + "mini lotv epilogue": static_preset(preset_mini_lotv_epilogue), + "mini nco": static_preset(preset_mini_nco), + "golden path": make_golden_path + }, +} +STR_OPTION_VALUES["min_difficulty"] = STR_OPTION_VALUES["difficulty"] +STR_OPTION_VALUES["max_difficulty"] = STR_OPTION_VALUES["difficulty"] +GLOBAL_ENTRY = "global" + +StrOption = lambda cat: And(str, lambda val: val.lower() in STR_OPTION_VALUES[cat]) +IntNegOne = And(int, lambda val: val >= -1) +IntZero = And(int, lambda val: val >= 0) +IntOne = And(int, lambda val: val >= 1) +IntPercent = And(int, lambda val: 0 <= val <= 100) +IntZeroToTen = And(int, lambda val: 0 <= val <= 10) + +SubRuleEntryRule = { + "rules": [{str: object}], # recursive schema checking is too hard + "amount": IntNegOne, +} +MissionCountEntryRule = { + "scope": [str], + "amount": IntNegOne, +} +BeatMissionsEntryRule = { + "scope": [str], +} +ItemEntryRule = { + "items": {str: int} +} +EntryRule = Or(SubRuleEntryRule, MissionCountEntryRule, BeatMissionsEntryRule, ItemEntryRule) +SchemaDifficulty = Or(*[value.value for value in Difficulty]) + +class CustomMissionOrder(OptionDict): + """ + Used to generate a custom mission order. Please see documentation to understand usage. + Will do nothing unless `mission_order` is set to `custom`. + """ + display_name = "Custom Mission Order" + visibility = Visibility.template + value: Dict[str, Dict[str, Any]] + default = { + "Default Campaign": { + "display_name": "null", + "unique_name": False, + "entry_rules": [], + "unique_progression_track": 0, + "goal": True, + "min_difficulty": "relative", + "max_difficulty": "relative", + GLOBAL_ENTRY: { + "display_name": "null", + "unique_name": False, + "entry_rules": [], + "unique_progression_track": 0, + "goal": False, + "exit": False, + "mission_pool": ["all missions"], + "min_difficulty": "relative", + "max_difficulty": "relative", + "missions": [], + }, + "Default Layout": { + "type": "grid", + "size": 9, + }, + }, + } + schema = Schema({ + # Campaigns + str: { + "display_name": [str], + "unique_name": bool, + "entry_rules": [EntryRule], + "unique_progression_track": int, + "goal": bool, + "min_difficulty": SchemaDifficulty, + "max_difficulty": SchemaDifficulty, + "single_layout_campaign": bool, + # Layouts + str: { + "display_name": [str], + "unique_name": bool, + # Type options + "type": lambda val: issubclass(getattr(layout_types, val), LayoutType), + "size": IntOne, + # Link options + "exit": bool, + "goal": bool, + "entry_rules": [EntryRule], + "unique_progression_track": int, + # Mission pool options + "mission_pool": {int}, + "min_difficulty": SchemaDifficulty, + "max_difficulty": SchemaDifficulty, + # Allow arbitrary options for layout types + Optional(str): Or(int, str, bool, [Or(int, str, bool)]), + # Mission slots + "missions": [{ + "index": [Or(int, str)], + Optional("entrance"): bool, + Optional("exit"): bool, + Optional("goal"): bool, + Optional("empty"): bool, + Optional("next"): [Or(int, str)], + Optional("entry_rules"): [EntryRule], + Optional("mission_pool"): {int}, + Optional("difficulty"): SchemaDifficulty, + Optional("victory_cache"): IntZeroToTen, + }], + }, + } + }) + + def __init__(self, yaml_value: Dict[str, Dict[str, Any]]) -> None: + # This function constructs self.value by parts, + # so the parent constructor isn't called + self.value: Dict[str, Dict[str, Any]] = {} + if yaml_value == self.default: # If this option is default, it shouldn't mess with its own values + yaml_value = copy.deepcopy(self.default) + + for campaign in yaml_value: + self.value[campaign] = {} + + # Check if this campaign has a layout type, making it a campaign-level layout + single_layout_campaign = "type" in yaml_value[campaign] + if single_layout_campaign: + # Single-layout campaigns are not allowed to declare more layouts + single_layout = {key: val for (key, val) in yaml_value[campaign].items() if type(val) != dict} + yaml_value[campaign] = {campaign: single_layout} + # Campaign should inherit certain values from the layout + if "goal" not in single_layout or not single_layout["goal"]: + yaml_value[campaign]["goal"] = False + if "unique_progression_track" in single_layout: + yaml_value[campaign]["unique_progression_track"] = single_layout["unique_progression_track"] + # Hide campaign name for single-layout campaigns + yaml_value[campaign]["display_name"] = "" + yaml_value[campaign]["single_layout_campaign"] = single_layout_campaign + + # Check if this campaign has a global layout + global_dict = {} + for name in yaml_value[campaign]: + if name.lower() == GLOBAL_ENTRY: + global_dict = yaml_value[campaign].pop(name) + break + + # Strip layouts and unknown options from the campaign + # The latter are assumed to be preset options + preset_key: str = yaml_value[campaign].pop("preset", "none") + layout_keys = [key for (key, val) in yaml_value[campaign].items() if type(val) == dict] + layouts = {key: yaml_value[campaign].pop(key) for key in layout_keys} + preset_option_keys = [key for key in yaml_value[campaign] if key not in self.default["Default Campaign"]] + preset_option_keys.remove("single_layout_campaign") + preset_options = {key: yaml_value[campaign].pop(key) for key in preset_option_keys} + + # Resolve preset + preset: Dict[str, Any] = _resolve_string_option_single("preset", preset_key)(preset_options) + # Preset global is resolved internally to avoid conflict with user global + preset_global_dict = {} + for name in preset: + if name.lower() == GLOBAL_ENTRY: + preset_global_dict = preset.pop(name) + break + preset_layout_keys = [key for (key, val) in preset.items() if type(val) == dict] + preset_layouts = {key: preset.pop(key) for key in preset_layout_keys} + ordered_layouts = {key: copy.deepcopy(preset_global_dict) for key in preset_layout_keys} + for key in preset_layout_keys: + ordered_layouts[key].update(preset_layouts[key]) + # Final layouts are preset layouts (updated by same-name user layouts) followed by custom user layouts + for key in layouts: + if key in ordered_layouts: + # Mission slots for presets should go before user mission slots + if "missions" in layouts[key] and "missions" in ordered_layouts[key]: + layouts[key]["missions"] = ordered_layouts[key]["missions"] + layouts[key]["missions"] + ordered_layouts[key].update(layouts[key]) + else: + ordered_layouts[key] = layouts[key] + + # Campaign values = default options (except for default layouts) + preset options (except for layouts) + campaign options + self.value[campaign] = {key: value for (key, value) in self.default["Default Campaign"].items() if type(value) != dict} + self.value[campaign].update(preset) + self.value[campaign].update(yaml_value[campaign]) + _resolve_special_options(self.value[campaign]) + + for layout in ordered_layouts: + # Layout values = default options + campaign's global options + layout options + self.value[campaign][layout] = copy.deepcopy(self.default["Default Campaign"][GLOBAL_ENTRY]) + self.value[campaign][layout].update(global_dict) + self.value[campaign][layout].update(ordered_layouts[layout]) + _resolve_special_options(self.value[campaign][layout]) + + for mission_slot_index in range(len(self.value[campaign][layout]["missions"])): + # Defaults for mission slots are handled by the mission slot struct + _resolve_special_options(self.value[campaign][layout]["missions"][mission_slot_index]) + + # Overloaded to remove pre-init schema validation + # Schema is still validated after __init__ + @classmethod + def from_any(cls, data: Dict[str, Any]) -> CustomMissionOrder: + if type(data) == dict: + return cls(data) + else: + raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}") + + +def _resolve_special_options(data: Dict[str, Any]): + # Handle range values & string-to-value conversions + for option in data: + option_value = data[option] + new_value = _resolve_special_option(option, option_value) + data[option] = new_value + + # Special case for canvas layouts determining their own size + if "type" in data and data["type"] == Canvas.__name__: + canvas: List[str] = data["canvas"] + longest_line = max(len(line) for line in canvas) + data["size"] = len(canvas) * longest_line + data["width"] = longest_line + + +def _resolve_special_option(option: str, option_value: Any) -> Any: + # Option values can be string representations of values + if option in STR_OPTION_VALUES: + return _resolve_string_option(option, option_value) + + if option == "mission_pool": + return _resolve_mission_pool(option_value) + + if option == "entry_rules": + rules = [_resolve_entry_rule(subrule) for subrule in option_value] + return rules + + if option == "display_name": + # Make sure all the values are strings + if type(option_value) == list: + names = [str(value) for value in option_value] + return names + elif option_value == "null": + # "null" means no custom display name + return [] + else: + return [str(option_value)] + + if option in ["index", "next"]: + # All index values could be ranges + if type(option_value) == list: + # Flatten any nested lists + indices = [idx for val in [idx if type(idx) == list else [idx] for idx in option_value] for idx in val] + indices = [_resolve_potential_range(index) for index in indices] + indices = [idx if type(idx) == int else str(idx) for idx in indices] + return indices + else: + idx = _resolve_potential_range(option_value) + return [idx if type(idx) == int else str(idx)] + + # Option values can be ranges + return _resolve_potential_range(option_value) + + +def _resolve_string_option_single(option: str, option_value: str) -> Any: + formatted_value = option_value.lower().replace("_", " ") + if formatted_value not in STR_OPTION_VALUES[option]: + raise ValueError( + f"Option \"{option}\" received unknown value \"{option_value}\".\n" + f"Allowed values are: {list(STR_OPTION_VALUES[option].keys())}" + ) + return STR_OPTION_VALUES[option][formatted_value] + + +def _resolve_string_option(option: str, option_value: Union[List[str], str]) -> Any: + if type(option_value) == list: + return [_resolve_string_option_single(option, val) for val in option_value] + else: + return _resolve_string_option_single(option, option_value) + + +def _resolve_entry_rule(option_value: Dict[str, Any]) -> Dict[str, Any]: + resolved: Dict[str, Any] = {} + mutually_exclusive: List[str] = [] + if "amount" in option_value: + resolved["amount"] = _resolve_potential_range(option_value["amount"]) + if "scope" in option_value: + mutually_exclusive.append("scope") + # A scope may be a list or a single address + if type(option_value["scope"]) == list: + resolved["scope"] = [str(subscope) for subscope in option_value["scope"]] + else: + resolved["scope"] = [str(option_value["scope"])] + if "rules" in option_value: + mutually_exclusive.append("rules") + resolved["rules"] = [_resolve_entry_rule(subrule) for subrule in option_value["rules"]] + # Make sure sub-rule rules have a specified amount + if "amount" not in option_value: + resolved["amount"] = -1 + if "items" in option_value: + mutually_exclusive.append("items") + option_items: Dict[str, Any] = option_value["items"] + resolved_items = {item: int(_resolve_potential_range(str(amount))) for (item, amount) in option_items.items()} + resolved_items = _resolve_item_names(resolved_items) + resolved["items"] = {} + for item in resolved_items: + if item not in item_table: + if item.casefold() == GENERIC_KEY_NAME or item.casefold().startswith(GENERIC_PROGRESSIVE_KEY_NAME): + resolved["items"][item] = max(0, resolved_items[item]) + continue + raise ValueError(f"Item rule contains \"{item}\", which is not a valid item name.") + amount = max(0, resolved_items[item]) + quantity = item_table[item].quantity + if amount == 0: + final_amount = quantity + elif quantity == 0: + final_amount = amount + else: + final_amount = amount + resolved["items"][item] = final_amount + if len(mutually_exclusive) > 1: + raise ValueError( + "Entry rule contains too many identifiers.\n" + f"Rule: {option_value}\n" + f"Remove all but one of these entries: {mutually_exclusive}" + ) + return resolved + + +def _resolve_potential_range(option_value: Union[Any, str]) -> Union[Any, int]: + # An option value may be a range + if type(option_value) == str and option_value.startswith("random-range-"): + resolved = _custom_range(option_value) + return resolved + else: + # As this is a catch-all function, + # assume non-range option values are handled elsewhere + # or intended to fall through + return option_value + + +def _resolve_mission_pool(option_value: Union[str, List[str]]) -> Set[int]: + if type(option_value) == str: + pool = _get_target_missions(option_value) + else: + pool: Set[int] = set() + for line in option_value: + if line.startswith("~"): + if len(pool) == 0: + raise ValueError(f"Mission Pool term {line} tried to remove missions from an empty pool.") + term = line[1:].strip() + missions = _get_target_missions(term) + pool.difference_update(missions) + elif line.startswith("^"): + if len(pool) == 0: + raise ValueError(f"Mission Pool term {line} tried to remove missions from an empty pool.") + term = line[1:].strip() + missions = _get_target_missions(term) + pool.intersection_update(missions) + else: + if line.startswith("+"): + term = line[1:].strip() + else: + term = line.strip() + missions = _get_target_missions(term) + pool.update(missions) + if len(pool) == 0: + raise ValueError(f"Mission pool evaluated to zero missions: {option_value}") + return pool + + +def _get_target_missions(term: str) -> Set[int]: + if term in lookup_name_to_mission: + return {lookup_name_to_mission[term].id} + else: + groups = [mission_groups[group] for group in mission_groups if group.casefold() == term.casefold()] + if len(groups) > 0: + return {lookup_name_to_mission[mission].id for mission in groups[0]} + else: + raise ValueError(f"Mission pool term \"{term}\" did not resolve to any specific mission or mission group.") + + +# Class-agnostic version of AP Options.Range.custom_range +def _custom_range(text: str) -> int: + textsplit = text.split("-") + try: + random_range = [int(textsplit[len(textsplit) - 2]), int(textsplit[len(textsplit) - 1])] + except ValueError: + raise ValueError(f"Invalid random range {text} for option {CustomMissionOrder.__name__}") + random_range.sort() + if text.startswith("random-range-low"): + return _triangular(random_range[0], random_range[1], random_range[0]) + elif text.startswith("random-range-middle"): + return _triangular(random_range[0], random_range[1]) + elif text.startswith("random-range-high"): + return _triangular(random_range[0], random_range[1], random_range[1]) + else: + return random.randint(random_range[0], random_range[1]) + + +def _triangular(lower: int, end: int, tri: typing.Optional[int] = None) -> int: + return int(round(random.triangular(lower, end, tri), 0)) + + +# Version of options.Sc2ItemDict.verify without World +def _resolve_item_names(value: Dict[str, int]) -> Dict[str, int]: + new_value: dict[str, int] = {} + case_insensitive_group_mapping = { + group_name.casefold(): group_value for group_name, group_value in item_name_groups.items() + } + case_insensitive_group_mapping.update({item.casefold(): {item} for item in item_table}) + for group_name in value: + item_names = case_insensitive_group_mapping.get(group_name.casefold(), {group_name}) + for item_name in item_names: + new_value[item_name] = new_value.get(item_name, 0) + value[group_name] + return new_value + \ No newline at end of file diff --git a/worlds/sc2/mission_order/presets_scripted.py b/worlds/sc2/mission_order/presets_scripted.py new file mode 100644 index 00000000..010c7785 --- /dev/null +++ b/worlds/sc2/mission_order/presets_scripted.py @@ -0,0 +1,164 @@ +from typing import Dict, Any, List +import copy + +def _required_option(option: str, options: Dict[str, Any]) -> Any: + """Returns the option value, or raises an error if the option is not present.""" + if option not in options: + raise KeyError(f"Campaign preset is missing required option \"{option}\".") + return options.pop(option) + +def _validate_option(option: str, options: Dict[str, str], default: str, valid_values: List[str]) -> str: + """Returns the option value if it is present and valid, the default if it is not present, or raises an error if it is present but not valid.""" + result = options.pop(option, default) + if result not in valid_values: + raise ValueError(f"Preset option \"{option}\" received unknown value \"{result}\".") + return result + +def make_golden_path(options: Dict[str, Any]) -> Dict[str, Any]: + chain_name_options = ['Mar Sara', 'Agria', 'Redstone', 'Meinhoff', 'Haven', 'Tarsonis', 'Valhalla', 'Char', + 'Umoja', 'Kaldir', 'Zerus', 'Skygeirr Station', 'Dominion Space', 'Korhal', + 'Aiur', 'Glacius', 'Shakuras', 'Ulnar', 'Slayn', + 'Antiga', 'Braxis', 'Chau Sara', 'Moria', 'Tyrador', 'Xil', 'Zhakul', + 'Azeroth', 'Crouton', 'Draenor', 'Sanctuary'] + + size = max(_required_option("size", options), 4) + keys_option_values = ["none", "layouts", "missions", "progressive_layouts", "progressive_missions", "progressive_per_layout"] + keys_option = _validate_option("keys", options, "none", keys_option_values) + min_chains = 2 + max_chains = 6 + two_start_positions = options.pop("two_start_positions", False) + # Compensating for empty mission at start + if two_start_positions: + size += 1 + + class Campaign: + def __init__(self, missions_remaining: int): + self.chain_lengths = [1] + self.chain_padding = [0] + self.required_missions = [0] + self.padding = 0 + self.missions_remaining = missions_remaining + self.mission_counter = 1 + + def add_mission(self, chain: int, required_missions: int = 0, *, is_final: bool = False): + if self.missions_remaining == 0 and not is_final: + return + + self.mission_counter += 1 + self.chain_lengths[chain] += 1 + self.missions_remaining -= 1 + + if chain == 0: + self.padding += 1 + self.required_missions.append(required_missions) + + def add_chain(self): + self.chain_lengths.append(0) + self.chain_padding.append(self.padding) + + campaign = Campaign(size - 2) + current_required_missions = 0 + main_chain_length = 0 + while campaign.missions_remaining > 0: + main_chain_length += 1 + if main_chain_length % 2 == 1: # Adding branches + chains_to_make = 0 if len(campaign.chain_lengths) >= max_chains else min_chains if main_chain_length == 1 else 1 + for _ in range(chains_to_make): + campaign.add_chain() + # Updating branches + for side_chain in range(len(campaign.chain_lengths) - 1, 0, -1): + campaign.add_mission(side_chain) + # Adding main path mission + current_required_missions = (campaign.mission_counter * 3) // 4 + if two_start_positions: + # Compensating for skipped mission at start + current_required_missions -= 1 + campaign.add_mission(0, current_required_missions) + campaign.add_mission(0, current_required_missions, is_final = True) + + # Create mission order preset out of campaign + layout_base = { + "type": "column", + "display_name": chain_name_options, + "unique_name": True, + "missions": [], + } + # Optionally add key requirement to layouts + if keys_option == "layouts": + layout_base["entry_rules"] = [{ "items": { "Key": 1 }}] + elif keys_option == "progressive_layouts": + layout_base["entry_rules"] = [{ "items": { "Progressive Key": 0 }}] + preset = { + str(chain): copy.deepcopy(layout_base) for chain in range(len(campaign.chain_lengths)) + } + preset["0"]["exit"] = True + if not two_start_positions: + preset["0"].pop("entry_rules", []) + for chain in range(len(campaign.chain_lengths)): + length = campaign.chain_lengths[chain] + padding = campaign.chain_padding[chain] + preset[str(chain)]["size"] = padding + length + # Add padding to chain + if padding > 0: + preset[str(chain)]["missions"].append({ + "index": [pad for pad in range(padding)], + "empty": True + }) + + if chain == 0: + if two_start_positions: + preset["0"]["missions"].append({ + "index": 0, + "empty": True + }) + # Main path gets number requirements + for mission in range(1, len(campaign.required_missions)): + preset["0"]["missions"].append({ + "index": mission, + "entry_rules": [{ + "scope": "../..", + "amount": campaign.required_missions[mission] + }] + }) + # Optionally add key requirements except to the starter mission + if keys_option == "missions": + for slot in preset["0"]["missions"]: + if "entry_rules" in slot: + slot["entry_rules"].append({ "items": { "Key": 1 }}) + elif keys_option == "progressive_missions": + for slot in preset["0"]["missions"]: + if "entry_rules" in slot: + slot["entry_rules"].append({ "items": { "Progressive Key": 1 }}) + # No main chain keys for progressive_per_layout keys + else: + # Other paths get main path requirements + if two_start_positions and chain < 3: + preset[str(chain)].pop("entry_rules", []) + for mission in range(length): + target = padding + mission + if two_start_positions and mission == 0 and chain < 3: + preset[str(chain)]["missions"].append({ + "index": target, + "entrance": True + }) + else: + preset[str(chain)]["missions"].append({ + "index": target, + "entry_rules": [{ + "scope": f"../../0/{target}" + }] + }) + # Optionally add key requirements + if keys_option == "missions": + for slot in preset[str(chain)]["missions"]: + if "entry_rules" in slot: + slot["entry_rules"].append({ "items": { "Key": 1 }}) + elif keys_option == "progressive_missions": + for slot in preset[str(chain)]["missions"]: + if "entry_rules" in slot: + slot["entry_rules"].append({ "items": { "Progressive Key": 1 }}) + elif keys_option == "progressive_per_layout": + for slot in preset[str(chain)]["missions"]: + if "entry_rules" in slot: + slot["entry_rules"].append({ "items": { "Progressive Key": 0 }}) + return preset diff --git a/worlds/sc2/mission_order/presets_static.py b/worlds/sc2/mission_order/presets_static.py new file mode 100644 index 00000000..b52e5f13 --- /dev/null +++ b/worlds/sc2/mission_order/presets_static.py @@ -0,0 +1,916 @@ +from typing import Dict, Any, Callable, List, Tuple +import copy + +from ..mission_groups import MissionGroupNames +from ..mission_tables import SC2Mission, SC2Campaign + +preset_mini_wol_with_prophecy = { + "global": { + "type": "column", + "mission_pool": [ + MissionGroupNames.WOL_MISSIONS, + "~ " + MissionGroupNames.RACESWAP_MISSIONS + ] + }, + "Mar Sara": { + "size": 1 + }, + "Colonist": { + "size": 2, + "entry_rules": [ + { "scope": "../Mar Sara" }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] } + ] + }, + "Artifact": { + "size": 3, + "entry_rules": [ + { "scope": "../Mar Sara" }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }, + { "index": 1, "entry_rules": [{ "scope": "../..", "amount": 4 }, { "items": { "Key": 1 }}] }, + { "index": 2, "entry_rules": [{ "scope": "../..", "amount": 8 }, { "items": { "Key": 1 }}] } + ] + }, + "Prophecy": { + "size": 2, + "entry_rules": [ + { "scope": "../Artifact/1" }, + { "items": { "Key": 1 }} + ], + "mission_pool": [ + MissionGroupNames.PROPHECY_MISSIONS, + "~ " + MissionGroupNames.RACESWAP_MISSIONS + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] } + ] + }, + "Covert": { + "size": 2, + "entry_rules": [ + { "scope": "../Mar Sara" }, + { "scope": "..", "amount": 2 }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] } + ] + }, + "Rebellion": { + "size": 2, + "entry_rules": [ + { "scope": "../Mar Sara" }, + { "scope": "..", "amount": 3 }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] } + ] + }, + "Char": { + "size": 3, + "entry_rules": [ + { "scope": "../Artifact" }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }, + { "index": 0, "next": [2] }, + { "index": 1, "entrance": True } + ] + } +} + +preset_mini_wol = copy.deepcopy(preset_mini_wol_with_prophecy) +preset_mini_prophecy = { "Prophecy": preset_mini_wol.pop("Prophecy") } +preset_mini_prophecy["Prophecy"].pop("entry_rules") +preset_mini_prophecy["Prophecy"]["type"] = "gauntlet" +preset_mini_prophecy["Prophecy"]["display_name"] = "" +preset_mini_prophecy["Prophecy"]["missions"].append({ "index": "entrances", "entry_rules": [] }) + +preset_mini_hots = { + "global": { + "type": "column", + "mission_pool": [ + MissionGroupNames.HOTS_MISSIONS, + "~ " + MissionGroupNames.RACESWAP_MISSIONS + ] + }, + "Umoja": { + "size": 1, + }, + "Kaldir": { + "size": 2, + "entry_rules": [ + { "scope": "../Umoja" }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] } + ] + }, + "Char": { + "size": 2, + "entry_rules": [ + { "scope": "../Umoja" }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] } + ] + }, + "Zerus": { + "size": 2, + "entry_rules": [ + { "scope": "../Umoja" }, + { "scope": "..", "amount": 3 }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] } + ] + }, + "Skygeirr Station": { + "size": 2, + "entry_rules": [ + { "scope": "../Zerus" }, + { "scope": "..", "amount": 5 }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] } + ] + }, + "Dominion Space": { + "size": 2, + "entry_rules": [ + { "scope": "../Zerus" }, + { "scope": "..", "amount": 5 }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] } + ] + }, + "Korhal": { + "size": 2, + "entry_rules": [ + { "scope": "../Zerus" }, + { "scope": "..", "amount": 8 }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] } + ] + } +} + +preset_mini_lotv_prologue = { + "min_difficulty": "easy", + "Prologue": { + "display_name": "", + "type": "gauntlet", + "size": 2, + "mission_pool": [ + MissionGroupNames.PROLOGUE_MISSIONS, + "~ " + MissionGroupNames.RACESWAP_MISSIONS + ], + "missions": [ + { "index": 1, "entry_rules": [{ "items": { "Key": 1 }}] } + ] + } +} + +preset_mini_lotv = { + "global": { + "type": "column", + "mission_pool": [ + MissionGroupNames.LOTV_MISSIONS, + "~ " + MissionGroupNames.RACESWAP_MISSIONS + ] + }, + "Aiur": { + "size": 2, + "missions": [ + { "index": 1, "entry_rules": [{ "items": { "Key": 1 }}] } + ] + }, + "Korhal": { + "size": 1, + "entry_rules": [ + { "scope": "../Aiur" }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] } + ] + }, + "Shakuras": { + "size": 1, + "entry_rules": [ + { "scope": "../Aiur" }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] } + ] + }, + "Purifier": { + "size": 2, + "entry_rules": [ + { "scope": "../Korhal" }, + { "scope": "../Shakuras" }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }, + { "index": 1, "entry_rules": [{ "scope": "../../Ulnar" }, { "items": { "Key": 1 }}] } + ] + }, + "Ulnar": { + "size": 1, + "entry_rules": [ + { "scope": "../Purifier/0" }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] } + ] + }, + "Tal'darim": { + "size": 1, + "entry_rules": [ + { "scope": "../Ulnar" }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] } + ] + }, + "Return to Aiur": { + "size": 2, + "entry_rules": [ + { "scope": "../Purifier" }, + { "scope": "../Tal'darim" }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] } + ] + } +} + +preset_mini_lotv_epilogue = { + "min_difficulty": "very hard", + "Epilogue": { + "display_name": "", + "type": "gauntlet", + "size": 2, + "mission_pool": [ + MissionGroupNames.EPILOGUE_MISSIONS, + "~ " + MissionGroupNames.RACESWAP_MISSIONS + ], + "missions": [ + { "index": 1, "entry_rules": [{ "items": { "Key": 1 }}] } + ] + } +} + +preset_mini_nco = { + "min_difficulty": "easy", + "global": { + "type": "column", + "mission_pool": [ + MissionGroupNames.NCO_MISSIONS, + "~ " + MissionGroupNames.RACESWAP_MISSIONS + ] + }, + "Mission Pack 1": { + "size": 2, + "missions": [ + { "index": 1, "entry_rules": [{ "items": { "Key": 1 }}] } + ] + }, + "Mission Pack 2": { + "size": 1, + "entry_rules": [ + { "scope": "../Mission Pack 1" }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] } + ] + }, + "Mission Pack 3": { + "size": 2, + "entry_rules": [ + { "scope": "../Mission Pack 2" }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] } + ] + }, +} + +preset_wol_with_prophecy = { + "global": { + "type": "column", + "mission_pool": [ + MissionGroupNames.WOL_MISSIONS, + "~ " + MissionGroupNames.RACESWAP_MISSIONS + ] + }, + "Mar Sara": { + "size": 3, + "missions": [ + { "index": 0, "mission_pool": SC2Mission.LIBERATION_DAY.mission_name }, + { "index": 1, "mission_pool": SC2Mission.THE_OUTLAWS.mission_name }, + { "index": 2, "mission_pool": SC2Mission.ZERO_HOUR.mission_name }, + { "index": [1, 2], "entry_rules": [{ "items": { "Key": 1 }}] } + ] + }, + "Colonist": { + "size": 4, + "entry_rules": [ + { "scope": "../Mar Sara" }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": 1, "next": [2, 3] }, + { "index": 2, "next": [] }, + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }, + { "index": [2, 3], "entry_rules": [{ "scope": "../..", "amount": 7 }, { "items": { "Key": 1 }}] }, + { "index": 0, "mission_pool": SC2Mission.EVACUATION.mission_name }, + { "index": 1, "mission_pool": SC2Mission.OUTBREAK.mission_name }, + { "index": 2, "mission_pool": SC2Mission.SAFE_HAVEN.mission_name }, + { "index": 3, "mission_pool": SC2Mission.HAVENS_FALL.mission_name }, + ] + }, + "Artifact": { + "size": 5, + "entry_rules": [ + { "scope": "../Mar Sara" }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }, + { "index": 1, "entry_rules": [{ "scope": "../..", "amount": 8 }, { "items": { "Key": 1 }}] }, + { "index": 2, "entry_rules": [{ "scope": "../..", "amount": 11 }, { "items": { "Key": 1 }}] }, + { "index": 3, "entry_rules": [{ "scope": "../..", "amount": 14 }, { "items": { "Key": 1 }}] }, + { "index": 0, "mission_pool": SC2Mission.SMASH_AND_GRAB.mission_name }, + { "index": 1, "mission_pool": SC2Mission.THE_DIG.mission_name }, + { "index": 2, "mission_pool": SC2Mission.THE_MOEBIUS_FACTOR.mission_name }, + { "index": 3, "mission_pool": SC2Mission.SUPERNOVA.mission_name }, + { "index": 4, "mission_pool": SC2Mission.MAW_OF_THE_VOID.mission_name }, + ] + }, + "Prophecy": { + "size": 4, + "entry_rules": [ + { "scope": "../Artifact/1" }, + { "items": { "Key": 1 }} + ], + "mission_pool": [ + MissionGroupNames.PROPHECY_MISSIONS, + "~ " + MissionGroupNames.RACESWAP_MISSIONS + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }, + { "index": 0, "mission_pool": SC2Mission.WHISPERS_OF_DOOM.mission_name }, + { "index": 1, "mission_pool": SC2Mission.A_SINISTER_TURN.mission_name }, + { "index": 2, "mission_pool": SC2Mission.ECHOES_OF_THE_FUTURE.mission_name }, + { "index": 3, "mission_pool": SC2Mission.IN_UTTER_DARKNESS.mission_name }, + ] + }, + "Covert": { + "size": 4, + "entry_rules": [ + { "scope": "../Mar Sara" }, + { "scope": "..", "amount": 4 }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": 1, "next": [2, 3] }, + { "index": 2, "next": [] }, + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }, + { "index": [2, 3], "entry_rules": [{ "scope": "../..", "amount": 8 }, { "items": { "Key": 1 }}] }, + { "index": 0, "mission_pool": SC2Mission.DEVILS_PLAYGROUND.mission_name }, + { "index": 1, "mission_pool": SC2Mission.WELCOME_TO_THE_JUNGLE.mission_name }, + { "index": 2, "mission_pool": SC2Mission.BREAKOUT.mission_name }, + { "index": 3, "mission_pool": SC2Mission.GHOST_OF_A_CHANCE.mission_name }, + ] + }, + "Rebellion": { + "size": 5, + "entry_rules": [ + { "scope": "../Mar Sara" }, + { "scope": "..", "amount": 6 }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }, + { "index": 0, "mission_pool": SC2Mission.THE_GREAT_TRAIN_ROBBERY.mission_name }, + { "index": 1, "mission_pool": SC2Mission.CUTTHROAT.mission_name }, + { "index": 2, "mission_pool": SC2Mission.ENGINE_OF_DESTRUCTION.mission_name }, + { "index": 3, "mission_pool": SC2Mission.MEDIA_BLITZ.mission_name }, + { "index": 4, "mission_pool": SC2Mission.PIERCING_OF_THE_SHROUD.mission_name }, + ] + }, + "Char": { + "size": 4, + "entry_rules": [ + { "scope": "../Artifact" }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": 0, "next": [1, 2] }, + { "index": [1, 2], "next": [3] }, + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }, + { "index": 0, "mission_pool": SC2Mission.GATES_OF_HELL.mission_name }, + { "index": 1, "mission_pool": SC2Mission.BELLY_OF_THE_BEAST.mission_name }, + { "index": 2, "mission_pool": SC2Mission.SHATTER_THE_SKY.mission_name }, + { "index": 3, "mission_pool": SC2Mission.ALL_IN.mission_name }, + ] + } +} + +preset_wol = copy.deepcopy(preset_wol_with_prophecy) +preset_prophecy = { "Prophecy": preset_wol.pop("Prophecy") } +preset_prophecy["Prophecy"].pop("entry_rules") +preset_prophecy["Prophecy"]["type"] = "gauntlet" +preset_prophecy["Prophecy"]["display_name"] = "" +preset_prophecy["Prophecy"]["missions"].append({ "index": "entrances", "entry_rules": [] }) + +preset_hots = { + "global": { + "type": "column", + "mission_pool": [ + MissionGroupNames.HOTS_MISSIONS, + "~ " + MissionGroupNames.RACESWAP_MISSIONS + ] + }, + "Umoja": { + "size": 3, + "missions": [ + { "index": [1, 2], "entry_rules": [{ "items": { "Key": 1 }}] }, + { "index": 0, "mission_pool": SC2Mission.LAB_RAT.mission_name }, + { "index": 1, "mission_pool": SC2Mission.BACK_IN_THE_SADDLE.mission_name }, + { "index": 2, "mission_pool": SC2Mission.RENDEZVOUS.mission_name }, + ] + }, + "Kaldir": { + "size": 3, + "entry_rules": [ + { "scope": "../Umoja" }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }, + { "index": 0, "mission_pool": SC2Mission.HARVEST_OF_SCREAMS.mission_name }, + { "index": 1, "mission_pool": SC2Mission.SHOOT_THE_MESSENGER.mission_name }, + { "index": 2, "mission_pool": SC2Mission.ENEMY_WITHIN.mission_name }, + ] + }, + "Char": { + "size": 3, + "entry_rules": [ + { "scope": "../Umoja" }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }, + { "index": 0, "mission_pool": SC2Mission.DOMINATION.mission_name }, + { "index": 1, "mission_pool": SC2Mission.FIRE_IN_THE_SKY.mission_name }, + { "index": 2, "mission_pool": SC2Mission.OLD_SOLDIERS.mission_name }, + ] + }, + "Zerus": { + "size": 3, + "entry_rules": [ + { + "rules": [ + { "scope": "../Kaldir" }, + { "scope": "../Char" } + ], + "amount": 1 + }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }, + { "index": 0, "mission_pool": SC2Mission.WAKING_THE_ANCIENT.mission_name }, + { "index": 1, "mission_pool": SC2Mission.THE_CRUCIBLE.mission_name }, + { "index": 2, "mission_pool": SC2Mission.SUPREME.mission_name }, + ] + }, + "Skygeirr Station": { + "size": 3, + "entry_rules": [ + { "scope": ["../Kaldir", "../Char", "../Zerus"] }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }, + { "index": 0, "mission_pool": SC2Mission.INFESTED.mission_name }, + { "index": 1, "mission_pool": SC2Mission.HAND_OF_DARKNESS.mission_name }, + { "index": 2, "mission_pool": SC2Mission.PHANTOMS_OF_THE_VOID.mission_name }, + ] + }, + "Dominion Space": { + "size": 2, + "entry_rules": [ + { "scope": ["../Kaldir", "../Char", "../Zerus"] }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }, + { "index": 0, "mission_pool": SC2Mission.WITH_FRIENDS_LIKE_THESE.mission_name }, + { "index": 1, "mission_pool": SC2Mission.CONVICTION.mission_name }, + ] + }, + "Korhal": { + "size": 3, + "entry_rules": [ + { "scope": ["../Skygeirr Station", "../Dominion Space"] }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }, + { "index": 0, "mission_pool": SC2Mission.PLANETFALL.mission_name }, + { "index": 1, "mission_pool": SC2Mission.DEATH_FROM_ABOVE.mission_name }, + { "index": 2, "mission_pool": SC2Mission.THE_RECKONING.mission_name }, + ] + } +} + +preset_lotv_prologue = { + "min_difficulty": "easy", + "Prologue": { + "display_name": "", + "type": "gauntlet", + "size": 3, + "mission_pool": [ + MissionGroupNames.PROLOGUE_MISSIONS, + "~ " + MissionGroupNames.RACESWAP_MISSIONS + ], + "missions": [ + { "index": [1, 2], "entry_rules": [{ "items": { "Key": 1 }}] }, + { "index": 0, "mission_pool": SC2Mission.DARK_WHISPERS.mission_name }, + { "index": 1, "mission_pool": SC2Mission.GHOSTS_IN_THE_FOG.mission_name }, + { "index": 2, "mission_pool": SC2Mission.EVIL_AWOKEN.mission_name }, + ] + } +} + +preset_lotv = { + "global": { + "type": "column", + "mission_pool": [ + MissionGroupNames.LOTV_MISSIONS, + "~ " + MissionGroupNames.RACESWAP_MISSIONS + ] + }, + "Aiur": { + "size": 3, + "missions": [ + { "index": [1, 2], "entry_rules": [{ "items": { "Key": 1 }}] }, + { "index": 0, "mission_pool": SC2Mission.FOR_AIUR.mission_name }, + { "index": 1, "mission_pool": SC2Mission.THE_GROWING_SHADOW.mission_name }, + { "index": 2, "mission_pool": SC2Mission.THE_SPEAR_OF_ADUN.mission_name }, + ] + }, + "Korhal": { + "size": 2, + "entry_rules": [ + { "scope": "../Aiur" }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }, + { "index": 0, "mission_pool": SC2Mission.SKY_SHIELD.mission_name }, + { "index": 1, "mission_pool": SC2Mission.BROTHERS_IN_ARMS.mission_name }, + ] + }, + "Shakuras": { + "size": 2, + "entry_rules": [ + { "scope": "../Aiur" }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }, + { "index": 0, "mission_pool": SC2Mission.AMON_S_REACH.mission_name }, + { "index": 1, "mission_pool": SC2Mission.LAST_STAND.mission_name }, + ] + }, + "Purifier": { + "size": 3, + "entry_rules": [ + { + "rules": [ + { "scope": "../Korhal" }, + { "scope": "../Shakuras" } + ], + "amount": 1 + }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }, + { "index": 1, "entry_rules": [{ "scope": "../../Ulnar" }, { "items": { "Key": 1 }}] }, + { "index": 0, "mission_pool": SC2Mission.FORBIDDEN_WEAPON.mission_name }, + { "index": 1, "mission_pool": SC2Mission.UNSEALING_THE_PAST.mission_name }, + { "index": 2, "mission_pool": SC2Mission.PURIFICATION.mission_name }, + ] + }, + "Ulnar": { + "size": 3, + "entry_rules": [ + { + "scope": [ + "../Korhal", + "../Shakuras", + "../Purifier/0" + ] + }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }, + { "index": 0, "mission_pool": SC2Mission.TEMPLE_OF_UNIFICATION.mission_name }, + { "index": 1, "mission_pool": SC2Mission.THE_INFINITE_CYCLE.mission_name }, + { "index": 2, "mission_pool": SC2Mission.HARBINGER_OF_OBLIVION.mission_name }, + ] + }, + "Tal'darim": { + "size": 2, + "entry_rules": [ + { "scope": "../Ulnar" }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }, + { "index": 0, "mission_pool": SC2Mission.STEPS_OF_THE_RITE.mission_name }, + { "index": 1, "mission_pool": SC2Mission.RAK_SHIR.mission_name }, + ] + }, + "Moebius": { + "size": 1, + "entry_rules": [ + { + "rules": [ + { "scope": "../Purifier" }, + { "scope": "../Tal'darim" } + ], + "amount": 1 + }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }, + { "index": 0, "mission_pool": SC2Mission.TEMPLAR_S_CHARGE.mission_name }, + ] + }, + "Return to Aiur": { + "size": 3, + "entry_rules": [ + { "scope": "../Purifier" }, + { "scope": "../Tal'darim" }, + { "scope": "../Moebius" }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }, + { "index": 0, "mission_pool": SC2Mission.TEMPLAR_S_RETURN.mission_name }, + { "index": 1, "mission_pool": SC2Mission.THE_HOST.mission_name }, + { "index": 2, "mission_pool": SC2Mission.SALVATION.mission_name }, + ] + } +} + +preset_lotv_epilogue = { + "min_difficulty": "very hard", + "Epilogue": { + "display_name": "", + "type": "gauntlet", + "size": 3, + "mission_pool": [ + MissionGroupNames.EPILOGUE_MISSIONS, + "~ " + MissionGroupNames.RACESWAP_MISSIONS + ], + "missions": [ + { "index": [1, 2], "entry_rules": [{ "items": { "Key": 1 }}] }, + { "index": 0, "mission_pool": SC2Mission.INTO_THE_VOID.mission_name }, + { "index": 1, "mission_pool": SC2Mission.THE_ESSENCE_OF_ETERNITY.mission_name }, + { "index": 2, "mission_pool": SC2Mission.AMON_S_FALL.mission_name }, + ] + } +} + +preset_nco = { + "min_difficulty": "easy", + "global": { + "type": "column", + "mission_pool": [ + MissionGroupNames.NCO_MISSIONS, + "~ " + MissionGroupNames.RACESWAP_MISSIONS + ] + }, + "Mission Pack 1": { + "size": 3, + "missions": [ + { "index": [1, 2], "entry_rules": [{ "items": { "Key": 1 }}] }, + { "index": 0, "mission_pool": SC2Mission.THE_ESCAPE.mission_name }, + { "index": 1, "mission_pool": SC2Mission.SUDDEN_STRIKE.mission_name }, + { "index": 2, "mission_pool": SC2Mission.ENEMY_INTELLIGENCE.mission_name }, + ] + }, + "Mission Pack 2": { + "size": 3, + "entry_rules": [ + { "scope": "../Mission Pack 1" }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }, + { "index": 0, "mission_pool": SC2Mission.TROUBLE_IN_PARADISE.mission_name }, + { "index": 1, "mission_pool": SC2Mission.NIGHT_TERRORS.mission_name }, + { "index": 2, "mission_pool": SC2Mission.FLASHPOINT.mission_name }, + ] + }, + "Mission Pack 3": { + "size": 3, + "entry_rules": [ + { "scope": "../Mission Pack 2" }, + { "items": { "Key": 1 }} + ], + "missions": [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }, + { "index": 0, "mission_pool": SC2Mission.IN_THE_ENEMY_S_SHADOW.mission_name }, + { "index": 1, "mission_pool": SC2Mission.DARK_SKIES.mission_name }, + { "index": 2, "mission_pool": SC2Mission.END_GAME.mission_name }, + ] + }, +} + +def _build_static_preset(preset: Dict[str, Any], options: Dict[str, Any]) -> Dict[str, Any]: + # Raceswap shuffling + raceswaps = options.pop("shuffle_raceswaps", False) + if not isinstance(raceswaps, bool): + raise ValueError( + f"Preset option \"shuffle_raceswaps\" received unknown value \"{raceswaps}\".\n" + "Valid values are: true, false" + ) + elif raceswaps == True: + # Remove "~ Raceswap Missions" operation from mission pool options + # Also add raceswap variants to plando'd vanilla missions + for layout in preset.values(): + if type(layout) == dict: + # Currently mission pools in layouts are always ["X campaign missions", "~ raceswap missions"] + layout_mission_pool: List[str] = layout.get("mission_pool", None) + if layout_mission_pool is not None: + layout_mission_pool.pop() + layout["mission_pool"] = layout_mission_pool + if "missions" in layout: + for slot in layout["missions"]: + # Currently mission pools in slots are always strings + slot_mission_pool: str = slot.get("mission_pool", None) + # Identify raceswappable missions by their race in brackets + if slot_mission_pool is not None and slot_mission_pool[-1] == ")": + mission_name = slot_mission_pool[:slot_mission_pool.rfind("(")] + new_mission_pool = [f"{mission_name}({race})" for race in ["Terran", "Zerg", "Protoss"]] + slot["mission_pool"] = new_mission_pool + # The presets are set up for no raceswaps, so raceswaps == False doesn't need to be covered + + # Mission pool selection + missions = options.pop("missions", "random") + if missions == "vanilla": + pass # use preset as it is + elif missions == "vanilla_shuffled": + # remove pre-set missions + for layout in preset.values(): + if type(layout) == dict and "missions" in layout: + for slot in layout["missions"]: + slot.pop("mission_pool", ()) + elif missions == "random": + # remove pre-set missions and mission pools + for layout in preset.values(): + if type(layout) == dict: + layout.pop("mission_pool", ()) + if "missions" in layout: + for slot in layout["missions"]: + slot.pop("mission_pool", ()) + else: + raise ValueError( + f"Preset option \"missions\" received unknown value \"{missions}\".\n" + "Valid values are: random, vanilla, vanilla_shuffled" + ) + + # Key rule selection + keys = options.pop("keys", "none") + if keys == "layouts": + # remove keys from mission entry rules + for layout in preset.values(): + if type(layout) == dict and "missions" in layout: + for slot in layout["missions"]: + if "entry_rules" in slot: + slot["entry_rules"] = _remove_key_rules(slot["entry_rules"]) + elif keys == "missions": + # remove keys from layout entry rules + for layout in preset.values(): + if type(layout) == dict and "entry_rules" in layout: + layout["entry_rules"] = _remove_key_rules(layout["entry_rules"]) + elif keys == "progressive_layouts": + # remove keys from mission entry rules, replace keys in layout entry rules with unique-track keys + for layout in preset.values(): + if type(layout) == dict: + if "entry_rules" in layout: + layout["entry_rules"] = _make_key_rules_progressive(layout["entry_rules"], 0) + if "missions" in layout: + for slot in layout["missions"]: + if "entry_rules" in slot: + slot["entry_rules"] = _remove_key_rules(slot["entry_rules"]) + elif keys == "progressive_missions": + # remove keys from layout entry rules, replace keys in mission entry rules + for layout in preset.values(): + if type(layout) == dict: + if "entry_rules" in layout: + layout["entry_rules"] = _remove_key_rules(layout["entry_rules"]) + if "missions" in layout: + for slot in layout["missions"]: + if "entry_rules" in slot: + slot["entry_rules"] = _make_key_rules_progressive(slot["entry_rules"], 1) + elif keys == "progressive_per_layout": + # remove keys from layout entry rules, replace keys in mission entry rules with unique-track keys + # specifically ignore layouts that have no entry rules (and are thus the first of their campaign) + for layout in preset.values(): + if type(layout) == dict and "entry_rules" in layout: + layout["entry_rules"] = _remove_key_rules(layout["entry_rules"]) + if "missions" in layout: + for slot in layout["missions"]: + if "entry_rules" in slot: + slot["entry_rules"] = _make_key_rules_progressive(slot["entry_rules"], 0) + elif keys == "none": + # remove keys from both layout and mission entry rules + for layout in preset.values(): + if type(layout) == dict: + if "entry_rules" in layout: + layout["entry_rules"] = _remove_key_rules(layout["entry_rules"]) + if "missions" in layout: + for slot in layout["missions"]: + if "entry_rules" in slot: + slot["entry_rules"] = _remove_key_rules(slot["entry_rules"]) + else: + raise ValueError( + f"Preset option \"keys\" received unknown value \"{keys}\".\n" + "Valid values are: none, missions, layouts, progressive_missions, progressive_layouts, progressive_per_layout" + ) + + return preset + +def _remove_key_rules(entry_rules: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + return [rule for rule in entry_rules if not ("items" in rule and "Key" in rule["items"])] + +def _make_key_rules_progressive(entry_rules: List[Dict[str, Any]], track: int) -> List[Dict[str, Any]]: + for rule in entry_rules: + if "items" in rule and "Key" in rule["items"]: + new_items: Dict[str, Any] = {} + for (item, amount) in rule["items"].items(): + if item == "Key": + new_items["Progressive Key"] = track + else: + new_items[item] = amount + rule["items"] = new_items + return entry_rules + +def static_preset(preset: Dict[str, Any]) -> Callable[[Dict[str, Any]], Dict[str, Any]]: + return lambda options: _build_static_preset(copy.deepcopy(preset), options) + +def get_used_layout_names() -> Dict[SC2Campaign, Tuple[int, List[str]]]: + campaign_to_preset: Dict[SC2Campaign, Dict[str, Any]] = { + SC2Campaign.WOL: preset_wol_with_prophecy, + SC2Campaign.PROPHECY: preset_prophecy, + SC2Campaign.HOTS: preset_hots, + SC2Campaign.PROLOGUE: preset_lotv_prologue, + SC2Campaign.LOTV: preset_lotv, + SC2Campaign.EPILOGUE: preset_lotv_epilogue, + SC2Campaign.NCO: preset_nco + } + campaign_to_layout_names: Dict[SC2Campaign, Tuple[int, List[str]]] = { SC2Campaign.GLOBAL: (0, []) } + for campaign in SC2Campaign: + if campaign == SC2Campaign.GLOBAL: + continue + previous_campaign = [prev for prev in SC2Campaign if prev.id == campaign.id - 1][0] + previous_size = campaign_to_layout_names[previous_campaign][0] + preset = campaign_to_preset[campaign] + new_layouts = [value for value in preset.keys() if isinstance(preset[value], dict) and value != "global"] + campaign_to_layout_names[campaign] = (previous_size + len(campaign_to_layout_names[previous_campaign][1]), new_layouts) + campaign_to_layout_names.pop(SC2Campaign.GLOBAL) + return campaign_to_layout_names diff --git a/worlds/sc2/mission_order/slot_data.py b/worlds/sc2/mission_order/slot_data.py new file mode 100644 index 00000000..44d4f405 --- /dev/null +++ b/worlds/sc2/mission_order/slot_data.py @@ -0,0 +1,53 @@ +""" +Houses the data structures representing a mission order in slot data. +Creating these is handled by the nodes they represent in .nodes.py. +""" + +from __future__ import annotations +from typing import List, Protocol +from dataclasses import dataclass + +from .entry_rules import SubRuleRuleData + +class MissionOrderObjectSlotData(Protocol): + entry_rule: SubRuleRuleData + + +@dataclass +class CampaignSlotData: + name: str + entry_rule: SubRuleRuleData + exits: List[int] + layouts: List[LayoutSlotData] + + @staticmethod + def legacy(name: str, layouts: List[LayoutSlotData]) -> CampaignSlotData: + return CampaignSlotData(name, SubRuleRuleData.empty(), [], layouts) + + +@dataclass +class LayoutSlotData: + name: str + entry_rule: SubRuleRuleData + exits: List[int] + missions: List[List[MissionSlotData]] + + @staticmethod + def legacy(name: str, missions: List[List[MissionSlotData]]) -> LayoutSlotData: + return LayoutSlotData(name, SubRuleRuleData.empty(), [], missions) + + +@dataclass +class MissionSlotData: + mission_id: int + prev_mission_ids: List[int] + entry_rule: SubRuleRuleData + victory_cache_size: int = 0 + + @staticmethod + def empty() -> MissionSlotData: + return MissionSlotData(-1, [], SubRuleRuleData.empty()) + + @staticmethod + def legacy(mission_id: int, prev_mission_ids: List[int], entry_rule: SubRuleRuleData) -> MissionSlotData: + return MissionSlotData(mission_id, prev_mission_ids, entry_rule) diff --git a/worlds/sc2/mission_tables.py b/worlds/sc2/mission_tables.py new file mode 100644 index 00000000..3dfe50ff --- /dev/null +++ b/worlds/sc2/mission_tables.py @@ -0,0 +1,577 @@ +from typing import NamedTuple, Dict, List, Set, Union, Literal, Iterable, Optional +from enum import IntEnum, Enum, IntFlag, auto + + +class SC2Race(IntEnum): + ANY = 0 + TERRAN = 1 + ZERG = 2 + PROTOSS = 3 + + def get_title(self): + return self.name.lower().capitalize() + + def get_mission_flag(self): + return MissionFlag.__getitem__(self.get_title()) + +class MissionPools(IntEnum): + STARTER = 0 + EASY = 1 + MEDIUM = 2 + HARD = 3 + VERY_HARD = 4 + FINAL = 5 + + +class MissionFlag(IntFlag): + none = 0 + Terran = auto() + Zerg = auto() + Protoss = auto() + NoBuild = auto() + Defense = auto() + AutoScroller = auto() # The mission is won by waiting out a timer or victory is gated behind a timer + Countdown = auto() # Overall, the mission must be beaten before a loss timer counts down + Kerrigan = auto() # The player controls Kerrigan in the mission + VanillaSoa = auto() # The player controls the Spear of Adun in the vanilla version of the mission + Nova = auto() # The player controls NCO Nova in the mission + AiTerranAlly = auto() # The mission has a Terran AI ally that can be taken over + AiZergAlly = auto() # The mission has a Zerg AI ally that can be taken over + AiProtossAlly = auto() # The mission has a Protoss AI ally that can be taken over + VsTerran = auto() + VsZerg = auto() + VsProtoss = auto() + HasRaceSwap = auto() # The mission has variants that use different factions from the vanilla experience. + RaceSwap = auto() # The mission uses different factions from the vanilla experience. + WoLNova = auto() # The player controls WoL Nova in the mission + + AiAlly = AiTerranAlly|AiZergAlly|AiProtossAlly + TimedDefense = AutoScroller|Defense + VsTZ = VsTerran|VsZerg + VsTP = VsTerran|VsProtoss + VsPZ = VsProtoss|VsZerg + VsAll = VsTerran|VsProtoss|VsZerg + + +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 + + def __lt__(self, other: "SC2Campaign"): + return self.id < other.id + + 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.VERY_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, flags: MissionFlag): + 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.flags = flags + + def get_short_name(self): + if self.mission_name.find(' (') == -1: + return self.mission_name + else: + return self.mission_name[:self.mission_name.find(' (')] + + # Wings of Liberty + LIBERATION_DAY = 1, "Liberation Day", SC2Campaign.WOL, "Mar Sara", SC2Race.ANY, MissionPools.STARTER, "ap_liberation_day", MissionFlag.Terran|MissionFlag.NoBuild|MissionFlag.VsTerran + THE_OUTLAWS = 2, "The Outlaws (Terran)", SC2Campaign.WOL, "Mar Sara", SC2Race.TERRAN, MissionPools.EASY, "ap_the_outlaws", MissionFlag.Terran|MissionFlag.VsTerran|MissionFlag.HasRaceSwap + ZERO_HOUR = 3, "Zero Hour (Terran)", SC2Campaign.WOL, "Mar Sara", SC2Race.TERRAN, MissionPools.EASY, "ap_zero_hour", MissionFlag.Terran|MissionFlag.TimedDefense|MissionFlag.VsZerg|MissionFlag.HasRaceSwap + EVACUATION = 4, "Evacuation (Terran)", SC2Campaign.WOL, "Colonist", SC2Race.TERRAN, MissionPools.EASY, "ap_evacuation", MissionFlag.Terran|MissionFlag.AutoScroller|MissionFlag.VsZerg|MissionFlag.HasRaceSwap + OUTBREAK = 5, "Outbreak (Terran)", SC2Campaign.WOL, "Colonist", SC2Race.TERRAN, MissionPools.EASY, "ap_outbreak", MissionFlag.Terran|MissionFlag.Defense|MissionFlag.VsZerg|MissionFlag.HasRaceSwap + SAFE_HAVEN = 6, "Safe Haven (Terran)", SC2Campaign.WOL, "Colonist", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_safe_haven", MissionFlag.Terran|MissionFlag.Countdown|MissionFlag.VsProtoss|MissionFlag.HasRaceSwap + HAVENS_FALL = 7, "Haven's Fall (Terran)", SC2Campaign.WOL, "Colonist", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_havens_fall", MissionFlag.Terran|MissionFlag.VsZerg|MissionFlag.HasRaceSwap + SMASH_AND_GRAB = 8, "Smash and Grab (Terran)", SC2Campaign.WOL, "Artifact", SC2Race.TERRAN, MissionPools.EASY, "ap_smash_and_grab", MissionFlag.Terran|MissionFlag.Countdown|MissionFlag.VsPZ|MissionFlag.HasRaceSwap + THE_DIG = 9, "The Dig (Terran)", SC2Campaign.WOL, "Artifact", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_the_dig", MissionFlag.Terran|MissionFlag.TimedDefense|MissionFlag.VsProtoss|MissionFlag.HasRaceSwap + THE_MOEBIUS_FACTOR = 10, "The Moebius Factor (Terran)", SC2Campaign.WOL, "Artifact", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_the_moebius_factor", MissionFlag.Terran|MissionFlag.Countdown|MissionFlag.VsZerg|MissionFlag.HasRaceSwap + SUPERNOVA = 11, "Supernova (Terran)", SC2Campaign.WOL, "Artifact", SC2Race.TERRAN, MissionPools.HARD, "ap_supernova", MissionFlag.Terran|MissionFlag.Countdown|MissionFlag.VsProtoss|MissionFlag.HasRaceSwap + MAW_OF_THE_VOID = 12, "Maw of the Void (Terran)", SC2Campaign.WOL, "Artifact", SC2Race.TERRAN, MissionPools.HARD, "ap_maw_of_the_void", MissionFlag.Terran|MissionFlag.VsProtoss|MissionFlag.HasRaceSwap + DEVILS_PLAYGROUND = 13, "Devil's Playground (Terran)", SC2Campaign.WOL, "Covert", SC2Race.TERRAN, MissionPools.EASY, "ap_devils_playground", MissionFlag.Terran|MissionFlag.VsZerg|MissionFlag.HasRaceSwap + WELCOME_TO_THE_JUNGLE = 14, "Welcome to the Jungle (Terran)", SC2Campaign.WOL, "Covert", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_welcome_to_the_jungle", MissionFlag.Terran|MissionFlag.VsProtoss|MissionFlag.HasRaceSwap + BREAKOUT = 15, "Breakout", SC2Campaign.WOL, "Covert", SC2Race.ANY, MissionPools.STARTER, "ap_breakout", MissionFlag.Terran|MissionFlag.NoBuild|MissionFlag.VsTerran + GHOST_OF_A_CHANCE = 16, "Ghost of a Chance", SC2Campaign.WOL, "Covert", SC2Race.ANY, MissionPools.STARTER, "ap_ghost_of_a_chance", MissionFlag.Terran|MissionFlag.NoBuild|MissionFlag.VsTerran|MissionFlag.WoLNova + THE_GREAT_TRAIN_ROBBERY = 17, "The Great Train Robbery (Terran)", SC2Campaign.WOL, "Rebellion", SC2Race.TERRAN, MissionPools.EASY, "ap_the_great_train_robbery", MissionFlag.Terran|MissionFlag.AutoScroller|MissionFlag.VsTerran|MissionFlag.HasRaceSwap + CUTTHROAT = 18, "Cutthroat (Terran)", SC2Campaign.WOL, "Rebellion", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_cutthroat", MissionFlag.Terran|MissionFlag.Countdown|MissionFlag.VsTerran|MissionFlag.HasRaceSwap + ENGINE_OF_DESTRUCTION = 19, "Engine of Destruction (Terran)", SC2Campaign.WOL, "Rebellion", SC2Race.TERRAN, MissionPools.HARD, "ap_engine_of_destruction", MissionFlag.Terran|MissionFlag.AutoScroller|MissionFlag.VsTerran|MissionFlag.HasRaceSwap + MEDIA_BLITZ = 20, "Media Blitz (Terran)", SC2Campaign.WOL, "Rebellion", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_media_blitz", MissionFlag.Terran|MissionFlag.VsTerran|MissionFlag.HasRaceSwap + PIERCING_OF_THE_SHROUD = 21, "Piercing the Shroud", SC2Campaign.WOL, "Rebellion", SC2Race.TERRAN, MissionPools.STARTER, "ap_piercing_the_shroud", MissionFlag.Terran|MissionFlag.NoBuild|MissionFlag.VsAll + GATES_OF_HELL = 26, "Gates of Hell (Terran)", SC2Campaign.WOL, "Char", SC2Race.TERRAN, MissionPools.HARD, "ap_gates_of_hell", MissionFlag.Terran|MissionFlag.VsZerg|MissionFlag.HasRaceSwap + BELLY_OF_THE_BEAST = 27, "Belly of the Beast", SC2Campaign.WOL, "Char", SC2Race.ANY, MissionPools.STARTER, "ap_belly_of_the_beast", MissionFlag.Terran|MissionFlag.NoBuild|MissionFlag.VsZerg + SHATTER_THE_SKY = 28, "Shatter the Sky (Terran)", SC2Campaign.WOL, "Char", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_shatter_the_sky", MissionFlag.Terran|MissionFlag.VsZerg|MissionFlag.HasRaceSwap + ALL_IN = 29, "All-In (Terran)", SC2Campaign.WOL, "Char", SC2Race.TERRAN, MissionPools.VERY_HARD, "ap_all_in", MissionFlag.Terran|MissionFlag.TimedDefense|MissionFlag.VsZerg|MissionFlag.HasRaceSwap + + # Prophecy + WHISPERS_OF_DOOM = 22, "Whispers of Doom", SC2Campaign.PROPHECY, "_1", SC2Race.ANY, MissionPools.STARTER, "ap_whispers_of_doom", MissionFlag.Protoss|MissionFlag.NoBuild|MissionFlag.VsZerg + A_SINISTER_TURN = 23, "A Sinister Turn (Protoss)", SC2Campaign.PROPHECY, "_2", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_a_sinister_turn", MissionFlag.Protoss|MissionFlag.VsProtoss|MissionFlag.HasRaceSwap + ECHOES_OF_THE_FUTURE = 24, "Echoes of the Future (Protoss)", SC2Campaign.PROPHECY, "_3", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_echoes_of_the_future", MissionFlag.Protoss|MissionFlag.VsZerg|MissionFlag.HasRaceSwap + IN_UTTER_DARKNESS = 25, "In Utter Darkness (Protoss)", SC2Campaign.PROPHECY, "_4", SC2Race.PROTOSS, MissionPools.HARD, "ap_in_utter_darkness", MissionFlag.Protoss|MissionFlag.TimedDefense|MissionFlag.VsZerg|MissionFlag.HasRaceSwap + + # Heart of the Swarm + LAB_RAT = 30, "Lab Rat (Zerg)", SC2Campaign.HOTS, "Umoja", SC2Race.ZERG, MissionPools.STARTER, "ap_lab_rat", MissionFlag.Zerg|MissionFlag.VsTerran|MissionFlag.HasRaceSwap + BACK_IN_THE_SADDLE = 31, "Back in the Saddle", SC2Campaign.HOTS, "Umoja", SC2Race.ANY, MissionPools.STARTER, "ap_back_in_the_saddle", MissionFlag.Zerg|MissionFlag.Kerrigan|MissionFlag.NoBuild|MissionFlag.VsTZ + RENDEZVOUS = 32, "Rendezvous (Zerg)", SC2Campaign.HOTS, "Umoja", SC2Race.ZERG, MissionPools.EASY, "ap_rendezvous", MissionFlag.Zerg|MissionFlag.Kerrigan|MissionFlag.AutoScroller|MissionFlag.VsTerran|MissionFlag.HasRaceSwap + HARVEST_OF_SCREAMS = 33, "Harvest of Screams (Zerg)", SC2Campaign.HOTS, "Kaldir", SC2Race.ZERG, MissionPools.EASY, "ap_harvest_of_screams", MissionFlag.Zerg|MissionFlag.Kerrigan|MissionFlag.VsProtoss|MissionFlag.HasRaceSwap + SHOOT_THE_MESSENGER = 34, "Shoot the Messenger (Zerg)", SC2Campaign.HOTS, "Kaldir", SC2Race.ZERG, MissionPools.EASY, "ap_shoot_the_messenger", MissionFlag.Zerg|MissionFlag.Kerrigan|MissionFlag.TimedDefense|MissionFlag.Countdown|MissionFlag.VsProtoss|MissionFlag.HasRaceSwap + ENEMY_WITHIN = 35, "Enemy Within", SC2Campaign.HOTS, "Kaldir", SC2Race.ANY, MissionPools.EASY, "ap_enemy_within", MissionFlag.Zerg|MissionFlag.NoBuild|MissionFlag.VsProtoss + DOMINATION = 36, "Domination (Zerg)", SC2Campaign.HOTS, "Char", SC2Race.ZERG, MissionPools.EASY, "ap_domination", MissionFlag.Zerg|MissionFlag.Kerrigan|MissionFlag.Countdown|MissionFlag.VsZerg|MissionFlag.HasRaceSwap + FIRE_IN_THE_SKY = 37, "Fire in the Sky (Zerg)", SC2Campaign.HOTS, "Char", SC2Race.ZERG, MissionPools.MEDIUM, "ap_fire_in_the_sky", MissionFlag.Zerg|MissionFlag.Kerrigan|MissionFlag.Countdown|MissionFlag.VsTerran|MissionFlag.HasRaceSwap + OLD_SOLDIERS = 38, "Old Soldiers (Zerg)", SC2Campaign.HOTS, "Char", SC2Race.ZERG, MissionPools.MEDIUM, "ap_old_soldiers", MissionFlag.Zerg|MissionFlag.Kerrigan|MissionFlag.VsTerran|MissionFlag.HasRaceSwap + WAKING_THE_ANCIENT = 39, "Waking the Ancient (Zerg)", SC2Campaign.HOTS, "Zerus", SC2Race.ZERG, MissionPools.MEDIUM, "ap_waking_the_ancient", MissionFlag.Zerg|MissionFlag.Kerrigan|MissionFlag.VsZerg|MissionFlag.HasRaceSwap + THE_CRUCIBLE = 40, "The Crucible (Zerg)", SC2Campaign.HOTS, "Zerus", SC2Race.ZERG, MissionPools.MEDIUM, "ap_the_crucible", MissionFlag.Zerg|MissionFlag.TimedDefense|MissionFlag.VsZerg|MissionFlag.HasRaceSwap + SUPREME = 41, "Supreme", SC2Campaign.HOTS, "Zerus", SC2Race.ANY, MissionPools.MEDIUM, "ap_supreme", MissionFlag.Zerg|MissionFlag.Kerrigan|MissionFlag.NoBuild|MissionFlag.VsZerg + INFESTED = 42, "Infested (Zerg)", SC2Campaign.HOTS, "Skygeirr Station", SC2Race.ZERG, MissionPools.MEDIUM, "ap_infested", MissionFlag.Zerg|MissionFlag.Kerrigan|MissionFlag.VsTerran|MissionFlag.HasRaceSwap + HAND_OF_DARKNESS = 43, "Hand of Darkness (Zerg)", SC2Campaign.HOTS, "Skygeirr Station", SC2Race.ZERG, MissionPools.HARD, "ap_hand_of_darkness", MissionFlag.Zerg|MissionFlag.Kerrigan|MissionFlag.Countdown|MissionFlag.VsTerran|MissionFlag.HasRaceSwap + PHANTOMS_OF_THE_VOID = 44, "Phantoms of the Void (Zerg)", SC2Campaign.HOTS, "Skygeirr Station", SC2Race.ZERG, MissionPools.MEDIUM, "ap_phantoms_of_the_void", MissionFlag.Zerg|MissionFlag.Countdown|MissionFlag.VsProtoss|MissionFlag.HasRaceSwap + WITH_FRIENDS_LIKE_THESE = 45, "With Friends Like These", SC2Campaign.HOTS, "Dominion Space", SC2Race.ANY, MissionPools.STARTER, "ap_with_friends_like_these", MissionFlag.Terran|MissionFlag.NoBuild|MissionFlag.VsTerran + CONVICTION = 46, "Conviction", SC2Campaign.HOTS, "Dominion Space", SC2Race.ANY, MissionPools.MEDIUM, "ap_conviction", MissionFlag.Zerg|MissionFlag.Kerrigan|MissionFlag.NoBuild|MissionFlag.VsTerran + PLANETFALL = 47, "Planetfall (Zerg)", SC2Campaign.HOTS, "Korhal", SC2Race.ZERG, MissionPools.HARD, "ap_planetfall", MissionFlag.Zerg|MissionFlag.Kerrigan|MissionFlag.AutoScroller|MissionFlag.VsTerran|MissionFlag.HasRaceSwap + DEATH_FROM_ABOVE = 48, "Death From Above (Zerg)", SC2Campaign.HOTS, "Korhal", SC2Race.ZERG, MissionPools.HARD, "ap_death_from_above", MissionFlag.Zerg|MissionFlag.Kerrigan|MissionFlag.VsTerran|MissionFlag.HasRaceSwap + THE_RECKONING = 49, "The Reckoning (Zerg)", SC2Campaign.HOTS, "Korhal", SC2Race.ZERG, MissionPools.VERY_HARD, "ap_the_reckoning", MissionFlag.Zerg|MissionFlag.Kerrigan|MissionFlag.VsTerran|MissionFlag.AiTerranAlly|MissionFlag.HasRaceSwap + + # Prologue + DARK_WHISPERS = 50, "Dark Whispers (Protoss)", SC2Campaign.PROLOGUE, "_1", SC2Race.PROTOSS, MissionPools.EASY, "ap_dark_whispers", MissionFlag.Protoss|MissionFlag.Countdown|MissionFlag.VsTZ|MissionFlag.HasRaceSwap + GHOSTS_IN_THE_FOG = 51, "Ghosts in the Fog (Protoss)", SC2Campaign.PROLOGUE, "_2", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_ghosts_in_the_fog", MissionFlag.Protoss|MissionFlag.VsProtoss|MissionFlag.HasRaceSwap + EVIL_AWOKEN = 52, "Evil Awoken", SC2Campaign.PROLOGUE, "_3", SC2Race.PROTOSS, MissionPools.STARTER, "ap_evil_awoken", MissionFlag.Protoss|MissionFlag.NoBuild|MissionFlag.VsProtoss + + # LotV + FOR_AIUR = 53, "For Aiur!", SC2Campaign.LOTV, "Aiur", SC2Race.ANY, MissionPools.STARTER, "ap_for_aiur", MissionFlag.Protoss|MissionFlag.NoBuild|MissionFlag.VsZerg + THE_GROWING_SHADOW = 54, "The Growing Shadow (Protoss)", SC2Campaign.LOTV, "Aiur", SC2Race.PROTOSS, MissionPools.EASY, "ap_the_growing_shadow", MissionFlag.Protoss|MissionFlag.VsPZ|MissionFlag.HasRaceSwap + THE_SPEAR_OF_ADUN = 55, "The Spear of Adun (Protoss)", SC2Campaign.LOTV, "Aiur", SC2Race.PROTOSS, MissionPools.EASY, "ap_the_spear_of_adun", MissionFlag.Protoss|MissionFlag.VanillaSoa|MissionFlag.VsPZ|MissionFlag.HasRaceSwap + SKY_SHIELD = 56, "Sky Shield (Protoss)", SC2Campaign.LOTV, "Korhal", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_sky_shield", MissionFlag.Protoss|MissionFlag.VanillaSoa|MissionFlag.Countdown|MissionFlag.VsTerran|MissionFlag.AiTerranAlly|MissionFlag.HasRaceSwap + BROTHERS_IN_ARMS = 57, "Brothers in Arms (Protoss)", SC2Campaign.LOTV, "Korhal", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_brothers_in_arms", MissionFlag.Protoss|MissionFlag.VanillaSoa|MissionFlag.VsTerran|MissionFlag.AiTerranAlly|MissionFlag.HasRaceSwap + AMON_S_REACH = 58, "Amon's Reach (Protoss)", SC2Campaign.LOTV, "Shakuras", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_amon_s_reach", MissionFlag.Protoss|MissionFlag.VanillaSoa|MissionFlag.VsZerg|MissionFlag.HasRaceSwap + LAST_STAND = 59, "Last Stand (Protoss)", SC2Campaign.LOTV, "Shakuras", SC2Race.PROTOSS, MissionPools.HARD, "ap_last_stand", MissionFlag.Protoss|MissionFlag.VanillaSoa|MissionFlag.TimedDefense|MissionFlag.VsZerg|MissionFlag.HasRaceSwap + FORBIDDEN_WEAPON = 60, "Forbidden Weapon (Protoss)", SC2Campaign.LOTV, "Purifier", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_forbidden_weapon", MissionFlag.Protoss|MissionFlag.VanillaSoa|MissionFlag.Countdown|MissionFlag.VsProtoss|MissionFlag.HasRaceSwap + TEMPLE_OF_UNIFICATION = 61, "Temple of Unification (Protoss)", SC2Campaign.LOTV, "Ulnar", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_temple_of_unification", MissionFlag.Protoss|MissionFlag.VanillaSoa|MissionFlag.VsTP|MissionFlag.HasRaceSwap + THE_INFINITE_CYCLE = 62, "The Infinite Cycle", SC2Campaign.LOTV, "Ulnar", SC2Race.ANY, MissionPools.HARD, "ap_the_infinite_cycle", MissionFlag.Protoss|MissionFlag.Kerrigan|MissionFlag.NoBuild|MissionFlag.VsTP + HARBINGER_OF_OBLIVION = 63, "Harbinger of Oblivion (Protoss)", SC2Campaign.LOTV, "Ulnar", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_harbinger_of_oblivion", MissionFlag.Protoss|MissionFlag.VanillaSoa|MissionFlag.Countdown|MissionFlag.VsTP|MissionFlag.AiZergAlly|MissionFlag.HasRaceSwap + UNSEALING_THE_PAST = 64, "Unsealing the Past (Protoss)", SC2Campaign.LOTV, "Purifier", SC2Race.PROTOSS, MissionPools.HARD, "ap_unsealing_the_past", MissionFlag.Protoss|MissionFlag.VanillaSoa|MissionFlag.AutoScroller|MissionFlag.VsZerg|MissionFlag.HasRaceSwap + PURIFICATION = 65, "Purification (Protoss)", SC2Campaign.LOTV, "Purifier", SC2Race.PROTOSS, MissionPools.HARD, "ap_purification", MissionFlag.Protoss|MissionFlag.VanillaSoa|MissionFlag.VsZerg|MissionFlag.HasRaceSwap + STEPS_OF_THE_RITE = 66, "Steps of the Rite (Protoss)", SC2Campaign.LOTV, "Tal'darim", SC2Race.PROTOSS, MissionPools.HARD, "ap_steps_of_the_rite", MissionFlag.Protoss|MissionFlag.VanillaSoa|MissionFlag.VsProtoss|MissionFlag.HasRaceSwap + RAK_SHIR = 67, "Rak'Shir (Protoss)", SC2Campaign.LOTV, "Tal'darim", SC2Race.PROTOSS, MissionPools.HARD, "ap_rak_shir", MissionFlag.Protoss|MissionFlag.VanillaSoa|MissionFlag.VsProtoss|MissionFlag.HasRaceSwap + TEMPLAR_S_CHARGE = 68, "Templar's Charge (Protoss)", SC2Campaign.LOTV, "Moebius", SC2Race.PROTOSS, MissionPools.HARD, "ap_templar_s_charge", MissionFlag.Protoss|MissionFlag.VanillaSoa|MissionFlag.VsTerran|MissionFlag.HasRaceSwap + TEMPLAR_S_RETURN = 69, "Templar's Return", SC2Campaign.LOTV, "Return to Aiur", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_templar_s_return", MissionFlag.Protoss|MissionFlag.NoBuild|MissionFlag.VsPZ + THE_HOST = 70, "The Host (Protoss)", SC2Campaign.LOTV, "Return to Aiur", SC2Race.PROTOSS, MissionPools.VERY_HARD, "ap_the_host", MissionFlag.Protoss|MissionFlag.VanillaSoa|MissionFlag.VsAll|MissionFlag.HasRaceSwap + SALVATION = 71, "Salvation (Protoss)", SC2Campaign.LOTV, "Return to Aiur", SC2Race.PROTOSS, MissionPools.VERY_HARD, "ap_salvation", MissionFlag.Protoss|MissionFlag.VanillaSoa|MissionFlag.TimedDefense|MissionFlag.VsPZ|MissionFlag.AiProtossAlly|MissionFlag.HasRaceSwap + + # Epilogue + INTO_THE_VOID = 72, "Into the Void", SC2Campaign.EPILOGUE, "_1", SC2Race.PROTOSS, MissionPools.VERY_HARD, "ap_into_the_void", MissionFlag.Protoss|MissionFlag.VanillaSoa|MissionFlag.VsAll|MissionFlag.AiTerranAlly|MissionFlag.AiZergAlly + THE_ESSENCE_OF_ETERNITY = 73, "The Essence of Eternity", SC2Campaign.EPILOGUE, "_2", SC2Race.TERRAN, MissionPools.VERY_HARD, "ap_the_essence_of_eternity", MissionFlag.Terran|MissionFlag.TimedDefense|MissionFlag.VsAll|MissionFlag.AiZergAlly|MissionFlag.AiProtossAlly + AMON_S_FALL = 74, "Amon's Fall", SC2Campaign.EPILOGUE, "_3", SC2Race.ZERG, MissionPools.VERY_HARD, "ap_amon_s_fall", MissionFlag.Zerg|MissionFlag.AutoScroller|MissionFlag.VsAll|MissionFlag.AiTerranAlly|MissionFlag.AiProtossAlly + + # Nova Covert Ops + THE_ESCAPE = 75, "The Escape", SC2Campaign.NCO, "_1", SC2Race.ANY, MissionPools.MEDIUM, "ap_the_escape", MissionFlag.Terran|MissionFlag.Nova|MissionFlag.NoBuild|MissionFlag.VsTerran + SUDDEN_STRIKE = 76, "Sudden Strike", SC2Campaign.NCO, "_1", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_sudden_strike", MissionFlag.Terran|MissionFlag.Nova|MissionFlag.TimedDefense|MissionFlag.VsZerg + ENEMY_INTELLIGENCE = 77, "Enemy Intelligence", SC2Campaign.NCO, "_1", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_enemy_intelligence", MissionFlag.Terran|MissionFlag.Nova|MissionFlag.Defense|MissionFlag.VsZerg + TROUBLE_IN_PARADISE = 78, "Trouble In Paradise", SC2Campaign.NCO, "_2", SC2Race.TERRAN, MissionPools.HARD, "ap_trouble_in_paradise", MissionFlag.Terran|MissionFlag.Nova|MissionFlag.Countdown|MissionFlag.VsPZ + NIGHT_TERRORS = 79, "Night Terrors", SC2Campaign.NCO, "_2", SC2Race.TERRAN, MissionPools.HARD, "ap_night_terrors", MissionFlag.Terran|MissionFlag.Nova|MissionFlag.VsPZ + FLASHPOINT = 80, "Flashpoint", SC2Campaign.NCO, "_2", SC2Race.TERRAN, MissionPools.HARD, "ap_flashpoint", MissionFlag.Terran|MissionFlag.Nova|MissionFlag.VsZerg + IN_THE_ENEMY_S_SHADOW = 81, "In the Enemy's Shadow", SC2Campaign.NCO, "_3", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_in_the_enemy_s_shadow", MissionFlag.Terran|MissionFlag.Nova|MissionFlag.NoBuild|MissionFlag.VsTerran + DARK_SKIES = 82, "Dark Skies", SC2Campaign.NCO, "_3", SC2Race.TERRAN, MissionPools.HARD, "ap_dark_skies", MissionFlag.Terran|MissionFlag.Nova|MissionFlag.TimedDefense|MissionFlag.VsProtoss + END_GAME = 83, "End Game", SC2Campaign.NCO, "_3", SC2Race.TERRAN, MissionPools.VERY_HARD, "ap_end_game", MissionFlag.Terran|MissionFlag.Nova|MissionFlag.Defense|MissionFlag.VsTerran + + # Race-Swapped Variants + # 84/85 - Liberation Day + THE_OUTLAWS_Z = 86, "The Outlaws (Zerg)", SC2Campaign.WOL, "Mar Sara", SC2Race.ZERG, MissionPools.EASY, "ap_the_outlaws", MissionFlag.Zerg|MissionFlag.VsTerran|MissionFlag.RaceSwap + THE_OUTLAWS_P = 87, "The Outlaws (Protoss)", SC2Campaign.WOL, "Mar Sara", SC2Race.PROTOSS, MissionPools.EASY, "ap_the_outlaws", MissionFlag.Protoss|MissionFlag.VsTerran|MissionFlag.RaceSwap + ZERO_HOUR_Z = 88, "Zero Hour (Zerg)", SC2Campaign.WOL, "Mar Sara", SC2Race.ZERG, MissionPools.MEDIUM, "ap_zero_hour", MissionFlag.Zerg|MissionFlag.TimedDefense|MissionFlag.VsZerg|MissionFlag.RaceSwap + ZERO_HOUR_P = 89, "Zero Hour (Protoss)", SC2Campaign.WOL, "Mar Sara", SC2Race.PROTOSS, MissionPools.EASY, "ap_zero_hour", MissionFlag.Protoss|MissionFlag.TimedDefense|MissionFlag.VsZerg|MissionFlag.RaceSwap + EVACUATION_Z = 90, "Evacuation (Zerg)", SC2Campaign.WOL, "Colonist", SC2Race.ZERG, MissionPools.EASY, "ap_evacuation", MissionFlag.Zerg|MissionFlag.AutoScroller|MissionFlag.VsZerg|MissionFlag.RaceSwap + EVACUATION_P = 91, "Evacuation (Protoss)", SC2Campaign.WOL, "Colonist", SC2Race.PROTOSS, MissionPools.EASY, "ap_evacuation", MissionFlag.Protoss|MissionFlag.AutoScroller|MissionFlag.VsZerg|MissionFlag.RaceSwap + OUTBREAK_Z = 92, "Outbreak (Zerg)", SC2Campaign.WOL, "Colonist", SC2Race.ZERG, MissionPools.MEDIUM, "ap_outbreak", MissionFlag.Zerg|MissionFlag.Defense|MissionFlag.VsZerg|MissionFlag.RaceSwap + OUTBREAK_P = 93, "Outbreak (Protoss)", SC2Campaign.WOL, "Colonist", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_outbreak", MissionFlag.Protoss|MissionFlag.Defense|MissionFlag.VsZerg|MissionFlag.RaceSwap + SAFE_HAVEN_Z = 94, "Safe Haven (Zerg)", SC2Campaign.WOL, "Colonist", SC2Race.ZERG, MissionPools.MEDIUM, "ap_safe_haven", MissionFlag.Zerg|MissionFlag.Countdown|MissionFlag.VsProtoss|MissionFlag.RaceSwap + SAFE_HAVEN_P = 95, "Safe Haven (Protoss)", SC2Campaign.WOL, "Colonist", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_safe_haven", MissionFlag.Protoss|MissionFlag.Countdown|MissionFlag.VsProtoss|MissionFlag.RaceSwap + HAVENS_FALL_Z = 96, "Haven's Fall (Zerg)", SC2Campaign.WOL, "Colonist", SC2Race.ZERG, MissionPools.MEDIUM, "ap_havens_fall", MissionFlag.Zerg|MissionFlag.VsZerg|MissionFlag.RaceSwap + HAVENS_FALL_P = 97, "Haven's Fall (Protoss)", SC2Campaign.WOL, "Colonist", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_havens_fall", MissionFlag.Protoss|MissionFlag.VsZerg|MissionFlag.RaceSwap + SMASH_AND_GRAB_Z = 98, "Smash and Grab (Zerg)", SC2Campaign.WOL, "Artifact", SC2Race.ZERG, MissionPools.EASY, "ap_smash_and_grab", MissionFlag.Zerg|MissionFlag.Countdown|MissionFlag.VsPZ|MissionFlag.RaceSwap + SMASH_AND_GRAB_P = 99, "Smash and Grab (Protoss)", SC2Campaign.WOL, "Artifact", SC2Race.PROTOSS, MissionPools.EASY, "ap_smash_and_grab", MissionFlag.Protoss|MissionFlag.Countdown|MissionFlag.VsPZ|MissionFlag.RaceSwap + THE_DIG_Z = 100, "The Dig (Zerg)", SC2Campaign.WOL, "Artifact", SC2Race.ZERG, MissionPools.MEDIUM, "ap_the_dig", MissionFlag.Zerg|MissionFlag.TimedDefense|MissionFlag.VsProtoss|MissionFlag.RaceSwap + THE_DIG_P = 101, "The Dig (Protoss)", SC2Campaign.WOL, "Artifact", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_the_dig", MissionFlag.Protoss|MissionFlag.TimedDefense|MissionFlag.VsProtoss|MissionFlag.RaceSwap + THE_MOEBIUS_FACTOR_Z = 102, "The Moebius Factor (Zerg)", SC2Campaign.WOL, "Artifact", SC2Race.ZERG, MissionPools.MEDIUM, "ap_the_moebius_factor", MissionFlag.Zerg|MissionFlag.Countdown|MissionFlag.VsZerg|MissionFlag.RaceSwap + THE_MOEBIUS_FACTOR_P = 103, "The Moebius Factor (Protoss)", SC2Campaign.WOL, "Artifact", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_the_moebius_factor", MissionFlag.Protoss|MissionFlag.Countdown|MissionFlag.VsZerg|MissionFlag.RaceSwap + SUPERNOVA_Z = 104, "Supernova (Zerg)", SC2Campaign.WOL, "Artifact", SC2Race.ZERG, MissionPools.HARD, "ap_supernova", MissionFlag.Zerg|MissionFlag.Countdown|MissionFlag.VsProtoss|MissionFlag.RaceSwap + SUPERNOVA_P = 105, "Supernova (Protoss)", SC2Campaign.WOL, "Artifact", SC2Race.PROTOSS, MissionPools.HARD, "ap_supernova", MissionFlag.Protoss|MissionFlag.Countdown|MissionFlag.VsProtoss|MissionFlag.RaceSwap + MAW_OF_THE_VOID_Z = 106, "Maw of the Void (Zerg)", SC2Campaign.WOL, "Artifact", SC2Race.ZERG, MissionPools.HARD, "ap_maw_of_the_void", MissionFlag.Zerg|MissionFlag.VsProtoss|MissionFlag.RaceSwap + MAW_OF_THE_VOID_P = 107, "Maw of the Void (Protoss)", SC2Campaign.WOL, "Artifact", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_maw_of_the_void", MissionFlag.Protoss|MissionFlag.VsProtoss|MissionFlag.RaceSwap + DEVILS_PLAYGROUND_Z = 108, "Devil's Playground (Zerg)", SC2Campaign.WOL, "Covert", SC2Race.ZERG, MissionPools.EASY, "ap_devils_playground", MissionFlag.Zerg|MissionFlag.VsZerg|MissionFlag.RaceSwap + DEVILS_PLAYGROUND_P = 109, "Devil's Playground (Protoss)", SC2Campaign.WOL, "Covert", SC2Race.PROTOSS, MissionPools.EASY, "ap_devils_playground", MissionFlag.Protoss|MissionFlag.VsZerg|MissionFlag.RaceSwap + WELCOME_TO_THE_JUNGLE_Z = 110, "Welcome to the Jungle (Zerg)", SC2Campaign.WOL, "Covert", SC2Race.ZERG, MissionPools.HARD, "ap_welcome_to_the_jungle", MissionFlag.Zerg|MissionFlag.VsProtoss|MissionFlag.RaceSwap + WELCOME_TO_THE_JUNGLE_P = 111, "Welcome to the Jungle (Protoss)", SC2Campaign.WOL, "Covert", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_welcome_to_the_jungle", MissionFlag.Protoss|MissionFlag.VsProtoss|MissionFlag.RaceSwap + # 112/113 - Breakout + # 114/115 - Ghost of a Chance + THE_GREAT_TRAIN_ROBBERY_Z = 116, "The Great Train Robbery (Zerg)", SC2Campaign.WOL, "Rebellion", SC2Race.ZERG, MissionPools.EASY, "ap_the_great_train_robbery", MissionFlag.Zerg|MissionFlag.AutoScroller|MissionFlag.VsTerran|MissionFlag.RaceSwap + THE_GREAT_TRAIN_ROBBERY_P = 117, "The Great Train Robbery (Protoss)", SC2Campaign.WOL, "Rebellion", SC2Race.PROTOSS, MissionPools.EASY, "ap_the_great_train_robbery", MissionFlag.Protoss|MissionFlag.AutoScroller|MissionFlag.VsTerran|MissionFlag.RaceSwap + CUTTHROAT_Z = 118, "Cutthroat (Zerg)", SC2Campaign.WOL, "Rebellion", SC2Race.ZERG, MissionPools.MEDIUM, "ap_cutthroat", MissionFlag.Zerg|MissionFlag.Countdown|MissionFlag.VsTerran|MissionFlag.RaceSwap + CUTTHROAT_P = 119, "Cutthroat (Protoss)", SC2Campaign.WOL, "Rebellion", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_cutthroat", MissionFlag.Protoss|MissionFlag.Countdown|MissionFlag.VsTerran|MissionFlag.RaceSwap + ENGINE_OF_DESTRUCTION_Z = 120, "Engine of Destruction (Zerg)", SC2Campaign.WOL, "Rebellion", SC2Race.ZERG, MissionPools.HARD, "ap_engine_of_destruction", MissionFlag.Zerg|MissionFlag.AutoScroller|MissionFlag.VsTerran|MissionFlag.RaceSwap + ENGINE_OF_DESTRUCTION_P = 121, "Engine of Destruction (Protoss)", SC2Campaign.WOL, "Rebellion", SC2Race.PROTOSS, MissionPools.HARD, "ap_engine_of_destruction", MissionFlag.Protoss|MissionFlag.AutoScroller|MissionFlag.VsTerran|MissionFlag.RaceSwap + MEDIA_BLITZ_Z = 122, "Media Blitz (Zerg)", SC2Campaign.WOL, "Rebellion", SC2Race.ZERG, MissionPools.HARD, "ap_media_blitz", MissionFlag.Zerg|MissionFlag.VsTerran|MissionFlag.RaceSwap + MEDIA_BLITZ_P = 123, "Media Blitz (Protoss)", SC2Campaign.WOL, "Rebellion", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_media_blitz", MissionFlag.Protoss|MissionFlag.VsTerran|MissionFlag.RaceSwap + # 124/125 - Piercing the Shroud + # 126/127 - Whispers of Doom + A_SINISTER_TURN_T = 128, "A Sinister Turn (Terran)", SC2Campaign.PROPHECY, "_2", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_a_sinister_turn", MissionFlag.Terran|MissionFlag.VsProtoss|MissionFlag.RaceSwap + A_SINISTER_TURN_Z = 129, "A Sinister Turn (Zerg)", SC2Campaign.PROPHECY, "_2", SC2Race.ZERG, MissionPools.MEDIUM, "ap_a_sinister_turn", MissionFlag.Zerg|MissionFlag.VsProtoss|MissionFlag.RaceSwap + ECHOES_OF_THE_FUTURE_T = 130, "Echoes of the Future (Terran)", SC2Campaign.PROPHECY, "_3", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_echoes_of_the_future", MissionFlag.Terran|MissionFlag.VsZerg|MissionFlag.RaceSwap + ECHOES_OF_THE_FUTURE_Z = 131, "Echoes of the Future (Zerg)", SC2Campaign.PROPHECY, "_3", SC2Race.ZERG, MissionPools.MEDIUM, "ap_echoes_of_the_future", MissionFlag.Zerg|MissionFlag.VsZerg|MissionFlag.RaceSwap + IN_UTTER_DARKNESS_T = 132, "In Utter Darkness (Terran)", SC2Campaign.PROPHECY, "_4", SC2Race.TERRAN, MissionPools.HARD, "ap_in_utter_darkness", MissionFlag.Terran|MissionFlag.TimedDefense|MissionFlag.VsZerg|MissionFlag.RaceSwap + IN_UTTER_DARKNESS_Z = 133, "In Utter Darkness (Zerg)", SC2Campaign.PROPHECY, "_4", SC2Race.ZERG, MissionPools.HARD, "ap_in_utter_darkness", MissionFlag.Zerg|MissionFlag.TimedDefense|MissionFlag.VsZerg|MissionFlag.RaceSwap + GATES_OF_HELL_Z = 134, "Gates of Hell (Zerg)", SC2Campaign.WOL, "Char", SC2Race.ZERG, MissionPools.HARD, "ap_gates_of_hell", MissionFlag.Zerg|MissionFlag.VsZerg|MissionFlag.RaceSwap + GATES_OF_HELL_P = 135, "Gates of Hell (Protoss)", SC2Campaign.WOL, "Char", SC2Race.PROTOSS, MissionPools.HARD, "ap_gates_of_hell", MissionFlag.Protoss|MissionFlag.VsZerg|MissionFlag.RaceSwap + # 136/137 - Belly of the Beast + SHATTER_THE_SKY_Z = 138, "Shatter the Sky (Zerg)", SC2Campaign.WOL, "Char", SC2Race.ZERG, MissionPools.HARD, "ap_shatter_the_sky", MissionFlag.Zerg|MissionFlag.VsZerg|MissionFlag.RaceSwap + SHATTER_THE_SKY_P = 139, "Shatter the Sky (Protoss)", SC2Campaign.WOL, "Char", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_shatter_the_sky", MissionFlag.Protoss|MissionFlag.VsZerg|MissionFlag.RaceSwap + ALL_IN_Z = 140, "All-In (Zerg)", SC2Campaign.WOL, "Char", SC2Race.ZERG, MissionPools.VERY_HARD, "ap_all_in", MissionFlag.Zerg|MissionFlag.TimedDefense|MissionFlag.VsZerg|MissionFlag.RaceSwap + ALL_IN_P = 141, "All-In (Protoss)", SC2Campaign.WOL, "Char", SC2Race.PROTOSS, MissionPools.VERY_HARD, "ap_all_in", MissionFlag.Protoss|MissionFlag.TimedDefense|MissionFlag.VsZerg|MissionFlag.RaceSwap + LAB_RAT_T = 142, "Lab Rat (Terran)", SC2Campaign.HOTS, "Umoja", SC2Race.TERRAN, MissionPools.STARTER, "ap_lab_rat", MissionFlag.Terran|MissionFlag.VsTerran|MissionFlag.RaceSwap + LAB_RAT_P = 143, "Lab Rat (Protoss)", SC2Campaign.HOTS, "Umoja", SC2Race.PROTOSS, MissionPools.STARTER, "ap_lab_rat", MissionFlag.Protoss|MissionFlag.VsTerran|MissionFlag.RaceSwap + # 144/145 - Back in the Saddle + RENDEZVOUS_T = 146, "Rendezvous (Terran)", SC2Campaign.HOTS, "Umoja", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_rendezvous", MissionFlag.Terran|MissionFlag.AutoScroller|MissionFlag.VsTerran|MissionFlag.RaceSwap + RENDEZVOUS_P = 147, "Rendezvous (Protoss)", SC2Campaign.HOTS, "Umoja", SC2Race.PROTOSS, MissionPools.EASY, "ap_rendezvous", MissionFlag.Protoss|MissionFlag.AutoScroller|MissionFlag.VsTerran|MissionFlag.RaceSwap + HARVEST_OF_SCREAMS_T = 148, "Harvest of Screams (Terran)", SC2Campaign.HOTS, "Kaldir", SC2Race.TERRAN, MissionPools.EASY, "ap_harvest_of_screams", MissionFlag.Terran|MissionFlag.VsProtoss|MissionFlag.RaceSwap + HARVEST_OF_SCREAMS_P = 149, "Harvest of Screams (Protoss)", SC2Campaign.HOTS, "Kaldir", SC2Race.PROTOSS, MissionPools.EASY, "ap_harvest_of_screams", MissionFlag.Protoss|MissionFlag.VsProtoss|MissionFlag.RaceSwap + SHOOT_THE_MESSENGER_T = 150, "Shoot the Messenger (Terran)", SC2Campaign.HOTS, "Kaldir", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_shoot_the_messenger", MissionFlag.Terran|MissionFlag.TimedDefense|MissionFlag.Countdown|MissionFlag.VsProtoss|MissionFlag.RaceSwap + SHOOT_THE_MESSENGER_P = 151, "Shoot the Messenger (Protoss)", SC2Campaign.HOTS, "Kaldir", SC2Race.PROTOSS, MissionPools.EASY, "ap_shoot_the_messenger", MissionFlag.Protoss|MissionFlag.TimedDefense|MissionFlag.Countdown|MissionFlag.VsProtoss|MissionFlag.RaceSwap + # 152/153 - Enemy Within + DOMINATION_T = 154, "Domination (Terran)", SC2Campaign.HOTS, "Char", SC2Race.TERRAN, MissionPools.EASY, "ap_domination", MissionFlag.Terran|MissionFlag.Countdown|MissionFlag.VsZerg|MissionFlag.RaceSwap + DOMINATION_P = 155, "Domination (Protoss)", SC2Campaign.HOTS, "Char", SC2Race.PROTOSS, MissionPools.EASY, "ap_domination", MissionFlag.Protoss|MissionFlag.Countdown|MissionFlag.VsZerg|MissionFlag.RaceSwap + FIRE_IN_THE_SKY_T = 156, "Fire in the Sky (Terran)", SC2Campaign.HOTS, "Char", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_fire_in_the_sky", MissionFlag.Terran|MissionFlag.Countdown|MissionFlag.VsTerran|MissionFlag.RaceSwap + FIRE_IN_THE_SKY_P = 157, "Fire in the Sky (Protoss)", SC2Campaign.HOTS, "Char", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_fire_in_the_sky", MissionFlag.Protoss|MissionFlag.Countdown|MissionFlag.VsTerran|MissionFlag.RaceSwap + OLD_SOLDIERS_T = 158, "Old Soldiers (Terran)", SC2Campaign.HOTS, "Char", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_old_soldiers", MissionFlag.Terran|MissionFlag.VsTerran|MissionFlag.RaceSwap + OLD_SOLDIERS_P = 159, "Old Soldiers (Protoss)", SC2Campaign.HOTS, "Char", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_old_soldiers", MissionFlag.Protoss|MissionFlag.VsTerran|MissionFlag.RaceSwap + WAKING_THE_ANCIENT_T = 160, "Waking the Ancient (Terran)", SC2Campaign.HOTS, "Zerus", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_waking_the_ancient", MissionFlag.Terran|MissionFlag.VsZerg|MissionFlag.RaceSwap + WAKING_THE_ANCIENT_P = 161, "Waking the Ancient (Protoss)", SC2Campaign.HOTS, "Zerus", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_waking_the_ancient", MissionFlag.Protoss|MissionFlag.VsZerg|MissionFlag.RaceSwap + THE_CRUCIBLE_T = 162, "The Crucible (Terran)", SC2Campaign.HOTS, "Zerus", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_the_crucible", MissionFlag.Terran|MissionFlag.TimedDefense|MissionFlag.VsZerg|MissionFlag.RaceSwap + THE_CRUCIBLE_P = 163, "The Crucible (Protoss)", SC2Campaign.HOTS, "Zerus", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_the_crucible", MissionFlag.Protoss|MissionFlag.TimedDefense|MissionFlag.VsZerg|MissionFlag.RaceSwap + # 164/165 - Supreme + INFESTED_T = 166, "Infested (Terran)", SC2Campaign.HOTS, "Skygeirr Station", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_infested", MissionFlag.Terran|MissionFlag.VsTerran|MissionFlag.RaceSwap + INFESTED_P = 167, "Infested (Protoss)", SC2Campaign.HOTS, "Skygeirr Station", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_infested", MissionFlag.Protoss|MissionFlag.VsTerran|MissionFlag.RaceSwap + HAND_OF_DARKNESS_T = 168, "Hand of Darkness (Terran)", SC2Campaign.HOTS, "Skygeirr Station", SC2Race.TERRAN, MissionPools.HARD, "ap_hand_of_darkness", MissionFlag.Terran|MissionFlag.Countdown|MissionFlag.VsTerran|MissionFlag.RaceSwap + HAND_OF_DARKNESS_P = 169, "Hand of Darkness (Protoss)", SC2Campaign.HOTS, "Skygeirr Station", SC2Race.PROTOSS, MissionPools.HARD, "ap_hand_of_darkness", MissionFlag.Protoss|MissionFlag.Countdown|MissionFlag.VsTerran|MissionFlag.RaceSwap + PHANTOMS_OF_THE_VOID_T = 170, "Phantoms of the Void (Terran)", SC2Campaign.HOTS, "Skygeirr Station", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_phantoms_of_the_void", MissionFlag.Terran|MissionFlag.Countdown|MissionFlag.VsProtoss|MissionFlag.RaceSwap + PHANTOMS_OF_THE_VOID_P = 171, "Phantoms of the Void (Protoss)", SC2Campaign.HOTS, "Skygeirr Station", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_phantoms_of_the_void", MissionFlag.Protoss|MissionFlag.Countdown|MissionFlag.VsProtoss|MissionFlag.RaceSwap + # 172/173 - With Friends Like These + # 174/175 - Conviction + PLANETFALL_T = 176, "Planetfall (Terran)", SC2Campaign.HOTS, "Korhal", SC2Race.TERRAN, MissionPools.HARD, "ap_planetfall", MissionFlag.Terran|MissionFlag.AutoScroller|MissionFlag.VsTerran|MissionFlag.RaceSwap + PLANETFALL_P = 177, "Planetfall (Protoss)", SC2Campaign.HOTS, "Korhal", SC2Race.PROTOSS, MissionPools.HARD, "ap_planetfall", MissionFlag.Protoss|MissionFlag.AutoScroller|MissionFlag.VsTerran|MissionFlag.RaceSwap + DEATH_FROM_ABOVE_T = 178, "Death From Above (Terran)", SC2Campaign.HOTS, "Korhal", SC2Race.TERRAN, MissionPools.HARD, "ap_death_from_above", MissionFlag.Terran|MissionFlag.VsTerran|MissionFlag.RaceSwap + DEATH_FROM_ABOVE_P = 179, "Death From Above (Protoss)", SC2Campaign.HOTS, "Korhal", SC2Race.PROTOSS, MissionPools.HARD, "ap_death_from_above", MissionFlag.Protoss|MissionFlag.VsTerran|MissionFlag.RaceSwap + THE_RECKONING_T = 180, "The Reckoning (Terran)", SC2Campaign.HOTS, "Korhal", SC2Race.TERRAN, MissionPools.VERY_HARD, "ap_the_reckoning", MissionFlag.Terran|MissionFlag.VsTerran|MissionFlag.AiTerranAlly|MissionFlag.RaceSwap + THE_RECKONING_P = 181, "The Reckoning (Protoss)", SC2Campaign.HOTS, "Korhal", SC2Race.PROTOSS, MissionPools.VERY_HARD, "ap_the_reckoning", MissionFlag.Protoss|MissionFlag.VsTerran|MissionFlag.AiTerranAlly|MissionFlag.RaceSwap + DARK_WHISPERS_T = 182, "Dark Whispers (Terran)", SC2Campaign.PROLOGUE, "_1", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_dark_whispers", MissionFlag.Terran|MissionFlag.Countdown|MissionFlag.VsTZ|MissionFlag.RaceSwap + DARK_WHISPERS_Z = 183, "Dark Whispers (Zerg)", SC2Campaign.PROLOGUE, "_1", SC2Race.ZERG, MissionPools.MEDIUM, "ap_dark_whispers", MissionFlag.Zerg|MissionFlag.Countdown|MissionFlag.VsTZ|MissionFlag.RaceSwap + GHOSTS_IN_THE_FOG_T = 184, "Ghosts in the Fog (Terran)", SC2Campaign.PROLOGUE, "_2", SC2Race.TERRAN, MissionPools.HARD, "ap_ghosts_in_the_fog", MissionFlag.Terran|MissionFlag.VsProtoss|MissionFlag.RaceSwap + GHOSTS_IN_THE_FOG_Z = 185, "Ghosts in the Fog (Zerg)", SC2Campaign.PROLOGUE, "_2", SC2Race.ZERG, MissionPools.HARD, "ap_ghosts_in_the_fog", MissionFlag.Zerg|MissionFlag.VsProtoss|MissionFlag.RaceSwap + # 186/187 - Evil Awoken + # 188/189 - For Aiur! + THE_GROWING_SHADOW_T = 190, "The Growing Shadow (Terran)", SC2Campaign.LOTV, "Aiur", SC2Race.TERRAN, MissionPools.EASY, "ap_the_growing_shadow", MissionFlag.Terran|MissionFlag.VsPZ|MissionFlag.RaceSwap + THE_GROWING_SHADOW_Z = 191, "The Growing Shadow (Zerg)", SC2Campaign.LOTV, "Aiur", SC2Race.ZERG, MissionPools.EASY, "ap_the_growing_shadow", MissionFlag.Zerg|MissionFlag.VsPZ|MissionFlag.RaceSwap + THE_SPEAR_OF_ADUN_T = 192, "The Spear of Adun (Terran)", SC2Campaign.LOTV, "Aiur", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_the_spear_of_adun", MissionFlag.Terran|MissionFlag.VsPZ|MissionFlag.RaceSwap + THE_SPEAR_OF_ADUN_Z = 193, "The Spear of Adun (Zerg)", SC2Campaign.LOTV, "Aiur", SC2Race.ZERG, MissionPools.MEDIUM, "ap_the_spear_of_adun", MissionFlag.Zerg|MissionFlag.VsPZ|MissionFlag.RaceSwap + SKY_SHIELD_T = 194, "Sky Shield (Terran)", SC2Campaign.LOTV, "Korhal", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_sky_shield", MissionFlag.Terran|MissionFlag.Countdown|MissionFlag.VsTerran|MissionFlag.AiTerranAlly|MissionFlag.RaceSwap + SKY_SHIELD_Z = 195, "Sky Shield (Zerg)", SC2Campaign.LOTV, "Korhal", SC2Race.ZERG, MissionPools.MEDIUM, "ap_sky_shield", MissionFlag.Zerg|MissionFlag.Countdown|MissionFlag.VsTerran|MissionFlag.AiTerranAlly|MissionFlag.RaceSwap + BROTHERS_IN_ARMS_T = 196, "Brothers in Arms (Terran)", SC2Campaign.LOTV, "Korhal", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_brothers_in_arms", MissionFlag.Terran|MissionFlag.VsTerran|MissionFlag.AiTerranAlly|MissionFlag.RaceSwap + BROTHERS_IN_ARMS_Z = 197, "Brothers in Arms (Zerg)", SC2Campaign.LOTV, "Korhal", SC2Race.ZERG, MissionPools.MEDIUM, "ap_brothers_in_arms", MissionFlag.Zerg|MissionFlag.VsTerran|MissionFlag.AiTerranAlly|MissionFlag.RaceSwap + AMON_S_REACH_T = 198, "Amon's Reach (Terran)", SC2Campaign.LOTV, "Shakuras", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_amon_s_reach", MissionFlag.Terran|MissionFlag.VsZerg|MissionFlag.RaceSwap + AMON_S_REACH_Z = 199, "Amon's Reach (Zerg)", SC2Campaign.LOTV, "Shakuras", SC2Race.ZERG, MissionPools.MEDIUM, "ap_amon_s_reach", MissionFlag.Zerg|MissionFlag.VsZerg|MissionFlag.RaceSwap + LAST_STAND_T = 200, "Last Stand (Terran)", SC2Campaign.LOTV, "Shakuras", SC2Race.TERRAN, MissionPools.HARD, "ap_last_stand", MissionFlag.Terran|MissionFlag.TimedDefense|MissionFlag.VsZerg|MissionFlag.RaceSwap + LAST_STAND_Z = 201, "Last Stand (Zerg)", SC2Campaign.LOTV, "Shakuras", SC2Race.ZERG, MissionPools.HARD, "ap_last_stand", MissionFlag.Zerg|MissionFlag.TimedDefense|MissionFlag.VsZerg|MissionFlag.RaceSwap + FORBIDDEN_WEAPON_T = 202, "Forbidden Weapon (Terran)", SC2Campaign.LOTV, "Purifier", SC2Race.TERRAN, MissionPools.HARD, "ap_forbidden_weapon", MissionFlag.Terran|MissionFlag.Countdown|MissionFlag.VsProtoss|MissionFlag.RaceSwap + FORBIDDEN_WEAPON_Z = 203, "Forbidden Weapon (Zerg)", SC2Campaign.LOTV, "Purifier", SC2Race.ZERG, MissionPools.HARD, "ap_forbidden_weapon", MissionFlag.Zerg|MissionFlag.Countdown|MissionFlag.VsProtoss|MissionFlag.RaceSwap + TEMPLE_OF_UNIFICATION_T = 204, "Temple of Unification (Terran)", SC2Campaign.LOTV, "Ulnar", SC2Race.TERRAN, MissionPools.HARD, "ap_temple_of_unification", MissionFlag.Terran|MissionFlag.VsTP|MissionFlag.RaceSwap + TEMPLE_OF_UNIFICATION_Z = 205, "Temple of Unification (Zerg)", SC2Campaign.LOTV, "Ulnar", SC2Race.ZERG, MissionPools.HARD, "ap_temple_of_unification", MissionFlag.Zerg|MissionFlag.VsTP|MissionFlag.RaceSwap + # 206/207 - The Infinite Cycle + HARBINGER_OF_OBLIVION_T = 208, "Harbinger of Oblivion (Terran)", SC2Campaign.LOTV, "Ulnar", SC2Race.TERRAN, MissionPools.HARD, "ap_harbinger_of_oblivion", MissionFlag.Terran|MissionFlag.Countdown|MissionFlag.VsTP|MissionFlag.AiZergAlly|MissionFlag.RaceSwap + HARBINGER_OF_OBLIVION_Z = 209, "Harbinger of Oblivion (Zerg)", SC2Campaign.LOTV, "Ulnar", SC2Race.ZERG, MissionPools.HARD, "ap_harbinger_of_oblivion", MissionFlag.Zerg|MissionFlag.Countdown|MissionFlag.VsTP|MissionFlag.AiZergAlly|MissionFlag.RaceSwap + UNSEALING_THE_PAST_T = 210, "Unsealing the Past (Terran)", SC2Campaign.LOTV, "Purifier", SC2Race.TERRAN, MissionPools.HARD, "ap_unsealing_the_past", MissionFlag.Terran|MissionFlag.AutoScroller|MissionFlag.VsZerg|MissionFlag.RaceSwap + UNSEALING_THE_PAST_Z = 211, "Unsealing the Past (Zerg)", SC2Campaign.LOTV, "Purifier", SC2Race.ZERG, MissionPools.HARD, "ap_unsealing_the_past", MissionFlag.Zerg|MissionFlag.AutoScroller|MissionFlag.VsZerg|MissionFlag.RaceSwap + PURIFICATION_T = 212, "Purification (Terran)", SC2Campaign.LOTV, "Purifier", SC2Race.TERRAN, MissionPools.HARD, "ap_purification", MissionFlag.Terran|MissionFlag.VsZerg|MissionFlag.RaceSwap + PURIFICATION_Z = 213, "Purification (Zerg)", SC2Campaign.LOTV, "Purifier", SC2Race.ZERG, MissionPools.HARD, "ap_purification", MissionFlag.Zerg|MissionFlag.VsZerg|MissionFlag.RaceSwap + STEPS_OF_THE_RITE_T = 214, "Steps of the Rite (Terran)", SC2Campaign.LOTV, "Tal'darim", SC2Race.TERRAN, MissionPools.HARD, "ap_steps_of_the_rite", MissionFlag.Terran|MissionFlag.VsProtoss|MissionFlag.RaceSwap + STEPS_OF_THE_RITE_Z = 215, "Steps of the Rite (Zerg)", SC2Campaign.LOTV, "Tal'darim", SC2Race.ZERG, MissionPools.HARD, "ap_steps_of_the_rite", MissionFlag.Zerg|MissionFlag.VsProtoss|MissionFlag.RaceSwap + RAK_SHIR_T = 216, "Rak'Shir (Terran)", SC2Campaign.LOTV, "Tal'darim", SC2Race.TERRAN, MissionPools.HARD, "ap_rak_shir", MissionFlag.Terran|MissionFlag.VsProtoss|MissionFlag.RaceSwap + RAK_SHIR_Z = 217, "Rak'Shir (Zerg)", SC2Campaign.LOTV, "Tal'darim", SC2Race.ZERG, MissionPools.HARD, "ap_rak_shir", MissionFlag.Zerg|MissionFlag.VsProtoss|MissionFlag.RaceSwap + TEMPLAR_S_CHARGE_T = 218, "Templar's Charge (Terran)", SC2Campaign.LOTV, "Moebius", SC2Race.TERRAN, MissionPools.HARD, "ap_templar_s_charge", MissionFlag.Terran|MissionFlag.VsTerran|MissionFlag.RaceSwap + TEMPLAR_S_CHARGE_Z = 219, "Templar's Charge (Zerg)", SC2Campaign.LOTV, "Moebius", SC2Race.ZERG, MissionPools.HARD, "ap_templar_s_charge", MissionFlag.Zerg|MissionFlag.VsTerran|MissionFlag.RaceSwap + # 220/221 - Templar's Return + THE_HOST_T = 222, "The Host (Terran)", SC2Campaign.LOTV, "Return to Aiur", SC2Race.TERRAN, MissionPools.VERY_HARD, "ap_the_host", MissionFlag.Terran|MissionFlag.VsAll|MissionFlag.RaceSwap + THE_HOST_Z = 223, "The Host (Zerg)", SC2Campaign.LOTV, "Return to Aiur", SC2Race.ZERG, MissionPools.VERY_HARD, "ap_the_host", MissionFlag.Zerg|MissionFlag.VsAll|MissionFlag.RaceSwap + SALVATION_T = 224, "Salvation (Terran)", SC2Campaign.LOTV, "Return to Aiur", SC2Race.TERRAN, MissionPools.VERY_HARD, "ap_salvation", MissionFlag.Terran|MissionFlag.TimedDefense|MissionFlag.VsPZ|MissionFlag.AiProtossAlly|MissionFlag.RaceSwap + SALVATION_Z = 225, "Salvation (Zerg)", SC2Campaign.LOTV, "Return to Aiur", SC2Race.ZERG, MissionPools.VERY_HARD, "ap_salvation", MissionFlag.Zerg|MissionFlag.TimedDefense|MissionFlag.VsPZ|MissionFlag.AiProtossAlly|MissionFlag.RaceSwap + # 226/227 - Into the Void + # 228/229 - The Essence of Eternity + # 230/231 - Amon's Fall + # 232/233 - The Escape + # 234/235 - Sudden Strike + # 236/237 - Enemy Intelligence + # 238/239 - Trouble In Paradise + # 240/241 - Night Terrors + # 242/243 - Flashpoint + # 244/245 - In the Enemy's Shadow + # 246/247 - Dark Skies + # 248/249 - 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 # How many blank padding tiles go above this mission in the launcher + + + +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 +} +for mission in SC2Mission: + if MissionFlag.HasRaceSwap in mission.flags and ' (' in mission.mission_name: + # Short names for non-race-swapped missions for client compatibility + short_name = mission.get_short_name() + lookup_name_to_mission[short_name] = mission + +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, Optional[SC2CampaignGoal]] = { + SC2Campaign.WOL: SC2CampaignGoal(SC2Mission.ALL_IN, f'{SC2Mission.ALL_IN.mission_name}: Victory'), + SC2Campaign.PROPHECY: SC2CampaignGoal(SC2Mission.IN_UTTER_DARKNESS, f'{SC2Mission.IN_UTTER_DARKNESS.mission_name}: Defeat'), + SC2Campaign.HOTS: SC2CampaignGoal(SC2Mission.THE_RECKONING, f'{SC2Mission.THE_RECKONING.mission_name}: Victory'), + SC2Campaign.PROLOGUE: SC2CampaignGoal(SC2Mission.EVIL_AWOKEN, f'{SC2Mission.EVIL_AWOKEN.mission_name}: Victory'), + SC2Campaign.LOTV: SC2CampaignGoal(SC2Mission.SALVATION, f'{SC2Mission.SALVATION.mission_name}: Victory'), + SC2Campaign.EPILOGUE: None, + SC2Campaign.NCO: SC2CampaignGoal(SC2Mission.END_GAME, f'{SC2Mission.END_GAME.mission_name}: Victory'), +} + +campaign_alt_final_mission_locations: Dict[SC2Campaign, Dict[SC2Mission, str]] = { + SC2Campaign.WOL: { + SC2Mission.MAW_OF_THE_VOID: f'{SC2Mission.MAW_OF_THE_VOID.mission_name}: Victory', + SC2Mission.ENGINE_OF_DESTRUCTION: f'{SC2Mission.ENGINE_OF_DESTRUCTION.mission_name}: Victory', + SC2Mission.SUPERNOVA: f'{SC2Mission.SUPERNOVA.mission_name}: Victory', + SC2Mission.GATES_OF_HELL: f'{SC2Mission.GATES_OF_HELL.mission_name}: Victory', + SC2Mission.SHATTER_THE_SKY: f'{SC2Mission.SHATTER_THE_SKY.mission_name}: Victory', + + SC2Mission.MAW_OF_THE_VOID_Z: f'{SC2Mission.MAW_OF_THE_VOID_Z.mission_name}: Victory', + SC2Mission.ENGINE_OF_DESTRUCTION_Z: f'{SC2Mission.ENGINE_OF_DESTRUCTION_Z.mission_name}: Victory', + SC2Mission.SUPERNOVA_Z: f'{SC2Mission.SUPERNOVA_Z.mission_name}: Victory', + SC2Mission.GATES_OF_HELL_Z: f'{SC2Mission.GATES_OF_HELL_Z.mission_name}: Victory', + SC2Mission.SHATTER_THE_SKY_Z: f'{SC2Mission.SHATTER_THE_SKY_Z.mission_name}: Victory', + + SC2Mission.MAW_OF_THE_VOID_P: f'{SC2Mission.MAW_OF_THE_VOID_P.mission_name}: Victory', + SC2Mission.ENGINE_OF_DESTRUCTION_P: f'{SC2Mission.ENGINE_OF_DESTRUCTION_P.mission_name}: Victory', + SC2Mission.SUPERNOVA_P: f'{SC2Mission.SUPERNOVA_P.mission_name}: Victory', + SC2Mission.GATES_OF_HELL_P: f'{SC2Mission.GATES_OF_HELL_P.mission_name}: Victory', + SC2Mission.SHATTER_THE_SKY_P: f'{SC2Mission.SHATTER_THE_SKY_P.mission_name}: Victory' + }, + SC2Campaign.PROPHECY: {}, + SC2Campaign.HOTS: { + SC2Mission.THE_CRUCIBLE: f'{SC2Mission.THE_CRUCIBLE.mission_name}: Victory', + SC2Mission.HAND_OF_DARKNESS: f'{SC2Mission.HAND_OF_DARKNESS.mission_name}: Victory', + SC2Mission.PHANTOMS_OF_THE_VOID: f'{SC2Mission.PHANTOMS_OF_THE_VOID.mission_name}: Victory', + SC2Mission.PLANETFALL: f'{SC2Mission.PLANETFALL.mission_name}: Victory', + SC2Mission.DEATH_FROM_ABOVE: f'{SC2Mission.DEATH_FROM_ABOVE.mission_name}: Victory', + + SC2Mission.THE_CRUCIBLE_T: f'{SC2Mission.THE_CRUCIBLE_T.mission_name}: Victory', + SC2Mission.HAND_OF_DARKNESS_T: f'{SC2Mission.HAND_OF_DARKNESS_T.mission_name}: Victory', + SC2Mission.PHANTOMS_OF_THE_VOID_T: f'{SC2Mission.PHANTOMS_OF_THE_VOID_T.mission_name}: Victory', + SC2Mission.PLANETFALL_T: f'{SC2Mission.PLANETFALL_T.mission_name}: Victory', + SC2Mission.DEATH_FROM_ABOVE_T: f'{SC2Mission.DEATH_FROM_ABOVE_T.mission_name}: Victory', + + SC2Mission.THE_CRUCIBLE_P: f'{SC2Mission.THE_CRUCIBLE_P.mission_name}: Victory', + SC2Mission.HAND_OF_DARKNESS_P: f'{SC2Mission.HAND_OF_DARKNESS_P.mission_name}: Victory', + SC2Mission.PHANTOMS_OF_THE_VOID_P: f'{SC2Mission.PHANTOMS_OF_THE_VOID_P.mission_name}: Victory', + SC2Mission.PLANETFALL_P: f'{SC2Mission.PLANETFALL_P.mission_name}: Victory', + SC2Mission.DEATH_FROM_ABOVE_P: f'{SC2Mission.DEATH_FROM_ABOVE_P.mission_name}: Victory' + }, + SC2Campaign.PROLOGUE: { + SC2Mission.GHOSTS_IN_THE_FOG: f'{SC2Mission.GHOSTS_IN_THE_FOG.mission_name}: Victory', + SC2Mission.GHOSTS_IN_THE_FOG_T: f'{SC2Mission.GHOSTS_IN_THE_FOG_T.mission_name}: Victory', + SC2Mission.GHOSTS_IN_THE_FOG_Z: f'{SC2Mission.GHOSTS_IN_THE_FOG_Z.mission_name}: Victory' + }, + SC2Campaign.LOTV: { + SC2Mission.THE_HOST: f'{SC2Mission.THE_HOST.mission_name}: Victory', + SC2Mission.TEMPLAR_S_CHARGE: f'{SC2Mission.TEMPLAR_S_CHARGE.mission_name}: Victory', + + SC2Mission.THE_HOST_T: f'{SC2Mission.THE_HOST_T.mission_name}: Victory', + SC2Mission.TEMPLAR_S_CHARGE_T: f'{SC2Mission.TEMPLAR_S_CHARGE_T.mission_name}: Victory', + + SC2Mission.THE_HOST_Z: f'{SC2Mission.THE_HOST_Z.mission_name}: Victory', + SC2Mission.TEMPLAR_S_CHARGE_Z: f'{SC2Mission.TEMPLAR_S_CHARGE_Z.mission_name}: Victory' + }, + SC2Campaign.EPILOGUE: { + SC2Mission.AMON_S_FALL: f'{SC2Mission.AMON_S_FALL.mission_name}: Victory', + SC2Mission.INTO_THE_VOID: f'{SC2Mission.INTO_THE_VOID.mission_name}: Victory', + SC2Mission.THE_ESSENCE_OF_ETERNITY: f'{SC2Mission.THE_ESSENCE_OF_ETERNITY.mission_name}: Victory', + }, + SC2Campaign.NCO: { + SC2Mission.FLASHPOINT: f'{SC2Mission.FLASHPOINT.mission_name}: Victory', + SC2Mission.DARK_SKIES: f'{SC2Mission.DARK_SKIES.mission_name}: Victory', + SC2Mission.NIGHT_TERRORS: f'{SC2Mission.NIGHT_TERRORS.mission_name}: Victory', + SC2Mission.TROUBLE_IN_PARADISE: f'{SC2Mission.TROUBLE_IN_PARADISE.mission_name}: 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 mission in campaign_alt_goals: + return campaign_alt_goals.get(mission) + + return (mission.mission_name + ": Defeat") \ + if mission in [SC2Mission.IN_UTTER_DARKNESS, SC2Mission.IN_UTTER_DARKNESS_T, SC2Mission.IN_UTTER_DARKNESS_Z] \ + else mission.mission_name + ": Victory" + + +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: + for mission in alt_goal_locations.keys(): + missions.append(mission) + + return missions + + +def get_missions_with_any_flags_in_list(flags: MissionFlag) -> List[SC2Mission]: + return [mission for mission in SC2Mission if flags & mission.flags] diff --git a/worlds/sc2/options.py b/worlds/sc2/options.py new file mode 100644 index 00000000..08be7e18 --- /dev/null +++ b/worlds/sc2/options.py @@ -0,0 +1,1746 @@ +import functools +from dataclasses import fields, Field, dataclass +from typing import * +from datetime import timedelta + +from Options import ( + Choice, Toggle, DefaultOnToggle, OptionSet, Range, + PerGameCommonOptions, Option, VerifyKeys, StartInventory, + is_iterable_except_str, OptionGroup, Visibility, ItemDict +) +from Utils import get_fuzzy_results +from BaseClasses import PlandoOptions +from .item import item_names, item_tables +from .item.item_groups import kerrigan_active_abilities, kerrigan_passives, nova_weapons, nova_gadgets +from .mission_tables import ( + SC2Campaign, SC2Mission, lookup_name_to_mission, MissionPools, get_missions_with_any_flags_in_list, + campaign_mission_table, SC2Race, MissionFlag +) +from .mission_groups import mission_groups, MissionGroupNames +from .mission_order.options import CustomMissionOrder + +if TYPE_CHECKING: + from worlds.AutoWorld import World + from . import SC2World + + +class Sc2MissionSet(OptionSet): + """Option set made for handling missions and expanding mission groups""" + valid_keys: Iterable[str] = [x.mission_name for x in SC2Mission] + + @classmethod + def from_any(cls, data: Any): + if is_iterable_except_str(data): + return cls(data) + return cls.from_text(str(data)) + + def verify(self, world: Type['World'], player_name: str, plando_options: PlandoOptions) -> None: + """Overridden version of function from Options.VerifyKeys for a better error message""" + new_value: set[str] = set() + case_insensitive_group_mapping = { + group_name.casefold(): group_value for group_name, group_value in mission_groups.items() + } + case_insensitive_group_mapping.update({mission.mission_name.casefold(): [mission.mission_name] for mission in SC2Mission}) + for group_name in self.value: + item_names = case_insensitive_group_mapping.get(group_name.casefold(), {group_name}) + new_value.update(item_names) + self.value = new_value + for item_name in self.value: + if item_name not in self.valid_keys: + picks = get_fuzzy_results( + item_name, + list(self.valid_keys) + list(MissionGroupNames.get_all_group_names()), + limit=1, + ) + raise Exception(f"Mission {item_name} from option {self} " + f"is not a valid mission name from {world.game}. " + f"Did you mean '{picks[0][0]}' ({picks[0][1]}% sure)") + + def __iter__(self) -> Iterator[str]: + return self.value.__iter__() + + def __len__(self) -> int: + return self.value.__len__() + + +class SelectRaces(OptionSet): + """ + Pick which factions' missions and items can be shuffled into the world. + """ + display_name = "Select Playable Races" + valid_keys = {race.get_title() for race in SC2Race if race != SC2Race.ANY} + default = valid_keys + + +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 DifficultyDamageModifier(DefaultOnToggle): + """ + Enables or disables vanilla difficulty-based damage received modifier + Handles the 1.25 Brutal damage modifier in HotS and Prologue and 0.5 Casual damage modifier outside WoL and Prophecy + """ + display_name = "Difficulty Damage Modifier" + + +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(DefaultOnToggle): + """ + 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 + default = 'random' + + +class MissionOrder(Choice): + """ + Determines the order the missions are played in. The first three mission orders ignore the Maximum Campaign Size option. + 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. + Blitz: Missions are divided into sets. Complete one mission from a set to advance to the next set. + Gauntlet: A linear path of missions to complete the campaign. + Grid: Missions are arranged into a grid. Completing a mission unlocks the adjacent missions. Corners may be omitted to make the grid more square. Complete the bottom-right mission to win. + Golden Path: A required line of missions with several optional branches, similar to the Wings of Liberty campaign. + Hopscotch: Missions alternate between mandatory missions and pairs of optional missions. + Custom: Uses the YAML's custom mission order option. See documentation for usage. + """ + display_name = "Mission Order" + option_vanilla = 0 + option_vanilla_shuffled = 1 + option_mini_campaign = 2 + option_blitz = 5 + option_gauntlet = 6 + option_grid = 9 + option_golden_path = 10 + option_hopscotch = 11 + option_custom = 99 + + +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 = len(SC2Mission) + default = 83 + + +class TwoStartPositions(Toggle): + """ + If turned on and 'grid', 'hopscotch', or 'golden_path' mission orders are selected, + removes the first mission and allows both of the next two missions to be played from the start. + """ + display_name = "Start with two unlocked missions on grid" + default = Toggle.option_false + + +class KeyMode(Choice): + """ + Optionally creates Key items that must be found in the multiworld to unlock parts of the mission order, + in addition to any regular requirements a mission may have. + + "Questline" options will only work for Vanilla, Vanilla Shuffled, Mini Campaign, and Golden Path mission orders. + + Disabled: Don't create any keys. + Questlines: Create keys for questlines besides the starter ones, eg. "Colonist (Wings of Liberty) Questline Key". + Missions: Create keys for missions besides the starter ones, eg. "Zero Hour Mission Key". + Progressive Questlines: Create one type of progressive key for questlines within each campaign, eg. "Progressive Key #1". + Progressive Missions: Create one type of progressive key for all missions, "Progressive Mission Key". + Progressive Per Questline: All questlines besides the starter ones get a unique progressive key for their missions, eg. "Progressive Key #1". + """ + display_name = "Key Mode" + option_disabled = 0 + option_questlines = 1 + option_missions = 2 + option_progressive_questlines = 3 + option_progressive_missions = 4 + option_progressive_per_questline = 5 + default = option_disabled + + +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_mengsk = 17 + option_bright_lime = 18 + option_arcane = 19 + option_ember = 20 + option_hot_pink = 21 + option_default = 22 + default = option_default + + +class PlayerColorTerranRaynor(ColorChoice): + """Determines in-game player team color in Wings of Liberty missions.""" + display_name = "Terran Player Color (Raynor)" + + +class PlayerColorProtoss(ColorChoice): + """Determines in-game player team color in Legacy of the Void missions.""" + display_name = "Protoss Player Color" + + +class PlayerColorZerg(ColorChoice): + """Determines in-game player team color in Heart of the Swarm missions before unlocking Primal Kerrigan.""" + display_name = "Zerg Player Color" + + +class PlayerColorZergPrimal(ColorChoice): + """Determines in-game player team color in Heart of the Swarm after unlocking Primal Kerrigan.""" + display_name = "Zerg Player Color (Primal)" + + +class PlayerColorNova(ColorChoice): + """Determines in-game player team color in Nova Covert Ops missions.""" + display_name = "Terran Player Color (Nova)" + + +class EnabledCampaigns(OptionSet): + """Determines which campaign's missions will be used""" + display_name = "Enabled Campaigns" + valid_keys = {campaign.campaign_name for campaign in SC2Campaign if campaign != SC2Campaign.GLOBAL} + default = valid_keys + + +class EnableRaceSwapVariants(Choice): + """ + Allow mission variants where you play a faction other than the one the map was initially + designed for. NOTE: Cutscenes are always skipped on race-swapped mission variants. + + Disabled: Don't shuffle any non-vanilla map variants into the pool. + Pick One: Shuffle up to 1 valid version of each map into the pool, depending on other settings. + Pick One Non-Vanilla: Shuffle up to 1 valid version other than the original one of each map into the pool, depending on other settings. + Shuffle All: Each version of a map can appear in the same pool (so a map can appear up to 3 times as different races) + Shuffle All Non-Vanilla: Each version of a map besides the original can appear in the same pool (so a map can appear up to 2 times as different races) + """ + display_name = "Enable Race-Swapped Mission Variants" + option_disabled = 0 + option_pick_one = 1 + option_pick_one_non_vanilla = 2 + option_shuffle_all = 3 + option_shuffle_all_non_vanilla = 4 + default = option_disabled + + +class EnableMissionRaceBalancing(Choice): + """ + If enabled, picks missions in such a way that the appearance rate of races is roughly equal. + The final rates may deviate if there are not enough missions enabled to accommodate each race. + + Disabled: Pick missions at random. + Semi Balanced: Use a weighting system to pick missions in a random, but roughly equal ratio. + Fully Balanced: Pick missions to preserve equal race counts whenever possible. + """ + display_name = "Enable Mission Race Balancing" + option_disabled = 0 + option_semi_balanced = 1 + option_fully_balanced = 2 + default = option_semi_balanced + + +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. + Any Units: Logic guarantees faction-appropriate units appear early without regard to what those units are. + i.e. if the third mission is a protoss build mission, + logic guarantees at least 2 protoss units are reachable before starting it. + May render the run impossible on harder difficulties. + 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_any_units = 2 + option_no_logic = 3 + + +class EnableVoidTrade(Toggle): + """ + Enables the Void Trade Wormhole to be built from the Advanced Construction tab of SCVs, Drones and Probes. + This structure allows sending units to the Archipelago server, as well as buying random units from the server. + + Note: Always disabled if there is no other Starcraft II world with Void Trade enabled in the multiworld. You cannot receive units that you send. + """ + display_name = "Enable Void Trade" + + +class VoidTradeAgeLimit(Choice): + """ + Determines the maximum allowed age for units you can receive from Void Trade. + Units that are older than your choice will still be available to other players, but not to you. + + This does not put a time limit on units you send to other players. Your own units are only affected by other players' choices for this option. + """ + display_name = "Void Trade Age Limit" + option_disabled = 0 + option_1_week = 1 + option_1_day = 2 + option_4_hours = 3 + option_2_hours = 4 + option_1_hour = 5 + option_30_minutes = 6 + option_5_minutes = 7 + default = option_30_minutes + + +class VoidTradeWorkers(Toggle): + """ + If enabled, you are able to send and receive workers via Void Trade. + + Sending workers is a cheap way to get a lot of units from other players, + at the cost of reducing the strength of received units for other players. + + Receiving workers allows you to build units of other races, but potentially skips large parts of your multiworld progression. + """ + display_name = "Allow Workers in Void Trade" + + +class MaxUpgradeLevel(Range): + """Controls the maximum number of weapon/armor upgrades that can be found or unlocked.""" + display_name = "Maximum Upgrade Level" + range_start = 3 + range_end = 5 + default = 3 + + +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. + + If the mission order is unable to be beaten by this value (if above 0), the generator will place additional + weapon / armor upgrades into start inventory + """ + display_name = "Generic Upgrade Missions" + range_start = 0 + range_end = 100 # Higher values lead to fails often + 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 GenericUpgradeResearchSpeedup(Toggle): + """ + If turned on, the weapon and armor upgrades are researched more quickly if level 4 or higher is unlocked. + The research times of upgrades are cut proportionally, so you're able to hit the maximum available level + at the same time, as you'd hit level 3 normally. + + Turning this on will help you to be able to research level 4 or 5 upgrade levels in timed missions. + + Has no effect if Maximum Upgrade Level is set to 3 + or Generic Upgrade Research doesn't require you to research upgrades in build missions. + """ + display_name = "Generic Upgrade Research Speedup" + + +class GenericUpgradeItems(Choice): + """Determines how weapon and armor upgrades are split into items. + + All options produce a number of levels of each item equal to the Maximum Upgrade Level. + The examples below consider a Maximum Upgrade Level of 3. + + 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 VanillaItemsOnly(Toggle): + """If turned on, the item pool is limited only to items that appear in the main 3 vanilla campaigns. + Weapon/Armor upgrades are unaffected; use max_upgrade_level to control maximum level. + Locked Items may override these exclusions.""" + display_name = "Vanilla Items Only" + + +class ExcludeOverpoweredItems(Toggle): + """ + If turned on, a curated list of very strong items are excluded. + These items were selected for promoting repetitive strategies, or for providing a lot of power in a boring way. + Recommended off for players looking for a challenge or for repeat playthroughs. + Excluding an OP item overrides the exclusion from this item rather than add to it. + OP items may be unexcluded or locked with Unexcluded Items or Locked Items options. + Enabling this can force a unit nerf even if Allow Unit Nerfs is set to false for some units. + """ + display_name = "Exclude Overpowered Items" + + +# Current maximum number of upgrades for a unit +MAX_UPGRADES_OPTION = 13 + + +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 upgrade items a unit/structure can have. + Note that most units have 4 to 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 upgrade items a unit/structure can have. + -1 is used to define unlimited. + Note that most units 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 MercenaryHighlanders(DefaultOnToggle): + """ + If enabled, it limits the controllable amount of certain mercenaries to 1, even if you have unlimited mercenaries upgrade. + With this upgrade you can still call the mercenary again if it dies. + + Affected mercenaries: Jackson's Revenge (Battlecruiser), Wise Old Torrasque (Ultralisk) + """ + display_name = "Mercenary Highlanders" + + +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 + + 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 + + +class KerriganLevelsPerMissionCompleted(Range): + """ + Determines how many levels Kerrigan gains when a mission is beaten. + """ + 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 KerriganMaxActiveAbilities(Range): + """ + Determines the maximum number of Kerrigan active abilities that can be present in the game + Additional abilities may spawn if those are required to beat the game. + """ + display_name = "Kerrigan Maximum Active Abilities" + range_start = 0 + range_end = len(kerrigan_active_abilities) + default = range_end + + +class KerriganMaxPassiveAbilities(Range): + """ + Determines the maximum number of Kerrigan passive abilities that can be present in the game + Additional abilities may spawn if those are required to beat the game. + """ + display_name = "Kerrigan Maximum Passive Abilities" + range_start = 0 + range_end = len(kerrigan_passives) + default = range_end + + +class EnableMorphling(Toggle): + """ + Determines whether the player can build Morphlings, which allow for inefficient morphing of advanced units + like Ravagers and Lurkers without requiring the base unit to be unlocked first. + """ + display_name = "Enable Morphling" + + +class WarCouncilNerfs(Toggle): + """ + Controls whether most Protoss units can initially be found in a nerfed state, with upgrades restoring their stronger power level. + For example, nerfed Zealots will lack the whirlwind upgrade until it is found as an item. + """ + display_name = "Allow Unit Nerfs" + + +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. + Vanilla: Spear of Adun calldowns are only available where they appear in the basegame (Protoss missions after The Growing Shadow) + Protoss: Spear of Adun calldowns are available in any Protoss mission + Everywhere: Spear of Adun calldowns are available in any mission of any race + Any Race LotV: Spear of Adun calldowns are available in any race-swapped variant of a LotV mission + """ + display_name = "Spear of Adun Presence" + option_not_present = 0 + option_vanilla = 4 + option_protoss = 2 + option_everywhere = 3 + option_any_race_lotv = 1 + default = option_vanilla + + # Fix case + @classmethod + def get_option_name(cls, value: int) -> str: + if value == SpearOfAdunPresence.option_any_race_lotv: + return "Any Race LotV" + 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 SpearOfAdunPassiveAbilityPresence(Choice): + """ + Determines availability of Spear of Adun passive powers. + Affects abilities like Reconstruction Beam or Overwatch. + Does not affect building abilities like Orbital Assimilators or Warp Harmonization. + + Not Present: Autocasts are not available. + Vanilla: Spear of Adun calldowns are only available where it appears in the basegame (Protoss missions after The Growing Shadow) + Protoss: Spear of Adun autocasts are available in any Protoss mission + Everywhere: Spear of Adun autocasts are available in any mission of any race + Any Race LotV: Spear of Adun autocasts are available in any race-swapped variant of a LotV mission + """ + display_name = "Spear of Adun Passive Ability Presence" + option_not_present = 0 + option_any_race_lotv = 1 + option_protoss = 2 + option_everywhere = 3 + option_vanilla = 4 + default = option_vanilla + + # Fix case + @classmethod + def get_option_name(cls, value: int) -> str: + if value == SpearOfAdunPresence.option_any_race_lotv: + return "Any Race LotV" + else: + return super().get_option_name(value) + + +class SpearOfAdunPassivesPresentInNoBuild(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 Passive Ability Presence". + If turned off, Spear of Adun autocasts are unavailable in all no-build missions + """ + display_name = "Spear of Adun Passive Abilities Present in No-Build" + + +class SpearOfAdunMaxActiveAbilities(Range): + """ + Determines the maximum number of Spear of Adun active abilities (top bar) that can be present in the game + Additional abilities may spawn if those are required to beat the game. + + Note: Warp in Reinforcements is treated as a second level of Warp in Pylon + """ + display_name = "Spear of Adun Maximum Active Abilities" + range_start = 0 + range_end = sum([item.quantity for item_name, item in item_tables.get_full_item_list().items() if item_name in item_tables.spear_of_adun_calldowns]) + default = range_end + + +class SpearOfAdunMaxAutocastAbilities(Range): + """ + Determines the maximum number of Spear of Adun passive abilities that can be present in the game + Additional abilities may spawn if those are required to beat the game. + Does not affect building abilities like Orbital Assimilators or Warp Harmonization. + """ + display_name = "Spear of Adun Maximum Passive Abilities" + range_start = 0 + range_end = sum(item.quantity for item_name, item in item_tables.get_full_item_list().items() if item_name in item_tables.spear_of_adun_castable_passives) + default = range_end + + +class GrantStoryTech(Choice): + """ + Controls handling of no-build missions that may require very specific items, such as Kerrigan or Nova abilities. + + no_grant: don't grant anything special; the player must find items to play the missions + grant: grant a minimal inventory that will allow the player to beat the mission, in addition to other items found + allow_substitutes: Reworks the most constrained mission - Supreme - to allow other items to substitute for Leaping Strike and Mend + + Locked to "grant" if Required Tactics is set to no logic. + """ + display_name = "Grant Story Tech" + option_no_grant = 0 + option_grant = 1 + option_allow_substitutes = 2 + + +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 NovaMaxWeapons(Range): + """ + Determines maximum number of Nova weapons that can be present in the game + Additional weapons may spawn if those are required to beat the game. + + Note: Nova can swap between unlocked weapons anytime during the gameplay. + """ + display_name = "Nova Maximum Weapons" + range_start = 0 + range_end = len(nova_weapons) + default = range_end + + +class NovaMaxGadgets(Range): + """ + Determines maximum number of Nova gadgets that can be present in the game. + Gadgets are a vanilla category including 2 grenade abilities, Stim, Holo Decoy, and Ionic Force Field. + Additional gadgets may spawn if those are required to beat the game. + + Note: Nova can use any unlocked ability anytime during gameplay. + """ + display_name = "Nova Maximum Gadgets" + range_start = 0 + range_end = len(nova_gadgets) + default = range_end + + +class NovaGhostOfAChanceVariant(Choice): + """ + Determines which variant of Nova should be used in Ghost of a Chance mission. + + WoL: Uses Nova from Wings of Liberty campaign (vanilla) + NCO: Uses Nova from Nova Covert Ops campaign + Auto: Uses NCO if a mission from Nova Covert Ops is actually shuffled, if not uses WoL + """ + display_name = "Nova Ghost of Chance Variant" + option_wol = 0 + option_nco = 1 + option_auto = 2 + default = option_wol + + # Fix case + @classmethod + def get_option_name(cls, value: int) -> str: + if value == NovaGhostOfAChanceVariant.option_wol: + return "WoL" + elif value == NovaGhostOfAChanceVariant.option_nco: + return "NCO" + return super().get_option_name(value) + + +class TakeOverAIAllies(Toggle): + """ + On maps supporting this feature allows you to take control over an AI Ally. + """ + display_name = "Take Over AI Allies" + + +class Sc2ItemDict(Option[Dict[str, int]], VerifyKeys, Mapping[str, int]): + """A branch of ItemDict that supports item counts of 0""" + default = {} + supports_weighting = False + verify_item_name = True + # convert_name_groups = True + display_name = 'Unnamed dictionary' + minimum_value: int = 0 + + def __init__(self, value: Dict[str, int]): + self.value = {key: val for key, val in value.items()} + + @classmethod + def from_any(cls, data: Union[List[str], Dict[str, int]]) -> 'Sc2ItemDict': + if isinstance(data, list): + # This is a little default that gets us backwards compatibility with lists. + # It doesn't play nice with trigger merging dicts and lists together, though, so best not to advertise it overmuch. + data = {item: 0 for item in data} + if isinstance(data, dict): + for key, value in data.items(): + if not isinstance(value, int): + raise ValueError(f"Invalid type in '{cls.display_name}': element '{key}' maps to '{value}', expected an integer") + if value < cls.minimum_value: + raise ValueError(f"Invalid value for '{cls.display_name}': element '{key}' maps to {value}, which is less than the minimum ({cls.minimum_value})") + return cls(data) + else: + raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}") + + def verify(self, world: Type['World'], player_name: str, plando_options: PlandoOptions) -> None: + """Overridden version of function from Options.VerifyKeys for a better error message""" + new_value: dict[str, int] = {} + case_insensitive_group_mapping = { + group_name.casefold(): group_value for group_name, group_value in world.item_name_groups.items() + } + case_insensitive_group_mapping.update({item.casefold(): {item} for item in world.item_names}) + for group_name in self.value: + item_names = case_insensitive_group_mapping.get(group_name.casefold(), {group_name}) + for item_name in item_names: + new_value[item_name] = new_value.get(item_name, 0) + self.value[group_name] + self.value = new_value + for item_name in self.value: + if item_name not in world.item_names: + from .item import item_groups + picks = get_fuzzy_results( + item_name, + list(world.item_names) + list(item_groups.ItemGroupNames.get_all_group_names()), + limit=1, + ) + raise Exception(f"Item {item_name} from option {self} " + f"is not a valid item name from {world.game}. " + f"Did you mean '{picks[0][0]}' ({picks[0][1]}% sure)") + + def get_option_name(self, value): + return ", ".join(f"{key}: {v}" for key, v in value.items()) + + def __getitem__(self, item: str) -> int: + return self.value.__getitem__(item) + + def __iter__(self) -> Iterator[str]: + return self.value.__iter__() + + def __len__(self) -> int: + return self.value.__len__() + + +class Sc2StartInventory(Sc2ItemDict): + """Start with these items.""" + display_name = StartInventory.display_name + + +class LockedItems(Sc2ItemDict): + """Guarantees that these items will be unlockable, in the amount specified. + Specify an amount of 0 to lock all copies of an item.""" + display_name = "Locked Items" + + +class ExcludedItems(Sc2ItemDict): + """Guarantees that these items will not be unlockable, in the amount specified. + Specify an amount of 0 to exclude all copies of an item.""" + display_name = "Excluded Items" + + +class UnexcludedItems(Sc2ItemDict): + """Undoes an item exclusion; useful for whitelisting or fine-tuning a category. + Specify an amount of 0 to unexclude all copies of an item.""" + display_name = "Unexcluded Items" + + +class ExcludedMissions(Sc2MissionSet): + """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 DifficultyCurve(Choice): + """ + Determines whether campaign missions will be placed with a smooth difficulty curve. + Standard: The campaign will start with easy missions and end with challenging missions. Short campaigns will be more difficult. + Uneven: The campaign will start with easy missions, but easy missions can still appear later in the campaign. Short campaigns will be easier. + """ + display_name = "Difficulty Curve" + option_standard = 0 + option_uneven = 1 + + +class ExcludeVeryHardMissions(Choice): + """ + Excludes Very Hard missions outside of Epilogue campaign (All-In, The Reckoning, 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, Enabled Campaigns, 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 VictoryCache(Range): + """ + Controls how many additional checks are awarded for completing a mission. + Goal missions are unaffected by this option. + """ + display_name = "Victory Checks" + range_start = 0 + range_end = 10 + default = 0 + + +class LocationInclusion(Choice): + option_enabled = 0 + option_half_chance = 3 + option_filler = 1 + option_disabled = 2 + + +class VanillaLocations(LocationInclusion): + """ + Enables or disables checks 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: Locations of this type give normal rewards. + Half Chance: Locations of this type have a 50% chance of being excluded. + Filler: Forces these locations to contain filler items. + 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 checks 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: Locations of this type give normal rewards. + Half Chance: Locations of this type have a 50% chance of being excluded. + Filler: Forces these locations to contain filler items. + 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 checks 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: Locations of this type give normal rewards. + Half Chance: Locations of this type have a 50% chance of being excluded. + Filler: Forces these locations to contain filler items. + 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 checks 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: Locations of this type give normal rewards. + Half Chance: Locations of this type have a 50% chance of being excluded. + Filler: Forces these locations to contain filler items. + 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 BasebustLocations(LocationInclusion): + """ + Enables or disables checks for killing non-objective bases. + These challenges are about destroying enemy bases that you normally don't have to fight to win a mission. + Enable these locations if you like sieges or being rewarded for achieving alternate win conditions. + + Enabled: Locations of this type give normal rewards. + Half Chance: Locations of this type have a 50% chance of being excluded. + *Note setting this for both challenge and basebust will have a 25% of a challenge-basebust location spawning. + Filler: Forces these locations to contain filler items. + 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 = "Base-Bust Locations" + + +class SpeedrunLocations(LocationInclusion): + """ + Enables or disables checks for overcoming speedrun challenges. + These challenges are often based on speed achievements or community challenges. + Enable these locations if you want to be rewarded for going fast. + + Enabled: Locations of this type give normal rewards. + Half Chance: Locations of this type have a 50% chance of being excluded. + *Note setting this for both challenge and speedrun will have a 25% of a challenge-speedrun location spawning. + Filler: Forces these locations to contain filler items. + 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 = "Speedrun Locations" + + +class PreventativeLocations(LocationInclusion): + """ + Enables or disables checks for overcoming preventative challenges. + These challenges are about winning or achieving something while preventing something else from happening, + such as beating Evacuation without losing a colonist. + Enable these locations if you want to be rewarded for achieving a higher standard on some locations. + + Enabled: Locations of this type give normal rewards. + Half Chance: Locations of this type have a 50% chance of being excluded. + *Note setting this for both challenge and preventative will have a 25% of a challenge-preventative location spawning. + Filler: Forces these locations to contain filler items. + 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 = "Preventative Locations" + + +class MissionOrderScouting(Choice): + """ + Allow the Sc2 mission order client tabs to indicate the type of item (i.e., progression, useful, etc.) available at each location of a mission. + The option defines when this information will be available for the player. + By default, this option is deactivated. + + None: Never provide information + Completed: Only for missions that were completed + Available: Only for missions that are available to play + Layout: Only for missions that are in an accessible layout (e.g. Char, Mar Sara, etc.) + Campaign: Only for missions that are in an accessible campaign (e.g. WoL, HotS, etc.) + All: All missions + """ + display_name = "Mission Order Scouting" + option_none = 0 + option_completed = 1 + option_available = 2 + option_layout = 3 + option_campaign = 4 + option_all = 5 + + default = option_none + + +class FillerPercentage(Range): + """ + Percentage of the item pool filled with filler items. + If the world has more locations than items, additional filler items may be generated. + """ + display_name = "Filler Percentage" + range_start = 0 + range_end = 70 + default = 0 + + +class MineralsPerItem(Range): + """ + Configures how many minerals are given per resource item. + """ + display_name = "Minerals Per Item" + range_start = 0 + range_end = 200 + 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 = 200 + 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 = 16 + default = 2 + + +class MaximumSupplyPerItem(Range): + """ + Configures how much the maximum supply limit increases per item. + """ + display_name = "Maximum Supply Per Item" + range_start = 0 + range_end = 10 + default = 1 + + +class MaximumSupplyReductionPerItem(Range): + """ + Configures how much maximum supply is reduced per trap item. + """ + display_name = "Maximum Supply Reduction Per Item" + range_start = 1 + range_end = 10 + default = 1 + + +class LowestMaximumSupply(Range): + """Controls how far max supply reduction traps can reduce maximum supply.""" + display_name = "Lowest Maximum Supply" + range_start = 100 + range_end = 200 + default = 180 + +class ResearchCostReductionPerItem(Range): + """ + Controls how much weapon/armor research cost is cut per research cost filler item. + Affects both minerals and vespene. + """ + display_name = "Upgrade Cost Discount Per Item" + range_start = 0 + range_end = 10 + default = 2 + + +class FillerItemsDistribution(ItemDict): + """ + Controls the relative probability of each filler item being generated over others. + Items that are bound to specific race or option are automatically eliminated. + Kerrigan levels generated this way don't go against Kerrigan level item sum + """ + default = { + item_names.STARTING_MINERALS: 1, + item_names.STARTING_VESPENE: 1, + item_names.STARTING_SUPPLY: 1, + item_names.MAX_SUPPLY: 1, + item_names.SHIELD_REGENERATION: 1, + item_names.BUILDING_CONSTRUCTION_SPEED: 1, + item_names.KERRIGAN_LEVELS_1: 0, + item_names.UPGRADE_RESEARCH_SPEED: 1, + item_names.UPGRADE_RESEARCH_COST: 1, + item_names.REDUCED_MAX_SUPPLY: 0, + } + valid_keys = default.keys() + display_name = "Filler Items Distribution" + + def __init__(self, value: Dict[str, int]): + # Allow zeros that the parent class doesn't allow + if any(item_count < 0 for item_count in value.values()): + raise Exception("Cannot have negative item weight.") + super(ItemDict, self).__init__(value) + + +@dataclass +class Starcraft2Options(PerGameCommonOptions): + start_inventory: Sc2StartInventory # type: ignore + game_difficulty: GameDifficulty + difficulty_damage_modifier: DifficultyDamageModifier + game_speed: GameSpeed + disable_forced_camera: DisableForcedCamera + skip_cutscenes: SkipCutscenes + all_in_map: AllInMap + mission_order: MissionOrder + maximum_campaign_size: MaximumCampaignSize + two_start_positions: TwoStartPositions + key_mode: KeyMode + player_color_terran_raynor: PlayerColorTerranRaynor + player_color_protoss: PlayerColorProtoss + player_color_zerg: PlayerColorZerg + player_color_zerg_primal: PlayerColorZergPrimal + player_color_nova: PlayerColorNova + selected_races: SelectRaces + enabled_campaigns: EnabledCampaigns + enable_race_swap: EnableRaceSwapVariants + mission_race_balancing: EnableMissionRaceBalancing + shuffle_campaigns: ShuffleCampaigns + shuffle_no_build: ShuffleNoBuild + starter_unit: StarterUnit + required_tactics: RequiredTactics + enable_void_trade: EnableVoidTrade + void_trade_age_limit: VoidTradeAgeLimit + void_trade_workers: VoidTradeWorkers + ensure_generic_items: EnsureGenericItems + min_number_of_upgrades: MinNumberOfUpgrades + max_number_of_upgrades: MaxNumberOfUpgrades + mercenary_highlanders: MercenaryHighlanders + max_upgrade_level: MaxUpgradeLevel + generic_upgrade_missions: GenericUpgradeMissions + generic_upgrade_research: GenericUpgradeResearch + generic_upgrade_research_speedup: GenericUpgradeResearchSpeedup + 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 + kerrigan_max_active_abilities: KerriganMaxActiveAbilities + kerrigan_max_passive_abilities: KerriganMaxPassiveAbilities + enable_morphling: EnableMorphling + war_council_nerfs: WarCouncilNerfs + spear_of_adun_presence: SpearOfAdunPresence + spear_of_adun_present_in_no_build: SpearOfAdunPresentInNoBuild + spear_of_adun_passive_ability_presence: SpearOfAdunPassiveAbilityPresence + spear_of_adun_passive_present_in_no_build: SpearOfAdunPassivesPresentInNoBuild + spear_of_adun_max_active_abilities: SpearOfAdunMaxActiveAbilities + spear_of_adun_max_passive_abilities: SpearOfAdunMaxAutocastAbilities + grant_story_tech: GrantStoryTech + grant_story_levels: GrantStoryLevels + nova_max_weapons: NovaMaxWeapons + nova_max_gadgets: NovaMaxGadgets + nova_ghost_of_a_chance_variant: NovaGhostOfAChanceVariant + take_over_ai_allies: TakeOverAIAllies + locked_items: LockedItems + excluded_items: ExcludedItems + unexcluded_items: UnexcludedItems + excluded_missions: ExcludedMissions + difficulty_curve: DifficultyCurve + exclude_very_hard_missions: ExcludeVeryHardMissions + vanilla_items_only: VanillaItemsOnly + exclude_overpowered_items: ExcludeOverpoweredItems + victory_cache: VictoryCache + vanilla_locations: VanillaLocations + extra_locations: ExtraLocations + challenge_locations: ChallengeLocations + mastery_locations: MasteryLocations + basebust_locations: BasebustLocations + speedrun_locations: SpeedrunLocations + preventative_locations: PreventativeLocations + filler_percentage: FillerPercentage + minerals_per_item: MineralsPerItem + vespene_per_item: VespenePerItem + starting_supply_per_item: StartingSupplyPerItem + maximum_supply_per_item: MaximumSupplyPerItem + maximum_supply_reduction_per_item: MaximumSupplyReductionPerItem + lowest_maximum_supply: LowestMaximumSupply + research_cost_reduction_per_item: ResearchCostReductionPerItem + filler_items_distribution: FillerItemsDistribution + mission_order_scouting: MissionOrderScouting + + custom_mission_order: CustomMissionOrder + +option_groups = [ + OptionGroup("Difficulty Settings", [ + GameDifficulty, + GameSpeed, + StarterUnit, + RequiredTactics, + WarCouncilNerfs, + DifficultyCurve, + ]), + OptionGroup("Primary Campaign Settings", [ + MissionOrder, + MaximumCampaignSize, + EnabledCampaigns, + EnableRaceSwapVariants, + ShuffleNoBuild, + ]), + OptionGroup("Optional Campaign Settings", [ + KeyMode, + ShuffleCampaigns, + AllInMap, + TwoStartPositions, + SelectRaces, + ExcludeVeryHardMissions, + EnableMissionRaceBalancing, + ]), + OptionGroup("Unit Upgrades", [ + EnsureGenericItems, + MinNumberOfUpgrades, + MaxNumberOfUpgrades, + MaxUpgradeLevel, + GenericUpgradeMissions, + GenericUpgradeResearch, + GenericUpgradeResearchSpeedup, + GenericUpgradeItems, + ]), + OptionGroup("Kerrigan", [ + KerriganPresence, + GrantStoryLevels, + KerriganLevelsPerMissionCompleted, + KerriganLevelsPerMissionCompletedCap, + KerriganLevelItemSum, + KerriganLevelItemDistribution, + KerriganTotalLevelCap, + StartPrimaryAbilities, + KerriganPrimalStatus, + KerriganMaxActiveAbilities, + KerriganMaxPassiveAbilities, + ]), + OptionGroup("Spear of Adun", [ + SpearOfAdunPresence, + SpearOfAdunPresentInNoBuild, + SpearOfAdunPassiveAbilityPresence, + SpearOfAdunPassivesPresentInNoBuild, + SpearOfAdunMaxActiveAbilities, + SpearOfAdunMaxAutocastAbilities, + ]), + OptionGroup("Nova", [ + NovaMaxWeapons, + NovaMaxGadgets, + NovaGhostOfAChanceVariant, + ]), + OptionGroup("Race Specific Options", [ + EnableMorphling, + MercenaryHighlanders, + ]), + OptionGroup("Check Locations", [ + VictoryCache, + VanillaLocations, + ExtraLocations, + ChallengeLocations, + MasteryLocations, + BasebustLocations, + SpeedrunLocations, + PreventativeLocations, + ]), + OptionGroup("Filler Options", [ + FillerPercentage, + MineralsPerItem, + VespenePerItem, + StartingSupplyPerItem, + MaximumSupplyPerItem, + MaximumSupplyReductionPerItem, + LowestMaximumSupply, + ResearchCostReductionPerItem, + FillerItemsDistribution, + ]), + OptionGroup("Inclusions & Exclusions", [ + LockedItems, + ExcludedItems, + UnexcludedItems, + VanillaItemsOnly, + ExcludeOverpoweredItems, + ExcludedMissions, + ]), + OptionGroup("Advanced Gameplay", [ + MissionOrderScouting, + DifficultyDamageModifier, + TakeOverAIAllies, + EnableVoidTrade, + VoidTradeAgeLimit, + VoidTradeWorkers, + GrantStoryTech, + CustomMissionOrder, + ]), + OptionGroup("Cosmetics", [ + PlayerColorTerranRaynor, + PlayerColorProtoss, + PlayerColorZerg, + PlayerColorZergPrimal, + PlayerColorNova, + ]) +] + +def get_option_value(world: Union['SC2World', None], name: str) -> int: + """ + You should basically never use this unless `world` can be `None`. + Use `world.options..value` instead for better typing, autocomplete, and error messages. + """ + if world is None: + field: Field = [class_field for class_field in fields(Starcraft2Options) if class_field.name == name][0] + if isinstance(field.type, str): + if field.type in globals(): + return globals()[field.type].default + import Options + return Options.__dict__[field.type].default + return field.type.default + + player_option = getattr(world.options, name) + + return player_option.value + + +def get_enabled_races(world: Optional['SC2World']) -> Set[SC2Race]: + race_names = world.options.selected_races.value if world and len(world.options.selected_races.value) > 0 else SelectRaces.valid_keys + return {race for race in SC2Race if race.get_title() in race_names} + + +def get_enabled_campaigns(world: Optional['SC2World']) -> Set[SC2Campaign]: + if world is None: + return {campaign for campaign in SC2Campaign if campaign.campaign_name in EnabledCampaigns.default} + campaign_names = world.options.enabled_campaigns + campaigns = {campaign for campaign in SC2Campaign if campaign.campaign_name in campaign_names} + if (world.options.mission_order.value == MissionOrder.option_vanilla + and get_enabled_races(world) != {SC2Race.TERRAN, SC2Race.ZERG, SC2Race.PROTOSS} + and SC2Campaign.EPILOGUE in campaigns + ): + campaigns.remove(SC2Campaign.EPILOGUE) + if len(campaigns) == 0: + # Everything is disabled, roll as everything enabled + return {campaign for campaign in SC2Campaign if campaign != SC2Campaign.GLOBAL} + return campaigns + + +def get_disabled_campaigns(world: 'SC2World') -> 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_disabled_flags(world: 'SC2World') -> MissionFlag: + excluded = ( + (MissionFlag.Terran | MissionFlag.Zerg | MissionFlag.Protoss) + ^ functools.reduce(lambda a, b: a | b, [race.get_mission_flag() for race in get_enabled_races(world)]) + ) + # filter out no-build missions + if not world.options.shuffle_no_build.value: + excluded |= MissionFlag.NoBuild + raceswap_option = world.options.enable_race_swap.value + if raceswap_option == EnableRaceSwapVariants.option_disabled: + excluded |= MissionFlag.RaceSwap + elif raceswap_option in [EnableRaceSwapVariants.option_pick_one_non_vanilla, EnableRaceSwapVariants.option_shuffle_all_non_vanilla]: + excluded |= MissionFlag.HasRaceSwap + # TODO: add more flags to potentially exclude once we have a way to get that from the player + return MissionFlag(excluded) + + +def get_excluded_missions(world: 'SC2World') -> Set[SC2Mission]: + mission_order_type = world.options.mission_order.value + excluded_mission_names = world.options.excluded_missions.value + disabled_campaigns = get_disabled_campaigns(world) + disabled_flags = get_disabled_flags(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 (mission_order_type != MissionOrder.option_vanilla and + ( + world.options.exclude_very_hard_missions == ExcludeVeryHardMissions.option_true + or ( + world.options.exclude_very_hard_missions == ExcludeVeryHardMissions.option_default + and ( + ( + mission_order_type in dynamic_mission_orders + and world.options.maximum_campaign_size < 20 + ) + or mission_order_type == MissionOrder.option_mini_campaign + ) + ) + ) + ): + excluded_missions = excluded_missions.union( + [mission for mission in SC2Mission if + mission.pool == MissionPools.VERY_HARD and mission.campaign != SC2Campaign.EPILOGUE] + ) + # Omitting missions with flags we don't want + if disabled_flags: + excluded_missions = excluded_missions.union(get_missions_with_any_flags_in_list(disabled_flags)) + # Omitting missions not in enabled campaigns + for campaign in disabled_campaigns: + excluded_missions = excluded_missions.union(campaign_mission_table[campaign]) + # Omitting unwanted mission variants + if world.options.enable_race_swap.value in [EnableRaceSwapVariants.option_pick_one, EnableRaceSwapVariants.option_pick_one_non_vanilla]: + swaps = [ + mission for mission in SC2Mission + if mission not in excluded_missions + and mission.flags & (MissionFlag.HasRaceSwap|MissionFlag.RaceSwap) + ] + while len(swaps) > 0: + curr = swaps[0] + variants = [mission for mission in swaps if mission.map_file == curr.map_file] + variants.sort(key=lambda mission: mission.id) + swaps = [mission for mission in swaps if mission not in variants] + if len(variants) > 1: + variants.pop(world.random.randint(0, len(variants)-1)) + excluded_missions = excluded_missions.union(variants) + + return excluded_missions + + +def is_mission_in_soa_presence( + spear_of_adun_presence: int, + mission: SC2Mission, + option_class: Type[SpearOfAdunPresence] | Type[SpearOfAdunPassiveAbilityPresence] = SpearOfAdunPresence +) -> bool: + """ + Returns True if the mission can have Spear of Adun abilities. + No-build presence must be checked separately. + """ + return ( + (spear_of_adun_presence == option_class.option_everywhere) + or (spear_of_adun_presence == option_class.option_protoss and MissionFlag.Protoss in mission.flags) + or (spear_of_adun_presence == option_class.option_any_race_lotv + and (mission.campaign == SC2Campaign.LOTV or MissionFlag.VanillaSoa in mission.flags) + ) + or (spear_of_adun_presence == option_class.option_vanilla + and (MissionFlag.VanillaSoa in mission.flags # Keeps SOA off on Growing Shadow, as that's vanilla behaviour + or (MissionFlag.NoBuild in mission.flags and mission.campaign == SC2Campaign.LOTV) + ) + ) + ) + + + +static_mission_orders = [ + MissionOrder.option_vanilla, + MissionOrder.option_vanilla_shuffled, + MissionOrder.option_mini_campaign, +] + +dynamic_mission_orders = [ + MissionOrder.option_golden_path, + MissionOrder.option_grid, + MissionOrder.option_gauntlet, + MissionOrder.option_blitz, + MissionOrder.option_hopscotch, +] + +LEGACY_GRID_ORDERS = {3, 4, 8} # Medium Grid, Mini Grid, and Tiny Grid respectively + +kerrigan_unit_available = [ + KerriganPresence.option_vanilla, +] + +# Names of upgrades to be included for different options +upgrade_included_names: Dict[int, Set[str]] = { + GenericUpgradeItems.option_individual_items: { + item_names.PROGRESSIVE_TERRAN_INFANTRY_WEAPON, + item_names.PROGRESSIVE_TERRAN_INFANTRY_ARMOR, + item_names.PROGRESSIVE_TERRAN_VEHICLE_WEAPON, + item_names.PROGRESSIVE_TERRAN_VEHICLE_ARMOR, + item_names.PROGRESSIVE_TERRAN_SHIP_WEAPON, + item_names.PROGRESSIVE_TERRAN_SHIP_ARMOR, + item_names.PROGRESSIVE_ZERG_MELEE_ATTACK, + item_names.PROGRESSIVE_ZERG_MISSILE_ATTACK, + item_names.PROGRESSIVE_ZERG_GROUND_CARAPACE, + item_names.PROGRESSIVE_ZERG_FLYER_ATTACK, + item_names.PROGRESSIVE_ZERG_FLYER_CARAPACE, + item_names.PROGRESSIVE_PROTOSS_GROUND_WEAPON, + item_names.PROGRESSIVE_PROTOSS_GROUND_ARMOR, + item_names.PROGRESSIVE_PROTOSS_SHIELDS, + item_names.PROGRESSIVE_PROTOSS_AIR_WEAPON, + item_names.PROGRESSIVE_PROTOSS_AIR_ARMOR, + }, + GenericUpgradeItems.option_bundle_weapon_and_armor: { + item_names.PROGRESSIVE_TERRAN_WEAPON_UPGRADE, + item_names.PROGRESSIVE_TERRAN_ARMOR_UPGRADE, + item_names.PROGRESSIVE_ZERG_WEAPON_UPGRADE, + item_names.PROGRESSIVE_ZERG_ARMOR_UPGRADE, + item_names.PROGRESSIVE_PROTOSS_WEAPON_UPGRADE, + item_names.PROGRESSIVE_PROTOSS_ARMOR_UPGRADE, + }, + GenericUpgradeItems.option_bundle_unit_class: { + item_names.PROGRESSIVE_TERRAN_INFANTRY_UPGRADE, + item_names.PROGRESSIVE_TERRAN_VEHICLE_UPGRADE, + item_names.PROGRESSIVE_TERRAN_SHIP_UPGRADE, + item_names.PROGRESSIVE_ZERG_GROUND_UPGRADE, + item_names.PROGRESSIVE_ZERG_FLYER_UPGRADE, + item_names.PROGRESSIVE_PROTOSS_GROUND_UPGRADE, + item_names.PROGRESSIVE_PROTOSS_AIR_UPGRADE, + }, + GenericUpgradeItems.option_bundle_all: { + item_names.PROGRESSIVE_TERRAN_WEAPON_ARMOR_UPGRADE, + item_names.PROGRESSIVE_ZERG_WEAPON_ARMOR_UPGRADE, + item_names.PROGRESSIVE_PROTOSS_WEAPON_ARMOR_UPGRADE, + } +} + +# Mapping trade age limit options to their millisecond equivalents +void_trade_age_limits_ms: Dict[int, int] = { + VoidTradeAgeLimit.option_5_minutes: 1000 * int(timedelta(minutes = 5).total_seconds()), + VoidTradeAgeLimit.option_30_minutes: 1000 * int(timedelta(minutes = 30).total_seconds()), + VoidTradeAgeLimit.option_1_hour: 1000 * int(timedelta(hours = 1).total_seconds()), + VoidTradeAgeLimit.option_2_hours: 1000 * int(timedelta(hours = 2).total_seconds()), + VoidTradeAgeLimit.option_4_hours: 1000 * int(timedelta(hours = 4).total_seconds()), + VoidTradeAgeLimit.option_1_day: 1000 * int(timedelta(days = 1).total_seconds()), + VoidTradeAgeLimit.option_1_week: 1000 * int(timedelta(weeks = 1).total_seconds()), +} diff --git a/worlds/sc2/pool_filter.py b/worlds/sc2/pool_filter.py new file mode 100644 index 00000000..31e47934 --- /dev/null +++ b/worlds/sc2/pool_filter.py @@ -0,0 +1,493 @@ +import logging +from typing import Callable, Dict, List, Set, Tuple, TYPE_CHECKING, Iterable + +from BaseClasses import Location, ItemClassification +from .item import StarcraftItem, ItemFilterFlags, item_names, item_parents, item_groups +from .item.item_tables import item_table, TerranItemType, ZergItemType, spear_of_adun_calldowns, \ + spear_of_adun_castable_passives +from .options import RequiredTactics + +if TYPE_CHECKING: + from . import SC2World + + +# Items that can be placed before resources if not already in +# General upgrades and Mercs +second_pass_placeable_items: Tuple[str, ...] = ( + # Global weapon/armor upgrades + item_names.PROGRESSIVE_TERRAN_ARMOR_UPGRADE, + item_names.PROGRESSIVE_TERRAN_WEAPON_UPGRADE, + item_names.PROGRESSIVE_TERRAN_WEAPON_ARMOR_UPGRADE, + item_names.PROGRESSIVE_ZERG_ARMOR_UPGRADE, + item_names.PROGRESSIVE_ZERG_WEAPON_UPGRADE, + item_names.PROGRESSIVE_ZERG_WEAPON_ARMOR_UPGRADE, + item_names.PROGRESSIVE_PROTOSS_ARMOR_UPGRADE, + item_names.PROGRESSIVE_PROTOSS_WEAPON_UPGRADE, + item_names.PROGRESSIVE_PROTOSS_WEAPON_ARMOR_UPGRADE, + item_names.PROGRESSIVE_PROTOSS_SHIELDS, + # Terran Buildings without upgrades + item_names.SENSOR_TOWER, + item_names.HIVE_MIND_EMULATOR, + item_names.PSI_DISRUPTER, + item_names.PERDITION_TURRET, + # General Terran upgrades without any dependencies + item_names.SCV_ADVANCED_CONSTRUCTION, + item_names.SCV_DUAL_FUSION_WELDERS, + item_names.SCV_CONSTRUCTION_JUMP_JETS, + item_names.PROGRESSIVE_FIRE_SUPPRESSION_SYSTEM, + item_names.PROGRESSIVE_ORBITAL_COMMAND, + item_names.ULTRA_CAPACITORS, + item_names.VANADIUM_PLATING, + item_names.ORBITAL_DEPOTS, + item_names.MICRO_FILTERING, + item_names.AUTOMATED_REFINERY, + item_names.COMMAND_CENTER_COMMAND_CENTER_REACTOR, + item_names.COMMAND_CENTER_SCANNER_SWEEP, + item_names.COMMAND_CENTER_MULE, + item_names.COMMAND_CENTER_EXTRA_SUPPLIES, + item_names.TECH_REACTOR, + item_names.CELLULAR_REACTOR, + item_names.PROGRESSIVE_REGENERATIVE_BIO_STEEL, # Place only L1 + item_names.STRUCTURE_ARMOR, + item_names.HI_SEC_AUTO_TRACKING, + item_names.ADVANCED_OPTICS, + item_names.ROGUE_FORCES, + # Mercenaries (All races) + *[item_name for item_name, item_data in item_table.items() + if item_data.type in (TerranItemType.Mercenary, ZergItemType.Mercenary)], + # Kerrigan and Nova levels, abilities and generally useful stuff + *[item_name for item_name, item_data in item_table.items() + if item_data.type in ( + ZergItemType.Level, + ZergItemType.Ability, + ZergItemType.Evolution_Pit, + TerranItemType.Nova_Gear + )], + item_names.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE, + # Zerg static defenses + item_names.SPORE_CRAWLER, + item_names.SPINE_CRAWLER, + # Overseer + item_names.OVERLORD_OVERSEER_ASPECT, + # Spear of Adun Abilities + item_names.SOA_CHRONO_SURGE, + item_names.SOA_PROGRESSIVE_PROXY_PYLON, + item_names.SOA_PYLON_OVERCHARGE, + item_names.SOA_ORBITAL_STRIKE, + item_names.SOA_TEMPORAL_FIELD, + item_names.SOA_SOLAR_LANCE, + item_names.SOA_MASS_RECALL, + item_names.SOA_SHIELD_OVERCHARGE, + item_names.SOA_DEPLOY_FENIX, + item_names.SOA_PURIFIER_BEAM, + item_names.SOA_TIME_STOP, + item_names.SOA_SOLAR_BOMBARDMENT, + # Protoss generic upgrades + item_names.MATRIX_OVERLOAD, + item_names.QUATRO, + item_names.NEXUS_OVERCHARGE, + item_names.ORBITAL_ASSIMILATORS, + item_names.WARP_HARMONIZATION, + item_names.GUARDIAN_SHELL, + item_names.RECONSTRUCTION_BEAM, + item_names.OVERWATCH, + item_names.SUPERIOR_WARP_GATES, + item_names.KHALAI_INGENUITY, + item_names.AMPLIFIED_ASSIMILATORS, + # Protoss static defenses + item_names.PHOTON_CANNON, + item_names.KHAYDARIN_MONOLITH, + item_names.SHIELD_BATTERY, +) + + +def copy_item(item: StarcraftItem) -> StarcraftItem: + return StarcraftItem(item.name, item.classification, item.code, item.player, item.filter_flags) + + +class ValidInventory: + def __init__(self, world: 'SC2World', item_pool: List[StarcraftItem]) -> None: + self.multiworld = world.multiworld + self.player = world.player + self.world: 'SC2World' = world + # Track all Progression items and those with complex rules for filtering + self.logical_inventory: Dict[str, int] = {} + for item in item_pool: + if not item_table[item.name].is_important_for_filtering(): + continue + self.logical_inventory.setdefault(item.name, 0) + self.logical_inventory[item.name] += 1 + self.item_pool = item_pool + self.item_name_to_item: Dict[str, List[StarcraftItem]] = {} + self.item_name_to_child_items: Dict[str, List[StarcraftItem]] = {} + for item in item_pool: + self.item_name_to_item.setdefault(item.name, []).append(item) + for parent_item in item_parents.child_item_to_parent_items.get(item.name, []): + self.item_name_to_child_items.setdefault(parent_item, []).append(item) + + def has(self, item: str, player: int, count: int = 1) -> bool: + return self.logical_inventory.get(item, 0) >= count + + def has_any(self, items: Set[str], player: int) -> bool: + return any(self.logical_inventory.get(item) for item in items) + + def has_all(self, items: Set[str], player: int) -> bool: + return all(self.logical_inventory.get(item) for item in items) + + def has_group(self, item_group: str, player: int, count: int = 1) -> bool: + 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 self.logical_inventory.get(item, 0) + + def count_from_list(self, items: Iterable[str], player: int) -> int: + return sum(self.logical_inventory.get(item, 0) for item in items) + + def count_from_list_unique(self, items: Iterable[str], player: int) -> int: + return sum(item in self.logical_inventory for item in items) + + def generate_reduced_inventory(self, inventory_size: int, filler_amount: int, mission_requirements: List[Tuple[str, Callable]]) -> List[StarcraftItem]: + """Attempts to generate a reduced inventory that can fulfill the mission requirements.""" + inventory: List[StarcraftItem] = list(self.item_pool) + requirements = mission_requirements + min_upgrades_per_unit = self.world.options.min_number_of_upgrades.value + max_upgrades_per_unit = self.world.options.max_number_of_upgrades.value + if max_upgrades_per_unit > -1 and min_upgrades_per_unit > max_upgrades_per_unit: + logging.getLogger("Starcraft 2").warning( + f"min upgrades per unit is greater than max upgrades per unit ({min_upgrades_per_unit} > {max_upgrades_per_unit}). " + f"Setting both to minimum value ({min_upgrades_per_unit})" + ) + max_upgrades_per_unit = min_upgrades_per_unit + + def attempt_removal( + item: StarcraftItem, + remove_flag: ItemFilterFlags = ItemFilterFlags.FilterExcluded, + ) -> str: + """ + Returns empty string and applies `remove_flag` if the item is removable, + else returns a string containing failed locations and applies ItemFilterFlags.LogicLocked + """ + # Only run logic checks when removing logic items + if self.logical_inventory.get(item.name, 0) > 0: + self.logical_inventory[item.name] -= 1 + failed_rules = [name for name, requirement in mission_requirements if not requirement(self)] + if failed_rules: + # If item cannot be removed, lock and revert + self.logical_inventory[item.name] += 1 + item.filter_flags |= ItemFilterFlags.LogicLocked + return f"{len(failed_rules)} rules starting with \"{failed_rules[0]}\"" + if not self.logical_inventory[item.name]: + del self.logical_inventory[item.name] + item.filter_flags |= remove_flag + return "" + + def remove_child_items( + parent_item: StarcraftItem, + remove_flag: ItemFilterFlags = ItemFilterFlags.FilterExcluded, + ) -> None: + child_items = self.item_name_to_child_items.get(parent_item.name, []) + for child_item in child_items: + if (ItemFilterFlags.AllowedOrphan|ItemFilterFlags.Unexcludable) & child_item.filter_flags: + continue + parent_id = item_table[child_item.name].parent + assert parent_id is not None + if item_parents.parent_present[parent_id](self.logical_inventory, self.world.options): + continue + if not attempt_removal(child_item, remove_flag): + remove_child_items(child_item, remove_flag) + + def cull_items_over_maximum(group: List[StarcraftItem], allowed_max: int) -> None: + for item in group: + if len([x for x in group if ItemFilterFlags.Culled not in x.filter_flags]) <= allowed_max: + break + if ItemFilterFlags.Uncullable & item.filter_flags: + continue + attempt_removal(item, remove_flag=ItemFilterFlags.Culled) + + def request_minimum_items(group: List[StarcraftItem], requested_minimum) -> None: + for item in group: + if len([x for x in group if ItemFilterFlags.RequestedOrBetter & x.filter_flags]) >= requested_minimum: + break + if ItemFilterFlags.Culled & item.filter_flags: + continue + item.filter_flags |= ItemFilterFlags.Requested + + # Process Excluded items, validate if the item can get actually excluded + excluded_items: List[StarcraftItem] = [starcraft_item for starcraft_item in inventory if ItemFilterFlags.Excluded & starcraft_item.filter_flags] + self.world.random.shuffle(excluded_items) + for excluded_item in excluded_items: + if ItemFilterFlags.Unexcludable & excluded_item.filter_flags: + continue + removal_failed = attempt_removal(excluded_item, remove_flag=ItemFilterFlags.Removed) + if removal_failed: + if ItemFilterFlags.UserExcluded in excluded_item.filter_flags: + logging.getLogger("Starcraft 2").warning( + f"Cannot exclude item {excluded_item.name} as it would break {removal_failed}" + ) + else: + assert False, f"Item filtering excluded an item which is logically required: {excluded_item.name}" + continue + remove_child_items(excluded_item, remove_flag=ItemFilterFlags.Removed) + inventory = [item for item in inventory if ItemFilterFlags.Removed not in item.filter_flags] + + # Clear excluded flags; all existing ones should be implemented or out-of-logic + for item in inventory: + item.filter_flags &= ~ItemFilterFlags.Excluded + + # Determine item groups to be constrained by min/max upgrades per unit + group_to_item: Dict[str, List[StarcraftItem]] = {} + group: str = "" + for group, group_member_names in item_parents.item_upgrade_groups.items(): + group_to_item[group] = [] + for item_name in group_member_names: + inventory_items = self.item_name_to_item.get(item_name, []) + group_to_item[group].extend(item for item in inventory_items if ItemFilterFlags.Removed not in item.filter_flags) + + # Limit the maximum number of upgrades + if max_upgrades_per_unit != -1: + for group_name, group_items in group_to_item.items(): + self.world.random.shuffle(group_to_item[group]) + cull_items_over_maximum(group_items, max_upgrades_per_unit) + + # Requesting minimum upgrades for items that have already been locked/placed when minimum required + if min_upgrades_per_unit != -1: + for group_name, group_items in group_to_item.items(): + self.world.random.shuffle(group_items) + request_minimum_items(group_items, min_upgrades_per_unit) + + # Kerrigan max abilities + kerrigan_actives = [item for item in inventory if item.name in item_groups.kerrigan_active_abilities] + self.world.random.shuffle(kerrigan_actives) + cull_items_over_maximum(kerrigan_actives, self.world.options.kerrigan_max_active_abilities.value) + + kerrigan_passives = [item for item in inventory if item.name in item_groups.kerrigan_passives] + self.world.random.shuffle(kerrigan_passives) + cull_items_over_maximum(kerrigan_passives, self.world.options.kerrigan_max_passive_abilities.value) + + # Spear of Adun max abilities + spear_of_adun_actives = [item for item in inventory if item.name in spear_of_adun_calldowns] + self.world.random.shuffle(spear_of_adun_actives) + cull_items_over_maximum(spear_of_adun_actives, self.world.options.spear_of_adun_max_active_abilities.value) + + spear_of_adun_autocasts = [item for item in inventory if item.name in spear_of_adun_castable_passives] + self.world.random.shuffle(spear_of_adun_autocasts) + cull_items_over_maximum(spear_of_adun_autocasts, self.world.options.spear_of_adun_max_passive_abilities.value) + + # Nova items + nova_weapon_items = [item for item in inventory if item.name in item_groups.nova_weapons] + self.world.random.shuffle(nova_weapon_items) + cull_items_over_maximum(nova_weapon_items, self.world.options.nova_max_weapons.value) + + nova_gadget_items = [item for item in inventory if item.name in item_groups.nova_gadgets] + self.world.random.shuffle(nova_gadget_items) + cull_items_over_maximum(nova_gadget_items, self.world.options.nova_max_gadgets.value) + + # Determining if the full-size inventory can complete campaign + # Note(mm): Now that user excludes are checked against logic, this can probably never fail unless there's a bug. + 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: List[StarcraftItem] = [ + starcraft_item for starcraft_item in inventory + if starcraft_item.name in second_pass_placeable_items + and ( + not ItemFilterFlags.CulledOrBetter & starcraft_item.filter_flags + or ItemFilterFlags.RequestedOrBetter & starcraft_item.filter_flags + ) + ] + reserved_generic_percent = self.world.options.ensure_generic_items.value / 100 + reserved_generic_amount = int(len(generic_items) * reserved_generic_percent) + self.world.random.shuffle(generic_items) + for starcraft_item in generic_items[:reserved_generic_amount]: + starcraft_item.filter_flags |= ItemFilterFlags.Requested + + # Main cull process + def remove_random_item( + removable: List[StarcraftItem], + dont_remove_flags: ItemFilterFlags, + remove_flag: ItemFilterFlags = ItemFilterFlags.Removed, + ) -> bool: + if len(removable) == 0: + return False + item = self.world.random.choice(removable) + # Do not remove item if it would drop upgrades below minimum + if min_upgrades_per_unit > 0: + group_name = None + parent = item_table[item.name].parent + if parent is not None: + group_name = item_parents.parent_present[parent].constraint_group + if group_name is not None: + children = group_to_item.get(group_name, []) + children = [x for x in children if not (ItemFilterFlags.CulledOrBetter & x.filter_flags)] + if len(children) <= min_upgrades_per_unit: + # Attempt to remove a parent instead, if possible + dont_remove = ItemFilterFlags.Removed|dont_remove_flags + parent_items = [ + parent_item + for parent_name in item_parents.child_item_to_parent_items[item.name] + for parent_item in self.item_name_to_item.get(parent_name, []) + if not (dont_remove & parent_item.filter_flags) + ] + if parent_items: + item = self.world.random.choice(parent_items) + else: + # Lock remaining upgrades + for item in children: + item.filter_flags |= ItemFilterFlags.Locked + return False + if not attempt_removal(item, remove_flag): + remove_child_items(item, remove_flag) + return True + return False + + def item_included(item: StarcraftItem) -> bool: + return bool( + ItemFilterFlags.Removed not in item.filter_flags + and ((ItemFilterFlags.Unexcludable|ItemFilterFlags.Excluded) & item.filter_flags) != ItemFilterFlags.Excluded + ) + + # Actually remove culled items; we won't re-add them + inventory = [ + item for item in inventory + if (((ItemFilterFlags.Uncullable|ItemFilterFlags.Culled) & item.filter_flags) != ItemFilterFlags.Culled) + ] + + # Part 1: Remove items that are not requested + start_inventory_size = len([item for item in inventory if ItemFilterFlags.StartInventory in item.filter_flags]) + current_inventory_size = len([item for item in inventory if item_included(item)]) + cullable_items = [item for item in inventory if not (ItemFilterFlags.Uncullable & item.filter_flags)] + while current_inventory_size - start_inventory_size > inventory_size - filler_amount: + if len(cullable_items) == 0: + if filler_amount > 0: + filler_amount -= 1 + else: + break + if remove_random_item(cullable_items, ItemFilterFlags.Uncullable): + inventory = [item for item in inventory if ItemFilterFlags.Removed not in item.filter_flags] + current_inventory_size = len([item for item in inventory if item_included(item)]) + cullable_items = [ + item for item in cullable_items + if not ((ItemFilterFlags.Removed|ItemFilterFlags.Uncullable) & item.filter_flags) + ] + + # Handle too many requested + if current_inventory_size - start_inventory_size > inventory_size - filler_amount: + for item in inventory: + item.filter_flags &= ~ItemFilterFlags.Requested + + # Part 2: If we need to remove more, allow removing requested items + excludable_items = [item for item in inventory if not (ItemFilterFlags.Unexcludable & item.filter_flags)] + while current_inventory_size - start_inventory_size > inventory_size - filler_amount: + if len(excludable_items) == 0: + break + if remove_random_item(excludable_items, ItemFilterFlags.Unexcludable): + inventory = [item for item in inventory if ItemFilterFlags.Removed not in item.filter_flags] + current_inventory_size = len([item for item in inventory if item_included(item)]) + excludable_items = [ + item for item in inventory + if not ((ItemFilterFlags.Removed|ItemFilterFlags.Unexcludable) & item.filter_flags) + ] + + # Part 3: If it still doesn't fit, move locked items to start inventory until it fits + precollect_items = current_inventory_size - inventory_size - start_inventory_size - filler_amount + if precollect_items > 0: + promotable = [ + item + for item in inventory + if ItemFilterFlags.StartInventory not in item.filter_flags + and ItemFilterFlags.Locked in item.filter_flags + ] + self.world.random.shuffle(promotable) + for item in promotable[:precollect_items]: + item.filter_flags |= ItemFilterFlags.StartInventory + start_inventory_size += 1 + + # Removing extra dependencies + # Transport Hook + if not self.logical_inventory.get(item_names.MEDIVAC): + # Don't allow L2 Siege Tank Transport Hook without Medivac + inventory_transport_hooks = [item for item in inventory if item.name == item_names.SIEGE_TANK_PROGRESSIVE_TRANSPORT_HOOK] + removable_transport_hooks = [item for item in inventory_transport_hooks if not (ItemFilterFlags.Unexcludable & item.filter_flags)] + if len(inventory_transport_hooks) > 1 and removable_transport_hooks: + inventory.remove(removable_transport_hooks[0]) + + # Weapon/Armour upgrades + def exclude_wa(prefix: str) -> List[StarcraftItem]: + return [ + item for item in inventory + if (ItemFilterFlags.UnexcludableUpgrade & item.filter_flags) + or not item.name.startswith(prefix) + ] + used_item_names: Set[str] = {item.name for item in inventory} + if used_item_names.isdisjoint(item_groups.barracks_wa_group): + inventory = exclude_wa(item_names.TERRAN_INFANTRY_UPGRADE_PREFIX) + if used_item_names.isdisjoint(item_groups.factory_wa_group): + inventory = exclude_wa(item_names.TERRAN_VEHICLE_UPGRADE_PREFIX) + if used_item_names.isdisjoint(item_groups.starport_wa_group): + inventory = exclude_wa(item_names.TERRAN_SHIP_UPGRADE_PREFIX) + if used_item_names.isdisjoint(item_groups.zerg_melee_wa): + inventory = exclude_wa(item_names.PROGRESSIVE_ZERG_MELEE_ATTACK) + if used_item_names.isdisjoint(item_groups.zerg_ranged_wa): + inventory = exclude_wa(item_names.PROGRESSIVE_ZERG_MISSILE_ATTACK) + if used_item_names.isdisjoint(item_groups.zerg_air_units): + inventory = exclude_wa(item_names.ZERG_FLYER_UPGRADE_PREFIX) + if used_item_names.isdisjoint(item_groups.protoss_ground_wa): + inventory = exclude_wa(item_names.PROTOSS_GROUND_UPGRADE_PREFIX) + if used_item_names.isdisjoint(item_groups.protoss_air_wa): + inventory = exclude_wa(item_names.PROTOSS_AIR_UPGRADE_PREFIX) + + # Part 4: Last-ditch effort to reduce inventory size; upgrades can go in start inventory + current_inventory_size = len(inventory) + precollect_items = current_inventory_size - inventory_size - start_inventory_size - filler_amount + if precollect_items > 0: + promotable = [ + item + for item in inventory + if ItemFilterFlags.StartInventory not in item.filter_flags + ] + self.world.random.shuffle(promotable) + for item in promotable[:precollect_items]: + item.filter_flags |= ItemFilterFlags.StartInventory + start_inventory_size += 1 + + assert current_inventory_size - start_inventory_size <= inventory_size - filler_amount, ( + f"Couldn't reduce inventory to fit. target={inventory_size}, poolsize={current_inventory_size}, " + f"start_inventory={starcraft_item}, filler_amount={filler_amount}" + ) + + return inventory + + +def filter_items(world: 'SC2World', location_cache: List[Location], item_pool: List[StarcraftItem]) -> List[StarcraftItem]: + """ + 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) + # Most of the excluded locations get actually removed but Victory ones are mandatory in order to allow the game + # to progress normally. Since regular items aren't flagged as filler, we need to generate enough filler for those + # locations as we need to have something that can be actually placed there. + # Therefore, we reserve those to be filler. + excluded_locations = [location for location in open_locations if location.name in world.options.exclude_locations.value] + reserved_filler_count = len(excluded_locations) + target_nonfiller_item_count = inventory_size - reserved_filler_count + filler_amount = (inventory_size * world.options.filler_percentage) // 100 + if world.options.required_tactics.value == RequiredTactics.option_no_logic: + mission_requirements = [] + else: + mission_requirements = [(location.name, location.access_rule) for location in location_cache] + valid_inventory = ValidInventory(world, item_pool) + + valid_items = valid_inventory.generate_reduced_inventory(target_nonfiller_item_count, filler_amount, mission_requirements) + for _ in range(reserved_filler_count): + filler_item = world.create_item(world.get_filler_item_name()) + if filler_item.classification & ItemClassification.progression: + filler_item.classification = ItemClassification.filler # Must be flagged as Filler, even if it's a Kerrigan level + valid_items.append(filler_item) + return valid_items diff --git a/worlds/sc2/regions.py b/worlds/sc2/regions.py new file mode 100644 index 00000000..26d127a1 --- /dev/null +++ b/worlds/sc2/regions.py @@ -0,0 +1,532 @@ +from typing import TYPE_CHECKING, List, Dict, Any, Tuple, Optional + +from Options import OptionError +from .locations import LocationData, Location +from .mission_tables import ( + SC2Mission, SC2Campaign, MissionFlag, get_campaign_goal_priority, + campaign_final_mission_locations, campaign_alt_final_mission_locations +) +from .options import ( + ShuffleNoBuild, RequiredTactics, ShuffleCampaigns, + kerrigan_unit_available, TakeOverAIAllies, MissionOrder, get_excluded_missions, get_enabled_campaigns, + static_mission_orders, + TwoStartPositions, KeyMode, EnableMissionRaceBalancing, EnableRaceSwapVariants, NovaGhostOfAChanceVariant, + WarCouncilNerfs, GrantStoryTech +) +from .mission_order.options import CustomMissionOrder +from .mission_order import SC2MissionOrder +from .mission_order.nodes import SC2MOGenMissionOrder, Difficulty +from .mission_order.mission_pools import SC2MOGenMissionPools +from .mission_order.generation import resolve_unlocks, fill_depths, resolve_difficulties, fill_missions, make_connections, resolve_generic_keys + +if TYPE_CHECKING: + from . import SC2World + + +def create_mission_order( + world: 'SC2World', locations: Tuple[LocationData, ...], location_cache: List[Location] +): + # 'locations' contains both actual game locations and beat event locations for all mission regions + # When a region (mission) is accessible, all its locations are potentially accessible + # Accessible in this context always means "its access rule evaluates to True" + # This includes the beat events, which copy the access rules of the victory locations + # Beat events being added to logical inventory is auto-magic: + # Event locations contain an event item of (by default) identical name, + # which Archipelago's generator will consider part of the logical inventory + # whenever the event location becomes accessible + + # Set up mission pools + mission_pools = SC2MOGenMissionPools() + mission_pools.set_exclusions(get_excluded_missions(world), []) # TODO set unexcluded + adjust_mission_pools(world, mission_pools) + setup_mission_pool_balancing(world, mission_pools) + + mission_order_type = world.options.mission_order + if mission_order_type == MissionOrder.option_custom: + mission_order_dict = world.options.custom_mission_order.value + else: + mission_order_option = create_regular_mission_order(world, mission_pools) + if mission_order_type in static_mission_orders: + # Static orders get converted early to curate preset content, so it can be used as-is + mission_order_dict = mission_order_option + else: + mission_order_dict = CustomMissionOrder(mission_order_option).value + mission_order = SC2MOGenMissionOrder(world, mission_order_dict) + + # Set up requirements for individual parts of the mission order + resolve_unlocks(mission_order) + + # Ensure total accessibilty and resolve relative difficulties + fill_depths(mission_order) + resolve_difficulties(mission_order) + + # Build the mission order + fill_missions(mission_order, mission_pools, world, [], locations, location_cache) # TODO set locked missions + make_connections(mission_order, world) + + # Fill in Key requirements now that missions are placed + resolve_generic_keys(mission_order) + + return SC2MissionOrder(mission_order, mission_pools) + +def adjust_mission_pools(world: 'SC2World', pools: SC2MOGenMissionPools): + # Mission pool changes + mission_order_type = world.options.mission_order.value + enabled_campaigns = get_enabled_campaigns(world) + adv_tactics = world.options.required_tactics.value != RequiredTactics.option_standard + shuffle_no_build = world.options.shuffle_no_build.value + extra_locations = world.options.extra_locations.value + grant_story_tech = world.options.grant_story_tech.value + grant_story_levels = world.options.grant_story_levels.value + war_council_nerfs = world.options.war_council_nerfs.value == WarCouncilNerfs.option_true + + # WoL + if shuffle_no_build == ShuffleNoBuild.option_false or adv_tactics: + # Replacing No Build missions with Easy missions + # WoL + pools.move_mission(SC2Mission.ZERO_HOUR, Difficulty.EASY, Difficulty.STARTER) + pools.move_mission(SC2Mission.EVACUATION, Difficulty.EASY, Difficulty.STARTER) + pools.move_mission(SC2Mission.EVACUATION_Z, Difficulty.EASY, Difficulty.STARTER) + pools.move_mission(SC2Mission.EVACUATION_P, Difficulty.EASY, Difficulty.STARTER) + pools.move_mission(SC2Mission.DEVILS_PLAYGROUND, Difficulty.EASY, Difficulty.STARTER) + pools.move_mission(SC2Mission.DEVILS_PLAYGROUND_Z, Difficulty.EASY, Difficulty.STARTER) + pools.move_mission(SC2Mission.DEVILS_PLAYGROUND_P, Difficulty.EASY, Difficulty.STARTER) + if world.options.required_tactics != RequiredTactics.option_any_units: + # Per playtester feedback: doing this mission with only one unit is flaky + # but there are enough viable comps that >= 2 random units is probably workable + pools.move_mission(SC2Mission.THE_GREAT_TRAIN_ROBBERY, Difficulty.EASY, Difficulty.STARTER) + pools.move_mission(SC2Mission.THE_GREAT_TRAIN_ROBBERY_Z, Difficulty.EASY, Difficulty.STARTER) + pools.move_mission(SC2Mission.THE_GREAT_TRAIN_ROBBERY_P, Difficulty.EASY, Difficulty.STARTER) + # LotV + pools.move_mission(SC2Mission.THE_GROWING_SHADOW, Difficulty.EASY, Difficulty.STARTER) + if shuffle_no_build == ShuffleNoBuild.option_false: + # Pushing Outbreak to Normal, as it cannot be placed as the second mission on Build-Only + pools.move_mission(SC2Mission.OUTBREAK, Difficulty.EASY, Difficulty.MEDIUM) + # Pushing extra Normal missions to Easy + pools.move_mission(SC2Mission.ECHOES_OF_THE_FUTURE, Difficulty.MEDIUM, Difficulty.EASY) + pools.move_mission(SC2Mission.CUTTHROAT, Difficulty.MEDIUM, Difficulty.EASY) + # Additional changes on Advanced Tactics + if adv_tactics: + # WoL + pools.move_mission(SC2Mission.SMASH_AND_GRAB, Difficulty.EASY, Difficulty.STARTER) + pools.move_mission(SC2Mission.THE_MOEBIUS_FACTOR, Difficulty.MEDIUM, Difficulty.EASY) + pools.move_mission(SC2Mission.THE_MOEBIUS_FACTOR_Z, Difficulty.MEDIUM, Difficulty.EASY) + pools.move_mission(SC2Mission.THE_MOEBIUS_FACTOR_P, Difficulty.MEDIUM, Difficulty.EASY) + pools.move_mission(SC2Mission.WELCOME_TO_THE_JUNGLE, Difficulty.MEDIUM, Difficulty.EASY) + pools.move_mission(SC2Mission.ENGINE_OF_DESTRUCTION, Difficulty.HARD, Difficulty.MEDIUM) + # Prophecy needs to be adjusted if by itself + if enabled_campaigns == {SC2Campaign.PROPHECY}: + pools.move_mission(SC2Mission.A_SINISTER_TURN, Difficulty.MEDIUM, Difficulty.EASY) + # Prologue's only valid starter is the goal mission + if enabled_campaigns == {SC2Campaign.PROLOGUE} \ + or mission_order_type in static_mission_orders \ + and world.options.shuffle_campaigns.value == ShuffleCampaigns.option_false: + pools.move_mission(SC2Mission.DARK_WHISPERS, Difficulty.EASY, Difficulty.STARTER) + # HotS + kerriganless = world.options.kerrigan_presence.value not in kerrigan_unit_available \ + or SC2Campaign.HOTS not in enabled_campaigns + if grant_story_tech == GrantStoryTech.option_grant: + # Additional starter mission if player is granted story tech + pools.move_mission(SC2Mission.ENEMY_WITHIN, Difficulty.EASY, Difficulty.STARTER) + pools.move_mission(SC2Mission.TEMPLAR_S_RETURN, Difficulty.MEDIUM, Difficulty.STARTER) + pools.move_mission(SC2Mission.THE_ESCAPE, Difficulty.MEDIUM, Difficulty.STARTER) + pools.move_mission(SC2Mission.IN_THE_ENEMY_S_SHADOW, Difficulty.MEDIUM, Difficulty.STARTER) + if not war_council_nerfs: + pools.move_mission(SC2Mission.TEMPLAR_S_RETURN, Difficulty.MEDIUM, Difficulty.STARTER) + if (grant_story_tech == GrantStoryTech.option_grant and grant_story_levels) or kerriganless: + # The player has, all the stuff he needs, provided under these settings + pools.move_mission(SC2Mission.SUPREME, Difficulty.MEDIUM, Difficulty.STARTER) + pools.move_mission(SC2Mission.THE_INFINITE_CYCLE, Difficulty.HARD, Difficulty.STARTER) + pools.move_mission(SC2Mission.CONVICTION, Difficulty.MEDIUM, Difficulty.STARTER) + if (grant_story_tech != GrantStoryTech.option_grant + and ( + world.options.nova_ghost_of_a_chance_variant == NovaGhostOfAChanceVariant.option_nco + or ( + SC2Campaign.NCO in enabled_campaigns + and world.options.nova_ghost_of_a_chance_variant.value == NovaGhostOfAChanceVariant.option_auto + ) + ) + ): + # Using NCO tech for this mission that must be acquired + pools.move_mission(SC2Mission.GHOST_OF_A_CHANCE, Difficulty.STARTER, Difficulty.MEDIUM) + if world.options.take_over_ai_allies.value == TakeOverAIAllies.option_true: + pools.move_mission(SC2Mission.HARBINGER_OF_OBLIVION, Difficulty.MEDIUM, Difficulty.STARTER) + if pools.get_pool_size(Difficulty.STARTER) < 2 and not kerriganless or adv_tactics: + # Conditionally moving Easy missions to Starter + pools.move_mission(SC2Mission.HARVEST_OF_SCREAMS, Difficulty.EASY, Difficulty.STARTER) + pools.move_mission(SC2Mission.DOMINATION, Difficulty.EASY, Difficulty.STARTER) + if pools.get_pool_size(Difficulty.STARTER) < 2: + pools.move_mission(SC2Mission.DOMINATION, Difficulty.EASY, Difficulty.STARTER) + pools.move_mission(SC2Mission.DOMINATION_T, Difficulty.EASY, Difficulty.STARTER) + pools.move_mission(SC2Mission.DOMINATION_P, Difficulty.EASY, Difficulty.STARTER) + if pools.get_pool_size(Difficulty.STARTER) + pools.get_pool_size(Difficulty.EASY) < 2: + # Flashpoint needs just a few items at start but competent comp at the end + pools.move_mission(SC2Mission.FLASHPOINT, Difficulty.HARD, Difficulty.EASY) + +def setup_mission_pool_balancing(world: 'SC2World', pools: SC2MOGenMissionPools): + race_mission_balance = world.options.mission_race_balancing.value + flag_ratios: Dict[MissionFlag, int] = {} + flag_weights: Dict[MissionFlag, int] = {} + if race_mission_balance == EnableMissionRaceBalancing.option_semi_balanced: + flag_weights = { MissionFlag.Terran: 1, MissionFlag.Zerg: 1, MissionFlag.Protoss: 1 } + elif race_mission_balance == EnableMissionRaceBalancing.option_fully_balanced: + flag_ratios = { MissionFlag.Terran: 1, MissionFlag.Zerg: 1, MissionFlag.Protoss: 1 } + pools.set_flag_balances(flag_ratios, flag_weights) + +def create_regular_mission_order(world: 'SC2World', mission_pools: SC2MOGenMissionPools) -> Dict[str, Dict[str, Any]]: + mission_order_type = world.options.mission_order.value + + if mission_order_type in static_mission_orders: + return create_static_mission_order(world, mission_order_type, mission_pools) + else: + return create_dynamic_mission_order(world, mission_order_type, mission_pools) + +def create_static_mission_order(world: 'SC2World', mission_order_type: int, mission_pools: SC2MOGenMissionPools) -> Dict[str, Dict[str, Any]]: + mission_order: Dict[str, Dict[str, Any]] = {} + + enabled_campaigns = get_enabled_campaigns(world) + if mission_order_type == MissionOrder.option_vanilla: + missions = "vanilla" + elif world.options.shuffle_campaigns.value == ShuffleCampaigns.option_true: + missions = "random" + else: + missions = "vanilla_shuffled" + + if world.options.enable_race_swap.value == EnableRaceSwapVariants.option_disabled: + shuffle_raceswaps = False + else: + # Picking specific raceswap variants is handled by mission exclusion + shuffle_raceswaps = True + + key_mode_option = world.options.key_mode.value + if key_mode_option == KeyMode.option_missions: + keys = "missions" + elif key_mode_option == KeyMode.option_questlines: + keys = "layouts" + elif key_mode_option == KeyMode.option_progressive_missions: + keys = "progressive_missions" + elif key_mode_option == KeyMode.option_progressive_questlines: + keys = "progressive_layouts" + elif key_mode_option == KeyMode.option_progressive_per_questline: + keys = "progressive_per_layout" + else: + keys = "none" + + if mission_order_type == MissionOrder.option_mini_campaign: + prefix = "mini " + else: + prefix = "" + + def mission_order_preset(name: str) -> Dict[str, str]: + return { + "preset": prefix + name, + "missions": missions, + "shuffle_raceswaps": shuffle_raceswaps, + "keys": keys + } + + prophecy_enabled = SC2Campaign.PROPHECY in enabled_campaigns + wol_enabled = SC2Campaign.WOL in enabled_campaigns + if wol_enabled: + mission_order[SC2Campaign.WOL.campaign_name] = mission_order_preset("wol") + + if prophecy_enabled: + mission_order[SC2Campaign.PROPHECY.campaign_name] = mission_order_preset("prophecy") + + if SC2Campaign.HOTS in enabled_campaigns: + mission_order[SC2Campaign.HOTS.campaign_name] = mission_order_preset("hots") + + if SC2Campaign.PROLOGUE in enabled_campaigns: + mission_order[SC2Campaign.PROLOGUE.campaign_name] = mission_order_preset("prologue") + + if SC2Campaign.LOTV in enabled_campaigns: + mission_order[SC2Campaign.LOTV.campaign_name] = mission_order_preset("lotv") + + if SC2Campaign.EPILOGUE in enabled_campaigns: + mission_order[SC2Campaign.EPILOGUE.campaign_name] = mission_order_preset("epilogue") + entry_rules = [] + if SC2Campaign.WOL in enabled_campaigns: + entry_rules.append({ "scope": SC2Campaign.WOL.campaign_name }) + if SC2Campaign.HOTS in enabled_campaigns: + entry_rules.append({ "scope": SC2Campaign.HOTS.campaign_name }) + if SC2Campaign.LOTV in enabled_campaigns: + entry_rules.append({ "scope": SC2Campaign.LOTV.campaign_name }) + mission_order[SC2Campaign.EPILOGUE.campaign_name]["entry_rules"] = entry_rules + + if SC2Campaign.NCO in enabled_campaigns: + mission_order[SC2Campaign.NCO.campaign_name] = mission_order_preset("nco") + + # Resolve immediately so the layout updates are simpler + mission_order = CustomMissionOrder(mission_order).value + + # WoL requirements should count missions from Prophecy if both are enabled, and Prophecy should require a WoL mission + # There is a preset that already does this, but special-casing this way is easier to work with for other code + if wol_enabled and prophecy_enabled: + fix_wol_prophecy_entry_rules(mission_order) + + # Vanilla Shuffled is allowed to drop some slots + if mission_order_type == MissionOrder.option_vanilla_shuffled: + remove_missions(world, mission_order, mission_pools) + + # Curate final missions and goal campaigns + force_final_missions(world, mission_order, mission_order_type) + + return mission_order + + +def fix_wol_prophecy_entry_rules(mission_order: Dict[str, Dict[str, Any]]): + prophecy_name = SC2Campaign.PROPHECY.campaign_name + + # Make the mission count entry rules in WoL also count Prophecy + def fix_entry_rule(entry_rule: Dict[str, Any], local_campaign_scope: str): + # This appends Prophecy to any scope that points at the local campaign (WoL) + if "scope" in entry_rule: + if entry_rule["scope"] == local_campaign_scope: + entry_rule["scope"] = [local_campaign_scope, prophecy_name] + elif isinstance(entry_rule["scope"], list) and local_campaign_scope in entry_rule["scope"]: + entry_rule["scope"] = entry_rule["scope"] + [prophecy_name] + + for layout_dict in mission_order[SC2Campaign.WOL.campaign_name].values(): + if not isinstance(layout_dict, dict): + continue + if "entry_rules" in layout_dict: + for entry_rule in layout_dict["entry_rules"]: + fix_entry_rule(entry_rule, "..") + if "missions" in layout_dict: + for mission_dict in layout_dict["missions"]: + if "entry_rules" in mission_dict: + for entry_rule in mission_dict["entry_rules"]: + fix_entry_rule(entry_rule, "../..") + + # Make Prophecy require Artifact's second mission + mission_order[prophecy_name][prophecy_name]["entry_rules"] = [{ "scope": [f"{SC2Campaign.WOL.campaign_name}/Artifact/1"]}] + + +def force_final_missions(world: 'SC2World', mission_order: Dict[str, Dict[str, Any]], mission_order_type: int): + goal_mission: Optional[SC2Mission] = None + excluded_missions = get_excluded_missions(world) + enabled_campaigns = get_enabled_campaigns(world) + raceswap_variants = [mission for mission in SC2Mission if mission.flags & MissionFlag.RaceSwap] + # Prefer long campaigns over shorter ones and harder missions over easier ones + goal_priorities = {campaign: get_campaign_goal_priority(campaign, excluded_missions) for campaign in enabled_campaigns} + goal_level = max(goal_priorities.values()) + candidate_campaigns: List[SC2Campaign] = [campaign for campaign, goal_priority in goal_priorities.items() if goal_priority == goal_level] + candidate_campaigns.sort(key=lambda it: it.id) + + # Vanilla Shuffled & Mini Campaign get a curated final mission + if mission_order_type != MissionOrder.option_vanilla: + for goal_campaign in 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()) + # Also allow raceswaps of curated final missions, provided they're not excluded + for candidate_with_raceswaps in [mission for mission in candidate_missions if mission.flags & MissionFlag.HasRaceSwap]: + raceswap_candidates = [mission for mission in raceswap_variants if mission.map_file == candidate_with_raceswaps.map_file] + candidate_missions.extend(raceswap_candidates) + candidate_missions = [mission for mission in candidate_missions if mission not in excluded_missions] + if len(candidate_missions) == 0: + raise OptionError(f"There are no valid goal missions for campaign {goal_campaign.campaign_name}. Please exclude fewer missions.") + goal_mission = world.random.choice(candidate_missions) + else: + goal_mission = primary_goal.mission + + # The goal layout for static presets is the layout corresponding to the last key + goal_layout = list(mission_order[goal_campaign.campaign_name].keys())[-1] + goal_index = mission_order[goal_campaign.campaign_name][goal_layout]["size"] - 1 + mission_order[goal_campaign.campaign_name][goal_layout]["missions"].append({ + "index": [goal_index], + "mission_pool": [goal_mission.id] + }) + + # Remove goal status from lower priority campaigns + for campaign in enabled_campaigns: + if campaign not in candidate_campaigns: + mission_order[campaign.campaign_name]["goal"] = False + +def remove_missions(world: 'SC2World', mission_order: Dict[str, Dict[str, Any]], mission_pools: SC2MOGenMissionPools): + enabled_campaigns = get_enabled_campaigns(world) + removed_counts: Dict[SC2Campaign, Dict[str, int]] = {} + for campaign in enabled_campaigns: + # Count missing missions for each campaign individually + campaign_size = sum(layout["size"] for layout in mission_order[campaign.campaign_name].values() if type(layout) == dict) + allowed_missions = mission_pools.count_allowed_missions(campaign) + removal_count = campaign_size - allowed_missions + if removal_count > len(removal_priorities[campaign]): + raise OptionError(f"Too many missions of campaign {campaign.campaign_name} excluded, cannot fill vanilla shuffled mission order.") + for layout in removal_priorities[campaign][:removal_count]: + removed_counts.setdefault(campaign, {}).setdefault(layout, 0) + removed_counts[campaign][layout] += 1 + mission_order[campaign.campaign_name][layout]["size"] -= 1 + + # Fix mission indices & nexts + for (campaign, layouts) in removed_counts.items(): + for (layout, amount) in layouts.items(): + new_size = mission_order[campaign.campaign_name][layout]["size"] + original_size = new_size + amount + for removed_idx in range(new_size, original_size): + for mission in mission_order[campaign.campaign_name][layout]["missions"]: + if "index" in mission and removed_idx in mission["index"]: + mission["index"].remove(removed_idx) + if "next" in mission and removed_idx in mission["next"]: + mission["next"].remove(removed_idx) + + # Special cases + if SC2Campaign.WOL in removed_counts: + if "Char" in removed_counts[SC2Campaign.WOL]: + # Remove the first two mission changes that create the branching path + mission_order[SC2Campaign.WOL.campaign_name]["Char"]["missions"] = mission_order[SC2Campaign.WOL.campaign_name]["Char"]["missions"][2:] + if SC2Campaign.NCO in removed_counts: + # Remove the whole last layout if its size is 0 + if "Mission Pack 3" in removed_counts[SC2Campaign.NCO] and removed_counts[SC2Campaign.NCO]["Mission Pack 3"] == 3: + mission_order[SC2Campaign.NCO.campaign_name].pop("Mission Pack 3") + +removal_priorities: Dict[SC2Campaign, List[str]] = { + SC2Campaign.WOL: [ + "Colonist", + "Covert", + "Covert", + "Char", + "Rebellion", + "Artifact", + "Artifact", + "Rebellion" + ], + SC2Campaign.PROPHECY: [ + "Prophecy", + "Prophecy" + ], + SC2Campaign.HOTS: [ + "Umoja", + "Kaldir", + "Char", + "Zerus", + "Skygeirr Station" + ], + SC2Campaign.PROLOGUE: [ + "Prologue", + ], + SC2Campaign.LOTV: [ + "Ulnar", + "Return to Aiur", + "Aiur", + "Tal'darim", + "Purifier", + "Shakuras", + "Korhal" + ], + SC2Campaign.EPILOGUE: [ + "Epilogue", + ], + SC2Campaign.NCO: [ + "Mission Pack 3", + "Mission Pack 3", + "Mission Pack 2", + "Mission Pack 2", + "Mission Pack 1", + "Mission Pack 1", + "Mission Pack 3" + ] +} + +def make_grid(world: 'SC2World', size: int) -> Dict[str, Dict[str, Any]]: + mission_order = { + "grid": { + "display_name": "", + "type": "grid", + "size": size, + "two_start_positions": world.options.two_start_positions.value == TwoStartPositions.option_true + } + } + return mission_order + +def make_golden_path(world: 'SC2World', size: int) -> Dict[str, Dict[str, Any]]: + key_mode = world.options.key_mode.value + if key_mode == KeyMode.option_missions: + keys = "missions" + elif key_mode == KeyMode.option_questlines: + keys = "layouts" + elif key_mode == KeyMode.option_progressive_missions: + keys = "progressive_missions" + elif key_mode == KeyMode.option_progressive_questlines: + keys = "progressive_layouts" + elif key_mode == KeyMode.option_progressive_per_questline: + keys = "progressive_per_layout" + else: + keys = "none" + + mission_order = { + "golden path": { + "display_name": "", + "preset": "golden path", + "size": size, + "keys": keys, + "two_start_positions": world.options.two_start_positions.value == TwoStartPositions.option_true + } + } + return mission_order + +def make_gauntlet(size: int) -> Dict[str, Dict[str, Any]]: + mission_order = { + "gauntlet": { + "display_name": "", + "type": "gauntlet", + "size": size, + } + } + return mission_order + +def make_blitz(size: int) -> Dict[str, Dict[str, Any]]: + mission_order = { + "blitz": { + "display_name": "", + "type": "blitz", + "size": size, + } + } + return mission_order + +def make_hopscotch(world: 'SC2World', size: int) -> Dict[str, Dict[str, Any]]: + mission_order = { + "hopscotch": { + "display_name": "", + "type": "hopscotch", + "size": size, + "two_start_positions": world.options.two_start_positions.value == TwoStartPositions.option_true + } + } + return mission_order + +def create_dynamic_mission_order(world: 'SC2World', mission_order_type: int, mission_pools: SC2MOGenMissionPools) -> Dict[str, Dict[str, Any]]: + num_missions = min(mission_pools.get_allowed_mission_count(), world.options.maximum_campaign_size.value) + num_missions = max(1, num_missions) + if mission_order_type == MissionOrder.option_golden_path: + return make_golden_path(world, num_missions) + + if mission_order_type == MissionOrder.option_grid: + mission_order = make_grid(world, num_missions) + elif mission_order_type == MissionOrder.option_gauntlet: + mission_order = make_gauntlet(num_missions) + elif mission_order_type == MissionOrder.option_blitz: + mission_order = make_blitz(num_missions) + elif mission_order_type == MissionOrder.option_hopscotch: + mission_order = make_hopscotch(world, num_missions) + else: + raise ValueError("Received unknown Mission Order type") + + # Optionally add key requirements + # This only works for layout types that don't define their own entry rules (which is currently all of them) + # Golden Path handles Key Mode on its own + key_mode = world.options.key_mode.value + if key_mode == KeyMode.option_missions: + mission_order[list(mission_order.keys())[0]]["missions"] = [ + { "index": "all", "entry_rules": [{ "items": { "Key": 1 }}] }, + { "index": "entrances", "entry_rules": [] } + ] + elif key_mode == KeyMode.option_progressive_missions: + mission_order[list(mission_order.keys())[0]]["missions"] = [ + { "index": "all", "entry_rules": [{ "items": { "Progressive Key": 1 }}] }, + { "index": "entrances", "entry_rules": [] } + ] + + return mission_order diff --git a/worlds/sc2/rules.py b/worlds/sc2/rules.py new file mode 100644 index 00000000..03072594 --- /dev/null +++ b/worlds/sc2/rules.py @@ -0,0 +1,3582 @@ +from math import floor +from typing import TYPE_CHECKING, Set, Optional, Callable, Dict, Tuple, Iterable + +from BaseClasses import CollectionState, Location +from .item.item_groups import kerrigan_non_ulimates, kerrigan_logic_active_abilities +from .item.item_names import PROGRESSIVE_PROTOSS_AIR_WEAPON, PROGRESSIVE_PROTOSS_AIR_ARMOR, PROGRESSIVE_PROTOSS_SHIELDS +from .options import ( + RequiredTactics, + kerrigan_unit_available, + AllInMap, + GrantStoryTech, + GrantStoryLevels, + SpearOfAdunPassiveAbilityPresence, + SpearOfAdunPresence, + MissionOrder, + EnableMorphling, + NovaGhostOfAChanceVariant, + get_enabled_campaigns, + get_enabled_races, +) +from .item.item_tables import ( + tvx_defense_ratings, + tvz_defense_ratings, + tvx_air_defense_ratings, + kerrigan_levels, + get_full_item_list, + zvx_air_defense_ratings, + zvx_defense_ratings, + pvx_defense_ratings, + pvz_defense_ratings, + no_logic_basic_units, + advanced_basic_units, + basic_units, + upgrade_bundle_inverted_lookup, + WEAPON_ARMOR_UPGRADE_MAX_LEVEL, + soa_ultimate_ratings, + soa_energy_ratings, + terran_passive_ratings, + soa_passive_ratings, + zerg_passive_ratings, + protoss_passive_ratings, +) +from .mission_tables import SC2Race, SC2Campaign +from .item import item_groups, item_names + +if TYPE_CHECKING: + from . import SC2World + + +class SC2Logic: + def __init__(self, world: Optional["SC2World"]): + # Note: Don't store a reference to the world so we can cache this object on the world object + self.player = -1 if world is None else world.player + self.logic_level: int = world.options.required_tactics.value if world else RequiredTactics.default + self.advanced_tactics = self.logic_level != RequiredTactics.option_standard + self.take_over_ai_allies = bool(world and world.options.take_over_ai_allies) + self.kerrigan_unit_available = ( + (True if world is None else (world.options.kerrigan_presence.value in kerrigan_unit_available)) + and SC2Campaign.HOTS in get_enabled_campaigns(world) + and SC2Race.ZERG in get_enabled_races(world) + ) + self.kerrigan_levels_per_mission_completed = 0 if world is None else world.options.kerrigan_levels_per_mission_completed.value + self.kerrigan_levels_per_mission_completed_cap = -1 if world is None else world.options.kerrigan_levels_per_mission_completed_cap.value + self.kerrigan_total_level_cap = -1 if world is None else world.options.kerrigan_total_level_cap.value + self.morphling_enabled = False if world is None else (world.options.enable_morphling.value == EnableMorphling.option_true) + self.grant_story_tech = GrantStoryTech.option_no_grant if world is None else (world.options.grant_story_tech.value) + self.story_levels_granted = False if world is None else (world.options.grant_story_levels.value != GrantStoryLevels.option_disabled) + self.basic_terran_units = get_basic_units(self.logic_level, SC2Race.TERRAN) + self.basic_zerg_units = get_basic_units(self.logic_level, SC2Race.ZERG) + self.basic_protoss_units = get_basic_units(self.logic_level, SC2Race.PROTOSS) + self.spear_of_adun_presence = SpearOfAdunPresence.default if world is None else world.options.spear_of_adun_presence.value + self.spear_of_adun_passive_presence = ( + SpearOfAdunPassiveAbilityPresence.default if world is None else world.options.spear_of_adun_passive_ability_presence.value + ) + self.enabled_campaigns = get_enabled_campaigns(world) + self.mission_order = MissionOrder.default if world is None else world.options.mission_order.value + self.generic_upgrade_missions = 0 if world is None else world.options.generic_upgrade_missions.value + self.all_in_map = AllInMap.option_ground if world is None else world.options.all_in_map.value + self.nova_ghost_of_a_chance_variant = NovaGhostOfAChanceVariant.option_wol if world is None else world.options.nova_ghost_of_a_chance_variant.value + self.war_council_upgrades = True if world is None else not world.options.war_council_nerfs.value + self.base_power_rating = 2 if self.advanced_tactics else 0 + + # Must be set externally for accurate logic checking of upgrade level when generic_upgrade_missions is checked + self.total_mission_count = 1 + + # Must be set externally + self.nova_used = True + + # Conditionally set to False by the world after culling items + self.has_barracks_unit: bool = True + self.has_factory_unit: bool = True + self.has_starport_unit: bool = True + self.has_zerg_melee_unit: bool = True + self.has_zerg_ranged_unit: bool = True + self.has_zerg_air_unit: bool = True + self.has_protoss_ground_unit: bool = True + self.has_protoss_air_unit: bool = True + + self.unit_count_functions: Dict[Tuple[SC2Race, int], Callable[[CollectionState], bool]] = {} + """Cache of logic functions used by any_units logic level""" + + # Super Globals + + def is_item_placement(self, state: CollectionState) -> bool: + """ + Tells if it's item placement or item pool filter + :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) + + def get_very_hard_required_upgrade_level(self): + return 2 if self.advanced_tactics else 3 + + def weapon_armor_upgrade_count(self, upgrade_item: str, state: CollectionState) -> int: + assert upgrade_item in upgrade_bundle_inverted_lookup.keys() + count: int = 0 + if self.generic_upgrade_missions > 0: + if (not self.is_item_placement(state)) or self.logic_level == RequiredTactics.option_no_logic: + # Item pool filtering, W/A upgrades aren't items + # No Logic: Don't care about W/A in this case + return WEAPON_ARMOR_UPGRADE_MAX_LEVEL + else: + count += floor((100 / self.generic_upgrade_missions) * (state.count_group("Missions", self.player) / self.total_mission_count)) + count += state.count(upgrade_item, self.player) + count += state.count_from_list(upgrade_bundle_inverted_lookup[upgrade_item], self.player) + if upgrade_item == item_names.PROGRESSIVE_PROTOSS_SHIELDS: + count += max( + state.count(item_names.PROGRESSIVE_PROTOSS_GROUND_UPGRADE, self.player), + state.count(item_names.PROGRESSIVE_PROTOSS_AIR_UPGRADE, self.player), + ) + if upgrade_item in item_groups.protoss_generic_upgrades and state.has(item_names.QUATRO, self.player): + count += 1 + return count + + def soa_power_rating(self, state: CollectionState): + power_rating = 0 + # Spear of Adun Ultimates (Strongest) + for item, rating in soa_ultimate_ratings.items(): + if state.has(item, self.player): + power_rating += rating + break + # Spear of Adun ability that consumes energy (Strongest, then second strongest) + found_main_weapon = False + for item, rating in soa_energy_ratings.items(): + count = 1 + if item == item_names.SOA_PROGRESSIVE_PROXY_PYLON: + count = 2 + if state.has(item, self.player, count): + if not found_main_weapon: + power_rating += rating + found_main_weapon = True + else: + power_rating += rating // 2 + break + # Mass Recall (Negligible energy cost) + if state.has(item_names.SOA_MASS_RECALL, self.player): + power_rating += 2 + return power_rating + + # Global Terran + + def terran_power_rating(self, state: CollectionState) -> int: + power_score = self.base_power_rating + # Passive Score (Economic upgrades and global army upgrades) + power_score += sum((rating for item, rating in terran_passive_ratings.items() if state.has(item, self.player))) + # Spear of Adun + if self.spear_of_adun_presence == SpearOfAdunPresence.option_everywhere: + power_score += self.soa_power_rating(state) + if self.spear_of_adun_passive_presence == SpearOfAdunPassiveAbilityPresence.option_everywhere: + power_score += sum((rating for item, rating in soa_passive_ratings.items() if state.has(item, self.player))) + return power_score + + def terran_army_weapon_armor_upgrade_min_level(self, state: CollectionState) -> int: + """ + Minimum W/A upgrade level for unit classes present in the world + """ + count: int = WEAPON_ARMOR_UPGRADE_MAX_LEVEL + if self.has_barracks_unit: + count = min( + count, + self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_INFANTRY_WEAPON, state), + self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_INFANTRY_ARMOR, state), + ) + if self.has_factory_unit: + count = min( + count, + self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_VEHICLE_WEAPON, state), + self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_VEHICLE_ARMOR, state), + ) + if self.has_starport_unit: + count = min( + count, + self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_SHIP_WEAPON, state), + self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_SHIP_ARMOR, state), + ) + return count + + def terran_very_hard_mission_weapon_armor_level(self, state: CollectionState) -> bool: + return self.terran_army_weapon_armor_upgrade_min_level(state) >= self.get_very_hard_required_upgrade_level() + + # 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( + {item_names.MARINE, item_names.DOMINION_TROOPER, item_names.FIREBAT, item_names.MARAUDER, item_names.REAPER, item_names.HELLION}, + self.player, + ) or ( + self.advanced_tactics and state.has_any({item_names.GOLIATH, item_names.DIAMONDBACK, item_names.VIKING, item_names.BANSHEE}, self.player) + ) + + def terran_air(self, state: CollectionState) -> bool: + """ + Air units or drops on advanced tactics + """ + return ( + state.has_any({item_names.VIKING, item_names.WRAITH, item_names.BANSHEE, item_names.BATTLECRUISER}, self.player) + or state.has_all((item_names.VALKYRIE, item_names.VALKYRIE_FLECHETTE_MISSILES), self.player) + or state.has_all((item_names.LIBERATOR, item_names.LIBERATOR_RAID_ARTILLERY), self.player) + or ( + self.advanced_tactics + and ( + (state.has_any({item_names.HERCULES, item_names.MEDIVAC}, self.player) and self.terran_common_unit(state)) + or (state.has_all((item_names.RAVEN, item_names.RAVEN_HUNTER_SEEKER_WEAPON), self.player)) + ) + ) + ) + + def terran_air_anti_air(self, state: CollectionState) -> bool: + """ + Air-to-air + """ + return ( + state.has(item_names.VIKING, self.player) + or state.has_all({item_names.WRAITH, item_names.WRAITH_ADVANCED_LASER_TECHNOLOGY}, self.player) + or state.has_all({item_names.BATTLECRUISER, item_names.BATTLECRUISER_ATX_LASER_BATTERY}, self.player) + or ( + self.advanced_tactics + and state.has_any({item_names.WRAITH, item_names.VALKYRIE, item_names.BATTLECRUISER}, self.player) + and self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_SHIP_WEAPON, state) >= 2 + ) + ) + + def terran_any_air_unit(self, state: CollectionState) -> bool: + return state.has_any( + { + item_names.VIKING, + item_names.MEDIVAC, + item_names.RAVEN, + item_names.BANSHEE, + item_names.SCIENCE_VESSEL, + item_names.BATTLECRUISER, + item_names.WRAITH, + item_names.HERCULES, + item_names.LIBERATOR, + item_names.VALKYRIE, + item_names.SKY_FURY, + item_names.NIGHT_HAWK, + item_names.EMPERORS_GUARDIAN, + item_names.NIGHT_WOLF, + item_names.PRIDE_OF_AUGUSTRGRAD, + }, + self.player, + ) + + def terran_competent_ground_to_air(self, state: CollectionState) -> bool: + """ + Ground-to-air + """ + return ( + state.has(item_names.GOLIATH, self.player) + or ( + state.has_any({item_names.MARINE, item_names.DOMINION_TROOPER}, self.player) + and self.terran_bio_heal(state) + and self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_INFANTRY_WEAPON, state) >= 2 + ) + or self.advanced_tactics + and ( + state.has(item_names.CYCLONE, self.player) + or state.has_all((item_names.THOR, item_names.THOR_PROGRESSIVE_HIGH_IMPACT_PAYLOAD), self.player) + ) + ) + + def terran_competent_anti_air(self, state: CollectionState) -> bool: + """ + Good AA unit + """ + return self.terran_competent_ground_to_air(state) or self.terran_air_anti_air(state) + + def terran_any_anti_air(self, state: CollectionState) -> bool: + return ( + state.has_any( + ( + # Barracks + item_names.MARINE, + item_names.WAR_PIGS, + item_names.SON_OF_KORHAL, + item_names.DOMINION_TROOPER, + item_names.GHOST, + item_names.SPECTRE, + item_names.EMPERORS_SHADOW, + # Factory + item_names.GOLIATH, + item_names.SPARTAN_COMPANY, + item_names.BULWARK_COMPANY, + item_names.CYCLONE, + item_names.WIDOW_MINE, + item_names.THOR, + item_names.JOTUN, + item_names.BLACKHAMMER, + # Ships + item_names.WRAITH, + item_names.WINGED_NIGHTMARES, + item_names.NIGHT_HAWK, + item_names.VIKING, + item_names.HELS_ANGELS, + item_names.SKY_FURY, + item_names.LIBERATOR, + item_names.MIDNIGHT_RIDERS, + item_names.EMPERORS_GUARDIAN, + item_names.VALKYRIE, + item_names.BRYNHILDS, + item_names.BATTLECRUISER, + item_names.JACKSONS_REVENGE, + item_names.PRIDE_OF_AUGUSTRGRAD, + item_names.RAVEN, + # Buildings + item_names.MISSILE_TURRET, + ), + self.player, + ) + or state.has_all((item_names.REAPER, item_names.REAPER_JET_PACK_OVERDRIVE), self.player) + or state.has_all((item_names.PLANETARY_FORTRESS, item_names.PLANETARY_FORTRESS_IBIKS_TRACKING_SCANNERS), self.player) + or ( + state.has(item_names.MEDIVAC, self.player) + and state.has_any((item_names.SIEGE_TANK, item_names.SIEGE_BREAKERS, item_names.SHOCK_DIVISION), self.player) + and state.count(item_names.SIEGE_TANK_PROGRESSIVE_TRANSPORT_HOOK, self.player) >= 2 + ) + ) + + def terran_any_anti_air_or_science_vessels(self, state: CollectionState) -> bool: + return self.terran_any_anti_air(state) or state.has(item_names.SCIENCE_VESSEL, self.player) + + def terran_moderate_anti_air(self, state: CollectionState) -> bool: + return self.terran_competent_anti_air(state) or ( + state.has_any( + ( + item_names.MARINE, + item_names.DOMINION_TROOPER, + item_names.THOR, + item_names.CYCLONE, + item_names.BATTLECRUISER, + item_names.WRAITH, + item_names.VALKYRIE, + ), + self.player, + ) + or ( + state.has_all((item_names.MEDIVAC, item_names.SIEGE_TANK), self.player) + and state.count(item_names.SIEGE_TANK_PROGRESSIVE_TRANSPORT_HOOK, self.player) >= 2 + ) + or (self.advanced_tactics and state.has_any((item_names.GHOST, item_names.SPECTRE, item_names.LIBERATOR), self.player)) + ) + + def terran_basic_anti_air(self, state: CollectionState) -> bool: + """ + Basic AA to deal with few air units + """ + return ( + state.has_any( + ( + item_names.MISSILE_TURRET, + item_names.WAR_PIGS, + item_names.SPARTAN_COMPANY, + item_names.HELS_ANGELS, + item_names.WINGED_NIGHTMARES, + item_names.BRYNHILDS, + item_names.SKY_FURY, + item_names.SON_OF_KORHAL, + item_names.BULWARK_COMPANY, + ), + self.player, + ) + or self.terran_moderate_anti_air(state) + or self.advanced_tactics + and ( + state.has_any( + ( + item_names.WIDOW_MINE, + item_names.PRIDE_OF_AUGUSTRGRAD, + item_names.BLACKHAMMER, + item_names.EMPERORS_SHADOW, + item_names.EMPERORS_GUARDIAN, + item_names.NIGHT_HAWK, + ), + 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: Whether the enemy is zerg + :param air_enemy: Whether the enemy attacks with air + :return: + """ + defense_score = sum((tvx_defense_ratings[item] for item in tvx_defense_ratings if state.has(item, self.player))) + # Manned Bunker + if state.has_any({item_names.MARINE, item_names.DOMINION_TROOPER, item_names.MARAUDER}, self.player) and state.has( + item_names.BUNKER, self.player + ): + defense_score += 3 + elif zerg_enemy and state.has(item_names.FIREBAT, self.player) and state.has(item_names.BUNKER, self.player): + defense_score += 2 + # Siege Tank upgrades + if state.has_all({item_names.SIEGE_TANK, item_names.SIEGE_TANK_MAELSTROM_ROUNDS}, self.player): + defense_score += 2 + if state.has_all({item_names.SIEGE_TANK, item_names.SIEGE_TANK_GRADUATING_RANGE}, self.player): + defense_score += 1 + # Widow Mine upgrade + if state.has_all({item_names.WIDOW_MINE, item_names.WIDOW_MINE_CONCEALMENT}, self.player): + defense_score += 1 + # Viking with splash + if state.has_all({item_names.VIKING, item_names.VIKING_SHREDDER_ROUNDS}, self.player): + defense_score += 2 + + # General enemy-based rules + if zerg_enemy: + defense_score += sum((tvz_defense_ratings[item] for item in tvz_defense_ratings if state.has(item, self.player))) + if air_enemy: + # Capped at 2 + defense_score += min(sum((tvx_air_defense_ratings[item] for item in tvx_air_defense_ratings if state.has(item, self.player))), 2) + if air_enemy and zerg_enemy and state.has(item_names.VALKYRIE, self.player): + # Valkyries shred mass Mutas, the 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: + # All competent comps require anti-air + if not self.terran_competent_anti_air(state): + return False + # Infantry with Healing + infantry_weapons = self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_INFANTRY_WEAPON, state) + infantry_armor = self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_INFANTRY_ARMOR, state) + infantry = state.has_any({item_names.MARINE, item_names.DOMINION_TROOPER, item_names.MARAUDER}, self.player) + if infantry_weapons >= 2 and infantry_armor >= 1 and infantry and self.terran_bio_heal(state): + return True + # Mass Air-To-Ground + ship_weapons = self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_SHIP_WEAPON, state) + ship_armor = self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_SHIP_ARMOR, state) + if ship_weapons >= 1 and ship_armor >= 1: + air = ( + state.has_any({item_names.BANSHEE, item_names.BATTLECRUISER}, self.player) + or state.has_all({item_names.LIBERATOR, item_names.LIBERATOR_RAID_ARTILLERY}, self.player) + or state.has_all({item_names.WRAITH, item_names.WRAITH_ADVANCED_LASER_TECHNOLOGY}, self.player) + or state.has_all({item_names.VALKYRIE, item_names.VALKYRIE_FLECHETTE_MISSILES}, self.player) + and ship_weapons >= 2 + ) + if air and self.terran_mineral_dump(state): + return True + # Strong Mech + vehicle_weapons = self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_VEHICLE_WEAPON, state) + vehicle_armor = self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_VEHICLE_ARMOR, state) + if vehicle_weapons >= 1 and vehicle_armor >= 1: + strong_vehicle = state.has_any({item_names.THOR, item_names.SIEGE_TANK}, self.player) + light_frontline = state.has_any( + {item_names.MARINE, item_names.DOMINION_TROOPER, item_names.HELLION, item_names.VULTURE}, self.player + ) or state.has_all({item_names.REAPER, item_names.REAPER_RESOURCE_EFFICIENCY}, self.player) + if strong_vehicle and light_frontline: + return True + # Mech with Healing + vehicle = state.has_any({item_names.GOLIATH, item_names.WARHOUND}, self.player) + micro_gas_vehicle = self.advanced_tactics and state.has_any({item_names.DIAMONDBACK, item_names.CYCLONE}, self.player) + if self.terran_sustainable_mech_heal(state) and (vehicle or (micro_gas_vehicle and light_frontline)): + return True + return False + + def terran_mineral_dump(self, state: CollectionState) -> bool: + """ + Can build something using only minerals + """ + return ( + state.has_any({item_names.MARINE, item_names.VULTURE, item_names.HELLION, item_names.SON_OF_KORHAL}, self.player) + or state.has_all({item_names.REAPER, item_names.REAPER_RESOURCE_EFFICIENCY}, self.player) + or (self.advanced_tactics and state.has_any({item_names.PERDITION_TURRET, item_names.DEVASTATOR_TURRET}, self.player)) + ) + + def terran_beats_protoss_deathball(self, state: CollectionState) -> bool: + """ + Ability to deal with Immortals, Colossi with some air support + """ + return ( + ( + state.has_any({item_names.BANSHEE, item_names.BATTLECRUISER}, self.player) + or state.has_all({item_names.LIBERATOR, item_names.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) + ) and self.terran_army_weapon_armor_upgrade_min_level(state) >= 2 + + def marine_medic_upgrade(self, state: CollectionState) -> bool: + """ + Infantry upgrade to infantry-only no-build segments + """ + return ( + state.has_any({item_names.MARINE_COMBAT_SHIELD, item_names.MARINE_MAGRAIL_MUNITIONS, item_names.MEDIC_STABILIZER_MEDPACKS}, self.player) + or (state.count(item_names.MARINE_PROGRESSIVE_STIMPACK, self.player) >= 2 and state.has_group("Missions", self.player, 1)) + or self.advanced_tactics + and state.has(item_names.MARINE_LASER_TARGETING_SYSTEM, self.player) + ) + + def marine_medic_firebat_upgrade(self, state: CollectionState) -> bool: + return ( + self.marine_medic_upgrade(state) + or state.count(item_names.FIREBAT_PROGRESSIVE_STIMPACK, self.player) >= 2 + or state.has_any((item_names.FIREBAT_NANO_PROJECTORS, item_names.FIREBAT_JUGGERNAUT_PLATING), self.player) + ) + + def terran_bio_heal(self, state: CollectionState) -> bool: + """ + Ability to heal bio units + """ + return state.has_any({item_names.MEDIC, item_names.MEDIVAC, item_names.FIELD_RESPONSE_THETA}, self.player) or ( + self.advanced_tactics and state.has_all({item_names.RAVEN, item_names.RAVEN_BIO_MECHANICAL_REPAIR_DRONE}, self.player) + ) + + def terran_base_trasher(self, state: CollectionState) -> bool: + """ + Can attack heavily defended bases + """ + if not self.terran_competent_comp(state): + return False + if not self.terran_very_hard_mission_weapon_armor_level(state): + return False + return ( + state.has_all((item_names.SIEGE_TANK, item_names.SIEGE_TANK_JUMP_JETS), self.player) + or state.has_all({item_names.BATTLECRUISER, item_names.BATTLECRUISER_ATX_LASER_BATTERY}, self.player) + or state.has_all({item_names.LIBERATOR, item_names.LIBERATOR_RAID_ARTILLERY}, self.player) + or ( + self.advanced_tactics + and (state.has_all({item_names.RAVEN, item_names.RAVEN_HUNTER_SEEKER_WEAPON}, self.player)) + and ( + state.has_all({item_names.VIKING, item_names.VIKING_SHREDDER_ROUNDS}, self.player) + or state.has_all({item_names.BANSHEE, item_names.BANSHEE_SHOCKWAVE_MISSILE_BATTERY}, self.player) + ) + ) + ) + + def terran_mobile_detector(self, state: CollectionState) -> bool: + return state.has_any({item_names.RAVEN, item_names.SCIENCE_VESSEL, item_names.COMMAND_CENTER_SCANNER_SWEEP}, self.player) + + def can_nuke(self, state: CollectionState) -> bool: + """ + Ability to launch nukes + """ + return self.advanced_tactics and ( + state.has_any({item_names.GHOST, item_names.SPECTRE}, self.player) + or state.has_all({item_names.THOR, item_names.THOR_BUTTON_WITH_A_SKULL_ON_IT}, self.player) + ) + + def terran_sustainable_mech_heal(self, state: CollectionState) -> bool: + """ + Can heal mech units without spending resources + """ + return ( + state.has(item_names.SCIENCE_VESSEL, self.player) + or ( + state.has_any({item_names.MEDIC, item_names.FIELD_RESPONSE_THETA}, self.player) + and state.has(item_names.MEDIC_ADAPTIVE_MEDPACKS, self.player) + ) + or state.count(item_names.PROGRESSIVE_REGENERATIVE_BIO_STEEL, self.player) >= 3 + or ( + self.advanced_tactics + and ( + state.has_all({item_names.RAVEN, item_names.RAVEN_BIO_MECHANICAL_REPAIR_DRONE}, self.player) + or state.count(item_names.PROGRESSIVE_REGENERATIVE_BIO_STEEL, self.player) >= 2 + ) + ) + ) + + def terran_cliffjumper(self, state: CollectionState) -> bool: + return ( + state.has(item_names.REAPER, self.player) + or state.has_all({item_names.GOLIATH, item_names.GOLIATH_JUMP_JETS}, self.player) + or state.has_all({item_names.SIEGE_TANK, item_names.SIEGE_TANK_JUMP_JETS}, self.player) + ) + + def nova_any_nobuild_damage(self, state: CollectionState) -> bool: + return state.has_any( + ( + item_names.NOVA_C20A_CANISTER_RIFLE, + item_names.NOVA_HELLFIRE_SHOTGUN, + item_names.NOVA_PLASMA_RIFLE, + item_names.NOVA_MONOMOLECULAR_BLADE, + item_names.NOVA_BLAZEFIRE_GUNBLADE, + item_names.NOVA_PULSE_GRENADES, + item_names.NOVA_DOMINATION, + ), + self.player, + ) + + def nova_any_weapon(self, state: CollectionState) -> bool: + return state.has_any( + { + item_names.NOVA_C20A_CANISTER_RIFLE, + item_names.NOVA_HELLFIRE_SHOTGUN, + item_names.NOVA_PLASMA_RIFLE, + item_names.NOVA_MONOMOLECULAR_BLADE, + item_names.NOVA_BLAZEFIRE_GUNBLADE, + }, + self.player, + ) + + def nova_ranged_weapon(self, state: CollectionState) -> bool: + return state.has_any({item_names.NOVA_C20A_CANISTER_RIFLE, item_names.NOVA_HELLFIRE_SHOTGUN, item_names.NOVA_PLASMA_RIFLE}, self.player) + + def nova_anti_air_weapon(self, state: CollectionState) -> bool: + return state.has_any({item_names.NOVA_C20A_CANISTER_RIFLE, item_names.NOVA_PLASMA_RIFLE, item_names.NOVA_BLAZEFIRE_GUNBLADE}, self.player) + + def nova_splash(self, state: CollectionState) -> bool: + return state.has_any({item_names.NOVA_HELLFIRE_SHOTGUN, item_names.NOVA_PULSE_GRENADES}, self.player) or ( + self.advanced_tactics and state.has_any({item_names.NOVA_PLASMA_RIFLE, item_names.NOVA_MONOMOLECULAR_BLADE}, self.player) + ) + + def nova_dash(self, state: CollectionState) -> bool: + return state.has_any({item_names.NOVA_MONOMOLECULAR_BLADE, item_names.NOVA_BLINK}, self.player) + + def nova_full_stealth(self, state: CollectionState) -> bool: + return state.count(item_names.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE, self.player) >= 2 + + def nova_heal(self, state: CollectionState) -> bool: + return state.has_any({item_names.NOVA_ARMORED_SUIT_MODULE, item_names.NOVA_STIM_INFUSION}, self.player) + + def nova_escape_assist(self, state: CollectionState) -> bool: + return state.has_any({item_names.NOVA_BLINK, item_names.NOVA_HOLO_DECOY, item_names.NOVA_IONIC_FORCE_FIELD}, self.player) + + def nova_beat_stone(self, state: CollectionState) -> bool: + """ + Used for any units logic for beating Stone. Shotgun may not be possible; may need feedback. + """ + return ( + state.has_any(( + item_names.NOVA_DOMINATION, + item_names.NOVA_BLAZEFIRE_GUNBLADE, + item_names.NOVA_C20A_CANISTER_RIFLE, + ), self.player) + or (( + state.has_any(( + item_names.NOVA_PLASMA_RIFLE, + item_names.NOVA_MONOMOLECULAR_BLADE, + ), self.player) + or state.has_all(( + item_names.NOVA_HELLFIRE_SHOTGUN, + item_names.NOVA_STIM_INFUSION + ), self.player) + ) + and state.has_any(( + item_names.NOVA_JUMP_SUIT_MODULE, + item_names.NOVA_ARMORED_SUIT_MODULE, + item_names.NOVA_ENERGY_SUIT_MODULE, + ), self.player) + and state.has_any(( + item_names.NOVA_FLASHBANG_GRENADES, + item_names.NOVA_STIM_INFUSION, + item_names.NOVA_BLINK, + item_names.NOVA_IONIC_FORCE_FIELD, + ), self.player) + ) + ) + + # Global Zerg + def zerg_power_rating(self, state: CollectionState) -> int: + power_score = self.base_power_rating + # Passive Score (Economic upgrades and global army upgrades) + power_score += sum((rating for item, rating in zerg_passive_ratings.items() if state.has(item, self.player))) + # Spear of Adun + if self.spear_of_adun_presence == SpearOfAdunPresence.option_everywhere: + power_score += self.soa_power_rating(state) + if self.spear_of_adun_passive_presence == SpearOfAdunPassiveAbilityPresence.option_everywhere: + power_score += sum((rating for item, rating in soa_passive_ratings.items() if state.has(item, self.player))) + return power_score + + def zerg_defense_rating(self, state: CollectionState, zerg_enemy: bool, air_enemy: bool = True) -> int: + """ + Ability to handle defensive missions + :param state: + :param zerg_enemy: Whether the enemy is zerg + :param air_enemy: Whether the enemy attacks with air + """ + defense_score = sum((zvx_defense_ratings[item] for item in zvx_defense_ratings if state.has(item, self.player))) + # Twin Drones + if state.has(item_names.TWIN_DRONES, self.player): + if state.has(item_names.SPINE_CRAWLER, self.player): + defense_score += 1 + if state.has(item_names.SPORE_CRAWLER, self.player) and air_enemy: + defense_score += 1 + # Impaler + if self.morph_impaler(state): + defense_score += 3 + if state.has(item_names.IMPALER_SUNKEN_SPINES, self.player): + defense_score += 1 + if zerg_enemy: + defense_score += -1 + # Lurker + if self.morph_lurker(state): + defense_score += 2 + if state.has(item_names.LURKER_SEISMIC_SPINES, self.player): + defense_score += 2 + if state.has(item_names.LURKER_ADAPTED_SPINES, self.player) and not zerg_enemy: + defense_score += 1 + if zerg_enemy: + defense_score += 1 + # Brood Lord + if self.morph_brood_lord(state): + defense_score += 2 + # Corpser Roach + if state.has_all({item_names.ROACH, item_names.ROACH_CORPSER_STRAIN}, self.player): + defense_score += 1 + if zerg_enemy: + defense_score += 1 + # Igniter + if self.morph_igniter(state) and zerg_enemy: + defense_score += 2 + # Creep Tumors + if self.spread_creep(state, False): + if not zerg_enemy: + defense_score += 1 + if state.has(item_names.MALIGNANT_CREEP, self.player): + defense_score += 1 + # Infested Siege Tanks + if self.zerg_infested_tank_with_ammo(state): + defense_score += 5 + # Infested Liberators + if state.has_all((item_names.INFESTED_LIBERATOR, item_names.INFESTED_LIBERATOR_DEFENDER_MODE), self.player): + defense_score += 3 + # Bile Launcher upgrades + if state.has_all((item_names.BILE_LAUNCHER, item_names.BILE_LAUNCHER_RAPID_BOMBARMENT), self.player): + defense_score += 2 + + # General enemy-based rules + if air_enemy: + # Capped at 2 + defense_score += min(sum((zvx_air_defense_ratings[item] for item in zvx_air_defense_ratings if state.has(item, self.player))), 2) + # Advanced Tactics bumps defense rating requirements down by 2 + if self.advanced_tactics: + defense_score += 2 + return defense_score + + def zerg_army_weapon_armor_upgrade_min_level(self, state: CollectionState) -> int: + count: int = WEAPON_ARMOR_UPGRADE_MAX_LEVEL + if self.has_zerg_melee_unit: + count = min(count, self.zerg_melee_weapon_armor_upgrade_min_level(state)) + if self.has_zerg_ranged_unit: + count = min(count, self.zerg_ranged_weapon_armor_upgrade_min_level(state)) + if self.has_zerg_air_unit: + count = min(count, self.zerg_flyer_weapon_armor_upgrade_min_level(state)) + return count + + def zerg_melee_weapon_armor_upgrade_min_level(self, state: CollectionState) -> int: + return min( + self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_ZERG_MELEE_ATTACK, state), + self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_ZERG_GROUND_CARAPACE, state), + ) + + def zerg_ranged_weapon_armor_upgrade_min_level(self, state: CollectionState) -> int: + return min( + self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_ZERG_MISSILE_ATTACK, state), + self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_ZERG_GROUND_CARAPACE, state), + ) + + def zerg_flyer_weapon_armor_upgrade_min_level(self, state: CollectionState) -> int: + return min( + self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_ZERG_FLYER_ATTACK, state), + self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_ZERG_FLYER_CARAPACE, state), + ) + + def zerg_can_collect_pickup_across_gap(self, state: CollectionState) -> bool: + """Any way for zerg to get any ground unit across gaps longer than viper yoink range to collect a pickup.""" + return ( + state.has_any( + ( + item_names.NYDUS_WORM, + item_names.ECHIDNA_WORM, + item_names.OVERLORD_VENTRAL_SACS, + item_names.YGGDRASIL, + item_names.INFESTED_BANSHEE, + ), + self.player, + ) + or (self.morph_ravager(state) and state.has(item_names.RAVAGER_DEEP_TUNNEL, self.player)) + or state.has_all( + ( + item_names.INFESTED_SIEGE_TANK, + item_names.INFESTED_SIEGE_TANK_DEEP_TUNNEL, + item_names.OVERLORD_GENERATE_CREEP, + ), + self.player, + ) + or state.has_all((item_names.SWARM_QUEEN_DEEP_TUNNEL, item_names.OVERLORD_OVERSEER_ASPECT), self.player) # Deep tunnel to a creep tumor + ) + + def zerg_has_infested_scv(self, state: CollectionState) -> bool: + return ( + state.has_any(( + item_names.INFESTED_MARINE, + item_names.INFESTED_BUNKER, + item_names.INFESTED_DIAMONDBACK, + item_names.INFESTED_SIEGE_TANK, + item_names.INFESTED_BANSHEE, + item_names.BULLFROG, + item_names.INFESTED_LIBERATOR, + item_names.INFESTED_MISSILE_TURRET, + ), self.player) + ) + + def zerg_very_hard_mission_weapon_armor_level(self, state: CollectionState) -> bool: + return self.zerg_army_weapon_armor_upgrade_min_level(state) >= self.get_very_hard_required_upgrade_level() + + 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({item_names.HYDRALISK, item_names.MUTALISK, item_names.CORRUPTOR, item_names.BROOD_QUEEN}, self.player) or ( + self.advanced_tactics and state.has(item_names.INFESTOR, self.player) + ) + + def zerg_moderate_anti_air(self, state: CollectionState) -> bool: + return ( + self.zerg_competent_anti_air(state) + or self.zerg_basic_air_to_air(state) + or ( + state.has(item_names.SWARM_QUEEN, self.player) + or state.has_all({item_names.SWARM_HOST, item_names.SWARM_HOST_PRESSURIZED_GLANDS}, self.player) + or (self.spread_creep(state, True) and state.has(item_names.INFESTED_BUNKER, self.player)) + ) + or (self.advanced_tactics and state.has(item_names.INFESTED_MARINE, self.player)) + ) + + def zerg_kerrigan_or_any_anti_air(self, state: CollectionState) -> bool: + return self.kerrigan_unit_available or self.zerg_any_anti_air(state) + + def zerg_any_anti_air(self, state: CollectionState) -> bool: + return ( + state.has_any( + ( + item_names.HYDRALISK, + item_names.SWARM_QUEEN, + item_names.BROOD_QUEEN, + item_names.MUTALISK, + item_names.CORRUPTOR, + item_names.SCOURGE, + item_names.INFESTOR, + item_names.INFESTED_MARINE, + item_names.INFESTED_LIBERATOR, + item_names.SPORE_CRAWLER, + item_names.INFESTED_MISSILE_TURRET, + item_names.INFESTED_BUNKER, + item_names.HUNTER_KILLERS, + item_names.CAUSTIC_HORRORS, + ), + self.player, + ) + or state.has_all((item_names.SWARM_HOST, item_names.SWARM_HOST_PRESSURIZED_GLANDS), self.player) + or state.has_all((item_names.ABERRATION, item_names.ABERRATION_PROGRESSIVE_BANELING_LAUNCH), self.player) + or state.has_all((item_names.INFESTED_DIAMONDBACK, item_names.INFESTED_DIAMONDBACK_PROGRESSIVE_FUNGAL_SNARE), self.player) + or self.morph_ravager(state) + or self.morph_viper(state) + or self.morph_devourer(state) + or (self.morph_guardian(state) and state.has(item_names.GUARDIAN_PRIMAL_ADAPTATION, self.player)) + ) + + def zerg_basic_anti_air(self, state: CollectionState) -> bool: + return self.zerg_basic_kerriganless_anti_air(state) or self.kerrigan_unit_available + + def zerg_basic_kerriganless_anti_air(self, state: CollectionState) -> bool: + return ( + self.zerg_moderate_anti_air(state) + or state.has_any((item_names.HUNTER_KILLERS, item_names.CAUSTIC_HORRORS), self.player) + or (self.advanced_tactics and state.has_any({item_names.SPORE_CRAWLER, item_names.INFESTED_MISSILE_TURRET}, self.player)) + ) + + def zerg_basic_air_to_air(self, state: CollectionState) -> bool: + return ( + state.has_any( + {item_names.MUTALISK, item_names.CORRUPTOR, item_names.BROOD_QUEEN, item_names.SCOURGE, item_names.INFESTED_LIBERATOR}, self.player + ) + or self.morph_devourer(state) + or self.morph_viper(state) + or (self.morph_guardian(state) and state.has(item_names.GUARDIAN_PRIMAL_ADAPTATION, self.player)) + ) + + def zerg_basic_air_to_ground(self, state: CollectionState) -> bool: + return ( + state.has_any({item_names.MUTALISK, item_names.INFESTED_BANSHEE}, self.player) + or self.morph_guardian(state) + or self.morph_brood_lord(state) + or (self.morph_devourer(state) and state.has(item_names.DEVOURER_PRESCIENT_SPORES, self.player)) + ) + + def zerg_versatile_air(self, state: CollectionState) -> bool: + return self.zerg_basic_air_to_air(state) and self.zerg_basic_air_to_ground(state) + + def zerg_infested_tank_with_ammo(self, state: CollectionState) -> bool: + return state.has(item_names.INFESTED_SIEGE_TANK, self.player) and ( + state.has_all({item_names.INFESTOR, item_names.INFESTOR_INFESTED_TERRAN}, self.player) + or state.has(item_names.INFESTED_BUNKER, self.player) + or (self.advanced_tactics and state.has(item_names.INFESTED_MARINE, self.player)) + or state.count(item_names.INFESTED_SIEGE_TANK_PROGRESSIVE_AUTOMATED_MITOSIS, self.player) >= (1 if self.advanced_tactics else 2) + ) + + def morph_baneling(self, state: CollectionState) -> bool: + return (state.has(item_names.ZERGLING, self.player) or self.morphling_enabled) and state.has(item_names.ZERGLING_BANELING_ASPECT, self.player) + + def morph_ravager(self, state: CollectionState) -> bool: + return (state.has(item_names.ROACH, self.player) or self.morphling_enabled) and state.has(item_names.ROACH_RAVAGER_ASPECT, self.player) + + def morph_brood_lord(self, state: CollectionState) -> bool: + return (state.has_any({item_names.MUTALISK, item_names.CORRUPTOR}, self.player) or self.morphling_enabled) and state.has( + item_names.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT, self.player + ) + + def morph_guardian(self, state: CollectionState) -> bool: + return (state.has_any({item_names.MUTALISK, item_names.CORRUPTOR}, self.player) or self.morphling_enabled) and state.has( + item_names.MUTALISK_CORRUPTOR_GUARDIAN_ASPECT, self.player + ) + + def morph_viper(self, state: CollectionState) -> bool: + return (state.has_any({item_names.MUTALISK, item_names.CORRUPTOR}, self.player) or self.morphling_enabled) and state.has( + item_names.MUTALISK_CORRUPTOR_VIPER_ASPECT, self.player + ) + + def morph_devourer(self, state: CollectionState) -> bool: + return (state.has_any({item_names.MUTALISK, item_names.CORRUPTOR}, self.player) or self.morphling_enabled) and state.has( + item_names.MUTALISK_CORRUPTOR_DEVOURER_ASPECT, self.player + ) + + def morph_impaler(self, state: CollectionState) -> bool: + return (state.has(item_names.HYDRALISK, self.player) or self.morphling_enabled) and state.has( + item_names.HYDRALISK_IMPALER_ASPECT, self.player + ) + + def morph_lurker(self, state: CollectionState) -> bool: + return (state.has(item_names.HYDRALISK, self.player) or self.morphling_enabled) and state.has(item_names.HYDRALISK_LURKER_ASPECT, self.player) + + def morph_impaler_or_lurker(self, state: CollectionState) -> bool: + return self.morph_impaler(state) or self.morph_lurker(state) + + def morph_igniter(self, state: CollectionState) -> bool: + return (state.has(item_names.ROACH, self.player) or self.morphling_enabled) and state.has(item_names.ROACH_PRIMAL_IGNITER_ASPECT, self.player) + + def morph_tyrannozor(self, state: CollectionState) -> bool: + return state.has(item_names.ULTRALISK_TYRANNOZOR_ASPECT, self.player) and ( + state.has(item_names.ULTRALISK, self.player) or self.morphling_enabled + ) + + def zerg_competent_comp(self, state: CollectionState) -> bool: + if self.zerg_army_weapon_armor_upgrade_min_level(state) < 2: + return False + advanced = self.advanced_tactics + core_unit = state.has_any( + {item_names.ROACH, item_names.ABERRATION, item_names.ZERGLING, item_names.INFESTED_DIAMONDBACK}, self.player + ) or self.morph_igniter(state) + support_unit = ( + state.has_any({item_names.SWARM_QUEEN, item_names.HYDRALISK, item_names.INFESTED_BANSHEE}, self.player) + or self.morph_brood_lord(state) + or state.has_all((item_names.MUTALISK, item_names.MUTALISK_SEVERING_GLAIVE, item_names.MUTALISK_VICIOUS_GLAIVE), self.player) + or advanced + and (state.has_any({item_names.INFESTOR, item_names.DEFILER}, self.player) or self.morph_viper(state)) + ) + if core_unit and support_unit: + return True + vespene_unit = ( + state.has_any({item_names.ULTRALISK, item_names.ABERRATION}, self.player) + or ( + self.morph_guardian(state) + and state.has_any( + (item_names.GUARDIAN_SORONAN_ACID, item_names.GUARDIAN_EXPLOSIVE_SPORES, item_names.GUARDIAN_PRIMORDIAL_FURY), self.player + ) + ) + or advanced + and self.morph_viper(state) + ) + return vespene_unit and state.has_any({item_names.ZERGLING, item_names.SWARM_QUEEN}, self.player) + + def zerg_common_unit_basic_aa(self, state: CollectionState) -> bool: + return self.zerg_common_unit(state) and self.zerg_basic_anti_air(state) + + def zerg_common_unit_competent_aa(self, state: CollectionState) -> bool: + return self.zerg_common_unit(state) and self.zerg_competent_anti_air(state) + + def zerg_competent_comp_basic_aa(self, state: CollectionState) -> bool: + return self.zerg_competent_comp(state) and self.zerg_basic_anti_air(state) + + def zerg_competent_comp_competent_aa(self, state: CollectionState) -> bool: + return self.zerg_competent_comp(state) and self.zerg_competent_anti_air(state) + + def spread_creep(self, state: CollectionState, free_creep_tumor=True) -> bool: + return (self.advanced_tactics and free_creep_tumor) or state.has_any( + {item_names.SWARM_QUEEN, item_names.OVERLORD_OVERSEER_ASPECT}, self.player + ) + + def zerg_mineral_dump(self, state: CollectionState) -> bool: + return ( + state.has_any({item_names.ZERGLING, item_names.PYGALISK, item_names.INFESTED_BUNKER}, self.player) + or state.has_all({item_names.SWARM_QUEEN, item_names.SWARM_QUEEN_RESOURCE_EFFICIENCY}, self.player) + or (self.advanced_tactics and self.spread_creep(state) and state.has(item_names.SPINE_CRAWLER, self.player)) + ) + + def zerg_big_monsters(self, state: CollectionState) -> bool: + """ + Durable units with some capacity for damage + """ + return ( + self.morph_tyrannozor(state) + or state.has_any((item_names.ABERRATION, item_names.ULTRALISK), self.player) + or (self.spread_creep(state, False) and state.has(item_names.INFESTED_BUNKER, self.player)) + ) + + def zerg_base_buster(self, state: CollectionState) -> bool: + """Powerful and sustainable zerg anti-ground for busting big bases; anti-air not included""" + if not self.zerg_competent_comp(state): + return False + return ( + ( + self.zerg_melee_weapon_armor_upgrade_min_level(state) >= self.get_very_hard_required_upgrade_level() + and ( + self.morph_tyrannozor(state) + or ( + state.has(item_names.ULTRALISK, self.player) + and state.has_any((item_names.ULTRALISK_TORRASQUE_STRAIN, item_names.ULTRALISK_CHITINOUS_PLATING), self.player) + ) + or (self.morph_baneling(state) and state.has(item_names.BANELING_SPLITTER_STRAIN, self.player)) + ) + and state.has(item_names.SWARM_QUEEN, self.player) # Healing to sustain the frontline + ) + or ( + self.zerg_ranged_weapon_armor_upgrade_min_level(state) >= self.get_very_hard_required_upgrade_level() + and ( + self.morph_impaler(state) + or self.morph_lurker(state) + and state.has_all((item_names.LURKER_SEISMIC_SPINES, item_names.LURKER_ADAPTED_SPINES), self.player) + or state.has_all( + ( + item_names.ROACH, + item_names.ROACH_CORPSER_STRAIN, + item_names.ROACH_ADAPTIVE_PLATING, + item_names.ROACH_GLIAL_RECONSTITUTION, + ), + self.player, + ) + or self.morph_igniter(state) + and state.has(item_names.PRIMAL_IGNITER_PRIMAL_TENACITY, self.player) + or state.has_all((item_names.INFESTOR, item_names.INFESTOR_INFESTED_TERRAN), self.player) + or self.spread_creep(state, False) + and state.has(item_names.INFESTED_BUNKER, self.player) + or self.zerg_infested_tank_with_ammo(state) + # Highly-upgraded swarm hosts may also work, but that would require promoting many upgrades to progression + ) + ) + or ( + self.zerg_flyer_weapon_armor_upgrade_min_level(state) >= self.get_very_hard_required_upgrade_level() + and ( + self.morph_brood_lord(state) + or self.morph_guardian(state) + and state.has_all((item_names.GUARDIAN_PROPELLANT_SACS, item_names.GUARDIAN_SORONAN_ACID), self.player) + or state.has_all((item_names.INFESTED_BANSHEE, item_names.INFESTED_BANSHEE_FLESHFUSED_TARGETING_OPTICS), self.player) + # Highly-upgraded anti-ground devourers would also be good + ) + ) + ) + + def zergling_hydra_roach_start(self, state: CollectionState): + """ + Created mainly for engine of destruction start, but works for other missions with no-build starts. + """ + return state.has_any( + { + item_names.ZERGLING_ADRENAL_OVERLOAD, + item_names.HYDRALISK_FRENZY, + item_names.ROACH_HYDRIODIC_BILE, + item_names.ZERGLING_RAPTOR_STRAIN, + item_names.ROACH_CORPSER_STRAIN, + }, + self.player, + ) + + def kerrigan_levels(self, state: CollectionState, target: int, story_levels_available=True) -> bool: + if (story_levels_available and 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 basic_kerrigan(self, state: CollectionState) -> bool: + # One active ability that can be used to defeat enemies directly on Standard + if not state.has_any( + ( + item_names.KERRIGAN_LEAPING_STRIKE, + item_names.KERRIGAN_KINETIC_BLAST, + item_names.KERRIGAN_SPAWN_BANELINGS, + item_names.KERRIGAN_PSIONIC_SHIFT, + item_names.KERRIGAN_CRUSHING_GRIP, + ), + self.player, + ): + return False + # Two non-ultimate abilities + count = 0 + for item in kerrigan_non_ulimates: + 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_logic_active_abilities, self.player): + count += 1 + return count >= 2 + + # Global Protoss + def protoss_power_rating(self, state: CollectionState) -> int: + power_score = self.base_power_rating + # War Council Upgrades (all units are improved) + if self.war_council_upgrades: + power_score += 3 + # Passive Score (Economic upgrades and global army upgrades) + power_score += sum((rating for item, rating in protoss_passive_ratings.items() if state.has(item, self.player))) + # Spear of Adun + if self.spear_of_adun_presence in (SpearOfAdunPresence.option_everywhere, SpearOfAdunPresence.option_protoss): + power_score += self.soa_power_rating(state) + if self.spear_of_adun_passive_presence in (SpearOfAdunPassiveAbilityPresence.option_everywhere, SpearOfAdunPresence.option_protoss): + power_score += sum((rating for item, rating in soa_passive_ratings.items() if state.has(item, self.player))) + return power_score + + def protoss_army_weapon_armor_upgrade_min_level(self, state: CollectionState) -> int: + count: int = WEAPON_ARMOR_UPGRADE_MAX_LEVEL + 1 # +1 for Quatro + if self.has_protoss_ground_unit: + count = min( + count, + self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_PROTOSS_GROUND_WEAPON, state), + self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_PROTOSS_GROUND_ARMOR, state), + ) + if self.has_protoss_air_unit: + count = min( + count, + self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_PROTOSS_AIR_WEAPON, state), + self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_PROTOSS_AIR_ARMOR, state), + ) + if self.has_protoss_ground_unit or self.has_protoss_air_unit: + count = min( + count, + self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_PROTOSS_SHIELDS, state), + ) + return count + + def protoss_very_hard_mission_weapon_armor_level(self, state: CollectionState) -> bool: + return self.protoss_army_weapon_armor_upgrade_min_level(state) >= self.get_very_hard_required_upgrade_level() + + def protoss_defense_rating(self, state: CollectionState, zerg_enemy: bool) -> int: + """ + Ability to handle defensive missions + :param state: + :param zerg_enemy: Whether the enemy is zerg + """ + defense_score = sum((pvx_defense_ratings[item] for item in pvx_defense_ratings if state.has(item, self.player))) + # Vanguard + rapid fire + if state.has_all((item_names.VANGUARD, item_names.VANGUARD_RAPIDFIRE_CANNON), self.player): + defense_score += 1 + # Fire Colossus + if state.has_all((item_names.COLOSSUS, item_names.COLOSSUS_FIRE_LANCE), self.player): + defense_score += 2 + if zerg_enemy: + defense_score += 2 + if ( + state.has_any((item_names.PHOTON_CANNON, item_names.KHAYDARIN_MONOLITH, item_names.NEXUS_OVERCHARGE), self.player) + and state.has(item_names.SHIELD_BATTERY, self.player) + ): + defense_score += 2 + + # No anti-air defense dict here, use an existing logic rule instead + if zerg_enemy: + defense_score += sum((pvz_defense_ratings[item] for item in pvz_defense_ratings if state.has(item, self.player))) + # Advanced Tactics bumps defense rating requirements down by 2 + if self.advanced_tactics: + defense_score += 2 + return defense_score + + def protoss_common_unit(self, state: CollectionState) -> bool: + return state.has_any(self.basic_protoss_units, self.player) + + def protoss_any_gap_transport(self, state: CollectionState) -> bool: + """Can get ground units across large gaps, larger than blink range""" + return ( + state.has_any( + ( + item_names.WARP_PRISM, + item_names.ARBITER, + ), + self.player, + ) + or state.has(item_names.SOA_PROGRESSIVE_PROXY_PYLON, self.player, count=2) + or state.has_all((item_names.MISTWING, item_names.MISTWING_PILOT), self.player) + or ( + state.has(item_names.SOA_PROGRESSIVE_PROXY_PYLON, self.player) + and ( + state.has_any(item_groups.gateway_units + [item_names.ELDER_PROBES, item_names.PROBE_WARPIN], self.player) + or (state.has(item_names.WARP_HARMONIZATION, self.player) and state.has_any(item_groups.protoss_ground_wa, self.player)) + ) + ) + ) + + def protoss_any_anti_air_unit_or_soa_any_protoss(self, state: CollectionState) -> bool: + return self.protoss_any_anti_air_unit(state) or ( + self.spear_of_adun_presence in (SpearOfAdunPresence.option_everywhere, SpearOfAdunPresence.option_protoss) + and self.protoss_any_anti_air_soa(state) + ) + + def protoss_any_anti_air_unit_or_soa(self, state: CollectionState) -> bool: + return self.protoss_any_anti_air_unit(state) or self.protoss_any_anti_air_soa(state) + + def protoss_any_anti_air_soa(self, state: CollectionState) -> bool: + return ( + state.has_any( + ( + item_names.SOA_ORBITAL_STRIKE, + item_names.SOA_SOLAR_LANCE, + item_names.SOA_SOLAR_BOMBARDMENT, + item_names.SOA_PURIFIER_BEAM, + item_names.SOA_PYLON_OVERCHARGE, + ), + self.player, + ) + or state.has(item_names.SOA_PROGRESSIVE_PROXY_PYLON, self.player, 2) # Warp-In Reinforcements + ) + + def protoss_any_anti_air_unit(self, state: CollectionState) -> bool: + return ( + state.has_any( + ( + # Gateway + item_names.STALKER, + item_names.SLAYER, + item_names.INSTIGATOR, + item_names.DRAGOON, + item_names.ADEPT, + item_names.SENTRY, + item_names.ENERGIZER, + item_names.HIGH_TEMPLAR, + item_names.SIGNIFIER, + item_names.ASCENDANT, + item_names.DARK_ARCHON, + # Robo + item_names.ANNIHILATOR, + # Stargate + item_names.PHOENIX, + item_names.MIRAGE, + item_names.CORSAIR, + item_names.SCOUT, + item_names.MISTWING, + item_names.CALADRIUS, + item_names.OPPRESSOR, + item_names.ARBITER, + item_names.VOID_RAY, + item_names.DESTROYER, + item_names.PULSAR, + item_names.CARRIER, + item_names.SKYLORD, + item_names.TEMPEST, + item_names.MOTHERSHIP, + # Buildings + item_names.NEXUS_OVERCHARGE, + item_names.PHOTON_CANNON, + item_names.KHAYDARIN_MONOLITH, + ), + self.player, + ) + or state.has_all((item_names.SUPPLICANT, item_names.SUPPLICANT_ZENITH_PITCH), self.player) + or state.has_all((item_names.WARP_PRISM, item_names.WARP_PRISM_PHASE_BLASTER), self.player) + or state.has_all((item_names.WRATHWALKER, item_names.WRATHWALKER_AERIAL_TRACKING), self.player) + or state.has_all((item_names.DISRUPTOR, item_names.DISRUPTOR_PERFECTED_POWER), self.player) + or state.has_all((item_names.IMMORTAL, item_names.IMMORTAL_ANNIHILATOR_ADVANCED_TARGETING), self.player) + or state.has_all((item_names.SKIRMISHER, item_names.SKIRMISHER_PEER_CONTEMPT), self.player) + or state.has_all((item_names.TRIREME, item_names.TRIREME_SOLAR_BEAM), self.player) + or ( + state.has(item_names.DARK_TEMPLAR, self.player) + and state.has_any((item_names.DARK_TEMPLAR_DARK_ARCHON_MELD, item_names.DARK_TEMPLAR_ARCHON_MERGE), self.player) + ) + ) + + def protoss_basic_anti_air(self, state: CollectionState) -> bool: + return ( + self.protoss_competent_anti_air(state) + or state.has_any( + { + item_names.PHOENIX, + item_names.MIRAGE, + item_names.CORSAIR, + item_names.CARRIER, + item_names.SKYLORD, + item_names.SCOUT, + item_names.DARK_ARCHON, + item_names.MOTHERSHIP, + item_names.MISTWING, + item_names.CALADRIUS, + item_names.OPPRESSOR, + item_names.PULSAR, + item_names.DRAGOON, + }, + self.player, + ) + or state.has_all({item_names.TRIREME, item_names.TRIREME_SOLAR_BEAM}, self.player) + or state.has_all({item_names.WRATHWALKER, item_names.WRATHWALKER_AERIAL_TRACKING}, self.player) + or state.has_all({item_names.WARP_PRISM, item_names.WARP_PRISM_PHASE_BLASTER}, self.player) + or self.advanced_tactics + and state.has_any({item_names.HIGH_TEMPLAR, item_names.SIGNIFIER, item_names.SENTRY, item_names.ENERGIZER}, self.player) + or self.protoss_can_merge_archon(state) + or self.protoss_can_merge_dark_archon(state) + ) + + def protoss_anti_armor_anti_air(self, state: CollectionState) -> bool: + return ( + self.protoss_competent_anti_air(state) + or state.has_any((item_names.SCOUT, item_names.MISTWING, item_names.DRAGOON), self.player) + or ( + state.has_any({item_names.IMMORTAL, item_names.ANNIHILATOR}, self.player) + and state.has(item_names.IMMORTAL_ANNIHILATOR_ADVANCED_TARGETING, self.player) + ) + or state.has_all({item_names.WRATHWALKER, item_names.WRATHWALKER_AERIAL_TRACKING}, self.player) + ) + + def protoss_anti_light_anti_air(self, state: CollectionState) -> bool: + return ( + self.protoss_competent_anti_air(state) + or state.has_any( + { + item_names.PHOENIX, + item_names.MIRAGE, + item_names.CORSAIR, + item_names.CARRIER, + }, + self.player, + ) + or state.has_all((item_names.SKIRMISHER, item_names.SKIRMISHER_PEER_CONTEMPT), self.player) + ) + + def protoss_moderate_anti_air(self, state: CollectionState) -> bool: + return ( + self.protoss_competent_anti_air(state) + or self.protoss_anti_light_anti_air(state) + or self.protoss_anti_armor_anti_air(state) + or state.has(item_names.SKYLORD, self.player) + ) + + def protoss_common_unit_basic_aa(self, state: CollectionState) -> bool: + return self.protoss_common_unit(state) and self.protoss_basic_anti_air(state) + + def protoss_common_unit_anti_light_air(self, state: CollectionState) -> bool: + return self.protoss_common_unit(state) and self.protoss_anti_light_anti_air(state) + + def protoss_common_unit_anti_armor_air(self, state: CollectionState) -> bool: + return self.protoss_common_unit(state) and self.protoss_anti_armor_anti_air(state) + + def protoss_competent_anti_air(self, state: CollectionState) -> bool: + return ( + state.has_any( + { + item_names.STALKER, + item_names.SLAYER, + item_names.INSTIGATOR, + item_names.ADEPT, + item_names.VOID_RAY, + item_names.DESTROYER, + item_names.TEMPEST, + item_names.CALADRIUS, + }, + self.player, + ) + or ( + ( + state.has_any( + { + item_names.PHOENIX, + item_names.MIRAGE, + item_names.CORSAIR, + item_names.CARRIER, + }, + self.player, + ) + or state.has_all((item_names.SKIRMISHER, item_names.SKIRMISHER_PEER_CONTEMPT), self.player) + ) + and ( + state.has_any((item_names.SCOUT, item_names.MISTWING, item_names.DRAGOON), self.player) + or state.has_all({item_names.WRATHWALKER, item_names.WRATHWALKER_AERIAL_TRACKING}, self.player) + or ( + state.has_any({item_names.IMMORTAL, item_names.ANNIHILATOR}, self.player) + and state.has(item_names.IMMORTAL_ANNIHILATOR_ADVANCED_TARGETING, self.player) + ) + ) + ) + or ( + self.advanced_tactics + and state.has_any({item_names.IMMORTAL, item_names.ANNIHILATOR}, self.player) + and state.has(item_names.IMMORTAL_ANNIHILATOR_ADVANCED_TARGETING, self.player) + ) + ) + + def protoss_has_blink(self, state: CollectionState) -> bool: + return ( + state.has_any({item_names.STALKER, item_names.INSTIGATOR}, self.player) + or state.has_all({item_names.SLAYER, item_names.SLAYER_PHASE_BLINK}, self.player) + or ( + state.has(item_names.DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_BLINK, self.player) + and state.has_any({item_names.DARK_TEMPLAR, item_names.BLOOD_HUNTER, item_names.AVENGER}, self.player) + ) + ) + + def protoss_fleet(self, state: CollectionState) -> bool: + return ( + ( + state.has_any( + { + item_names.CARRIER, + item_names.SKYLORD, + item_names.TEMPEST, + item_names.VOID_RAY, + item_names.DESTROYER, + }, + self.player, + ) + ) + or ( + state.has_all((item_names.TRIREME, item_names.TRIREME_SOLAR_BEAM), self.player) + and ( + state.has_any((item_names.PHOENIX, item_names.MIRAGE, item_names.CORSAIR), self.player) + or state.has_all((item_names.SKIRMISHER, item_names.SKIRMISHER_PEER_CONTEMPT), self.player) + ) + ) + and self.weapon_armor_upgrade_count(PROGRESSIVE_PROTOSS_AIR_WEAPON, state) >= 2 + and self.weapon_armor_upgrade_count(PROGRESSIVE_PROTOSS_AIR_ARMOR, state) >= 2 + and self.weapon_armor_upgrade_count(PROGRESSIVE_PROTOSS_SHIELDS, state) >= 2 + ) + + def protoss_hybrid_counter(self, state: CollectionState) -> bool: + """ + Ground Hybrids + """ + return ( + state.has_any( + { + item_names.ANNIHILATOR, + item_names.ASCENDANT, + item_names.TEMPEST, + item_names.CARRIER, + item_names.TRIREME, + item_names.VOID_RAY, + item_names.WRATHWALKER, + }, + self.player, + ) + or state.has_all((item_names.VANGUARD, item_names.VANGUARD_FUSION_MORTARS), self.player) + or ( + (state.has(item_names.IMMORTAL, self.player) or self.advanced_tactics) + and (state.has_any({item_names.STALKER, item_names.DRAGOON, item_names.ADEPT, item_names.INSTIGATOR, item_names.SLAYER}, self.player)) + ) + or (self.advanced_tactics and state.has_all((item_names.OPPRESSOR, item_names.OPPRESSOR_VULCAN_BLASTER), self.player)) + ) + + def protoss_basic_splash(self, state: CollectionState) -> bool: + return ( + state.has_any(( + item_names.COLOSSUS, + item_names.VANGUARD, + item_names.HIGH_TEMPLAR, + item_names.SIGNIFIER, + item_names.REAVER, + item_names.ASCENDANT, + item_names.DAWNBRINGER, + ), self.player) + or state.has_all((item_names.ZEALOT, item_names.ZEALOT_WHIRLWIND), self.player) + or ( + state.has_all( + (item_names.DARK_TEMPLAR, item_names.DARK_TEMPLAR_LESSER_SHADOW_FURY, item_names.DARK_TEMPLAR_GREATER_SHADOW_FURY), self.player + ) + ) + or ( + state.has(item_names.DESTROYER, self.player) + and ( + state.has_any(( + item_names.DESTROYER_REFORGED_BLOODSHARD_CORE, + item_names.DESTROYER_RESOURCE_EFFICIENCY, + ), self.player) + ) + ) + ) + + def protoss_static_defense(self, state: CollectionState) -> bool: + return state.has_any({item_names.PHOTON_CANNON, item_names.KHAYDARIN_MONOLITH}, self.player) + + def protoss_can_merge_archon(self, state: CollectionState) -> bool: + return ( + state.has_any({item_names.HIGH_TEMPLAR, item_names.SIGNIFIER}, self.player) + or state.has_all({item_names.ASCENDANT, item_names.ASCENDANT_ARCHON_MERGE}, self.player) + or state.has_all({item_names.DARK_TEMPLAR, item_names.DARK_TEMPLAR_ARCHON_MERGE}, self.player) + ) + + def protoss_can_merge_dark_archon(self, state: CollectionState) -> bool: + return state.has(item_names.DARK_ARCHON, self.player) or state.has_all( + {item_names.DARK_TEMPLAR, item_names.DARK_TEMPLAR_DARK_ARCHON_MELD}, self.player + ) + + def protoss_competent_comp(self, state: CollectionState) -> bool: + if not self.protoss_competent_anti_air(state): + return False + if self.protoss_fleet(state) and self.protoss_mineral_dump(state): + return True + if self.protoss_deathball(state): + return True + core_unit: bool = state.has_any( + ( + item_names.ZEALOT, + item_names.CENTURION, + item_names.SENTINEL, + item_names.STALKER, + item_names.INSTIGATOR, + item_names.SLAYER, + item_names.ADEPT, + ), + self.player, + ) + support_unit: bool = ( + state.has_any( + ( + item_names.SENTRY, + item_names.ENERGIZER, + item_names.IMMORTAL, + item_names.VANGUARD, + item_names.COLOSSUS, + item_names.REAVER, + item_names.VOID_RAY, + item_names.PHOENIX, + item_names.CORSAIR, + ), + self.player, + ) + or state.has_all((item_names.MIRAGE, item_names.MIRAGE_GRAVITON_BEAM), self.player) + or state.has_all( + (item_names.DARK_TEMPLAR, item_names.DARK_TEMPLAR_LESSER_SHADOW_FURY, item_names.DARK_TEMPLAR_GREATER_SHADOW_FURY), self.player + ) + or ( + self.advanced_tactics + and ( + state.has_any( + ( + item_names.HIGH_TEMPLAR, + item_names.SIGNIFIER, + item_names.ASCENDANT, + item_names.ANNIHILATOR, + item_names.WRATHWALKER, + item_names.SKIRMISHER, + item_names.ARBITER, + ), + self.player, + ) + ) + ) + ) + if core_unit and support_unit: + return True + return False + + def protoss_deathball(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) + and self.protoss_army_weapon_armor_upgrade_min_level(state) >= 2 + ) + + def protoss_heal(self, state: CollectionState) -> bool: + return state.has_any((item_names.SENTRY, item_names.SHIELD_BATTERY, item_names.RECONSTRUCTION_BEAM), self.player) or state.has_all( + (item_names.CARRIER, item_names.CARRIER_REPAIR_DRONES), self.player + ) + + def protoss_mineral_dump(self, state: CollectionState) -> bool: + return ( + state.has_any((item_names.ZEALOT, item_names.SENTINEL, item_names.PHOTON_CANNON), self.player) + or state.has_all((item_names.CENTURION, item_names.CENTURION_RESOURCE_EFFICIENCY), self.player) + or self.advanced_tactics + and state.has_any((item_names.SUPPLICANT, item_names.SHIELD_BATTERY), self.player) + ) + + def zealot_sentry_slayer_start(self, state: CollectionState): + """ + Created mainly for engine of destruction start, but works for other missions with no-build starts. + """ + return state.has_any( + { + item_names.ZEALOT_WHIRLWIND, + item_names.SENTRY_DOUBLE_SHIELD_RECHARGE, + item_names.SLAYER_PHASE_BLINK, + item_names.STALKER_INSTIGATOR_SLAYER_DISINTEGRATING_PARTICLES, + item_names.STALKER_INSTIGATOR_SLAYER_PARTICLE_REFLECTION, + }, + self.player, + ) + + # Mission-specific rules + def ghost_of_a_chance_requirement(self, state: CollectionState) -> bool: + return ( + self.grant_story_tech == GrantStoryTech.option_grant + or self.nova_ghost_of_a_chance_variant == NovaGhostOfAChanceVariant.option_wol + or not self.nova_used + or ( + self.nova_ranged_weapon(state) + and state.has_any({item_names.NOVA_DOMINATION, item_names.NOVA_C20A_CANISTER_RIFLE}, self.player) + and (self.nova_full_stealth(state) or self.nova_heal(state)) + and self.nova_anti_air_weapon(state) + ) + ) + + def terran_outbreak_requirement(self, state: CollectionState) -> bool: + """Outbreak mission requirement""" + return self.terran_defense_rating(state, True, False) >= 4 and (self.terran_common_unit(state) or state.has(item_names.REAPER, self.player)) + + def zerg_outbreak_requirement(self, state: CollectionState) -> bool: + """ + Outbreak mission requirement. + Need to boot out Aberration-based comp + """ + return ( + self.zerg_defense_rating(state, True, False) >= 4 + and self.zerg_common_unit(state) + and ( + state.has_any( + ( + item_names.SWARM_QUEEN, + item_names.HYDRALISK, + item_names.ROACH, + item_names.MUTALISK, + item_names.INFESTED_BANSHEE, + ), + self.player, + ) + or self.morph_lurker(state) + or self.morph_brood_lord(state) + or ( + self.advanced_tactics + and ( + self.morph_impaler(state) + or self.morph_igniter(state) + or state.has_any((item_names.INFESTED_DIAMONDBACK, item_names.INFESTED_SIEGE_TANK), self.player) + ) + ) + ) + ) + + def protoss_outbreak_requirement(self, state: CollectionState) -> bool: + """ + Outbreak mission requirement + Something other than Zealot-based comp is required. + """ + return ( + self.protoss_defense_rating(state, True) >= 4 + and self.protoss_common_unit(state) + and self.protoss_basic_splash(state) + and ( + state.has_any( + ( + item_names.STALKER, + item_names.SLAYER, + item_names.INSTIGATOR, + item_names.ADEPT, + item_names.COLOSSUS, + item_names.VANGUARD, + item_names.SKIRMISHER, + item_names.OPPRESSOR, + item_names.CARRIER, + item_names.SKYLORD, + item_names.TRIREME, + item_names.DAWNBRINGER, + ), + self.player, + ) + or (self.advanced_tactics and (state.has_any((item_names.VOID_RAY, item_names.DESTROYER), self.player))) + ) + ) + + def terran_safe_haven_requirement(self, state: CollectionState) -> bool: + """Safe Haven mission requirement""" + return self.terran_common_unit(state) and self.terran_competent_anti_air(state) + + def terran_havens_fall_requirement(self, state: CollectionState) -> bool: + """Haven's Fall mission requirement""" + return self.terran_common_unit(state) and ( + self.terran_competent_comp(state) + or ( + self.terran_competent_anti_air(state) + and ( + state.has_any((item_names.VIKING, item_names.BATTLECRUISER), self.player) + or state.has_all((item_names.WRAITH, item_names.WRAITH_ADVANCED_LASER_TECHNOLOGY), self.player) + or state.has_all((item_names.LIBERATOR, item_names.LIBERATOR_RAID_ARTILLERY), 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 + """ + return self.terran_havens_fall_requirement(state) and ( + self.terran_air_anti_air(state) + or ( + state.has_any({item_names.BATTLECRUISER, item_names.VALKYRIE}, self.player) + and self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_SHIP_WEAPON, state) >= 2 + ) + ) + + def zerg_havens_fall_requirement(self, state: CollectionState) -> bool: + return ( + self.zerg_common_unit(state) + and self.zerg_competent_anti_air(state) + and (state.has(item_names.MUTALISK, self.player) or self.zerg_competent_comp(state)) + ) + + def zerg_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 + """ + return self.zerg_havens_fall_requirement(state) and ( + self.morph_devourer(state) + or state.has_any({item_names.MUTALISK, item_names.CORRUPTOR}, self.player) + or self.advanced_tactics + and (self.morph_viper(state) or state.has_any({item_names.BROOD_QUEEN, item_names.SCOURGE}, self.player)) + ) + + def protoss_havens_fall_requirement(self, state: CollectionState) -> bool: + return ( + self.protoss_common_unit(state) + and self.protoss_competent_anti_air(state) + and ( + self.protoss_competent_comp(state) + or ( + state.has_any((item_names.TEMPEST, item_names.SKYLORD, item_names.DESTROYER), self.player) + or ( + self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_PROTOSS_AIR_WEAPON, state) >= 2 + and state.has(item_names.CARRIER, self.player) + or state.has_all((item_names.SKIRMISHER, item_names.SKIRMISHER_PEER_CONTEMPT), self.player) + ) + ) + ) + ) + + def protoss_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 + """ + return self.protoss_havens_fall_requirement(state) and ( + state.has_any({item_names.CARRIER, item_names.SKYLORD, item_names.DESTROYER, item_names.TEMPEST}, self.player) + # handle mutas + or ( + state.has_any( + { + item_names.PHOENIX, + item_names.MIRAGE, + item_names.CORSAIR, + }, + self.player, + ) + or state.has_all((item_names.SKIRMISHER, item_names.SKIRMISHER_PEER_CONTEMPT), self.player) + ) + # handle brood lords and virophages + and ( + state.has_any( + { + item_names.VOID_RAY, + }, + self.player, + ) + or self.advanced_tactics + and state.has_all({item_names.SCOUT, item_names.MISTWING}, self.player) + ) + ) + + def terran_gates_of_hell_requirement(self, state: CollectionState) -> bool: + """Gates of Hell mission requirement""" + return self.terran_competent_comp(state) and (self.terran_defense_rating(state, True) > 6) + + def zerg_gates_of_hell_requirement(self, state: CollectionState) -> bool: + """Gates of Hell mission requirement""" + return self.zerg_competent_comp_competent_aa(state) and (self.zerg_defense_rating(state, True) > 8) + + def protoss_gates_of_hell_requirement(self, state: CollectionState) -> bool: + """Gates of Hell mission requirement""" + return self.protoss_competent_comp(state) and (self.protoss_defense_rating(state, True) > 6) + + def terran_welcome_to_the_jungle_requirement(self, state: CollectionState) -> bool: + """ + Welcome to the Jungle requirements - able to deal with Scouts, Void Rays, Zealots and Stalkers + """ + if self.terran_power_rating(state) < 5: + return False + return (self.terran_common_unit(state) and self.terran_competent_ground_to_air(state)) or ( + self.advanced_tactics + and state.has_any({item_names.MARINE, item_names.DOMINION_TROOPER, item_names.VULTURE}, self.player) + and self.terran_air_anti_air(state) + ) + + def zerg_welcome_to_the_jungle_requirement(self, state: CollectionState) -> bool: + """ + Welcome to the Jungle requirements - able to deal with Scouts, Void Rays, Zealots and Stalkers + """ + if self.zerg_power_rating(state) < 5: + return False + return (self.zerg_competent_comp(state) and state.has_any({item_names.HYDRALISK, item_names.MUTALISK}, self.player)) or ( + self.advanced_tactics + and self.zerg_common_unit(state) + and ( + state.has_any({item_names.MUTALISK, item_names.INFESTOR}, self.player) + or (self.morph_devourer(state) and state.has_any({item_names.HYDRALISK, item_names.SWARM_QUEEN}, self.player)) + or (self.morph_viper(state) and state.has(item_names.VIPER_PARASITIC_BOMB, self.player)) + ) + and self.zerg_army_weapon_armor_upgrade_min_level(state) >= 1 + ) + + def protoss_welcome_to_the_jungle_requirement(self, state: CollectionState) -> bool: + """ + Welcome to the Jungle requirements - able to deal with Scouts, Void Rays, Zealots and Stalkers + """ + if self.protoss_power_rating(state) < 5: + return False + return self.protoss_common_unit(state) and self.protoss_anti_armor_anti_air(state) + + def terran_can_grab_ghosts_in_the_fog_east_rock_formation(self, state: CollectionState) -> bool: + """ + Able to shoot by a long range or from air to claim the rock formation separated by a chasm + """ + return ( + state.has_any( + { + item_names.MEDIVAC, + item_names.HERCULES, + item_names.VIKING, + item_names.BANSHEE, + item_names.WRAITH, + item_names.SIEGE_TANK, + item_names.BATTLECRUISER, + item_names.NIGHT_HAWK, + item_names.NIGHT_WOLF, + item_names.SHOCK_DIVISION, + item_names.SKY_FURY, + }, + self.player, + ) + or state.has_all({item_names.VALKYRIE, item_names.VALKYRIE_FLECHETTE_MISSILES}, self.player) + or state.has_all({item_names.RAVEN, item_names.RAVEN_HUNTER_SEEKER_WEAPON}, self.player) + or ( + state.has_any({item_names.LIBERATOR, item_names.EMPERORS_GUARDIAN}, self.player) + and state.has(item_names.LIBERATOR_RAID_ARTILLERY, self.player) + ) + or ( + self.advanced_tactics + and ( + state.has_any( + { + item_names.HELS_ANGELS, + item_names.DUSK_WINGS, + item_names.WINGED_NIGHTMARES, + item_names.SIEGE_BREAKERS, + item_names.BRYNHILDS, + item_names.JACKSONS_REVENGE, + }, + self.player, + ) + ) + or state.has_all({item_names.MIDNIGHT_RIDERS, item_names.LIBERATOR_RAID_ARTILLERY}, self.player) + ) + ) + + def terran_great_train_robbery_train_stopper(self, state: CollectionState) -> bool: + """ + Ability to deal with trains (moving target with a lot of HP) + """ + return state.has_any( + {item_names.SIEGE_TANK, item_names.DIAMONDBACK, item_names.MARAUDER, item_names.CYCLONE, item_names.BANSHEE}, self.player + ) or ( + self.advanced_tactics + and ( + state.has_all({item_names.REAPER, item_names.REAPER_G4_CLUSTERBOMB}, self.player) + or state.has_all({item_names.SPECTRE, item_names.SPECTRE_PSIONIC_LASH}, self.player) + or state.has_any({item_names.VULTURE, item_names.LIBERATOR}, self.player) + ) + ) + + def zerg_great_train_robbery_train_stopper(self, state: CollectionState) -> bool: + """ + Ability to deal with trains (moving target with a lot of HP) + """ + return ( + state.has_any( + ( + item_names.ABERRATION, + item_names.INFESTED_DIAMONDBACK, + item_names.INFESTED_BANSHEE, + ), + self.player, + ) + or state.has_all({item_names.MUTALISK, item_names.MUTALISK_SUNDERING_GLAIVE}, self.player) + or state.has_all((item_names.HYDRALISK, item_names.HYDRALISK_MUSCULAR_AUGMENTS), self.player) + or ( + state.has(item_names.ZERGLING, self.player) + and ( + state.has_any( + (item_names.ZERGLING_SHREDDING_CLAWS, item_names.ZERGLING_SHREDDING_CLAWS, item_names.ZERGLING_RAPTOR_STRAIN), self.player + ) + ) + and (self.advanced_tactics or state.has_any((item_names.ZERGLING_METABOLIC_BOOST, item_names.ZERGLING_RAPTOR_STRAIN), self.player)) + ) + or self.zerg_infested_tank_with_ammo(state) + or (self.advanced_tactics and (self.morph_tyrannozor(state))) + ) + + def protoss_great_train_robbery_train_stopper(self, state: CollectionState) -> bool: + """ + Ability to deal with trains (moving target with a lot of HP) + """ + return ( + state.has_any( + (item_names.ANNIHILATOR, item_names.IMMORTAL, item_names.STALKER, item_names.WRATHWALKER, item_names.VOID_RAY, item_names.DESTROYER), + self.player, + ) + or state.has_all({item_names.SLAYER, item_names.SLAYER_PHASE_BLINK}, self.player) + or state.has_all((item_names.REAVER, item_names.REAVER_KHALAI_REPLICATORS), self.player) + or state.has_all({item_names.VANGUARD, item_names.VANGUARD_FUSION_MORTARS}, self.player) + or ( + state.has(item_names.INSTIGATOR, self.player) + and state.has_any((item_names.INSTIGATOR_BLINK_OVERDRIVE, item_names.INSTIGATOR_MODERNIZED_SERVOS), self.player) + ) + or (state.has_all((item_names.OPPRESSOR, item_names.SCOUT_GRAVITIC_THRUSTERS, item_names.SCOUT_ADVANCED_PHOTON_BLASTERS), self.player)) + or state.has_all((item_names.ORACLE, item_names.ORACLE_TEMPORAL_ACCELERATION_BEAM), self.player) + or ( + self.advanced_tactics + and ( + state.has(item_names.TEMPEST, self.player) + or state.has_all((item_names.ADEPT, item_names.ADEPT_RESONATING_GLAIVES), self.player) + or state.has_all({item_names.VANGUARD, item_names.VANGUARD_RAPIDFIRE_CANNON}, self.player) + or state.has_all((item_names.OPPRESSOR, item_names.SCOUT_GRAVITIC_THRUSTERS, item_names.OPPRESSOR_VULCAN_BLASTER), self.player) + or state.has_all((item_names.ASCENDANT, item_names.ASCENDANT_POWER_OVERWHELMING, item_names.SUPPLICANT), self.player) + or state.has_all( + (item_names.DARK_TEMPLAR, item_names.DARK_TEMPLAR_LESSER_SHADOW_FURY, item_names.DARK_TEMPLAR_GREATER_SHADOW_FURY), + self.player, + ) + or ( + state.has(item_names.DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_BLINK, self.player) + and ( + state.has_any((item_names.DARK_TEMPLAR, item_names.AVENGER), self.player) + or state.has_all((item_names.BLOOD_HUNTER, item_names.BLOOD_HUNTER_BRUTAL_EFFICIENCY), self.player) + ) + ) + ) + ) + ) + + def terran_can_rescue(self, state) -> bool: + """ + Rescuing in The Moebius Factor + """ + return state.has_any({item_names.MEDIVAC, item_names.HERCULES, item_names.RAVEN, item_names.VIKING}, self.player) or self.advanced_tactics + + def terran_supernova_requirement(self, state) -> bool: + return self.terran_beats_protoss_deathball(state) and self.terran_power_rating(state) >= 6 + + def zerg_supernova_requirement(self, state) -> bool: + return ( + self.zerg_common_unit(state) + and self.zerg_power_rating(state) >= 6 + and (self.advanced_tactics or state.has(item_names.YGGDRASIL, self.player)) + ) + + def protoss_supernova_requirement(self, state: CollectionState): + return ( + ( + state.count(item_names.PROGRESSIVE_WARP_RELOCATE, self.player) >= 2 + or (self.advanced_tactics and state.has(item_names.PROGRESSIVE_WARP_RELOCATE, self.player)) + ) + and self.protoss_competent_anti_air(state) + and (self.protoss_fleet(state) or (self.protoss_competent_comp(state) and self.protoss_power_rating(state) >= 6)) + ) + + def terran_maw_requirement(self, state: CollectionState) -> bool: + """ + Ability to deal with large areas with environment damage + """ + return ( + state.has(item_names.BATTLECRUISER, self.player) + and ( + self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_SHIP_WEAPON, state) >= 2 + or state.has(item_names.BATTLECRUISER_ATX_LASER_BATTERY, self.player) + ) + ) or ( + self.terran_air(state) + and ( + # Avoid dropping Troopers or units that do barely damage + state.has_any( + ( + item_names.GOLIATH, + item_names.THOR, + item_names.WARHOUND, + item_names.VIKING, + item_names.BANSHEE, + item_names.WRAITH, + item_names.BATTLECRUISER, + ), + self.player, + ) + or state.has_all((item_names.LIBERATOR, item_names.LIBERATOR_RAID_ARTILLERY), self.player) + or state.has_all((item_names.VALKYRIE, item_names.VALKYRIE_FLECHETTE_MISSILES), self.player) + or (state.has(item_names.MARAUDER, self.player) and self.terran_bio_heal(state)) + ) + and ( + # Can deal damage to air units inside rip fields + state.has_any((item_names.GOLIATH, item_names.CYCLONE, item_names.VIKING), self.player) + or ( + state.has_any((item_names.WRAITH, item_names.VALKYRIE, item_names.BATTLECRUISER), self.player) + and self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_SHIP_WEAPON, state) >= 2 + ) + or state.has_all((item_names.THOR, item_names.THOR_PROGRESSIVE_HIGH_IMPACT_PAYLOAD), self.player) + ) + and self.terran_competent_comp(state) + and self.terran_competent_anti_air(state) + and self.terran_sustainable_mech_heal(state) + ) + + def zerg_maw_requirement(self, state: CollectionState) -> bool: + """ + Ability to cross defended gaps, deal with skytoss, and avoid costly losses. + """ + if self.advanced_tactics and state.has(item_names.INFESTOR, self.player): + return True + usable_muta = ( + state.has_all((item_names.MUTALISK, item_names.MUTALISK_RAPID_REGENERATION), self.player) + and state.has_any((item_names.MUTALISK_SEVERING_GLAIVE, item_names.MUTALISK_VICIOUS_GLAIVE), self.player) + and ( + state.has(item_names.MUTALISK_SUNDERING_GLAIVE, self.player) + or state.has_all((item_names.MUTALISK_SEVERING_GLAIVE, item_names.MUTALISK_VICIOUS_GLAIVE), self.player) + ) + ) + return ( + # Heal + ( + state.has(item_names.SWARM_QUEEN, self.player) + or self.advanced_tactics + and ((self.morph_tyrannozor(state) and state.has(item_names.TYRANNOZOR_HEALING_ADAPTATION, self.player)) or (usable_muta)) + ) + # Cross the gap + and ( + state.has_any((item_names.NYDUS_WORM, item_names.OVERLORD_VENTRAL_SACS), self.player) + or (self.advanced_tactics and state.has(item_names.YGGDRASIL, self.player)) + ) + # Air to ground + and (self.morph_brood_lord(state) or self.morph_guardian(state) or usable_muta) + # Ground to air + and ( + state.has(item_names.INFESTOR, self.player) + or self.morph_tyrannozor(state) + or state.has_all( + {item_names.SWARM_HOST, item_names.SWARM_HOST_RESOURCE_EFFICIENCY, item_names.SWARM_HOST_PRESSURIZED_GLANDS}, self.player + ) + or state.has_all({item_names.HYDRALISK, item_names.HYDRALISK_RESOURCE_EFFICIENCY}, self.player) + or state.has_all({item_names.INFESTED_DIAMONDBACK, item_names.INFESTED_DIAMONDBACK_PROGRESSIVE_FUNGAL_SNARE}, self.player) + ) + # Survives rip-field + and ( + state.has_any({item_names.ABERRATION, item_names.ROACH, item_names.ULTRALISK}, self.player) + or self.morph_tyrannozor(state) + or (self.advanced_tactics and usable_muta) + ) + # Air-to-air + and (state.has_any({item_names.MUTALISK, item_names.CORRUPTOR, item_names.INFESTED_LIBERATOR, item_names.BROOD_QUEEN}, self.player)) + # Upgrades / general + and self.zerg_competent_anti_air(state) + and self.zerg_competent_comp(state) + ) + + def protoss_maw_requirement(self, state: CollectionState) -> bool: + """ + Ability to cross defended gaps and deal with skytoss. + """ + return ( + ( + state.has(item_names.WARP_PRISM, self.player) + or ( + self.advanced_tactics + and (state.has(item_names.ARBITER, self.player) or state.has_all((item_names.MISTWING, item_names.MISTWING_PILOT), self.player)) + ) + ) + and self.protoss_common_unit_anti_armor_air(state) + and self.protoss_fleet(state) + ) + + def terran_engine_of_destruction_requirement(self, state: CollectionState) -> int: + power_rating = self.terran_power_rating(state) + if power_rating < 3 or not self.marine_medic_upgrade(state) or not self.terran_common_unit(state): + return False + if power_rating >= 7 and self.terran_competent_comp(state): + return True + else: + return ( + state.has_any((item_names.WRAITH, item_names.BATTLECRUISER), self.player) + or self.terran_air_anti_air(state) + and state.has_any((item_names.BANSHEE, item_names.LIBERATOR), self.player) + ) + + def zerg_engine_of_destruction_requirement(self, state: CollectionState) -> int: + power_rating = self.zerg_power_rating(state) + if ( + power_rating < 3 + or not self.zergling_hydra_roach_start(state) + or not self.zerg_common_unit(state) + or not self.zerg_competent_anti_air(state) + or not self.zerg_repair_odin(state) + ): + return False + if power_rating >= 7 and self.zerg_competent_comp(state): + return True + else: + return self.zerg_base_buster(state) + + def protoss_engine_of_destruction_requirement(self, state: CollectionState): + return ( + self.zealot_sentry_slayer_start(state) + and self.protoss_repair_odin(state) + and (self.protoss_deathball(state) or self.protoss_fleet(state)) + ) + + def zerg_repair_odin(self, state: CollectionState): + return ( + self.zerg_has_infested_scv(state) + or state.has_all({item_names.SWARM_QUEEN_BIO_MECHANICAL_TRANSFUSION, item_names.SWARM_QUEEN}, self.player) + or (self.advanced_tactics and state.has(item_names.SWARM_QUEEN, self.player)) + ) + + def protoss_repair_odin(self, state: CollectionState): + return ( + state.has(item_names.SENTRY, self.player) + or state.has_all((item_names.CARRIER, item_names.CARRIER_REPAIR_DRONES), self.player) + or ( + self.spear_of_adun_passive_presence + in [SpearOfAdunPassiveAbilityPresence.option_protoss, SpearOfAdunPassiveAbilityPresence.option_everywhere] + and state.has(item_names.RECONSTRUCTION_BEAM, self.player) + ) + or (self.advanced_tactics and state.has_all({item_names.SHIELD_BATTERY, item_names.KHALAI_INGENUITY}, self.player)) + ) + + def terran_in_utter_darkness_requirement(self, state: CollectionState) -> bool: + return self.terran_competent_comp(state) and self.terran_defense_rating(state, True, True) >= 8 + + def zerg_in_utter_darkness_requirement(self, state: CollectionState) -> bool: + return self.zerg_competent_comp(state) and self.zerg_competent_anti_air(state) and self.zerg_defense_rating(state, True, True) >= 8 + + def protoss_in_utter_darkness_requirement(self, state: CollectionState) -> bool: + return self.protoss_competent_comp(state) and self.protoss_defense_rating(state, True) >= 4 + + def terran_all_in_requirement(self, state: CollectionState): + """ + All-in + """ + if not self.terran_very_hard_mission_weapon_armor_level(state): + return False + beats_kerrigan = ( + state.has_any({item_names.MARINE, item_names.DOMINION_TROOPER, item_names.BANSHEE}, self.player) + or state.has_all({item_names.REAPER, item_names.REAPER_RESOURCE_EFFICIENCY}, self.player) + or (self.all_in_map == AllInMap.option_air and state.has_all((item_names.VALKYRIE, item_names.VALKYRIE_FLECHETTE_MISSILES), self.player)) + or (self.advanced_tactics and state.has_all((item_names.GHOST, item_names.GHOST_EMP_ROUNDS), self.player)) + ) + if not beats_kerrigan: + return False + if not self.terran_competent_comp(state): + return False + if self.all_in_map == AllInMap.option_ground: + # Ground + defense_rating = self.terran_defense_rating(state, True, False) + if state.has_any({item_names.BATTLECRUISER, item_names.BANSHEE}, self.player): + defense_rating += 2 + return defense_rating >= 13 + else: + # Air + defense_rating = self.terran_defense_rating(state, True, True) + return ( + defense_rating >= 9 + and self.terran_competent_anti_air(state) + and state.has_any({item_names.VIKING, item_names.BATTLECRUISER, item_names.VALKYRIE}, self.player) + and state.has_any({item_names.HIVE_MIND_EMULATOR, item_names.PSI_DISRUPTER, item_names.MISSILE_TURRET}, self.player) + ) + + def zerg_all_in_requirement(self, state: CollectionState): + """ + All-in (Zerg) + """ + if not self.zerg_very_hard_mission_weapon_armor_level(state): + return False + beats_kerrigan = ( + state.has_any({item_names.INFESTED_MARINE, item_names.INFESTED_BANSHEE, item_names.INFESTED_BUNKER}, self.player) + or state.has_all({item_names.SWARM_HOST, item_names.SWARM_HOST_RESOURCE_EFFICIENCY}, self.player) + or self.morph_brood_lord(state) + ) + if not beats_kerrigan: + return False + if not self.zerg_competent_comp(state): + return False + if self.all_in_map == AllInMap.option_ground: + # Ground + defense_rating = self.zerg_defense_rating(state, True, False) + if ( + state.has_any({item_names.MUTALISK, item_names.INFESTED_BANSHEE}, self.player) + or self.morph_brood_lord(state) + or self.morph_guardian(state) + ): + defense_rating += 3 + if state.has(item_names.SPINE_CRAWLER, self.player): + defense_rating += 2 + return defense_rating >= 13 + else: + # Air + defense_rating = self.zerg_defense_rating(state, True, True) + return ( + defense_rating >= 9 + and state.has_any({item_names.MUTALISK, item_names.CORRUPTOR}, self.player) + and state.has_any({item_names.SPORE_CRAWLER, item_names.INFESTED_MISSILE_TURRET}, self.player) + ) + + def protoss_all_in_requirement(self, state: CollectionState): + """ + All-in (Protoss) + """ + if not self.protoss_very_hard_mission_weapon_armor_level(state): + return False + beats_kerrigan = ( + # cheap units with multiple small attacks, or anything with Feedback + state.has_any({item_names.ZEALOT, item_names.SENTINEL, item_names.SKIRMISHER, item_names.HIGH_TEMPLAR}, self.player) + or state.has_all((item_names.CENTURION, item_names.CENTURION_RESOURCE_EFFICIENCY), self.player) + or state.has_all({item_names.SIGNIFIER, item_names.SIGNIFIER_FEEDBACK}, self.player) + or (self.protoss_can_merge_archon(state) and state.has(item_names.ARCHON_HIGH_ARCHON, self.player)) + or (self.protoss_can_merge_dark_archon(state) and state.has(item_names.DARK_ARCHON_FEEDBACK, self.player)) + ) + if not beats_kerrigan: + return False + if not self.protoss_competent_comp(state): + return False + if self.all_in_map == AllInMap.option_ground: + # Ground + defense_rating = self.protoss_defense_rating(state, True) + if ( + state.has_any({item_names.SKIRMISHER, item_names.DARK_TEMPLAR, item_names.TEMPEST, item_names.TRIREME}, self.player) + or state.has_all((item_names.BLOOD_HUNTER, item_names.BLOOD_HUNTER_BRUTAL_EFFICIENCY), self.player) + or state.has_all((item_names.AVENGER, item_names.AVENGER_KRYHAS_CLOAK), self.player) + ): + defense_rating += 2 + if state.has(item_names.PHOTON_CANNON, self.player): + defense_rating += 2 + return defense_rating >= 13 + else: + # Air + defense_rating = self.protoss_defense_rating(state, True) + if state.has(item_names.KHAYDARIN_MONOLITH, self.player): + defense_rating += 2 + if state.has(item_names.PHOTON_CANNON, self.player): + defense_rating += 2 + return defense_rating >= 9 and (state.has_any({item_names.TEMPEST, item_names.SKYLORD, item_names.VOID_RAY}, self.player)) + + def zerg_can_grab_ghosts_in_the_fog_east_rock_formation(self, state: CollectionState) -> bool: + return ( + state.has_any({item_names.MUTALISK, item_names.INFESTED_BANSHEE, item_names.OVERLORD_VENTRAL_SACS, item_names.INFESTOR}, self.player) + or (self.morph_devourer(state) and state.has(item_names.DEVOURER_PRESCIENT_SPORES, self.player)) + or (self.morph_guardian(state) and state.has(item_names.GUARDIAN_PRIMAL_ADAPTATION, self.player)) + or ((self.morph_guardian(state) or self.morph_brood_lord(state)) and self.zerg_basic_air_to_air(state)) + or ( + self.advanced_tactics + and ( + state.has_any({item_names.INFESTED_SIEGE_BREAKERS, item_names.INFESTED_DUSK_WINGS}, self.player) + or (state.has(item_names.HUNTERLING, self.player) and self.zerg_basic_air_to_air(state)) + ) + ) + ) + def zerg_any_units_back_in_the_saddle_requirement(self, state: CollectionState) -> bool: + return ( + self.grant_story_tech == GrantStoryTech.option_grant + # Note(mm): This check isn't necessary as self.kerrigan_levels cover it, + # and it's not fully desirable in future when we support non-grant story tech + kerriganless. + # or not self.kerrigan_presence + or state.has_any(( + # Cases tested by Snarky + item_names.KERRIGAN_KINETIC_BLAST, + item_names.KERRIGAN_LEAPING_STRIKE, + item_names.KERRIGAN_CRUSHING_GRIP, + item_names.KERRIGAN_PSIONIC_SHIFT, + item_names.KERRIGAN_SPAWN_BANELINGS, + item_names.KERRIGAN_FURY, + item_names.KERRIGAN_APOCALYPSE, + item_names.KERRIGAN_DROP_PODS, + item_names.KERRIGAN_SPAWN_LEVIATHAN, + item_names.KERRIGAN_IMMOBILIZATION_WAVE, # Involves a 1-minute cooldown wait before the ultra + item_names.KERRIGAN_MEND, # See note from THE EV below + ), self.player) + or self.kerrigan_levels(state, 20) + or (self.kerrigan_levels(state, 10) and state.has(item_names.KERRIGAN_CHAIN_REACTION, self.player)) + # Tested by THE EV, "facetank with Kerrigan and stutter step to the end with >10s left" + # > have to lure the first group of Zerg in the 2nd timed section into the first room of the second area + # > (with the heal box) so you can kill them before the timer starts. + # + # phaneros: Technically possible without the levels, but adding them in for safety margin and to hopefully + # make generation force this branch less often + or (state.has_any((item_names.KERRIGAN_HEROIC_FORTITUDE, item_names.KERRIGAN_INFEST_BROODLINGS), self.player) + and self.kerrigan_levels(state, 5) + ) + # Insufficient: Wild Mutation, Assimilation Aura + ) + + def zerg_pass_vents(self, state: CollectionState) -> bool: + return ( + self.grant_story_tech == GrantStoryTech.option_grant + or state.has_any({item_names.ZERGLING, item_names.HYDRALISK, item_names.ROACH}, self.player) + or (self.advanced_tactics and state.has(item_names.INFESTOR, self.player)) + ) + + def supreme_requirement(self, state: CollectionState) -> bool: + return ( + self.grant_story_tech == GrantStoryTech.option_grant + or not self.kerrigan_unit_available or (self.grant_story_tech == GrantStoryTech.option_allow_substitutes + and state.has_any(( + item_names.KERRIGAN_LEAPING_STRIKE, + item_names.OVERLORD_VENTRAL_SACS, + item_names.YGGDRASIL, + item_names.MUTALISK_CORRUPTOR_VIPER_ASPECT, + item_names.NYDUS_WORM, + item_names.BULLFROG, + ), self.player) + and state.has_any(( + item_names.KERRIGAN_MEND, + item_names.SWARM_QUEEN, + item_names.INFESTED_MEDICS, + ), self.player) + and self.kerrigan_levels(state, 35) + ) + or (state.has_all((item_names.KERRIGAN_LEAPING_STRIKE, item_names.KERRIGAN_MEND), self.player) and self.kerrigan_levels(state, 35)) + ) + + def terran_infested_garrison_claimer(self, state: CollectionState) -> bool: + return state.has_any((item_names.GHOST, item_names.SPECTRE, item_names.EMPERORS_SHADOW), self.player) + + def protoss_infested_garrison_claimer(self, state: CollectionState) -> bool: + return state.has_any( + (item_names.HIGH_TEMPLAR, item_names.SIGNIFIER, item_names.ASCENDANT), self.player + ) or self.protoss_can_merge_dark_archon(state) + + def terran_hand_of_darkness_requirement(self, state: CollectionState) -> bool: + return self.terran_competent_comp(state) and self.terran_power_rating(state) >= 6 + + def zerg_hand_of_darkness_requirement(self, state: CollectionState) -> bool: + return ( + self.zerg_competent_comp(state) + and (self.zerg_competent_anti_air(state) or self.advanced_tactics and self.zerg_moderate_anti_air(state)) + and (self.basic_kerrigan(state) or self.zerg_power_rating(state) >= 4) + ) + + def protoss_hand_of_darkness_requirement(self, state: CollectionState) -> bool: + return self.protoss_competent_comp(state) and self.protoss_power_rating(state) >= 6 + + def terran_planetfall_requirement(self, state: CollectionState) -> bool: + return self.terran_beats_protoss_deathball(state) and self.terran_power_rating(state) >= 8 + + def zerg_planetfall_requirement(self, state: CollectionState) -> bool: + return self.zerg_competent_comp(state) and self.zerg_competent_anti_air(state) and self.zerg_power_rating(state) >= 8 + + def protoss_planetfall_requirement(self, state: CollectionState) -> bool: + return self.protoss_deathball(state) and self.protoss_power_rating(state) >= 8 + + def zerg_the_reckoning_requirement(self, state: CollectionState) -> bool: + if not (self.zerg_power_rating(state) >= 6 or self.basic_kerrigan(state)): + return False + 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)) + and self.terran_very_hard_mission_weapon_armor_level(state) + and self.zerg_very_hard_mission_weapon_armor_level(state) + ) + else: + return self.zerg_competent_comp(state) and self.zerg_competent_anti_air(state) and self.zerg_very_hard_mission_weapon_armor_level(state) + + def terran_the_reckoning_requirement(self, state: CollectionState) -> bool: + return self.terran_very_hard_mission_weapon_armor_level(state) and self.terran_base_trasher(state) + + def protoss_the_reckoning_requirement(self, state: CollectionState) -> bool: + return ( + self.protoss_very_hard_mission_weapon_armor_level(state) + and self.protoss_deathball(state) + and (not self.take_over_ai_allies or (self.terran_competent_comp(state) and self.terran_very_hard_mission_weapon_armor_level(state))) + ) + + def protoss_can_attack_behind_chasm(self, state: CollectionState) -> bool: + return ( + state.has_any( + { + item_names.SCOUT, + item_names.TEMPEST, + item_names.CARRIER, + item_names.SKYLORD, + item_names.TRIREME, + item_names.VOID_RAY, + item_names.DESTROYER, + item_names.PULSAR, + item_names.DAWNBRINGER, + item_names.MOTHERSHIP, + }, + self.player, + ) + or self.protoss_has_blink(state) + or ( + state.has(item_names.WARP_PRISM, self.player) + and (self.protoss_common_unit(state) or state.has(item_names.WARP_PRISM_PHASE_BLASTER, self.player)) + ) + or (self.advanced_tactics and state.has_any({item_names.ORACLE, item_names.ARBITER}, self.player)) + ) + + def the_infinite_cycle_requirement(self, state: CollectionState) -> bool: + return ( + self.grant_story_tech == GrantStoryTech.option_grant + or not self.kerrigan_unit_available + or ( + state.has_any( + ( + item_names.KERRIGAN_KINETIC_BLAST, + item_names.KERRIGAN_SPAWN_BANELINGS, + item_names.KERRIGAN_LEAPING_STRIKE, + item_names.KERRIGAN_SPAWN_LEVIATHAN, + ), + self.player, + ) + and self.basic_kerrigan(state) + and self.kerrigan_levels(state, 70) + ) + ) + + def templars_return_phase_2_requirement(self, state: CollectionState) -> bool: + return ( + self.grant_story_tech == GrantStoryTech.option_grant + or self.advanced_tactics + or ( + state.has_any( + ( + item_names.IMMORTAL, + item_names.ANNIHILATOR, + item_names.VANGUARD, + item_names.COLOSSUS, + item_names.WRATHWALKER, + item_names.REAVER, + item_names.DARK_TEMPLAR, + item_names.HIGH_TEMPLAR, + item_names.ENERGIZER, + item_names.SENTRY, + ), + self.player, + ) + ) + ) + + def templars_return_phase_3_reach_colossus_requirement(self, state: CollectionState) -> bool: + return self.templars_return_phase_2_requirement(state) and ( + self.grant_story_tech == GrantStoryTech.option_grant + or self.advanced_tactics + and state.has_any({item_names.ZEALOT_WHIRLWIND, item_names.VANGUARD_RAPIDFIRE_CANNON}, self.player) + or state.has_all(( + item_names.ZEALOT_WHIRLWIND, item_names.VANGUARD_RAPIDFIRE_CANNON + ), self.player) + ) + + def templars_return_phase_3_reach_dts_requirement(self, state: CollectionState) -> bool: + return self.templars_return_phase_3_reach_colossus_requirement(state) and ( + self.grant_story_tech == GrantStoryTech.option_grant + or ( + (self.advanced_tactics or state.has(item_names.ENERGIZER_MOBILE_CHRONO_BEAM, self.player)) + and (state.has(item_names.COLOSSUS_FIRE_LANCE, self.player) + or ( + state.has_all( + { + item_names.COLOSSUS_PACIFICATION_PROTOCOL, + item_names.ENERGIZER_MOBILE_CHRONO_BEAM, + }, + self.player, + ) + ) + )) + ) + + def terran_spear_of_adun_requirement(self, state: CollectionState) -> bool: + return self.terran_common_unit(state) and self.terran_competent_anti_air(state) and self.terran_defense_rating(state, False, False) >= 5 + + def zerg_spear_of_adun_requirement(self, state: CollectionState) -> bool: + return self.zerg_common_unit(state) and self.zerg_competent_anti_air(state) and self.zerg_defense_rating(state, False, False) >= 5 + + def protoss_spear_of_adun_requirement(self, state: CollectionState) -> bool: + return ( + self.protoss_common_unit(state) + and self.protoss_anti_light_anti_air(state) + and ( + state.has_any((item_names.ZEALOT, item_names.CENTURION, item_names.SENTINEL, item_names.ADEPT), self.player) + or self.protoss_basic_splash(state) + ) + and self.protoss_defense_rating(state, False) >= 5 + ) + + def terran_sky_shield_requirement(self, state: CollectionState) -> bool: + return self.terran_common_unit(state) and self.terran_competent_anti_air(state) and self.terran_power_rating(state) >= 7 + + def zerg_sky_shield_requirement(self, state: CollectionState) -> bool: + return self.zerg_common_unit(state) and self.zerg_competent_anti_air(state) and self.zerg_power_rating(state) >= 7 + + def protoss_sky_shield_requirement(self, state: CollectionState) -> bool: + return self.protoss_common_unit(state) and self.protoss_competent_anti_air(state) and self.protoss_power_rating(state) >= 7 + + def protoss_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({item_names.BATTLECRUISER, item_names.LIBERATOR, item_names.SIEGE_TANK}, self.player) + or (self.advanced_tactics and state.has_all({item_names.SPECTRE, item_names.SPECTRE_PSIONIC_LASH}, self.player)) + or ( + state.has(item_names.IMMORTAL, self.player) + and state.has_any({item_names.MARINE, item_names.DOMINION_TROOPER, item_names.MARAUDER}, self.player) + and self.terran_bio_heal(state) + ) + ) + ) + + def zerg_brothers_in_arms_requirement(self, state: CollectionState) -> bool: + return ( + self.zerg_common_unit(state) and self.zerg_competent_comp(state) and self.zerg_competent_anti_air(state) and self.zerg_big_monsters(state) + ) or ( + self.take_over_ai_allies + and (self.zerg_common_unit(state) or self.terran_common_unit(state)) + and (self.terran_competent_anti_air(state) or self.zerg_competent_anti_air(state)) + and ( + self.zerg_big_monsters(state) + or state.has_any({item_names.BATTLECRUISER, item_names.LIBERATOR, item_names.SIEGE_TANK}, self.player) + or (self.advanced_tactics and state.has_all({item_names.SPECTRE, item_names.SPECTRE_PSIONIC_LASH}, self.player)) + or ( + state.has(item_names.ABERRATION, self.player) + and state.has_any({item_names.MARINE, item_names.DOMINION_TROOPER, item_names.MARAUDER}, self.player) + and self.terran_bio_heal(state) + ) + ) + ) + + def protoss_amons_reach_requirement(self, state: CollectionState) -> bool: + return self.protoss_common_unit_anti_light_air(state) and self.protoss_basic_splash(state) and self.protoss_power_rating(state) >= 7 + + def protoss_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.protoss_defense_rating(state, False) >= 8 + ) + + def terran_last_stand_requirement(self, state: CollectionState) -> bool: + return ( + self.terran_common_unit(state) + and state.has_any({item_names.SIEGE_TANK, item_names.LIBERATOR}, self.player) + and state.has_any({item_names.PERDITION_TURRET, item_names.DEVASTATOR_TURRET, item_names.PLANETARY_FORTRESS}, self.player) + and self.terran_air_anti_air(state) + and state.has_any({item_names.VIKING, item_names.BATTLECRUISER}, self.player) + and self.terran_defense_rating(state, True, False) >= 10 + and self.terran_army_weapon_armor_upgrade_min_level(state) >= 2 + ) + + def zerg_last_stand_requirement(self, state: CollectionState) -> bool: + return ( + self.zerg_common_unit(state) + and self.zerg_competent_anti_air(state) + and state.has(item_names.SPINE_CRAWLER, self.player) + and ( + self.morph_lurker(state) + or state.has_all({item_names.MUTALISK, item_names.MUTALISK_SEVERING_GLAIVE, item_names.MUTALISK_VICIOUS_GLAIVE}, self.player) + or self.zerg_infested_tank_with_ammo(state) + or self.advanced_tactics + and state.has_all({item_names.ULTRALISK, item_names.ULTRALISK_CHITINOUS_PLATING, item_names.ULTRALISK_MONARCH_BLADES}, self.player) + ) + and ( + self.morph_impaler(state) + or state.has_all({item_names.INFESTED_LIBERATOR, item_names.INFESTED_LIBERATOR_DEFENDER_MODE}, self.player) + or self.zerg_infested_tank_with_ammo(state) + or state.has(item_names.BILE_LAUNCHER, self.player) + ) + and ( + self.morph_devourer(state) + or state.has_all({item_names.MUTALISK, item_names.MUTALISK_SUNDERING_GLAIVE}, self.player) + or self.advanced_tactics + and state.has(item_names.BROOD_QUEEN, self.player) + ) + and self.zerg_mineral_dump(state) + and self.zerg_army_weapon_armor_upgrade_min_level(state) >= 2 + ) + + def terran_temple_of_unification_requirement(self, state: CollectionState) -> bool: + return self.terran_beats_protoss_deathball(state) and self.terran_power_rating(state) >= 10 + + def zerg_temple_of_unification_requirement(self, state: CollectionState) -> bool: + # Don't be locked to roach/hydra + return ( + self.zerg_competent_comp(state) + and self.zerg_competent_anti_air(state) + and ( + state.has(item_names.INFESTED_BANSHEE, self.player) + or state.has_all((item_names.INFESTED_LIBERATOR, item_names.INFESTED_LIBERATOR_DEFENDER_MODE), self.player) + or state.has_all({item_names.MUTALISK, item_names.MUTALISK_SUNDERING_GLAIVE}, self.player) + or self.zerg_big_monsters(state) + or ( + self.advanced_tactics + and (state.has_any({item_names.INFESTOR, item_names.DEFILER, item_names.BROOD_QUEEN}, self.player) or self.morph_viper(state)) + ) + ) + and self.zerg_power_rating(state) >= 10 + ) + + def protoss_temple_of_unification_requirement(self, state: CollectionState) -> bool: + return self.protoss_competent_comp(state) and self.protoss_power_rating(state) >= 10 + + def protoss_harbinger_of_oblivion_requirement(self, state: CollectionState) -> bool: + return ( + self.protoss_anti_armor_anti_air(state) + and ( + self.take_over_ai_allies + and (self.protoss_common_unit(state) or self.zerg_common_unit(state)) + or (self.protoss_competent_comp(state) and self.protoss_hybrid_counter(state)) + ) + and self.protoss_power_rating(state) >= 6 + ) + + def terran_harbinger_of_oblivion_requirement(self, state: CollectionState) -> bool: + return ( + self.terran_competent_anti_air(state) + and ( + self.take_over_ai_allies + and (self.terran_common_unit(state) or self.zerg_common_unit(state)) + or ( + self.terran_beats_protoss_deathball(state) + and state.has_any({item_names.BATTLECRUISER, item_names.LIBERATOR, item_names.SIEGE_TANK, item_names.THOR}, self.player) + ) + ) + and self.terran_power_rating(state) >= 6 + ) + + def zerg_harbinger_of_oblivion_requirement(self, state: CollectionState) -> bool: + return ( + self.zerg_competent_anti_air(state) + and self.zerg_common_unit(state) + and (self.take_over_ai_allies or (self.zerg_competent_comp(state) and self.zerg_big_monsters(state))) + and self.zerg_power_rating(state) >= 6 + ) + + def terran_unsealing_the_past_requirement(self, state: CollectionState) -> bool: + return ( + self.terran_competent_anti_air(state) + and self.terran_competent_comp(state) + and self.terran_power_rating(state) >= 6 + and ( + state.has_all({item_names.SIEGE_TANK, item_names.SIEGE_TANK_JUMP_JETS}, self.player) + or state.has_all( + {item_names.BATTLECRUISER, item_names.BATTLECRUISER_ATX_LASER_BATTERY, item_names.BATTLECRUISER_MOIRAI_IMPULSE_DRIVE}, self.player + ) + or ( + self.advanced_tactics + and ( + state.has_all({item_names.SIEGE_TANK, item_names.SIEGE_TANK_SMART_SERVOS}, self.player) + or ( + state.has_all({item_names.LIBERATOR, item_names.LIBERATOR_SMART_SERVOS}, self.player) + and ( + ( + state.has_all({item_names.HELLION, item_names.HELLION_HELLBAT}, self.player) + or state.has(item_names.FIREBAT, self.player) + ) + and self.terran_bio_heal(state) + or state.has_all({item_names.VIKING, item_names.VIKING_SHREDDER_ROUNDS}, self.player) + or state.has(item_names.BANSHEE, self.player) + ) + ) + ) + ) + ) + ) + + def zerg_unsealing_the_past_requirement(self, state: CollectionState) -> bool: + return ( + self.zerg_competent_comp(state) + and self.zerg_competent_anti_air(state) + and self.zerg_power_rating(state) >= 6 + and ( + self.morph_brood_lord(state) + or self.zerg_big_monsters(state) + or state.has_all({item_names.MUTALISK, item_names.MUTALISK_SEVERING_GLAIVE, item_names.MUTALISK_VICIOUS_GLAIVE}, self.player) + or ( + self.advanced_tactics + and (self.morph_igniter(state) or (self.morph_lurker(state) and state.has(item_names.LURKER_SEISMIC_SPINES, self.player))) + ) + ) + ) + + def terran_purification_requirement(self, state: CollectionState) -> bool: + return ( + self.terran_competent_comp(state) + and self.terran_very_hard_mission_weapon_armor_level(state) + and self.terran_defense_rating(state, True, False) >= 10 + and ( + state.has_any({item_names.LIBERATOR, item_names.THOR}, self.player) + or ( + state.has(item_names.SIEGE_TANK, self.player) + and (self.advanced_tactics or state.has(item_names.SIEGE_TANK_MAELSTROM_ROUNDS, self.player)) + ) + ) + and ( + state.has_all({item_names.VIKING, item_names.VIKING_SHREDDER_ROUNDS}, self.player) + or ( + state.has(item_names.BANSHEE, self.player) + and ( + state.has(item_names.BANSHEE_SHOCKWAVE_MISSILE_BATTERY, self.player) + or (self.advanced_tactics and state.has(item_names.BANSHEE_ROCKET_BARRAGE, self.player)) + ) + ) + ) + ) + + def zerg_purification_requirement(self, state: CollectionState) -> bool: + return ( + self.zerg_competent_comp(state) + and self.zerg_competent_anti_air(state) + and self.zerg_defense_rating(state, True, True) >= 5 + and self.zerg_big_monsters(state) + and (state.has(item_names.ULTRALISK, self.player) or self.morph_igniter(state) or self.morph_lurker(state)) + ) + + def protoss_steps_of_the_rite_requirement(self, state: CollectionState) -> bool: + return self.protoss_deathball(state) or self.protoss_fleet(state) + + def terran_steps_of_the_rite_requirement(self, state: CollectionState) -> bool: + return ( + self.terran_beats_protoss_deathball(state) + and ( + state.has_any({item_names.SIEGE_TANK, item_names.LIBERATOR}, self.player) + or state.has_all({item_names.BATTLECRUISER, item_names.BATTLECRUISER_ATX_LASER_BATTERY}, self.player) + or state.has_all((item_names.BANSHEE, item_names.BANSHEE_SHOCKWAVE_MISSILE_BATTERY), self.player) + ) + and ( + state.has_all({item_names.BATTLECRUISER, item_names.BATTLECRUISER_ATX_LASER_BATTERY}, self.player) + or state.has(item_names.VALKYRIE, self.player) + or state.has_all((item_names.VIKING, item_names.VIKING_RIPWAVE_MISSILES), self.player) + ) + and self.terran_very_hard_mission_weapon_armor_level(state) + ) + + def zerg_steps_of_the_rite_requirement(self, state: CollectionState) -> bool: + return ( + self.zerg_competent_comp(state) + and self.zerg_competent_anti_air(state) + and self.zerg_base_buster(state) + and ( + self.morph_lurker(state) + or self.zerg_infested_tank_with_ammo(state) + or state.has_all({item_names.INFESTED_LIBERATOR, item_names.INFESTED_LIBERATOR_DEFENDER_MODE}, self.player) + or (state.has(item_names.SWARM_QUEEN, self.player) and self.zerg_big_monsters(state)) + ) + and ( + state.has(item_names.INFESTED_LIBERATOR, self.player) + or state.has_all({item_names.MUTALISK, item_names.MUTALISK_SEVERING_GLAIVE, item_names.MUTALISK_VICIOUS_GLAIVE}, self.player) + or (state.has(item_names.MUTALISK, self.player) and self.morph_devourer(state)) + ) + ) + + def terran_rak_shir_requirement(self, state: CollectionState) -> bool: + return self.terran_beats_protoss_deathball(state) and self.terran_power_rating(state) >= 10 + + def zerg_rak_shir_requirement(self, state: CollectionState) -> bool: + return ( + self.zerg_competent_comp(state) + and self.zerg_competent_anti_air(state) + and ( + self.zerg_big_monsters(state) + or state.has_all({item_names.INFESTED_LIBERATOR, item_names.INFESTED_LIBERATOR_DEFENDER_MODE}, self.player) + or self.morph_impaler_or_lurker(state) + ) + and ( + state.has_all({item_names.INFESTED_LIBERATOR, item_names.INFESTED_LIBERATOR_CLOUD_DISPERSAL}, self.player) + or ( + state.has(item_names.MUTALISK, self.player) + and (state.has(item_names.MUTALISK_SUNDERING_GLAIVE, self.player) or self.morph_devourer(state)) + ) + or state.has(item_names.CORRUPTOR, self.player) + or (self.advanced_tactics and state.has(item_names.INFESTOR, self.player)) + ) + and self.zerg_power_rating(state) >= 10 + ) + + def protoss_rak_shir_requirement(self, state: CollectionState) -> bool: + return (self.protoss_deathball(state) or self.protoss_fleet(state)) and self.protoss_power_rating(state) >= 10 + + def protoss_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) + and ( + state.has_any((item_names.ARBITER, item_names.CORSAIR, item_names.PHOENIX), self.player) + or state.has_all((item_names.MIRAGE, item_names.MIRAGE_GRAVITON_BEAM), self.player) + ) + ) + ) + ) + + def terran_templars_charge_requirement(self, state: CollectionState) -> bool: + return self.terran_very_hard_mission_weapon_armor_level(state) and ( + ( + state.has_all({item_names.BATTLECRUISER, item_names.BATTLECRUISER_ATX_LASER_BATTERY}, self.player) + and state.count(item_names.BATTLECRUISER_PROGRESSIVE_DEFENSIVE_MATRIX, self.player) >= 2 + ) + or ( + self.terran_air_anti_air(state) + and self.terran_sustainable_mech_heal(state) + and ( + state.has_any({item_names.BANSHEE, item_names.BATTLECRUISER}, self.player) + or state.has_all({item_names.LIBERATOR, item_names.LIBERATOR_RAID_ARTILLERY}, self.player) + or (self.advanced_tactics and (state.has_all({item_names.WRAITH, item_names.WRAITH_ADVANCED_LASER_TECHNOLOGY}, self.player))) + ) + ) + ) + + def zerg_templars_charge_requirement(self, state: CollectionState) -> bool: + return ( + self.zerg_competent_comp(state) + and self.zerg_competent_anti_air(state) + and state.has(item_names.SWARM_QUEEN, self.player) + and ( + self.morph_guardian(state) + or self.morph_brood_lord(state) + or state.has(item_names.INFESTED_BANSHEE, self.player) + or ( + self.advanced_tactics + and ( + state.has_all( + { + item_names.MUTALISK, + item_names.MUTALISK_SEVERING_GLAIVE, + item_names.MUTALISK_VICIOUS_GLAIVE, + item_names.MUTALISK_AERODYNAMIC_GLAIVE_SHAPE, + }, + self.player, + ) + or self.morph_viper(state) + ) + ) + ) + and ( + state.has_all({item_names.INFESTED_LIBERATOR, item_names.INFESTED_LIBERATOR_CLOUD_DISPERSAL}, self.player) + or (self.morph_devourer(state) and state.has(item_names.MUTALISK, self.player)) + or state.has_all({item_names.MUTALISK, item_names.MUTALISK_SUNDERING_GLAIVE}, self.player) + ) + ) + + def protoss_the_host_requirement(self, state: CollectionState) -> bool: + return ( + self.protoss_fleet(state) and self.protoss_static_defense(state) and self.protoss_army_weapon_armor_upgrade_min_level(state) >= 2 + ) or ( + self.protoss_deathball(state) + and state.has(item_names.SOA_TIME_STOP, self.player) + or self.advanced_tactics + and (state.has_any((item_names.SOA_SHIELD_OVERCHARGE, item_names.SOA_SOLAR_BOMBARDMENT), self.player)) + ) + + def terran_the_host_requirement(self, state: CollectionState) -> bool: + return ( + self.terran_beats_protoss_deathball(state) + and self.terran_very_hard_mission_weapon_armor_level(state) + and ( + ( + state.has_all({item_names.BATTLECRUISER, item_names.BATTLECRUISER_ATX_LASER_BATTERY}, self.player) + and state.count(item_names.BATTLECRUISER_PROGRESSIVE_DEFENSIVE_MATRIX, self.player) >= 2 + ) + or ( + self.terran_air_anti_air(state) + and self.terran_sustainable_mech_heal(state) + and ( + state.has_any({item_names.BANSHEE, item_names.BATTLECRUISER}, self.player) + or state.has_all({item_names.LIBERATOR, item_names.LIBERATOR_RAID_ARTILLERY}, self.player) + ) + ) + or ( + self.spear_of_adun_presence == SpearOfAdunPresence.option_everywhere + and state.has(item_names.SOA_TIME_STOP, self.player) + or self.advanced_tactics + and (state.has_any((item_names.SOA_SHIELD_OVERCHARGE, item_names.SOA_SOLAR_BOMBARDMENT), self.player)) + ) + ) + ) + + def zerg_the_host_requirement(self, state: CollectionState) -> bool: + return ( + self.zerg_competent_comp(state) + and self.zerg_competent_anti_air(state) + and self.zerg_very_hard_mission_weapon_armor_level(state) + and self.zerg_base_buster(state) + and self.zerg_big_monsters(state) + and ( + (self.morph_brood_lord(state) or self.morph_guardian(state)) + and ( + (self.morph_devourer(state) and state.has(item_names.MUTALISK, self.player)) + or state.has_all((item_names.INFESTED_LIBERATOR, item_names.INFESTED_LIBERATOR_CLOUD_DISPERSAL), self.player) + ) + or ( + state.has_all( + ( + item_names.MUTALISK, + item_names.MUTALISK_SEVERING_GLAIVE, + item_names.MUTALISK_VICIOUS_GLAIVE, + item_names.MUTALISK_SUNDERING_GLAIVE, + item_names.MUTALISK_RAPID_REGENERATION, + ), + self.player, + ) + ) + ) + or ( + self.spear_of_adun_presence == SpearOfAdunPresence.option_everywhere + and state.has(item_names.SOA_TIME_STOP, self.player) + or self.advanced_tactics + and (state.has_any((item_names.SOA_SHIELD_OVERCHARGE, item_names.SOA_SOLAR_BOMBARDMENT), self.player)) + ) + ) + + def protoss_salvation_requirement(self, state: CollectionState) -> bool: + return ( + ([self.protoss_competent_comp(state), self.protoss_fleet(state), self.protoss_static_defense(state)].count(True) >= 2) + and self.protoss_very_hard_mission_weapon_armor_level(state) + and self.protoss_power_rating(state) >= 6 + ) + + def terran_salvation_requirement(self, state: CollectionState) -> bool: + return ( + self.terran_beats_protoss_deathball(state) + and self.terran_very_hard_mission_weapon_armor_level(state) + and self.terran_air_anti_air(state) + and state.has_any({item_names.SIEGE_TANK, item_names.LIBERATOR}, self.player) + and state.has_any({item_names.PERDITION_TURRET, item_names.DEVASTATOR_TURRET, item_names.PLANETARY_FORTRESS}, self.player) + and self.terran_power_rating(state) >= 6 + ) + + def zerg_salvation_requirement(self, state: CollectionState) -> bool: + return ( + self.zerg_competent_comp(state) + and self.zerg_competent_anti_air(state) + and state.has(item_names.SPINE_CRAWLER, self.player) + and self.zerg_very_hard_mission_weapon_armor_level(state) + and ( + self.morph_impaler_or_lurker(state) + or state.has_all({item_names.INFESTED_LIBERATOR, item_names.INFESTED_LIBERATOR_DEFENDER_MODE}, self.player) + or state.has_all({item_names.MUTALISK, item_names.MUTALISK_SEVERING_GLAIVE, item_names.MUTALISK_VICIOUS_GLAIVE}, self.player) + ) + and ( + state.has_all({item_names.INFESTED_LIBERATOR, item_names.INFESTED_LIBERATOR_CLOUD_DISPERSAL}, self.player) + or (self.morph_devourer(state) and state.has(item_names.MUTALISK, self.player)) + or state.has_all({item_names.MUTALISK, item_names.MUTALISK_SUNDERING_GLAIVE}, self.player) + ) + and self.zerg_power_rating(state) >= 6 + ) + + def into_the_void_requirement(self, state: CollectionState) -> bool: + if not self.protoss_very_hard_mission_weapon_armor_level(state): + return False + if self.take_over_ai_allies and not ( + self.terran_very_hard_mission_weapon_armor_level(state) and self.zerg_very_hard_mission_weapon_armor_level(state) + ): + return False + return self.protoss_competent_comp(state) or ( + self.take_over_ai_allies + and ( + state.has(item_names.BATTLECRUISER, self.player) + or (state.has(item_names.ULTRALISK, self.player) and self.protoss_competent_anti_air(state)) + ) + ) + + def essence_of_eternity_requirement(self, state: CollectionState) -> bool: + if not self.terran_very_hard_mission_weapon_armor_level(state): + return False + if self.take_over_ai_allies and not ( + self.protoss_very_hard_mission_weapon_armor_level(state) and self.zerg_very_hard_mission_weapon_armor_level(state) + ): + return False + 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 >= 12 + and (self.terran_competent_anti_air(state) or self.take_over_ai_allies and self.protoss_competent_anti_air(state)) + and ( + state.has(item_names.BATTLECRUISER, self.player) + or ( + state.has_any((item_names.BANSHEE, item_names.LIBERATOR), self.player) + and state.has_any({item_names.VIKING, item_names.VALKYRIE}, self.player) + ) + or self.take_over_ai_allies + and self.protoss_fleet(state) + ) + and self.terran_power_rating(state) >= 6 + ) + + def amons_fall_requirement(self, state: CollectionState) -> bool: + if not self.zerg_very_hard_mission_weapon_armor_level(state): + return False + if not self.zerg_competent_anti_air(state): + return False + if self.zerg_power_rating(state) < 6: + return False + if self.take_over_ai_allies and not ( + self.terran_very_hard_mission_weapon_armor_level(state) and self.protoss_very_hard_mission_weapon_armor_level(state) + ): + return False + if self.take_over_ai_allies: + return ( + ( + state.has_any({item_names.BATTLECRUISER, item_names.CARRIER, item_names.SKYLORD, item_names.TRIREME}, self.player) + or ( + state.has(item_names.ULTRALISK, self.player) + and self.protoss_competent_anti_air(state) + and ( + state.has_any({item_names.LIBERATOR, item_names.BANSHEE, item_names.VALKYRIE, item_names.VIKING}, self.player) + or state.has_all({item_names.WRAITH, item_names.WRAITH_ADVANCED_LASER_TECHNOLOGY}, self.player) + or self.protoss_fleet(state) + ) + and ( + self.terran_sustainable_mech_heal(state) + or ( + self.spear_of_adun_passive_presence == SpearOfAdunPassiveAbilityPresence.option_everywhere + and state.has(item_names.RECONSTRUCTION_BEAM, self.player) + ) + ) + ) + ) + and self.terran_competent_anti_air(state) + and self.protoss_deathball(state) + and self.zerg_competent_comp(state) + ) + else: + return ( + ( + state.has_any((item_names.MUTALISK, item_names.CORRUPTOR, item_names.BROOD_QUEEN, item_names.INFESTED_BANSHEE), self.player) + or state.has_all((item_names.INFESTED_LIBERATOR, item_names.INFESTED_LIBERATOR_CLOUD_DISPERSAL), self.player) + or state.has_all((item_names.SCOURGE, item_names.SCOURGE_RESOURCE_EFFICIENCY), self.player) + or self.morph_brood_lord(state) + or self.morph_guardian(state) + or self.morph_devourer(state) + ) + or (self.advanced_tactics and self.spread_creep(state, False) and self.zerg_big_monsters(state)) + ) and self.zerg_competent_comp(state) + + 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.grant_story_tech == GrantStoryTech.option_grant 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_able_to_snipe_defiler(self, state: CollectionState) -> bool: + return ( + state.has(item_names.BANSHEE, self.player) + or ( + state.has(item_names.NOVA_JUMP_SUIT_MODULE, self.player) + and (state.has_any({item_names.NOVA_DOMINATION, item_names.NOVA_C20A_CANISTER_RIFLE, item_names.NOVA_PULSE_GRENADES}, self.player)) + ) + or (state.has_all({item_names.SIEGE_TANK, item_names.SIEGE_TANK_MAELSTROM_ROUNDS, item_names.SIEGE_TANK_JUMP_JETS}, self.player)) + ) + + def sudden_strike_requirement(self, state: CollectionState) -> bool: + return ( + self.terran_able_to_snipe_defiler(state) + and (self.terran_cliffjumper(state) or state.has(item_names.BANSHEE, self.player)) + and self.nova_splash(state) + and self.terran_defense_rating(state, True, False) >= 3 + and self.advanced_tactics + or state.has(item_names.NOVA_JUMP_SUIT_MODULE, self.player) + ) + + def enemy_intelligence_garrisonable_unit(self, state: CollectionState) -> bool: + """ + Has unit usable as a Garrison in Enemy Intelligence + """ + return ( + state.has_any(( + item_names.MARINE, + item_names.SON_OF_KORHAL, + item_names.REAPER, + item_names.MARAUDER, + item_names.GHOST, + item_names.SPECTRE, + item_names.HELLION, + item_names.GOLIATH, + item_names.WARHOUND, + item_names.DIAMONDBACK, + item_names.VIKING, + item_names.DOMINION_TROOPER, + ), self.player) + or (self.advanced_tactics + and state.has(item_names.ROGUE_FORCES, self.player) + and state.count_from_list(( + item_names.WAR_PIGS, + item_names.HAMMER_SECURITIES, + item_names.DEATH_HEADS, + item_names.SPARTAN_COMPANY, + item_names.HELS_ANGELS, + item_names.BRYNHILDS, + ), self.player) >= 3 + ) + ) + + def enemy_intelligence_cliff_garrison(self, state: CollectionState) -> bool: + return ( + state.has_any((item_names.REAPER, item_names.VIKING), self.player) + or (state.has_any((item_names.MEDIVAC, item_names.HERCULES), self.player) + and self.enemy_intelligence_garrisonable_unit(state) + ) + or state.has_all({item_names.GOLIATH, item_names.GOLIATH_JUMP_JETS}, self.player) + or (self.advanced_tactics and state.has_any({item_names.HELS_ANGELS, item_names.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(item_names.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.grant_story_tech == GrantStoryTech.option_grant + 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.grant_story_tech == GrantStoryTech.option_grant or (state.has(item_names.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE, self.player) and self.nova_dash(state)) + ) + + def enemy_intelligence_cliff_garrison_and_nova_mobility(self, state: CollectionState) -> bool: + return self.enemy_intelligence_cliff_garrison(state) and ( + self.nova_any_nobuild_damage(state) + or ( + state.has(item_names.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE, self.player, 2) + and state.has_any((item_names.NOVA_FLASHBANG_GRENADES, item_names.NOVA_BLINK), self.player) + ) + ) + + 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 + and self.terran_power_rating(state) >= 5 + ) + + 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(item_names.SIEGE_TANK, self.player) + or state.has_all({item_names.VIKING, item_names.VIKING_SHREDDER_ROUNDS}, self.player) + or state.has_all((item_names.BANSHEE, item_names.BANSHEE_SHOCKWAVE_MISSILE_BATTERY), self.player) + or ( + ( + # Regular infesteds + ( + state.has_any((item_names.FIREBAT, item_names.REAPER), self.player) + or state.has_all({item_names.HELLION, item_names.HELLION_HELLBAT}, self.player) + ) + and self.terran_bio_heal(state) + or (self.advanced_tactics and state.has_any({item_names.PERDITION_TURRET, item_names.PLANETARY_FORTRESS}, self.player)) + ) + and ( + # Volatile infesteds + state.has(item_names.LIBERATOR, self.player) + or ( + self.advanced_tactics + and state.has(item_names.VULTURE, self.player) + or (state.has(item_names.HERC, self.player) and self.terran_bio_heal(state)) + ) + ) + ) + ) + and self.terran_army_weapon_armor_upgrade_min_level(state) >= 2 + ) + + 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 + and self.terran_army_weapon_armor_upgrade_min_level(state) >= 2 + and self.nova_splash(state) + and (self.advanced_tactics or self.terran_competent_ground_to_air(state)) + ) + + def enemy_shadow_tripwires_tool(self, state: CollectionState) -> bool: + return state.has_any({item_names.NOVA_FLASHBANG_GRENADES, item_names.NOVA_BLINK, item_names.NOVA_DOMINATION}, self.player) + + def enemy_shadow_door_unlocks_tool(self, state: CollectionState) -> bool: + return state.has_any({item_names.NOVA_DOMINATION, item_names.NOVA_BLINK, item_names.NOVA_JUMP_SUIT_MODULE}, self.player) + + def enemy_shadow_nova_damage_and_blazefire_unlock(self, state: CollectionState) -> bool: + return self.nova_any_nobuild_damage(state) and ( + state.has(item_names.NOVA_BLINK, self.player) or state.has_all((item_names.NOVA_HOLO_DECOY, item_names.NOVA_DOMINATION), self.player) + ) + + def enemy_shadow_domination(self, state: CollectionState) -> bool: + return self.grant_story_tech == GrantStoryTech.option_grant or ( + self.nova_ranged_weapon(state) + and ( + self.nova_full_stealth(state) + or state.has(item_names.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.grant_story_tech == GrantStoryTech.option_grant + 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.grant_story_tech == GrantStoryTech.option_grant + or (self.nova_splash(state) or self.nova_heal(state) or self.nova_escape_assist(state)) + and (self.advanced_tactics or state.has(item_names.NOVA_GHOST_VISOR, self.player)) + ) + + def enemy_shadow_door_controls(self, state: CollectionState) -> bool: + return self.enemy_shadow_second_stage(state) and (self.grant_story_tech == GrantStoryTech.option_grant 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.grant_story_tech == GrantStoryTech.option_grant or (self.nova_heal(state) and self.nova_beat_stone(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 self.nova_any_weapon(state) + and self.nova_splash(state) + and ( + # Xanthos + state.has_any((item_names.BATTLECRUISER, item_names.VIKING, item_names.WARHOUND), self.player) + or state.has_all((item_names.LIBERATOR, item_names.LIBERATOR_SMART_SERVOS), self.player) + or state.has_all((item_names.THOR, item_names.THOR_PROGRESSIVE_HIGH_IMPACT_PAYLOAD), self.player) + or ( + state.has(item_names.VALKYRIE, self.player) + and state.has_any((item_names.VALKYRIE_AFTERBURNERS, item_names.VALKYRIE_SHAPED_HULL), self.player) + and state.has_any((item_names.VALKYRIE_FLECHETTE_MISSILES, item_names.VALKYRIE_ENHANCED_CLUSTER_LAUNCHERS), self.player) + ) + or (state.has(item_names.BANSHEE, self.player) and (self.advanced_tactics or state.has(item_names.BANSHEE_SHAPED_HULL, self.player))) + or ( + self.advanced_tactics + and ( + ( + state.has_all((item_names.MARINE, item_names.MARINE_PROGRESSIVE_STIMPACK), self.player) + and (self.terran_bio_heal(state) or state.count(item_names.MARINE_PROGRESSIVE_STIMPACK, self.player) >= 2) + ) + or (state.has(item_names.DOMINION_TROOPER, self.player) and self.terran_bio_heal(state)) + or state.has_all( + (item_names.PREDATOR, item_names.PREDATOR_RESOURCE_EFFICIENCY, item_names.PREDATOR_ADAPTIVE_DEFENSES), self.player + ) + or state.has_all((item_names.CYCLONE, item_names.CYCLONE_TARGETING_OPTICS), self.player) + ) + ) + ) + and ( # The enemy has 3/3 BCs + state.has_any( + (item_names.GOLIATH, item_names.VIKING, item_names.NOVA_C20A_CANISTER_RIFLE, item_names.NOVA_BLAZEFIRE_GUNBLADE), self.player + ) + or state.has_all((item_names.THOR, item_names.THOR_PROGRESSIVE_HIGH_IMPACT_PAYLOAD), self.player) + or state.has_all((item_names.GHOST, item_names.GHOST_LOCKDOWN), self.player) + or state.has_all((item_names.BATTLECRUISER, item_names.BATTLECRUISER_ATX_LASER_BATTERY), self.player) + ) + and self.terran_army_weapon_armor_upgrade_min_level(state) >= 3 + ) + + def has_terran_units(self, target: int) -> Callable[["CollectionState"], bool]: + def _has_terran_units(state: CollectionState) -> bool: + return (state.count_from_list_unique(item_groups.terran_units + item_groups.terran_buildings, self.player) >= target) and ( + # Anything that can hit buildings + state.has_any(( + # Infantry + item_names.MARINE, + item_names.FIREBAT, + item_names.MARAUDER, + item_names.REAPER, + item_names.HERC, + item_names.DOMINION_TROOPER, + item_names.GHOST, + item_names.SPECTRE, + # Vehicles + item_names.HELLION, + item_names.VULTURE, + item_names.SIEGE_TANK, + item_names.WARHOUND, + item_names.GOLIATH, + item_names.DIAMONDBACK, + item_names.THOR, + item_names.PREDATOR, + item_names.CYCLONE, + # Ships + item_names.WRAITH, + item_names.VIKING, + item_names.BANSHEE, + item_names.RAVEN, + item_names.BATTLECRUISER, + # RG + item_names.SON_OF_KORHAL, + item_names.AEGIS_GUARD, + item_names.EMPERORS_SHADOW, + item_names.BULWARK_COMPANY, + item_names.SHOCK_DIVISION, + item_names.BLACKHAMMER, + item_names.SKY_FURY, + item_names.NIGHT_WOLF, + item_names.NIGHT_HAWK, + item_names.PRIDE_OF_AUGUSTRGRAD, + ), self.player) + or state.has_all((item_names.LIBERATOR, item_names.LIBERATOR_RAID_ARTILLERY), self.player) + or state.has_all((item_names.EMPERORS_GUARDIAN, item_names.LIBERATOR_RAID_ARTILLERY), self.player) + or state.has_all((item_names.VALKYRIE, item_names.VALKYRIE_FLECHETTE_MISSILES), self.player) + or state.has_all((item_names.WIDOW_MINE, item_names.WIDOW_MINE_DEMOLITION_PAYLOAD), self.player) + or ( + state.has_any(( + # Mercs with shortest initial cooldown (300s) + item_names.WAR_PIGS, + item_names.DEATH_HEADS, + item_names.HELS_ANGELS, + item_names.WINGED_NIGHTMARES, + ), self.player) + # + 2 upgrades that allow getting faster/earlier mercs + and state.count_from_list(( + item_names.RAPID_REINFORCEMENT, + item_names.PROGRESSIVE_FAST_DELIVERY, + item_names.ROGUE_FORCES, + # item_names.SIGNAL_BEACON, # Probably doesn't help too much on the first unit + ), self.player) >= 2 + ) + ) + + return _has_terran_units + + def has_zerg_units(self, target: int) -> Callable[["CollectionState"], bool]: + def _has_zerg_units(state: CollectionState) -> bool: + num_units = ( + state.count_from_list_unique( + item_groups.zerg_nonmorph_units + item_groups.zerg_buildings + [item_names.OVERLORD_OVERSEER_ASPECT], + self.player + ) + + self.morph_baneling(state) + + self.morph_ravager(state) + + self.morph_igniter(state) + + self.morph_lurker(state) + + self.morph_impaler(state) + + self.morph_viper(state) + + self.morph_devourer(state) + + self.morph_brood_lord(state) + + self.morph_guardian(state) + + self.morph_tyrannozor(state) + ) + return ( + num_units >= target + and ( + # Anything that can hit buildings + state.has_any(( + item_names.ZERGLING, + item_names.SWARM_QUEEN, + item_names.ROACH, + item_names.HYDRALISK, + item_names.ABERRATION, + item_names.SWARM_HOST, + item_names.MUTALISK, + item_names.ULTRALISK, + item_names.PYGALISK, + item_names.INFESTED_MARINE, + item_names.INFESTED_BUNKER, + item_names.INFESTED_DIAMONDBACK, + item_names.INFESTED_SIEGE_TANK, + item_names.INFESTED_BANSHEE, + # Mercs with <= 300s first drop time + item_names.DEVOURING_ONES, + item_names.HUNTER_KILLERS, + item_names.CAUSTIC_HORRORS, + item_names.HUNTERLING, + ), self.player) + or state.has_all((item_names.INFESTOR, item_names.INFESTOR_INFESTED_TERRAN), self.player) + or self.morph_baneling(state) + or self.morph_lurker(state) + or self.morph_impaler(state) + or self.morph_brood_lord(state) + or self.morph_guardian(state) + or self.morph_ravager(state) + or self.morph_igniter(state) + or self.morph_tyrannozor(state) + or (self.morph_devourer(state) + and state.has(item_names.DEVOURER_PRESCIENT_SPORES, self.player) + ) + or ( + state.has_any(( + # Mercs with <= 300s first drop time + item_names.DEVOURING_ONES, + item_names.HUNTER_KILLERS, + item_names.CAUSTIC_HORRORS, + item_names.HUNTERLING, + ), self.player) + # + 2 upgrades that allow getting faster/earlier mercs + and state.count_from_list(( + item_names.UNRESTRICTED_MUTATION, + item_names.EVOLUTIONARY_LEAP, + item_names.CELL_DIVISION, + item_names.SELF_SUFFICIENT, + ), self.player) >= 2 + ) + ) + ) + + return _has_zerg_units + + def has_protoss_units(self, target: int) -> Callable[["CollectionState"], bool]: + def _has_protoss_units(state: CollectionState) -> bool: + return ( + state.count_from_list_unique(item_groups.protoss_units + item_groups.protoss_buildings + [item_names.NEXUS_OVERCHARGE], self.player) + >= target + ) and ( + # Anything that can hit buildings + state.has_any(( + # Gateway + item_names.ZEALOT, + item_names.CENTURION, + item_names.SENTINEL, + item_names.SUPPLICANT, + item_names.STALKER, + item_names.INSTIGATOR, + item_names.SLAYER, + item_names.DRAGOON, + item_names.ADEPT, + item_names.SENTRY, + item_names.ENERGIZER, + item_names.AVENGER, + item_names.DARK_TEMPLAR, + item_names.BLOOD_HUNTER, + item_names.HIGH_TEMPLAR, + item_names.SIGNIFIER, + item_names.ASCENDANT, + item_names.DARK_ARCHON, + # Robo + item_names.IMMORTAL, + item_names.ANNIHILATOR, + item_names.VANGUARD, + item_names.STALWART, + item_names.COLOSSUS, + item_names.WRATHWALKER, + item_names.REAVER, + item_names.DISRUPTOR, + # Stargate + item_names.SKIRMISHER, + item_names.SCOUT, + item_names.MISTWING, + item_names.OPPRESSOR, + item_names.PULSAR, + item_names.VOID_RAY, + item_names.DESTROYER, + item_names.DAWNBRINGER, + item_names.ARBITER, + item_names.ORACLE, + item_names.CARRIER, + item_names.TRIREME, + item_names.SKYLORD, + item_names.TEMPEST, + item_names.MOTHERSHIP, + ), self.player) + or state.has_all((item_names.WARP_PRISM, item_names.WARP_PRISM_PHASE_BLASTER), self.player) + or state.has_all((item_names.CALADRIUS, item_names.CALADRIUS_CORONA_BEAM), self.player) + or state.has_all((item_names.PHOTON_CANNON, item_names.KHALAI_INGENUITY), self.player) + or state.has_all((item_names.KHAYDARIN_MONOLITH, item_names.KHALAI_INGENUITY), self.player) + ) + + return _has_protoss_units + + def has_race_units(self, target: int, race: SC2Race) -> Callable[["CollectionState"], bool]: + if target == 0 or race == SC2Race.ANY: + return Location.access_rule + result = self.unit_count_functions.get((race, target)) + if result is not None: + return result + if race == SC2Race.TERRAN: + result = self.has_terran_units(target) + if race == SC2Race.ZERG: + result = self.has_zerg_units(target) + if race == SC2Race.PROTOSS: + result = self.has_protoss_units(target) + assert result + self.unit_count_functions[(race, target)] = result + return result + + +def get_basic_units(logic_level: int, race: SC2Race) -> Set[str]: + if logic_level > RequiredTactics.option_advanced: + return no_logic_basic_units[race] + elif logic_level == RequiredTactics.option_advanced: + return advanced_basic_units[race] + else: + return basic_units[race] diff --git a/worlds/sc2/settings.py b/worlds/sc2/settings.py new file mode 100644 index 00000000..26253b1e --- /dev/null +++ b/worlds/sc2/settings.py @@ -0,0 +1,49 @@ +from typing import Union +import settings + + +class Starcraft2Settings(settings.Group): + class WindowWidth(int): + """The starting width the client window in pixels""" + + class WindowHeight(int): + """The starting height the client window in pixels""" + + class GameWindowedMode(settings.Bool): + """Controls whether the game should start in windowed mode""" + + class TerranButtonColor(list): + """Defines the colour of terran mission buttons in the launcher in rgb format (3 elements ranging from 0 to 1)""" + + class ZergButtonColor(list): + """Defines the colour of zerg mission buttons in the launcher in rgb format (3 elements ranging from 0 to 1)""" + + class ProtossButtonColor(list): + """Defines the colour of protoss mission buttons in the launcher in rgb format (3 elements ranging from 0 to 1)""" + + class DisableForcedCamera(str): + """Overrides the disable forced-camera slot option. Possible values: `true`, `false`, `default`. Default uses slot value""" + + class SkipCutscenes(str): + """Overrides the skip cutscenes slot option. Possible values: `true`, `false`, `default`. Default uses slot value""" + + class GameDifficulty(str): + """Overrides the slot's difficulty setting. Possible values: `casual`, `normal`, `hard`, `brutal`, `default`. Default uses slot value""" + + class GameSpeed(str): + """Overrides the slot's gamespeed setting. Possible values: `slower`, `slow`, `normal`, `fast`, `faster`, `default`. Default uses slot value""" + + class ShowTraps(settings.Bool): + """If set to true, in-client scouting will show traps as distinct from filler""" + + window_width: WindowWidth = WindowWidth(1080) + window_height: WindowHeight = WindowHeight(720) + game_windowed_mode: Union[GameWindowedMode, bool] = False + show_traps: Union[ShowTraps, bool] = False + disable_forced_camera: DisableForcedCamera = DisableForcedCamera("default") + skip_cutscenes: SkipCutscenes = SkipCutscenes("default") + game_difficulty: GameDifficulty = GameDifficulty("default") + game_speed: GameSpeed = GameSpeed("default") + terran_button_color: TerranButtonColor = TerranButtonColor([0.0838, 0.2898, 0.2346]) + zerg_button_color: ZergButtonColor = ZergButtonColor([0.345, 0.22425, 0.12765]) + protoss_button_color: ProtossButtonColor = ProtossButtonColor([0.18975, 0.2415, 0.345]) diff --git a/worlds/sc2/starcraft2.kv b/worlds/sc2/starcraft2.kv new file mode 100644 index 00000000..9013986f --- /dev/null +++ b/worlds/sc2/starcraft2.kv @@ -0,0 +1,61 @@ + + scroll_type: ["content", "bars"] + bar_width: dp(12) + effect_cls: "ScrollEffect" + canvas.after: + Color: + rgba: (0.82, 0.2, 0, root.border_on) + Line: + width: 1.5 + rectangle: self.x+1, self.y+1, self.width-1, self.height-1 + + + color: (1, 1, 1, 1) + canvas.before: + Color: + rgba: (0xd2/0xff, 0x33/0xff, 0, 1) + Rectangle: + pos: (self.x - 8, self.y - 8) + size: (self.width + 30, self.height + 16) + + + cols: 1 + size_hint_y: None + height: self.minimum_height + 15 + padding: [5,0,dp(12),0] + +: + cols: 1 + +: + rows: 1 + +: + cols: 1 + +: + rows: 1 + +: + cols: 1 + spacing: [0,5] + +: + text_size: self.size + markup: True + halign: 'center' + valign: 'middle' + padding: [5,0,5,0] + outline_width: 1 + canvas.before: + Color: + rgba: (1, 193/255, 86/255, root.is_goal) + Line: + width: 1 + rectangle: (self.x, self.y + 0.5, self.width, self.height) + canvas.after: + Color: + rgba: (0.8, 0.8, 0.8, root.is_exit) + Line: + width: 1 + rectangle: (self.x + 2, self.y + 3, self.width - 4, self.height - 4) \ No newline at end of file diff --git a/worlds/sc2/test/test_Regions.py b/worlds/sc2/test/test_Regions.py deleted file mode 100644 index c268b65d..00000000 --- a/worlds/sc2/test/test_Regions.py +++ /dev/null @@ -1,41 +0,0 @@ -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) diff --git a/worlds/sc2/test/test_base.py b/worlds/sc2/test/test_base.py index 28529e37..6110814c 100644 --- a/worlds/sc2/test/test_base.py +++ b/worlds/sc2/test/test_base.py @@ -1,11 +1,52 @@ from typing import * +import unittest +import random +from argparse import Namespace +from BaseClasses import MultiWorld, CollectionState, PlandoOptions +from Generate import get_seed_name +from worlds import AutoWorld +from test.general import gen_steps, call_all -from test.TestBase import WorldTestBase +from test.bases import WorldTestBase from .. import SC2World -from .. import Client +from .. import client class Sc2TestBase(WorldTestBase): - game = Client.SC2Context.game + game = client.SC2Context.game world: SC2World player: ClassVar[int] = 1 skip_long_tests: bool = True + + +class Sc2SetupTestBase(unittest.TestCase): + """ + A custom sc2-specific test base class that provides an explicit function to generate the world from options. + This allows potentially generating multiple worlds in one test case, useful for tracking down a rare / sporadic + crash. + """ + seed: Optional[int] = None + game = SC2World.game + player = 1 + def generate_world(self, options: Dict[str, Any]) -> None: + self.multiworld = MultiWorld(1) + self.multiworld.game[self.player] = self.game + self.multiworld.player_name = {self.player: "Tester"} + self.multiworld.set_seed(self.seed) + random.seed(self.multiworld.seed) + self.multiworld.seed_name = get_seed_name(random) # only called to get same RNG progression as Generate.py + args = Namespace() + for name, option in AutoWorld.AutoWorldRegister.world_types[self.game].options_dataclass.type_hints.items(): + new_option = option.from_any(options.get(name, option.default)) + new_option.verify(SC2World, "Tester", PlandoOptions.items|PlandoOptions.connections|PlandoOptions.texts|PlandoOptions.bosses) + setattr(args, name, { + 1: new_option + }) + self.multiworld.set_options(args) + self.world: SC2World = cast(SC2World, self.multiworld.worlds[self.player]) + self.multiworld.state = CollectionState(self.multiworld) + try: + for step in gen_steps: + call_all(self.multiworld, step) + except Exception as ex: + ex.add_note(f"Seed: {self.multiworld.seed}") + raise diff --git a/worlds/sc2/test/test_custom_mission_orders.py b/worlds/sc2/test/test_custom_mission_orders.py new file mode 100644 index 00000000..f431e909 --- /dev/null +++ b/worlds/sc2/test/test_custom_mission_orders.py @@ -0,0 +1,216 @@ +""" +Unit tests for custom mission orders +""" + +from .test_base import Sc2SetupTestBase +from .. import MissionFlag +from ..item import item_tables, item_names +from BaseClasses import ItemClassification + +class TestCustomMissionOrders(Sc2SetupTestBase): + def test_mini_wol_generates(self): + world_options = { + 'mission_order': 'custom', + 'custom_mission_order': { + 'Mini Wings of Liberty': { + 'global': { + 'type': 'column', + 'mission_pool': [ + 'terran missions', + '^ wol missions' + ] + }, + 'Mar Sara': { + 'size': 1 + }, + 'Colonist': { + 'size': 2, + 'entry_rules': [{ + 'scope': '../Mar Sara' + }] + }, + 'Artifact': { + 'size': 3, + 'entry_rules': [{ + 'scope': '../Mar Sara' + }], + 'missions': [ + { + 'index': 1, + 'entry_rules': [{ + 'scope': 'Mini Wings of Liberty', + 'amount': 4 + }] + }, + { + 'index': 2, + 'entry_rules': [{ + 'scope': 'Mini Wings of Liberty', + 'amount': 8 + }] + } + ] + }, + 'Prophecy': { + 'size': 2, + 'entry_rules': [{ + 'scope': '../Artifact/1' + }], + 'mission_pool': [ + 'protoss missions', + '^ prophecy missions' + ] + }, + 'Covert': { + 'size': 2, + 'entry_rules': [{ + 'scope': 'Mini Wings of Liberty', + 'amount': 2 + }] + }, + 'Rebellion': { + 'size': 2, + 'entry_rules': [{ + 'scope': 'Mini Wings of Liberty', + 'amount': 3 + }] + }, + 'Char': { + 'size': 3, + 'entry_rules': [{ + 'scope': '../Artifact/2' + }], + 'missions': [ + { + 'index': 0, + 'next': [2] + }, + { + 'index': 1, + 'entrance': True + } + ] + } + } + } + } + + self.generate_world(world_options) + flags = self.world.custom_mission_order.get_used_flags() + self.assertEqual(flags[MissionFlag.Terran], 13) + self.assertEqual(flags[MissionFlag.Protoss], 2) + self.assertEqual(flags.get(MissionFlag.Zerg, 0), 0) + sc2_regions = set(self.multiworld.regions.region_cache[self.player]) - {"Menu"} + self.assertEqual(len(self.world.custom_mission_order.get_used_missions()), len(sc2_regions)) + + def test_locked_and_necessary_item_appears_once(self): + # This is a filler upgrade with a parent + test_item = item_names.MARINE_OPTIMIZED_LOGISTICS + world_options = { + 'mission_order': 'custom', + 'locked_items': { test_item: 1 }, + 'custom_mission_order': { + 'test': { + 'type': 'column', + 'size': 5, # Give the generator some space to place the key + 'max_difficulty': 'easy', + 'missions': [{ + 'index': 4, + 'entry_rules': [{ + 'items': { test_item: 1 } + }] + }] + } + } + } + + self.assertNotEqual(item_tables.item_table[test_item].classification, ItemClassification.progression, f"Test item {test_item} won't change classification") + + self.generate_world(world_options) + test_items_in_pool = [item for item in self.multiworld.itempool if item.name == test_item] + test_items_in_pool += [item for item in self.multiworld.precollected_items[self.player] if item.name == test_item] + self.assertEqual(len(test_items_in_pool), 1) + self.assertEqual(test_items_in_pool[0].classification, ItemClassification.progression) + + def test_start_inventory_and_necessary_item_appears_once(self): + # This is a filler upgrade with a parent + test_item = item_names.ZERGLING_METABOLIC_BOOST + world_options = { + 'mission_order': 'custom', + 'start_inventory': { test_item: 1 }, + 'custom_mission_order': { + 'test': { + 'type': 'column', + 'size': 5, # Give the generator some space to place the key + 'max_difficulty': 'easy', + 'missions': [{ + 'index': 4, + 'entry_rules': [{ + 'items': { test_item: 1 } + }] + }] + } + } + } + + self.generate_world(world_options) + test_items_in_pool = [item for item in self.multiworld.itempool if item.name == test_item] + self.assertEqual(len(test_items_in_pool), 0) + test_items_in_start_inventory = [item for item in self.multiworld.precollected_items[self.player] if item.name == test_item] + self.assertEqual(len(test_items_in_start_inventory), 1) + + def test_start_inventory_and_locked_and_necessary_item_appears_once(self): + # This is a filler upgrade with a parent + test_item = item_names.ZERGLING_METABOLIC_BOOST + world_options = { + 'mission_order': 'custom', + 'start_inventory': { test_item: 1 }, + 'locked_items': { test_item: 1 }, + 'custom_mission_order': { + 'test': { + 'type': 'column', + 'size': 5, # Give the generator some space to place the key + 'max_difficulty': 'easy', + 'missions': [{ + 'index': 4, + 'entry_rules': [{ + 'items': { test_item: 1 } + }] + }] + } + } + } + + self.generate_world(world_options) + test_items_in_pool = [item for item in self.multiworld.itempool if item.name == test_item] + self.assertEqual(len(test_items_in_pool), 0) + test_items_in_start_inventory = [item for item in self.multiworld.precollected_items[self.player] if item.name == test_item] + self.assertEqual(len(test_items_in_start_inventory), 1) + + def test_key_item_rule_creates_correct_item_amount(self): + # This is an item that normally only exists once + test_item = item_names.ZERGLING + test_amount = 3 + world_options = { + 'mission_order': 'custom', + 'locked_items': { test_item: 1 }, # Make sure it is generated as normal + 'custom_mission_order': { + 'test': { + 'type': 'column', + 'size': 12, # Give the generator some space to place the keys + 'max_difficulty': 'easy', + 'mission_pool': ['zerg missions'], # Make sure the item isn't excluded by race selection + 'missions': [{ + 'index': 10, + 'entry_rules': [{ + 'items': { test_item: test_amount } # Require more than the usual item amount + }] + }] + } + } + } + + self.generate_world(world_options) + test_items_in_pool = [item for item in self.multiworld.itempool if item.name == test_item] + test_items_in_start_inventory = [item for item in self.multiworld.precollected_items[self.player] if item.name == test_item] + self.assertEqual(len(test_items_in_pool + test_items_in_start_inventory), test_amount) diff --git a/worlds/sc2/test/test_generation.py b/worlds/sc2/test/test_generation.py new file mode 100644 index 00000000..faedb19a --- /dev/null +++ b/worlds/sc2/test/test_generation.py @@ -0,0 +1,1228 @@ +""" +Unit tests for world generation +""" +from typing import * +from .test_base import Sc2SetupTestBase + +from .. import mission_groups, mission_tables, options, locations, SC2Mission, SC2Campaign, SC2Race, unreleased_items +from ..item import item_groups, item_tables, item_names +from .. import get_all_missions, get_random_first_mission +from ..options import EnabledCampaigns, NovaGhostOfAChanceVariant, MissionOrder, ExcludeOverpoweredItems, \ + VanillaItemsOnly, MaximumCampaignSize + + +class TestItemFiltering(Sc2SetupTestBase): + def test_explicit_locks_excludes_interact_and_set_flags(self): + world_options = { + 'locked_items': { + item_names.MARINE: 0, + item_names.MARAUDER: 0, + item_names.MEDIVAC: 1, + item_names.FIREBAT: 1, + item_names.ZEALOT: 0, + item_names.PROGRESSIVE_REGENERATIVE_BIO_STEEL: 2, + }, + 'excluded_items': { + item_names.MARINE: 0, + item_names.MARAUDER: 0, + item_names.MEDIVAC: 0, + item_names.FIREBAT: 1, + item_names.ZERGLING: 0, + item_names.PROGRESSIVE_REGENERATIVE_BIO_STEEL: 2, + } + } + self.generate_world(world_options) + self.assertTrue(self.multiworld.itempool) + itempool = [item.name for item in self.multiworld.itempool] + self.assertIn(item_names.MARINE, itempool) + self.assertIn(item_names.MARAUDER, itempool) + self.assertIn(item_names.MEDIVAC, itempool) + self.assertIn(item_names.FIREBAT, itempool) + self.assertIn(item_names.ZEALOT, itempool) + self.assertNotIn(item_names.ZERGLING, itempool) + regen_biosteel_items = [x for x in itempool if x == item_names.PROGRESSIVE_REGENERATIVE_BIO_STEEL] + self.assertEqual(len(regen_biosteel_items), 2) + + def test_unexcludes_cancel_out_excludes(self): + world_options = { + 'grant_story_tech': options.GrantStoryTech.option_grant, + 'excluded_items': { + item_groups.ItemGroupNames.NOVA_EQUIPMENT: 15, + item_names.MARINE_PROGRESSIVE_STIMPACK: 1, + item_names.MARAUDER_PROGRESSIVE_STIMPACK: 2, + item_names.MARINE: 0, + item_names.MARAUDER: 0, + item_names.REAPER: 1, + item_names.DIAMONDBACK: 0, + item_names.HELLION: 1, + # Additional excludes to increase the likelihood that unexcluded items actually appear + item_groups.ItemGroupNames.STARPORT_UNITS: 0, + item_names.WARHOUND: 0, + item_names.VULTURE: 0, + item_names.WIDOW_MINE: 0, + item_names.THOR: 0, + item_names.GHOST: 0, + item_names.SPECTRE: 0, + item_groups.ItemGroupNames.MENGSK_UNITS: 0, + item_groups.ItemGroupNames.TERRAN_VETERANCY_UNITS: 0, + }, + 'unexcluded_items': { + item_names.NOVA_PLASMA_RIFLE: 1, # Necessary to pass logic + item_names.NOVA_PULSE_GRENADES: 0, # Necessary to pass logic + item_names.NOVA_JUMP_SUIT_MODULE: 0, # Necessary to pass logic + item_groups.ItemGroupNames.BARRACKS_UNITS: 0, + item_names.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE: 1, + item_names.HELLION: 1, + item_names.MARINE_PROGRESSIVE_STIMPACK: 1, + item_names.MARAUDER_PROGRESSIVE_STIMPACK: 0, + # Additional unexcludes for logic + item_names.MEDIVAC: 0, + item_names.BATTLECRUISER: 0, + item_names.SCIENCE_VESSEL: 0, + }, + # Terran-only + 'enabled_campaigns': { + SC2Campaign.WOL.campaign_name, + SC2Campaign.NCO.campaign_name + }, + } + self.generate_world(world_options) + self.assertTrue(self.multiworld.itempool) + itempool = [item.name for item in self.multiworld.itempool] + self.assertIn(item_names.MARINE, itempool) + self.assertIn(item_names.MARAUDER, itempool) + self.assertIn(item_names.REAPER, itempool) + self.assertEqual(itempool.count(item_names.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE), 1, "Stealth suit occurred the wrong number of times") + self.assertIn(item_names.HELLION, itempool) + self.assertEqual(itempool.count(item_names.MARINE_PROGRESSIVE_STIMPACK), 2, f"Marine stimpacks weren't unexcluded (seed {self.multiworld.seed})") + self.assertEqual(itempool.count(item_names.MARAUDER_PROGRESSIVE_STIMPACK), 2, f"Marauder stimpacks weren't unexcluded (seed {self.multiworld.seed})") + self.assertNotIn(item_names.DIAMONDBACK, itempool) + self.assertNotIn(item_names.NOVA_BLAZEFIRE_GUNBLADE, itempool) + self.assertNotIn(item_names.NOVA_ENERGY_SUIT_MODULE, itempool) + + def test_excluding_groups_excludes_all_items_in_group(self): + world_options = { + 'excluded_items': [ + item_groups.ItemGroupNames.BARRACKS_UNITS.lower(), + ] + } + self.generate_world(world_options) + itempool = [item.name for item in self.multiworld.itempool] + self.assertIn(item_names.MARINE, self.world.options.excluded_items) + for item_name in item_groups.barracks_units: + self.assertNotIn(item_name, itempool) + + def test_excluding_mission_groups_excludes_all_missions_in_group(self): + world_options = { + 'excluded_missions': [ + mission_groups.MissionGroupNames.HOTS_ZERUS_MISSIONS, + ], + 'mission_order': options.MissionOrder.option_grid, + } + self.generate_world(world_options) + missions = get_all_missions(self.world.custom_mission_order) + self.assertTrue(missions) + self.assertNotIn(mission_tables.SC2Mission.WAKING_THE_ANCIENT, missions) + self.assertNotIn(mission_tables.SC2Mission.THE_CRUCIBLE, missions) + self.assertNotIn(mission_tables.SC2Mission.SUPREME, missions) + + def test_excluding_campaigns_excludes_campaign_specific_items(self) -> None: + world_options = { + 'enabled_campaigns': { + SC2Campaign.WOL.campaign_name + }, + } + self.generate_world(world_options) + self.assertTrue(self.multiworld.itempool) + world_items = [(item.name, item_tables.item_table[item.name]) for item in self.multiworld.itempool] + for item_name, item_data in world_items: + self.assertNotIn(item_data.type, item_tables.ProtossItemType) + self.assertNotIn(item_data.type, item_tables.ZergItemType) + self.assertNotEqual(item_data.type, item_tables.TerranItemType.Nova_Gear) + self.assertNotEqual(item_name, item_names.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE) + + def test_starter_unit_populates_start_inventory(self): + world_options = { + 'enabled_campaigns': { + SC2Campaign.WOL.campaign_name, + }, + 'shuffle_no_build': options.ShuffleNoBuild.option_false, + 'mission_order': options.MissionOrder.option_grid, + 'starter_unit': options.StarterUnit.option_any_starter_unit, + } + self.generate_world(world_options) + self.assertTrue(self.multiworld.itempool) + self.assertTrue(self.multiworld.precollected_items[self.player]) + + def test_excluding_all_terran_missions_excludes_all_terran_items(self) -> None: + world_options = { + 'mission_order': options.MissionOrder.option_grid, + 'maximum_campaign_size': options.MaximumCampaignSize.range_end, + 'excluded_missions': [ + mission.mission_name for mission in mission_tables.SC2Mission + if mission_tables.MissionFlag.Terran in mission.flags + ], + } + self.generate_world(world_options) + self.assertTrue(self.multiworld.itempool) + world_items = [(item.name, item_tables.item_table[item.name]) for item in self.multiworld.itempool] + for item_name, item_data in world_items: + self.assertNotIn(item_data.type, item_tables.TerranItemType, f"Item '{item_name}' included when all terran missions are excluded") + + def test_excluding_all_terran_build_missions_excludes_all_terran_units(self) -> None: + world_options = { + 'mission_order': options.MissionOrder.option_grid, + 'maximum_campaign_size': options.MaximumCampaignSize.range_end, + 'excluded_missions': [ + mission.mission_name for mission in mission_tables.SC2Mission + if mission_tables.MissionFlag.Terran in mission.flags + and mission_tables.MissionFlag.NoBuild not in mission.flags + ], + } + self.generate_world(world_options) + self.assertTrue(self.multiworld.itempool) + world_items = [(item.name, item_tables.item_table[item.name]) for item in self.multiworld.itempool] + for item_name, item_data in world_items: + self.assertNotEqual(item_data.type, item_tables.TerranItemType.Unit, f"Item '{item_name}' included when all terran build missions are excluded") + self.assertNotEqual(item_data.type, item_tables.TerranItemType.Mercenary, f"Item '{item_name}' included when all terran build missions are excluded") + self.assertNotEqual(item_data.type, item_tables.TerranItemType.Building, f"Item '{item_name}' included when all terran build missions are excluded") + + def test_excluding_all_zerg_and_kerrigan_missions_excludes_all_zerg_items(self) -> None: + world_options = { + 'mission_order': options.MissionOrder.option_grid, + 'maximum_campaign_size': options.MaximumCampaignSize.range_end, + 'excluded_missions': [ + mission.mission_name for mission in mission_tables.SC2Mission + if (mission_tables.MissionFlag.Kerrigan | mission_tables.MissionFlag.Zerg) & mission.flags + ], + } + self.generate_world(world_options) + self.assertTrue(self.multiworld.itempool) + world_items = [(item.name, item_tables.item_table[item.name]) for item in self.multiworld.itempool] + for item_name, item_data in world_items: + self.assertNotIn(item_data.type, item_tables.ZergItemType, f"Item '{item_name}' included when all zerg missions are excluded") + + def test_excluding_all_zerg_build_missions_excludes_zerg_units(self) -> None: + world_options = { + 'mission_order': options.MissionOrder.option_grid, + 'maximum_campaign_size': options.MaximumCampaignSize.range_end, + 'excluded_missions': [ + *[mission.mission_name + for mission in mission_tables.SC2Mission + if mission_tables.MissionFlag.Zerg in mission.flags + and mission_tables.MissionFlag.NoBuild not in mission.flags], + mission_tables.SC2Mission.ENEMY_WITHIN.mission_name, + ], + } + self.generate_world(world_options) + self.assertTrue(self.multiworld.itempool) + world_items = [(item.name, item_tables.item_table[item.name]) for item in self.multiworld.itempool] + for item_name, item_data in world_items: + self.assertNotEqual(item_data.type, item_tables.ZergItemType.Unit, f"Item '{item_name}' included when all zerg build missions are excluded") + self.assertNotEqual(item_data.type, item_tables.ZergItemType.Mercenary, f"Item '{item_name}' included when all zerg build missions are excluded") + + def test_excluding_all_protoss_missions_excludes_all_protoss_items(self) -> None: + world_options = { + 'mission_order': options.MissionOrder.option_grid, + 'maximum_campaign_size': options.MaximumCampaignSize.range_end, + 'accessibility': 'locations', + 'excluded_missions': [ + *[mission.mission_name + for mission in mission_tables.SC2Mission + if mission_tables.MissionFlag.Protoss in mission.flags], + ], + } + self.generate_world(world_options) + self.assertTrue(self.multiworld.itempool) + world_items = [(item.name, item_tables.item_table[item.name]) for item in self.multiworld.itempool] + for item_name, item_data in world_items: + self.assertNotIn(item_data.type, item_tables.ProtossItemType, f"Item '{item_name}' included when all protoss missions are excluded") + + def test_excluding_all_protoss_build_missions_excludes_protoss_units(self) -> None: + world_options = { + 'mission_order': options.MissionOrder.option_grid, + 'maximum_campaign_size': options.MaximumCampaignSize.range_end, + 'accessibility': 'locations', + 'excluded_missions': [ + *[mission.mission_name + for mission in mission_tables.SC2Mission + if mission.race == mission_tables.SC2Race.PROTOSS + and mission_tables.MissionFlag.NoBuild not in mission.flags], + mission_tables.SC2Mission.TEMPLAR_S_RETURN.mission_name, + ], + } + self.generate_world(world_options) + self.assertTrue(self.multiworld.itempool) + world_items = [(item.name, item_tables.item_table[item.name]) for item in self.multiworld.itempool] + for item_name, item_data in world_items: + self.assertNotEqual(item_data.type, item_tables.ProtossItemType.Unit, f"Item '{item_name}' included when all protoss build missions are excluded") + self.assertNotEqual(item_data.type, item_tables.ProtossItemType.Unit_2, f"Item '{item_name}' included when all protoss build missions are excluded") + self.assertNotEqual(item_data.type, item_tables.ProtossItemType.Building, f"Item '{item_name}' included when all protoss build missions are excluded") + + def test_vanilla_items_only_excludes_terran_progressives(self) -> None: + world_options = { + 'enabled_campaigns': { + SC2Campaign.WOL.campaign_name, + SC2Campaign.NCO.campaign_name + }, + 'mission_order': options.MissionOrder.option_grid, + 'maximum_campaign_size': options.MaximumCampaignSize.range_end, + 'accessibility': 'locations', + 'vanilla_items_only': True, + } + self.generate_world(world_options) + world_items = [(item.name, item_tables.item_table[item.name]) for item in self.multiworld.itempool] + self.assertTrue(world_items) + occurrences: Dict[str, int] = {} + for item_name, _ in world_items: + if item_name in item_groups.terran_progressive_items: + if item_name in item_groups.nova_equipment: + # The option imposes no contraint on Nova equipment + continue + occurrences.setdefault(item_name, 0) + occurrences[item_name] += 1 + self.assertLessEqual(occurrences[item_name], 1, f"'{item_name}' unexpectedly appeared multiple times in the pool") + + def test_vanilla_items_only_includes_only_nova_equipment_and_vanilla_and_filler_items(self) -> None: + world_options = { + 'mission_order': options.MissionOrder.option_grid, + 'maximum_campaign_size': options.MaximumCampaignSize.range_end, + # Avoid options that lock non-vanilla items for logic + 'spear_of_adun_presence': options.SpearOfAdunPresence.option_protoss, + 'required_tactics': options.RequiredTactics.option_advanced, + 'mastery_locations': options.MasteryLocations.option_disabled, + 'accessibility': 'locations', + 'vanilla_items_only': True, + # Move the unit nerf items from the start inventory to the pool, + # else this option could push non-vanilla items past this test + 'war_council_nerfs': True, + } + + self.generate_world(world_options) + + world_items = [(item.name, item_tables.item_table[item.name]) for item in self.multiworld.itempool] + self.assertTrue(world_items) + self.assertNotIn(item_names.DESTROYER_REFORGED_BLOODSHARD_CORE, world_items) + for item_name, item_data in world_items: + if item_data.quantity == 0: + continue + self.assertIn(item_name, item_groups.vanilla_items + item_groups.nova_equipment) + + def test_evil_awoken_with_vanilla_items_only_generates(self) -> None: + world_options = { + 'enabled_campaigns': { + SC2Campaign.PROLOGUE.campaign_name, + SC2Campaign.LOTV.campaign_name + }, + 'mission_order': options.MissionOrder.option_grid, + 'maximum_campaign_size': options.MaximumCampaignSize.range_end, + 'accessibility': 'locations', + 'vanilla_items_only': True, + } + self.generate_world(world_options) + itempool = [item.name for item in self.multiworld.itempool] + self.assertTrue(itempool) + self.assertTrue(self.world.get_region(mission_tables.SC2Mission.EVIL_AWOKEN.mission_name)) + + def test_enemy_within_and_no_zerg_build_missions_generates(self) -> None: + world_options = { + # including WoL to allow for valid goal missions + 'enabled_campaigns': { + SC2Campaign.WOL.campaign_name, + SC2Campaign.HOTS.campaign_name + }, + 'excluded_missions': [ + mission.mission_name for mission in mission_tables.SC2Mission + if mission_tables.MissionFlag.Zerg in mission.flags + and mission_tables.MissionFlag.NoBuild not in mission.flags + ], + 'mission_order': options.MissionOrder.option_grid, + 'maximum_campaign_size': options.MaximumCampaignSize.range_end, + 'accessibility': 'locations', + 'vanilla_items_only': True, + } + self.generate_world(world_options) + itempool = [item.name for item in self.multiworld.itempool] + self.assertTrue(itempool) + self.assertTrue(self.world.get_region(mission_tables.SC2Mission.ENEMY_WITHIN.mission_name)) + self.assertNotIn(item_names.ULTRALISK, itempool) + self.assertNotIn(item_names.SWARM_QUEEN, itempool) + self.assertNotIn(item_names.MUTALISK, itempool) + self.assertNotIn(item_names.CORRUPTOR, itempool) + self.assertNotIn(item_names.SCOURGE, itempool) + + def test_soa_items_are_included_in_wol_when_presence_set_to_everywhere(self) -> None: + world_options = { + 'enabled_campaigns': { + SC2Campaign.WOL.campaign_name, + }, + 'spear_of_adun_presence': options.SpearOfAdunPresence.option_everywhere, + 'mission_order': options.MissionOrder.option_grid, + 'maximum_campaign_size': options.MaximumCampaignSize.range_end, + 'accessibility': 'locations', + # Ensure enough locations to fit all wanted items + 'generic_upgrade_missions': 1, + 'victory_cache': 5, + 'excluded_items': {item_groups.ItemGroupNames.BARRACKS_UNITS: 0}, + } + self.generate_world(world_options) + itempool = [item.name for item in self.multiworld.itempool] + self.assertTrue(itempool) + soa_items_in_pool = [item_name for item_name in itempool if item_tables.item_table[item_name].type == item_tables.ProtossItemType.Spear_Of_Adun] + self.assertGreater(len(soa_items_in_pool), 5) + + def test_lotv_only_doesnt_include_kerrigan_items_with_grant_story_tech(self) -> None: + world_options = { + 'enabled_campaigns': { + SC2Campaign.LOTV.campaign_name, + }, + 'mission_order': options.MissionOrder.option_grid, + 'maximum_campaign_size': options.MaximumCampaignSize.range_end, + 'accessibility': 'locations', + 'grant_story_tech': options.GrantStoryTech.option_grant, + } + self.generate_world(world_options) + missions = get_all_missions(self.world.custom_mission_order) + self.assertIn(mission_tables.SC2Mission.TEMPLE_OF_UNIFICATION, missions) + itempool = [item.name for item in self.multiworld.itempool] + self.assertTrue(itempool) + kerrigan_items_in_pool = set(item_groups.kerrigan_abilities).intersection(itempool) + self.assertFalse(kerrigan_items_in_pool) + kerrigan_passives_in_pool = set(item_groups.kerrigan_passives).intersection(itempool) + self.assertFalse(kerrigan_passives_in_pool) + + def test_excluding_zerg_units_with_morphling_enabled_doesnt_exclude_aspects(self) -> None: + world_options = { + 'enabled_campaigns': { + SC2Campaign.HOTS.campaign_name, + }, + 'required_tactics': options.RequiredTactics.option_no_logic, + 'enable_morphling': options.EnableMorphling.option_true, + 'excluded_items': [ + item_groups.ItemGroupNames.ZERG_UNITS.lower() + ], + 'unexcluded_items': [ + item_groups.ItemGroupNames.ZERG_MORPHS.lower() + ] + } + self.generate_world(world_options) + itempool = [item.name for item in self.multiworld.itempool] + self.assertTrue(itempool) + aspects_in_pool = list(set(itempool).intersection(set(item_groups.zerg_morphs))) + self.assertTrue(aspects_in_pool) + units_in_pool = list(set(itempool).intersection(set(item_groups.zerg_units)) + .difference(set(item_groups.zerg_morphs))) + self.assertFalse(units_in_pool) + + def test_excluding_zerg_units_with_morphling_disabled_should_exclude_aspects(self) -> None: + world_options = { + 'enabled_campaigns': { + SC2Campaign.HOTS.campaign_name, + }, + 'required_tactics': options.RequiredTactics.option_no_logic, + 'enable_morphling': options.EnableMorphling.option_false, + 'excluded_items': [ + item_groups.ItemGroupNames.ZERG_UNITS.lower() + ], + 'unexcluded_items': [ + item_groups.ItemGroupNames.ZERG_MORPHS.lower() + ] + } + self.generate_world(world_options) + itempool = [item.name for item in self.multiworld.itempool] + self.assertTrue(itempool) + aspects_in_pool = list(set(itempool).intersection(set(item_groups.zerg_morphs))) + if item_names.OVERLORD_OVERSEER_ASPECT in aspects_in_pool: + # Overseer morphs from Overlord, that's available always + aspects_in_pool.remove(item_names.OVERLORD_OVERSEER_ASPECT) + self.assertFalse(aspects_in_pool) + units_in_pool = list(set(itempool).intersection(set(item_groups.zerg_units)) + .difference(set(item_groups.zerg_morphs))) + self.assertFalse(units_in_pool) + + def test_deprecated_orbital_command_not_present(self) -> None: + """ + Orbital command got replaced. The item is still there for backwards compatibility. + It shouldn't be generated. + """ + world_options = {} + + self.generate_world(world_options) + itempool = [item.name for item in self.multiworld.itempool] + + self.assertTrue(itempool) + self.assertNotIn(item_names.PROGRESSIVE_ORBITAL_COMMAND, itempool) + + def test_planetary_orbital_module_not_present_without_cc_spells(self) -> None: + world_options = { + "excluded_items": [ + item_names.COMMAND_CENTER_MULE, + item_names.COMMAND_CENTER_SCANNER_SWEEP, + item_names.COMMAND_CENTER_EXTRA_SUPPLIES + ], + "locked_items": [ + item_names.PLANETARY_FORTRESS + ] + } + + self.generate_world(world_options) + itempool = [item.name for item in self.multiworld.itempool] + + self.assertTrue(itempool) + self.assertIn(item_names.PLANETARY_FORTRESS, itempool) + self.assertNotIn(item_names.PLANETARY_FORTRESS_ORBITAL_MODULE, itempool) + + def test_disabling_unit_nerfs_start_inventories_war_council_upgrades(self) -> None: + world_options = { + 'enabled_campaigns': { + SC2Campaign.PROPHECY.campaign_name, + SC2Campaign.PROLOGUE.campaign_name, + SC2Campaign.LOTV.campaign_name + }, + 'mission_order': options.MissionOrder.option_grid, + 'war_council_nerfs': options.WarCouncilNerfs.option_false, + } + + self.generate_world(world_options) + itempool = [item.name for item in self.multiworld.itempool] + war_council_item_names = set(item_groups.item_name_groups[item_groups.ItemGroupNames.WAR_COUNCIL]) + present_war_council_items = war_council_item_names.intersection(itempool) + starting_inventory = [item.name for item in self.multiworld.precollected_items[self.player]] + starting_war_council_items = war_council_item_names.intersection(starting_inventory) + + self.assertTrue(itempool) + self.assertFalse(present_war_council_items, f'Found war council upgrades when war_council_nerfs is false: {present_war_council_items}') + self.assertEqual(war_council_item_names, starting_war_council_items) + + def test_disabling_speedrun_locations_removes_them_from_the_pool(self) -> None: + world_options = { + 'enabled_campaigns': { + SC2Campaign.HOTS.campaign_name, + }, + 'mission_order': options.MissionOrder.option_grid, + 'speedrun_locations': options.SpeedrunLocations.option_disabled, + 'preventative_locations': options.PreventativeLocations.option_filler, + } + + self.generate_world(world_options) + world_regions = list(self.multiworld.regions) + world_location_names = [location.name for region in world_regions for location in region.locations] + all_location_names = [location_data.name for location_data in locations.DEFAULT_LOCATION_LIST] + speedrun_location_name = f"{mission_tables.SC2Mission.LAB_RAT.mission_name}: Win In Under 10 Minutes" + self.assertIn(speedrun_location_name, all_location_names) + self.assertNotIn(speedrun_location_name, world_location_names) + + def test_nco_and_wol_picks_correct_starting_mission(self): + world_options = { + 'enabled_campaigns': { + SC2Campaign.WOL.campaign_name, + SC2Campaign.NCO.campaign_name + }, + } + self.generate_world(world_options) + self.assertEqual(get_random_first_mission(self.world, self.world.custom_mission_order), mission_tables.SC2Mission.LIBERATION_DAY) + + def test_excluding_mission_short_name_excludes_all_variants_of_mission(self): + world_options = { + 'excluded_missions': [ + mission_tables.SC2Mission.ZERO_HOUR.mission_name.split(" (")[0] + ], + 'mission_order': options.MissionOrder.option_grid, + 'selected_races': options.SelectRaces.valid_keys, + 'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all, + 'enabled_campaigns': { + SC2Campaign.WOL.campaign_name, + }, + } + self.generate_world(world_options) + missions = get_all_missions(self.world.custom_mission_order) + self.assertTrue(missions) + self.assertNotIn(mission_tables.SC2Mission.ZERO_HOUR, missions) + self.assertNotIn(mission_tables.SC2Mission.ZERO_HOUR_Z, missions) + self.assertNotIn(mission_tables.SC2Mission.ZERO_HOUR_P, missions) + + def test_excluding_mission_variant_excludes_just_that_variant(self): + world_options = { + 'excluded_missions': [ + mission_tables.SC2Mission.ZERO_HOUR.mission_name + ], + 'mission_order': options.MissionOrder.option_grid, + 'selected_races': options.SelectRaces.valid_keys, + 'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all, + 'enabled_campaigns': { + SC2Campaign.WOL.campaign_name, + }, + } + self.generate_world(world_options) + missions = get_all_missions(self.world.custom_mission_order) + self.assertTrue(missions) + self.assertNotIn(mission_tables.SC2Mission.ZERO_HOUR, missions) + self.assertIn(mission_tables.SC2Mission.ZERO_HOUR_Z, missions) + self.assertIn(mission_tables.SC2Mission.ZERO_HOUR_P, missions) + + def test_weapon_armor_upgrades(self): + world_options = { + # Vanilla WoL with all missions + 'mission_order': options.MissionOrder.option_vanilla, + 'starter_unit': options.StarterUnit.option_off, + 'enabled_campaigns': { + SC2Campaign.WOL.campaign_name, + }, + 'start_inventory': { + item_names.GOLIATH: 1 # Don't fail with early item placement + }, + 'generic_upgrade_items': options.GenericUpgradeItems.option_individual_items, + # Disable locations in order to cause item culling + 'vanilla_locations': options.VanillaLocations.option_disabled, + 'extra_locations': options.ExtraLocations.option_disabled, + 'challenge_locations': options.ChallengeLocations.option_disabled, + 'mastery_locations': options.MasteryLocations.option_disabled, + 'speedrun_locations': options.SpeedrunLocations.option_disabled, + 'preventative_locations': options.PreventativeLocations.option_disabled, + } + + self.generate_world(world_options) + starting_inventory = [item.name for item in self.multiworld.precollected_items[self.player]] + itempool = [item.name for item in self.multiworld.itempool] + world_items = starting_inventory + itempool + vehicle_weapon_items = [x for x in world_items if x == item_names.PROGRESSIVE_TERRAN_VEHICLE_WEAPON] + other_bundle_items = [ + x for x in world_items if x in ( + item_names.PROGRESSIVE_TERRAN_WEAPON_ARMOR_UPGRADE, + item_names.PROGRESSIVE_TERRAN_WEAPON_UPGRADE, + item_names.PROGRESSIVE_TERRAN_VEHICLE_UPGRADE, + ) + ] + + # Under standard tactics you need to place L3 upgrades for available unit classes + self.assertGreaterEqual(len(vehicle_weapon_items), 3) + self.assertEqual(len(other_bundle_items), 0) + + def test_weapon_armor_upgrades_with_bundles(self): + world_options = { + # Vanilla WoL with all missions + 'mission_order': options.MissionOrder.option_vanilla, + 'starter_unit': options.StarterUnit.option_off, + 'enabled_campaigns': { + SC2Campaign.WOL.campaign_name, + }, + 'start_inventory': { + item_names.GOLIATH: 1 # Don't fail with early item placement + }, + 'generic_upgrade_items': options.GenericUpgradeItems.option_bundle_unit_class, + # Disable locations in order to cause item culling + 'vanilla_locations': options.VanillaLocations.option_disabled, + 'extra_locations': options.ExtraLocations.option_disabled, + 'challenge_locations': options.ChallengeLocations.option_disabled, + 'mastery_locations': options.MasteryLocations.option_disabled, + 'speedrun_locations': options.SpeedrunLocations.option_disabled, + 'preventative_locations': options.PreventativeLocations.option_disabled, + } + + self.generate_world(world_options) + starting_inventory = [item.name for item in self.multiworld.precollected_items[self.player]] + itempool = [item.name for item in self.multiworld.itempool] + world_items = starting_inventory + itempool + vehicle_upgrade_items = [x for x in world_items if x == item_names.PROGRESSIVE_TERRAN_VEHICLE_UPGRADE] + other_bundle_items = [ + x for x in world_items if x in ( + item_names.PROGRESSIVE_TERRAN_WEAPON_ARMOR_UPGRADE, + item_names.PROGRESSIVE_TERRAN_WEAPON_UPGRADE, + item_names.PROGRESSIVE_TERRAN_VEHICLE_WEAPON, + ) + ] + + # Under standard tactics you need to place L3 upgrades for available unit classes + self.assertGreaterEqual(len(vehicle_upgrade_items), 3) + self.assertEqual(len(other_bundle_items), 0) + + def test_weapon_armor_upgrades_all_in_air(self): + world_options = { + # Vanilla WoL with all missions + 'mission_order': options.MissionOrder.option_vanilla, + 'starter_unit': options.StarterUnit.option_off, + 'enabled_campaigns': { + SC2Campaign.WOL.campaign_name, + }, + 'all_in_map': options.AllInMap.option_air, # All-in air forces an air unit + 'start_inventory': { + item_names.GOLIATH: 1 # Don't fail with early item placement + }, + 'generic_upgrade_items': options.GenericUpgradeItems.option_individual_items, + # Disable locations in order to cause item culling + 'vanilla_locations': options.VanillaLocations.option_disabled, + 'extra_locations': options.ExtraLocations.option_disabled, + 'challenge_locations': options.ChallengeLocations.option_disabled, + 'mastery_locations': options.MasteryLocations.option_disabled, + 'speedrun_locations': options.SpeedrunLocations.option_disabled, + 'preventative_locations': options.PreventativeLocations.option_disabled, + } + + self.generate_world(world_options) + starting_inventory = [item.name for item in self.multiworld.precollected_items[self.player]] + itempool = [item.name for item in self.multiworld.itempool] + world_items = starting_inventory + itempool + vehicle_weapon_items = [x for x in world_items if x == item_names.PROGRESSIVE_TERRAN_VEHICLE_WEAPON] + ship_weapon_items = [x for x in world_items if x == item_names.PROGRESSIVE_TERRAN_SHIP_WEAPON] + + # Under standard tactics you need to place L3 upgrades for available unit classes + self.assertGreaterEqual(len(vehicle_weapon_items), 3) + self.assertGreaterEqual(len(ship_weapon_items), 3) + + def test_weapon_armor_upgrades_generic_upgrade_missions(self): + """ + Tests the case when there aren't enough missions in order to get required weapon/armor upgrades + for logic requirements. + :return: + """ + world_options = { + # Vanilla WoL with all missions + 'mission_order': options.MissionOrder.option_vanilla, + 'required_tactics': options.RequiredTactics.option_standard, + 'starter_unit': options.StarterUnit.option_off, + 'enabled_campaigns': { + SC2Campaign.WOL.campaign_name, + }, + 'all_in_map': options.AllInMap.option_air, # All-in air forces an air unit + 'start_inventory': { + item_names.GOLIATH: 1 # Don't fail with early item placement + }, + 'generic_upgrade_items': options.GenericUpgradeItems.option_individual_items, + 'generic_upgrade_missions': 100, # Fallback happens by putting weapon/armor upgrades into starting inventory + } + + self.generate_world(world_options) + starting_inventory = [item.name for item in self.multiworld.precollected_items[self.player]] + upgrade_items = [x for x in starting_inventory if x == item_names.PROGRESSIVE_TERRAN_WEAPON_ARMOR_UPGRADE] + + # Under standard tactics you need to place L3 upgrades for available unit classes + self.assertEqual(len(upgrade_items), 3) + + def test_weapon_armor_upgrades_generic_upgrade_missions_no_logic(self): + """ + Tests the case when there aren't enough missions in order to get required weapon/armor upgrades + for logic requirements. + + Except the case above it's No Logic, thus the fallback won't take place. + :return: + """ + world_options = { + # Vanilla WoL with all missions + 'mission_order': options.MissionOrder.option_vanilla, + 'required_tactics': options.RequiredTactics.option_no_logic, + 'starter_unit': options.StarterUnit.option_off, + 'enabled_campaigns': { + SC2Campaign.WOL.campaign_name, + }, + 'all_in_map': options.AllInMap.option_air, # All-in air forces an air unit + 'start_inventory': { + item_names.GOLIATH: 1 # Don't fail with early item placement + }, + 'generic_upgrade_items': options.GenericUpgradeItems.option_individual_items, + 'generic_upgrade_missions': 100, # Fallback happens by putting weapon/armor upgrades into starting inventory + } + + self.generate_world(world_options) + starting_inventory = [item.name for item in self.multiworld.precollected_items[self.player]] + upgrade_items = [x for x in starting_inventory if x == item_names.PROGRESSIVE_TERRAN_WEAPON_ARMOR_UPGRADE] + + # No logic won't take the fallback to trigger + self.assertEqual(len(upgrade_items), 0) + + def test_weapon_armor_upgrades_generic_upgrade_missions_no_countermeasure_needed(self): + world_options = { + # Vanilla WoL with all missions + 'mission_order': options.MissionOrder.option_vanilla, + 'required_tactics': options.RequiredTactics.option_standard, + 'starter_unit': options.StarterUnit.option_off, + 'enabled_campaigns': { + SC2Campaign.WOL.campaign_name, + }, + 'all_in_map': options.AllInMap.option_air, # All-in air forces an air unit + 'start_inventory': { + item_names.GOLIATH: 1 # Don't fail with early item placement + }, + 'generic_upgrade_items': options.GenericUpgradeItems.option_individual_items, + 'generic_upgrade_missions': 1, # Weapon / Armor upgrades should be available almost instantly + } + + self.generate_world(world_options) + starting_inventory = [item.name for item in self.multiworld.precollected_items[self.player]] + upgrade_items = [x for x in starting_inventory if x == item_names.PROGRESSIVE_TERRAN_WEAPON_ARMOR_UPGRADE] + + # No additional starting inventory item placement is needed + self.assertEqual(len(upgrade_items), 0) + + def test_kerrigan_levels_per_mission_triggering_pre_fill(self): + world_options = { + # Vanilla WoL with all missions + 'mission_order': options.MissionOrder.option_custom, + 'custom_mission_order': { + 'campaign': { + 'goal': True, + 'layout': { + 'type': 'column', + 'size': 3, + 'missions': [ + { + 'index': 0, + 'mission_pool': [SC2Mission.LIBERATION_DAY.mission_name] + }, + { + 'index': 1, + 'mission_pool': [SC2Mission.THE_INFINITE_CYCLE.mission_name] + }, + { + 'index': 2, + 'mission_pool': [SC2Mission.THE_RECKONING.mission_name] + }, + ] + } + } + }, + 'required_tactics': options.RequiredTactics.option_standard, + 'starter_unit': options.StarterUnit.option_off, + 'generic_upgrade_items': options.GenericUpgradeItems.option_individual_items, + 'grant_story_levels': options.GrantStoryLevels.option_disabled, + 'kerrigan_levels_per_mission_completed': 1, + 'kerrigan_level_item_distribution': options.KerriganLevelItemDistribution.option_size_2, + } + + self.generate_world(world_options) + starting_inventory = [item.name for item in self.multiworld.precollected_items[self.player]] + kerrigan_1_stacks = [x for x in starting_inventory if x == item_names.KERRIGAN_LEVELS_1] + + self.assertGreater(len(kerrigan_1_stacks), 0) + + def test_kerrigan_levels_per_mission_and_generic_upgrades_both_triggering_pre_fill(self): + world_options = { + # Vanilla WoL with all missions + 'mission_order': options.MissionOrder.option_custom, + 'custom_mission_order': { + 'campaign': { + 'goal': True, + 'layout': { + 'type': 'column', + 'size': 3, + 'missions': [ + { + 'index': 0, + 'mission_pool': [SC2Mission.LIBERATION_DAY.mission_name] + }, + { + 'index': 1, + 'mission_pool': [SC2Mission.THE_INFINITE_CYCLE.mission_name] + }, + { + 'index': 2, + 'mission_pool': [SC2Mission.THE_RECKONING.mission_name] + }, + ] + } + } + }, + 'required_tactics': options.RequiredTactics.option_standard, + 'starter_unit': options.StarterUnit.option_off, + 'generic_upgrade_items': options.GenericUpgradeItems.option_individual_items, + 'grant_story_levels': options.GrantStoryLevels.option_disabled, + 'kerrigan_levels_per_mission_completed': 1, + 'kerrigan_level_item_distribution': options.KerriganLevelItemDistribution.option_size_2, + 'generic_upgrade_missions': 100, # Weapon / Armor upgrades + } + + self.generate_world(world_options) + starting_inventory = [item.name for item in self.multiworld.precollected_items[self.player]] + itempool = [item.name for item in self.multiworld.itempool] + kerrigan_1_stacks = [x for x in starting_inventory if x == item_names.KERRIGAN_LEVELS_1] + upgrade_items = [x for x in starting_inventory if x == item_names.PROGRESSIVE_ZERG_WEAPON_ARMOR_UPGRADE] + + self.assertGreater(len(kerrigan_1_stacks), 0) # Kerrigan levels were added + self.assertEqual(len(upgrade_items), 3) # W/A upgrades were added + self.assertNotIn(item_names.KERRIGAN_LEVELS_70, itempool) + self.assertNotIn(item_names.KERRIGAN_LEVELS_70, starting_inventory) + + + + def test_locking_required_items(self): + world_options = { + 'mission_order': options.MissionOrder.option_custom, + 'custom_mission_order': { + 'campaign': { + 'goal': True, + 'layout': { + 'type': 'column', + 'size': 2, + 'missions': [ + { + 'index': 0, + 'mission_pool': [SC2Mission.LIBERATION_DAY.mission_name] + }, + { + 'index': 1, + 'mission_pool': [SC2Mission.SUPREME.mission_name] + }, + ] + } + } + }, + 'grant_story_levels': options.GrantStoryLevels.option_additive, + 'excluded_items': [ + item_names.KERRIGAN_LEAPING_STRIKE, + item_names.KERRIGAN_MEND, + ] + } + self.generate_world(world_options) + itempool = [item.name for item in self.multiworld.itempool] + + # These items will be in the pool despite exclusions + self.assertIn(item_names.KERRIGAN_LEAPING_STRIKE, itempool) + self.assertIn(item_names.KERRIGAN_MEND, itempool) + + + def test_fully_balanced_mission_races(self): + """ + Tests whether fully balanced mission race balancing actually is fully balanced. + """ + campaign_size = 57 + self.assertEqual(campaign_size % 3, 0, "Chosen test size cannot be perfectly balanced") + world_options = { + # Reasonably large grid with enough missions to balance races + 'mission_order': options.MissionOrder.option_grid, + 'maximum_campaign_size': campaign_size, + 'enabled_campaigns': EnabledCampaigns.valid_keys, + 'selected_races': options.SelectRaces.valid_keys, + 'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all, + 'mission_race_balancing': options.EnableMissionRaceBalancing.option_fully_balanced, + } + + self.generate_world(world_options) + world_regions = [region.name for region in self.multiworld.regions] + world_regions.remove('Menu') + missions = [mission_tables.lookup_name_to_mission[region] for region in world_regions] + race_flags = [mission_tables.MissionFlag.Terran, mission_tables.MissionFlag.Zerg, mission_tables.MissionFlag.Protoss] + race_counts = { flag: sum(flag in mission.flags for mission in missions) for flag in race_flags } + + self.assertEqual(race_counts[mission_tables.MissionFlag.Terran], race_counts[mission_tables.MissionFlag.Zerg]) + self.assertEqual(race_counts[mission_tables.MissionFlag.Zerg], race_counts[mission_tables.MissionFlag.Protoss]) + + def test_setting_filter_weight_to_zero_excludes_that_item(self) -> None: + world_options = { + 'filler_items_distribution': { + item_names.STARTING_MINERALS: 0, + item_names.STARTING_VESPENE: 1, + item_names.STARTING_SUPPLY: 0, + item_names.MAX_SUPPLY: 0, + item_names.REDUCED_MAX_SUPPLY: 0, + item_names.SHIELD_REGENERATION: 0, + item_names.BUILDING_CONSTRUCTION_SPEED: 0, + }, + # Exclude many items to get filler to generate + 'excluded_items': { + item_groups.ItemGroupNames.TERRAN_VETERANCY_UNITS: 0, + }, + 'max_number_of_upgrades': 2, + 'mission_order': options.MissionOrder.option_grid, + 'selected_races': { + SC2Race.TERRAN.get_title(), + }, + 'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all, + } + + self.generate_world(world_options) + itempool = [item.name for item in self.multiworld.itempool] + + self.assertNotIn(item_names.STARTING_MINERALS, itempool) + self.assertNotIn(item_names.STARTING_SUPPLY, itempool) + self.assertNotIn(item_names.MAX_SUPPLY, itempool) + self.assertNotIn(item_names.REDUCED_MAX_SUPPLY, itempool) + self.assertNotIn(item_names.SHIELD_REGENERATION, itempool) + self.assertNotIn(item_names.BUILDING_CONSTRUCTION_SPEED, itempool) + + self.assertIn(item_names.STARTING_VESPENE, itempool) + + def test_shields_filler_doesnt_appear_if_no_protoss_missions_appear(self) -> None: + world_options = { + 'filler_items_distribution': { + item_names.STARTING_MINERALS: 1, + item_names.STARTING_VESPENE: 0, + item_names.STARTING_SUPPLY: 0, + item_names.MAX_SUPPLY: 0, + item_names.REDUCED_MAX_SUPPLY: 1, + item_names.SHIELD_REGENERATION: 1, + item_names.BUILDING_CONSTRUCTION_SPEED: 0, + }, + # Exclude many items to get filler to generate + 'excluded_items': { + item_groups.ItemGroupNames.TERRAN_VETERANCY_UNITS: 0, + item_groups.ItemGroupNames.ZERG_MORPHS: 0, + }, + 'max_number_of_upgrades': 2, + 'mission_order': options.MissionOrder.option_grid, + 'selected_races': { + SC2Race.TERRAN.get_title(), + SC2Race.ZERG.get_title(), + }, + 'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all, + } + + self.generate_world(world_options) + itempool = [item.name for item in self.multiworld.itempool] + + self.assertNotIn(item_names.SHIELD_REGENERATION, itempool) + + self.assertNotIn(item_names.STARTING_VESPENE, itempool) + self.assertNotIn(item_names.STARTING_SUPPLY, itempool) + self.assertNotIn(item_names.MAX_SUPPLY, itempool) + self.assertNotIn(item_names.BUILDING_CONSTRUCTION_SPEED, itempool) + + self.assertIn(item_names.STARTING_MINERALS, itempool) + self.assertIn(item_names.REDUCED_MAX_SUPPLY, itempool) + + def test_weapon_armor_upgrade_items_capped_by_max_upgrade_level(self) -> None: + MAX_LEVEL = 3 + world_options = { + 'locked_items': { + item_groups.ItemGroupNames.TERRAN_GENERIC_UPGRADES: MAX_LEVEL, + item_groups.ItemGroupNames.ZERG_GENERIC_UPGRADES: MAX_LEVEL, + item_groups.ItemGroupNames.PROTOSS_GENERIC_UPGRADES: MAX_LEVEL + 1, + }, + 'max_upgrade_level': MAX_LEVEL, + 'mission_order': options.MissionOrder.option_grid, + 'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all, + 'generic_upgrade_items': options.GenericUpgradeItems.option_bundle_weapon_and_armor + } + + self.generate_world(world_options) + itempool = [item.name for item in self.multiworld.itempool] + upgrade_item_counts: Dict[str, int] = {} + for item_name in itempool: + if item_tables.item_table[item_name].type in ( + item_tables.TerranItemType.Upgrade, + item_tables.ZergItemType.Upgrade, + item_tables.ProtossItemType.Upgrade, + ): + upgrade_item_counts[item_name] = upgrade_item_counts.get(item_name, 0) + 1 + expected_result = { + item_names.PROGRESSIVE_TERRAN_ARMOR_UPGRADE: MAX_LEVEL, + item_names.PROGRESSIVE_TERRAN_WEAPON_UPGRADE: MAX_LEVEL, + item_names.PROGRESSIVE_ZERG_ARMOR_UPGRADE: MAX_LEVEL, + item_names.PROGRESSIVE_ZERG_WEAPON_UPGRADE: MAX_LEVEL, + item_names.PROGRESSIVE_PROTOSS_ARMOR_UPGRADE: MAX_LEVEL + 1, + item_names.PROGRESSIVE_PROTOSS_WEAPON_UPGRADE: MAX_LEVEL + 1, + } + self.assertDictEqual(expected_result, upgrade_item_counts) + + def test_ghost_of_a_chance_generates_without_nco(self) -> None: + world_options = { + 'mission_order': MissionOrder.option_custom, + 'nova_ghost_of_a_chance_variant': NovaGhostOfAChanceVariant.option_auto, + 'custom_mission_order': { + 'test': { + 'type': 'column', + 'size': 1, # Give the generator some space to place the key + 'mission_pool': [ + SC2Mission.GHOST_OF_A_CHANCE.mission_name + ] + } + } + } + + self.generate_world(world_options) + itempool = [item.name for item in self.multiworld.itempool] + + self.assertNotIn(item_names.NOVA_C20A_CANISTER_RIFLE, itempool) + self.assertNotIn(item_names.NOVA_DOMINATION, itempool) + + def test_ghost_of_a_chance_generates_using_nco_nova(self) -> None: + world_options = { + 'mission_order': MissionOrder.option_custom, + 'nova_ghost_of_a_chance_variant': NovaGhostOfAChanceVariant.option_nco, + 'custom_mission_order': { + 'test': { + 'type': 'column', + 'size': 2, # Give the generator some space to place the key + 'mission_pool': [ + SC2Mission.LIBERATION_DAY.mission_name, # Starter mission + SC2Mission.GHOST_OF_A_CHANCE.mission_name, + ] + } + } + } + + self.generate_world(world_options) + itempool = [item.name for item in self.multiworld.itempool] + + self.assertGreater(len({item_names.NOVA_C20A_CANISTER_RIFLE, item_names.NOVA_DOMINATION}.intersection(itempool)), 0) + + def test_ghost_of_a_chance_generates_with_nco(self) -> None: + world_options = { + 'mission_order': MissionOrder.option_custom, + 'nova_ghost_of_a_chance_variant': NovaGhostOfAChanceVariant.option_auto, + 'custom_mission_order': { + 'test': { + 'type': 'column', + 'size': 3, # Give the generator some space to place the key + 'mission_pool': [ + SC2Mission.LIBERATION_DAY.mission_name, # Starter mission + SC2Mission.GHOST_OF_A_CHANCE.mission_name, + SC2Mission.FLASHPOINT.mission_name, # A NCO mission + ] + } + } + } + + self.generate_world(world_options) + itempool = [item.name for item in self.multiworld.itempool] + + self.assertGreater(len({item_names.NOVA_C20A_CANISTER_RIFLE, item_names.NOVA_DOMINATION}.intersection(itempool)), 0) + + def test_exclude_overpowered_items(self) -> None: + world_options = { + 'mission_order': MissionOrder.option_grid, + 'exclude_overpowered_items': ExcludeOverpoweredItems.option_true, + 'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all, + 'selected_races': [SC2Race.TERRAN.get_title()], + } + + self.generate_world(world_options) + itempool = [item.name for item in self.multiworld.itempool] + regen_biosteel_items = [item for item in itempool if item == item_names.PROGRESSIVE_REGENERATIVE_BIO_STEEL] + atx_laser_battery_items = [item for item in itempool if item == item_names.BATTLECRUISER_ATX_LASER_BATTERY] + + self.assertEqual(len(regen_biosteel_items), 2) # Progressive, only top level is excluded + self.assertEqual(len(atx_laser_battery_items), 0) # Non-progressive + + def test_exclude_overpowered_items_not_excluded(self) -> None: + world_options = { + 'mission_order': MissionOrder.option_grid, + 'exclude_overpowered_items': ExcludeOverpoweredItems.option_false, + 'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all, + 'selected_races': [SC2Race.TERRAN.get_title()], + } + + self.generate_world(world_options) + itempool = [item.name for item in self.multiworld.itempool] + regen_biosteel_items = [item for item in itempool if item == item_names.PROGRESSIVE_REGENERATIVE_BIO_STEEL] + atx_laser_battery_items = [item for item in itempool if item == item_names.BATTLECRUISER_ATX_LASER_BATTERY] + + self.assertEqual(len(regen_biosteel_items), 3) + self.assertEqual(len(atx_laser_battery_items), 1) + + def test_exclude_overpowered_items_vanilla_only(self) -> None: + world_options = { + 'mission_order': MissionOrder.option_grid, + 'exclude_overpowered_items': ExcludeOverpoweredItems.option_true, + 'vanilla_items_only': VanillaItemsOnly.option_true, + 'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all, + 'selected_races': [SC2Race.TERRAN.get_title()], + } + + self.generate_world(world_options) + itempool = [item.name for item in self.multiworld.itempool] + # Regen biosteel is in both of the lists + regen_biosteel_items = [item for item in itempool if item == item_names.PROGRESSIVE_REGENERATIVE_BIO_STEEL] + + self.assertEqual(len(regen_biosteel_items), 1) # One stack shall remain + + def test_exclude_locked_overpowered_items(self) -> None: + locked_item = item_names.BATTLECRUISER_ATX_LASER_BATTERY + world_options = { + 'mission_order': MissionOrder.option_grid, + 'exclude_overpowered_items': ExcludeOverpoweredItems.option_true, + 'locked_items': [locked_item], + 'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all, + 'selected_races': [SC2Race.TERRAN.get_title()], + } + + self.generate_world(world_options) + itempool = [item.name for item in self.multiworld.itempool] + atx_laser_battery_items = [item for item in itempool if item == locked_item] + + self.assertEqual(len(atx_laser_battery_items), 1) # Locked, remains + + def test_unreleased_item_quantity(self) -> None: + """ + Checks if all unreleased items are marked properly not to generate + """ + world_options = { + 'mission_order': MissionOrder.option_grid, + 'exclude_overpowered_items': ExcludeOverpoweredItems.option_false, + 'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all, + } + + self.generate_world(world_options) + itempool = [item.name for item in self.multiworld.itempool] + + items_to_check: List[str] = unreleased_items + for item in items_to_check: + self.assertNotIn(item, itempool) + + def test_unreleased_item_quantity_locked(self) -> None: + """ + Checks if all unreleased items are marked properly not to generate + Locking overrides this behavior - if they're locked, they must appear + """ + world_options = { + 'mission_order': MissionOrder.option_grid, + 'exclude_overpowered_items': ExcludeOverpoweredItems.option_false, + 'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all, + 'locked_items': {item_name: 0 for item_name in unreleased_items}, + } + + self.generate_world(world_options) + itempool = [item.name for item in self.multiworld.itempool] + + items_to_check: List[str] = unreleased_items + for item in items_to_check: + self.assertIn(item, itempool) + + def test_merc_excluded_excludes_merc_upgrades(self) -> None: + world_options = { + 'mission_order': MissionOrder.option_grid, + 'maximum_campaign_size': MaximumCampaignSize.range_end, + 'excluded_items': [item_name for item_name in item_groups.terran_mercenaries], + 'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all, + } + + self.generate_world(world_options) + itempool = [item.name for item in self.multiworld.itempool] + + self.assertNotIn(item_names.ROGUE_FORCES, itempool) + + def test_unexcluded_items_applies_over_op_items(self) -> None: + world_options = { + 'mission_order': MissionOrder.option_grid, + 'maximum_campaign_size': MaximumCampaignSize.range_end, + 'exclude_overpowered_items': ExcludeOverpoweredItems.option_true, + 'unexcluded_items': [item_names.SOA_TIME_STOP], + 'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all, + } + + self.generate_world(world_options) + itempool = [item.name for item in self.multiworld.itempool] + + self.assertNotIn( + item_groups.overpowered_items[0], + itempool, + f"OP item {item_groups.overpowered_items[0]} in the item pool when exclude_overpowered_items was true" + ) + self.assertIn( + item_names.SOA_TIME_STOP, + itempool, + f"{item_names.SOA_TIME_STOP} was not unexcluded by unexcluded_items when exclude_overpowered_items was true" + ) + + def test_exclude_overpowered_items_and_not_allow_unit_nerfs(self) -> None: + world_options = { + 'mission_order': MissionOrder.option_grid, + 'maximum_campaign_size': MaximumCampaignSize.range_end, + 'exclude_overpowered_items': ExcludeOverpoweredItems.option_true, + 'war_council_nerfs': options.WarCouncilNerfs.option_false, + 'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all, + } + + self.generate_world(world_options) + starting_inventory = [item.name for item in self.multiworld.precollected_items[self.player]] + + # A unit nerf happens due to excluding OP items + self.assertNotIn(item_names.MOTHERSHIP_INTEGRATED_POWER, starting_inventory) diff --git a/worlds/sc2/test/test_item_filtering.py b/worlds/sc2/test/test_item_filtering.py new file mode 100644 index 00000000..898fb6da --- /dev/null +++ b/worlds/sc2/test/test_item_filtering.py @@ -0,0 +1,88 @@ +""" +Unit tests for item filtering like pool_filter.py +""" + +from .test_base import Sc2SetupTestBase +from ..item import item_groups, item_names +from .. import options +from ..mission_tables import SC2Race + +class ItemFilterTests(Sc2SetupTestBase): + def test_excluding_all_barracks_units_excludes_infantry_upgrades(self) -> None: + world_options = { + 'excluded_items': { + item_groups.ItemGroupNames.BARRACKS_UNITS: 0 + }, + 'required_tactics': 'standard', + 'min_number_of_upgrades': 1, + 'selected_races': { + SC2Race.TERRAN.get_title() + }, + 'mission_order': 'grid', + } + self.generate_world(world_options) + self.assertTrue(self.multiworld.itempool) + races = {mission.race for mission in self.world.custom_mission_order.get_used_missions()} + self.assertIn(SC2Race.TERRAN, races) + self.assertNotIn(SC2Race.ZERG, races) + self.assertNotIn(SC2Race.PROTOSS, races) + itempool = [item.name for item in self.multiworld.itempool] + self.assertNotIn(item_names.MARINE, itempool) + self.assertNotIn(item_names.MARAUDER, itempool) + + self.assertNotIn(item_names.PROGRESSIVE_TERRAN_INFANTRY_WEAPON, itempool) + self.assertNotIn(item_names.PROGRESSIVE_TERRAN_INFANTRY_ARMOR, itempool) + self.assertNotIn(item_names.PROGRESSIVE_TERRAN_INFANTRY_UPGRADE, itempool) + + def test_excluding_one_item_of_multi_parent_doesnt_filter_children(self) -> None: + world_options = { + 'locked_items': { + item_names.SENTINEL: 1, + item_names.CENTURION: 1, + }, + 'excluded_items': { + item_names.ZEALOT: 1, + # Exclude more items to make space + item_names.WRATHWALKER: 1, + item_names.ENERGIZER: 1, + item_names.AVENGER: 1, + item_names.ARBITER: 1, + item_names.VOID_RAY: 1, + item_names.PULSAR: 1, + item_names.DESTROYER: 1, + item_names.DAWNBRINGER: 1, + }, + 'min_number_of_upgrades': 2, + 'required_tactics': 'standard', + 'selected_races': { + SC2Race.PROTOSS.get_title() + }, + 'mission_order': 'grid', + 'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all, + } + self.generate_world(world_options) + self.assertTrue(self.multiworld.itempool) + itempool = [item.name for item in self.multiworld.itempool] + self.assertIn(item_names.ZEALOT_SENTINEL_CENTURION_SHIELD_CAPACITY, itempool) + self.assertIn(item_names.ZEALOT_SENTINEL_CENTURION_LEG_ENHANCEMENTS, itempool) + + def test_excluding_all_items_in_multiparent_excludes_child_items(self) -> None: + world_options = { + 'excluded_items': { + item_names.ZEALOT: 1, + item_names.SENTINEL: 1, + item_names.CENTURION: 1, + }, + 'min_number_of_upgrades': 2, + 'required_tactics': 'standard', + 'selected_races': { + SC2Race.PROTOSS.get_title() + }, + 'mission_order': 'grid', + } + self.generate_world(world_options) + self.assertTrue(self.multiworld.itempool) + itempool = [item.name for item in self.multiworld.itempool] + self.assertNotIn(item_names.ZEALOT_SENTINEL_CENTURION_SHIELD_CAPACITY, itempool) + self.assertNotIn(item_names.ZEALOT_SENTINEL_CENTURION_LEG_ENHANCEMENTS, itempool) + diff --git a/worlds/sc2/test/test_itemdescriptions.py b/worlds/sc2/test/test_itemdescriptions.py new file mode 100644 index 00000000..a4fd6d5c --- /dev/null +++ b/worlds/sc2/test/test_itemdescriptions.py @@ -0,0 +1,18 @@ +import unittest + +from ..item import item_descriptions, item_tables + + +class TestItemDescriptions(unittest.TestCase): + def test_all_items_have_description(self) -> None: + for item_name in item_tables.item_table: + self.assertIn(item_name, item_descriptions.item_descriptions) + + def test_all_descriptions_refer_to_item_and_end_in_dot(self) -> None: + for item_name, item_desc in item_descriptions.item_descriptions.items(): + self.assertIn(item_name, item_tables.item_table) + self.assertEqual(item_desc.strip()[-1], '.', msg=f"{item_name}'s item description does not end in a '.': '{item_desc}'") + + def test_item_descriptions_follow_single_space_after_period_style(self) -> None: + for item_name, item_desc in item_descriptions.item_descriptions.items(): + self.assertNotIn('. ', item_desc, f"Double-space after period in description for {item_name}") diff --git a/worlds/sc2/test/test_itemgroups.py b/worlds/sc2/test/test_itemgroups.py new file mode 100644 index 00000000..43848d20 --- /dev/null +++ b/worlds/sc2/test/test_itemgroups.py @@ -0,0 +1,32 @@ +""" +Unit tests for item_groups.py +""" + +import unittest +from ..item import item_groups, item_tables + + +class ItemGroupsUnitTests(unittest.TestCase): + def test_all_production_structure_groups_capture_all_units(self) -> None: + self.assertCountEqual( + item_groups.terran_units, + item_groups.barracks_units + item_groups.factory_units + item_groups.starport_units + item_groups.terran_mercenaries + ) + self.assertCountEqual( + item_groups.protoss_units, + item_groups.gateway_units + item_groups.robo_units + item_groups.stargate_units + ) + + def test_terran_original_progressive_group_fully_contained_in_wol_upgrades(self) -> None: + for item_name in item_groups.terran_original_progressive_upgrades: + self.assertIn(item_tables.item_table[item_name].type, ( + item_tables.TerranItemType.Progressive, item_tables.TerranItemType.Progressive_2), f"{item_name} is not progressive") + self.assertIn(item_name, item_groups.wol_upgrades) + + def test_all_items_in_stimpack_group_are_stimpacks(self) -> None: + for item_name in item_groups.terran_stimpacks: + self.assertIn("Stimpack", item_name) + + def test_all_item_group_names_have_a_group_defined(self) -> None: + for display_name in item_groups.ItemGroupNames.get_all_group_names(): + self.assertIn(display_name, item_groups.item_name_groups) diff --git a/worlds/sc2/test/test_items.py b/worlds/sc2/test/test_items.py new file mode 100644 index 00000000..049810b9 --- /dev/null +++ b/worlds/sc2/test/test_items.py @@ -0,0 +1,170 @@ +import unittest +from typing import List, Set + +from ..item import item_tables + + +class TestItems(unittest.TestCase): + def test_grouped_upgrades_number(self) -> None: + """ + Tests if grouped upgrades have set number correctly + """ + bundled_items = item_tables.upgrade_bundles.keys() + bundled_item_data = [item_tables.get_full_item_list()[item_name] for item_name in bundled_items] + bundled_item_numbers = [item_data.number for item_data in bundled_item_data] + + check_numbers = [number == -1 for number in bundled_item_numbers] + + self.assertNotIn(False, check_numbers) + + def test_non_grouped_upgrades_number(self) -> None: + """ + Checks if non-grouped upgrades number is set correctly thus can be sent into the game. + """ + check_modulo = 4 + bundled_items = item_tables.upgrade_bundles.keys() + non_bundled_upgrades = [ + item_name for item_name in item_tables.get_full_item_list().keys() + if (item_name not in bundled_items + and item_tables.get_full_item_list()[item_name].type in item_tables.upgrade_item_types) + ] + non_bundled_upgrade_data = [item_tables.get_full_item_list()[item_name] for item_name in non_bundled_upgrades] + non_bundled_upgrade_numbers = [item_data.number for item_data in non_bundled_upgrade_data] + + check_numbers = [number % check_modulo == 0 for number in non_bundled_upgrade_numbers] + + self.assertNotIn(False, check_numbers) + + def test_bundles_contain_only_basic_elements(self) -> None: + """ + Checks if there are no bundles within bundles. + """ + bundled_items = item_tables.upgrade_bundles.keys() + bundle_elements: List[str] = [item_name for values in item_tables.upgrade_bundles.values() for item_name in values] + + for element in bundle_elements: + self.assertNotIn(element, bundled_items) + + def test_weapon_armor_level(self) -> None: + """ + Checks if Weapon/Armor upgrade level is correctly set to all Weapon/Armor upgrade items. + """ + weapon_armor_upgrades = [item for item in item_tables.get_full_item_list() if item_tables.get_item_table()[item].type in item_tables.upgrade_item_types] + + for weapon_armor_upgrade in weapon_armor_upgrades: + self.assertEqual(item_tables.get_full_item_list()[weapon_armor_upgrade].quantity, item_tables.WEAPON_ARMOR_UPGRADE_MAX_LEVEL) + + def test_item_ids_distinct(self) -> None: + """ + Verifies if there are no duplicates of item ID. + """ + item_ids: Set[int] = {item_tables.get_full_item_list()[item_name].code for item_name in item_tables.get_full_item_list()} + + self.assertEqual(len(item_ids), len(item_tables.get_full_item_list())) + + def test_number_distinct_in_item_type(self) -> None: + """ + Tests if each item is distinct for sending into the mod. + """ + item_types: List[item_tables.ItemTypeEnum] = [ + *[item.value for item in item_tables.TerranItemType], + *[item.value for item in item_tables.ZergItemType], + *[item.value for item in item_tables.ProtossItemType], + *[item.value for item in item_tables.FactionlessItemType] + ] + + self.assertGreater(len(item_types), 0) + + for item_type in item_types: + item_names: List[str] = [ + item_name for item_name in item_tables.get_full_item_list() + if item_tables.get_full_item_list()[item_name].number >= 0 # Negative numbers have special meaning + and item_tables.get_full_item_list()[item_name].type == item_type + ] + item_numbers: Set[int] = {item_tables.get_full_item_list()[item_name] for item_name in item_names} + + self.assertEqual(len(item_names), len(item_numbers)) + + def test_progressive_has_quantity(self) -> None: + """ + :return: + """ + progressive_groups: List[item_tables.ItemTypeEnum] = [ + item_tables.TerranItemType.Progressive, + item_tables.TerranItemType.Progressive_2, + item_tables.ProtossItemType.Progressive, + item_tables.ZergItemType.Progressive + ] + + quantities: List[int] = [ + item_tables.get_full_item_list()[item].quantity for item in item_tables.get_full_item_list() + if item_tables.get_full_item_list()[item].type in progressive_groups + ] + + self.assertNotIn(1, quantities) + + def test_non_progressive_quantity(self) -> None: + """ + Check if non-progressive items have quantity at most 1. + """ + non_progressive_single_entity_groups: List[item_tables.ItemTypeEnum] = [ + # Terran + item_tables.TerranItemType.Unit, + item_tables.TerranItemType.Unit_2, + item_tables.TerranItemType.Mercenary, + item_tables.TerranItemType.Armory_1, + item_tables.TerranItemType.Armory_2, + item_tables.TerranItemType.Armory_3, + item_tables.TerranItemType.Armory_4, + item_tables.TerranItemType.Armory_5, + item_tables.TerranItemType.Armory_6, + item_tables.TerranItemType.Armory_7, + item_tables.TerranItemType.Building, + item_tables.TerranItemType.Laboratory, + item_tables.TerranItemType.Nova_Gear, + # Zerg + item_tables.ZergItemType.Unit, + item_tables.ZergItemType.Mercenary, + item_tables.ZergItemType.Morph, + item_tables.ZergItemType.Strain, + item_tables.ZergItemType.Mutation_1, + item_tables.ZergItemType.Mutation_2, + item_tables.ZergItemType.Mutation_3, + item_tables.ZergItemType.Evolution_Pit, + item_tables.ZergItemType.Ability, + # Protoss + item_tables.ProtossItemType.Unit, + item_tables.ProtossItemType.Unit_2, + item_tables.ProtossItemType.Building, + item_tables.ProtossItemType.Forge_1, + item_tables.ProtossItemType.Forge_2, + item_tables.ProtossItemType.Forge_3, + item_tables.ProtossItemType.Forge_4, + item_tables.ProtossItemType.Solarite_Core, + item_tables.ProtossItemType.Spear_Of_Adun + ] + + quantities: List[int] = [ + item_tables.get_full_item_list()[item].quantity for item in item_tables.get_full_item_list() + if item_tables.get_full_item_list()[item].type in non_progressive_single_entity_groups + ] + + for quantity in quantities: + self.assertLessEqual(quantity, 1) + + def test_item_number_less_than_30(self) -> None: + """ + Checks if all item numbers are within bounds supported by game mod. + """ + not_checked_item_types: List[item_tables.ItemTypeEnum] = [ + item_tables.ZergItemType.Level + ] + items_to_check: List[str] = [ + item for item in item_tables.get_full_item_list() + if item_tables.get_full_item_list()[item].type not in not_checked_item_types + ] + + for item in items_to_check: + item_number = item_tables.get_full_item_list()[item].number + self.assertLess(item_number, 30) + diff --git a/worlds/sc2/test/test_location_groups.py b/worlds/sc2/test/test_location_groups.py new file mode 100644 index 00000000..f429464f --- /dev/null +++ b/worlds/sc2/test/test_location_groups.py @@ -0,0 +1,37 @@ +import unittest +from .. import location_groups +from ..mission_tables import SC2Mission, MissionFlag + + +class TestLocationGroups(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.location_groups = location_groups.get_location_groups() + + def test_location_categories_have_a_group(self) -> None: + self.assertIn('Victory', self.location_groups) + self.assertIn(f'{SC2Mission.LIBERATION_DAY.mission_name}: Victory', self.location_groups['Victory']) + self.assertIn(f'{SC2Mission.IN_UTTER_DARKNESS.mission_name}: Defeat', self.location_groups['Victory']) + self.assertIn('Vanilla', self.location_groups) + self.assertIn(f'{SC2Mission.WELCOME_TO_THE_JUNGLE.mission_name}: Close Relic', self.location_groups['Vanilla']) + self.assertIn('Extra', self.location_groups) + self.assertIn(f'{SC2Mission.SMASH_AND_GRAB.mission_name}: First Forcefield Area Busted', self.location_groups['Extra']) + self.assertIn('Challenge', self.location_groups) + self.assertIn(f'{SC2Mission.ZERO_HOUR.mission_name}: First Hatchery', self.location_groups['Challenge']) + self.assertIn('Mastery', self.location_groups) + self.assertIn(f'{SC2Mission.WELCOME_TO_THE_JUNGLE.mission_name}: Protoss Cleared', self.location_groups['Mastery']) + + def test_missions_have_a_group(self) -> None: + self.assertIn(SC2Mission.LIBERATION_DAY.mission_name, self.location_groups) + self.assertIn(f'{SC2Mission.LIBERATION_DAY.mission_name}: Victory', self.location_groups[SC2Mission.LIBERATION_DAY.mission_name]) + self.assertIn(f'{SC2Mission.LIBERATION_DAY.mission_name}: Special Delivery', self.location_groups[SC2Mission.LIBERATION_DAY.mission_name]) + + def test_race_swapped_locations_share_a_group(self) -> None: + self.assertIn(MissionFlag.HasRaceSwap, SC2Mission.ZERO_HOUR.flags) + ZERO_HOUR = 'Zero Hour' + self.assertNotEqual(ZERO_HOUR, SC2Mission.ZERO_HOUR.mission_name) + self.assertIn(ZERO_HOUR, self.location_groups) + self.assertIn(f'{ZERO_HOUR}: Victory', self.location_groups) + self.assertIn(f'{SC2Mission.ZERO_HOUR.mission_name}: Victory', self.location_groups[f'{ZERO_HOUR}: Victory']) + self.assertIn(f'{SC2Mission.ZERO_HOUR_P.mission_name}: Victory', self.location_groups[f'{ZERO_HOUR}: Victory']) + self.assertIn(f'{SC2Mission.ZERO_HOUR_Z.mission_name}: Victory', self.location_groups[f'{ZERO_HOUR}: Victory']) diff --git a/worlds/sc2/test/test_mission_groups.py b/worlds/sc2/test/test_mission_groups.py new file mode 100644 index 00000000..6db7325d --- /dev/null +++ b/worlds/sc2/test/test_mission_groups.py @@ -0,0 +1,9 @@ +import unittest +from .. import mission_groups + + +class TestMissionGroups(unittest.TestCase): + def test_all_mission_groups_are_defined_and_nonempty(self) -> None: + for mission_group_name in mission_groups.MissionGroupNames.get_all_group_names(): + self.assertIn(mission_group_name, mission_groups.mission_groups) + self.assertTrue(mission_groups.mission_groups[mission_group_name]) diff --git a/worlds/sc2/test/test_options.py b/worlds/sc2/test/test_options.py index 30d21f39..69b834da 100644 --- a/worlds/sc2/test/test_options.py +++ b/worlds/sc2/test/test_options.py @@ -1,7 +1,19 @@ import unittest -from .test_base import Sc2TestBase -from .. import Options, MissionTables +from typing import Dict + +from .. import options +from ..item import item_parents + class TestOptions(unittest.TestCase): - def test_campaign_size_option_max_matches_number_of_missions(self): - self.assertEqual(Options.MaximumCampaignSize.range_end, len(MissionTables.SC2Mission)) + + def test_unit_max_upgrades_matching_items(self) -> None: + upgrade_group_to_count: Dict[str, int] = {} + for parent_id, child_list in item_parents.parent_id_to_children.items(): + main_parent = item_parents.parent_present[parent_id].constraint_group + if main_parent is None: + continue + upgrade_group_to_count.setdefault(main_parent, 0) + upgrade_group_to_count[main_parent] += len(child_list) + + self.assertEqual(options.MAX_UPGRADES_OPTION, max(upgrade_group_to_count.values())) diff --git a/worlds/sc2/test/test_regions.py b/worlds/sc2/test/test_regions.py new file mode 100644 index 00000000..880a02f9 --- /dev/null +++ b/worlds/sc2/test/test_regions.py @@ -0,0 +1,40 @@ +import unittest +from .test_base import Sc2TestBase +from .. import mission_tables, SC2Campaign +from .. import options +from ..mission_order.layout_types import Grid + +class TestGridsizes(unittest.TestCase): + def test_grid_sizes_meet_specs(self): + self.assertTupleEqual((1, 2, 0), Grid.get_grid_dimensions(2)) + self.assertTupleEqual((1, 3, 0), Grid.get_grid_dimensions(3)) + self.assertTupleEqual((2, 2, 0), Grid.get_grid_dimensions(4)) + self.assertTupleEqual((2, 3, 1), Grid.get_grid_dimensions(5)) + self.assertTupleEqual((2, 4, 1), Grid.get_grid_dimensions(7)) + self.assertTupleEqual((2, 4, 0), Grid.get_grid_dimensions(8)) + self.assertTupleEqual((3, 3, 0), Grid.get_grid_dimensions(9)) + self.assertTupleEqual((2, 5, 0), Grid.get_grid_dimensions(10)) + self.assertTupleEqual((3, 4, 1), Grid.get_grid_dimensions(11)) + self.assertTupleEqual((3, 4, 0), Grid.get_grid_dimensions(12)) + self.assertTupleEqual((3, 5, 0), Grid.get_grid_dimensions(15)) + self.assertTupleEqual((4, 4, 0), Grid.get_grid_dimensions(16)) + self.assertTupleEqual((4, 6, 0), Grid.get_grid_dimensions(24)) + self.assertTupleEqual((5, 5, 0), Grid.get_grid_dimensions(25)) + self.assertTupleEqual((5, 6, 1), Grid.get_grid_dimensions(29)) + self.assertTupleEqual((5, 7, 2), Grid.get_grid_dimensions(33)) + + +class TestGridGeneration(Sc2TestBase): + options = { + "mission_order": options.MissionOrder.option_grid, + "excluded_missions": [mission_tables.SC2Mission.ZERO_HOUR.mission_name,], + "enabled_campaigns": { + SC2Campaign.WOL.campaign_name, + SC2Campaign.PROPHECY.campaign_name, + } + } + + def test_size_matches_exclusions(self): + self.assertNotIn(mission_tables.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) diff --git a/worlds/sc2/test/test_rules.py b/worlds/sc2/test/test_rules.py new file mode 100644 index 00000000..d43a4d4e --- /dev/null +++ b/worlds/sc2/test/test_rules.py @@ -0,0 +1,186 @@ +import itertools +from dataclasses import fields +from random import Random +import unittest +from typing import List, Set, Iterable + +from BaseClasses import ItemClassification, MultiWorld +import Options as CoreOptions +from .. import options, locations +from ..item import item_tables +from ..rules import SC2Logic +from ..mission_tables import SC2Race, MissionFlag, lookup_name_to_mission + + +class TestInventory: + """ + Runs checks against inventory with validation if all target items are progression and returns a random result + """ + def __init__(self) -> None: + self.random: Random = Random() + self.progression_types: Set[ItemClassification] = {ItemClassification.progression, ItemClassification.progression_skip_balancing} + + def is_item_progression(self, item: str) -> bool: + return item_tables.item_table[item].classification in self.progression_types + + def random_boolean(self): + return self.random.choice([True, False]) + + def has(self, item: str, player: int, count: int = 1): + if not self.is_item_progression(item): + raise AssertionError("Logic item {} is not a progression item".format(item)) + return self.random_boolean() + + def has_any(self, items: Set[str], player: int): + non_progression_items = [item for item in items if not self.is_item_progression(item)] + if len(non_progression_items) > 0: + raise AssertionError("Logic items {} are not progression items".format(non_progression_items)) + return self.random_boolean() + + def has_all(self, items: Set[str], player: int): + return self.has_any(items, player) + + def has_group(self, item_group: str, player: int, count: int = 1): + return self.random_boolean() + + def count_group(self, item_name_group: str, player: int) -> int: + return self.random.randrange(0, 20) + + def count(self, item: str, player: int) -> int: + if not self.is_item_progression(item): + raise AssertionError("Item {} is not a progression item".format(item)) + random_value: int = self.random.randrange(0, 5) + if random_value == 4: # 0-3 has a higher chance due to logic rules + return self.random.randrange(4, 100) + else: + return random_value + + def count_from_list(self, items: Iterable[str], player: int) -> int: + return sum(self.count(item_name, player) for item_name in items) + + def count_from_list_unique(self, items: Iterable[str], player: int) -> int: + return sum(self.count(item_name, player) for item_name in items) + + +class TestWorld: + """ + Mock world to simulate different player options for logic rules + """ + def __init__(self) -> None: + defaults = dict() + for field in fields(options.Starcraft2Options): + field_class = field.type + option_name = field.name + if isinstance(field_class, str): + if field_class in globals(): + field_class = globals()[field_class] + else: + field_class = CoreOptions.__dict__[field.type] + defaults[option_name] = field_class(options.get_option_value(None, option_name)) + self.options: options.Starcraft2Options = options.Starcraft2Options(**defaults) + + self.options.mission_order.value = options.MissionOrder.option_vanilla_shuffled + + self.player = 1 + self.multiworld = MultiWorld(1) + + +class TestRules(unittest.TestCase): + def setUp(self) -> None: + self.required_tactics_values: List[int] = [ + options.RequiredTactics.option_standard, options.RequiredTactics.option_advanced + ] + self.all_in_map_values: List[int] = [ + options.AllInMap.option_ground, options.AllInMap.option_air + ] + self.take_over_ai_allies_values: List[int] = [ + options.TakeOverAIAllies.option_true, options.TakeOverAIAllies.option_false + ] + self.kerrigan_presence_values: List[int] = [ + options.KerriganPresence.option_vanilla, options.KerriganPresence.option_not_present + ] + self.NUM_TEST_RUNS = 100 + + @staticmethod + def _get_world( + required_tactics: int = options.RequiredTactics.default, + all_in_map: int = options.AllInMap.default, + take_over_ai_allies: int = options.TakeOverAIAllies.default, + kerrigan_presence: int = options.KerriganPresence.default, + # setting this to everywhere catches one extra logic check for Amon's Fall without missing any + spear_of_adun_passive_presence: int = options.SpearOfAdunPassiveAbilityPresence.option_everywhere, + ) -> TestWorld: + test_world = TestWorld() + test_world.options.required_tactics.value = required_tactics + test_world.options.all_in_map.value = all_in_map + test_world.options.take_over_ai_allies.value = take_over_ai_allies + test_world.options.kerrigan_presence.value = kerrigan_presence + test_world.options.spear_of_adun_passive_ability_presence.value = spear_of_adun_passive_presence + test_world.logic = SC2Logic(test_world) # type: ignore + return test_world + + def test_items_in_rules_are_progression(self): + test_inventory = TestInventory() + for option in self.required_tactics_values: + test_world = self._get_world(required_tactics=option) + location_data = locations.get_locations(test_world) + for location in location_data: + for _ in range(self.NUM_TEST_RUNS): + location.rule(test_inventory) + + def test_items_in_all_in_are_progression(self): + test_inventory = TestInventory() + for test_options in itertools.product(self.required_tactics_values, self.all_in_map_values): + test_world = self._get_world(required_tactics=test_options[0], all_in_map=test_options[1]) + for location in locations.get_locations(test_world): + if 'All-In' not in location.region: + continue + for _ in range(self.NUM_TEST_RUNS): + location.rule(test_inventory) + + def test_items_in_kerriganless_missions_are_progression(self): + test_inventory = TestInventory() + for test_options in itertools.product(self.required_tactics_values, self.kerrigan_presence_values): + test_world = self._get_world(required_tactics=test_options[0], kerrigan_presence=test_options[1]) + for location in locations.get_locations(test_world): + mission = lookup_name_to_mission[location.region] + if MissionFlag.Kerrigan not in mission.flags: + continue + for _ in range(self.NUM_TEST_RUNS): + location.rule(test_inventory) + + def test_items_in_ai_takeover_missions_are_progression(self): + test_inventory = TestInventory() + for test_options in itertools.product(self.required_tactics_values, self.take_over_ai_allies_values): + test_world = self._get_world(required_tactics=test_options[0], take_over_ai_allies=test_options[1]) + for location in locations.get_locations(test_world): + mission = lookup_name_to_mission[location.region] + if MissionFlag.AiAlly not in mission.flags: + continue + for _ in range(self.NUM_TEST_RUNS): + location.rule(test_inventory) + + def test_items_in_hard_rules_are_progression(self): + test_inventory = TestInventory() + test_world = TestWorld() + test_world.options.required_tactics.value = options.RequiredTactics.option_any_units + test_world.logic = SC2Logic(test_world) + location_data = locations.get_locations(test_world) + for location in location_data: + if location.hard_rule is not None: + for _ in range(10): + location.hard_rule(test_inventory) + + def test_items_in_any_units_rules_are_progression(self): + test_inventory = TestInventory() + test_world = TestWorld() + test_world.options.required_tactics.value = options.RequiredTactics.option_any_units + logic = SC2Logic(test_world) + test_world.logic = logic + for race in (SC2Race.TERRAN, SC2Race.PROTOSS, SC2Race.ZERG): + for target in range(1, 5): + rule = logic.has_race_units(target, race) + for _ in range(10): + rule(test_inventory) + + diff --git a/worlds/sc2/test/test_usecases.py b/worlds/sc2/test/test_usecases.py new file mode 100644 index 00000000..a87d1766 --- /dev/null +++ b/worlds/sc2/test/test_usecases.py @@ -0,0 +1,492 @@ +""" +Unit tests for yaml usecases we want to support +""" + +from .test_base import Sc2SetupTestBase +from .. import get_all_missions, mission_tables, options +from ..item import item_groups, item_tables, item_names +from ..mission_tables import SC2Race, SC2Mission, SC2Campaign, MissionFlag +from ..options import EnabledCampaigns, MasteryLocations + + +class TestSupportedUseCases(Sc2SetupTestBase): + def test_vanilla_all_campaigns_generates(self) -> None: + world_options = { + 'mission_order': options.MissionOrder.option_vanilla, + 'enabled_campaigns': EnabledCampaigns.valid_keys, + } + + self.generate_world(world_options) + world_regions = [region.name for region in self.multiworld.regions if region.name != "Menu"] + + self.assertEqual(len(world_regions), 83, "Unexpected number of missions for vanilla mission order") + + def test_terran_with_nco_units_only_generates(self): + world_options = { + 'enabled_campaigns': { + SC2Campaign.WOL.campaign_name, + SC2Campaign.NCO.campaign_name + }, + 'excluded_items': { + item_groups.ItemGroupNames.TERRAN_UNITS: 0, + }, + 'unexcluded_items': { + item_groups.ItemGroupNames.NCO_UNITS: 0, + }, + 'max_number_of_upgrades': 2, + } + + self.generate_world(world_options) + self.assertTrue(self.multiworld.itempool) + world_item_names = [item.name for item in self.multiworld.itempool] + + self.assertIn(item_names.MARINE, world_item_names) + self.assertIn(item_names.RAVEN, world_item_names) + self.assertIn(item_names.LIBERATOR, world_item_names) + self.assertIn(item_names.BATTLECRUISER, world_item_names) + self.assertNotIn(item_names.DIAMONDBACK, world_item_names) + self.assertNotIn(item_names.DIAMONDBACK_BURST_CAPACITORS, world_item_names) + self.assertNotIn(item_names.VIKING, world_item_names) + + def test_nco_with_nobuilds_excluded_generates(self): + world_options = { + 'enabled_campaigns': { + SC2Campaign.NCO.campaign_name + }, + 'shuffle_no_build': options.ShuffleNoBuild.option_false, + 'mission_order': options.MissionOrder.option_mini_campaign, + } + + self.generate_world(world_options) + self.assertTrue(self.multiworld.itempool) + missions = get_all_missions(self.world.custom_mission_order) + + self.assertNotIn(mission_tables.SC2Mission.THE_ESCAPE, missions) + self.assertNotIn(mission_tables.SC2Mission.IN_THE_ENEMY_S_SHADOW, missions) + for mission in missions: + self.assertEqual(mission_tables.SC2Campaign.NCO, mission.campaign) + + def test_terran_with_nco_upgrades_units_only_generates(self): + world_options = { + 'enabled_campaigns': { + SC2Campaign.WOL.campaign_name, + SC2Campaign.NCO.campaign_name + }, + 'mission_order': options.MissionOrder.option_vanilla_shuffled, + 'excluded_items': { + item_groups.ItemGroupNames.TERRAN_ITEMS: 0, + }, + 'unexcluded_items': { + item_groups.ItemGroupNames.NCO_MAX_PROGRESSIVE_ITEMS: 0, + item_groups.ItemGroupNames.NCO_MIN_PROGRESSIVE_ITEMS: 1, + }, + 'excluded_missions': [ + # These missions have trouble fulfilling Terran Power Rating under these terms + SC2Mission.SUPERNOVA.mission_name, + SC2Mission.WELCOME_TO_THE_JUNGLE.mission_name, + SC2Mission.TROUBLE_IN_PARADISE.mission_name, + ], + 'mastery_locations': MasteryLocations.option_disabled, + } + + self.generate_world(world_options) + world_item_names = [item.name for item in self.multiworld.itempool + self.multiworld.precollected_items[1]] + self.assertTrue(world_item_names) + missions = get_all_missions(self.world.custom_mission_order) + + for mission in missions: + self.assertIn(mission_tables.MissionFlag.Terran, mission.flags) + self.assertIn(item_names.MARINE, world_item_names) + self.assertIn(item_names.MARAUDER, world_item_names) + self.assertIn(item_names.BUNKER, world_item_names) + self.assertIn(item_names.BANSHEE, world_item_names) + self.assertIn(item_names.BATTLECRUISER_ATX_LASER_BATTERY, world_item_names) + self.assertIn(item_names.NOVA_C20A_CANISTER_RIFLE, world_item_names) + self.assertGreaterEqual(world_item_names.count(item_names.BANSHEE_PROGRESSIVE_CROSS_SPECTRUM_DAMPENERS), 2) + self.assertGreaterEqual(world_item_names.count(item_names.PROGRESSIVE_TERRAN_SHIP_WEAPON), 3) + self.assertNotIn(item_names.MEDIC, world_item_names) + self.assertNotIn(item_names.PSI_DISRUPTER, world_item_names) + self.assertNotIn(item_names.BATTLECRUISER_PROGRESSIVE_MISSILE_PODS, world_item_names) + self.assertNotIn(item_names.HELLION_INFERNAL_PLATING, world_item_names) + self.assertNotIn(item_names.CELLULAR_REACTOR, world_item_names) + self.assertNotIn(item_names.TECH_REACTOR, world_item_names) + + def test_nco_and_2_wol_missions_only_can_generate_with_vanilla_items_only(self) -> None: + world_options = { + 'enabled_campaigns': { + SC2Campaign.WOL.campaign_name, + SC2Campaign.NCO.campaign_name + }, + 'excluded_missions': [ + mission.mission_name for mission in mission_tables.SC2Mission + if mission.campaign == mission_tables.SC2Campaign.WOL + and mission.mission_name not in (mission_tables.SC2Mission.LIBERATION_DAY.mission_name, mission_tables.SC2Mission.THE_OUTLAWS.mission_name) + ], + 'mission_order': options.MissionOrder.option_grid, + 'maximum_campaign_size': options.MaximumCampaignSize.range_end, + 'mastery_locations': options.MasteryLocations.option_disabled, + 'vanilla_items_only': True, + } + + self.generate_world(world_options) + world_item_names = [item.name for item in self.multiworld.itempool] + + self.assertTrue(item_names) + self.assertNotIn(item_names.LIBERATOR, world_item_names) + self.assertNotIn(item_names.MARAUDER_PROGRESSIVE_STIMPACK, world_item_names) + self.assertNotIn(item_names.HELLION_HELLBAT, world_item_names) + self.assertNotIn(item_names.BATTLECRUISER_CLOAK, world_item_names) + + def test_free_protoss_only_generates(self) -> None: + world_options = { + 'enabled_campaigns': { + SC2Campaign.PROPHECY.campaign_name, + SC2Campaign.PROLOGUE.campaign_name + }, + # todo(mm): Currently, these settings don't generate on grid because there are not enough EASY missions + 'mission_order': options.MissionOrder.option_vanilla_shuffled, + 'maximum_campaign_size': options.MaximumCampaignSize.range_end, + 'accessibility': 'locations', + } + + self.generate_world(world_options) + world_item_names = [item.name for item in self.multiworld.itempool] + self.assertTrue(world_item_names) + missions = get_all_missions(self.world.custom_mission_order) + + self.assertEqual(len(missions), 7, "Wrong number of missions in free protoss seed") + for mission in missions: + self.assertIn(mission.campaign, (mission_tables.SC2Campaign.PROLOGUE, mission_tables.SC2Campaign.PROPHECY)) + for item_name in world_item_names: + self.assertIn(item_tables.item_table[item_name].race, (mission_tables.SC2Race.ANY, mission_tables.SC2Race.PROTOSS)) + + def test_resource_filler_items_may_be_put_in_start_inventory(self) -> None: + NUM_RESOURCE_ITEMS = 10 + world_options = { + 'start_inventory': { + item_names.STARTING_MINERALS: NUM_RESOURCE_ITEMS, + item_names.STARTING_VESPENE: NUM_RESOURCE_ITEMS, + item_names.STARTING_SUPPLY: NUM_RESOURCE_ITEMS, + }, + } + + self.generate_world(world_options) + start_item_names = [item.name for item in self.multiworld.precollected_items[self.player]] + + self.assertEqual(start_item_names.count(item_names.STARTING_MINERALS), NUM_RESOURCE_ITEMS, "Wrong number of starting minerals in starting inventory") + self.assertEqual(start_item_names.count(item_names.STARTING_VESPENE), NUM_RESOURCE_ITEMS, "Wrong number of starting vespene in starting inventory") + self.assertEqual(start_item_names.count(item_names.STARTING_SUPPLY), NUM_RESOURCE_ITEMS, "Wrong number of starting supply in starting inventory") + + def test_excluding_protoss_excludes_campaigns_and_items(self) -> None: + world_options = { + 'selected_races': { + SC2Race.TERRAN.get_title(), + SC2Race.ZERG.get_title(), + }, + 'enabled_campaigns': options.EnabledCampaigns.valid_keys, + 'mission_order': options.MissionOrder.option_grid, + } + + self.generate_world(world_options) + world_item_names = [item.name for item in self.multiworld.itempool] + world_regions = [region.name for region in self.multiworld.regions] + world_regions.remove('Menu') + + for item_name in world_item_names: + self.assertNotEqual(item_tables.item_table[item_name].race, mission_tables.SC2Race.PROTOSS, f"{item_name} is a PROTOSS item!") + for region in world_regions: + self.assertNotIn(mission_tables.lookup_name_to_mission[region].campaign, + (mission_tables.SC2Campaign.LOTV, mission_tables.SC2Campaign.PROPHECY, mission_tables.SC2Campaign.PROLOGUE), + f"{region} is a PROTOSS mission!") + + def test_excluding_terran_excludes_campaigns_and_items(self) -> None: + world_options = { + 'selected_races': { + SC2Race.ZERG.get_title(), + SC2Race.PROTOSS.get_title(), + }, + 'enabled_campaigns': EnabledCampaigns.valid_keys, + 'mission_order': options.MissionOrder.option_grid, + } + + self.generate_world(world_options) + world_item_names = [item.name for item in self.multiworld.itempool] + world_regions = [region.name for region in self.multiworld.regions] + world_regions.remove('Menu') + + for item_name in world_item_names: + self.assertNotEqual(item_tables.item_table[item_name].race, mission_tables.SC2Race.TERRAN, + f"{item_name} is a TERRAN item!") + for region in world_regions: + self.assertNotIn(mission_tables.lookup_name_to_mission[region].campaign, + (mission_tables.SC2Campaign.WOL, mission_tables.SC2Campaign.NCO), + f"{region} is a TERRAN mission!") + + def test_excluding_zerg_excludes_campaigns_and_items(self) -> None: + world_options = { + 'selected_races': { + SC2Race.TERRAN.get_title(), + SC2Race.PROTOSS.get_title(), + }, + 'enabled_campaigns': EnabledCampaigns.valid_keys, + 'mission_order': options.MissionOrder.option_grid, + 'excluded_missions': [ + SC2Mission.THE_INFINITE_CYCLE.mission_name + ] + } + + self.generate_world(world_options) + world_item_names = [item.name for item in self.multiworld.itempool] + world_regions = [region.name for region in self.multiworld.regions] + world_regions.remove('Menu') + + for item_name in world_item_names: + self.assertNotEqual(item_tables.item_table[item_name].race, mission_tables.SC2Race.ZERG, + f"{item_name} is a ZERG item!") + # have to manually exclude the only non-zerg HotS mission... + for region in filter(lambda region: region != "With Friends Like These", world_regions): + self.assertNotIn(mission_tables.lookup_name_to_mission[region].campaign, + ([mission_tables.SC2Campaign.HOTS]), + f"{region} is a ZERG mission!") + + def test_excluding_faction_on_vanilla_order_excludes_epilogue(self) -> None: + world_options = { + 'selected_races': { + SC2Race.TERRAN.get_title(), + SC2Race.PROTOSS.get_title(), + }, + 'enabled_campaigns': EnabledCampaigns.valid_keys, + 'mission_order': options.MissionOrder.option_vanilla, + } + + self.generate_world(world_options) + world_regions = [region.name for region in self.multiworld.regions] + world_regions.remove('Menu') + + for region in world_regions: + self.assertNotIn(mission_tables.lookup_name_to_mission[region].campaign, + ([mission_tables.SC2Campaign.EPILOGUE]), + f"{region} is an epilogue mission!") + + def test_race_swap_pick_one_has_correct_length_and_includes_swaps(self) -> None: + world_options = { + 'selected_races': options.SelectRaces.valid_keys, + 'enable_race_swap': options.EnableRaceSwapVariants.option_pick_one, + 'enabled_campaigns': { + SC2Campaign.WOL.campaign_name, + }, + 'mission_order': options.MissionOrder.option_grid, + 'excluded_missions': [mission_tables.SC2Mission.ZERO_HOUR.mission_name], + } + + self.generate_world(world_options) + world_regions = [region.name for region in self.multiworld.regions] + world_regions.remove('Menu') + NUM_WOL_MISSIONS = len([mission for mission in SC2Mission if mission.campaign == SC2Campaign.WOL and MissionFlag.RaceSwap not in mission.flags]) + races = set(mission_tables.lookup_name_to_mission[mission].race for mission in world_regions) + + self.assertEqual(len(world_regions), NUM_WOL_MISSIONS) + self.assertTrue(SC2Race.ZERG in races or SC2Race.PROTOSS in races) + + def test_start_inventory_upgrade_level_includes_only_correct_bundle(self) -> None: + world_options = { + 'start_inventory': { + item_groups.ItemGroupNames.TERRAN_GENERIC_UPGRADES: 1, + }, + 'locked_items': { + # One unit of each class to guarantee upgrades are available + item_names.MARINE: 1, + item_names.VULTURE: 1, + item_names.BANSHEE: 1, + }, + 'generic_upgrade_items': options.GenericUpgradeItems.option_bundle_unit_class, + 'selected_races': { + SC2Race.TERRAN.get_title(), + }, + 'enable_race_swap': options.EnableRaceSwapVariants.option_disabled, + 'enabled_campaigns': { + SC2Campaign.WOL.campaign_name, + }, + 'mission_order': options.MissionOrder.option_grid, + } + self.generate_world(world_options) + self.assertTrue(self.multiworld.itempool) + world_item_names = [item.name for item in self.multiworld.itempool] + start_inventory = [item.name for item in self.multiworld.precollected_items[self.player]] + + # Start inventory + self.assertIn(item_names.PROGRESSIVE_TERRAN_INFANTRY_UPGRADE, start_inventory) + self.assertIn(item_names.PROGRESSIVE_TERRAN_VEHICLE_UPGRADE, start_inventory) + self.assertIn(item_names.PROGRESSIVE_TERRAN_SHIP_UPGRADE, start_inventory) + self.assertNotIn(item_names.PROGRESSIVE_TERRAN_INFANTRY_WEAPON, start_inventory) + self.assertNotIn(item_names.PROGRESSIVE_TERRAN_INFANTRY_ARMOR, start_inventory) + self.assertNotIn(item_names.PROGRESSIVE_TERRAN_VEHICLE_WEAPON, start_inventory) + self.assertNotIn(item_names.PROGRESSIVE_TERRAN_VEHICLE_ARMOR, start_inventory) + self.assertNotIn(item_names.PROGRESSIVE_TERRAN_SHIP_WEAPON, start_inventory) + self.assertNotIn(item_names.PROGRESSIVE_TERRAN_SHIP_ARMOR, start_inventory) + self.assertNotIn(item_names.PROGRESSIVE_TERRAN_ARMOR_UPGRADE, start_inventory) + + # Additional items in pool -- standard tactics will require additional levels + self.assertIn(item_names.PROGRESSIVE_TERRAN_INFANTRY_UPGRADE, world_item_names) + self.assertIn(item_names.PROGRESSIVE_TERRAN_VEHICLE_UPGRADE, world_item_names) + self.assertIn(item_names.PROGRESSIVE_TERRAN_SHIP_UPGRADE, world_item_names) + self.assertNotIn(item_names.PROGRESSIVE_TERRAN_INFANTRY_WEAPON, world_item_names) + self.assertNotIn(item_names.PROGRESSIVE_TERRAN_INFANTRY_ARMOR, world_item_names) + self.assertNotIn(item_names.PROGRESSIVE_TERRAN_VEHICLE_WEAPON, world_item_names) + self.assertNotIn(item_names.PROGRESSIVE_TERRAN_VEHICLE_ARMOR, world_item_names) + self.assertNotIn(item_names.PROGRESSIVE_TERRAN_SHIP_WEAPON, world_item_names) + self.assertNotIn(item_names.PROGRESSIVE_TERRAN_SHIP_ARMOR, world_item_names) + self.assertNotIn(item_names.PROGRESSIVE_TERRAN_ARMOR_UPGRADE, world_item_names) + + def test_kerrigan_max_active_abilities(self): + target_number: int = 8 + world_options = { + 'mission_order': options.MissionOrder.option_grid, + 'maximum_campaign_size': options.MaximumCampaignSize.range_end, + 'selected_races': { + SC2Race.ZERG.get_title(), + }, + 'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all, + 'kerrigan_max_active_abilities': target_number, + } + + self.generate_world(world_options) + world_item_names = [item.name for item in self.multiworld.itempool] + kerrigan_actives = [item_name for item_name in world_item_names if item_name in item_groups.kerrigan_active_abilities] + + self.assertLessEqual(len(kerrigan_actives), target_number) + + def test_kerrigan_max_passive_abilities(self): + target_number: int = 3 + world_options = { + 'mission_order': options.MissionOrder.option_grid, + 'maximum_campaign_size': options.MaximumCampaignSize.range_end, + 'selected_races': { + SC2Race.ZERG.get_title(), + }, + 'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all, + 'kerrigan_max_passive_abilities': target_number, + } + + self.generate_world(world_options) + world_item_names = [item.name for item in self.multiworld.itempool] + kerrigan_passives = [item_name for item_name in world_item_names if item_name in item_groups.kerrigan_passives] + + self.assertLessEqual(len(kerrigan_passives), target_number) + + def test_spear_of_adun_max_active_abilities(self): + target_number: int = 8 + world_options = { + 'mission_order': options.MissionOrder.option_grid, + 'maximum_campaign_size': options.MaximumCampaignSize.range_end, + 'selected_races': { + SC2Race.PROTOSS.get_title(), + }, + 'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all, + 'spear_of_adun_max_active_abilities': target_number, + } + + self.generate_world(world_options) + world_item_names = [item.name for item in self.multiworld.itempool] + spear_of_adun_actives = [item_name for item_name in world_item_names if item_name in item_tables.spear_of_adun_calldowns] + + self.assertLessEqual(len(spear_of_adun_actives), target_number) + + + def test_spear_of_adun_max_autocasts(self): + target_number: int = 2 + world_options = { + 'mission_order': options.MissionOrder.option_grid, + 'maximum_campaign_size': options.MaximumCampaignSize.range_end, + 'selected_races': { + SC2Race.PROTOSS.get_title(), + }, + 'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all, + 'spear_of_adun_max_passive_abilities': target_number, + } + + self.generate_world(world_options) + world_item_names = [item.name for item in self.multiworld.itempool] + spear_of_adun_autocasts = [item_name for item_name in world_item_names if item_name in item_tables.spear_of_adun_castable_passives] + + self.assertLessEqual(len(spear_of_adun_autocasts), target_number) + + + def test_nova_max_weapons(self): + target_number: int = 3 + world_options = { + 'mission_order': options.MissionOrder.option_grid, + 'maximum_campaign_size': options.MaximumCampaignSize.range_end, + 'selected_races': { + SC2Race.TERRAN.get_title(), + }, + 'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all, + 'nova_max_weapons': target_number, + } + + self.generate_world(world_options) + world_item_names = [item.name for item in self.multiworld.itempool] + nova_weapons = [item_name for item_name in world_item_names if item_name in item_groups.nova_weapons] + + self.assertLessEqual(len(nova_weapons), target_number) + + + def test_nova_max_gadgets(self): + target_number: int = 3 + world_options = { + 'mission_order': options.MissionOrder.option_grid, + 'maximum_campaign_size': options.MaximumCampaignSize.range_end, + 'selected_races': { + SC2Race.TERRAN.get_title(), + }, + 'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all, + 'nova_max_gadgets': target_number, + } + + self.generate_world(world_options) + world_item_names = [item.name for item in self.multiworld.itempool] + nova_gadgets = [item_name for item_name in world_item_names if item_name in item_groups.nova_gadgets] + + self.assertLessEqual(len(nova_gadgets), target_number) + + def test_mercs_only(self) -> None: + world_options = { + 'selected_races': [ + SC2Race.TERRAN.get_title(), + SC2Race.ZERG.get_title(), + ], + 'required_tactics': options.RequiredTactics.option_any_units, + 'excluded_items': { + item_groups.ItemGroupNames.TERRAN_UNITS: 0, + item_groups.ItemGroupNames.ZERG_UNITS: 0, + }, + 'unexcluded_items': { + item_groups.ItemGroupNames.TERRAN_MERCENARIES: 0, + item_groups.ItemGroupNames.ZERG_MERCENARIES: 0, + }, + 'start_inventory': { + item_names.PROGRESSIVE_FAST_DELIVERY: 1, + item_names.ROGUE_FORCES: 1, + item_names.UNRESTRICTED_MUTATION: 1, + item_names.EVOLUTIONARY_LEAP: 1, + }, + 'mission_order': options.MissionOrder.option_grid, + 'excluded_missions': [ + SC2Mission.ENEMY_WITHIN.mission_name, # Requires a unit for Niadra to build + ], + } + self.generate_world(world_options) + world_item_names = [item.name for item in self.multiworld.itempool] + terran_nonmerc_units = tuple( + item_name + for item_name in world_item_names + if item_name in item_groups.terran_units and item_name not in item_groups.terran_mercenaries + ) + zerg_nonmerc_units = tuple( + item_name + for item_name in world_item_names + if item_name in item_groups.zerg_units and item_name not in item_groups.zerg_mercenaries + ) + + self.assertTupleEqual(terran_nonmerc_units, ()) + self.assertTupleEqual(zerg_nonmerc_units, ()) diff --git a/worlds/sc2/transfer_data.py b/worlds/sc2/transfer_data.py new file mode 100644 index 00000000..0718c350 --- /dev/null +++ b/worlds/sc2/transfer_data.py @@ -0,0 +1,38 @@ +from typing import Dict, List + +""" +This file is for handling SC2 data read via the bot +""" + +normalized_unit_types: Dict[str, str] = { + # Thor morphs + "AP_ThorAP": "AP_Thor", + "AP_MercThorAP": "AP_MercThor", + "AP_ThorMengskSieged": "AP_ThorMengsk", + "AP_ThorMengskAP": "AP_ThorMengsk", + # Siege Tank morphs + "AP_SiegeTankSiegedTransportable": "AP_SiegeTank", + "AP_SiegeTankMengskSiegedTransportable": "AP_SiegeTankMengsk", + "AP_SiegeBreakerSiegedTransportable": "AP_SiegeBreaker", + "AP_InfestedSiegeBreakerSiegedTransportable": "AP_InfestedSiegeBreaker", + "AP_StukovInfestedSiegeTank": "AP_StukovInfestedSiegeTankUprooted", + # Cargo size upgrades + "AP_FirebatOptimizedLogistics": "AP_Firebat", + "AP_DevilDogOptimizedLogistics": "AP_DevilDog", + "AP_GhostResourceEfficiency": "AP_Ghost", + "AP_GhostMengskResourceEfficiency": "AP_GhostMengsk", + "AP_SpectreResourceEfficiency": "AP_Spectre", + "AP_UltraliskResourceEfficiency": "AP_Ultralisk", + "AP_MercUltraliskResourceEfficiency": "AP_MercUltralisk", + "AP_ReaperResourceEfficiency": "AP_Reaper", + "AP_MercReaperResourceEfficiency": "AP_MercReaper", +} + +worker_units: List[str] = [ + "AP_SCV", + "AP_MULE", # Mules can't currently build (or be traded due to timed life), this is future proofing just in case + "AP_Drone", + "AP_SISCV", # Infested SCV + "AP_Probe", + "AP_ElderProbe", +]