diff --git a/WebHostLib/templates/supportedGames.html b/WebHostLib/templates/supportedGames.html
index 82f6348db2e9..63b70216d705 100644
--- a/WebHostLib/templates/supportedGames.html
+++ b/WebHostLib/templates/supportedGames.html
@@ -4,16 +4,27 @@
Supported Games
+
{% endblock %}
{% block body %}
{% include 'header/oceanHeader.html' %}
Currently Supported Games
+
+
+
+
+
+
+
+
{% for game_name in worlds | title_sorted %}
{% set world = worlds[game_name] %}
-
{{ game_name }}
-
+
+ ▶ {{ game_name }}
+
+
{{ world.__doc__ | default("No description provided.", true) }} Game Page
{% if world.web.tutorials %}
diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py
index d3fd0fb0364e..0d9ead795161 100644
--- a/WebHostLib/tracker.py
+++ b/WebHostLib/tracker.py
@@ -9,9 +9,9 @@
from werkzeug.exceptions import abort
from MultiServer import Context, get_saving_second
-from NetUtils import SlotType, NetworkSlot
+from NetUtils import ClientStatus, SlotType, NetworkSlot
from Utils import restricted_loads
-from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name, network_data_package
+from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name, network_data_package, games
from worlds.alttp import Items
from . import app, cache
from .models import GameDataPackage, Room
@@ -990,6 +990,7 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
SC2WOL_LOC_ID_OFFSET = 1000
SC2WOL_ITEM_ID_OFFSET = 1000
+
icons = {
"Starting Minerals": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/icons/icon-mineral-protoss.png",
"Starting Vespene": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/icons/icon-gas-terran.png",
@@ -1034,15 +1035,36 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
"Reaper": "https://static.wikia.nocookie.net/starcraft/images/7/7d/Reaper_SC2_Icon1.jpg",
"Stimpack (Marine)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png",
+ "Super Stimpack (Marine)": "/static/static/icons/sc2/superstimpack.png",
"Combat Shield (Marine)": "https://0rganics.org/archipelago/sc2wol/CombatShieldCampaign.png",
+ "Laser Targeting System (Marine)": "/static/static/icons/sc2/lasertargetingsystem.png",
+ "Magrail Munitions (Marine)": "/static/static/icons/sc2/magrailmunitions.png",
+ "Optimized Logistics (Marine)": "/static/static/icons/sc2/optimizedlogistics.png",
"Advanced Medic Facilities (Medic)": "https://0rganics.org/archipelago/sc2wol/AdvancedMedicFacilities.png",
"Stabilizer Medpacks (Medic)": "https://0rganics.org/archipelago/sc2wol/StabilizerMedpacks.png",
+ "Restoration (Medic)": "/static/static/icons/sc2/restoration.png",
+ "Optical Flare (Medic)": "/static/static/icons/sc2/opticalflare.png",
+ "Optimized Logistics (Medic)": "/static/static/icons/sc2/optimizedlogistics.png",
"Incinerator Gauntlets (Firebat)": "https://0rganics.org/archipelago/sc2wol/IncineratorGauntlets.png",
"Juggernaut Plating (Firebat)": "https://0rganics.org/archipelago/sc2wol/JuggernautPlating.png",
+ "Stimpack (Firebat)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png",
+ "Super Stimpack (Firebat)": "/static/static/icons/sc2/superstimpack.png",
+ "Optimized Logistics (Firebat)": "/static/static/icons/sc2/optimizedlogistics.png",
"Concussive Shells (Marauder)": "https://0rganics.org/archipelago/sc2wol/ConcussiveShellsCampaign.png",
"Kinetic Foam (Marauder)": "https://0rganics.org/archipelago/sc2wol/KineticFoam.png",
+ "Stimpack (Marauder)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png",
+ "Super Stimpack (Marauder)": "/static/static/icons/sc2/superstimpack.png",
+ "Laser Targeting System (Marauder)": "/static/static/icons/sc2/lasertargetingsystem.png",
+ "Magrail Munitions (Marauder)": "/static/static/icons/sc2/magrailmunitions.png",
+ "Internal Tech Module (Marauder)": "/static/static/icons/sc2/internalizedtechmodule.png",
"U-238 Rounds (Reaper)": "https://0rganics.org/archipelago/sc2wol/U-238Rounds.png",
"G-4 Clusterbomb (Reaper)": "https://0rganics.org/archipelago/sc2wol/G-4Clusterbomb.png",
+ "Stimpack (Reaper)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png",
+ "Super Stimpack (Reaper)": "/static/static/icons/sc2/superstimpack.png",
+ "Laser Targeting System (Reaper)": "/static/static/icons/sc2/lasertargetingsystem.png",
+ "Advanced Cloaking Field (Reaper)": "/static/static/icons/sc2/terran-cloak-color.png",
+ "Spider Mines (Reaper)": "/static/static/icons/sc2/spidermine.png",
+ "Combat Drugs (Reaper)": "/static/static/icons/sc2/reapercombatdrugs.png",
"Hellion": "https://static.wikia.nocookie.net/starcraft/images/5/56/Hellion_SC2_Icon1.jpg",
"Vulture": "https://static.wikia.nocookie.net/starcraft/images/d/da/Vulture_WoL.jpg",
@@ -1052,14 +1074,35 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
"Twin-Linked Flamethrower (Hellion)": "https://0rganics.org/archipelago/sc2wol/Twin-LinkedFlamethrower.png",
"Thermite Filaments (Hellion)": "https://0rganics.org/archipelago/sc2wol/ThermiteFilaments.png",
- "Cerberus Mine (Vulture)": "https://0rganics.org/archipelago/sc2wol/CerberusMine.png",
+ "Hellbat Aspect (Hellion)": "/static/static/icons/sc2/hellionbattlemode.png",
+ "Smart Servos (Hellion)": "/static/static/icons/sc2/transformationservos.png",
+ "Optimized Logistics (Hellion)": "/static/static/icons/sc2/optimizedlogistics.png",
+ "Jump Jets (Hellion)": "/static/static/icons/sc2/jumpjets.png",
+ "Stimpack (Hellion)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png",
+ "Super Stimpack (Hellion)": "/static/static/icons/sc2/superstimpack.png",
+ "Cerberus Mine (Spider Mine)": "https://0rganics.org/archipelago/sc2wol/CerberusMine.png",
+ "High Explosive Munition (Spider Mine)": "/static/static/icons/sc2/high-explosive-spidermine.png",
"Replenishable Magazine (Vulture)": "https://0rganics.org/archipelago/sc2wol/ReplenishableMagazine.png",
+ "Ion Thrusters (Vulture)": "/static/static/icons/sc2/emergencythrusters.png",
+ "Auto Launchers (Vulture)": "/static/static/icons/sc2/jotunboosters.png",
"Multi-Lock Weapons System (Goliath)": "https://0rganics.org/archipelago/sc2wol/Multi-LockWeaponsSystem.png",
"Ares-Class Targeting System (Goliath)": "https://0rganics.org/archipelago/sc2wol/Ares-ClassTargetingSystem.png",
+ "Jump Jets (Goliath)": "/static/static/icons/sc2/jumpjets.png",
+ "Optimized Logistics (Goliath)": "/static/static/icons/sc2/optimizedlogistics.png",
"Tri-Lithium Power Cell (Diamondback)": "https://0rganics.org/archipelago/sc2wol/Tri-LithiumPowerCell.png",
"Shaped Hull (Diamondback)": "https://0rganics.org/archipelago/sc2wol/ShapedHull.png",
+ "Hyperfluxor (Diamondback)": "/static/static/icons/sc2/hyperfluxor.png",
+ "Burst Capacitors (Diamondback)": "/static/static/icons/sc2/burstcapacitors.png",
+ "Optimized Logistics (Diamondback)": "/static/static/icons/sc2/optimizedlogistics.png",
"Maelstrom Rounds (Siege Tank)": "https://0rganics.org/archipelago/sc2wol/MaelstromRounds.png",
"Shaped Blast (Siege Tank)": "https://0rganics.org/archipelago/sc2wol/ShapedBlast.png",
+ "Jump Jets (Siege Tank)": "/static/static/icons/sc2/jumpjets.png",
+ "Spider Mines (Siege Tank)": "/static/static/icons/sc2/siegetank-spidermines.png",
+ "Smart Servos (Siege Tank)": "/static/static/icons/sc2/transformationservos.png",
+ "Graduating Range (Siege Tank)": "/static/static/icons/sc2/siegetankrange.png",
+ "Laser Targeting System (Siege Tank)": "/static/static/icons/sc2/lasertargetingsystem.png",
+ "Advanced Siege Tech (Siege Tank)": "/static/static/icons/sc2/improvedsiegemode.png",
+ "Internal Tech Module (Siege Tank)": "/static/static/icons/sc2/internalizedtechmodule.png",
"Medivac": "https://static.wikia.nocookie.net/starcraft/images/d/db/Medivac_SC2_Icon1.jpg",
"Wraith": "https://static.wikia.nocookie.net/starcraft/images/7/75/Wraith_WoL.jpg",
@@ -1069,25 +1112,77 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
"Rapid Deployment Tube (Medivac)": "https://0rganics.org/archipelago/sc2wol/RapidDeploymentTube.png",
"Advanced Healing AI (Medivac)": "https://0rganics.org/archipelago/sc2wol/AdvancedHealingAI.png",
+ "Expanded Hull (Medivac)": "/static/static/icons/sc2/neosteelfortifiedarmor.png",
+ "Afterburners (Medivac)": "/static/static/icons/sc2/medivacemergencythrusters.png",
"Tomahawk Power Cells (Wraith)": "https://0rganics.org/archipelago/sc2wol/TomahawkPowerCells.png",
"Displacement Field (Wraith)": "https://0rganics.org/archipelago/sc2wol/DisplacementField.png",
+ "Advanced Laser Technology (Wraith)": "/static/static/icons/sc2/improvedburstlaser.png",
"Ripwave Missiles (Viking)": "https://0rganics.org/archipelago/sc2wol/RipwaveMissiles.png",
"Phobos-Class Weapons System (Viking)": "https://0rganics.org/archipelago/sc2wol/Phobos-ClassWeaponsSystem.png",
- "Cross-Spectrum Dampeners (Banshee)": "https://0rganics.org/archipelago/sc2wol/Cross-SpectrumDampeners.png",
+ "Smart Servos (Viking)": "/static/static/icons/sc2/transformationservos.png",
+ "Magrail Munitions (Viking)": "/static/static/icons/sc2/magrailmunitions.png",
+ "Cross-Spectrum Dampeners (Banshee)": "/static/static/icons/sc2/crossspectrumdampeners.png",
+ "Advanced Cross-Spectrum Dampeners (Banshee)": "https://0rganics.org/archipelago/sc2wol/Cross-SpectrumDampeners.png",
"Shockwave Missile Battery (Banshee)": "https://0rganics.org/archipelago/sc2wol/ShockwaveMissileBattery.png",
+ "Hyperflight Rotors (Banshee)": "/static/static/icons/sc2/hyperflightrotors.png",
+ "Laser Targeting System (Banshee)": "/static/static/icons/sc2/lasertargetingsystem.png",
+ "Internal Tech Module (Banshee)": "/static/static/icons/sc2/internalizedtechmodule.png",
"Missile Pods (Battlecruiser)": "https://0rganics.org/archipelago/sc2wol/MissilePods.png",
"Defensive Matrix (Battlecruiser)": "https://0rganics.org/archipelago/sc2wol/DefensiveMatrix.png",
+ "Tactical Jump (Battlecruiser)": "/static/static/icons/sc2/warpjump.png",
+ "Cloak (Battlecruiser)": "/static/static/icons/sc2/terran-cloak-color.png",
+ "ATX Laser Battery (Battlecruiser)": "/static/static/icons/sc2/specialordance.png",
+ "Optimized Logistics (Battlecruiser)": "/static/static/icons/sc2/optimizedlogistics.png",
+ "Internal Tech Module (Battlecruiser)": "/static/static/icons/sc2/internalizedtechmodule.png",
"Ghost": "https://static.wikia.nocookie.net/starcraft/images/6/6e/Ghost_SC2_Icon1.jpg",
"Spectre": "https://static.wikia.nocookie.net/starcraft/images/0/0d/Spectre_WoL.jpg",
"Thor": "https://static.wikia.nocookie.net/starcraft/images/e/ef/Thor_SC2_Icon1.jpg",
+ "Widow Mine": "/static/static/icons/sc2/widowmine.png",
+ "Cyclone": "/static/static/icons/sc2/cyclone.png",
+ "Liberator": "/static/static/icons/sc2/liberator.png",
+ "Valkyrie": "/static/static/icons/sc2/valkyrie.png",
+
"Ocular Implants (Ghost)": "https://0rganics.org/archipelago/sc2wol/OcularImplants.png",
"Crius Suit (Ghost)": "https://0rganics.org/archipelago/sc2wol/CriusSuit.png",
+ "EMP Rounds (Ghost)": "/static/static/icons/sc2/terran-emp-color.png",
+ "Lockdown (Ghost)": "/static/static/icons/sc2/lockdown.png",
"Psionic Lash (Spectre)": "https://0rganics.org/archipelago/sc2wol/PsionicLash.png",
"Nyx-Class Cloaking Module (Spectre)": "https://0rganics.org/archipelago/sc2wol/Nyx-ClassCloakingModule.png",
+ "Impaler Rounds (Spectre)": "/static/static/icons/sc2/impalerrounds.png",
"330mm Barrage Cannon (Thor)": "https://0rganics.org/archipelago/sc2wol/330mmBarrageCannon.png",
"Immortality Protocol (Thor)": "https://0rganics.org/archipelago/sc2wol/ImmortalityProtocol.png",
+ "High Impact Payload (Thor)": "/static/static/icons/sc2/thorsiegemode.png",
+ "Smart Servos (Thor)": "/static/static/icons/sc2/transformationservos.png",
+
+ "Optimized Logistics (Predator)": "/static/static/icons/sc2/optimizedlogistics.png",
+ "Drilling Claws (Widow Mine)": "/static/static/icons/sc2/drillingclaws.png",
+ "Concealment (Widow Mine)": "/static/static/icons/sc2/widowminehidden.png",
+ "Black Market Launchers (Widow Mine)": "/static/static/icons/sc2/widowmine-attackrange.png",
+ "Executioner Missiles (Widow Mine)": "/static/static/icons/sc2/widowmine-deathblossom.png",
+ "Mag-Field Accelerators (Cyclone)": "/static/static/icons/sc2/magfieldaccelerator.png",
+ "Mag-Field Launchers (Cyclone)": "/static/static/icons/sc2/cyclonerangeupgrade.png",
+ "Targeting Optics (Cyclone)": "/static/static/icons/sc2/targetingoptics.png",
+ "Rapid Fire Launchers (Cyclone)": "/static/static/icons/sc2/ripwavemissiles.png",
+ "Bio Mechanical Repair Drone (Raven)": "/static/static/icons/sc2/biomechanicaldrone.png",
+ "Spider Mines (Raven)": "/static/static/icons/sc2/siegetank-spidermines.png",
+ "Railgun Turret (Raven)": "/static/static/icons/sc2/autoturretblackops.png",
+ "Hunter-Seeker Weapon (Raven)": "/static/static/icons/sc2/specialordance.png",
+ "Interference Matrix (Raven)": "/static/static/icons/sc2/interferencematrix.png",
+ "Anti-Armor Missile (Raven)": "/static/static/icons/sc2/shreddermissile.png",
+ "Internal Tech Module (Raven)": "/static/static/icons/sc2/internalizedtechmodule.png",
+ "EMP Shockwave (Science Vessel)": "/static/static/icons/sc2/staticempblast.png",
+ "Defensive Matrix (Science Vessel)": "https://0rganics.org/archipelago/sc2wol/DefensiveMatrix.png",
+ "Advanced Ballistics (Liberator)": "/static/static/icons/sc2/advanceballistics.png",
+ "Raid Artillery (Liberator)": "/static/static/icons/sc2/terrandefendermodestructureattack.png",
+ "Cloak (Liberator)": "/static/static/icons/sc2/terran-cloak-color.png",
+ "Laser Targeting System (Liberator)": "/static/static/icons/sc2/lasertargetingsystem.png",
+ "Optimized Logistics (Liberator)": "/static/static/icons/sc2/optimizedlogistics.png",
+ "Enhanced Cluster Launchers (Valkyrie)": "https://0rganics.org/archipelago/sc2wol/HellstormBatteries.png",
+ "Shaped Hull (Valkyrie)": "https://0rganics.org/archipelago/sc2wol/ShapedHull.png",
+ "Burst Lasers (Valkyrie)": "/static/static/icons/sc2/improvedburstlaser.png",
+ "Afterburners (Valkyrie)": "/static/static/icons/sc2/medivacemergencythrusters.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",
@@ -1109,14 +1204,15 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
"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": "https://static.wikia.nocookie.net/starcraft/images/4/44/SC2_Lab_Shrike_Turret_Icon.png",
- "Fortified Bunker": "https://static.wikia.nocookie.net/starcraft/images/4/4f/SC2_Lab_FortBunker_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",
"Predator": "https://static.wikia.nocookie.net/starcraft/images/8/83/SC2_Lab_Predator_Icon.png",
"Hercules": "https://static.wikia.nocookie.net/starcraft/images/4/40/SC2_Lab_Hercules_Icon.png",
"Cellular Reactor": "https://static.wikia.nocookie.net/starcraft/images/d/d8/SC2_Lab_CellReactor_Icon.png",
- "Regenerative Bio-Steel": "https://static.wikia.nocookie.net/starcraft/images/d/d3/SC2_Lab_BioSteel_Icon.png",
+ "Regenerative Bio-Steel Level 1": "/static/static/icons/sc2/SC2_Lab_BioSteel_L1.png",
+ "Regenerative Bio-Steel Level 2": "/static/static/icons/sc2/SC2_Lab_BioSteel_L2.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",
@@ -1132,40 +1228,71 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
"Nothing": "",
}
-
sc2wol_location_ids = {
- "Liberation Day": [SC2WOL_LOC_ID_OFFSET + 100, SC2WOL_LOC_ID_OFFSET + 101, SC2WOL_LOC_ID_OFFSET + 102, SC2WOL_LOC_ID_OFFSET + 103, SC2WOL_LOC_ID_OFFSET + 104, SC2WOL_LOC_ID_OFFSET + 105, SC2WOL_LOC_ID_OFFSET + 106],
- "The Outlaws": [SC2WOL_LOC_ID_OFFSET + 200, SC2WOL_LOC_ID_OFFSET + 201],
- "Zero Hour": [SC2WOL_LOC_ID_OFFSET + 300, SC2WOL_LOC_ID_OFFSET + 301, SC2WOL_LOC_ID_OFFSET + 302, SC2WOL_LOC_ID_OFFSET + 303],
- "Evacuation": [SC2WOL_LOC_ID_OFFSET + 400, SC2WOL_LOC_ID_OFFSET + 401, SC2WOL_LOC_ID_OFFSET + 402, SC2WOL_LOC_ID_OFFSET + 403],
- "Outbreak": [SC2WOL_LOC_ID_OFFSET + 500, SC2WOL_LOC_ID_OFFSET + 501, SC2WOL_LOC_ID_OFFSET + 502],
- "Safe Haven": [SC2WOL_LOC_ID_OFFSET + 600, SC2WOL_LOC_ID_OFFSET + 601, SC2WOL_LOC_ID_OFFSET + 602, SC2WOL_LOC_ID_OFFSET + 603],
- "Haven's Fall": [SC2WOL_LOC_ID_OFFSET + 700, SC2WOL_LOC_ID_OFFSET + 701, SC2WOL_LOC_ID_OFFSET + 702, SC2WOL_LOC_ID_OFFSET + 703],
- "Smash and Grab": [SC2WOL_LOC_ID_OFFSET + 800, SC2WOL_LOC_ID_OFFSET + 801, SC2WOL_LOC_ID_OFFSET + 802, SC2WOL_LOC_ID_OFFSET + 803, SC2WOL_LOC_ID_OFFSET + 804],
- "The Dig": [SC2WOL_LOC_ID_OFFSET + 900, SC2WOL_LOC_ID_OFFSET + 901, SC2WOL_LOC_ID_OFFSET + 902, SC2WOL_LOC_ID_OFFSET + 903],
- "The Moebius Factor": [SC2WOL_LOC_ID_OFFSET + 1000, SC2WOL_LOC_ID_OFFSET + 1003, SC2WOL_LOC_ID_OFFSET + 1004, SC2WOL_LOC_ID_OFFSET + 1005, SC2WOL_LOC_ID_OFFSET + 1006, SC2WOL_LOC_ID_OFFSET + 1007, SC2WOL_LOC_ID_OFFSET + 1008],
- "Supernova": [SC2WOL_LOC_ID_OFFSET + 1100, SC2WOL_LOC_ID_OFFSET + 1101, SC2WOL_LOC_ID_OFFSET + 1102, SC2WOL_LOC_ID_OFFSET + 1103, SC2WOL_LOC_ID_OFFSET + 1104],
- "Maw of the Void": [SC2WOL_LOC_ID_OFFSET + 1200, SC2WOL_LOC_ID_OFFSET + 1201, SC2WOL_LOC_ID_OFFSET + 1202, SC2WOL_LOC_ID_OFFSET + 1203, SC2WOL_LOC_ID_OFFSET + 1204, SC2WOL_LOC_ID_OFFSET + 1205],
- "Devil's Playground": [SC2WOL_LOC_ID_OFFSET + 1300, SC2WOL_LOC_ID_OFFSET + 1301, SC2WOL_LOC_ID_OFFSET + 1302],
- "Welcome to the Jungle": [SC2WOL_LOC_ID_OFFSET + 1400, SC2WOL_LOC_ID_OFFSET + 1401, SC2WOL_LOC_ID_OFFSET + 1402, SC2WOL_LOC_ID_OFFSET + 1403],
- "Breakout": [SC2WOL_LOC_ID_OFFSET + 1500, SC2WOL_LOC_ID_OFFSET + 1501, SC2WOL_LOC_ID_OFFSET + 1502],
- "Ghost of a Chance": [SC2WOL_LOC_ID_OFFSET + 1600, SC2WOL_LOC_ID_OFFSET + 1601, SC2WOL_LOC_ID_OFFSET + 1602, SC2WOL_LOC_ID_OFFSET + 1603, SC2WOL_LOC_ID_OFFSET + 1604, SC2WOL_LOC_ID_OFFSET + 1605],
- "The Great Train Robbery": [SC2WOL_LOC_ID_OFFSET + 1700, SC2WOL_LOC_ID_OFFSET + 1701, SC2WOL_LOC_ID_OFFSET + 1702, SC2WOL_LOC_ID_OFFSET + 1703],
- "Cutthroat": [SC2WOL_LOC_ID_OFFSET + 1800, SC2WOL_LOC_ID_OFFSET + 1801, SC2WOL_LOC_ID_OFFSET + 1802, SC2WOL_LOC_ID_OFFSET + 1803, SC2WOL_LOC_ID_OFFSET + 1804],
- "Engine of Destruction": [SC2WOL_LOC_ID_OFFSET + 1900, SC2WOL_LOC_ID_OFFSET + 1901, SC2WOL_LOC_ID_OFFSET + 1902, SC2WOL_LOC_ID_OFFSET + 1903, SC2WOL_LOC_ID_OFFSET + 1904, SC2WOL_LOC_ID_OFFSET + 1905],
- "Media Blitz": [SC2WOL_LOC_ID_OFFSET + 2000, SC2WOL_LOC_ID_OFFSET + 2001, SC2WOL_LOC_ID_OFFSET + 2002, SC2WOL_LOC_ID_OFFSET + 2003, SC2WOL_LOC_ID_OFFSET + 2004],
- "Piercing the Shroud": [SC2WOL_LOC_ID_OFFSET + 2100, SC2WOL_LOC_ID_OFFSET + 2101, SC2WOL_LOC_ID_OFFSET + 2102, SC2WOL_LOC_ID_OFFSET + 2103, SC2WOL_LOC_ID_OFFSET + 2104, SC2WOL_LOC_ID_OFFSET + 2105],
- "Whispers of Doom": [SC2WOL_LOC_ID_OFFSET + 2200, SC2WOL_LOC_ID_OFFSET + 2201, SC2WOL_LOC_ID_OFFSET + 2202, SC2WOL_LOC_ID_OFFSET + 2203],
- "A Sinister Turn": [SC2WOL_LOC_ID_OFFSET + 2300, SC2WOL_LOC_ID_OFFSET + 2301, SC2WOL_LOC_ID_OFFSET + 2302, SC2WOL_LOC_ID_OFFSET + 2303],
- "Echoes of the Future": [SC2WOL_LOC_ID_OFFSET + 2400, SC2WOL_LOC_ID_OFFSET + 2401, SC2WOL_LOC_ID_OFFSET + 2402],
- "In Utter Darkness": [SC2WOL_LOC_ID_OFFSET + 2500, SC2WOL_LOC_ID_OFFSET + 2501, SC2WOL_LOC_ID_OFFSET + 2502],
- "Gates of Hell": [SC2WOL_LOC_ID_OFFSET + 2600, SC2WOL_LOC_ID_OFFSET + 2601],
- "Belly of the Beast": [SC2WOL_LOC_ID_OFFSET + 2700, SC2WOL_LOC_ID_OFFSET + 2701, SC2WOL_LOC_ID_OFFSET + 2702, SC2WOL_LOC_ID_OFFSET + 2703],
- "Shatter the Sky": [SC2WOL_LOC_ID_OFFSET + 2800, SC2WOL_LOC_ID_OFFSET + 2801, SC2WOL_LOC_ID_OFFSET + 2802, SC2WOL_LOC_ID_OFFSET + 2803, SC2WOL_LOC_ID_OFFSET + 2804, SC2WOL_LOC_ID_OFFSET + 2805],
+ "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),
}
display_data = {}
+ # Grouped Items
+ grouped_item_ids = {
+ "Progressive Weapon Upgrade": 107 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Armor Upgrade": 108 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Infantry Upgrade": 109 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Vehicle Upgrade": 110 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Ship Upgrade": 111 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Weapon/Armor Upgrade": 112 + SC2WOL_ITEM_ID_OFFSET
+ }
+ grouped_item_replacements = {
+ "Progressive Weapon Upgrade": ["Progressive Infantry Weapon", "Progressive Vehicle Weapon", "Progressive Ship Weapon"],
+ "Progressive Armor Upgrade": ["Progressive Infantry Armor", "Progressive Vehicle Armor", "Progressive Ship Armor"],
+ "Progressive Infantry Upgrade": ["Progressive Infantry Weapon", "Progressive Infantry Armor"],
+ "Progressive Vehicle Upgrade": ["Progressive Vehicle Weapon", "Progressive Vehicle Armor"],
+ "Progressive Ship Upgrade": ["Progressive Ship Weapon", "Progressive Ship Armor"]
+ }
+ grouped_item_replacements["Progressive Weapon/Armor Upgrade"] = grouped_item_replacements["Progressive Weapon Upgrade"] + grouped_item_replacements["Progressive Armor Upgrade"]
+ replacement_item_ids = {
+ "Progressive Infantry Weapon": 100 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Infantry Armor": 102 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Vehicle Weapon": 103 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Vehicle Armor": 104 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Ship Weapon": 105 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Ship Armor": 106 + SC2WOL_ITEM_ID_OFFSET,
+ }
+ 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]
+ inventory[replacement_id] = count
+
# Determine display for progressive items
progressive_items = {
"Progressive Infantry Weapon": 100 + SC2WOL_ITEM_ID_OFFSET,
@@ -1173,7 +1300,15 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
"Progressive Vehicle Weapon": 103 + SC2WOL_ITEM_ID_OFFSET,
"Progressive Vehicle Armor": 104 + SC2WOL_ITEM_ID_OFFSET,
"Progressive Ship Weapon": 105 + SC2WOL_ITEM_ID_OFFSET,
- "Progressive Ship Armor": 106 + SC2WOL_ITEM_ID_OFFSET
+ "Progressive Ship Armor": 106 + 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 High Impact Payload (Thor)": 361 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Cross-Spectrum Dampeners (Banshee)": 316 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Regenerative Bio-Steel": 617 + SC2WOL_ITEM_ID_OFFSET
}
progressive_names = {
"Progressive Infantry Weapon": ["Infantry Weapons Level 1", "Infantry Weapons Level 1", "Infantry Weapons Level 2", "Infantry Weapons Level 3"],
@@ -1181,14 +1316,27 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
"Progressive Vehicle Weapon": ["Vehicle Weapons Level 1", "Vehicle Weapons Level 1", "Vehicle Weapons Level 2", "Vehicle Weapons Level 3"],
"Progressive Vehicle Armor": ["Vehicle Armor Level 1", "Vehicle Armor Level 1", "Vehicle Armor Level 2", "Vehicle Armor Level 3"],
"Progressive Ship Weapon": ["Ship Weapons Level 1", "Ship Weapons Level 1", "Ship Weapons Level 2", "Ship Weapons Level 3"],
- "Progressive Ship Armor": ["Ship Armor Level 1", "Ship Armor Level 1", "Ship Armor Level 2", "Ship Armor Level 3"]
+ "Progressive Ship Armor": ["Ship Armor Level 1", "Ship Armor Level 1", "Ship Armor Level 2", "Ship Armor Level 3"],
+ "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 High Impact Payload (Thor)": ["High Impact Payload (Thor)", "High Impact Payload (Thor)", "Smart Servos (Thor)"],
+ "Progressive Cross-Spectrum Dampeners (Banshee)": ["Cross-Spectrum Dampeners (Banshee)", "Cross-Spectrum Dampeners (Banshee)", "Advanced Cross-Spectrum Dampeners (Banshee)"],
+ "Progressive Regenerative Bio-Steel": ["Regenerative Bio-Steel Level 1", "Regenerative Bio-Steel Level 1", "Regenerative Bio-Steel Level 2"]
}
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(' ', '_')
+ 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]
+ display_data[base_name + "_name"] = display_name
# Multi-items
multi_items = {
@@ -1220,12 +1368,12 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
checks_in_area['Total'] = sum(checks_in_area.values())
return render_template("sc2wolTracker.html",
- inventory=inventory, icons=icons,
- acquired_items={lookup_any_item_id_to_name[id] for id in inventory if
- id in lookup_any_item_id_to_name},
- player=player, team=team, room=room, player_name=playerName,
- checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
- **display_data)
+ inventory=inventory, icons=icons,
+ acquired_items={lookup_any_item_id_to_name[id] for id in inventory if
+ id in lookup_any_item_id_to_name},
+ player=player, team=team, room=room, player_name=playerName,
+ checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
+ **display_data)
def __renderChecksfinder(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
inventory: Counter, team: int, player: int, playerName: str,
@@ -1366,6 +1514,10 @@ def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[s
for playernumber in range(1, len(team) + 1) if playernumber not in groups}
for teamnumber, team in enumerate(names)}
+ total_locations = {teamnumber: sum(len(locations[playernumber])
+ for playernumber in range(1, len(team) + 1) if playernumber not in groups)
+ for teamnumber, team in enumerate(names)}
+
hints = {team: set() for team in range(len(names))}
if room.multisave:
multisave = restricted_loads(room.multisave)
@@ -1390,11 +1542,14 @@ def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[s
activity_timers[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp)
player_names = {}
+ completed_worlds = 0
states: typing.Dict[typing.Tuple[int, int], int] = {}
for team, names in enumerate(names):
for player, name in enumerate(names, 1):
player_names[team, player] = name
states[team, player] = multisave.get("client_game_state", {}).get((team, player), 0)
+ if states[team, player] == ClientStatus.CLIENT_GOAL and player not in groups:
+ completed_worlds += 1
long_player_names = player_names.copy()
for (team, player), alias in multisave.get("name_aliases", {}).items():
player_names[team, player] = alias
@@ -1410,14 +1565,18 @@ def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[s
activity_timers=activity_timers, video=video, hints=hints,
long_player_names=long_player_names,
multisave=multisave, precollected_items=precollected_items, groups=groups,
- locations=locations, games=games, states=states,
+ locations=locations, total_locations=total_locations, games=games, states=states,
+ completed_worlds=completed_worlds,
custom_locations=custom_locations, custom_items=custom_items,
)
-def _get_inventory_data(data: typing.Dict[str, typing.Any]) -> typing.Dict[int, typing.Dict[int, int]]:
- inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in team_data}
- for teamnumber, team_data in data["checks_done"].items()}
+def _get_inventory_data(data: typing.Dict[str, typing.Any]) \
+ -> typing.Dict[int, typing.Dict[int, typing.Dict[int, int]]]:
+ inventory: typing.Dict[int, typing.Dict[int, typing.Dict[int, int]]] = {
+ teamnumber: {playernumber: collections.Counter() for playernumber in team_data}
+ for teamnumber, team_data in data["checks_done"].items()
+ }
groups = data["groups"]
@@ -1436,6 +1595,17 @@ def _get_inventory_data(data: typing.Dict[str, typing.Any]) -> typing.Dict[int,
return inventory
+def _get_named_inventory(inventory: typing.Dict[int, int], custom_items: typing.Dict[int, str] = None) \
+ -> typing.Dict[str, int]:
+ """slow"""
+ if custom_items:
+ mapping = collections.ChainMap(custom_items, lookup_any_item_id_to_name)
+ else:
+ mapping = lookup_any_item_id_to_name
+
+ return collections.Counter({mapping.get(item_id, None): count for item_id, count in inventory.items()})
+
+
@app.route('/tracker/')
@cache.memoize(timeout=60) # multisave is currently created at most every minute
def get_multiworld_tracker(tracker: UUID):
@@ -1447,18 +1617,22 @@ def get_multiworld_tracker(tracker: UUID):
return render_template("multiTracker.html", **data)
+if "Factorio" in games:
+ @app.route('/tracker//Factorio')
+ @cache.memoize(timeout=60) # multisave is currently created at most every minute
+ def get_Factorio_multiworld_tracker(tracker: UUID):
+ data = _get_multiworld_tracker_data(tracker)
+ if not data:
+ abort(404)
-@app.route('/tracker//Factorio')
-@cache.memoize(timeout=60) # multisave is currently created at most every minute
-def get_Factorio_multiworld_tracker(tracker: UUID):
- data = _get_multiworld_tracker_data(tracker)
- if not data:
- abort(404)
-
- data["inventory"] = _get_inventory_data(data)
- data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], "Factorio")
+ data["inventory"] = _get_inventory_data(data)
+ data["named_inventory"] = {team_id : {
+ player_id: _get_named_inventory(inventory, data["custom_items"])
+ for player_id, inventory in team_inventory.items()
+ } for team_id, team_inventory in data["inventory"].items()}
+ data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], "Factorio")
- return render_template("multiFactorioTracker.html", **data)
+ return render_template("multiFactorioTracker.html", **data)
@app.route('/tracker//A Link to the Past')
@@ -1509,7 +1683,7 @@ def attribute_item(team: int, recipient: int, item: int):
for item_id in precollected:
attribute_item(team, player, item_id)
for location in locations_checked:
- if location not in player_locations or location not in player_location_to_area[player]:
+ if location not in player_locations or location not in player_location_to_area.get(player, {}):
continue
item, recipient, flags = player_locations[location]
recipients = groups.get(recipient, [recipient])
@@ -1588,5 +1762,7 @@ def attribute_item(team: int, recipient: int, item: int):
multi_trackers: typing.Dict[str, typing.Callable] = {
"A Link to the Past": get_LttP_multiworld_tracker,
- "Factorio": get_Factorio_multiworld_tracker,
}
+
+if "Factorio" in games:
+ multi_trackers["Factorio"] = get_Factorio_multiworld_tracker
diff --git a/data/lua/connector_tloz.lua b/data/lua/connector_tloz.lua
index f48e4dfac1f2..4a2d2f25bf66 100644
--- a/data/lua/connector_tloz.lua
+++ b/data/lua/connector_tloz.lua
@@ -67,6 +67,7 @@ local itemsObtained = 0x0677
local takeAnyCavesChecked = 0x0678
local localTriforce = 0x0679
local bonusItemsObtained = 0x067A
+local itemsObtainedHigh = 0x067B
itemAPids = {
["Boomerang"] = 7100,
@@ -173,11 +174,18 @@ for key, value in pairs(itemAPids) do
itemIDNames[value] = key
end
+local function getItemsObtained()
+ return bit.bor(bit.lshift(u8(itemsObtainedHigh), 8), u8(itemsObtained))
+end
+local function setItemsObtained(value)
+ wU8(itemsObtainedHigh, bit.rshift(value, 8))
+ wU8(itemsObtained, bit.band(value, 0xFF))
+end
local function determineItem(array)
memdomain.ram()
- currentItemsObtained = u8(itemsObtained)
+ currentItemsObtained = getItemsObtained()
end
@@ -364,8 +372,8 @@ local function gotItem(item)
wU8(0x505, itemCode)
wU8(0x506, 128)
wU8(0x602, 4)
- numberObtained = u8(itemsObtained) + 1
- wU8(itemsObtained, numberObtained)
+ numberObtained = getItemsObtained() + 1
+ setItemsObtained(numberObtained)
if itemName == "Boomerang" then gotBoomerang() end
if itemName == "Bow" then gotBow() end
if itemName == "Magical Boomerang" then gotMagicalBoomerang() end
@@ -476,7 +484,7 @@ function processBlock(block)
if i > u8(bonusItemsObtained) then
if u8(0x505) == 0 then
gotItem(item)
- wU8(itemsObtained, u8(itemsObtained) - 1)
+ setItemsObtained(getItemsObtained() - 1)
wU8(bonusItemsObtained, u8(bonusItemsObtained) + 1)
end
end
@@ -494,7 +502,7 @@ function processBlock(block)
for i, item in ipairs(itemsBlock) do
memDomain.ram()
if u8(0x505) == 0 then
- if i > u8(itemsObtained) then
+ if i > getItemsObtained() then
gotItem(item)
end
end
@@ -546,7 +554,7 @@ function receive()
retTable["gameMode"] = gameMode
retTable["overworldHC"] = getHCLocation()
retTable["overworldPB"] = getPBLocation()
- retTable["itemsObtained"] = u8(itemsObtained)
+ retTable["itemsObtained"] = getItemsObtained()
msg = json.encode(retTable).."\n"
local ret, error = zeldaSocket:send(msg)
if ret == nil then
@@ -606,4 +614,4 @@ function main()
end
end
-main()
\ No newline at end of file
+main()
diff --git a/kvui.py b/kvui.py
index 77b96b896fd0..835f0dad45f4 100644
--- a/kvui.py
+++ b/kvui.py
@@ -1,7 +1,14 @@
import os
import logging
+import sys
import typing
+if sys.platform == "win32":
+ import ctypes
+ # kivy 2.2.0 introduced DPI awareness on Windows, but it makes the UI enter an infinitely recursive re-layout
+ # by setting the application to not DPI Aware, Windows handles scaling the entire window on its own, ignoring kivy's
+ ctypes.windll.shcore.SetProcessDpiAwareness(0)
+
os.environ["KIVY_NO_CONSOLELOG"] = "1"
os.environ["KIVY_NO_FILELOG"] = "1"
os.environ["KIVY_NO_ARGS"] = "1"
diff --git a/playerSettings.yaml b/playerSettings.yaml
index e28963ddb340..f9585da246b8 100644
--- a/playerSettings.yaml
+++ b/playerSettings.yaml
@@ -26,7 +26,7 @@ name: YourName{number} # Your name in-game. Spaces will be replaced with undersc
game: # Pick a game to play
A Link to the Past: 1
requires:
- version: 0.3.3 # Version of Archipelago required for this yaml to work as expected.
+ version: 0.4.3 # Version of Archipelago required for this yaml to work as expected.
A Link to the Past:
progression_balancing:
# A system that can move progression earlier, to try and prevent the player from getting stuck and bored early.
@@ -114,6 +114,9 @@ A Link to the Past:
different_world: 0
universal: 0
start_with: 0
+ key_drop_shuffle: # Shuffle keys found in pots or dropped from killed enemies
+ off: 50
+ on: 0
compass_shuffle: # Compass Placement
original_dungeon: 50
own_dungeons: 0
diff --git a/requirements.txt b/requirements.txt
index 610892848d15..bfc637a80a2b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,7 @@
colorama>=0.4.5
websockets>=11.0.3
PyYAML>=6.0.1
-jellyfish>=1.0.0
+jellyfish>=1.0.1
jinja2>=3.1.2
schema>=0.7.5
kivy>=2.2.0
@@ -9,4 +9,4 @@ bsdiff4>=1.2.3
platformdirs>=3.9.1
certifi>=2023.7.22
cython>=0.29.35
-cymem>=2.0.7
+cymem>=2.0.8
diff --git a/settings.py b/settings.py
index 4c9c1d1c3042..a7dcbbf8ddbf 100644
--- a/settings.py
+++ b/settings.py
@@ -118,7 +118,7 @@ def get_type_hints(cls) -> Dict[str, Any]:
cls._type_cache = typing.get_type_hints(cls, globalns=mod_dict, localns=cls.__dict__)
return cls._type_cache
- def get(self, key: str, default: Any) -> Any:
+ def get(self, key: str, default: Any = None) -> Any:
if key in self:
return self[key]
return default
diff --git a/setup.py b/setup.py
index 212bcc5d0954..ce35c0f1cc5d 100644
--- a/setup.py
+++ b/setup.py
@@ -80,7 +80,6 @@
"Raft",
"Secret of Evermore",
"Slay the Spire",
- "Starcraft 2 Wings of Liberty",
"Sudoku",
"Super Mario 64",
"VVVVVV",
@@ -91,6 +90,7 @@
# LogicMixin is broken before 3.10 import revamp
if sys.version_info < (3,10):
non_apworlds.add("Hollow Knight")
+ non_apworlds.add("Starcraft 2 Wings of Liberty")
def download_SNI():
print("Updating SNI")
diff --git a/test/TestBase.py b/test/TestBase.py
index dc79ad2855ed..856428fb57ed 100644
--- a/test/TestBase.py
+++ b/test/TestBase.py
@@ -25,8 +25,9 @@ def get_state(self, items):
state = CollectionState(self.multiworld)
for item in items:
item.classification = ItemClassification.progression
- state.collect(item)
+ state.collect(item, event=True)
state.sweep_for_events()
+ state.update_reachable_regions(1)
self._state_cache[self.multiworld, tuple(items)] = state
return state
@@ -53,7 +54,8 @@ def run_location_tests(self, access_pool):
with self.subTest(msg="Reach Location", location=location, access=access, items=items,
all_except=all_except, path=path, entry=i):
- self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access)
+ self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access,
+ f"failed {self.multiworld.get_location(location, 1)} with: {item_pool}")
# check for partial solution
if not all_except and access: # we are not supposed to be able to reach location with partial inventory
@@ -61,7 +63,10 @@ def run_location_tests(self, access_pool):
with self.subTest(msg="Location reachable without required item", location=location,
items=item_pool[0], missing_item=missing_item, entry=i):
state = self._get_items_partial(item_pool, missing_item)
- self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), False)
+
+ self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), False,
+ f"failed {self.multiworld.get_location(location, 1)}: succeeded with "
+ f"{missing_item} removed from: {item_pool}")
def run_entrance_tests(self, access_pool):
for i, (entrance, access, *item_pool) in enumerate(access_pool):
@@ -80,7 +85,8 @@ def run_entrance_tests(self, access_pool):
with self.subTest(msg="Entrance reachable without required item", entrance=entrance,
items=item_pool[0], missing_item=missing_item, entry=i):
state = self._get_items_partial(item_pool, missing_item)
- self.assertEqual(self.multiworld.get_entrance(entrance, 1).can_reach(state), False)
+ self.assertEqual(self.multiworld.get_entrance(entrance, 1).can_reach(state), False,
+ f"failed {self.multiworld.get_entrance(entrance, 1)} with: {item_pool}")
def _get_items(self, item_pool, all_except):
if all_except and len(all_except) > 0:
@@ -135,13 +141,16 @@ def world_setup(self, seed: typing.Optional[int] = None) -> None:
call_all(self.multiworld, step)
# methods that can be called within tests
- def collect_all_but(self, item_names: typing.Union[str, typing.Iterable[str]]) -> None:
+ def collect_all_but(self, item_names: typing.Union[str, typing.Iterable[str]],
+ state: typing.Optional[CollectionState] = None) -> None:
"""Collects all pre-placed items and items in the multiworld itempool except those provided"""
if isinstance(item_names, str):
item_names = (item_names,)
+ if not state:
+ state = self.multiworld.state
for item in self.multiworld.get_items():
if item.name not in item_names:
- self.multiworld.state.collect(item)
+ state.collect(item)
def get_item_by_name(self, item_name: str) -> Item:
"""Returns the first item found in placed items, or in the itempool with the matching name"""
@@ -168,6 +177,12 @@ def collect(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None:
items = (items,)
for item in items:
self.multiworld.state.collect(item)
+
+ def remove_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]:
+ """Remove all of the items in the item pool with the given names from state"""
+ items = self.get_items_by_name(item_names)
+ self.remove(items)
+ return items
def remove(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None:
"""Removes the provided item(s) from state"""
@@ -192,23 +207,32 @@ def count(self, item_name: str) -> int:
def assertAccessDependency(self,
locations: typing.List[str],
- possible_items: typing.Iterable[typing.Iterable[str]]) -> None:
+ possible_items: typing.Iterable[typing.Iterable[str]],
+ only_check_listed: bool = False) -> None:
"""Asserts that the provided locations can't be reached without the listed items but can be reached with any
one of the provided combinations"""
all_items = [item_name for item_names in possible_items for item_name in item_names]
- self.collect_all_but(all_items)
- for location in self.multiworld.get_locations():
- loc_reachable = self.multiworld.state.can_reach(location)
- self.assertEqual(loc_reachable, location.name not in locations,
- f"{location.name} is reachable without {all_items}" if loc_reachable
- else f"{location.name} is not reachable without {all_items}")
+ state = CollectionState(self.multiworld)
+ self.collect_all_but(all_items, state)
+ if only_check_listed:
+ for location in locations:
+ self.assertFalse(state.can_reach(location, "Location", 1), f"{location} is reachable without {all_items}")
+ else:
+ for location in self.multiworld.get_locations():
+ loc_reachable = state.can_reach(location, "Location", 1)
+ self.assertEqual(loc_reachable, location.name not in locations,
+ f"{location.name} is reachable without {all_items}" if loc_reachable
+ else f"{location.name} is not reachable without {all_items}")
for item_names in possible_items:
- items = self.collect_by_name(item_names)
+ items = self.get_items_by_name(item_names)
+ for item in items:
+ state.collect(item)
for location in locations:
- self.assertTrue(self.can_reach_location(location),
+ self.assertTrue(state.can_reach(location, "Location", 1),
f"{location} not reachable with {item_names}")
- self.remove(items)
+ for item in items:
+ state.remove(item)
def assertBeatable(self, beatable: bool):
"""Asserts that the game can be beaten with the current state"""
diff --git a/test/webhost/TestAPIGenerate.py b/test/webhost/TestAPIGenerate.py
index 5c14373a90e6..8ea78f27f93a 100644
--- a/test/webhost/TestAPIGenerate.py
+++ b/test/webhost/TestAPIGenerate.py
@@ -5,7 +5,8 @@
class TestDocs(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
- from WebHost import get_app, raw_app
+ from WebHostLib import app as raw_app
+ from WebHost import get_app
raw_app.config["PONY"] = {
"provider": "sqlite",
"filename": ":memory:",
diff --git a/test/webhost/TestFileGeneration.py b/test/webhost/TestFileGeneration.py
index 6010202c4101..f01b70e14f90 100644
--- a/test/webhost/TestFileGeneration.py
+++ b/test/webhost/TestFileGeneration.py
@@ -14,7 +14,8 @@ def setUpClass(cls) -> None:
cls.incorrect_path = os.path.join(os.path.split(os.path.dirname(__file__))[0], "WebHostLib")
def testOptions(self):
- WebHost.create_options_files()
+ from WebHostLib.options import create as create_options_files
+ create_options_files()
target = os.path.join(self.correct_path, "static", "generated", "configs")
self.assertTrue(os.path.exists(target))
self.assertFalse(os.path.exists(os.path.join(self.incorrect_path, "static", "generated", "configs")))
diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py
index 217269aa9927..e2fda16b87d3 100644
--- a/worlds/AutoWorld.py
+++ b/worlds/AutoWorld.py
@@ -358,6 +358,21 @@ def get_filler_item_name(self) -> str:
logging.warning(f"World {self} is generating a filler item without custom filler pool.")
return self.multiworld.random.choice(tuple(self.item_name_to_id.keys()))
+ @classmethod
+ def create_group(cls, multiworld: "MultiWorld", new_player_id: int, players: Set[int]) -> World:
+ """Creates a group, which is an instance of World that is responsible for multiple others.
+ An example case is ItemLinks creating these."""
+ import Options
+
+ for option_key, option in cls.option_definitions.items():
+ getattr(multiworld, option_key)[new_player_id] = option(option.default)
+ for option_key, option in Options.common_options.items():
+ getattr(multiworld, option_key)[new_player_id] = option(option.default)
+ for option_key, option in Options.per_game_common_options.items():
+ getattr(multiworld, option_key)[new_player_id] = option(option.default)
+
+ return cls(multiworld, new_player_id)
+
# decent place to implement progressive items, in most cases can stay as-is
def collect_item(self, state: "CollectionState", item: "Item", remove: bool = False) -> Optional[str]:
"""Collect an item name into state. For speed reasons items that aren't logically useful get skipped.
diff --git a/worlds/_sc2common/bot/maps.py b/worlds/_sc2common/bot/maps.py
index f14b5af9009e..29ce9f658164 100644
--- a/worlds/_sc2common/bot/maps.py
+++ b/worlds/_sc2common/bot/maps.py
@@ -8,18 +8,31 @@
def get(name: str) -> Map:
- # Iterate through 2 folder depths
for map_dir in (p for p in Paths.MAPS.iterdir()):
- if map_dir.is_dir():
- for map_file in (p for p in map_dir.iterdir()):
- if Map.matches_target_map_name(map_file, name):
- return Map(map_file)
- elif Map.matches_target_map_name(map_dir, name):
- return Map(map_dir)
+ map = find_map_in_dir(name, map_dir)
+ if map is not None:
+ return map
raise KeyError(f"Map '{name}' was not found. Please put the map file in \"/StarCraft II/Maps/\".")
+# Go deeper
+def find_map_in_dir(name, path):
+ if Map.matches_target_map_name(path, name):
+ return Map(path)
+
+ if path.name.endswith("SC2Map"):
+ return None
+
+ if path.is_dir():
+ for childPath in (p for p in path.iterdir()):
+ map = find_map_in_dir(name, childPath)
+ if map is not None:
+ return map
+
+ return None
+
+
class Map:
def __init__(self, path: Path):
diff --git a/worlds/alttp/Client.py b/worlds/alttp/Client.py
index 3939e626724a..edc68473b93f 100644
--- a/worlds/alttp/Client.py
+++ b/worlds/alttp/Client.py
@@ -107,7 +107,7 @@
"Hyrule Castle - Zelda's Chest": (0x80, 0x10),
'Hyrule Castle - Big Key Drop': (0x80, 0x400),
'Sewers - Dark Cross': (0x32, 0x10),
- 'Hyrule Castle - Key Rat Key Drop': (0x21, 0x400),
+ 'Sewers - Key Rat Key Drop': (0x21, 0x400),
'Sewers - Secret Room - Left': (0x11, 0x10),
'Sewers - Secret Room - Middle': (0x11, 0x20),
'Sewers - Secret Room - Right': (0x11, 0x40),
diff --git a/worlds/alttp/Dungeons.py b/worlds/alttp/Dungeons.py
index b789fd6db638..630d61e01959 100644
--- a/worlds/alttp/Dungeons.py
+++ b/worlds/alttp/Dungeons.py
@@ -8,7 +8,7 @@
from .Bosses import BossFactory, Boss
from .Items import ItemFactory
-from .Regions import lookup_boss_drops
+from .Regions import lookup_boss_drops, key_drop_data
from .Options import smallkey_shuffle
if typing.TYPE_CHECKING:
@@ -81,15 +81,17 @@ def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dunge
return dungeon
ES = make_dungeon('Hyrule Castle', None, ['Hyrule Castle', 'Sewers', 'Sewer Drop', 'Sewers (Dark)', 'Sanctuary'],
- None, [ItemFactory('Small Key (Hyrule Castle)', player)],
+ ItemFactory('Big Key (Hyrule Castle)', player),
+ ItemFactory(['Small Key (Hyrule Castle)'] * 4, player),
[ItemFactory('Map (Hyrule Castle)', player)])
EP = make_dungeon('Eastern Palace', 'Armos Knights', ['Eastern Palace'],
- ItemFactory('Big Key (Eastern Palace)', player), [],
+ ItemFactory('Big Key (Eastern Palace)', player),
+ ItemFactory(['Small Key (Eastern Palace)'] * 2, player),
ItemFactory(['Map (Eastern Palace)', 'Compass (Eastern Palace)'], player))
DP = make_dungeon('Desert Palace', 'Lanmolas',
['Desert Palace North', 'Desert Palace Main (Inner)', 'Desert Palace Main (Outer)',
'Desert Palace East'], ItemFactory('Big Key (Desert Palace)', player),
- [ItemFactory('Small Key (Desert Palace)', player)],
+ ItemFactory(['Small Key (Desert Palace)'] * 4, player),
ItemFactory(['Map (Desert Palace)', 'Compass (Desert Palace)'], player))
ToH = make_dungeon('Tower of Hera', 'Moldorm',
['Tower of Hera (Bottom)', 'Tower of Hera (Basement)', 'Tower of Hera (Top)'],
@@ -105,7 +107,8 @@ def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dunge
ItemFactory(['Small Key (Palace of Darkness)'] * 6, player),
ItemFactory(['Map (Palace of Darkness)', 'Compass (Palace of Darkness)'], player))
TT = make_dungeon('Thieves Town', 'Blind', ['Thieves Town (Entrance)', 'Thieves Town (Deep)', 'Blind Fight'],
- ItemFactory('Big Key (Thieves Town)', player), [ItemFactory('Small Key (Thieves Town)', player)],
+ ItemFactory('Big Key (Thieves Town)', player),
+ ItemFactory(['Small Key (Thieves Town)'] * 3, player),
ItemFactory(['Map (Thieves Town)', 'Compass (Thieves Town)'], player))
SW = make_dungeon('Skull Woods', 'Mothula', ['Skull Woods Final Section (Entrance)', 'Skull Woods First Section',
'Skull Woods Second Section', 'Skull Woods Second Section (Drop)',
@@ -113,52 +116,54 @@ def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dunge
'Skull Woods First Section (Right)',
'Skull Woods First Section (Left)', 'Skull Woods First Section (Top)'],
ItemFactory('Big Key (Skull Woods)', player),
- ItemFactory(['Small Key (Skull Woods)'] * 3, player),
+ ItemFactory(['Small Key (Skull Woods)'] * 5, player),
ItemFactory(['Map (Skull Woods)', 'Compass (Skull Woods)'], player))
SP = make_dungeon('Swamp Palace', 'Arrghus',
['Swamp Palace (Entrance)', 'Swamp Palace (First Room)', 'Swamp Palace (Starting Area)',
- 'Swamp Palace (Center)', 'Swamp Palace (North)'], ItemFactory('Big Key (Swamp Palace)', player),
- [ItemFactory('Small Key (Swamp Palace)', player)],
+ 'Swamp Palace (West)', 'Swamp Palace (Center)', 'Swamp Palace (North)'],
+ ItemFactory('Big Key (Swamp Palace)', player),
+ ItemFactory(['Small Key (Swamp Palace)'] * 6, player),
ItemFactory(['Map (Swamp Palace)', 'Compass (Swamp Palace)'], player))
IP = make_dungeon('Ice Palace', 'Kholdstare',
- ['Ice Palace (Entrance)', 'Ice Palace (Main)', 'Ice Palace (East)', 'Ice Palace (East Top)',
- 'Ice Palace (Kholdstare)'], ItemFactory('Big Key (Ice Palace)', player),
- ItemFactory(['Small Key (Ice Palace)'] * 2, player),
+ ['Ice Palace (Entrance)', 'Ice Palace (Second Section)', 'Ice Palace (Main)', 'Ice Palace (East)',
+ 'Ice Palace (East Top)', 'Ice Palace (Kholdstare)'], ItemFactory('Big Key (Ice Palace)', player),
+ ItemFactory(['Small Key (Ice Palace)'] * 6, player),
ItemFactory(['Map (Ice Palace)', 'Compass (Ice Palace)'], player))
MM = make_dungeon('Misery Mire', 'Vitreous',
['Misery Mire (Entrance)', 'Misery Mire (Main)', 'Misery Mire (West)', 'Misery Mire (Final Area)',
'Misery Mire (Vitreous)'], ItemFactory('Big Key (Misery Mire)', player),
- ItemFactory(['Small Key (Misery Mire)'] * 3, player),
+ ItemFactory(['Small Key (Misery Mire)'] * 6, player),
ItemFactory(['Map (Misery Mire)', 'Compass (Misery Mire)'], player))
TR = make_dungeon('Turtle Rock', 'Trinexx',
['Turtle Rock (Entrance)', 'Turtle Rock (First Section)', 'Turtle Rock (Chain Chomp Room)',
+ 'Turtle Rock (Pokey Room)',
'Turtle Rock (Second Section)', 'Turtle Rock (Big Chest)', 'Turtle Rock (Crystaroller Room)',
'Turtle Rock (Dark Room)', 'Turtle Rock (Eye Bridge)', 'Turtle Rock (Trinexx)'],
ItemFactory('Big Key (Turtle Rock)', player),
- ItemFactory(['Small Key (Turtle Rock)'] * 4, player),
+ ItemFactory(['Small Key (Turtle Rock)'] * 6, player),
ItemFactory(['Map (Turtle Rock)', 'Compass (Turtle Rock)'], player))
if multiworld.mode[player] != 'inverted':
AT = make_dungeon('Agahnims Tower', 'Agahnim', ['Agahnims Tower', 'Agahnim 1'], None,
- ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), [])
+ ItemFactory(['Small Key (Agahnims Tower)'] * 4, player), [])
GT = make_dungeon('Ganons Tower', 'Agahnim2',
['Ganons Tower (Entrance)', 'Ganons Tower (Tile Room)', 'Ganons Tower (Compass Room)',
'Ganons Tower (Hookshot Room)', 'Ganons Tower (Map Room)', 'Ganons Tower (Firesnake Room)',
'Ganons Tower (Teleport Room)', 'Ganons Tower (Bottom)', 'Ganons Tower (Top)',
'Ganons Tower (Before Moldorm)', 'Ganons Tower (Moldorm)', 'Agahnim 2'],
ItemFactory('Big Key (Ganons Tower)', player),
- ItemFactory(['Small Key (Ganons Tower)'] * 4, player),
+ ItemFactory(['Small Key (Ganons Tower)'] * 8, player),
ItemFactory(['Map (Ganons Tower)', 'Compass (Ganons Tower)'], player))
else:
AT = make_dungeon('Inverted Agahnims Tower', 'Agahnim', ['Inverted Agahnims Tower', 'Agahnim 1'], None,
- ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), [])
+ ItemFactory(['Small Key (Agahnims Tower)'] * 4, player), [])
GT = make_dungeon('Inverted Ganons Tower', 'Agahnim2',
['Inverted Ganons Tower (Entrance)', 'Ganons Tower (Tile Room)',
'Ganons Tower (Compass Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower (Map Room)',
'Ganons Tower (Firesnake Room)', 'Ganons Tower (Teleport Room)', 'Ganons Tower (Bottom)',
'Ganons Tower (Top)', 'Ganons Tower (Before Moldorm)', 'Ganons Tower (Moldorm)',
'Agahnim 2'], ItemFactory('Big Key (Ganons Tower)', player),
- ItemFactory(['Small Key (Ganons Tower)'] * 4, player),
+ ItemFactory(['Small Key (Ganons Tower)'] * 8, player),
ItemFactory(['Map (Ganons Tower)', 'Compass (Ganons Tower)'], player))
GT.bosses['bottom'] = BossFactory('Armos Knights', player)
@@ -195,10 +200,11 @@ def fill_dungeons_restrictive(multiworld: MultiWorld):
dungeon_specific: set = set()
for subworld in multiworld.get_game_worlds("A Link to the Past"):
player = subworld.player
- localized |= {(player, item_name) for item_name in
- subworld.dungeon_local_item_names}
- dungeon_specific |= {(player, item_name) for item_name in
- subworld.dungeon_specific_item_names}
+ if player not in multiworld.groups:
+ localized |= {(player, item_name) for item_name in
+ subworld.dungeon_local_item_names}
+ dungeon_specific |= {(player, item_name) for item_name in
+ subworld.dungeon_specific_item_names}
if localized:
in_dungeon_items = [item for item in get_dungeon_item_pool(multiworld) if (item.player, item.name) in localized]
@@ -249,7 +255,16 @@ def fill_dungeons_restrictive(multiworld: MultiWorld):
if all_state_base.has("Triforce", player):
all_state_base.remove(multiworld.worlds[player].create_item("Triforce"))
- fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True, allow_excluded=True)
+ for (player, key_drop_shuffle) in multiworld.key_drop_shuffle.items():
+ if not key_drop_shuffle and player not in multiworld.groups:
+ for key_loc in key_drop_data:
+ key_data = key_drop_data[key_loc]
+ all_state_base.remove(ItemFactory(key_data[3], player))
+ loc = multiworld.get_location(key_loc, player)
+
+ if loc in all_state_base.events:
+ all_state_base.events.remove(loc)
+ fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True)
dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A],
diff --git a/worlds/alttp/EntranceShuffle.py b/worlds/alttp/EntranceShuffle.py
index b7fe688431b7..07bb587eebe3 100644
--- a/worlds/alttp/EntranceShuffle.py
+++ b/worlds/alttp/EntranceShuffle.py
@@ -3134,6 +3134,7 @@ def plando_connect(world, player: int):
('Swamp Palace Moat', 'Swamp Palace (First Room)'),
('Swamp Palace Small Key Door', 'Swamp Palace (Starting Area)'),
('Swamp Palace (Center)', 'Swamp Palace (Center)'),
+ ('Swamp Palace (West)', 'Swamp Palace (West)'),
('Swamp Palace (North)', 'Swamp Palace (North)'),
('Thieves Town Big Key Door', 'Thieves Town (Deep)'),
('Skull Woods Torch Room', 'Skull Woods Final Section (Mothula)'),
@@ -3148,7 +3149,8 @@ def plando_connect(world, player: int):
('Blind Fight', 'Blind Fight'),
('Desert Palace Pots (Outer)', 'Desert Palace Main (Inner)'),
('Desert Palace Pots (Inner)', 'Desert Palace Main (Outer)'),
- ('Ice Palace Entrance Room', 'Ice Palace (Main)'),
+ ('Ice Palace (Main)', 'Ice Palace (Main)'),
+ ('Ice Palace (Second Section)', 'Ice Palace (Second Section)'),
('Ice Palace (East)', 'Ice Palace (East)'),
('Ice Palace (East Top)', 'Ice Palace (East Top)'),
('Ice Palace (Kholdstare)', 'Ice Palace (Kholdstare)'),
@@ -3158,9 +3160,11 @@ def plando_connect(world, player: int):
('Misery Mire (Vitreous)', 'Misery Mire (Vitreous)'),
('Turtle Rock Entrance Gap', 'Turtle Rock (First Section)'),
('Turtle Rock Entrance Gap Reverse', 'Turtle Rock (Entrance)'),
- ('Turtle Rock Pokey Room', 'Turtle Rock (Chain Chomp Room)'),
+ ('Turtle Rock Entrance to Pokey Room', 'Turtle Rock (Pokey Room)'),
+ ('Turtle Rock (Pokey Room) (South)', 'Turtle Rock (First Section)'),
+ ('Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Chain Chomp Room)'),
('Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Second Section)'),
- ('Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock (First Section)'),
+ ('Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock (Pokey Room)'),
('Turtle Rock Chain Chomp Staircase', 'Turtle Rock (Chain Chomp Room)'),
('Turtle Rock (Big Chest) (North)', 'Turtle Rock (Second Section)'),
('Turtle Rock Big Key Door', 'Turtle Rock (Crystaroller Room)'),
@@ -3285,6 +3289,7 @@ def plando_connect(world, player: int):
('Swamp Palace Moat', 'Swamp Palace (First Room)'),
('Swamp Palace Small Key Door', 'Swamp Palace (Starting Area)'),
('Swamp Palace (Center)', 'Swamp Palace (Center)'),
+ ('Swamp Palace (West)', 'Swamp Palace (West)'),
('Swamp Palace (North)', 'Swamp Palace (North)'),
('Thieves Town Big Key Door', 'Thieves Town (Deep)'),
('Skull Woods Torch Room', 'Skull Woods Final Section (Mothula)'),
@@ -3299,7 +3304,8 @@ def plando_connect(world, player: int):
('Blind Fight', 'Blind Fight'),
('Desert Palace Pots (Outer)', 'Desert Palace Main (Inner)'),
('Desert Palace Pots (Inner)', 'Desert Palace Main (Outer)'),
- ('Ice Palace Entrance Room', 'Ice Palace (Main)'),
+ ('Ice Palace (Main)', 'Ice Palace (Main)'),
+ ('Ice Palace (Second Section)', 'Ice Palace (Second Section)'),
('Ice Palace (East)', 'Ice Palace (East)'),
('Ice Palace (East Top)', 'Ice Palace (East Top)'),
('Ice Palace (Kholdstare)', 'Ice Palace (Kholdstare)'),
@@ -3309,9 +3315,11 @@ def plando_connect(world, player: int):
('Misery Mire (Vitreous)', 'Misery Mire (Vitreous)'),
('Turtle Rock Entrance Gap', 'Turtle Rock (First Section)'),
('Turtle Rock Entrance Gap Reverse', 'Turtle Rock (Entrance)'),
- ('Turtle Rock Pokey Room', 'Turtle Rock (Chain Chomp Room)'),
+ ('Turtle Rock Entrance to Pokey Room', 'Turtle Rock (Pokey Room)'),
+ ('Turtle Rock (Pokey Room) (South)', 'Turtle Rock (First Section)'),
+ ('Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Chain Chomp Room)'),
('Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Second Section)'),
- ('Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock (First Section)'),
+ ('Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock (Pokey Room)'),
('Turtle Rock Chain Chomp Staircase', 'Turtle Rock (Chain Chomp Room)'),
('Turtle Rock (Big Chest) (North)', 'Turtle Rock (Second Section)'),
('Turtle Rock Big Key Door', 'Turtle Rock (Crystaroller Room)'),
diff --git a/worlds/alttp/InvertedRegions.py b/worlds/alttp/InvertedRegions.py
index acec73bf33be..ffa23881d3d9 100644
--- a/worlds/alttp/InvertedRegions.py
+++ b/worlds/alttp/InvertedRegions.py
@@ -149,41 +149,37 @@ def create_inverted_regions(world, player):
create_lw_region(world, player, 'Desert Palace Entrance (North) Spot', None,
['Desert Palace Entrance (North)', 'Desert Ledge Return Rocks',
'Desert Palace North Mirror Spot']),
- create_dungeon_region(world, player, 'Desert Palace Main (Outer)', 'Desert Palace',
- ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'],
- ['Desert Palace Pots (Outer)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)',
- 'Desert Palace East Wing']),
- create_dungeon_region(world, player, 'Desert Palace Main (Inner)', 'Desert Palace', None,
- ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']),
- create_dungeon_region(world, player, 'Desert Palace East', 'Desert Palace',
- ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']),
+ create_dungeon_region(world, player, 'Desert Palace Main (Outer)', 'Desert Palace', ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'],
+ ['Desert Palace Pots (Outer)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)', 'Desert Palace East Wing']),
+ create_dungeon_region(world, player, 'Desert Palace Main (Inner)', 'Desert Palace', None, ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']),
+ create_dungeon_region(world, player, 'Desert Palace East', 'Desert Palace', ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']),
create_dungeon_region(world, player, 'Desert Palace North', 'Desert Palace',
- ['Desert Palace - Boss', 'Desert Palace - Prize'], ['Desert Palace Exit (North)']),
+ ['Desert Palace - Desert Tiles 1 Pot Key', 'Desert Palace - Beamos Hall Pot Key',
+ 'Desert Palace - Desert Tiles 2 Pot Key',
+ 'Desert Palace - Boss', 'Desert Palace - Prize'], ['Desert Palace Exit (North)']),
create_dungeon_region(world, player, 'Eastern Palace', 'Eastern Palace',
['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest',
'Eastern Palace - Cannonball Chest',
- 'Eastern Palace - Big Key Chest', 'Eastern Palace - Map Chest', 'Eastern Palace - Boss',
- 'Eastern Palace - Prize'], ['Eastern Palace Exit']),
+ 'Eastern Palace - Dark Square Pot Key', 'Eastern Palace - Dark Eyegore Key Drop',
+ 'Eastern Palace - Big Key Chest',
+ 'Eastern Palace - Map Chest', 'Eastern Palace - Boss', 'Eastern Palace - Prize'],
+ ['Eastern Palace Exit']),
create_lw_region(world, player, 'Master Sword Meadow', ['Master Sword Pedestal']),
create_cave_region(world, player, 'Lost Woods Gamble', 'a game of chance'),
- create_lw_region(world, player, 'Hyrule Castle Ledge', None,
- ['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Inverted Ganons Tower',
- 'Hyrule Castle Ledge Courtyard Drop', 'Inverted Pyramid Hole']),
+ create_lw_region(world, player, 'Hyrule Castle Ledge', None, ['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Inverted Ganons Tower', 'Hyrule Castle Ledge Courtyard Drop', 'Inverted Pyramid Hole']),
create_dungeon_region(world, player, 'Hyrule Castle', 'Hyrule Castle',
['Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest',
- 'Hyrule Castle - Zelda\'s Chest'],
+ 'Hyrule Castle - Zelda\'s Chest',
+ 'Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop',
+ 'Hyrule Castle - Big Key Drop'],
['Hyrule Castle Exit (East)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (South)',
'Throne Room']),
create_dungeon_region(world, player, 'Sewer Drop', 'a drop\'s exit', None, ['Sewer Drop']), # This exists only to be referenced for access checks
- create_dungeon_region(world, player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross'],
- ['Sewers Door']),
- create_dungeon_region(world, player, 'Sewers', 'a drop\'s exit',
- ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle',
- 'Sewers - Secret Room - Right'], ['Sanctuary Push Door', 'Sewers Back Door']),
+ create_dungeon_region(world, player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross', 'Sewers - Key Rat Key Drop'], ['Sewers Door']),
+ create_dungeon_region(world, player, 'Sewers', 'a drop\'s exit', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle',
+ 'Sewers - Secret Room - Right'], ['Sanctuary Push Door', 'Sewers Back Door']),
create_dungeon_region(world, player, 'Sanctuary', 'a drop\'s exit', ['Sanctuary'], ['Sanctuary Exit']),
- create_dungeon_region(world, player, 'Inverted Agahnims Tower', 'Castle Tower',
- ['Castle Tower - Room 03', 'Castle Tower - Dark Maze'],
- ['Agahnim 1', 'Inverted Agahnims Tower Exit']),
+ create_dungeon_region(world, player, 'Inverted Agahnims Tower', 'Castle Tower', ['Castle Tower - Room 03', 'Castle Tower - Dark Maze', 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'], ['Agahnim 1', 'Inverted Agahnims Tower Exit']),
create_dungeon_region(world, player, 'Agahnim 1', 'Castle Tower', ['Agahnim 1'], None),
create_cave_region(world, player, 'Old Man Cave', 'a connector', ['Old Man'],
['Old Man Cave Exit (East)', 'Old Man Cave Exit (West)']),
@@ -253,14 +249,9 @@ def create_inverted_regions(world, player):
'Death Mountain (Top) Mirror Spot']),
create_dw_region(world, player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'],
['Bumper Cave Ledge Drop', 'Bumper Cave (Top)']),
- create_dungeon_region(world, player, 'Tower of Hera (Bottom)', 'Tower of Hera',
- ['Tower of Hera - Basement Cage', 'Tower of Hera - Map Chest'],
- ['Tower of Hera Small Key Door', 'Tower of Hera Big Key Door', 'Tower of Hera Exit']),
- create_dungeon_region(world, player, 'Tower of Hera (Basement)', 'Tower of Hera',
- ['Tower of Hera - Big Key Chest']),
- create_dungeon_region(world, player, 'Tower of Hera (Top)', 'Tower of Hera',
- ['Tower of Hera - Compass Chest', 'Tower of Hera - Big Chest', 'Tower of Hera - Boss',
- 'Tower of Hera - Prize']),
+ create_dungeon_region(world, player, 'Tower of Hera (Bottom)', 'Tower of Hera', ['Tower of Hera - Basement Cage', 'Tower of Hera - Map Chest'], ['Tower of Hera Small Key Door', 'Tower of Hera Big Key Door', 'Tower of Hera Exit']),
+ create_dungeon_region(world, player, 'Tower of Hera (Basement)', 'Tower of Hera', ['Tower of Hera - Big Key Chest']),
+ create_dungeon_region(world, player, 'Tower of Hera (Top)', 'Tower of Hera', ['Tower of Hera - Compass Chest', 'Tower of Hera - Big Chest', 'Tower of Hera - Boss', 'Tower of Hera - Prize']),
create_dw_region(world, player, 'East Dark World', ['Pyramid'],
['Pyramid Fairy', 'South Dark World Bridge', 'Palace of Darkness',
@@ -360,128 +351,82 @@ def create_inverted_regions(world, player):
['Floating Island Drop', 'Hookshot Cave Back Entrance']),
create_cave_region(world, player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']),
- create_dungeon_region(world, player, 'Swamp Palace (Entrance)', 'Swamp Palace', None,
- ['Swamp Palace Moat', 'Swamp Palace Exit']),
- create_dungeon_region(world, player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'],
- ['Swamp Palace Small Key Door']),
- create_dungeon_region(world, player, 'Swamp Palace (Starting Area)', 'Swamp Palace',
- ['Swamp Palace - Map Chest'], ['Swamp Palace (Center)']),
- create_dungeon_region(world, player, 'Swamp Palace (Center)', 'Swamp Palace',
- ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest',
- 'Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest'], ['Swamp Palace (North)']),
- create_dungeon_region(world, player, 'Swamp Palace (North)', 'Swamp Palace',
- ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right',
- 'Swamp Palace - Waterfall Room', 'Swamp Palace - Boss', 'Swamp Palace - Prize']),
- create_dungeon_region(world, player, 'Thieves Town (Entrance)', 'Thieves\' Town',
- ['Thieves\' Town - Big Key Chest',
- 'Thieves\' Town - Map Chest',
- 'Thieves\' Town - Compass Chest',
- 'Thieves\' Town - Ambush Chest'], ['Thieves Town Big Key Door', 'Thieves Town Exit']),
+ create_dungeon_region(world, player, 'Swamp Palace (Entrance)', 'Swamp Palace', None, ['Swamp Palace Moat', 'Swamp Palace Exit']),
+ create_dungeon_region(world, player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'], ['Swamp Palace Small Key Door']),
+ create_dungeon_region(world, player, 'Swamp Palace (Starting Area)', 'Swamp Palace', ['Swamp Palace - Map Chest', 'Swamp Palace - Pot Row Pot Key',
+ 'Swamp Palace - Trench 1 Pot Key'], ['Swamp Palace (Center)']),
+ create_dungeon_region(world, player, 'Swamp Palace (Center)', 'Swamp Palace', ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest', 'Swamp Palace - Hookshot Pot Key',
+ 'Swamp Palace - Trench 2 Pot Key'], ['Swamp Palace (North)', 'Swamp Palace (West)']),
+ create_dungeon_region(world, player, 'Swamp Palace (West)', 'Swamp Palace', ['Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest']),
+ create_dungeon_region(world, player, 'Swamp Palace (North)', 'Swamp Palace', ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right',
+ 'Swamp Palace - Waterway Pot Key', 'Swamp Palace - Waterfall Room',
+ 'Swamp Palace - Boss', 'Swamp Palace - Prize']),
+ create_dungeon_region(world, player, 'Thieves Town (Entrance)', 'Thieves\' Town', ['Thieves\' Town - Big Key Chest',
+ 'Thieves\' Town - Map Chest',
+ 'Thieves\' Town - Compass Chest',
+ 'Thieves\' Town - Ambush Chest'], ['Thieves Town Big Key Door', 'Thieves Town Exit']),
create_dungeon_region(world, player, 'Thieves Town (Deep)', 'Thieves\' Town', ['Thieves\' Town - Attic',
- 'Thieves\' Town - Big Chest',
- 'Thieves\' Town - Blind\'s Cell'],
+ 'Thieves\' Town - Big Chest',
+ 'Thieves\' Town - Hallway Pot Key',
+ 'Thieves\' Town - Spike Switch Pot Key',
+ 'Thieves\' Town - Blind\'s Cell'],
['Blind Fight']),
- create_dungeon_region(world, player, 'Blind Fight', 'Thieves\' Town',
- ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']),
- create_dungeon_region(world, player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'],
- ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump',
- 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']),
- create_dungeon_region(world, player, 'Skull Woods First Section (Right)', 'Skull Woods',
- ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']),
- create_dungeon_region(world, player, 'Skull Woods First Section (Left)', 'Skull Woods',
- ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'],
- ['Skull Woods First Section (Left) Door to Exit',
- 'Skull Woods First Section (Left) Door to Right']),
- create_dungeon_region(world, player, 'Skull Woods First Section (Top)', 'Skull Woods',
- ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']),
- create_dungeon_region(world, player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None,
- ['Skull Woods Second Section (Drop)']),
- create_dungeon_region(world, player, 'Skull Woods Second Section', 'Skull Woods',
- ['Skull Woods - Big Key Chest'],
- ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']),
- create_dungeon_region(world, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods',
- ['Skull Woods - Bridge Room'],
- ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']),
- create_dungeon_region(world, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods',
- ['Skull Woods - Boss', 'Skull Woods - Prize']),
- create_dungeon_region(world, player, 'Ice Palace (Entrance)', 'Ice Palace', None,
- ['Ice Palace Entrance Room', 'Ice Palace Exit']),
- create_dungeon_region(world, player, 'Ice Palace (Main)', 'Ice Palace',
- ['Ice Palace - Compass Chest', 'Ice Palace - Freezor Chest',
- 'Ice Palace - Big Chest', 'Ice Palace - Iced T Room'],
- ['Ice Palace (East)', 'Ice Palace (Kholdstare)']),
- create_dungeon_region(world, player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'],
- ['Ice Palace (East Top)']),
- create_dungeon_region(world, player, 'Ice Palace (East Top)', 'Ice Palace',
- ['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest']),
- create_dungeon_region(world, player, 'Ice Palace (Kholdstare)', 'Ice Palace',
- ['Ice Palace - Boss', 'Ice Palace - Prize']),
- create_dungeon_region(world, player, 'Misery Mire (Entrance)', 'Misery Mire', None,
- ['Misery Mire Entrance Gap', 'Misery Mire Exit']),
- create_dungeon_region(world, player, 'Misery Mire (Main)', 'Misery Mire',
- ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby',
- 'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest'],
- ['Misery Mire (West)', 'Misery Mire Big Key Door']),
- create_dungeon_region(world, player, 'Misery Mire (West)', 'Misery Mire',
- ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']),
- create_dungeon_region(world, player, 'Misery Mire (Final Area)', 'Misery Mire', None,
- ['Misery Mire (Vitreous)']),
- create_dungeon_region(world, player, 'Misery Mire (Vitreous)', 'Misery Mire',
- ['Misery Mire - Boss', 'Misery Mire - Prize']),
- create_dungeon_region(world, player, 'Turtle Rock (Entrance)', 'Turtle Rock', None,
- ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']),
- create_dungeon_region(world, player, 'Turtle Rock (First Section)', 'Turtle Rock',
- ['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left',
- 'Turtle Rock - Roller Room - Right'],
- ['Turtle Rock Pokey Room', 'Turtle Rock Entrance Gap Reverse']),
- create_dungeon_region(world, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock',
- ['Turtle Rock - Chain Chomps'],
+ create_dungeon_region(world, player, 'Blind Fight', 'Thieves\' Town', ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']),
+ create_dungeon_region(world, player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'], ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump', 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']),
+ create_dungeon_region(world, player, 'Skull Woods First Section (Right)', 'Skull Woods', ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']),
+ create_dungeon_region(world, player, 'Skull Woods First Section (Left)', 'Skull Woods', ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'], ['Skull Woods First Section (Left) Door to Exit', 'Skull Woods First Section (Left) Door to Right']),
+ create_dungeon_region(world, player, 'Skull Woods First Section (Top)', 'Skull Woods', ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']),
+ create_dungeon_region(world, player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None, ['Skull Woods Second Section (Drop)']),
+ create_dungeon_region(world, player, 'Skull Woods Second Section', 'Skull Woods', ['Skull Woods - Big Key Chest', 'Skull Woods - West Lobby Pot Key'], ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']),
+ create_dungeon_region(world, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', ['Skull Woods - Bridge Room', 'Skull Woods - Spike Corner Key Drop'], ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']),
+ create_dungeon_region(world, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', ['Skull Woods - Boss', 'Skull Woods - Prize']),
+ create_dungeon_region(world, player, 'Ice Palace (Entrance)', 'Ice Palace', ['Ice Palace - Jelly Key Drop'], ['Ice Palace (Second Section)', 'Ice Palace Exit']),
+ create_dungeon_region(world, player, 'Ice Palace (Second Section)', 'Ice Palace', ['Ice Palace - Conveyor Key Drop', 'Ice Palace - Compass Chest'], ['Ice Palace (Main)']),
+ create_dungeon_region(world, player, 'Ice Palace (Main)', 'Ice Palace', ['Ice Palace - Freezor Chest',
+ 'Ice Palace - Many Pots Pot Key',
+ 'Ice Palace - Big Chest', 'Ice Palace - Iced T Room'], ['Ice Palace (East)', 'Ice Palace (Kholdstare)']),
+ create_dungeon_region(world, player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'], ['Ice Palace (East Top)']),
+ create_dungeon_region(world, player, 'Ice Palace (East Top)', 'Ice Palace', ['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest', 'Ice Palace - Hammer Block Key Drop']),
+ create_dungeon_region(world, player, 'Ice Palace (Kholdstare)', 'Ice Palace', ['Ice Palace - Boss', 'Ice Palace - Prize']),
+ create_dungeon_region(world, player, 'Misery Mire (Entrance)', 'Misery Mire', None, ['Misery Mire Entrance Gap', 'Misery Mire Exit']),
+ create_dungeon_region(world, player, 'Misery Mire (Main)', 'Misery Mire', ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby',
+ 'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest',
+ 'Misery Mire - Spikes Pot Key', 'Misery Mire - Fishbone Pot Key',
+ 'Misery Mire - Conveyor Crystal Key Drop'], ['Misery Mire (West)', 'Misery Mire Big Key Door']),
+ create_dungeon_region(world, player, 'Misery Mire (West)', 'Misery Mire', ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']),
+ create_dungeon_region(world, player, 'Misery Mire (Final Area)', 'Misery Mire', None, ['Misery Mire (Vitreous)']),
+ create_dungeon_region(world, player, 'Misery Mire (Vitreous)', 'Misery Mire', ['Misery Mire - Boss', 'Misery Mire - Prize']),
+ create_dungeon_region(world, player, 'Turtle Rock (Entrance)', 'Turtle Rock', None, ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']),
+ create_dungeon_region(world, player, 'Turtle Rock (First Section)', 'Turtle Rock', ['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left',
+ 'Turtle Rock - Roller Room - Right'],
+ ['Turtle Rock Entrance to Pokey Room', 'Turtle Rock Entrance Gap Reverse']),
+ create_dungeon_region(world, player, 'Turtle Rock (Pokey Room)', 'Turtle Rock', ['Turtle Rock - Pokey 1 Key Drop'], ['Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Pokey Room) (South)']),
+ create_dungeon_region(world, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock', ['Turtle Rock - Chain Chomps'],
['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']),
create_dungeon_region(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock',
- ['Turtle Rock - Big Key Chest'],
+ ['Turtle Rock - Big Key Chest', 'Turtle Rock - Pokey 2 Key Drop'],
['Turtle Rock Ledge Exit (West)', 'Turtle Rock Chain Chomp Staircase',
'Turtle Rock Big Key Door']),
- create_dungeon_region(world, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'],
- ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']),
- create_dungeon_region(world, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock',
- ['Turtle Rock - Crystaroller Room'],
- ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']),
- create_dungeon_region(world, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None,
- ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']),
- create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock',
- ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right',
- 'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'],
- ['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)',
- 'Turtle Rock Isolated Ledge Exit']),
- create_dungeon_region(world, player, 'Turtle Rock (Trinexx)', 'Turtle Rock',
- ['Turtle Rock - Boss', 'Turtle Rock - Prize']),
- create_dungeon_region(world, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness',
- ['Palace of Darkness - Shooter Room'],
- ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall',
- 'Palace of Darkness Exit']),
- create_dungeon_region(world, player, 'Palace of Darkness (Center)', 'Palace of Darkness',
- ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'],
- ['Palace of Darkness Big Key Chest Staircase', 'Palace of Darkness (North)',
- 'Palace of Darkness Big Key Door']),
- create_dungeon_region(world, player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness',
- ['Palace of Darkness - Big Key Chest']),
- create_dungeon_region(world, player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness',
- ['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'],
- ['Palace of Darkness Hammer Peg Drop']),
- create_dungeon_region(world, player, 'Palace of Darkness (North)', 'Palace of Darkness',
- ['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left',
- 'Palace of Darkness - Dark Basement - Right'],
+ create_dungeon_region(world, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']),
+ create_dungeon_region(world, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']),
+ create_dungeon_region(world, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']),
+ create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right',
+ 'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'],
+ ['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Isolated Ledge Exit']),
+ create_dungeon_region(world, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']),
+ create_dungeon_region(world, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']),
+ create_dungeon_region(world, player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'],
+ ['Palace of Darkness Big Key Chest Staircase', 'Palace of Darkness (North)', 'Palace of Darkness Big Key Door']),
+ create_dungeon_region(world, player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness', ['Palace of Darkness - Big Key Chest']),
+ create_dungeon_region(world, player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'], ['Palace of Darkness Hammer Peg Drop']),
+ create_dungeon_region(world, player, 'Palace of Darkness (North)', 'Palace of Darkness', ['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Basement - Right'],
['Palace of Darkness Spike Statue Room Door', 'Palace of Darkness Maze Door']),
- create_dungeon_region(world, player, 'Palace of Darkness (Maze)', 'Palace of Darkness',
- ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom',
- 'Palace of Darkness - Big Chest']),
- create_dungeon_region(world, player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness',
- ['Palace of Darkness - Harmless Hellway']),
- create_dungeon_region(world, player, 'Palace of Darkness (Final Section)', 'Palace of Darkness',
- ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']),
+ create_dungeon_region(world, player, 'Palace of Darkness (Maze)', 'Palace of Darkness', ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Big Chest']),
+ create_dungeon_region(world, player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness', ['Palace of Darkness - Harmless Hellway']),
+ create_dungeon_region(world, player, 'Palace of Darkness (Final Section)', 'Palace of Darkness', ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']),
create_dungeon_region(world, player, 'Inverted Ganons Tower (Entrance)', 'Ganon\'s Tower',
['Ganons Tower - Bob\'s Torch', 'Ganons Tower - Hope Room - Left',
- 'Ganons Tower - Hope Room - Right'],
+ 'Ganons Tower - Hope Room - Right', 'Ganons Tower - Conveyor Cross Pot Key'],
['Ganons Tower (Tile Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower Big Key Door',
'Inverted Ganons Tower Exit']),
create_dungeon_region(world, player, 'Ganons Tower (Tile Room)', 'Ganon\'s Tower', ['Ganons Tower - Tile Room'],
@@ -489,10 +434,13 @@ def create_inverted_regions(world, player):
create_dungeon_region(world, player, 'Ganons Tower (Compass Room)', 'Ganon\'s Tower',
['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right',
'Ganons Tower - Compass Room - Bottom Left',
- 'Ganons Tower - Compass Room - Bottom Right'], ['Ganons Tower (Bottom) (East)']),
+ 'Ganons Tower - Compass Room - Bottom Right',
+ 'Ganons Tower - Conveyor Star Pits Pot Key'],
+ ['Ganons Tower (Bottom) (East)']),
create_dungeon_region(world, player, 'Ganons Tower (Hookshot Room)', 'Ganon\'s Tower',
['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right',
- 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right'],
+ 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right',
+ 'Ganons Tower - Double Switch Pot Key'],
['Ganons Tower (Map Room)', 'Ganons Tower (Double Switch Room)']),
create_dungeon_region(world, player, 'Ganons Tower (Map Room)', 'Ganon\'s Tower', ['Ganons Tower - Map Chest']),
create_dungeon_region(world, player, 'Ganons Tower (Firesnake Room)', 'Ganon\'s Tower',
@@ -501,21 +449,21 @@ def create_inverted_regions(world, player):
['Ganons Tower - Randomizer Room - Top Left',
'Ganons Tower - Randomizer Room - Top Right',
'Ganons Tower - Randomizer Room - Bottom Left',
- 'Ganons Tower - Randomizer Room - Bottom Right'], ['Ganons Tower (Bottom) (West)']),
+ 'Ganons Tower - Randomizer Room - Bottom Right'],
+ ['Ganons Tower (Bottom) (West)']),
create_dungeon_region(world, player, 'Ganons Tower (Bottom)', 'Ganon\'s Tower',
['Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest',
'Ganons Tower - Big Key Room - Left',
'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Chest']),
- create_dungeon_region(world, player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None,
- ['Ganons Tower Torch Rooms']),
+ create_dungeon_region(world, player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None, ['Ganons Tower Torch Rooms']),
create_dungeon_region(world, player, 'Ganons Tower (Before Moldorm)', 'Ganon\'s Tower',
['Ganons Tower - Mini Helmasaur Room - Left',
'Ganons Tower - Mini Helmasaur Room - Right',
- 'Ganons Tower - Pre-Moldorm Chest'], ['Ganons Tower Moldorm Door']),
- create_dungeon_region(world, player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None,
- ['Ganons Tower Moldorm Gap']),
- create_dungeon_region(world, player, 'Agahnim 2', 'Ganon\'s Tower',
- ['Ganons Tower - Validation Chest', 'Agahnim 2'], None),
+ 'Ganons Tower - Pre-Moldorm Chest', 'Ganons Tower - Mini Helmasaur Key Drop'],
+ ['Ganons Tower Moldorm Door']),
+ create_dungeon_region(world, player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None, ['Ganons Tower Moldorm Gap']),
+
+ create_dungeon_region(world, player, 'Agahnim 2', 'Ganon\'s Tower', ['Ganons Tower - Validation Chest', 'Agahnim 2'], None),
create_cave_region(world, player, 'Pyramid', 'a drop\'s exit', ['Ganon'], ['Ganon Drop']),
create_cave_region(world, player, 'Bottom of Pyramid', 'a drop\'s exit', None, ['Pyramid Exit']),
create_dw_region(world, player, 'Pyramid Ledge', None, ['Pyramid Drop']), # houlihan room exits here in inverted
diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py
index 56eb355837d4..f8fdd55ef657 100644
--- a/worlds/alttp/ItemPool.py
+++ b/worlds/alttp/ItemPool.py
@@ -12,6 +12,7 @@
from .Items import ItemFactory, GetBeemizerItem
from .Options import smallkey_shuffle, compass_shuffle, bigkey_shuffle, map_shuffle, LTTPBosses
from .StateHelpers import has_triforce_pieces, has_melee_weapon
+from .Regions import key_drop_data
# This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space.
# Some basic items that various modes require are placed here, including pendants and crystals. Medallion requirements for the two relevant entrances are also decided.
@@ -80,7 +81,7 @@
basicglove=basicgloves,
alwaysitems=alwaysitems,
legacyinsanity=legacyinsanity,
- universal_keys=['Small Key (Universal)'] * 28,
+ universal_keys=['Small Key (Universal)'] * 29,
extras=[easyfirst15extra, easysecond15extra, easythird10extra, easyfourth5extra, easyfinal25extra],
progressive_sword_limit=8,
progressive_shield_limit=6,
@@ -112,7 +113,7 @@
basicglove=basicgloves,
alwaysitems=alwaysitems,
legacyinsanity=legacyinsanity,
- universal_keys=['Small Key (Universal)'] * 18 + ['Rupees (20)'] * 10,
+ universal_keys=['Small Key (Universal)'] * 19 + ['Rupees (20)'] * 10,
extras=[normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra],
progressive_sword_limit=4,
progressive_shield_limit=3,
@@ -144,7 +145,7 @@
basicglove=basicgloves,
alwaysitems=alwaysitems,
legacyinsanity=legacyinsanity,
- universal_keys=['Small Key (Universal)'] * 12 + ['Rupees (5)'] * 16,
+ universal_keys=['Small Key (Universal)'] * 13 + ['Rupees (5)'] * 16,
extras=[normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra],
progressive_sword_limit=3,
progressive_shield_limit=2,
@@ -176,7 +177,7 @@
basicglove=basicgloves,
alwaysitems=alwaysitems,
legacyinsanity=legacyinsanity,
- universal_keys=['Small Key (Universal)'] * 12 + ['Rupees (5)'] * 16,
+ universal_keys=['Small Key (Universal)'] * 13 + ['Rupees (5)'] * 16,
extras=[normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra],
progressive_sword_limit=2,
progressive_shield_limit=1,
@@ -212,7 +213,7 @@
basicglove=['Nothing'] * 2,
alwaysitems=['Ice Rod'] + ['Nothing'] * 19,
legacyinsanity=['Nothing'] * 2,
- universal_keys=['Nothing'] * 28,
+ universal_keys=['Nothing'] * 29,
extras=[['Nothing'] * 15, ['Nothing'] * 15, ['Nothing'] * 10, ['Nothing'] * 5, ['Nothing'] * 25],
progressive_sword_limit=difficulties[diff].progressive_sword_limit,
progressive_shield_limit=difficulties[diff].progressive_shield_limit,
@@ -281,7 +282,6 @@ def generate_itempool(world):
itempool.extend(['Arrows (10)'] * 7)
if multiworld.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
itempool.extend(itemdiff.universal_keys)
- itempool.append('Small Key (Universal)')
for item in itempool:
multiworld.push_precollected(ItemFactory(item, player))
@@ -374,11 +374,38 @@ def generate_itempool(world):
dungeon_items = [item for item in get_dungeon_item_pool_player(world)
if item.name not in multiworld.worlds[player].dungeon_local_item_names]
- dungeon_item_replacements = difficulties[multiworld.difficulty[player]].extras[0]\
- + difficulties[multiworld.difficulty[player]].extras[1]\
- + difficulties[multiworld.difficulty[player]].extras[2]\
- + difficulties[multiworld.difficulty[player]].extras[3]\
- + difficulties[multiworld.difficulty[player]].extras[4]
+
+ for key_loc in key_drop_data:
+ key_data = key_drop_data[key_loc]
+ drop_item = ItemFactory(key_data[3], player)
+ if multiworld.goal[player] == 'icerodhunt' or not multiworld.key_drop_shuffle[player]:
+ if drop_item in dungeon_items:
+ dungeon_items.remove(drop_item)
+ else:
+ dungeon = drop_item.name.split("(")[1].split(")")[0]
+ if multiworld.mode[player] == 'inverted':
+ if dungeon == "Agahnims Tower":
+ dungeon = "Inverted Agahnims Tower"
+ if dungeon == "Ganons Tower":
+ dungeon = "Inverted Ganons Tower"
+ if drop_item in world.dungeons[dungeon].small_keys:
+ world.dungeons[dungeon].small_keys.remove(drop_item)
+ elif world.dungeons[dungeon].big_key is not None and world.dungeons[dungeon].big_key == drop_item:
+ world.dungeons[dungeon].big_key = None
+ if not multiworld.key_drop_shuffle[player]:
+ # key drop item was removed from the pool because key drop shuffle is off
+ # and it will now place the removed key into its original location
+ loc = multiworld.get_location(key_loc, player)
+ loc.place_locked_item(drop_item)
+ loc.address = None
+ elif multiworld.goal[player] == 'icerodhunt':
+ # key drop item removed because of icerodhunt
+ multiworld.itempool.append(ItemFactory(GetBeemizerItem(world, player, 'Nothing'), player))
+ multiworld.push_precollected(drop_item)
+ elif "Small" in key_data[3] and multiworld.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
+ # key drop shuffle and universal keys are on. Add universal keys in place of key drop keys.
+ multiworld.itempool.append(ItemFactory(GetBeemizerItem(world, player, 'Small Key (Universal)'), player))
+ dungeon_item_replacements = sum(difficulties[multiworld.difficulty[player]].extras, []) * 2
multiworld.random.shuffle(dungeon_item_replacements)
if multiworld.goal[player] == 'icerodhunt':
for item in dungeon_items:
@@ -391,7 +418,7 @@ def generate_itempool(world):
or (multiworld.bigkey_shuffle[player] == bigkey_shuffle.option_start_with and item.type == 'BigKey')
or (multiworld.compass_shuffle[player] == compass_shuffle.option_start_with and item.type == 'Compass')
or (multiworld.map_shuffle[player] == map_shuffle.option_start_with and item.type == 'Map')):
- dungeon_items.remove(item)
+ dungeon_items.pop(x)
multiworld.push_precollected(item)
multiworld.itempool.append(ItemFactory(dungeon_item_replacements.pop(), player))
multiworld.itempool.extend([item for item in dungeon_items])
@@ -639,14 +666,27 @@ def place_item(loc, item):
pool = ['Rupees (5)' if item in replace else item for item in pool]
if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
pool.extend(diff.universal_keys)
- item_to_place = 'Small Key (Universal)' if goal != 'icerodhunt' else 'Nothing'
if mode == 'standard':
- key_location = world.random.choice(
- ['Secret Passage', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest',
- 'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross'])
- place_item(key_location, item_to_place)
- else:
- pool.extend([item_to_place])
+ if world.key_drop_shuffle[player] and world.goal[player] != 'icerodhunt':
+ key_locations = ['Secret Passage', 'Hyrule Castle - Map Guard Key Drop']
+ key_location = world.random.choice(key_locations)
+ key_locations.remove(key_location)
+ place_item(key_location, "Small Key (Universal)")
+ key_locations += ['Hyrule Castle - Boomerang Guard Key Drop', 'Hyrule Castle - Boomerang Chest',
+ 'Hyrule Castle - Map Chest']
+ key_location = world.random.choice(key_locations)
+ key_locations.remove(key_location)
+ place_item(key_location, "Small Key (Universal)")
+ key_locations += ['Hyrule Castle - Big Key Drop', 'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross']
+ key_location = world.random.choice(key_locations)
+ key_locations.remove(key_location)
+ place_item(key_location, "Small Key (Universal)")
+ key_locations += ['Sewers - Key Rat Key Drop']
+ key_location = world.random.choice(key_locations)
+ place_item(key_location, "Small Key (Universal)")
+ pool = pool[:-3]
+ if world.key_drop_shuffle[player]:
+ pass # pool.extend([item_to_place] * (len(key_drop_data) - 1))
return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon,
additional_pieces_to_place)
@@ -799,7 +839,9 @@ def place_item(loc, item):
pool.extend(['Moon Pearl'] * customitemarray[28])
if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
- itemtotal = itemtotal - 28 # Corrects for small keys not being in item pool in universal mode
+ itemtotal = itemtotal - 28 # Corrects for small keys not being in item pool in universal Mode
+ if world.key_drop_shuffle[player]:
+ itemtotal = itemtotal - (len(key_drop_data) - 1)
if itemtotal < total_items_to_place:
pool.extend(['Nothing'] * (total_items_to_place - itemtotal))
logging.warning(f"Pool was filled up with {total_items_to_place - itemtotal} Nothing's for player {player}")
diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py
index b4b0958ac23f..0f35be7459a3 100644
--- a/worlds/alttp/Options.py
+++ b/worlds/alttp/Options.py
@@ -101,6 +101,11 @@ class map_shuffle(DungeonItem):
display_name = "Map Shuffle"
+class key_drop_shuffle(Toggle):
+ """Shuffle keys found in pots and dropped from killed enemies."""
+ display_name = "Key Drop Shuffle"
+ default = False
+
class Crystals(Range):
range_start = 0
range_end = 7
@@ -432,6 +437,7 @@ class AllowCollect(Toggle):
"open_pyramid": OpenPyramid,
"bigkey_shuffle": bigkey_shuffle,
"smallkey_shuffle": smallkey_shuffle,
+ "key_drop_shuffle": key_drop_shuffle,
"compass_shuffle": compass_shuffle,
"map_shuffle": map_shuffle,
"progressive": Progressive,
diff --git a/worlds/alttp/Regions.py b/worlds/alttp/Regions.py
index 9badbd877422..8311bc32694e 100644
--- a/worlds/alttp/Regions.py
+++ b/worlds/alttp/Regions.py
@@ -14,42 +14,26 @@ def create_regions(world, player):
world.regions += [
create_lw_region(world, player, 'Menu', None, ['Links House S&Q', 'Sanctuary S&Q', 'Old Man S&Q']),
create_lw_region(world, player, 'Light World', ['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure',
- 'Purple Chest', 'Flute Activation Spot'],
- ["Blinds Hideout", "Hyrule Castle Secret Entrance Drop", 'Zoras River',
- 'Kings Grave Outer Rocks', 'Dam',
- 'Links House', 'Tavern North', 'Chicken House', 'Aginahs Cave', 'Sahasrahlas Hut',
- 'Kakariko Well Drop', 'Kakariko Well Cave',
- 'Blacksmiths Hut', 'Bat Cave Drop Ledge', 'Bat Cave Cave', 'Sick Kids House', 'Hobo Bridge',
- 'Lost Woods Hideout Drop', 'Lost Woods Hideout Stump',
- 'Lumberjack Tree Tree', 'Lumberjack Tree Cave', 'Mini Moldorm Cave', 'Ice Rod Cave',
- 'Lake Hylia Central Island Pier',
- 'Bonk Rock Cave', 'Library', 'Potion Shop', 'Two Brothers House (East)',
- 'Desert Palace Stairs', 'Eastern Palace', 'Master Sword Meadow',
- 'Sanctuary', 'Sanctuary Grave', 'Death Mountain Entrance Rock', 'Flute Spot 1',
- 'Dark Desert Teleporter', 'East Hyrule Teleporter', 'South Hyrule Teleporter',
- 'Kakariko Teleporter',
- 'Elder House (East)', 'Elder House (West)', 'North Fairy Cave', 'North Fairy Cave Drop',
- 'Lost Woods Gamble', 'Snitch Lady (East)', 'Snitch Lady (West)', 'Tavern (Front)',
- 'Bush Covered House', 'Light World Bomb Hut', 'Kakariko Shop', 'Long Fairy Cave',
- 'Good Bee Cave', '20 Rupee Cave', 'Cave Shop (Lake Hylia)', 'Waterfall of Wishing',
- 'Hyrule Castle Main Gate',
- 'Bonk Fairy (Light)', '50 Rupee Cave', 'Fortune Teller (Light)', 'Lake Hylia Fairy',
- 'Light Hype Fairy', 'Desert Fairy', 'Lumberjack House', 'Lake Hylia Fortune Teller',
- 'Kakariko Gamble Game', 'Top of Pyramid']),
- create_lw_region(world, player, 'Death Mountain Entrance', None,
- ['Old Man Cave (West)', 'Death Mountain Entrance Drop']),
- create_lw_region(world, player, 'Lake Hylia Central Island', None,
- ['Capacity Upgrade', 'Lake Hylia Central Island Teleporter']),
+ 'Purple Chest', 'Flute Activation Spot'],
+ ["Blinds Hideout", "Hyrule Castle Secret Entrance Drop", 'Zoras River', 'Kings Grave Outer Rocks', 'Dam',
+ 'Links House', 'Tavern North', 'Chicken House', 'Aginahs Cave', 'Sahasrahlas Hut', 'Kakariko Well Drop', 'Kakariko Well Cave',
+ 'Blacksmiths Hut', 'Bat Cave Drop Ledge', 'Bat Cave Cave', 'Sick Kids House', 'Hobo Bridge', 'Lost Woods Hideout Drop', 'Lost Woods Hideout Stump',
+ 'Lumberjack Tree Tree', 'Lumberjack Tree Cave', 'Mini Moldorm Cave', 'Ice Rod Cave', 'Lake Hylia Central Island Pier',
+ 'Bonk Rock Cave', 'Library', 'Potion Shop', 'Two Brothers House (East)', 'Desert Palace Stairs', 'Eastern Palace', 'Master Sword Meadow',
+ 'Sanctuary', 'Sanctuary Grave', 'Death Mountain Entrance Rock', 'Flute Spot 1', 'Dark Desert Teleporter', 'East Hyrule Teleporter', 'South Hyrule Teleporter', 'Kakariko Teleporter',
+ 'Elder House (East)', 'Elder House (West)', 'North Fairy Cave', 'North Fairy Cave Drop', 'Lost Woods Gamble', 'Snitch Lady (East)', 'Snitch Lady (West)', 'Tavern (Front)',
+ 'Bush Covered House', 'Light World Bomb Hut', 'Kakariko Shop', 'Long Fairy Cave', 'Good Bee Cave', '20 Rupee Cave', 'Cave Shop (Lake Hylia)', 'Waterfall of Wishing', 'Hyrule Castle Main Gate',
+ 'Bonk Fairy (Light)', '50 Rupee Cave', 'Fortune Teller (Light)', 'Lake Hylia Fairy', 'Light Hype Fairy', 'Desert Fairy', 'Lumberjack House', 'Lake Hylia Fortune Teller', 'Kakariko Gamble Game', 'Top of Pyramid']),
+ create_lw_region(world, player, 'Death Mountain Entrance', None, ['Old Man Cave (West)', 'Death Mountain Entrance Drop']),
+ create_lw_region(world, player, 'Lake Hylia Central Island', None, ['Capacity Upgrade', 'Lake Hylia Central Island Teleporter']),
create_cave_region(world, player, 'Blinds Hideout', 'a bounty of five items', ["Blind\'s Hideout - Top",
- "Blind\'s Hideout - Left",
- "Blind\'s Hideout - Right",
- "Blind\'s Hideout - Far Left",
- "Blind\'s Hideout - Far Right"]),
- create_cave_region(world, player, 'Hyrule Castle Secret Entrance', 'a drop\'s exit',
- ['Link\'s Uncle', 'Secret Passage'], ['Hyrule Castle Secret Entrance Exit']),
+ "Blind\'s Hideout - Left",
+ "Blind\'s Hideout - Right",
+ "Blind\'s Hideout - Far Left",
+ "Blind\'s Hideout - Far Right"]),
+ create_cave_region(world, player, 'Hyrule Castle Secret Entrance', 'a drop\'s exit', ['Link\'s Uncle', 'Secret Passage'], ['Hyrule Castle Secret Entrance Exit']),
create_lw_region(world, player, 'Zoras River', ['King Zora', 'Zora\'s Ledge']),
- create_cave_region(world, player, 'Waterfall of Wishing', 'a cave with two chests',
- ['Waterfall Fairy - Left', 'Waterfall Fairy - Right']),
+ create_cave_region(world, player, 'Waterfall of Wishing', 'a cave with two chests', ['Waterfall Fairy - Left', 'Waterfall Fairy - Right']),
create_lw_region(world, player, 'Kings Grave Area', None, ['Kings Grave', 'Kings Grave Inner Rocks']),
create_cave_region(world, player, 'Kings Grave', 'a cave with a chest', ['King\'s Tomb']),
create_cave_region(world, player, 'North Fairy Cave', 'a drop\'s exit', None, ['North Fairy Cave Exit']),
@@ -57,8 +41,7 @@ def create_regions(world, player):
create_cave_region(world, player, 'Links House', 'your house', ['Link\'s House'], ['Links House Exit']),
create_cave_region(world, player, 'Chris Houlihan Room', 'I AM ERROR', None, ['Chris Houlihan Room Exit']),
create_cave_region(world, player, 'Tavern', 'the tavern', ['Kakariko Tavern']),
- create_cave_region(world, player, 'Elder House', 'a connector', None,
- ['Elder House Exit (East)', 'Elder House Exit (West)']),
+ create_cave_region(world, player, 'Elder House', 'a connector', None, ['Elder House Exit (East)', 'Elder House Exit (West)']),
create_cave_region(world, player, 'Snitch Lady (East)', 'a boring house'),
create_cave_region(world, player, 'Snitch Lady (West)', 'a boring house'),
create_cave_region(world, player, 'Bush Covered House', 'the grass man'),
@@ -79,12 +62,9 @@ def create_regions(world, player):
create_cave_region(world, player, 'Dark Death Mountain Healer Fairy', 'a fairy fountain'),
create_cave_region(world, player, 'Chicken House', 'a house with a chest', ['Chicken House']),
create_cave_region(world, player, 'Aginahs Cave', 'a cave with a chest', ['Aginah\'s Cave']),
- create_cave_region(world, player, 'Sahasrahlas Hut', 'Sahasrahla',
- ['Sahasrahla\'s Hut - Left', 'Sahasrahla\'s Hut - Middle', 'Sahasrahla\'s Hut - Right',
- 'Sahasrahla']),
- create_cave_region(world, player, 'Kakariko Well (top)', 'a drop\'s exit',
- ['Kakariko Well - Top', 'Kakariko Well - Left', 'Kakariko Well - Middle',
- 'Kakariko Well - Right', 'Kakariko Well - Bottom'], ['Kakariko Well (top to bottom)']),
+ create_cave_region(world, player, 'Sahasrahlas Hut', 'Sahasrahla', ['Sahasrahla\'s Hut - Left', 'Sahasrahla\'s Hut - Middle', 'Sahasrahla\'s Hut - Right', 'Sahasrahla']),
+ create_cave_region(world, player, 'Kakariko Well (top)', 'a drop\'s exit', ['Kakariko Well - Top', 'Kakariko Well - Left', 'Kakariko Well - Middle',
+ 'Kakariko Well - Right', 'Kakariko Well - Bottom'], ['Kakariko Well (top to bottom)']),
create_cave_region(world, player, 'Kakariko Well (bottom)', 'a drop\'s exit', None, ['Kakariko Well Exit']),
create_cave_region(world, player, 'Blacksmiths Hut', 'the smith', ['Blacksmith', 'Missing Smith']),
create_lw_region(world, player, 'Bat Cave Drop Ledge', None, ['Bat Cave Drop']),
@@ -92,12 +72,9 @@ def create_regions(world, player):
create_cave_region(world, player, 'Bat Cave (left)', 'a drop\'s exit', None, ['Bat Cave Exit']),
create_cave_region(world, player, 'Sick Kids House', 'the sick kid', ['Sick Kid']),
create_lw_region(world, player, 'Hobo Bridge', ['Hobo']),
- create_cave_region(world, player, 'Lost Woods Hideout (top)', 'a drop\'s exit', ['Lost Woods Hideout'],
- ['Lost Woods Hideout (top to bottom)']),
- create_cave_region(world, player, 'Lost Woods Hideout (bottom)', 'a drop\'s exit', None,
- ['Lost Woods Hideout Exit']),
- create_cave_region(world, player, 'Lumberjack Tree (top)', 'a drop\'s exit', ['Lumberjack Tree'],
- ['Lumberjack Tree (top to bottom)']),
+ create_cave_region(world, player, 'Lost Woods Hideout (top)', 'a drop\'s exit', ['Lost Woods Hideout'], ['Lost Woods Hideout (top to bottom)']),
+ create_cave_region(world, player, 'Lost Woods Hideout (bottom)', 'a drop\'s exit', None, ['Lost Woods Hideout Exit']),
+ create_cave_region(world, player, 'Lumberjack Tree (top)', 'a drop\'s exit', ['Lumberjack Tree'], ['Lumberjack Tree (top to bottom)']),
create_cave_region(world, player, 'Lumberjack Tree (bottom)', 'a drop\'s exit', None, ['Lumberjack Tree Exit']),
create_lw_region(world, player, 'Cave 45 Ledge', None, ['Cave 45']),
create_cave_region(world, player, 'Cave 45', 'a cave with an item', ['Cave 45']),
@@ -105,9 +82,8 @@ def create_regions(world, player):
create_cave_region(world, player, 'Graveyard Cave', 'a cave with an item', ['Graveyard Cave']),
create_cave_region(world, player, 'Checkerboard Cave', 'a cave with an item', ['Checkerboard Cave']),
create_cave_region(world, player, 'Long Fairy Cave', 'a fairy fountain'),
- create_cave_region(world, player, 'Mini Moldorm Cave', 'a bounty of five items',
- ['Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right',
- 'Mini Moldorm Cave - Far Right', 'Mini Moldorm Cave - Generous Guy']),
+ create_cave_region(world, player, 'Mini Moldorm Cave', 'a bounty of five items', ['Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right',
+ 'Mini Moldorm Cave - Far Right', 'Mini Moldorm Cave - Generous Guy']),
create_cave_region(world, player, 'Ice Rod Cave', 'a cave with a chest', ['Ice Rod Cave']),
create_cave_region(world, player, 'Good Bee Cave', 'a cold bee'),
create_cave_region(world, player, '20 Rupee Cave', 'a cave with some cash'),
@@ -119,91 +95,56 @@ def create_regions(world, player):
create_cave_region(world, player, 'Potion Shop', 'the potion shop', ['Potion Shop']),
create_lw_region(world, player, 'Lake Hylia Island', ['Lake Hylia Island']),
create_cave_region(world, player, 'Capacity Upgrade', 'the queen of fairies'),
- create_cave_region(world, player, 'Two Brothers House', 'a connector', None,
- ['Two Brothers House Exit (East)', 'Two Brothers House Exit (West)']),
+ create_cave_region(world, player, 'Two Brothers House', 'a connector', None, ['Two Brothers House Exit (East)', 'Two Brothers House Exit (West)']),
create_lw_region(world, player, 'Maze Race Ledge', ['Maze Race'], ['Two Brothers House (West)']),
create_cave_region(world, player, '50 Rupee Cave', 'a cave with some cash'),
- create_lw_region(world, player, 'Desert Ledge', ['Desert Ledge'],
- ['Desert Palace Entrance (North) Rocks', 'Desert Palace Entrance (West)']),
+ create_lw_region(world, player, 'Desert Ledge', ['Desert Ledge'], ['Desert Palace Entrance (North) Rocks', 'Desert Palace Entrance (West)']),
create_lw_region(world, player, 'Desert Ledge (Northeast)', None, ['Checkerboard Cave']),
create_lw_region(world, player, 'Desert Palace Stairs', None, ['Desert Palace Entrance (South)']),
- create_lw_region(world, player, 'Desert Palace Lone Stairs', None,
- ['Desert Palace Stairs Drop', 'Desert Palace Entrance (East)']),
- create_lw_region(world, player, 'Desert Palace Entrance (North) Spot', None,
- ['Desert Palace Entrance (North)', 'Desert Ledge Return Rocks']),
- create_dungeon_region(world, player, 'Desert Palace Main (Outer)', 'Desert Palace',
- ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'],
- ['Desert Palace Pots (Outer)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)',
- 'Desert Palace East Wing']),
- create_dungeon_region(world, player, 'Desert Palace Main (Inner)', 'Desert Palace', None,
- ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']),
- create_dungeon_region(world, player, 'Desert Palace East', 'Desert Palace',
- ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']),
- create_dungeon_region(world, player, 'Desert Palace North', 'Desert Palace',
- ['Desert Palace - Boss', 'Desert Palace - Prize'], ['Desert Palace Exit (North)']),
- create_dungeon_region(world, player, 'Eastern Palace', 'Eastern Palace',
- ['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest',
- 'Eastern Palace - Cannonball Chest',
- 'Eastern Palace - Big Key Chest', 'Eastern Palace - Map Chest', 'Eastern Palace - Boss',
- 'Eastern Palace - Prize'], ['Eastern Palace Exit']),
+ create_lw_region(world, player, 'Desert Palace Lone Stairs', None, ['Desert Palace Stairs Drop', 'Desert Palace Entrance (East)']),
+ create_lw_region(world, player, 'Desert Palace Entrance (North) Spot', None, ['Desert Palace Entrance (North)', 'Desert Ledge Return Rocks']),
+ create_dungeon_region(world, player, 'Desert Palace Main (Outer)', 'Desert Palace', ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'],
+ ['Desert Palace Pots (Outer)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)', 'Desert Palace East Wing']),
+ create_dungeon_region(world, player, 'Desert Palace Main (Inner)', 'Desert Palace', None, ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']),
+ create_dungeon_region(world, player, 'Desert Palace East', 'Desert Palace', ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']),
+ create_dungeon_region(world, player, 'Desert Palace North', 'Desert Palace', ['Desert Palace - Desert Tiles 1 Pot Key', 'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key',
+ 'Desert Palace - Boss', 'Desert Palace - Prize'], ['Desert Palace Exit (North)']),
+ create_dungeon_region(world, player, 'Eastern Palace', 'Eastern Palace', ['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest', 'Eastern Palace - Cannonball Chest',
+ 'Eastern Palace - Dark Square Pot Key', 'Eastern Palace - Dark Eyegore Key Drop', 'Eastern Palace - Big Key Chest',
+ 'Eastern Palace - Map Chest', 'Eastern Palace - Boss', 'Eastern Palace - Prize'], ['Eastern Palace Exit']),
create_lw_region(world, player, 'Master Sword Meadow', ['Master Sword Pedestal']),
create_cave_region(world, player, 'Lost Woods Gamble', 'a game of chance'),
- create_lw_region(world, player, 'Hyrule Castle Courtyard', None,
- ['Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Entrance (South)']),
- create_lw_region(world, player, 'Hyrule Castle Ledge', None,
- ['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Agahnims Tower',
- 'Hyrule Castle Ledge Courtyard Drop']),
- create_dungeon_region(world, player, 'Hyrule Castle', 'Hyrule Castle',
- ['Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest',
- 'Hyrule Castle - Zelda\'s Chest'],
- ['Hyrule Castle Exit (East)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (South)',
- 'Throne Room']),
+ create_lw_region(world, player, 'Hyrule Castle Courtyard', None, ['Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Entrance (South)']),
+ create_lw_region(world, player, 'Hyrule Castle Ledge', None, ['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Agahnims Tower', 'Hyrule Castle Ledge Courtyard Drop']),
+ create_dungeon_region(world, player, 'Hyrule Castle', 'Hyrule Castle', ['Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', 'Hyrule Castle - Zelda\'s Chest',
+ 'Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop', 'Hyrule Castle - Big Key Drop'],
+ ['Hyrule Castle Exit (East)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (South)', 'Throne Room']),
create_dungeon_region(world, player, 'Sewer Drop', 'a drop\'s exit', None, ['Sewer Drop']), # This exists only to be referenced for access checks
- create_dungeon_region(world, player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross'],
- ['Sewers Door']),
- create_dungeon_region(world, player, 'Sewers', 'a drop\'s exit',
- ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle',
- 'Sewers - Secret Room - Right'], ['Sanctuary Push Door', 'Sewers Back Door']),
+ create_dungeon_region(world, player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross', 'Sewers - Key Rat Key Drop'], ['Sewers Door']),
+ create_dungeon_region(world, player, 'Sewers', 'a drop\'s exit', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle',
+ 'Sewers - Secret Room - Right'], ['Sanctuary Push Door', 'Sewers Back Door']),
create_dungeon_region(world, player, 'Sanctuary', 'a drop\'s exit', ['Sanctuary'], ['Sanctuary Exit']),
- create_dungeon_region(world, player, 'Agahnims Tower', 'Castle Tower',
- ['Castle Tower - Room 03', 'Castle Tower - Dark Maze'],
- ['Agahnim 1', 'Agahnims Tower Exit']),
+ create_dungeon_region(world, player, 'Agahnims Tower', 'Castle Tower', ['Castle Tower - Room 03', 'Castle Tower - Dark Maze', 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'], ['Agahnim 1', 'Agahnims Tower Exit']),
create_dungeon_region(world, player, 'Agahnim 1', 'Castle Tower', ['Agahnim 1'], None),
- create_cave_region(world, player, 'Old Man Cave', 'a connector', ['Old Man'],
- ['Old Man Cave Exit (East)', 'Old Man Cave Exit (West)']),
- create_cave_region(world, player, 'Old Man House', 'a connector', None,
- ['Old Man House Exit (Bottom)', 'Old Man House Front to Back']),
- create_cave_region(world, player, 'Old Man House Back', 'a connector', None,
- ['Old Man House Exit (Top)', 'Old Man House Back to Front']),
- create_lw_region(world, player, 'Death Mountain', None,
- ['Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)',
- 'Death Mountain Return Cave (East)', 'Spectacle Rock Cave', 'Spectacle Rock Cave Peak',
- 'Spectacle Rock Cave (Bottom)', 'Broken Bridge (West)', 'Death Mountain Teleporter']),
- create_cave_region(world, player, 'Death Mountain Return Cave', 'a connector', None,
- ['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)']),
- create_lw_region(world, player, 'Death Mountain Return Ledge', None,
- ['Death Mountain Return Ledge Drop', 'Death Mountain Return Cave (West)']),
- create_cave_region(world, player, 'Spectacle Rock Cave (Top)', 'a connector', ['Spectacle Rock Cave'],
- ['Spectacle Rock Cave Drop', 'Spectacle Rock Cave Exit (Top)']),
- create_cave_region(world, player, 'Spectacle Rock Cave (Bottom)', 'a connector', None,
- ['Spectacle Rock Cave Exit']),
- create_cave_region(world, player, 'Spectacle Rock Cave (Peak)', 'a connector', None,
- ['Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave Exit (Peak)']),
- create_lw_region(world, player, 'East Death Mountain (Bottom)', None,
- ['Broken Bridge (East)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)',
- 'East Death Mountain Teleporter', 'Hookshot Fairy', 'Fairy Ascension Rocks',
- 'Spiral Cave (Bottom)']),
+ create_cave_region(world, player, 'Old Man Cave', 'a connector', ['Old Man'], ['Old Man Cave Exit (East)', 'Old Man Cave Exit (West)']),
+ create_cave_region(world, player, 'Old Man House', 'a connector', None, ['Old Man House Exit (Bottom)', 'Old Man House Front to Back']),
+ create_cave_region(world, player, 'Old Man House Back', 'a connector', None, ['Old Man House Exit (Top)', 'Old Man House Back to Front']),
+ create_lw_region(world, player, 'Death Mountain', None, ['Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)', 'Death Mountain Return Cave (East)', 'Spectacle Rock Cave', 'Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Bottom)', 'Broken Bridge (West)', 'Death Mountain Teleporter']),
+ create_cave_region(world, player, 'Death Mountain Return Cave', 'a connector', None, ['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)']),
+ create_lw_region(world, player, 'Death Mountain Return Ledge', None, ['Death Mountain Return Ledge Drop', 'Death Mountain Return Cave (West)']),
+ create_cave_region(world, player, 'Spectacle Rock Cave (Top)', 'a connector', ['Spectacle Rock Cave'], ['Spectacle Rock Cave Drop', 'Spectacle Rock Cave Exit (Top)']),
+ create_cave_region(world, player, 'Spectacle Rock Cave (Bottom)', 'a connector', None, ['Spectacle Rock Cave Exit']),
+ create_cave_region(world, player, 'Spectacle Rock Cave (Peak)', 'a connector', None, ['Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave Exit (Peak)']),
+ create_lw_region(world, player, 'East Death Mountain (Bottom)', None, ['Broken Bridge (East)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'East Death Mountain Teleporter', 'Hookshot Fairy', 'Fairy Ascension Rocks', 'Spiral Cave (Bottom)']),
create_cave_region(world, player, 'Hookshot Fairy', 'fairies deep in a cave'),
- create_cave_region(world, player, 'Paradox Cave Front', 'a connector', None,
- ['Paradox Cave Push Block Reverse', 'Paradox Cave Exit (Bottom)',
- 'Light World Death Mountain Shop']),
+ create_cave_region(world, player, 'Paradox Cave Front', 'a connector', None, ['Paradox Cave Push Block Reverse', 'Paradox Cave Exit (Bottom)', 'Light World Death Mountain Shop']),
create_cave_region(world, player, 'Paradox Cave Chest Area', 'a connector', ['Paradox Cave Lower - Far Left',
- 'Paradox Cave Lower - Left',
- 'Paradox Cave Lower - Right',
- 'Paradox Cave Lower - Far Right',
- 'Paradox Cave Lower - Middle',
- 'Paradox Cave Upper - Left',
- 'Paradox Cave Upper - Right'],
+ 'Paradox Cave Lower - Left',
+ 'Paradox Cave Lower - Right',
+ 'Paradox Cave Lower - Far Right',
+ 'Paradox Cave Lower - Middle',
+ 'Paradox Cave Upper - Left',
+ 'Paradox Cave Upper - Right'],
['Paradox Cave Push Block', 'Paradox Cave Bomb Jump']),
create_cave_region(world, player, 'Paradox Cave', 'a connector', None,
['Paradox Cave Exit (Middle)', 'Paradox Cave Exit (Top)', 'Paradox Cave Drop']),
@@ -342,162 +283,98 @@ def create_regions(world, player):
create_lw_region(world, player, 'Mimic Cave Ledge', None, ['Mimic Cave']),
create_cave_region(world, player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']),
- create_dungeon_region(world, player, 'Swamp Palace (Entrance)', 'Swamp Palace', None,
- ['Swamp Palace Moat', 'Swamp Palace Exit']),
- create_dungeon_region(world, player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'],
- ['Swamp Palace Small Key Door']),
- create_dungeon_region(world, player, 'Swamp Palace (Starting Area)', 'Swamp Palace',
- ['Swamp Palace - Map Chest'], ['Swamp Palace (Center)']),
- create_dungeon_region(world, player, 'Swamp Palace (Center)', 'Swamp Palace',
- ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest',
- 'Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest'], ['Swamp Palace (North)']),
- create_dungeon_region(world, player, 'Swamp Palace (North)', 'Swamp Palace',
- ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right',
- 'Swamp Palace - Waterfall Room', 'Swamp Palace - Boss', 'Swamp Palace - Prize']),
- create_dungeon_region(world, player, 'Thieves Town (Entrance)', 'Thieves\' Town',
- ['Thieves\' Town - Big Key Chest',
- 'Thieves\' Town - Map Chest',
- 'Thieves\' Town - Compass Chest',
- 'Thieves\' Town - Ambush Chest'], ['Thieves Town Big Key Door', 'Thieves Town Exit']),
+ create_dungeon_region(world, player, 'Swamp Palace (Entrance)', 'Swamp Palace', None, ['Swamp Palace Moat', 'Swamp Palace Exit']),
+ create_dungeon_region(world, player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'], ['Swamp Palace Small Key Door']),
+ create_dungeon_region(world, player, 'Swamp Palace (Starting Area)', 'Swamp Palace', ['Swamp Palace - Map Chest', 'Swamp Palace - Pot Row Pot Key',
+ 'Swamp Palace - Trench 1 Pot Key'], ['Swamp Palace (Center)']),
+ create_dungeon_region(world, player, 'Swamp Palace (Center)', 'Swamp Palace', ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest', 'Swamp Palace - Hookshot Pot Key',
+ 'Swamp Palace - Trench 2 Pot Key'], ['Swamp Palace (North)', 'Swamp Palace (West)']),
+ create_dungeon_region(world, player, 'Swamp Palace (West)', 'Swamp Palace', ['Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest']),
+ create_dungeon_region(world, player, 'Swamp Palace (North)', 'Swamp Palace', ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right',
+ 'Swamp Palace - Waterway Pot Key', 'Swamp Palace - Waterfall Room',
+ 'Swamp Palace - Boss', 'Swamp Palace - Prize']),
+ create_dungeon_region(world, player, 'Thieves Town (Entrance)', 'Thieves\' Town', ['Thieves\' Town - Big Key Chest',
+ 'Thieves\' Town - Map Chest',
+ 'Thieves\' Town - Compass Chest',
+ 'Thieves\' Town - Ambush Chest'], ['Thieves Town Big Key Door', 'Thieves Town Exit']),
create_dungeon_region(world, player, 'Thieves Town (Deep)', 'Thieves\' Town', ['Thieves\' Town - Attic',
- 'Thieves\' Town - Big Chest',
- 'Thieves\' Town - Blind\'s Cell'],
- ['Blind Fight']),
- create_dungeon_region(world, player, 'Blind Fight', 'Thieves\' Town',
- ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']),
- create_dungeon_region(world, player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'],
- ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump',
- 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']),
- create_dungeon_region(world, player, 'Skull Woods First Section (Right)', 'Skull Woods',
- ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']),
- create_dungeon_region(world, player, 'Skull Woods First Section (Left)', 'Skull Woods',
- ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'],
- ['Skull Woods First Section (Left) Door to Exit',
- 'Skull Woods First Section (Left) Door to Right']),
- create_dungeon_region(world, player, 'Skull Woods First Section (Top)', 'Skull Woods',
- ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']),
- create_dungeon_region(world, player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None,
- ['Skull Woods Second Section (Drop)']),
- create_dungeon_region(world, player, 'Skull Woods Second Section', 'Skull Woods',
- ['Skull Woods - Big Key Chest'],
- ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']),
- create_dungeon_region(world, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods',
- ['Skull Woods - Bridge Room'],
- ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']),
- create_dungeon_region(world, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods',
- ['Skull Woods - Boss', 'Skull Woods - Prize']),
- create_dungeon_region(world, player, 'Ice Palace (Entrance)', 'Ice Palace', None,
- ['Ice Palace Entrance Room', 'Ice Palace Exit']),
- create_dungeon_region(world, player, 'Ice Palace (Main)', 'Ice Palace',
- ['Ice Palace - Compass Chest', 'Ice Palace - Freezor Chest',
- 'Ice Palace - Big Chest', 'Ice Palace - Iced T Room'],
- ['Ice Palace (East)', 'Ice Palace (Kholdstare)']),
- create_dungeon_region(world, player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'],
- ['Ice Palace (East Top)']),
- create_dungeon_region(world, player, 'Ice Palace (East Top)', 'Ice Palace',
- ['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest']),
- create_dungeon_region(world, player, 'Ice Palace (Kholdstare)', 'Ice Palace',
- ['Ice Palace - Boss', 'Ice Palace - Prize']),
- create_dungeon_region(world, player, 'Misery Mire (Entrance)', 'Misery Mire', None,
- ['Misery Mire Entrance Gap', 'Misery Mire Exit']),
- create_dungeon_region(world, player, 'Misery Mire (Main)', 'Misery Mire',
- ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby',
- 'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest'],
- ['Misery Mire (West)', 'Misery Mire Big Key Door']),
- create_dungeon_region(world, player, 'Misery Mire (West)', 'Misery Mire',
- ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']),
- create_dungeon_region(world, player, 'Misery Mire (Final Area)', 'Misery Mire', None,
- ['Misery Mire (Vitreous)']),
- create_dungeon_region(world, player, 'Misery Mire (Vitreous)', 'Misery Mire',
- ['Misery Mire - Boss', 'Misery Mire - Prize']),
- create_dungeon_region(world, player, 'Turtle Rock (Entrance)', 'Turtle Rock', None,
- ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']),
- create_dungeon_region(world, player, 'Turtle Rock (First Section)', 'Turtle Rock',
- ['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left',
- 'Turtle Rock - Roller Room - Right'],
- ['Turtle Rock Pokey Room', 'Turtle Rock Entrance Gap Reverse']),
- create_dungeon_region(world, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock',
- ['Turtle Rock - Chain Chomps'],
- ['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']),
- create_dungeon_region(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock',
- ['Turtle Rock - Big Key Chest'],
- ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Chain Chomp Staircase',
- 'Turtle Rock Big Key Door']),
- create_dungeon_region(world, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'],
- ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']),
- create_dungeon_region(world, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock',
- ['Turtle Rock - Crystaroller Room'],
- ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']),
- create_dungeon_region(world, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None,
- ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']),
- create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock',
- ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right',
- 'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'],
- ['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)',
- 'Turtle Rock Isolated Ledge Exit']),
- create_dungeon_region(world, player, 'Turtle Rock (Trinexx)', 'Turtle Rock',
- ['Turtle Rock - Boss', 'Turtle Rock - Prize']),
- create_dungeon_region(world, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness',
- ['Palace of Darkness - Shooter Room'],
- ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall',
- 'Palace of Darkness Exit']),
- create_dungeon_region(world, player, 'Palace of Darkness (Center)', 'Palace of Darkness',
- ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'],
- ['Palace of Darkness Big Key Chest Staircase', 'Palace of Darkness (North)',
- 'Palace of Darkness Big Key Door']),
- create_dungeon_region(world, player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness',
- ['Palace of Darkness - Big Key Chest']),
- create_dungeon_region(world, player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness',
- ['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'],
- ['Palace of Darkness Hammer Peg Drop']),
- create_dungeon_region(world, player, 'Palace of Darkness (North)', 'Palace of Darkness',
- ['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left',
- 'Palace of Darkness - Dark Basement - Right'],
+ 'Thieves\' Town - Big Chest',
+ 'Thieves\' Town - Hallway Pot Key',
+ 'Thieves\' Town - Spike Switch Pot Key',
+ 'Thieves\' Town - Blind\'s Cell'], ['Blind Fight']),
+ create_dungeon_region(world, player, 'Blind Fight', 'Thieves\' Town', ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']),
+ create_dungeon_region(world, player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'], ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump', 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']),
+ create_dungeon_region(world, player, 'Skull Woods First Section (Right)', 'Skull Woods', ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']),
+ create_dungeon_region(world, player, 'Skull Woods First Section (Left)', 'Skull Woods', ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'], ['Skull Woods First Section (Left) Door to Exit', 'Skull Woods First Section (Left) Door to Right']),
+ create_dungeon_region(world, player, 'Skull Woods First Section (Top)', 'Skull Woods', ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']),
+ create_dungeon_region(world, player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None, ['Skull Woods Second Section (Drop)']),
+ create_dungeon_region(world, player, 'Skull Woods Second Section', 'Skull Woods', ['Skull Woods - Big Key Chest', 'Skull Woods - West Lobby Pot Key'], ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']),
+ create_dungeon_region(world, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', ['Skull Woods - Bridge Room'], ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']),
+ create_dungeon_region(world, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', ['Skull Woods - Spike Corner Key Drop', 'Skull Woods - Boss', 'Skull Woods - Prize']),
+ create_dungeon_region(world, player, 'Ice Palace (Entrance)', 'Ice Palace', ['Ice Palace - Jelly Key Drop'], ['Ice Palace (Second Section)', 'Ice Palace Exit']),
+ create_dungeon_region(world, player, 'Ice Palace (Second Section)', 'Ice Palace', ['Ice Palace - Conveyor Key Drop', 'Ice Palace - Compass Chest'], ['Ice Palace (Main)']),
+ create_dungeon_region(world, player, 'Ice Palace (Main)', 'Ice Palace', ['Ice Palace - Freezor Chest',
+ 'Ice Palace - Many Pots Pot Key',
+ 'Ice Palace - Big Chest', 'Ice Palace - Iced T Room'], ['Ice Palace (East)', 'Ice Palace (Kholdstare)']),
+ create_dungeon_region(world, player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'], ['Ice Palace (East Top)']),
+ create_dungeon_region(world, player, 'Ice Palace (East Top)', 'Ice Palace', ['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest', 'Ice Palace - Hammer Block Key Drop']),
+ create_dungeon_region(world, player, 'Ice Palace (Kholdstare)', 'Ice Palace', ['Ice Palace - Boss', 'Ice Palace - Prize']),
+ create_dungeon_region(world, player, 'Misery Mire (Entrance)', 'Misery Mire', None, ['Misery Mire Entrance Gap', 'Misery Mire Exit']),
+ create_dungeon_region(world, player, 'Misery Mire (Main)', 'Misery Mire', ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby',
+ 'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest',
+ 'Misery Mire - Spikes Pot Key', 'Misery Mire - Fishbone Pot Key',
+ 'Misery Mire - Conveyor Crystal Key Drop'], ['Misery Mire (West)', 'Misery Mire Big Key Door']),
+ create_dungeon_region(world, player, 'Misery Mire (West)', 'Misery Mire', ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']),
+ create_dungeon_region(world, player, 'Misery Mire (Final Area)', 'Misery Mire', None, ['Misery Mire (Vitreous)']),
+ create_dungeon_region(world, player, 'Misery Mire (Vitreous)', 'Misery Mire', ['Misery Mire - Boss', 'Misery Mire - Prize']),
+ create_dungeon_region(world, player, 'Turtle Rock (Entrance)', 'Turtle Rock', None, ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']),
+ create_dungeon_region(world, player, 'Turtle Rock (First Section)', 'Turtle Rock', ['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left',
+ 'Turtle Rock - Roller Room - Right'],
+ ['Turtle Rock Entrance to Pokey Room', 'Turtle Rock Entrance Gap Reverse']),
+ create_dungeon_region(world, player, 'Turtle Rock (Pokey Room)', 'Turtle Rock', ['Turtle Rock - Pokey 1 Key Drop'], ['Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Pokey Room) (South)']),
+ create_dungeon_region(world, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock', ['Turtle Rock - Chain Chomps'], ['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']),
+ create_dungeon_region(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock', ['Turtle Rock - Big Key Chest', 'Turtle Rock - Pokey 2 Key Drop'], ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door']),
+ create_dungeon_region(world, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']),
+ create_dungeon_region(world, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']),
+ create_dungeon_region(world, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']),
+ create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right',
+ 'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'],
+ ['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Isolated Ledge Exit']),
+ create_dungeon_region(world, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']),
+ create_dungeon_region(world, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']),
+ create_dungeon_region(world, player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'],
+ ['Palace of Darkness Big Key Chest Staircase', 'Palace of Darkness (North)', 'Palace of Darkness Big Key Door']),
+ create_dungeon_region(world, player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness', ['Palace of Darkness - Big Key Chest']),
+ create_dungeon_region(world, player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'], ['Palace of Darkness Hammer Peg Drop']),
+ create_dungeon_region(world, player, 'Palace of Darkness (North)', 'Palace of Darkness', ['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Basement - Right'],
['Palace of Darkness Spike Statue Room Door', 'Palace of Darkness Maze Door']),
- create_dungeon_region(world, player, 'Palace of Darkness (Maze)', 'Palace of Darkness',
- ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom',
- 'Palace of Darkness - Big Chest']),
- create_dungeon_region(world, player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness',
- ['Palace of Darkness - Harmless Hellway']),
- create_dungeon_region(world, player, 'Palace of Darkness (Final Section)', 'Palace of Darkness',
- ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']),
- create_dungeon_region(world, player, 'Ganons Tower (Entrance)', 'Ganon\'s Tower',
- ['Ganons Tower - Bob\'s Torch', 'Ganons Tower - Hope Room - Left',
- 'Ganons Tower - Hope Room - Right'],
- ['Ganons Tower (Tile Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower Big Key Door',
- 'Ganons Tower Exit']),
- create_dungeon_region(world, player, 'Ganons Tower (Tile Room)', 'Ganon\'s Tower', ['Ganons Tower - Tile Room'],
- ['Ganons Tower (Tile Room) Key Door']),
- create_dungeon_region(world, player, 'Ganons Tower (Compass Room)', 'Ganon\'s Tower',
- ['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right',
- 'Ganons Tower - Compass Room - Bottom Left',
- 'Ganons Tower - Compass Room - Bottom Right'], ['Ganons Tower (Bottom) (East)']),
- create_dungeon_region(world, player, 'Ganons Tower (Hookshot Room)', 'Ganon\'s Tower',
- ['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right',
- 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right'],
+ create_dungeon_region(world, player, 'Palace of Darkness (Maze)', 'Palace of Darkness', ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Big Chest']),
+ create_dungeon_region(world, player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness', ['Palace of Darkness - Harmless Hellway']),
+ create_dungeon_region(world, player, 'Palace of Darkness (Final Section)', 'Palace of Darkness', ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']),
+ create_dungeon_region(world, player, 'Ganons Tower (Entrance)', 'Ganon\'s Tower', ['Ganons Tower - Bob\'s Torch', 'Ganons Tower - Hope Room - Left',
+ 'Ganons Tower - Hope Room - Right', 'Ganons Tower - Conveyor Cross Pot Key'],
+ ['Ganons Tower (Tile Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower Big Key Door', 'Ganons Tower Exit']),
+ create_dungeon_region(world, player, 'Ganons Tower (Tile Room)', 'Ganon\'s Tower', ['Ganons Tower - Tile Room'], ['Ganons Tower (Tile Room) Key Door']),
+ create_dungeon_region(world, player, 'Ganons Tower (Compass Room)', 'Ganon\'s Tower', ['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right',
+ 'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Compass Room - Bottom Right',
+ 'Ganons Tower - Conveyor Star Pits Pot Key'],
+ ['Ganons Tower (Bottom) (East)']),
+ create_dungeon_region(world, player, 'Ganons Tower (Hookshot Room)', 'Ganon\'s Tower', ['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right',
+ 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right',
+ 'Ganons Tower - Double Switch Pot Key'],
['Ganons Tower (Map Room)', 'Ganons Tower (Double Switch Room)']),
create_dungeon_region(world, player, 'Ganons Tower (Map Room)', 'Ganon\'s Tower', ['Ganons Tower - Map Chest']),
- create_dungeon_region(world, player, 'Ganons Tower (Firesnake Room)', 'Ganon\'s Tower',
- ['Ganons Tower - Firesnake Room'], ['Ganons Tower (Firesnake Room)']),
- create_dungeon_region(world, player, 'Ganons Tower (Teleport Room)', 'Ganon\'s Tower',
- ['Ganons Tower - Randomizer Room - Top Left',
- 'Ganons Tower - Randomizer Room - Top Right',
- 'Ganons Tower - Randomizer Room - Bottom Left',
- 'Ganons Tower - Randomizer Room - Bottom Right'], ['Ganons Tower (Bottom) (West)']),
- create_dungeon_region(world, player, 'Ganons Tower (Bottom)', 'Ganon\'s Tower',
- ['Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest',
- 'Ganons Tower - Big Key Room - Left',
- 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Chest']),
- create_dungeon_region(world, player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None,
- ['Ganons Tower Torch Rooms']),
- create_dungeon_region(world, player, 'Ganons Tower (Before Moldorm)', 'Ganon\'s Tower',
- ['Ganons Tower - Mini Helmasaur Room - Left',
- 'Ganons Tower - Mini Helmasaur Room - Right',
- 'Ganons Tower - Pre-Moldorm Chest'], ['Ganons Tower Moldorm Door']),
- create_dungeon_region(world, player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None,
- ['Ganons Tower Moldorm Gap']),
- create_dungeon_region(world, player, 'Agahnim 2', 'Ganon\'s Tower',
- ['Ganons Tower - Validation Chest', 'Agahnim 2'], None),
+ create_dungeon_region(world, player, 'Ganons Tower (Firesnake Room)', 'Ganon\'s Tower', ['Ganons Tower - Firesnake Room'], ['Ganons Tower (Firesnake Room)']),
+ create_dungeon_region(world, player, 'Ganons Tower (Teleport Room)', 'Ganon\'s Tower', ['Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right',
+ 'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right'],
+ ['Ganons Tower (Bottom) (West)']),
+ create_dungeon_region(world, player, 'Ganons Tower (Bottom)', 'Ganon\'s Tower', ['Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest', 'Ganons Tower - Big Key Room - Left',
+ 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Chest']),
+ create_dungeon_region(world, player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None, ['Ganons Tower Torch Rooms']),
+ create_dungeon_region(world, player, 'Ganons Tower (Before Moldorm)', 'Ganon\'s Tower', ['Ganons Tower - Mini Helmasaur Room - Left', 'Ganons Tower - Mini Helmasaur Room - Right',
+ 'Ganons Tower - Pre-Moldorm Chest', 'Ganons Tower - Mini Helmasaur Key Drop'], ['Ganons Tower Moldorm Door']),
+ create_dungeon_region(world, player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None, ['Ganons Tower Moldorm Gap']),
+ create_dungeon_region(world, player, 'Agahnim 2', 'Ganon\'s Tower', ['Ganons Tower - Validation Chest', 'Agahnim 2'], None),
create_cave_region(world, player, 'Pyramid', 'a drop\'s exit', ['Ganon'], ['Ganon Drop']),
create_cave_region(world, player, 'Bottom of Pyramid', 'a drop\'s exit', None, ['Pyramid Exit']),
create_dw_region(world, player, 'Pyramid Ledge', None, ['Pyramid Entrance', 'Pyramid Drop']),
@@ -533,8 +410,12 @@ def _create_region(world: MultiWorld, player: int, name: str, type: LTTPRegionTy
ret.exits.append(Entrance(player, exit, ret))
if locations:
for location in locations:
- address, player_address, crystal, hint_text = location_table[location]
- ret.locations.append(ALttPLocation(player, location, address, crystal, hint_text, ret, player_address))
+ if location in key_drop_data:
+ ko_hint = key_drop_data[location][2]
+ ret.locations.append(ALttPLocation(player, location, key_drop_data[location][1], False, ko_hint, ret, key_drop_data[location][0]))
+ else:
+ address, player_address, crystal, hint_text = location_table[location]
+ ret.locations.append(ALttPLocation(player, location, address, crystal, hint_text, ret, player_address))
return ret
@@ -587,39 +468,39 @@ def mark_light_world_regions(world, player: int):
key_drop_data = {
- 'Hyrule Castle - Map Guard Key Drop': [0x140036, 0x140037],
- 'Hyrule Castle - Boomerang Guard Key Drop': [0x140033, 0x140034],
- 'Hyrule Castle - Key Rat Key Drop': [0x14000c, 0x14000d],
- 'Hyrule Castle - Big Key Drop': [0x14003c, 0x14003d],
- 'Eastern Palace - Dark Square Pot Key': [0x14005a, 0x14005b],
- 'Eastern Palace - Dark Eyegore Key Drop': [0x140048, 0x140049],
- 'Desert Palace - Desert Tiles 1 Pot Key': [0x140030, 0x140031],
- 'Desert Palace - Beamos Hall Pot Key': [0x14002a, 0x14002b],
- 'Desert Palace - Desert Tiles 2 Pot Key': [0x140027, 0x140028],
- 'Castle Tower - Dark Archer Key Drop': [0x140060, 0x140061],
- 'Castle Tower - Circle of Pots Key Drop': [0x140051, 0x140052],
- 'Swamp Palace - Pot Row Pot Key': [0x140018, 0x140019],
- 'Swamp Palace - Trench 1 Pot Key': [0x140015, 0x140016],
- 'Swamp Palace - Hookshot Pot Key': [0x140012, 0x140013],
- 'Swamp Palace - Trench 2 Pot Key': [0x14000f, 0x140010],
- 'Swamp Palace - Waterway Pot Key': [0x140009, 0x14000a],
- 'Skull Woods - West Lobby Pot Key': [0x14002d, 0x14002e],
- 'Skull Woods - Spike Corner Key Drop': [0x14001b, 0x14001c],
- 'Thieves\' Town - Hallway Pot Key': [0x14005d, 0x14005e],
- 'Thieves\' Town - Spike Switch Pot Key': [0x14004e, 0x14004f],
- 'Ice Palace - Jelly Key Drop': [0x140003, 0x140004],
- 'Ice Palace - Conveyor Key Drop': [0x140021, 0x140022],
- 'Ice Palace - Hammer Block Key Drop': [0x140024, 0x140025],
- 'Ice Palace - Many Pots Pot Key': [0x140045, 0x140046],
- 'Misery Mire - Spikes Pot Key': [0x140054, 0x140055],
- 'Misery Mire - Fishbone Pot Key': [0x14004b, 0x14004c],
- 'Misery Mire - Conveyor Crystal Key Drop': [0x140063, 0x140064],
- 'Turtle Rock - Pokey 1 Key Drop': [0x140057, 0x140058],
- 'Turtle Rock - Pokey 2 Key Drop': [0x140006, 0x140007],
- 'Ganons Tower - Conveyor Cross Pot Key': [0x14003f, 0x140040],
- 'Ganons Tower - Double Switch Pot Key': [0x140042, 0x140043],
- 'Ganons Tower - Conveyor Star Pits Pot Key': [0x140039, 0x14003a],
- 'Ganons Tower - Mini Helmasaur Key Drop': [0x14001e, 0x14001f]
+ 'Hyrule Castle - Map Guard Key Drop': [0x140036, 0x140037, 'in Hyrule Castle', 'Small Key (Hyrule Castle)'],
+ 'Hyrule Castle - Boomerang Guard Key Drop': [0x140033, 0x140034, 'in Hyrule Castle', 'Small Key (Hyrule Castle)'],
+ 'Sewers - Key Rat Key Drop': [0x14000c, 0x14000d, 'in the sewers', 'Small Key (Hyrule Castle)'],
+ 'Hyrule Castle - Big Key Drop': [0x14003c, 0x14003d, 'in Hyrule Castle', 'Big Key (Hyrule Castle)'],
+ 'Eastern Palace - Dark Square Pot Key': [0x14005a, 0x14005b, 'in Eastern Palace', 'Small Key (Eastern Palace)'],
+ 'Eastern Palace - Dark Eyegore Key Drop': [0x140048, 0x140049, 'in Eastern Palace', 'Small Key (Eastern Palace)'],
+ 'Desert Palace - Desert Tiles 1 Pot Key': [0x140030, 0x140031, 'in Desert Palace', 'Small Key (Desert Palace)'],
+ 'Desert Palace - Beamos Hall Pot Key': [0x14002a, 0x14002b, 'in Desert Palace', 'Small Key (Desert Palace)'],
+ 'Desert Palace - Desert Tiles 2 Pot Key': [0x140027, 0x140028, 'in Desert Palace', 'Small Key (Desert Palace)'],
+ 'Castle Tower - Dark Archer Key Drop': [0x140060, 0x140061, 'in Castle Tower', 'Small Key (Agahnims Tower)'],
+ 'Castle Tower - Circle of Pots Key Drop': [0x140051, 0x140052, 'in Castle Tower', 'Small Key (Agahnims Tower)'],
+ 'Swamp Palace - Pot Row Pot Key': [0x140018, 0x140019, 'in Swamp Palace', 'Small Key (Swamp Palace)'],
+ 'Swamp Palace - Trench 1 Pot Key': [0x140015, 0x140016, 'in Swamp Palace', 'Small Key (Swamp Palace)'],
+ 'Swamp Palace - Hookshot Pot Key': [0x140012, 0x140013, 'in Swamp Palace', 'Small Key (Swamp Palace)'],
+ 'Swamp Palace - Trench 2 Pot Key': [0x14000f, 0x140010, 'in Swamp Palace', 'Small Key (Swamp Palace)'],
+ 'Swamp Palace - Waterway Pot Key': [0x140009, 0x14000a, 'in Swamp Palace', 'Small Key (Swamp Palace)'],
+ 'Skull Woods - West Lobby Pot Key': [0x14002d, 0x14002e, 'in Skull Woods', 'Small Key (Skull Woods)'],
+ 'Skull Woods - Spike Corner Key Drop': [0x14001b, 0x14001c, 'near Mothula', 'Small Key (Skull Woods)'],
+ "Thieves' Town - Hallway Pot Key": [0x14005d, 0x14005e, "in Thieves' Town", 'Small Key (Thieves Town)'],
+ "Thieves' Town - Spike Switch Pot Key": [0x14004e, 0x14004f, "in Thieves' Town", 'Small Key (Thieves Town)'],
+ 'Ice Palace - Jelly Key Drop': [0x140003, 0x140004, 'in Ice Palace', 'Small Key (Ice Palace)'],
+ 'Ice Palace - Conveyor Key Drop': [0x140021, 0x140022, 'in Ice Palace', 'Small Key (Ice Palace)'],
+ 'Ice Palace - Hammer Block Key Drop': [0x140024, 0x140025, 'in Ice Palace', 'Small Key (Ice Palace)'],
+ 'Ice Palace - Many Pots Pot Key': [0x140045, 0x140046, 'in Ice Palace', 'Small Key (Ice Palace)'],
+ 'Misery Mire - Spikes Pot Key': [0x140054, 0x140055 , 'in Misery Mire', 'Small Key (Misery Mire)'],
+ 'Misery Mire - Fishbone Pot Key': [0x14004b, 0x14004c, 'in forgotten Mire', 'Small Key (Misery Mire)'],
+ 'Misery Mire - Conveyor Crystal Key Drop': [0x140063, 0x140064 , 'in Misery Mire', 'Small Key (Misery Mire)'],
+ 'Turtle Rock - Pokey 1 Key Drop': [0x140057, 0x140058, 'in Turtle Rock', 'Small Key (Turtle Rock)'],
+ 'Turtle Rock - Pokey 2 Key Drop': [0x140006, 0x140007, 'in Turtle Rock', 'Small Key (Turtle Rock)'],
+ 'Ganons Tower - Conveyor Cross Pot Key': [0x14003f, 0x140040, "in Ganon's Tower", 'Small Key (Ganons Tower)'],
+ 'Ganons Tower - Double Switch Pot Key': [0x140042, 0x140043, "in Ganon's Tower", 'Small Key (Ganons Tower)'],
+ 'Ganons Tower - Conveyor Star Pits Pot Key': [0x140039, 0x14003a, "in Ganon's Tower", 'Small Key (Ganons Tower)'],
+ 'Ganons Tower - Mini Helmasaur Key Drop': [0x14001e, 0x14001f, "atop Ganon's Tower", 'Small Key (Ganons Tower)']
}
# tuple contents:
diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py
index ed222b5f5d14..47cea8c20ea4 100644
--- a/worlds/alttp/Rom.py
+++ b/worlds/alttp/Rom.py
@@ -25,7 +25,7 @@
from .Shops import ShopType, ShopPriceType
from .Dungeons import dungeon_music_addresses
-from .Regions import old_location_address_to_new_location_address
+from .Regions import old_location_address_to_new_location_address, key_drop_data
from .Text import MultiByteTextMapper, text_addresses, Credits, TextTable
from .Text import Uncle_texts, Ganon1_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, \
Blind_texts, \
@@ -428,6 +428,18 @@ def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
rom.write_byte(0x04DE81, 6)
rom.write_byte(0x1B0101, 0) # Do not close boss room door on entry.
+ # Moblins attached to "key drop" locations crash the game when dropping their item when Key Drop Shuffle is on.
+ # Replace them with a Slime enemy if they are placed.
+ if multiworld.key_drop_shuffle[player]:
+ key_drop_enemies = {
+ 0x4DA20, 0x4DA5C, 0x4DB7F, 0x4DD73, 0x4DDC3, 0x4DE07, 0x4E201,
+ 0x4E20A, 0x4E326, 0x4E4F7, 0x4E686, 0x4E70C, 0x4E7C8, 0x4E7FA
+ }
+ for enemy in key_drop_enemies:
+ if rom.read_byte(enemy) == 0x12:
+ logging.debug(f"Moblin found and replaced at {enemy} in world {player}")
+ rom.write_byte(enemy, 0x8F)
+
for used in (randopatch_path, options_path):
try:
os.remove(used)
@@ -897,6 +909,29 @@ def credits_digit(num):
credits_total += 30 if 'w' in world.shop_shuffle[player] else 27
rom.write_byte(0x187010, credits_total) # dynamic credits
+
+ if world.key_drop_shuffle[player]:
+ rom.write_byte(0x140000, 1) # enable key drop shuffle
+ credits_total += len(key_drop_data)
+ # update dungeon counters
+ rom.write_byte(0x187001, 12) # Hyrule Castle
+ rom.write_byte(0x187002, 8) # Eastern Palace
+ rom.write_byte(0x187003, 9) # Desert Palace
+ rom.write_byte(0x187004, 4) # Agahnims Tower
+ rom.write_byte(0x187005, 15) # Swamp Palace
+ rom.write_byte(0x187007, 11) # Misery Mire
+ rom.write_byte(0x187008, 10) # Skull Woods
+ rom.write_byte(0x187009, 12) # Ice Palace
+ rom.write_byte(0x18700B, 10) # Thieves Town
+ rom.write_byte(0x18700C, 14) # Turtle Rock
+ rom.write_byte(0x18700D, 31) # Ganons Tower
+ # update credits GT Big Key counter
+ gt_bigkey_top, gt_bigkey_bottom = credits_digit(5)
+ rom.write_byte(0x118B6A, gt_bigkey_top)
+ rom.write_byte(0x118B88, gt_bigkey_bottom)
+
+
+
# collection rate address: 238C37
first_top, first_bot = credits_digit((credits_total / 100) % 10)
mid_top, mid_bot = credits_digit((credits_total / 10) % 10)
@@ -1824,10 +1859,10 @@ def apply_oof_sfx(rom, oof: str):
# (We need to insert the second sigil at the end)
rom.write_bytes(0x12803A, oof_bytes)
rom.write_bytes(0x12803A + len(oof_bytes), [0xEB, 0xEB])
-
+
#Enemizer patch: prevent Enemizer from overwriting $3188 in SPC memory with an unused sound effect ("WHAT")
rom.write_bytes(0x13000D, [0x00, 0x00, 0x00, 0x08])
-
+
def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, sprite: str, oof: str, palettes_options,
world=None, player=1, allow_random_on_event=False, reduceflashing=False,
diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py
index 09c63aca01d2..1fddecd8f4f4 100644
--- a/worlds/alttp/Rules.py
+++ b/worlds/alttp/Rules.py
@@ -32,7 +32,6 @@ def set_rules(world):
'WARNING! Seeds generated under this logic often require major glitches and may be impossible!')
if world.players == 1:
- world.get_region('Menu', player).can_reach_private = lambda state: True
no_logic_rules(world, player)
for exit in world.get_region('Menu', player).exits:
exit.hide_path = True
@@ -196,7 +195,6 @@ def global_rules(world, player):
add_item_rule(world.get_location(prize_location, player),
lambda item: item.name in crystals_and_pendants and item.player == player)
# determines which S&Q locations are available - hide from paths since it isn't an in-game location
- world.get_region('Menu', player).can_reach_private = lambda state: True
for exit in world.get_region('Menu', player).exits:
exit.hide_path = True
@@ -233,26 +231,41 @@ def global_rules(world, player):
set_rule(world.get_location('Hookshot Cave - Bottom Left', player), lambda state: state.has('Hookshot', player))
set_rule(world.get_entrance('Sewers Door', player),
- lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player) or (
+ lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4) or (
world.smallkey_shuffle[player] == smallkey_shuffle.option_universal and world.mode[
player] == 'standard')) # standard universal small keys cannot access the shop
set_rule(world.get_entrance('Sewers Back Door', player),
- lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player))
+ lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4))
set_rule(world.get_entrance('Agahnim 1', player),
- lambda state: has_sword(state, player) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2))
+ lambda state: has_sword(state, player) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 4))
set_rule(world.get_location('Castle Tower - Room 03', player), lambda state: can_kill_most_things(state, player, 8))
set_rule(world.get_location('Castle Tower - Dark Maze', player),
lambda state: can_kill_most_things(state, player, 8) and state._lttp_has_key('Small Key (Agahnims Tower)',
player))
-
+ set_rule(world.get_location('Castle Tower - Dark Archer Key Drop', player),
+ lambda state: can_kill_most_things(state, player, 8) and state._lttp_has_key('Small Key (Agahnims Tower)',
+ player, 2))
+ set_rule(world.get_location('Castle Tower - Circle of Pots Key Drop', player),
+ lambda state: can_kill_most_things(state, player, 8) and state._lttp_has_key('Small Key (Agahnims Tower)',
+ player, 3))
+ set_always_allow(world.get_location('Eastern Palace - Big Key Chest', player),
+ lambda state, item: item.name == 'Big Key (Eastern Palace)' and item.player == player)
+ set_rule(world.get_location('Eastern Palace - Big Key Chest', player),
+ lambda state: state._lttp_has_key('Small Key (Eastern Palace)', player, 2) or
+ ((location_item_name(state, 'Eastern Palace - Big Key Chest', player) == ('Big Key (Eastern Palace)', player)
+ and state.has('Small Key (Eastern Palace)', player))))
+ set_rule(world.get_location('Eastern Palace - Dark Eyegore Key Drop', player),
+ lambda state: state.has('Big Key (Eastern Palace)', player))
set_rule(world.get_location('Eastern Palace - Big Chest', player),
lambda state: state.has('Big Key (Eastern Palace)', player))
ep_boss = world.get_location('Eastern Palace - Boss', player)
set_rule(ep_boss, lambda state: state.has('Big Key (Eastern Palace)', player) and
+ state._lttp_has_key('Small Key (Eastern Palace)', player, 2) and
ep_boss.parent_region.dungeon.boss.can_defeat(state))
ep_prize = world.get_location('Eastern Palace - Prize', player)
set_rule(ep_prize, lambda state: state.has('Big Key (Eastern Palace)', player) and
+ state._lttp_has_key('Small Key (Eastern Palace)', player, 2) and
ep_prize.parent_region.dungeon.boss.can_defeat(state))
if not world.enemy_shuffle[player]:
add_rule(ep_boss, lambda state: can_shoot_arrows(state, player))
@@ -260,9 +273,13 @@ def global_rules(world, player):
set_rule(world.get_location('Desert Palace - Big Chest', player), lambda state: state.has('Big Key (Desert Palace)', player))
set_rule(world.get_location('Desert Palace - Torch', player), lambda state: state.has('Pegasus Boots', player))
- set_rule(world.get_entrance('Desert Palace East Wing', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player))
- set_rule(world.get_location('Desert Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and has_fire_source(state, player) and state.multiworld.get_location('Desert Palace - Prize', player).parent_region.dungeon.boss.can_defeat(state))
- set_rule(world.get_location('Desert Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and has_fire_source(state, player) and state.multiworld.get_location('Desert Palace - Boss', player).parent_region.dungeon.boss.can_defeat(state))
+
+ set_rule(world.get_entrance('Desert Palace East Wing', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 4))
+ set_rule(world.get_location('Desert Palace - Big Key Chest', player), lambda state: can_kill_most_things(state, player))
+ set_rule(world.get_location('Desert Palace - Beamos Hall Pot Key', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 2) and can_kill_most_things(state, player))
+ set_rule(world.get_location('Desert Palace - Desert Tiles 2 Pot Key', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 3) and can_kill_most_things(state, player))
+ set_rule(world.get_location('Desert Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 4) and state.has('Big Key (Desert Palace)', player) and has_fire_source(state, player) and state.multiworld.get_location('Desert Palace - Prize', player).parent_region.dungeon.boss.can_defeat(state))
+ set_rule(world.get_location('Desert Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 4) and state.has('Big Key (Desert Palace)', player) and has_fire_source(state, player) and state.multiworld.get_location('Desert Palace - Boss', player).parent_region.dungeon.boss.can_defeat(state))
# logic patch to prevent placing a crystal in Desert that's required to reach the required keys
if not (world.smallkey_shuffle[player] and world.bigkey_shuffle[player]):
@@ -277,57 +294,98 @@ def global_rules(world, player):
set_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Flippers', player) and state.has('Open Floodgate', player))
set_rule(world.get_entrance('Swamp Palace Small Key Door', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player))
- set_rule(world.get_entrance('Swamp Palace (Center)', player), lambda state: state.has('Hammer', player))
+ set_rule(world.get_location('Swamp Palace - Trench 1 Pot Key', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 2))
+ set_rule(world.get_entrance('Swamp Palace (Center)', player), lambda state: state.has('Hammer', player) and state._lttp_has_key('Small Key (Swamp Palace)', player, 3))
+ set_rule(world.get_location('Swamp Palace - Hookshot Pot Key', player), lambda state: state.has('Hookshot', player))
+ set_rule(world.get_entrance('Swamp Palace (West)', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6)
+ if state.has('Hookshot', player)
+ else state._lttp_has_key('Small Key (Swamp Palace)', player, 4))
set_rule(world.get_location('Swamp Palace - Big Chest', player), lambda state: state.has('Big Key (Swamp Palace)', player))
if world.accessibility[player] != 'locations':
allow_self_locking_items(world.get_location('Swamp Palace - Big Chest', player), 'Big Key (Swamp Palace)')
- set_rule(world.get_entrance('Swamp Palace (North)', player), lambda state: state.has('Hookshot', player))
+ set_rule(world.get_entrance('Swamp Palace (North)', player), lambda state: state.has('Hookshot', player) and state._lttp_has_key('Small Key (Swamp Palace)', player, 5))
if not world.smallkey_shuffle[player] and world.logic[player] not in ['hybridglitches', 'nologic']:
forbid_item(world.get_location('Swamp Palace - Entrance', player), 'Big Key (Swamp Palace)', player)
+ set_rule(world.get_location('Swamp Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6))
+ set_rule(world.get_location('Swamp Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6))
set_rule(world.get_entrance('Thieves Town Big Key Door', player), lambda state: state.has('Big Key (Thieves Town)', player))
- set_rule(world.get_entrance('Blind Fight', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
- set_rule(world.get_location('Thieves\' Town - Big Chest', player), lambda state: (state._lttp_has_key('Small Key (Thieves Town)', player)) and state.has('Hammer', player))
+
+ if world.worlds[player].dungeons["Thieves Town"].boss.enemizer_name == "Blind":
+ set_rule(world.get_entrance('Blind Fight', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3))
+
+ set_rule(world.get_location('Thieves\' Town - Big Chest', player),
+ lambda state: (state._lttp_has_key('Small Key (Thieves Town)', player, 3)) and state.has('Hammer', player))
if world.accessibility[player] != 'locations':
allow_self_locking_items(world.get_location('Thieves\' Town - Big Chest', player), 'Small Key (Thieves Town)')
- set_rule(world.get_location('Thieves\' Town - Attic', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
- set_rule(world.get_entrance('Skull Woods First Section South Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player))
- set_rule(world.get_entrance('Skull Woods First Section (Right) North Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player))
- set_rule(world.get_entrance('Skull Woods First Section West Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 2)) # ideally would only be one key, but we may have spent thst key already on escaping the right section
- set_rule(world.get_entrance('Skull Woods First Section (Left) Door to Exit', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 2))
+ set_rule(world.get_location('Thieves\' Town - Attic', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3))
+ set_rule(world.get_location('Thieves\' Town - Spike Switch Pot Key', player),
+ lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
+
+ # We need so many keys in the SW doors because they are all reachable as the last door (except for the door to mothula)
+ set_rule(world.get_entrance('Skull Woods First Section South Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
+ set_rule(world.get_entrance('Skull Woods First Section (Right) North Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
+ set_rule(world.get_entrance('Skull Woods First Section West Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
+ set_rule(world.get_entrance('Skull Woods First Section (Left) Door to Exit', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
set_rule(world.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player))
if world.accessibility[player] != 'locations':
allow_self_locking_items(world.get_location('Skull Woods - Big Chest', player), 'Big Key (Skull Woods)')
- set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player) and has_sword(state, player)) # sword required for curtain
+ set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 4) and state.has('Fire Rod', player) and has_sword(state, player)) # sword required for curtain
+ add_rule(world.get_location('Skull Woods - Prize', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
+ add_rule(world.get_location('Skull Woods - Boss', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
- set_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: can_melt_things(state, player))
+ set_rule(world.get_location('Ice Palace - Jelly Key Drop', player), lambda state: can_melt_things(state, player))
+ set_rule(world.get_entrance('Ice Palace (Second Section)', player), lambda state: can_melt_things(state, player) and state._lttp_has_key('Small Key (Ice Palace)', player))
+ set_rule(world.get_entrance('Ice Palace (Main)', player), lambda state: state._lttp_has_key('Small Key (Ice Palace)', player, 2))
set_rule(world.get_location('Ice Palace - Big Chest', player), lambda state: state.has('Big Key (Ice Palace)', player))
- set_rule(world.get_entrance('Ice Palace (Kholdstare)', player), lambda state: can_lift_rocks(state, player) and state.has('Hammer', player) and state.has('Big Key (Ice Palace)', player) and (state._lttp_has_key('Small Key (Ice Palace)', player, 2) or (state.has('Cane of Somaria', player) and state._lttp_has_key('Small Key (Ice Palace)', player, 1))))
- set_rule(world.get_entrance('Ice Palace (East)', player), lambda state: (state.has('Hookshot', player) or (
- item_name_in_location_names(state, 'Big Key (Ice Palace)', player, [('Ice Palace - Spike Room', player), ('Ice Palace - Big Key Chest', player), ('Ice Palace - Map Chest', player)]) and state._lttp_has_key('Small Key (Ice Palace)', player))) and (state.multiworld.can_take_damage[player] or state.has('Hookshot', player) or state.has('Cape', player) or state.has('Cane of Byrna', player)))
+ set_rule(world.get_entrance('Ice Palace (Kholdstare)', player), lambda state: can_lift_rocks(state, player) and state.has('Hammer', player) and state.has('Big Key (Ice Palace)', player) and (state._lttp_has_key('Small Key (Ice Palace)', player, 6) or (state.has('Cane of Somaria', player) and state._lttp_has_key('Small Key (Ice Palace)', player, 5))))
+ # This is a complicated rule, so let's break it down.
+ # Hookshot always suffices to get to the right side.
+ # Also, once you get over there, you have to cross the spikes, so that's the last line.
+ # Alternatively, we could not have hookshot. Then we open the keydoor into right side in order to get there.
+ # This is conditional on whether we have the big key or not, as big key opens the ability to waste more keys.
+ # Specifically, if we have big key we can burn 2 extra keys near the boss and will need +2 keys. That's all of them as this could be the last door.
+ # Hence if big key is available then it's 6 keys, otherwise 4 keys.
+ # If key_drop is off, then we have 3 drop keys available, and can never satisfy the 6 key requirement because one key is on right side,
+ # so this reduces perfectly to original logic.
+ set_rule(world.get_entrance('Ice Palace (East)', player), lambda state: (state.has('Hookshot', player) or
+ (state._lttp_has_key('Small Key (Ice Palace)', player, 4)
+ if item_name_in_location_names(state, 'Big Key (Ice Palace)', player, [('Ice Palace - Spike Room', player),
+ ('Ice Palace - Hammer Block Key Drop', player),
+ ('Ice Palace - Big Key Chest', player),
+ ('Ice Palace - Map Chest', player)])
+ else state._lttp_has_key('Small Key (Ice Palace)', player, 6))) and
+ (state.multiworld.can_take_damage[player] or state.has('Hookshot', player) or state.has('Cape', player) or state.has('Cane of Byrna', player)))
set_rule(world.get_entrance('Ice Palace (East Top)', player), lambda state: can_lift_rocks(state, player) and state.has('Hammer', player))
set_rule(world.get_entrance('Misery Mire Entrance Gap', player), lambda state: (state.has('Pegasus Boots', player) or state.has('Hookshot', player)) and (has_sword(state, player) or state.has('Fire Rod', player) or state.has('Ice Rod', player) or state.has('Hammer', player) or state.has('Cane of Somaria', player) or can_shoot_arrows(state, player))) # need to defeat wizzrobes, bombs don't work ...
+ set_rule(world.get_location('Misery Mire - Fishbone Pot Key', player), lambda state: state.has('Big Key (Misery Mire)', player) or state._lttp_has_key('Small Key (Misery Mire)', player, 4))
+
set_rule(world.get_location('Misery Mire - Big Chest', player), lambda state: state.has('Big Key (Misery Mire)', player))
set_rule(world.get_location('Misery Mire - Spike Chest', player), lambda state: (state.multiworld.can_take_damage[player] and has_hearts(state, player, 4)) or state.has('Cane of Byrna', player) or state.has('Cape', player))
set_rule(world.get_entrance('Misery Mire Big Key Door', player), lambda state: state.has('Big Key (Misery Mire)', player))
- # you can squander the free small key from the pot by opening the south door to the north west switch room, locking you out of accessing a color switch ...
- # big key gives backdoor access to that from the teleporter in the north west
- set_rule(world.get_location('Misery Mire - Map Chest', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 1) or state.has('Big Key (Misery Mire)', player))
- set_rule(world.get_location('Misery Mire - Main Lobby', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 1) or state._lttp_has_key('Big Key (Misery Mire)', player))
+ # How to access crystal switch:
+ # If have big key: then you will need 2 small keys to be able to hit switch and return to main area, as you can burn key in dark room
+ # If not big key: cannot burn key in dark room, hence need only 1 key. all doors immediately available lead to a crystal switch.
+ # The listed chests are those which can be reached if you can reach a crystal switch.
+ set_rule(world.get_location('Misery Mire - Map Chest', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 2))
+ set_rule(world.get_location('Misery Mire - Main Lobby', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 2))
# we can place a small key in the West wing iff it also contains/blocks the Big Key, as we cannot reach and softlock with the basement key door yet
- set_rule(world.get_entrance('Misery Mire (West)', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 2) if ((
- location_item_name(state, 'Misery Mire - Compass Chest', player) in [('Big Key (Misery Mire)', player)]) or
- (
- location_item_name(state, 'Misery Mire - Big Key Chest', player) in [('Big Key (Misery Mire)', player)])) else state._lttp_has_key('Small Key (Misery Mire)', player, 3))
+ set_rule(world.get_location('Misery Mire - Conveyor Crystal Key Drop', player),
+ lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 4)
+ if location_item_name(state, 'Misery Mire - Compass Chest', player) == ('Big Key (Misery Mire)', player) or location_item_name(state, 'Misery Mire - Big Key Chest', player) == ('Big Key (Misery Mire)', player) or location_item_name(state, 'Misery Mire - Conveyor Crystal Key Drop', player) == ('Big Key (Misery Mire)', player)
+ else state._lttp_has_key('Small Key (Misery Mire)', player, 5))
+ set_rule(world.get_entrance('Misery Mire (West)', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 5)
+ if ((location_item_name(state, 'Misery Mire - Compass Chest', player) in [('Big Key (Misery Mire)', player)]) or (location_item_name(state, 'Misery Mire - Big Key Chest', player) in [('Big Key (Misery Mire)', player)]))
+ else state._lttp_has_key('Small Key (Misery Mire)', player, 6))
set_rule(world.get_location('Misery Mire - Compass Chest', player), lambda state: has_fire_source(state, player))
set_rule(world.get_location('Misery Mire - Big Key Chest', player), lambda state: has_fire_source(state, player))
set_rule(world.get_entrance('Misery Mire (Vitreous)', player), lambda state: state.has('Cane of Somaria', player))
set_rule(world.get_entrance('Turtle Rock Entrance Gap', player), lambda state: state.has('Cane of Somaria', player))
set_rule(world.get_entrance('Turtle Rock Entrance Gap Reverse', player), lambda state: state.has('Cane of Somaria', player))
- set_rule(world.get_location('Turtle Rock - Compass Chest', player), lambda state: state.has('Cane of Somaria', player)) # We could get here from the middle section without Cane as we don't cross the entrance gap!
+ set_rule(world.get_location('Turtle Rock - Compass Chest', player), lambda state: state.has('Cane of Somaria', player))
set_rule(world.get_location('Turtle Rock - Roller Room - Left', player), lambda state: state.has('Cane of Somaria', player) and state.has('Fire Rod', player))
set_rule(world.get_location('Turtle Rock - Roller Room - Right', player), lambda state: state.has('Cane of Somaria', player) and state.has('Fire Rod', player))
set_rule(world.get_location('Turtle Rock - Big Chest', player), lambda state: state.has('Big Key (Turtle Rock)', player) and (state.has('Cane of Somaria', player) or state.has('Hookshot', player)))
@@ -339,7 +397,7 @@ def global_rules(world, player):
set_rule(world.get_location('Turtle Rock - Eye Bridge - Bottom Right', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
set_rule(world.get_location('Turtle Rock - Eye Bridge - Top Left', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
set_rule(world.get_location('Turtle Rock - Eye Bridge - Top Right', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
- set_rule(world.get_entrance('Turtle Rock (Trinexx)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 4) and state.has('Big Key (Turtle Rock)', player) and state.has('Cane of Somaria', player))
+ set_rule(world.get_entrance('Turtle Rock (Trinexx)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6) and state.has('Big Key (Turtle Rock)', player) and state.has('Cane of Somaria', player))
if not world.enemy_shuffle[player]:
set_rule(world.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: can_shoot_arrows(state, player))
@@ -363,35 +421,46 @@ def global_rules(world, player):
# these key rules are conservative, you might be able to get away with more lenient rules
randomizer_room_chests = ['Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right', 'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right']
- compass_room_chests = ['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right', 'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Compass Room - Bottom Right']
+ compass_room_chests = ['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right', 'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Compass Room - Bottom Right', 'Ganons Tower - Conveyor Star Pits Pot Key']
+ back_chests = ['Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest', 'Ganons Tower - Big Key Room - Left', 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Chest']
+
set_rule(world.get_location('Ganons Tower - Bob\'s Torch', player), lambda state: state.has('Pegasus Boots', player))
set_rule(world.get_entrance('Ganons Tower (Tile Room)', player), lambda state: state.has('Cane of Somaria', player))
set_rule(world.get_entrance('Ganons Tower (Hookshot Room)', player), lambda state: state.has('Hammer', player) and (state.has('Hookshot', player) or state.has('Pegasus Boots', player)))
- set_rule(world.get_entrance('Ganons Tower (Map Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 4) or (
- location_item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player), ('Small Key (Ganons Tower)', player)] and state._lttp_has_key('Small Key (Ganons Tower)', player, 3)))
- if world.accessibility[player] != 'locations':
- set_always_allow(world.get_location('Ganons Tower - Map Chest', player), lambda state, item: item.name == 'Small Key (Ganons Tower)' and item.player == player and state._lttp_has_key('Small Key (Ganons Tower)', player, 3) and state.can_reach('Ganons Tower (Hookshot Room)', 'region', player))
-
- # It is possible to need more than 2 keys to get through this entrance if you spend keys elsewhere. We reflect this in the chest requirements.
- # However we need to leave these at the lower values to derive that with 3 keys it is always possible to reach Bob and Ice Armos.
- set_rule(world.get_entrance('Ganons Tower (Double Switch Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 2))
- # It is possible to need more than 3 keys ....
- set_rule(world.get_entrance('Ganons Tower (Firesnake Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3))
-
- #The actual requirements for these rooms to avoid key-lock
- set_rule(world.get_location('Ganons Tower - Firesnake Room', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3) or ((
- item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) or item_name_in_location_names(state, 'Small Key (Ganons Tower)', player, [('Ganons Tower - Firesnake Room', player)])) and state._lttp_has_key('Small Key (Ganons Tower)', player, 2)))
+ if world.pot_shuffle[player]:
+ # Pot Shuffle can move this check into the hookshot room
+ set_rule(world.get_location('Ganons Tower - Conveyor Cross Pot Key', player), lambda state: state.has('Hammer', player) and (state.has('Hookshot', player) or state.has('Pegasus Boots', player)))
+ set_rule(world.get_entrance('Ganons Tower (Map Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 8) or (
+ location_item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player)] and state._lttp_has_key('Small Key (Ganons Tower)', player, 6)))
+
+ # this seemed to be causing generation failure, disable for now
+ # if world.accessibility[player] != 'locations':
+ # set_always_allow(world.get_location('Ganons Tower - Map Chest', player), lambda state, item: item.name == 'Small Key (Ganons Tower)' and item.player == player and state._lttp_has_key('Small Key (Ganons Tower)', player, 7) and state.can_reach('Ganons Tower (Hookshot Room)', 'region', player))
+
+ # It is possible to need more than 6 keys to get through this entrance if you spend keys elsewhere. We reflect this in the chest requirements.
+ # However we need to leave these at the lower values to derive that with 7 keys it is always possible to reach Bob and Ice Armos.
+ set_rule(world.get_entrance('Ganons Tower (Double Switch Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 6))
+ # It is possible to need more than 7 keys ....
+ set_rule(world.get_entrance('Ganons Tower (Firesnake Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 7) or (
+ item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests + back_chests, [player] * len(randomizer_room_chests + back_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 5)))
+
+ # The actual requirements for these rooms to avoid key-lock
+ set_rule(world.get_location('Ganons Tower - Firesnake Room', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 7) or
+ ((item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) or item_name_in_location_names(state, 'Small Key (Ganons Tower)', player, [('Ganons Tower - Firesnake Room', player)])) and state._lttp_has_key('Small Key (Ganons Tower)', player, 5)))
for location in randomizer_room_chests:
- set_rule(world.get_location(location, player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 4) or (
- item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 3)))
-
- # Once again it is possible to need more than 3 keys...
- set_rule(world.get_entrance('Ganons Tower (Tile Room) Key Door', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3) and state.has('Fire Rod', player))
+ set_rule(world.get_location(location, player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 8) or (
+ item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 6)))
+
+ # Once again it is possible to need more than 7 keys...
+ set_rule(world.get_entrance('Ganons Tower (Tile Room) Key Door', player), lambda state: state.has('Fire Rod', player) and (state._lttp_has_key('Small Key (Ganons Tower)', player, 7) or (
+ item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(compass_room_chests, [player] * len(compass_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 5))))
+ set_rule(world.get_entrance('Ganons Tower (Bottom) (East)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 7) or (
+ item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(back_chests, [player] * len(back_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 5)))
# Actual requirements
for location in compass_room_chests:
- set_rule(world.get_location(location, player), lambda state: state.has('Fire Rod', player) and (state._lttp_has_key('Small Key (Ganons Tower)', player, 4) or (
- item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(compass_room_chests, [player] * len(compass_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 3))))
+ set_rule(world.get_location(location, player), lambda state: state.has('Fire Rod', player) and (state._lttp_has_key('Small Key (Ganons Tower)', player, 7) or (
+ item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(compass_room_chests, [player] * len(compass_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 5))))
set_rule(world.get_location('Ganons Tower - Big Chest', player), lambda state: state.has('Big Key (Ganons Tower)', player))
@@ -410,9 +479,9 @@ def global_rules(world, player):
set_rule(world.get_entrance('Ganons Tower Torch Rooms', player),
lambda state: has_fire_source(state, player) and state.multiworld.get_entrance('Ganons Tower Torch Rooms', player).parent_region.dungeon.bosses['middle'].can_defeat(state))
set_rule(world.get_location('Ganons Tower - Pre-Moldorm Chest', player),
- lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3))
+ lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 7))
set_rule(world.get_entrance('Ganons Tower Moldorm Door', player),
- lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 4))
+ lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 8))
set_rule(world.get_entrance('Ganons Tower Moldorm Gap', player),
lambda state: state.has('Hookshot', player) and state.multiworld.get_entrance('Ganons Tower Moldorm Gap', player).parent_region.dungeon.bosses['top'].can_defeat(state))
set_defeat_dungeon_boss_rule(world.get_location('Agahnim 2', player))
@@ -799,15 +868,21 @@ def add_conditional_lamp(spot, region, spottype='Location', accessible_torch=Fal
if world.mode[player] != 'inverted':
add_conditional_lamp('Agahnim 1', 'Agahnims Tower', 'Entrance')
add_conditional_lamp('Castle Tower - Dark Maze', 'Agahnims Tower')
+ add_conditional_lamp('Castle Tower - Dark Archer Key Drop', 'Agahnims Tower')
+ add_conditional_lamp('Castle Tower - Circle of Pots Key Drop', 'Agahnims Tower')
else:
add_conditional_lamp('Agahnim 1', 'Inverted Agahnims Tower', 'Entrance')
add_conditional_lamp('Castle Tower - Dark Maze', 'Inverted Agahnims Tower')
+ add_conditional_lamp('Castle Tower - Dark Archer Key Drop', 'Inverted Agahnims Tower')
+ add_conditional_lamp('Castle Tower - Circle of Pots Key Drop', 'Inverted Agahnims Tower')
add_conditional_lamp('Old Man', 'Old Man Cave')
add_conditional_lamp('Old Man Cave Exit (East)', 'Old Man Cave', 'Entrance')
add_conditional_lamp('Death Mountain Return Cave Exit (East)', 'Death Mountain Return Cave', 'Entrance')
add_conditional_lamp('Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave', 'Entrance')
add_conditional_lamp('Old Man House Front to Back', 'Old Man House', 'Entrance')
add_conditional_lamp('Old Man House Back to Front', 'Old Man House', 'Entrance')
+ add_conditional_lamp('Eastern Palace - Dark Square Pot Key', 'Eastern Palace')
+ add_conditional_lamp('Eastern Palace - Dark Eyegore Key Drop', 'Eastern Palace', 'Location', True)
add_conditional_lamp('Eastern Palace - Big Key Chest', 'Eastern Palace')
add_conditional_lamp('Eastern Palace - Boss', 'Eastern Palace', 'Location', True)
add_conditional_lamp('Eastern Palace - Prize', 'Eastern Palace', 'Location', True)
@@ -819,17 +894,32 @@ def add_conditional_lamp(spot, region, spottype='Location', accessible_torch=Fal
def open_rules(world, player):
- # softlock protection as you can reach the sewers small key door with a guard drop key
- set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player),
- lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player))
+ def basement_key_rule(state):
+ if location_item_name(state, 'Sewers - Key Rat Key Drop', player) == ("Small Key (Hyrule Castle)", player):
+ return state._lttp_has_key("Small Key (Hyrule Castle)", player, 2)
+ else:
+ return state._lttp_has_key("Small Key (Hyrule Castle)", player, 3)
+
+ set_rule(world.get_location('Hyrule Castle - Boomerang Guard Key Drop', player), basement_key_rule)
+ set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player), basement_key_rule)
+
+ set_rule(world.get_location('Sewers - Key Rat Key Drop', player),
+ lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 3))
+
+ set_rule(world.get_location('Hyrule Castle - Big Key Drop', player),
+ lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4))
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player),
- lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player))
+ lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4) and
+ state.has('Big Key (Hyrule Castle)', player))
def swordless_rules(world, player):
set_rule(world.get_entrance('Agahnim 1', player), lambda state: (state.has('Hammer', player) or state.has('Fire Rod', player) or can_shoot_arrows(state, player) or state.has('Cane of Somaria', player)) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2))
set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player)) # no curtain
- set_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player)) #in swordless mode bombos pads are present in the relevant parts of ice palace
+
+ set_rule(world.get_location('Ice Palace - Jelly Key Drop', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player))
+ set_rule(world.get_entrance('Ice Palace (Second Section)', player), lambda state: (state.has('Fire Rod', player) or state.has('Bombos', player)) and state._lttp_has_key('Small Key (Ice Palace)', player))
+
set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has('Hammer', player)) # need to damage ganon to get tiles to drop
if world.mode[player] != 'inverted':
@@ -852,11 +942,27 @@ def add_connection(parent_name, target_name, entrance_name, world, player):
def standard_rules(world, player):
add_connection('Menu', 'Hyrule Castle Secret Entrance', 'Uncle S&Q', world, player)
world.get_entrance('Uncle S&Q', player).hide_path = True
+ set_rule(world.get_entrance('Throne Room', player), lambda state: state.can_reach('Hyrule Castle - Zelda\'s Chest', 'Location', player))
set_rule(world.get_entrance('Hyrule Castle Exit (East)', player), lambda state: state.can_reach('Sanctuary', 'Region', player))
set_rule(world.get_entrance('Hyrule Castle Exit (West)', player), lambda state: state.can_reach('Sanctuary', 'Region', player))
set_rule(world.get_entrance('Links House S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player))
set_rule(world.get_entrance('Sanctuary S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player))
+ if world.smallkey_shuffle[player] != smallkey_shuffle.option_universal:
+ set_rule(world.get_location('Hyrule Castle - Boomerang Guard Key Drop', player),
+ lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 1))
+ set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player),
+ lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 1))
+
+ set_rule(world.get_location('Hyrule Castle - Big Key Drop', player),
+ lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 2))
+ set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player),
+ lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 2) and
+ state.has('Big Key (Hyrule Castle)', player))
+
+ set_rule(world.get_location('Sewers - Key Rat Key Drop', player),
+ lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 3))
+
def toss_junk_item(world, player):
items = ['Rupees (20)', 'Bombs (3)', 'Arrows (10)', 'Rupees (5)', 'Rupee (1)', 'Bombs (10)',
'Single Arrow', 'Rupees (50)', 'Rupees (100)', 'Single Bomb', 'Bee', 'Bee Trap',
@@ -871,7 +977,7 @@ def toss_junk_item(world, player):
def set_trock_key_rules(world, player):
# First set all relevant locked doors to impassible.
- for entrance in ['Turtle Rock Dark Room Staircase', 'Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock Pokey Room', 'Turtle Rock Big Key Door']:
+ for entrance in ['Turtle Rock Dark Room Staircase', 'Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock Entrance to Pokey Room', 'Turtle Rock (Pokey Room) (South)', 'Turtle Rock (Pokey Room) (North)', 'Turtle Rock Big Key Door']:
set_rule(world.get_entrance(entrance, player), lambda state: False)
all_state = world.get_all_state(use_cache=False)
@@ -894,6 +1000,7 @@ def set_trock_key_rules(world, player):
if can_reach_middle and not can_reach_back and not can_reach_front:
normal_regions = all_state.reachable_regions[player].copy()
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: True)
+ set_rule(world.get_entrance('Turtle Rock (Pokey Room) (South)', player), lambda state: True)
all_state.update_reachable_regions(player)
front_locked_regions = all_state.reachable_regions[player].difference(normal_regions)
front_locked_locations = set((location.name, player) for region in front_locked_regions for location in region.locations)
@@ -905,26 +1012,33 @@ def set_trock_key_rules(world, player):
# otherwise crystaroller room might not be properly marked as reachable through the back.
set_rule(world.get_entrance('Turtle Rock Big Key Door', player), lambda state: state.has('Big Key (Turtle Rock)', player))
- # No matter what, the key requirement for going from the middle to the bottom should be three keys.
- set_rule(world.get_entrance('Turtle Rock Dark Room Staircase', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3))
+ # No matter what, the key requirement for going from the middle to the bottom should be five keys.
+ set_rule(world.get_entrance('Turtle Rock Dark Room Staircase', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 5))
# Now we need to set rules based on which entrances we have access to. The most important point is whether we have back access. If we have back access, we
- # might open all the locked doors in any order so we need maximally restrictive rules.
+ # might open all the locked doors in any order, so we need maximally restrictive rules.
if can_reach_back:
- set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: (state._lttp_has_key('Small Key (Turtle Rock)', player, 4) or location_item_name(state, 'Turtle Rock - Big Key Chest', player) == ('Small Key (Turtle Rock)', player)))
- set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 4))
- # Only consider wasting the key on the Trinexx door for going from the front entrance to middle section. If other key doors are accessible, then these doors can be avoided
- set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3))
- set_rule(world.get_entrance('Turtle Rock Pokey Room', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 2))
+ set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: (state._lttp_has_key('Small Key (Turtle Rock)', player, 6) or location_item_name(state, 'Turtle Rock - Big Key Chest', player) == ('Small Key (Turtle Rock)', player)))
+ set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 5))
+ set_rule(world.get_entrance('Turtle Rock (Pokey Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6))
+
+ set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6))
+ set_rule(world.get_entrance('Turtle Rock (Pokey Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6))
+ set_rule(world.get_entrance('Turtle Rock Entrance to Pokey Room', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 5))
else:
- # Middle to front requires 2 keys if the back is locked, otherwise 4
- set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 2)
+ # Middle to front requires 3 keys if the back is locked by this door, otherwise 5
+ set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3)
+ if item_name_in_location_names(state, 'Big Key (Turtle Rock)', player, front_locked_locations.union({('Turtle Rock - Pokey 1 Key Drop', player)}))
+ else state._lttp_has_key('Small Key (Turtle Rock)', player, 5))
+ # Middle to front requires 4 keys if the back is locked by this door, otherwise 6
+ set_rule(world.get_entrance('Turtle Rock (Pokey Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 4)
if item_name_in_location_names(state, 'Big Key (Turtle Rock)', player, front_locked_locations)
- else state._lttp_has_key('Small Key (Turtle Rock)', player, 4))
+ else state._lttp_has_key('Small Key (Turtle Rock)', player, 6))
- # Front to middle requires 2 keys (if the middle is accessible then these doors can be avoided, otherwise no keys can be wasted)
- set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 2))
- set_rule(world.get_entrance('Turtle Rock Pokey Room', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 1))
+ # Front to middle requires 3 keys (if the middle is accessible then these doors can be avoided, otherwise no keys can be wasted)
+ set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3))
+ set_rule(world.get_entrance('Turtle Rock (Pokey Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 2))
+ set_rule(world.get_entrance('Turtle Rock Entrance to Pokey Room', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 1))
set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, tr_big_key_chest_keys_needed(state)))
@@ -935,8 +1049,8 @@ def tr_big_key_chest_keys_needed(state):
if item in [('Small Key (Turtle Rock)', player)]:
return 0
if item in [('Big Key (Turtle Rock)', player)]:
- return 2
- return 4
+ return 4
+ return 6
# If TR is only accessible from the middle, the big key must be further restricted to prevent softlock potential
if not can_reach_front and not world.smallkey_shuffle[player]:
@@ -945,10 +1059,12 @@ def tr_big_key_chest_keys_needed(state):
if not can_reach_big_chest:
# Must not go in the Chain Chomps chest - only 2 other chests available and 3+ keys required for all other chests
forbid_item(world.get_location('Turtle Rock - Chain Chomps', player), 'Big Key (Turtle Rock)', player)
+ forbid_item(world.get_location('Turtle Rock - Pokey 2 Key Drop', player), 'Big Key (Turtle Rock)', player)
if world.accessibility[player] == 'locations' and world.goal[player] != 'icerodhunt':
if world.bigkey_shuffle[player] and can_reach_big_chest:
# Must not go in the dungeon - all 3 available chests (Chomps, Big Chest, Crystaroller) must be keys to access laser bridge, and the big key is required first
for location in ['Turtle Rock - Chain Chomps', 'Turtle Rock - Compass Chest',
+ 'Turtle Rock - Pokey 1 Key Drop', 'Turtle Rock - Pokey 2 Key Drop',
'Turtle Rock - Roller Room - Left', 'Turtle Rock - Roller Room - Right']:
forbid_item(world.get_location(location, player), 'Big Key (Turtle Rock)', player)
else:
diff --git a/worlds/alttp/UnderworldGlitchRules.py b/worlds/alttp/UnderworldGlitchRules.py
index 11a95bf7cd6c..4b6bc54111e6 100644
--- a/worlds/alttp/UnderworldGlitchRules.py
+++ b/worlds/alttp/UnderworldGlitchRules.py
@@ -66,9 +66,12 @@ def underworld_glitches_rules(world, player):
fix_fake_worlds = world.fix_fake_world[player]
# Ice Palace Entrance Clip
- # This is the easiest one since it's a simple internal clip. Just need to also add melting to freezor chest since it's otherwise assumed.
- add_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: can_bomb_clip(state, world.get_region('Ice Palace (Entrance)', player), player), combine='or')
+ # This is the easiest one since it's a simple internal clip.
+ # Need to also add melting to freezor chest since it's otherwise assumed.
+ # Also can pick up the first jelly key from behind.
+ add_rule(world.get_entrance('Ice Palace (Main)', player), lambda state: can_bomb_clip(state, world.get_region('Ice Palace (Entrance)', player), player), combine='or')
add_rule(world.get_location('Ice Palace - Freezor Chest', player), lambda state: can_melt_things(state, player))
+ add_rule(world.get_location('Ice Palace - Jelly Key Drop', player), lambda state: can_bomb_clip(state, world.get_region('Ice Palace (Entrance)', player), player), combine='or')
# Kiki Skip
diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py
index 8815fae092e6..65e36da3bd6a 100644
--- a/worlds/alttp/__init__.py
+++ b/worlds/alttp/__init__.py
@@ -15,7 +15,7 @@
from .Items import item_init_table, item_name_groups, item_table, GetBeemizerItem
from .Options import alttp_options, smallkey_shuffle
from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions, lookup_vanilla_location_to_entrance, \
- is_main_entrance
+ is_main_entrance, key_drop_data
from .Client import ALTTPSNIClient
from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enemizer, apply_rom_settings, \
get_hash_string, get_base_rom_path, LttPDeltaPatch
@@ -303,6 +303,8 @@ def generate_early(self):
world.local_items[player].value |= self.item_name_groups[option.item_name_group]
elif option == "different_world":
world.non_local_items[player].value |= self.item_name_groups[option.item_name_group]
+ if world.mode[player] == "standard":
+ world.non_local_items[player].value -= {"Small Key (Hyrule Castle)"}
elif option.in_dungeon:
self.dungeon_local_item_names |= self.item_name_groups[option.item_name_group]
if option == "original_dungeon":
@@ -478,12 +480,17 @@ def pre_fill(self):
break
else:
raise FillError('Unable to place dungeon prizes')
+ if world.mode[player] == 'standard' and world.smallkey_shuffle[player] \
+ and world.smallkey_shuffle[player] != smallkey_shuffle.option_universal and \
+ world.smallkey_shuffle[player] != smallkey_shuffle.option_own_dungeons:
+ world.local_early_items[player]["Small Key (Hyrule Castle)"] = 1
@classmethod
def stage_pre_fill(cls, world):
from .Dungeons import fill_dungeons_restrictive
fill_dungeons_restrictive(world)
+
@classmethod
def stage_post_fill(cls, world):
ShopSlotFill(world)
@@ -618,7 +625,6 @@ def create_item(self, name: str) -> Item:
@classmethod
def stage_fill_hook(cls, world, progitempool, usefulitempool, filleritempool, fill_locations):
trash_counts = {}
-
for player in world.get_game_players("A Link to the Past"):
if not world.ganonstower_vanilla[player] or \
world.logic[player] in {'owglitches', 'hybridglitches', "nologic"}:
@@ -792,7 +798,7 @@ def fill_slot_data(self):
slot_options = ["crystals_needed_for_gt", "crystals_needed_for_ganon", "open_pyramid",
"bigkey_shuffle", "smallkey_shuffle", "compass_shuffle", "map_shuffle",
"progressive", "swordless", "retro_bow", "retro_caves", "shop_item_slots",
- "boss_shuffle", "pot_shuffle", "enemy_shuffle"]
+ "boss_shuffle", "pot_shuffle", "enemy_shuffle", "key_drop_shuffle"]
slot_data = {option_name: getattr(self.multiworld, option_name)[self.player].value for option_name in slot_options}
@@ -803,11 +809,11 @@ def fill_slot_data(self):
'mm_medalion': self.multiworld.required_medallions[self.player][0],
'tr_medalion': self.multiworld.required_medallions[self.player][1],
'shop_shuffle': self.multiworld.shop_shuffle[self.player],
- 'entrance_shuffle': self.multiworld.shuffle[self.player]
+ 'entrance_shuffle': self.multiworld.shuffle[self.player],
}
)
return slot_data
-
+
def get_same_seed(world, seed_def: tuple) -> str:
seeds: typing.Dict[tuple, str] = getattr(world, "__named_seeds", {})
diff --git a/worlds/alttp/test/dungeons/TestAgahnimsTower.py b/worlds/alttp/test/dungeons/TestAgahnimsTower.py
index 6d0e1085f5cb..94e785485882 100644
--- a/worlds/alttp/test/dungeons/TestAgahnimsTower.py
+++ b/worlds/alttp/test/dungeons/TestAgahnimsTower.py
@@ -16,6 +16,18 @@ def testTower(self):
["Castle Tower - Dark Maze", False, [], ['Progressive Sword', 'Hammer', 'Progressive Bow', 'Fire Rod', 'Ice Rod', 'Cane of Somaria', 'Cane of Byrna']],
["Castle Tower - Dark Maze", True, ['Progressive Sword', 'Small Key (Agahnims Tower)', 'Lamp']],
+ ["Castle Tower - Dark Archer Key Drop", False, []],
+ ["Castle Tower - Dark Archer Key Drop", False, ['Small Key (Agahnims Tower)', 'Small Key (Agahnims Tower)']],
+ ["Castle Tower - Dark Archer Key Drop", False, [], ['Lamp']],
+ ["Castle Tower - Dark Archer Key Drop", False, [], ['Progressive Sword', 'Hammer', 'Progressive Bow', 'Fire Rod', 'Ice Rod', 'Cane of Somaria', 'Cane of Byrna']],
+ ["Castle Tower - Dark Archer Key Drop", True, ['Progressive Sword', 'Small Key (Agahnims Tower)', 'Small Key (Agahnims Tower)', 'Lamp']],
+
+ ["Castle Tower - Circle of Pots Key Drop", False, []],
+ ["Castle Tower - Circle of Pots Key Drop", False, ['Small Key (Agahnims Tower)', 'Small Key (Agahnims Tower)']],
+ ["Castle Tower - Circle of Pots Key Drop", False, [], ['Lamp']],
+ ["Castle Tower - Circle of Pots Key Drop", False, [], ['Progressive Sword', 'Hammer', 'Progressive Bow', 'Fire Rod', 'Ice Rod', 'Cane of Somaria', 'Cane of Byrna']],
+ ["Castle Tower - Circle of Pots Key Drop", True, ['Progressive Sword', 'Small Key (Agahnims Tower)', 'Small Key (Agahnims Tower)', 'Lamp']],
+
["Agahnim 1", False, []],
["Agahnim 1", False, ['Small Key (Agahnims Tower)'], ['Small Key (Agahnims Tower)']],
["Agahnim 1", False, [], ['Progressive Sword']],
diff --git a/worlds/alttp/test/dungeons/TestDesertPalace.py b/worlds/alttp/test/dungeons/TestDesertPalace.py
index 8423e681cf16..2d1951391177 100644
--- a/worlds/alttp/test/dungeons/TestDesertPalace.py
+++ b/worlds/alttp/test/dungeons/TestDesertPalace.py
@@ -18,12 +18,27 @@ def testDesertPalace(self):
["Desert Palace - Compass Chest", False, []],
["Desert Palace - Compass Chest", False, [], ['Small Key (Desert Palace)']],
- ["Desert Palace - Compass Chest", True, ['Small Key (Desert Palace)']],
+ ["Desert Palace - Compass Chest", False, ['Progressive Sword', 'Hammer', 'Fire Rod', 'Ice Rod', 'Progressive Bow', 'Cane of Somaria', 'Cane of Byrna']],
+ ["Desert Palace - Compass Chest", False, ['Small Key (Desert Palace)']],
+ ["Desert Palace - Compass Chest", True, ['Progressive Sword', 'Small Key (Desert Palace)']],
- #@todo: Require a real weapon for enemizer?
["Desert Palace - Big Key Chest", False, []],
["Desert Palace - Big Key Chest", False, [], ['Small Key (Desert Palace)']],
- ["Desert Palace - Big Key Chest", True, ['Small Key (Desert Palace)']],
+ ["Desert Palace - Big Key Chest", False, ['Progressive Sword', 'Hammer', 'Fire Rod', 'Ice Rod', 'Progressive Bow', 'Cane of Somaria', 'Cane of Byrna']],
+ ["Desert Palace - Big Key Chest", False, ['Small Key (Desert Palace)']],
+ ["Desert Palace - Big Key Chest", True, ['Progressive Sword', 'Small Key (Desert Palace)']],
+
+ ["Desert Palace - Desert Tiles 1 Pot Key", True, []],
+
+ ["Desert Palace - Beamos Hall Pot Key", False, []],
+ ["Desert Palace - Beamos Hall Pot Key", False, [], ['Small Key (Desert Palace)']],
+ ["Desert Palace - Beamos Hall Pot Key", False, ['Progressive Sword', 'Hammer', 'Fire Rod', 'Ice Rod', 'Progressive Bow', 'Cane of Somaria', 'Cane of Byrna']],
+ ["Desert Palace - Beamos Hall Pot Key", True, ['Small Key (Desert Palace)', 'Progressive Sword']],
+
+ ["Desert Palace - Desert Tiles 2 Pot Key", False, []],
+ ["Desert Palace - Desert Tiles 2 Pot Key", False, ['Small Key (Desert Palace)']],
+ ["Desert Palace - Desert Tiles 2 Pot Key", False, ['Progressive Sword', 'Hammer', 'Fire Rod', 'Ice Rod', 'Progressive Bow', 'Cane of Somaria', 'Cane of Byrna']],
+ ["Desert Palace - Desert Tiles 2 Pot Key", True, ['Small Key (Desert Palace)', 'Progressive Sword']],
["Desert Palace - Boss", False, []],
["Desert Palace - Boss", False, [], ['Small Key (Desert Palace)']],
@@ -33,7 +48,6 @@ def testDesertPalace(self):
["Desert Palace - Boss", True, ['Small Key (Desert Palace)', 'Big Key (Desert Palace)', 'Fire Rod']],
["Desert Palace - Boss", True, ['Small Key (Desert Palace)', 'Big Key (Desert Palace)', 'Lamp', 'Progressive Sword']],
["Desert Palace - Boss", True, ['Small Key (Desert Palace)', 'Big Key (Desert Palace)', 'Lamp', 'Hammer']],
- ["Desert Palace - Boss", True, ['Small Key (Desert Palace)', 'Big Key (Desert Palace)', 'Lamp', 'Ice Rod']],
["Desert Palace - Boss", True, ['Small Key (Desert Palace)', 'Big Key (Desert Palace)', 'Lamp', 'Cane of Somaria']],
["Desert Palace - Boss", True, ['Small Key (Desert Palace)', 'Big Key (Desert Palace)', 'Lamp', 'Cane of Byrna']],
])
\ No newline at end of file
diff --git a/worlds/alttp/test/dungeons/TestDungeon.py b/worlds/alttp/test/dungeons/TestDungeon.py
index 73ece1541733..81085ab10a16 100644
--- a/worlds/alttp/test/dungeons/TestDungeon.py
+++ b/worlds/alttp/test/dungeons/TestDungeon.py
@@ -61,6 +61,7 @@ def run_tests(self, access_pool):
for item in items:
item.classification = ItemClassification.progression
- state.collect(item)
+ state.collect(item, event=True) # event=True prevents running sweep_for_events() and picking up
+ state.sweep_for_events() # key drop keys repeatedly
- self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access)
\ No newline at end of file
+ self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access, f"failed {self.multiworld.get_location(location, 1)} with: {item_pool}")
\ No newline at end of file
diff --git a/worlds/alttp/test/dungeons/TestEasternPalace.py b/worlds/alttp/test/dungeons/TestEasternPalace.py
index 0497a1132ee3..35c1b9928394 100644
--- a/worlds/alttp/test/dungeons/TestEasternPalace.py
+++ b/worlds/alttp/test/dungeons/TestEasternPalace.py
@@ -18,7 +18,8 @@ def testEastern(self):
["Eastern Palace - Big Key Chest", False, []],
["Eastern Palace - Big Key Chest", False, [], ['Lamp']],
- ["Eastern Palace - Big Key Chest", True, ['Lamp']],
+ ["Eastern Palace - Big Key Chest", True, ['Lamp', 'Small Key (Eastern Palace)', 'Small Key (Eastern Palace)']],
+ ["Eastern Palace - Big Key Chest", True, ['Lamp', 'Big Key (Eastern Palace)']],
#@todo: Advanced?
["Eastern Palace - Boss", False, []],
diff --git a/worlds/alttp/test/dungeons/TestGanonsTower.py b/worlds/alttp/test/dungeons/TestGanonsTower.py
index f81509273f00..d22dc92b366f 100644
--- a/worlds/alttp/test/dungeons/TestGanonsTower.py
+++ b/worlds/alttp/test/dungeons/TestGanonsTower.py
@@ -33,46 +33,50 @@ def testGanonsTower(self):
["Ganons Tower - Randomizer Room - Top Left", False, []],
["Ganons Tower - Randomizer Room - Top Left", False, [], ['Hammer']],
["Ganons Tower - Randomizer Room - Top Left", False, [], ['Hookshot']],
- ["Ganons Tower - Randomizer Room - Top Left", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
+ ["Ganons Tower - Randomizer Room - Top Left", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer', 'Fire Rod', 'Cane of Somaria']],
+ ["Ganons Tower - Randomizer Room - Top Left", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
["Ganons Tower - Randomizer Room - Top Right", False, []],
["Ganons Tower - Randomizer Room - Top Right", False, [], ['Hammer']],
["Ganons Tower - Randomizer Room - Top Right", False, [], ['Hookshot']],
- ["Ganons Tower - Randomizer Room - Top Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
+ ["Ganons Tower - Randomizer Room - Top Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer', 'Fire Rod', 'Cane of Somaria']],
+ ["Ganons Tower - Randomizer Room - Top Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
["Ganons Tower - Randomizer Room - Bottom Left", False, []],
["Ganons Tower - Randomizer Room - Bottom Left", False, [], ['Hammer']],
["Ganons Tower - Randomizer Room - Bottom Left", False, [], ['Hookshot']],
- ["Ganons Tower - Randomizer Room - Bottom Left", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
+ ["Ganons Tower - Randomizer Room - Bottom Left", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer', 'Fire Rod', 'Cane of Somaria']],
+ ["Ganons Tower - Randomizer Room - Bottom Left", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
["Ganons Tower - Randomizer Room - Bottom Right", False, []],
["Ganons Tower - Randomizer Room - Bottom Right", False, [], ['Hammer']],
["Ganons Tower - Randomizer Room - Bottom Right", False, [], ['Hookshot']],
- ["Ganons Tower - Randomizer Room - Bottom Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
+ ["Ganons Tower - Randomizer Room - Bottom Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer', 'Fire Rod', 'Cane of Somaria']],
+ ["Ganons Tower - Randomizer Room - Bottom Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
["Ganons Tower - Firesnake Room", False, []],
["Ganons Tower - Firesnake Room", False, [], ['Hammer']],
["Ganons Tower - Firesnake Room", False, [], ['Hookshot']],
- ["Ganons Tower - Firesnake Room", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
+ ["Ganons Tower - Firesnake Room", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
["Ganons Tower - Map Chest", False, []],
["Ganons Tower - Map Chest", False, [], ['Hammer']],
["Ganons Tower - Map Chest", False, [], ['Hookshot', 'Pegasus Boots']],
- ["Ganons Tower - Map Chest", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
- ["Ganons Tower - Map Chest", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hammer', 'Pegasus Boots']],
+ ["Ganons Tower - Map Chest", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
+ ["Ganons Tower - Map Chest", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hammer', 'Pegasus Boots']],
["Ganons Tower - Big Chest", False, []],
["Ganons Tower - Big Chest", False, [], ['Big Key (Ganons Tower)']],
- ["Ganons Tower - Big Chest", True, ['Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Cane of Somaria', 'Fire Rod']],
- ["Ganons Tower - Big Chest", True, ['Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
+ ["Ganons Tower - Big Chest", True, ['Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Cane of Somaria', 'Fire Rod']],
+ ["Ganons Tower - Big Chest", True, ['Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
["Ganons Tower - Hope Room - Left", True, []],
["Ganons Tower - Hope Room - Right", True, []],
["Ganons Tower - Bob's Chest", False, []],
- ["Ganons Tower - Bob's Chest", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Cane of Somaria', 'Fire Rod']],
- ["Ganons Tower - Bob's Chest", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
+ ["Ganons Tower - Bob's Chest", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Cane of Somaria', 'Fire Rod']],
+ ["Ganons Tower - Bob's Chest", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
["Ganons Tower - Tile Room", False, []],
["Ganons Tower - Tile Room", False, [], ['Cane of Somaria']],
@@ -81,34 +85,34 @@ def testGanonsTower(self):
["Ganons Tower - Compass Room - Top Left", False, []],
["Ganons Tower - Compass Room - Top Left", False, [], ['Cane of Somaria']],
["Ganons Tower - Compass Room - Top Left", False, [], ['Fire Rod']],
- ["Ganons Tower - Compass Room - Top Left", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Cane of Somaria']],
+ ["Ganons Tower - Compass Room - Top Left", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Cane of Somaria']],
["Ganons Tower - Compass Room - Top Right", False, []],
["Ganons Tower - Compass Room - Top Right", False, [], ['Cane of Somaria']],
["Ganons Tower - Compass Room - Top Right", False, [], ['Fire Rod']],
- ["Ganons Tower - Compass Room - Top Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Cane of Somaria']],
+ ["Ganons Tower - Compass Room - Top Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Cane of Somaria']],
["Ganons Tower - Compass Room - Bottom Left", False, []],
["Ganons Tower - Compass Room - Bottom Left", False, [], ['Cane of Somaria']],
["Ganons Tower - Compass Room - Bottom Left", False, [], ['Fire Rod']],
- ["Ganons Tower - Compass Room - Bottom Left", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Cane of Somaria']],
+ ["Ganons Tower - Compass Room - Bottom Left", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Cane of Somaria']],
["Ganons Tower - Compass Room - Bottom Right", False, []],
["Ganons Tower - Compass Room - Bottom Right", False, [], ['Cane of Somaria']],
["Ganons Tower - Compass Room - Bottom Right", False, [], ['Fire Rod']],
- ["Ganons Tower - Compass Room - Bottom Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Cane of Somaria']],
+ ["Ganons Tower - Compass Room - Bottom Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Cane of Somaria']],
["Ganons Tower - Big Key Chest", False, []],
- ["Ganons Tower - Big Key Chest", True, ['Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Cane of Somaria', 'Fire Rod']],
- ["Ganons Tower - Big Key Chest", True, ['Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
+ ["Ganons Tower - Big Key Chest", True, ['Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Cane of Somaria', 'Fire Rod']],
+ ["Ganons Tower - Big Key Chest", True, ['Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
["Ganons Tower - Big Key Room - Left", False, []],
- ["Ganons Tower - Big Key Room - Left", True, ['Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Cane of Somaria', 'Fire Rod']],
- ["Ganons Tower - Big Key Room - Left", True, ['Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
+ ["Ganons Tower - Big Key Room - Left", True, ['Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Cane of Somaria', 'Fire Rod']],
+ ["Ganons Tower - Big Key Room - Left", True, ['Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
["Ganons Tower - Big Key Room - Right", False, []],
- ["Ganons Tower - Big Key Room - Right", True, ['Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Cane of Somaria', 'Fire Rod']],
- ["Ganons Tower - Big Key Room - Right", True, ['Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
+ ["Ganons Tower - Big Key Room - Right", True, ['Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Cane of Somaria', 'Fire Rod']],
+ ["Ganons Tower - Big Key Room - Right", True, ['Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
["Ganons Tower - Mini Helmasaur Room - Left", False, []],
["Ganons Tower - Mini Helmasaur Room - Left", False, [], ['Progressive Bow']],
@@ -128,8 +132,8 @@ def testGanonsTower(self):
["Ganons Tower - Pre-Moldorm Chest", False, [], ['Progressive Bow']],
["Ganons Tower - Pre-Moldorm Chest", False, [], ['Big Key (Ganons Tower)']],
["Ganons Tower - Pre-Moldorm Chest", False, [], ['Lamp', 'Fire Rod']],
- ["Ganons Tower - Pre-Moldorm Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp']],
- ["Ganons Tower - Pre-Moldorm Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod']],
+ ["Ganons Tower - Pre-Moldorm Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp']],
+ ["Ganons Tower - Pre-Moldorm Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod']],
["Ganons Tower - Validation Chest", False, []],
["Ganons Tower - Validation Chest", False, [], ['Hookshot']],
@@ -137,8 +141,8 @@ def testGanonsTower(self):
["Ganons Tower - Validation Chest", False, [], ['Big Key (Ganons Tower)']],
["Ganons Tower - Validation Chest", False, [], ['Lamp', 'Fire Rod']],
["Ganons Tower - Validation Chest", False, [], ['Progressive Sword', 'Hammer']],
- ["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Progressive Sword']],
- ["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Progressive Sword']],
- ["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Hammer']],
- ["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Hammer']],
+ ["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Progressive Sword']],
+ ["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Progressive Sword']],
+ ["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Hammer']],
+ ["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Hammer']],
])
\ No newline at end of file
diff --git a/worlds/alttp/test/dungeons/TestIcePalace.py b/worlds/alttp/test/dungeons/TestIcePalace.py
index 3c075fe5ea48..edc9f1fbae9e 100644
--- a/worlds/alttp/test/dungeons/TestIcePalace.py
+++ b/worlds/alttp/test/dungeons/TestIcePalace.py
@@ -72,8 +72,9 @@ def testIcePalace(self):
["Ice Palace - Boss", False, [], ['Big Key (Ice Palace)']],
["Ice Palace - Boss", False, [], ['Fire Rod', 'Bombos']],
["Ice Palace - Boss", False, [], ['Fire Rod', 'Progressive Sword']],
- ["Ice Palace - Boss", True, ['Progressive Glove', 'Big Key (Ice Palace)', 'Fire Rod', 'Hammer', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)']],
- ["Ice Palace - Boss", True, ['Progressive Glove', 'Big Key (Ice Palace)', 'Fire Rod', 'Hammer', 'Cane of Somaria', 'Small Key (Ice Palace)']],
- ["Ice Palace - Boss", True, ['Progressive Glove', 'Big Key (Ice Palace)', 'Bombos', 'Progressive Sword', 'Hammer', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)']],
- ["Ice Palace - Boss", True, ['Progressive Glove', 'Big Key (Ice Palace)', 'Bombos', 'Progressive Sword', 'Hammer', 'Cane of Somaria', 'Small Key (Ice Palace)']],
+ # need hookshot now to reach the right side for the 6th key
+ ["Ice Palace - Boss", True, ['Progressive Glove', 'Big Key (Ice Palace)', 'Fire Rod', 'Hammer', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)', 'Hookshot']],
+ ["Ice Palace - Boss", True, ['Progressive Glove', 'Big Key (Ice Palace)', 'Fire Rod', 'Hammer', 'Cane of Somaria', 'Small Key (Ice Palace)', 'Hookshot']],
+ ["Ice Palace - Boss", True, ['Progressive Glove', 'Big Key (Ice Palace)', 'Bombos', 'Progressive Sword', 'Hammer', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)', 'Hookshot']],
+ ["Ice Palace - Boss", True, ['Progressive Glove', 'Big Key (Ice Palace)', 'Bombos', 'Progressive Sword', 'Hammer', 'Cane of Somaria', 'Small Key (Ice Palace)', 'Hookshot']],
])
\ No newline at end of file
diff --git a/worlds/alttp/test/dungeons/TestSkullWoods.py b/worlds/alttp/test/dungeons/TestSkullWoods.py
index 2dab840cf449..7f97c4d2f823 100644
--- a/worlds/alttp/test/dungeons/TestSkullWoods.py
+++ b/worlds/alttp/test/dungeons/TestSkullWoods.py
@@ -26,18 +26,18 @@ def testSkullWoodsFrontOnly(self):
["Skull Woods - Big Chest", False, [], ['Never in logic']],
["Skull Woods - Compass Chest", False, []],
- ["Skull Woods - Compass Chest", False, ['Small Key (Skull Woods)'], ['Small Key (Skull Woods)']],
- ["Skull Woods - Compass Chest", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)']],
+ ["Skull Woods - Compass Chest", False, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)'], ['Small Key (Skull Woods)']],
+ ["Skull Woods - Compass Chest", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)']],
["Skull Woods - Map Chest", True, []],
["Skull Woods - Pot Prison", False, []],
- ["Skull Woods - Pot Prison", False, ['Small Key (Skull Woods)'], ['Small Key (Skull Woods)']],
- ["Skull Woods - Pot Prison", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)']],
+ ["Skull Woods - Pot Prison", False, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)'], ['Small Key (Skull Woods)']],
+ ["Skull Woods - Pot Prison", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)']],
["Skull Woods - Pinball Room", False, []],
- ["Skull Woods - Pinball Room", False, [], ['Small Key (Skull Woods)']],
- ["Skull Woods - Pinball Room", True, ['Small Key (Skull Woods)']]
+ ["Skull Woods - Pinball Room", False, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)'], ['Small Key (Skull Woods)']],
+ ["Skull Woods - Pinball Room", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)']],
])
def testSkullWoodsLeftOnly(self):
@@ -50,8 +50,8 @@ def testSkullWoodsLeftOnly(self):
["Skull Woods - Compass Chest", True, []],
["Skull Woods - Map Chest", False, []],
- ["Skull Woods - Map Chest", False, [], ['Small Key (Skull Woods)']],
- ["Skull Woods - Map Chest", True, ['Small Key (Skull Woods)']],
+ ["Skull Woods - Map Chest", False, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)'], ['Small Key (Skull Woods)']],
+ ["Skull Woods - Map Chest", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)']],
["Skull Woods - Pot Prison", True, []],
@@ -67,18 +67,18 @@ def testSkullWoodsBackOnly(self):
["Skull Woods - Big Chest", True, ['Big Key (Skull Woods)']],
["Skull Woods - Compass Chest", False, []],
- ["Skull Woods - Compass Chest", False, ['Small Key (Skull Woods)'], ['Small Key (Skull Woods)']],
- ["Skull Woods - Compass Chest", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)']],
+ ["Skull Woods - Compass Chest", False, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)'], ['Small Key (Skull Woods)']],
+ ["Skull Woods - Compass Chest", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)']],
["Skull Woods - Map Chest", True, []],
["Skull Woods - Pot Prison", False, []],
- ["Skull Woods - Pot Prison", False, ['Small Key (Skull Woods)'], ['Small Key (Skull Woods)']],
- ["Skull Woods - Pot Prison", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)']],
+ ["Skull Woods - Pot Prison", False, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)'], ['Small Key (Skull Woods)']],
+ ["Skull Woods - Pot Prison", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)']],
["Skull Woods - Pinball Room", False, []],
- ["Skull Woods - Pinball Room", False, [], ['Small Key (Skull Woods)']],
- ["Skull Woods - Pinball Room", True, ['Small Key (Skull Woods)']]
+ ["Skull Woods - Pinball Room", False, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)'], ['Small Key (Skull Woods)']],
+ ["Skull Woods - Pinball Room", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)']]
])
def testSkullWoodsMiddle(self):
@@ -94,6 +94,6 @@ def testSkullWoodsBack(self):
["Skull Woods - Boss", False, []],
["Skull Woods - Boss", False, [], ['Fire Rod']],
["Skull Woods - Boss", False, [], ['Progressive Sword']],
- ["Skull Woods - Boss", False, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)'], ['Small Key (Skull Woods)']],
- ["Skull Woods - Boss", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Fire Rod', 'Progressive Sword']],
+ ["Skull Woods - Boss", False, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)'], ['Small Key (Skull Woods)']],
+ ["Skull Woods - Boss", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Fire Rod', 'Progressive Sword']],
])
\ No newline at end of file
diff --git a/worlds/alttp/test/dungeons/TestThievesTown.py b/worlds/alttp/test/dungeons/TestThievesTown.py
index a7e20bc52014..01f1570a2581 100644
--- a/worlds/alttp/test/dungeons/TestThievesTown.py
+++ b/worlds/alttp/test/dungeons/TestThievesTown.py
@@ -6,10 +6,6 @@ class TestThievesTown(TestDungeon):
def testThievesTown(self):
self.starting_regions = ['Thieves Town (Entrance)']
self.run_tests([
- ["Thieves' Town - Attic", False, []],
- ["Thieves' Town - Attic", False, [], ['Big Key (Thieves Town)']],
- ["Thieves' Town - Attic", False, [], ['Small Key (Thieves Town)']],
- ["Thieves' Town - Attic", True, ['Big Key (Thieves Town)', 'Small Key (Thieves Town)']],
["Thieves' Town - Big Key Chest", True, []],
@@ -19,6 +15,19 @@ def testThievesTown(self):
["Thieves' Town - Ambush Chest", True, []],
+ ["Thieves' Town - Hallway Pot Key", False, []],
+ ["Thieves' Town - Hallway Pot Key", False, [], ['Big Key (Thieves Town)']],
+ ["Thieves' Town - Hallway Pot Key", True, ['Big Key (Thieves Town)']],
+
+ ["Thieves' Town - Spike Switch Pot Key", False, []],
+ ["Thieves' Town - Spike Switch Pot Key", False, [], ['Big Key (Thieves Town)']],
+ ["Thieves' Town - Spike Switch Pot Key", True, ['Big Key (Thieves Town)']],
+
+ ["Thieves' Town - Attic", False, []],
+ ["Thieves' Town - Attic", False, [], ['Big Key (Thieves Town)']],
+ ["Thieves' Town - Attic", False, [], ['Small Key (Thieves Town)']],
+ ["Thieves' Town - Attic", True, ['Big Key (Thieves Town)', 'Small Key (Thieves Town)']],
+
["Thieves' Town - Big Chest", False, []],
["Thieves' Town - Big Chest", False, [], ['Big Key (Thieves Town)']],
["Thieves' Town - Big Chest", False, [], ['Small Key (Thieves Town)']],
@@ -31,7 +40,6 @@ def testThievesTown(self):
["Thieves' Town - Boss", False, []],
["Thieves' Town - Boss", False, [], ['Big Key (Thieves Town)']],
- ["Thieves' Town - Boss", False, [], ['Small Key (Thieves Town)']],
["Thieves' Town - Boss", False, [], ['Hammer', 'Progressive Sword', 'Cane of Somaria', 'Cane of Byrna']],
["Thieves' Town - Boss", True, ['Small Key (Thieves Town)', 'Big Key (Thieves Town)', 'Hammer']],
["Thieves' Town - Boss", True, ['Small Key (Thieves Town)', 'Big Key (Thieves Town)', 'Progressive Sword']],
diff --git a/worlds/alttp/test/inverted/TestInvertedTurtleRock.py b/worlds/alttp/test/inverted/TestInvertedTurtleRock.py
index 533e3c650f70..fe8979c1ef02 100644
--- a/worlds/alttp/test/inverted/TestInvertedTurtleRock.py
+++ b/worlds/alttp/test/inverted/TestInvertedTurtleRock.py
@@ -18,10 +18,9 @@ def testTurtleRock(self):
["Turtle Rock - Chain Chomps", False, []],
["Turtle Rock - Chain Chomps", False, [], ['Magic Mirror', 'Cane of Somaria']],
- # Item rando only needs 1 key. ER needs to consider the case when the back is accessible, but not the middle (key wasted on Trinexx door)
["Turtle Rock - Chain Chomps", False, ['Small Key (Turtle Rock)'], ['Magic Mirror', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Chain Chomps", True, ['Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Chain Chomps", True, ['Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Chain Chomps", True, ['Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Chain Chomps", True, ['Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
["Turtle Rock - Chain Chomps", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']],
["Turtle Rock - Chain Chomps", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot']],
["Turtle Rock - Chain Chomps", True, ['Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot']],
@@ -55,8 +54,8 @@ def testTurtleRock(self):
["Turtle Rock - Big Chest", False, [], ['Big Key (Turtle Rock)']],
["Turtle Rock - Big Chest", False, [], ['Magic Mirror', 'Cane of Somaria']],
["Turtle Rock - Big Chest", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Magic Mirror', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Cane of Somaria']],
["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Hookshot']],
["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot']],
@@ -66,8 +65,8 @@ def testTurtleRock(self):
["Turtle Rock - Big Key Chest", False, []],
["Turtle Rock - Big Key Chest", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Small Key (Turtle Rock)']],
- ["Turtle Rock - Big Key Chest", True, ['Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Big Key Chest", True, ['Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Big Key Chest", True, ['Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Big Key Chest", True, ['Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
# Mirror in from ledge, use left side entrance, have enough keys to get to the chest
["Turtle Rock - Big Key Chest", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
["Turtle Rock - Big Key Chest", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
@@ -80,8 +79,8 @@ def testTurtleRock(self):
["Turtle Rock - Crystaroller Room", False, [], ['Big Key (Turtle Rock)', 'Lamp']],
["Turtle Rock - Crystaroller Room", False, [], ['Magic Mirror', 'Cane of Somaria']],
["Turtle Rock - Crystaroller Room", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Magic Mirror', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']],
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot']],
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot']],
@@ -98,9 +97,9 @@ def testTurtleRock(self):
["Turtle Rock - Boss", False, [], ['Big Key (Turtle Rock)']],
["Turtle Rock - Boss", False, [], ['Magic Mirror', 'Lamp']],
["Turtle Rock - Boss", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Small Key (Turtle Rock)']],
- ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Flute', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
- ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
- ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Magic Upgrade (1/2)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)','Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
+ ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Flute', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
+ ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
+ ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Magic Upgrade (1/2)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)','Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Hammer', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Flute', 'Magic Mirror', 'Moon Pearl', 'Hookshot', 'Hammer', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']]
])
@@ -115,12 +114,12 @@ def testEyeBridge(self):
[location, False, [], ['Magic Mirror', 'Cane of Somaria']],
[location, False, [], ['Magic Mirror', 'Lamp']],
[location, False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Magic Mirror', 'Small Key (Turtle Rock)']],
- [location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cane of Byrna']],
- [location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cane of Byrna']],
- [location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cape']],
- [location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cape']],
- [location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']],
- [location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']],
+ [location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cane of Byrna']],
+ [location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cane of Byrna']],
+ [location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cape']],
+ [location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cape']],
+ [location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']],
+ [location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']],
# Mirroring into Eye Bridge does not require Cane of Somaria
[location, True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Cane of Byrna']],
diff --git a/worlds/alttp/test/inverted_minor_glitches/TestInvertedTurtleRock.py b/worlds/alttp/test/inverted_minor_glitches/TestInvertedTurtleRock.py
index a25d89a6f4f9..d7b5c9f79788 100644
--- a/worlds/alttp/test/inverted_minor_glitches/TestInvertedTurtleRock.py
+++ b/worlds/alttp/test/inverted_minor_glitches/TestInvertedTurtleRock.py
@@ -20,8 +20,8 @@ def testTurtleRock(self):
["Turtle Rock - Chain Chomps", False, [], ['Magic Mirror', 'Cane of Somaria']],
# Item rando only needs 1 key. ER needs to consider the case when the back is accessible, but not the middle (key wasted on Trinexx door)
["Turtle Rock - Chain Chomps", False, ['Small Key (Turtle Rock)'], ['Magic Mirror', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Chain Chomps", True, ['Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Chain Chomps", True, ['Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Chain Chomps", True, ['Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Chain Chomps", True, ['Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
["Turtle Rock - Chain Chomps", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']],
["Turtle Rock - Chain Chomps", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot']],
["Turtle Rock - Chain Chomps", True, ['Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot']],
@@ -55,8 +55,8 @@ def testTurtleRock(self):
["Turtle Rock - Big Chest", False, [], ['Big Key (Turtle Rock)']],
["Turtle Rock - Big Chest", False, [], ['Magic Mirror', 'Cane of Somaria']],
["Turtle Rock - Big Chest", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Magic Mirror', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Cane of Somaria']],
["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Hookshot']],
["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot']],
@@ -66,8 +66,8 @@ def testTurtleRock(self):
["Turtle Rock - Big Key Chest", False, []],
["Turtle Rock - Big Key Chest", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Small Key (Turtle Rock)']],
- ["Turtle Rock - Big Key Chest", True, ['Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Big Key Chest", True, ['Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Big Key Chest", True, ['Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Big Key Chest", True, ['Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
# Mirror in from ledge, use left side entrance, have enough keys to get to the chest
["Turtle Rock - Big Key Chest", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
["Turtle Rock - Big Key Chest", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
@@ -80,8 +80,8 @@ def testTurtleRock(self):
["Turtle Rock - Crystaroller Room", False, [], ['Big Key (Turtle Rock)', 'Lamp']],
["Turtle Rock - Crystaroller Room", False, [], ['Magic Mirror', 'Cane of Somaria']],
["Turtle Rock - Crystaroller Room", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Magic Mirror', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']],
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot']],
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot']],
@@ -98,9 +98,9 @@ def testTurtleRock(self):
["Turtle Rock - Boss", False, [], ['Big Key (Turtle Rock)']],
["Turtle Rock - Boss", False, [], ['Magic Mirror', 'Lamp']],
["Turtle Rock - Boss", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Small Key (Turtle Rock)']],
- ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Flute', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
- ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
- ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Magic Upgrade (1/2)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)','Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
+ ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Flute', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
+ ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
+ ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Magic Upgrade (1/2)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)','Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Hammer', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Flute', 'Magic Mirror', 'Moon Pearl', 'Hookshot', 'Hammer', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']]
])
@@ -116,12 +116,12 @@ def testEyeBridge(self):
[location, False, [], ['Magic Mirror', 'Cane of Somaria']],
[location, False, [], ['Magic Mirror', 'Lamp']],
[location, False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Magic Mirror', 'Small Key (Turtle Rock)']],
- [location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cane of Byrna']],
- [location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cane of Byrna']],
- [location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cape']],
- [location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cape']],
- [location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']],
- [location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']],
+ [location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cane of Byrna']],
+ [location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cane of Byrna']],
+ [location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cape']],
+ [location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cape']],
+ [location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']],
+ [location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']],
# Mirroring into Eye Bridge does not require Cane of Somaria
[location, True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Cane of Byrna']],
diff --git a/worlds/alttp/test/owg/TestDungeons.py b/worlds/alttp/test/owg/TestDungeons.py
index 284b489b1626..4f878969679a 100644
--- a/worlds/alttp/test/owg/TestDungeons.py
+++ b/worlds/alttp/test/owg/TestDungeons.py
@@ -6,6 +6,7 @@ class TestDungeons(TestVanillaOWG):
def testFirstDungeonChests(self):
self.run_location_tests([
["Hyrule Castle - Map Chest", True, []],
+ ["Hyrule Castle - Map Guard Key Drop", True, []],
["Sanctuary", True, []],
diff --git a/worlds/blasphemous/Rules.py b/worlds/blasphemous/Rules.py
index 2ef36c575e63..4218fa94cf64 100644
--- a/worlds/blasphemous/Rules.py
+++ b/worlds/blasphemous/Rules.py
@@ -367,25 +367,25 @@ def has_boss_strength(name: str) -> bool:
elif boss == "Graveyard":
return (
has_boss_strength("amanecida")
- and state.has_all({"D01BZ07S01[Santos]", "D02Z03S23[E]", "D02Z02S14[W]", "Wall Climb Ability"}, player)
+ and state.has_all({"D01Z06S01[Santos]", "D02Z03S23[E]", "D02Z02S14[W]", "Wall Climb Ability"}, player)
)
elif boss == "Jondo":
return (
has_boss_strength("amanecida")
- and state.has("D01BZ07S01[Santos]", player)
+ and state.has("D01Z06S01[Santos]", player)
and state.has_any({"D20Z01S05[W]", "D20Z01S05[E]"}, player)
and state.has_any({"D03Z01S03[W]", "D03Z01S03[SW]"}, player)
)
elif boss == "Patio":
return (
has_boss_strength("amanecida")
- and state.has_all({"D01BZ07S01[Santos]", "D06Z01S18[E]"}, player)
+ and state.has_all({"D01Z06S01[Santos]", "D06Z01S18[E]"}, player)
and state.has_any({"D04Z01S04[W]", "D04Z01S04[E]", "D04Z01S04[Cherubs]"}, player)
)
elif boss == "Wall":
return (
has_boss_strength("amanecida")
- and state.has_all({"D01BZ07S01[Santos]", "D09BZ01S01[Cell24]"}, player)
+ and state.has_all({"D01Z06S01[Santos]", "D09BZ01S01[Cell24]"}, player)
and state.has_any({"D09Z01S01[W]", "D09Z01S01[E]"}, player)
)
elif boss == "Hall":
@@ -2451,6 +2451,8 @@ def rules(blasphemousworld):
# Items
set_rule(world.get_location("PotSS: 4th meeting with Redento", player),
lambda state: redento(state, blasphemousworld, player, 4))
+ set_rule(world.get_location("PotSS: Amanecida of the Chiselled Steel", player),
+ lambda state: can_beat_boss(state, "Patio", logic, player))
# No doors
@@ -4191,8 +4193,9 @@ def rules(blasphemousworld):
# Items
set_rule(world.get_location("BotSS: Platforming gauntlet", player),
lambda state: (
- state.has("D17BZ02S01[FrontR]", player)
- or state.has_all({"Dash Ability", "Wall Climb Ability"}, player)
+ #state.has("D17BZ02S01[FrontR]", player) or
+ # TODO: actually fix this once door rando is real
+ state.has_all({"Dash Ability", "Wall Climb Ability"}, player)
))
# Doors
set_rule(world.get_entrance("D17BZ02S01[FrontR]", player),
diff --git a/worlds/bumpstik/docs/setup_en.md b/worlds/bumpstik/docs/setup_en.md
index 51334aa27701..e64a6e9f297d 100644
--- a/worlds/bumpstik/docs/setup_en.md
+++ b/worlds/bumpstik/docs/setup_en.md
@@ -1,21 +1,19 @@
## Required Software
-Download the game from the [Bumper Stickers GitHub releases page](https://github.com/FelicitusNeko/FlixelBumpStik/releases).
-
-*A web version will be made available on itch.io at a later time.*
+Download the game from the [Bumper Stickers GitHub releases page](https://github.com/FelicitusNeko/FlixelBumpStik/releases), or from the [Bumper Stickers AP Itch page](https://kewliomzx.itch.io/bumpstik-ap), where you can also play it in your browser.
## Installation Procedures
Simply download the latest version of Bumper Stickers from the link above, and extract it wherever you like.
-- ⚠️ Do not extract Bumper Stickers to Program Files, as this will cause file access issues.
+- ⚠️ It is not recommended to copy this game, or any files, directly into your Program Files folder under Windows.
## Joining a Multiworld Game
-1. Run `BumpStik-AP.exe`.
+1. Run `BumpStikAP.exe`.
2. Select "Archipelago Mode".
3. Enter your server details in the fields provided, and click "Start".
- - ※ If you are connecting to a WSS server (such as archipelago.gg), specify `wss://` in the host name. Otherwise, the game will assume `ws://`.
+ - The game will attempt to automatically detect whether to connect via normal (WS) or secure (WSS) server, but you can specify `ws://` or `wss://` to prioritise one or the other.
## How to play Bumper Stickers (Classic)
diff --git a/worlds/dark_souls_3/Locations.py b/worlds/dark_souls_3/Locations.py
index 4e595ad36ac5..df241a5fd1fb 100644
--- a/worlds/dark_souls_3/Locations.py
+++ b/worlds/dark_souls_3/Locations.py
@@ -77,6 +77,7 @@ def get_name_to_id() -> dict:
"Progressive Items 3",
"Progressive Items 4",
"Progressive Items DLC",
+ "Progressive Items Health",
]
output = {}
@@ -581,11 +582,7 @@ def get_name_to_id() -> dict:
[DS3LocationData(f"Titanite Shard #{i + 1}", "Titanite Shard", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(26)] +
[DS3LocationData(f"Large Titanite Shard #{i + 1}", "Large Titanite Shard", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(28)] +
[DS3LocationData(f"Titanite Slab #{i + 1}", "Titanite Slab", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
- [DS3LocationData(f"Twinkling Titanite #{i + 1}", "Twinkling Titanite", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(15)] +
-
- # Healing
- [DS3LocationData(f"Estus Shard #{i + 1}", "Estus Shard", DS3LocationCategory.HEALTH) for i in range(11)] +
- [DS3LocationData(f"Undead Bone Shard #{i + 1}", "Undead Bone Shard", DS3LocationCategory.HEALTH) for i in range(10)],
+ [DS3LocationData(f"Twinkling Titanite #{i + 1}", "Twinkling Titanite", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(15)],
"Progressive Items 2": [] +
# Items
@@ -683,7 +680,12 @@ def get_name_to_id() -> dict:
[DS3LocationData(f"Dark Gem ${i + 1}", "Dark Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
[DS3LocationData(f"Blood Gem ${i + 1}", "Blood Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] +
[DS3LocationData(f"Blessed Gem ${i + 1}", "Blessed Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
- [DS3LocationData(f"Hollow Gem ${i + 1}", "Hollow Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)]
+ [DS3LocationData(f"Hollow Gem ${i + 1}", "Hollow Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)],
+
+ "Progressive Items Health": [] +
+ # Healing
+ [DS3LocationData(f"Estus Shard #{i + 1}", "Estus Shard", DS3LocationCategory.HEALTH) for i in range(11)] +
+ [DS3LocationData(f"Undead Bone Shard #{i + 1}", "Undead Bone Shard", DS3LocationCategory.HEALTH) for i in range(10)],
}
location_dictionary: Dict[str, DS3LocationData] = {}
diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py
index faf6c2812177..195d319887d5 100644
--- a/worlds/dark_souls_3/__init__.py
+++ b/worlds/dark_souls_3/__init__.py
@@ -46,12 +46,20 @@ class DarkSouls3World(World):
option_definitions = dark_souls_options
topology_present: bool = True
web = DarkSouls3Web()
- data_version = 7
+ data_version = 8
base_id = 100000
enabled_location_categories: Set[DS3LocationCategory]
required_client_version = (0, 4, 2)
item_name_to_id = DarkSouls3Item.get_name_to_id()
location_name_to_id = DarkSouls3Location.get_name_to_id()
+ item_name_groups = {
+ "Cinders": {
+ "Cinders of a Lord - Abyss Watcher",
+ "Cinders of a Lord - Aldrich",
+ "Cinders of a Lord - Yhorm the Giant",
+ "Cinders of a Lord - Lothric Prince"
+ }
+ }
def __init__(self, multiworld: MultiWorld, player: int):
@@ -89,7 +97,7 @@ def generate_early(self):
def create_regions(self):
progressive_location_table = []
- if self.multiworld.enable_progressive_locations[self.player].value:
+ if self.multiworld.enable_progressive_locations[self.player]:
progressive_location_table = [] + \
location_tables["Progressive Items 1"] + \
location_tables["Progressive Items 2"] + \
@@ -99,6 +107,9 @@ def create_regions(self):
if self.multiworld.enable_dlc[self.player].value:
progressive_location_table += location_tables["Progressive Items DLC"]
+ if self.multiworld.enable_health_upgrade_locations[self.player]:
+ progressive_location_table += location_tables["Progressive Items Health"]
+
# Create Vanilla Regions
regions: Dict[str, Region] = {}
regions["Menu"] = self.create_region("Menu", progressive_location_table)
@@ -502,6 +513,15 @@ def fill_slot_data(self) -> Dict[str, object]:
slot_data = {
"options": {
+ "enable_weapon_locations": self.multiworld.enable_weapon_locations[self.player].value,
+ "enable_shield_locations": self.multiworld.enable_shield_locations[self.player].value,
+ "enable_armor_locations": self.multiworld.enable_armor_locations[self.player].value,
+ "enable_ring_locations": self.multiworld.enable_ring_locations[self.player].value,
+ "enable_spell_locations": self.multiworld.enable_spell_locations[self.player].value,
+ "enable_key_locations": self.multiworld.enable_key_locations[self.player].value,
+ "enable_boss_locations": self.multiworld.enable_boss_locations[self.player].value,
+ "enable_npc_locations": self.multiworld.enable_npc_locations[self.player].value,
+ "enable_misc_locations": self.multiworld.enable_misc_locations[self.player].value,
"auto_equip": self.multiworld.auto_equip[self.player].value,
"lock_equip": self.multiworld.lock_equip[self.player].value,
"no_weapon_requirements": self.multiworld.no_weapon_requirements[self.player].value,
diff --git a/worlds/dark_souls_3/docs/en_Dark Souls III.md b/worlds/dark_souls_3/docs/en_Dark Souls III.md
index 3ad8236ccfae..e844925df1ea 100644
--- a/worlds/dark_souls_3/docs/en_Dark Souls III.md
+++ b/worlds/dark_souls_3/docs/en_Dark Souls III.md
@@ -7,20 +7,22 @@ config file.
## What does randomization do to this game?
-In Dark Souls III, all unique items you can earn from a static corpse, a chest or the death of a Boss/NPC are
-randomized.
-An option is available from the settings page to also randomize the upgrade materials, the Estus shards and the
-consumables.
-Another option is available to randomize the level of the generated weapons(from +0 to +10/+5)
+Items that can be picked up from static corpses, taken from chests, or earned from defeating enemies or NPCs can be
+randomized. Common pickups like titanite shards or firebombs can be randomized as "progressive" items. That is, the
+location "Titanite Shard #5" is the fifth titanite shard you pick up, no matter where it was from. This is also what
+happens when you randomize Estus Shards and Undead Bone Shards.
-To beat the game you need to collect the 4 "Cinders of a Lord" randomized in the multiworld
-and kill the final boss "Soul of Cinder"
+It's also possible to randomize the upgrade level of weapons and shields as well as their infusions (if they can have
+one). Additionally, there are settings that can make the randomized experience more convenient or more interesting, such as
+removing weapon requirements or auto-equipping whatever equipment you most recently received.
+
+The goal is to find the four "Cinders of a Lord" items randomized into the multiworld and defeat the Soul of Cinder.
## What Dark Souls III items can appear in other players' worlds?
-Every unique item from Dark Souls III can appear in other player's worlds, such as a piece of armor, an upgraded weapon,
-or a key item.
+Practically anything can be found in other worlds including pieces of armor, upgraded weapons, key items, consumables,
+spells, upgrade materials, etc...
## What does another world's item look like in Dark Souls III?
-In Dark Souls III, items which need to be sent to other worlds appear as a Prism Stone.
+In Dark Souls III, items which are sent to other worlds appear as Prism Stones.
diff --git a/worlds/dkc3/docs/setup_en.md b/worlds/dkc3/docs/setup_en.md
index 56ef80d4a55f..bb1075630016 100644
--- a/worlds/dkc3/docs/setup_en.md
+++ b/worlds/dkc3/docs/setup_en.md
@@ -87,8 +87,7 @@ first time launching, you may be prompted to allow it to communicate through the
3. Click on **New Lua Script Window...**
4. In the new window, click **Browse...**
5. Select the connector lua file included with your client
- - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
- emulator is 64-bit or 32-bit.
+ - Look in the Archipelago folder for `/SNI/lua/Connector.lua`.
6. If you see an error while loading the script that states `socket.dll missing` or similar, navigate to the folder of
the lua you are using in your file explorer and copy the `socket.dll` to the base folder of your snes9x install.
@@ -100,8 +99,7 @@ the lua you are using in your file explorer and copy the `socket.dll` to the bas
2. Load your ROM file if it hasn't already been loaded.
If you changed your core preference after loading the ROM, don't forget to reload it (default hotkey: Ctrl+R).
3. Drag+drop the `Connector.lua` file included with your client onto the main EmuHawk window.
- - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
- emulator is 64-bit or 32-bit. Please note the most recent versions of BizHawk are 64-bit only.
+ - Look in the Archipelago folder for `/SNI/lua/Connector.lua`.
- You could instead open the Lua Console manually, click `Script` 〉 `Open Script`, and navigate to `Connector.lua`
with the file picker.
diff --git a/worlds/doom_1993/Locations.py b/worlds/doom_1993/Locations.py
index 942c7d2a42d4..778efb4661a8 100644
--- a/worlds/doom_1993/Locations.py
+++ b/worlds/doom_1993/Locations.py
@@ -1794,13 +1794,13 @@ class LocationDict(TypedDict, total=False):
'map': 7,
'index': 65,
'doom_type': 2004,
- 'region': "Limbo (E3M7) Red"},
+ 'region': "Limbo (E3M7) Green"},
351297: {'name': 'Limbo (E3M7) - Armor',
'episode': 3,
'map': 7,
'index': 67,
'doom_type': 2018,
- 'region': "Limbo (E3M7) Red"},
+ 'region': "Limbo (E3M7) Green"},
351298: {'name': 'Limbo (E3M7) - Yellow skull key',
'episode': 3,
'map': 7,
@@ -2496,19 +2496,19 @@ class LocationDict(TypedDict, total=False):
'map': 6,
'index': 77,
'doom_type': 38,
- 'region': "Against Thee Wickedly (E4M6) Yellow"},
+ 'region': "Against Thee Wickedly (E4M6) Magenta"},
351414: {'name': 'Against Thee Wickedly (E4M6) - Invulnerability',
'episode': 4,
'map': 6,
'index': 78,
'doom_type': 2022,
- 'region': "Against Thee Wickedly (E4M6) Red"},
+ 'region': "Against Thee Wickedly (E4M6) Pink"},
351415: {'name': 'Against Thee Wickedly (E4M6) - Invulnerability 2',
'episode': 4,
'map': 6,
'index': 89,
'doom_type': 2022,
- 'region': "Against Thee Wickedly (E4M6) Red"},
+ 'region': "Against Thee Wickedly (E4M6) Magenta"},
351416: {'name': 'Against Thee Wickedly (E4M6) - BFG9000',
'episode': 4,
'map': 6,
@@ -2520,7 +2520,7 @@ class LocationDict(TypedDict, total=False):
'map': 6,
'index': 102,
'doom_type': 8,
- 'region': "Against Thee Wickedly (E4M6) Red"},
+ 'region': "Against Thee Wickedly (E4M6) Pink"},
351418: {'name': 'Against Thee Wickedly (E4M6) - Berserk',
'episode': 4,
'map': 6,
@@ -2550,7 +2550,7 @@ class LocationDict(TypedDict, total=False):
'map': 6,
'index': -1,
'doom_type': -1,
- 'region': "Against Thee Wickedly (E4M6) Red"},
+ 'region': "Against Thee Wickedly (E4M6) Magenta"},
351423: {'name': 'And Hell Followed (E4M7) - Shotgun',
'episode': 4,
'map': 7,
@@ -2628,7 +2628,7 @@ class LocationDict(TypedDict, total=False):
'map': 7,
'index': 182,
'doom_type': 39,
- 'region': "And Hell Followed (E4M7) Main"},
+ 'region': "And Hell Followed (E4M7) Blue"},
351436: {'name': 'And Hell Followed (E4M7) - Red skull key',
'episode': 4,
'map': 7,
@@ -3414,6 +3414,7 @@ class LocationDict(TypedDict, total=False):
"Command Control (E1M4) - Supercharge",
"Command Control (E1M4) - Mega Armor",
"Containment Area (E2M2) - Supercharge",
+ "Containment Area (E2M2) - Plasma gun",
"Pandemonium (E3M3) - Mega Armor",
"House of Pain (E3M4) - Chaingun",
"House of Pain (E3M4) - Invulnerability",
diff --git a/worlds/doom_1993/Regions.py b/worlds/doom_1993/Regions.py
index 58626e62ae25..602c29f5bd83 100644
--- a/worlds/doom_1993/Regions.py
+++ b/worlds/doom_1993/Regions.py
@@ -394,7 +394,8 @@ class RegionDict(TypedDict, total=False):
"episode":3,
"connections":[
"Limbo (E3M7) Red",
- "Limbo (E3M7) Blue"]},
+ "Limbo (E3M7) Blue",
+ "Limbo (E3M7) Pink"]},
{"name":"Limbo (E3M7) Blue",
"connects_to_hub":False,
"episode":3,
@@ -404,11 +405,24 @@ class RegionDict(TypedDict, total=False):
"episode":3,
"connections":[
"Limbo (E3M7) Main",
- "Limbo (E3M7) Yellow"]},
+ "Limbo (E3M7) Yellow",
+ "Limbo (E3M7) Green"]},
{"name":"Limbo (E3M7) Yellow",
"connects_to_hub":False,
"episode":3,
"connections":["Limbo (E3M7) Red"]},
+ {"name":"Limbo (E3M7) Pink",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[
+ "Limbo (E3M7) Green",
+ "Limbo (E3M7) Main"]},
+ {"name":"Limbo (E3M7) Green",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[
+ "Limbo (E3M7) Pink",
+ "Limbo (E3M7) Red"]},
# Dis (E3M8)
{"name":"Dis (E3M8) Main",
@@ -529,19 +543,32 @@ class RegionDict(TypedDict, total=False):
{"name":"Against Thee Wickedly (E4M6) Main",
"connects_to_hub":True,
"episode":4,
+ "connections":["Against Thee Wickedly (E4M6) Blue"]},
+ {"name":"Against Thee Wickedly (E4M6) Red",
+ "connects_to_hub":False,
+ "episode":4,
"connections":[
"Against Thee Wickedly (E4M6) Blue",
+ "Against Thee Wickedly (E4M6) Pink",
+ "Against Thee Wickedly (E4M6) Main"]},
+ {"name":"Against Thee Wickedly (E4M6) Blue",
+ "connects_to_hub":False,
+ "episode":4,
+ "connections":[
+ "Against Thee Wickedly (E4M6) Main",
"Against Thee Wickedly (E4M6) Yellow",
"Against Thee Wickedly (E4M6) Red"]},
- {"name":"Against Thee Wickedly (E4M6) Red",
+ {"name":"Against Thee Wickedly (E4M6) Magenta",
"connects_to_hub":False,
"episode":4,
"connections":["Against Thee Wickedly (E4M6) Main"]},
- {"name":"Against Thee Wickedly (E4M6) Blue",
+ {"name":"Against Thee Wickedly (E4M6) Yellow",
"connects_to_hub":False,
"episode":4,
- "connections":["Against Thee Wickedly (E4M6) Main"]},
- {"name":"Against Thee Wickedly (E4M6) Yellow",
+ "connections":[
+ "Against Thee Wickedly (E4M6) Blue",
+ "Against Thee Wickedly (E4M6) Magenta"]},
+ {"name":"Against Thee Wickedly (E4M6) Pink",
"connects_to_hub":False,
"episode":4,
"connections":["Against Thee Wickedly (E4M6) Main"]},
diff --git a/worlds/doom_1993/Rules.py b/worlds/doom_1993/Rules.py
index 6f24112cbefa..6e13a8af34ce 100644
--- a/worlds/doom_1993/Rules.py
+++ b/worlds/doom_1993/Rules.py
@@ -50,9 +50,6 @@ def set_episode1_rules(player, world):
set_rule(world.get_entrance("Command Control (E1M4) Blue -> Command Control (E1M4) Main", player), lambda state:
state.has("Command Control (E1M4) - Yellow keycard", player, 1) or
state.has("Command Control (E1M4) - Blue keycard", player, 1))
- set_rule(world.get_entrance("Command Control (E1M4) Yellow -> Command Control (E1M4) Main", player), lambda state:
- state.has("Command Control (E1M4) - Yellow keycard", player, 1) or
- state.has("Command Control (E1M4) - Blue keycard", player, 1))
# Phobos Lab (E1M5)
set_rule(world.get_entrance("Hub -> Phobos Lab (E1M5) Main", player), lambda state:
@@ -354,8 +351,12 @@ def set_episode3_rules(player, world):
state.has("Limbo (E3M7) - Red skull key", player, 1))
set_rule(world.get_entrance("Limbo (E3M7) Main -> Limbo (E3M7) Blue", player), lambda state:
state.has("Limbo (E3M7) - Blue skull key", player, 1))
+ set_rule(world.get_entrance("Limbo (E3M7) Main -> Limbo (E3M7) Pink", player), lambda state:
+ state.has("Limbo (E3M7) - Blue skull key", player, 1))
set_rule(world.get_entrance("Limbo (E3M7) Red -> Limbo (E3M7) Yellow", player), lambda state:
state.has("Limbo (E3M7) - Yellow skull key", player, 1))
+ set_rule(world.get_entrance("Limbo (E3M7) Pink -> Limbo (E3M7) Green", player), lambda state:
+ state.has("Limbo (E3M7) - Red skull key", player, 1))
# Dis (E3M8)
set_rule(world.get_entrance("Hub -> Dis (E3M8) Main", player), lambda state:
@@ -466,12 +467,10 @@ def set_episode4_rules(player, world):
state.has("BFG9000", player, 1)))
set_rule(world.get_entrance("Against Thee Wickedly (E4M6) Main -> Against Thee Wickedly (E4M6) Blue", player), lambda state:
state.has("Against Thee Wickedly (E4M6) - Blue skull key", player, 1))
- set_rule(world.get_entrance("Against Thee Wickedly (E4M6) Main -> Against Thee Wickedly (E4M6) Yellow", player), lambda state:
+ set_rule(world.get_entrance("Against Thee Wickedly (E4M6) Blue -> Against Thee Wickedly (E4M6) Yellow", player), lambda state:
state.has("Against Thee Wickedly (E4M6) - Yellow skull key", player, 1))
- set_rule(world.get_entrance("Against Thee Wickedly (E4M6) Main -> Against Thee Wickedly (E4M6) Red", player), lambda state:
+ set_rule(world.get_entrance("Against Thee Wickedly (E4M6) Blue -> Against Thee Wickedly (E4M6) Red", player), lambda state:
state.has("Against Thee Wickedly (E4M6) - Red skull key", player, 1))
- set_rule(world.get_entrance("Against Thee Wickedly (E4M6) Blue -> Against Thee Wickedly (E4M6) Main", player), lambda state:
- state.has("Against Thee Wickedly (E4M6) - Blue skull key", player, 1))
# And Hell Followed (E4M7)
set_rule(world.get_entrance("Hub -> And Hell Followed (E4M7) Main", player), lambda state:
diff --git a/worlds/doom_1993/docs/setup_en.md b/worlds/doom_1993/docs/setup_en.md
index 5afe6d3e26e4..cfd97f623a0c 100644
--- a/worlds/doom_1993/docs/setup_en.md
+++ b/worlds/doom_1993/docs/setup_en.md
@@ -11,9 +11,9 @@
## Installing AP Doom
1. Download [APDOOM.zip](https://github.com/Daivuk/apdoom/releases) and extract it.
-2. Copy DOOM.WAD from your steam install into the extracted folder.
+2. Copy `DOOM.WAD` from your game's installation directory into the newly extracted folder.
You can find the folder in steam by finding the game in your library,
- right clicking it and choosing *Manage→Browse Local Files*.
+ right-clicking it and choosing **Manage -> Browse Local Files**.
## Joining a MultiWorld Game
diff --git a/worlds/factorio/Client.py b/worlds/factorio/Client.py
index 58dbb6df83dd..050455bb076a 100644
--- a/worlds/factorio/Client.py
+++ b/worlds/factorio/Client.py
@@ -446,6 +446,10 @@ async def factorio_spinup_server(ctx: FactorioContext) -> bool:
logger.warning("It appears your mods are loaded from Appdata, "
"this can lead to problems with multiple Factorio instances. "
"If this is the case, you will get a file locked error running Factorio.")
+ elif "Couldn't create lock file" in msg:
+ raise Exception(f"This Factorio (at {executable}) is either already running, "
+ "or a Factorio sharing data directories is already running. "
+ "Server could not start up.")
if not rcon_client and "Starting RCON interface at IP ADDR:" in msg:
rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
if ctx.mod_version == ctx.__class__.mod_version:
diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py
index 0331c2d013ea..18eee67e036f 100644
--- a/worlds/factorio/Options.py
+++ b/worlds/factorio/Options.py
@@ -146,8 +146,8 @@ class TechTreeLayout(Choice):
class TechTreeInformation(Choice):
"""How much information should be displayed in the tech tree.
- None: No indication what a research unlocks
- Advancement: Indicators which researches unlock items that are considered logical advancements
+ None: No indication of what a research unlocks.
+ Advancement: Indicates if a research unlocks an item that is considered logical advancement, but not who it is for.
Full: Labels with exact names and recipients of unlocked items; all researches are prefilled into the !hint command.
"""
display_name = "Technology Tree Information"
@@ -390,8 +390,8 @@ class FactorioWorldGen(OptionDict):
def __init__(self, value: typing.Dict[str, typing.Any]):
advanced = {"pollution", "enemy_evolution", "enemy_expansion"}
self.value = {
- "basic": {key: value[key] for key in value.keys() - advanced},
- "advanced": {key: value[key] for key in value.keys() & advanced}
+ "basic": {k: v for k, v in value.items() if k not in advanced},
+ "advanced": {k: v for k, v in value.items() if k in advanced}
}
# verify min_values <= max_values
diff --git a/worlds/factorio/Technologies.py b/worlds/factorio/Technologies.py
index d68c6f2f779e..096396c0e774 100644
--- a/worlds/factorio/Technologies.py
+++ b/worlds/factorio/Technologies.py
@@ -1,6 +1,6 @@
from __future__ import annotations
-import json
+import orjson
import logging
import os
import string
@@ -20,7 +20,7 @@
def load_json_data(data_name: str) -> Union[List[str], Dict[str, Any]]:
- return json.loads(pkgutil.get_data(__name__, "data/" + data_name + ".json").decode())
+ return orjson.loads(pkgutil.get_data(__name__, "data/" + data_name + ".json"))
techs_future = pool.submit(load_json_data, "techs")
diff --git a/worlds/factorio/requirements.txt b/worlds/factorio/requirements.txt
index c45fb771da6a..8fb74e933045 100644
--- a/worlds/factorio/requirements.txt
+++ b/worlds/factorio/requirements.txt
@@ -1 +1,2 @@
factorio-rcon-py>=2.0.1
+orjson>=3.9.7
diff --git a/worlds/ff1/docs/en_Final Fantasy.md b/worlds/ff1/docs/en_Final Fantasy.md
index a62b5ec12605..29d4d29f8094 100644
--- a/worlds/ff1/docs/en_Final Fantasy.md
+++ b/worlds/ff1/docs/en_Final Fantasy.md
@@ -8,19 +8,19 @@ website: [FF1R Website](https://finalfantasyrandomizer.com/)
## What does randomization do to this game?
-A better questions is what isn't randomized at this point. Enemies stats and spell, character spells, shop inventory and
-boss stats and spells are all commonly randomized. Unlike most other randomizers it is also most standard to shuffle
-progression items and non-progression items into separate pools and then redistribute them to their respective
-locations. So, for example, Princess Sarah may have the CANOE instead of the LUTE; however, she will never have a Heal
-Pot or some armor. There are plenty of other things that can be randomized on the main randomizer
-site: [FF1R Website](https://finalfantasyrandomizer.com/)
+Enemy stats and spell, boss stats and spells, character spells, and shop inventories are all commonly randomized. Unlike
+most other randomizers, it is standard to shuffle progression items and non-progression items into separate pools
+and then redistribute them to their respective locations. For example, Princess Sarah may have the CANOE instead
+of the LUTE; however, she will never have a Heal Pot or armor.
+
+Plenty of other things to be randomized can be found on the main randomizer site:
+[FF1R Website](https://finalfantasyrandomizer.com/)
## What Final Fantasy items can appear in other players' worlds?
-All items can appear in other players worlds. This includes consumables, shards, weapons, armor and, of course, key
-items.
+All items can appear in other players worlds, including consumables, shards, weapons, armor, and key items.
## What does another world's item look like in Final Fantasy
-All local and remote items appear the same. It will say that you received an item and then BOTH the client log and the
+All local and remote items appear the same. Final Fantasy will say that you received an item, then BOTH the client log and the
emulator will display what was found external to the in-game text box.
diff --git a/worlds/ff1/docs/multiworld_en.md b/worlds/ff1/docs/multiworld_en.md
index 51fcd9b7bfc4..d3dc457f01be 100644
--- a/worlds/ff1/docs/multiworld_en.md
+++ b/worlds/ff1/docs/multiworld_en.md
@@ -32,14 +32,14 @@ Generate a game by going to the site and performing the following steps:
prefer, or it is your first time we suggest starting with the 'Shard Hunt' preset (which requires you to collect a
number of shards to go to the end dungeon) or the 'Beginner' preset if you prefer to kill the original fiends.
2. Go to the `Goal` tab and ensure `Archipelago` is enabled. Set your player name to any name that represents you.
-3. Upload you `Final Fantasy(USA).nes` (and click `Remember ROM` for the future!)
+3. Upload your `Final Fantasy(USA).nes` (and click `Remember ROM` for the future!)
4. Press the `NEW` button beside `Seed` a few times
5. Click `GENERATE ROM`
-It should download two files. One is the `*.nes` file which your emulator will run and the other is the yaml file
+It should download two files. One is the `*.nes` file which your emulator will run, and the other is the yaml file
required by Archipelago.gg
-At this point you are ready to join the multiworld. If you are uncertain on how to generate, host or join a multiworld
+At this point, you are ready to join the multiworld. If you are uncertain on how to generate, host, or join a multiworld,
please refer to the [game agnostic setup guide](/tutorial/Archipelago/setup/en).
## Running the Client Program and Connecting to the Server
@@ -67,7 +67,7 @@ Once the Archipelago server has been hosted:
## Play the game
-When the client shows both NES and server are connected you are good to go. You can check the connection status of the
+When the client shows both NES and server are connected, you are good to go. You can check the connection status of the
NES at any time by running `/nes`
### Other Client Commands
diff --git a/worlds/hk/docs/setup_en.md b/worlds/hk/docs/setup_en.md
index e25e6bc4ac6f..adf975ff515e 100644
--- a/worlds/hk/docs/setup_en.md
+++ b/worlds/hk/docs/setup_en.md
@@ -4,13 +4,10 @@
* Download and unzip the Scarab+ Mod Manager from the [Scarab+ website](https://themulhima.github.io/Scarab/).
* A legal copy of Hollow Knight.
-## Optional Software
-* Archipelago Map Mod from Scarab+
- * Ensure that both RandoMapMod and MapChanger are uninstalled or disabled as they are incompatible with Archipelago Map Mod.
-
-## Installing the Archipelago Mod using Scarab
+## Installing the Archipelago Mod using Scarab+
1. Launch Scarab+ and ensure it locates your Hollow Knight installation directory.
2. Click the "Install" button near the "Archipelago" mod entry.
+ * If desired, also install "Archipelago Map Mod" to use as an in-game tracker.
3. Launch the game, you're all set!
### What to do if Scarab+ fails to find your XBox Game Pass installation directory
diff --git a/worlds/hylics2/Items.py b/worlds/hylics2/Items.py
index e09144c6cc99..4556a4a6748b 100644
--- a/worlds/hylics2/Items.py
+++ b/worlds/hylics2/Items.py
@@ -98,7 +98,7 @@ class ItemDict(TypedDict):
'count': 4,
'name': 'MULTI-JUICE'},
200651: {'classification': ItemClassification.filler,
- 'count': 1,
+ 'count': 3,
'name': 'MULTI STEM CELL'},
200652: {'classification': ItemClassification.filler,
'count': 6,
diff --git a/worlds/hylics2/Locations.py b/worlds/hylics2/Locations.py
index 80e02b1c7169..053bfbd5856c 100644
--- a/worlds/hylics2/Locations.py
+++ b/worlds/hylics2/Locations.py
@@ -220,6 +220,10 @@ class LocationDict(TypedDict, total=False):
'region': 15},
200754: {'name': "Sage Labyrinth: 2F Sarcophagus",
'region': 15},
+ 200786: {'name': "Sage Labyrinth: Boss Secret Chest 1",
+ 'region': 15},
+ 200787: {'name': "Sage Labyrinth: Boss Secret Chest 2",
+ 'region': 15},
200725: {'name': "Sage Labyrinth: Motor Hunter Sarcophagus",
'region': 15},
200726: {'name': "Sage Labyrinth: Sage Item 1",
diff --git a/worlds/hylics2/__init__.py b/worlds/hylics2/__init__.py
index 11401b11371e..f721fb474923 100644
--- a/worlds/hylics2/__init__.py
+++ b/worlds/hylics2/__init__.py
@@ -36,7 +36,7 @@ class Hylics2World(World):
topology_present: bool = True
- data_version = 2
+ data_version = 3
start_location = "Waynehouse"
diff --git a/worlds/kh2/docs/en_Kingdom Hearts 2.md b/worlds/kh2/docs/en_Kingdom Hearts 2.md
index d132b29ca4dd..8258a099cc95 100644
--- a/worlds/kh2/docs/en_Kingdom Hearts 2.md
+++ b/worlds/kh2/docs/en_Kingdom Hearts 2.md
@@ -2,7 +2,7 @@
Changes from the vanilla game
-This randomizer takes Kingdom Hearts 2 and randomizes the locations of the items for a more dynamic play experience. The items that randomize currently are all items within Chests, Popups, Get Bonuses, Form Levels, and Sora's Levels. This allows abilities that Sora would normally have to also be placed on Keyblades with random stats. With several options on ways to finish the game.
+This randomizer creates a more dynamic play experience by randomizing the locations of most items in Kingdom Hearts 2. Currently all items within Chests, Popups, Get Bonuses, Form Levels, and Sora's Levels are randomized. This allows abilities that Sora would normally have to be placed on Keyblades with random stats. Additionally, there are several options for ways to finish the game, allowing for different goals beyond beating the final boss.
Where is the settings page
@@ -12,12 +12,18 @@ The [player settings page for this game](../player-settings) contains all the op
What is randomized in this game?
-The Chests, Popups, Get Bonuses, Form Levels, and Sora's Levels.
+- Chests
+- Popups
+- Get Bonuses
+- Form Levels
+- Sora's Levels
+- Keyblade Stats
+- Keyblade Abilities
What Kingdom Hearts 2 items can appear in other players' worlds?
-Every item in the game with the exception being party members' abilities.
+Every item in the game except for party members' abilities.
What is The Garden of Assemblage "GoA"?
@@ -37,10 +43,10 @@ It is added to your inventory. If you obtain magic, you will need to pause your
What Happens if I die before Room Saving?
-When you die in Kingdom Hearts 2, you are reverted to the last non-boss room you entered and your status is reverted to what it was at that time. However, in archipelago, any item that you have sent/received will not be taken away from the player, any chest you have opened will remain open, and you will keep your level but lose the expereince. Unlike vanilla Kingdom Hearts 2.
+When you die in vanilla Kingdom Hearts 2, you are reverted to the last non-boss room you entered and your status is reverted to what it was at that time. However, in archipelago, any item that you have sent/received will not be taken away from the player, any chest you have opened will remain open, and you will keep your level, but lose the experience.
-For example, if you are fighting Roxas and you receive Reflect Element and you die fighting Roxas, you will keep that reflect. You will still need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable.
+For example, if you are fighting Roxas, receive Reflect Element, then die mid-fight, you will keep that Reflect Element. You will still need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable.
Customization options:
@@ -49,13 +55,12 @@ For example, if you are fighting Roxas and you receive Reflect Element and you d
1. Obtain Three Proofs.
2. Obtain a desired amount of Lucky Emblems.
3. Obtain a desired amount of Bounties that are on late locations.
-- Customize how many World Locking Items You Need to Progress in that World.
-- Customize the Amount of World Locking Items You Start With.
-- Customize how many locations you want on Sora's Levels.
-- Customize the EXP Multiplier of everything that affects Sora.
-- Customize the Available Abilities on Keyblades.
-- Customize the level of Progressive Movement (Growth Abilities) you start with.
-- Customize the amount of Progressive Movement (Growth Abilities) you start with.
+- Customize how many World-Locking Items you need to progress in that world.
+- Customize the amount of World-Locking Items you start with.
+- Customize how many of Sora's Levels are locations.
+- Customize the EXP multiplier for Sora, his Drive Forms, and his Summons.
+- Customize the available abilities on keyblades.
+- Customize the amount and level of progressive movement (Growth Abilities) you start with.
- Customize start inventory, i.e., begin every run with certain items or spells of your choice.
Quality of life:
diff --git a/worlds/lufia2ac/docs/setup_en.md b/worlds/lufia2ac/docs/setup_en.md
index f9e0d3725cfb..4236c26e8a70 100644
--- a/worlds/lufia2ac/docs/setup_en.md
+++ b/worlds/lufia2ac/docs/setup_en.md
@@ -82,8 +82,7 @@ first time launching, you may be prompted to allow it to communicate through the
3. Click on **New Lua Script Window...**
4. In the new window, click **Browse...**
5. Select the connector lua file included with your client
- - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
- emulator is 64-bit or 32-bit.
+ - Look in the Archipelago folder for `/SNI/lua/Connector.lua`.
6. If you see an error while loading the script that states `socket.dll missing` or similar, navigate to the folder of
the lua you are using in your file explorer and copy the `socket.dll` to the base folder of your snes9x install.
@@ -94,9 +93,8 @@ the lua you are using in your file explorer and copy the `socket.dll` to the bas
- (≥ 2.9) `Config` 〉 `Preferred Cores` 〉 `SNES` 〉 `BSNESv115+`
2. Load your ROM file if it hasn't already been loaded.
If you changed your core preference after loading the ROM, don't forget to reload it (default hotkey: Ctrl+R).
-3. Drag+drop the `Connector.lua` file that you downloaded above onto the main EmuHawk window.
- - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
- emulator is 64-bit or 32-bit. Please note the most recent versions of BizHawk are 64-bit only.
+3. Drag+drop the `Connector.lua` file included with your client onto the main EmuHawk window.
+ - Look in the Archipelago folder for `/SNI/lua/Connector.lua`.
- You could instead open the Lua Console manually, click `Script` 〉 `Open Script`, and navigate to `Connector.lua`
with the file picker.
diff --git a/worlds/messenger/Rules.py b/worlds/messenger/Rules.py
index b72d454a7e0f..c24f60fbaadb 100644
--- a/worlds/messenger/Rules.py
+++ b/worlds/messenger/Rules.py
@@ -182,8 +182,10 @@ def __init__(self, world: MessengerWorld) -> None:
"Searing Crags Seal - Raining Rocks": lambda state: self.has_vertical(state) or self.can_destroy_projectiles(state),
"Searing Crags Seal - Rhythm Rocks": lambda state: self.has_vertical(state) or self.can_destroy_projectiles(state),
"Searing Crags - Power Thistle": lambda state: self.has_vertical(state) or self.can_destroy_projectiles(state),
- "Glacial Peak Seal - Ice Climbers": self.has_vertical,
+ "Glacial Peak Seal - Ice Climbers": lambda state: self.has_vertical(state) or self.can_dboost(state),
"Glacial Peak Seal - Projectile Spike Pit": self.true,
+ "Glacial Peak Seal - Glacial Air Swag": lambda state: self.has_windmill(state) or self.has_vertical(state),
+ "Glacial Peak Mega Shard": lambda state: self.has_windmill(state) or self.has_vertical(state),
"Cloud Ruins Seal - Ghost Pit": self.true,
"Bamboo Creek - Claustro": self.has_wingsuit,
"Tower of Time Seal - Lantern Climb": self.has_wingsuit,
@@ -201,10 +203,7 @@ def __init__(self, world: MessengerWorld) -> None:
"Elemental Skylands - Key of Symbiosis": lambda state: self.has_windmill(state) or self.can_dboost(state),
"Autumn Hills Seal - Spike Ball Darts": lambda state: (self.has_dart(state) and self.has_windmill(state))
or self.has_wingsuit(state),
- "Glacial Peak Seal - Glacial Air Swag": self.has_windmill,
- "Glacial Peak Seal - Ice Climbers": lambda state: self.has_wingsuit(state) or self.can_dboost(state),
- "Underworld Seal - Fireball Wave": lambda state: state.has_all({"Lightfoot Tabi", "Windmill Shuriken"},
- self.player),
+ "Underworld Seal - Fireball Wave": self.has_windmill,
}
def has_windmill(self, state: CollectionState) -> bool:
diff --git a/worlds/messenger/test/TestLogic.py b/worlds/messenger/test/TestLogic.py
index 45b0d0dab629..932bc1386701 100644
--- a/worlds/messenger/test/TestLogic.py
+++ b/worlds/messenger/test/TestLogic.py
@@ -1,3 +1,5 @@
+from typing import Iterable, List
+
from BaseClasses import ItemClassification
from . import MessengerTestBase
@@ -5,6 +7,7 @@
class HardLogicTest(MessengerTestBase):
options = {
"logic_level": "hard",
+ "shuffle_shards": "true",
}
def testVertical(self) -> None:
@@ -19,16 +22,20 @@ def testVertical(self) -> None:
"Autumn Hills - Climbing Claws", "Autumn Hills - Key of Hope", "Autumn Hills - Leaf Golem",
"Autumn Hills Seal - Trip Saws", "Autumn Hills Seal - Double Swing Saws",
"Autumn Hills Seal - Spike Ball Swing", "Autumn Hills Seal - Spike Ball Darts",
+ "Autumn Hills Mega Shard", "Hidden Entrance Mega Shard",
# forlorn temple
"Forlorn Temple - Demon King",
"Forlorn Temple Seal - Rocket Maze", "Forlorn Temple Seal - Rocket Sunset",
+ "Sunny Day Mega Shard", "Down Under Mega Shard",
# catacombs
"Catacombs - Necro", "Catacombs - Ruxxtin's Amulet", "Catacombs - Ruxxtin",
"Catacombs Seal - Triple Spike Crushers", "Catacombs Seal - Crusher Gauntlet", "Catacombs Seal - Dirty Pond",
+ "Catacombs Mega Shard",
# bamboo creek
"Bamboo Creek - Claustro",
"Bamboo Creek Seal - Spike Crushers and Doors", "Bamboo Creek Seal - Spike Ball Pits",
"Bamboo Creek Seal - Spike Crushers and Doors v2",
+ "Above Entrance Mega Shard", "Abandoned Mega Shard", "Time Loop Mega Shard",
# howling grotto
"Howling Grotto - Emerald Golem", "Howling Grotto Seal - Crushing Pits", "Howling Grotto Seal - Crushing Pits",
# searing crags
@@ -36,6 +43,7 @@ def testVertical(self) -> None:
# cloud ruins
"Cloud Ruins - Acro", "Cloud Ruins Seal - Ghost Pit",
"Cloud Ruins Seal - Toothbrush Alley", "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room",
+ "Cloud Entrance Mega Shard", "Time Warp Mega Shard", "Money Farm Room Mega Shard 1", "Money Farm Room Mega Shard 2",
# underworld
"Underworld Seal - Rising Fanta", "Underworld Seal - Sharp and Windy Climb",
# elemental skylands
@@ -73,6 +81,18 @@ def testWindmill(self) -> None:
item = self.get_item_by_name("Rope Dart")
self.collect(item)
self.assertTrue(self.can_reach_location(special_loc))
+
+ def testGlacial(self) -> None:
+ """Test Glacial Peak locations."""
+ self.assertAccessDependency(["Glacial Peak Seal - Ice Climbers"],
+ [["Second Wind", "Meditation"], ["Rope Dart"], ["Wingsuit"]],
+ True)
+ self.assertAccessDependency(["Glacial Peak Seal - Projectile Spike Pit"],
+ [["Strike of the Ninja"], ["Windmill Shuriken"], ["Rope Dart"], ["Wingsuit"]],
+ True)
+ self.assertAccessDependency(["Glacial Peak Seal - Glacial Air Swag", "Glacial Peak Mega Shard"],
+ [["Windmill Shuriken"], ["Wingsuit"], ["Rope Dart"]],
+ True)
class NoLogicTest(MessengerTestBase):
diff --git a/worlds/minecraft/docs/en_Minecraft.md b/worlds/minecraft/docs/en_Minecraft.md
index 2d4f063b79a9..1ef347983bc4 100644
--- a/worlds/minecraft/docs/en_Minecraft.md
+++ b/worlds/minecraft/docs/en_Minecraft.md
@@ -29,82 +29,82 @@ sequence either by skipping it or watching hit play out.
## Which recipes are locked?
* Archery
- * Bow
- * Arrow
- * Crossbow
+ * Bow
+ * Arrow
+ * Crossbow
* Brewing
- * Blaze Powder
- * Brewing Stand
+ * Blaze Powder
+ * Brewing Stand
* Enchanting
- * Enchanting Table
- * Bookshelf
+ * Enchanting Table
+ * Bookshelf
* Bucket
* Flint & Steel
* All Beds
* Bottles
* Shield
* Fishing Rod
- * Fishing Rod
- * Carrot on a Stick
- * Warped Fungus on a Stick
+ * Fishing Rod
+ * Carrot on a Stick
+ * Warped Fungus on a Stick
* Campfire
- * Campfire
- * Soul Campfire
+ * Campfire
+ * Soul Campfire
* Spyglass
* Lead
* Progressive Weapons
- * Tier I
- * Stone Sword
- * Stone Axe
- * Tier II
- * Iron Sword
- * Iron Axe
- * Tier III
- * Diamond Sword
- * Diamond Axe
+ * Tier I
+ * Stone Sword
+ * Stone Axe
+ * Tier II
+ * Iron Sword
+ * Iron Axe
+ * Tier III
+ * Diamond Sword
+ * Diamond Axe
* Progessive Tools
- * Tier I
- * Stone Shovel
- * Stone Hoe
- * Tier II
- * Iron Shovel
- * Iron Hoe
- * Tier III
- * Diamond Shovel
- * Diamond Hoe
- * Netherite Ingot
+ * Tier I
+ * Stone Shovel
+ * Stone Hoe
+ * Tier II
+ * Iron Shovel
+ * Iron Hoe
+ * Tier III
+ * Diamond Shovel
+ * Diamond Hoe
+ * Netherite Ingot
* Progressive Armor
- * Tier I
- * Iron Helmet
- * Iron Chestplate
- * Iron Leggings
- * Iron Boots
- * Tier II
- * Diamond Helmet
- * Diamond Chestplate
- * Diamond Leggings
- * Diamond Boots
+ * Tier I
+ * Iron Helmet
+ * Iron Chestplate
+ * Iron Leggings
+ * Iron Boots
+ * Tier II
+ * Diamond Helmet
+ * Diamond Chestplate
+ * Diamond Leggings
+ * Diamond Boots
* Progressive Resource Crafting
- * Tier I
- * Iron Ingot from Nuggets
- * Iron Nugget
- * Gold Ingot from Nuggets
- * Gold Nugget
- * Furnace
- * Blast Furnace
- * Tier II
- * Redstone
- * Redstone Block
- * Glowstone
- * Iron Ingot from Iron Block
- * Iron Block
- * Gold Ingot from Gold Block
- * Gold Block
- * Diamond
- * Diamond Block
- * Netherite Block
- * Netherite Ingot from Netherite Block
- * Anvil
- * Emerald
- * Emerald Block
- * Copper Block
+ * Tier I
+ * Iron Ingot from Nuggets
+ * Iron Nugget
+ * Gold Ingot from Nuggets
+ * Gold Nugget
+ * Furnace
+ * Blast Furnace
+ * Tier II
+ * Redstone
+ * Redstone Block
+ * Glowstone
+ * Iron Ingot from Iron Block
+ * Iron Block
+ * Gold Ingot from Gold Block
+ * Gold Block
+ * Diamond
+ * Diamond Block
+ * Netherite Block
+ * Netherite Ingot from Netherite Block
+ * Anvil
+ * Emerald
+ * Emerald Block
+ * Copper Block
diff --git a/worlds/musedash/Options.py b/worlds/musedash/Options.py
index cc9f5d705684..b2f15ecc8e6c 100644
--- a/worlds/musedash/Options.py
+++ b/worlds/musedash/Options.py
@@ -3,8 +3,8 @@
class AllowJustAsPlannedDLCSongs(Toggle):
- """Whether 'Just as Planned DLC' songs, and all the DLCs along with it, will be included in the randomizer."""
- display_name = "Allow Just As Planned DLC Songs"
+ """Whether [Just as Planned]/[Muse Plus] DLC Songs, and all the DLCs along with it, will be included in the randomizer."""
+ display_name = "Allow [Just as Planned]/[Muse Plus] DLC Songs"
class StreamerModeEnabled(Toggle):
@@ -125,7 +125,7 @@ class TrapTypes(Choice):
- VFX Traps consist of visual effects that play over the song. (i.e. Grayscale.)
- SFX Traps consist of changing your sfx setting to one possibly more annoying sfx.
Traps last the length of a song, or until you die.
- Note: SFX traps are only available with Just As Planned dlc songs.
+ Note: SFX traps are only available if [Just as Planned] DLC songs are enabled.
"""
display_name = "Available Trap Types"
option_None = 0
diff --git a/worlds/musedash/docs/en_Muse Dash.md b/worlds/musedash/docs/en_Muse Dash.md
index 5f4673d256a8..008fd4d2df0c 100644
--- a/worlds/musedash/docs/en_Muse Dash.md
+++ b/worlds/musedash/docs/en_Muse Dash.md
@@ -17,7 +17,7 @@ The goal of Muse Dash is to collect a number of **Music Sheets**. Once you've co
Only the base Muse Dash game is required in order to play this game.
-However, the **Just as Planned DLC** is recommended as the number of possible songs increases from 60+ to 400+ songs, which adds to the variety and increases replayability.
+However, the **[Just as Planned]**/**[Muse Plus]** DLC is recommended, as it increases the number of possible songs from ~60 to 400+ songs, which adds to the variety and increases replayability.
## What Other Adjustments have been made to the Base Game?
- Several song select filters have been added to make finding songs to play easy.
diff --git a/worlds/musedash/docs/setup_en.md b/worlds/musedash/docs/setup_en.md
index 7ad701829735..1ab61ff22ac6 100644
--- a/worlds/musedash/docs/setup_en.md
+++ b/worlds/musedash/docs/setup_en.md
@@ -8,7 +8,7 @@
- Windows 8 or Newer.
- Muse Dash: [Available on Steam](https://store.steampowered.com/app/774171/Muse_Dash/)
- - \[Optional\] Just As Planned DLC: [Also Available on Steam](https://store.steampowered.com/app/1055810/Muse_Dash__Just_as_planned/)
+ - \[Optional\] [Just as Planned] DLC: [Also Available on Steam](https://store.steampowered.com/app/1055810/Muse_Dash__Just_as_planned/)
- Melon Loader: [GitHub](https://github.com/LavaGang/MelonLoader/releases/latest)
- .Net Framework 4.8 may be needed for the installer: [Download](https://dotnet.microsoft.com/en-us/download/dotnet-framework/net48)
- .Net 6.0 (If not already installed): [Download](https://dotnet.microsoft.com/en-us/download/dotnet/6.0#runtime-6.0.15)
diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py
index 64f62adddb63..11aa737e0f26 100644
--- a/worlds/pokemon_rb/__init__.py
+++ b/worlds/pokemon_rb/__init__.py
@@ -49,14 +49,25 @@ class RomStart(str):
class PokemonWebWorld(WebWorld):
- tutorials = [Tutorial(
+ setup_en = Tutorial(
"Multiworld Setup Guide",
"A guide to playing Pokemon Red and Blue with Archipelago.",
"English",
"setup_en.md",
"setup/en",
["Alchav"]
- )]
+ )
+
+ setup_es = Tutorial(
+ setup_en.tutorial_name,
+ setup_en.description,
+ "Español",
+ "setup_es.md",
+ "setup/es",
+ ["Shiny"]
+ )
+
+ tutorials = [setup_en, setup_es]
class PokemonRedBlueWorld(World):
@@ -138,7 +149,7 @@ def encode_name(name, t):
if self.multiworld.key_items_only[self.player]:
self.multiworld.trainersanity[self.player] = self.multiworld.trainersanity[self.player].from_text("off")
- self.multiworld.dexsanity[self.player] = self.multiworld.dexsanity[self.player].from_text("false")
+ self.multiworld.dexsanity[self.player].value = 0
self.multiworld.randomize_hidden_items[self.player] = \
self.multiworld.randomize_hidden_items[self.player].from_text("off")
@@ -717,6 +728,15 @@ def fill_slot_data(self) -> dict:
"death_link": self.multiworld.death_link[self.player].value,
"prizesanity": self.multiworld.prizesanity[self.player].value,
"key_items_only": self.multiworld.key_items_only[self.player].value,
+ "poke_doll_skip": self.multiworld.poke_doll_skip[self.player].value,
+ "bicycle_gate_skips": self.multiworld.bicycle_gate_skips[self.player].value,
+ "stonesanity": self.multiworld.stonesanity[self.player].value,
+ "door_shuffle": self.multiworld.door_shuffle[self.player].value,
+ "warp_tile_shuffle": self.multiworld.warp_tile_shuffle[self.player].value,
+ "dark_rock_tunnel_logic": self.multiworld.dark_rock_tunnel_logic[self.player].value,
+ "split_card_key": self.multiworld.split_card_key[self.player].value,
+ "all_elevators_locked": self.multiworld.all_elevators_locked[self.player].value,
+
}
@@ -730,4 +750,4 @@ def __init__(self, name, player: int = None):
name,
item_data.classification,
item_data.id, player
- )
\ No newline at end of file
+ )
diff --git a/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md b/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md
index 535054182707..daefd6b2f7eb 100644
--- a/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md
+++ b/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md
@@ -20,7 +20,7 @@ Many baseline changes are made to the game, including:
* PC item storage increased to 64 slots (up from 50).
* You can hold B to run (or bike extra fast!).
* You can hold select while talking to a trainer to re-battle them.
-* You can select "Pallet Warp" below the "Continue" option to warp to Pallet Towna s you load your save.
+* You can select "Pallet Warp" below the "Continue" option to warp to Pallet Town as you load your save.
* Mew can be encountered at the S.S. Anne dock truck. This can be randomized depending on your settings.
* The S.S. Anne will never depart.
* Seafoam Islands entrances are swapped. This means you need Strength to travel through from Cinnabar Island to Fuchsia
diff --git a/worlds/pokemon_rb/docs/setup_es.md b/worlds/pokemon_rb/docs/setup_es.md
new file mode 100644
index 000000000000..40731037757a
--- /dev/null
+++ b/worlds/pokemon_rb/docs/setup_es.md
@@ -0,0 +1,110 @@
+# Guía de instalación para Pokémon Red and Blue: Archipelago
+
+## Importante
+
+Al usar BizHawk, esta guía solo es aplicable en los sistemas de Windows y Linux.
+
+## Software Requerido
+
+- BizHawk: [BizHawk Releases en TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
+ - La versión 2.3.1 y posteriores son soportadas. Se recomienda la versión 2.7 para estabilidad.
+ - Instrucciones de instalación detalladas para BizHawk se pueden encontrar en el enlace de arriba.
+ - Los usuarios de Windows deben ejecutar el instalador de prerrequisitos (prereq installer) primero, que también se
+ encuentra en el enlace de arriba.
+- El cliente incorporado de Archipelago, que se puede encontrar [aquí](https://github.com/ArchipelagoMW/Archipelago/releases)
+ (selecciona `Pokemon Client` durante la instalación).
+- Los ROMs originales de Pokémon Red y/o Blue. La comunidad de Archipelago no puede proveerlos.
+
+## Software Opcional
+
+- [Tracker de mapa para Pokémon Red and Blue Archipelago](https://github.com/j-imbo/pkmnrb_jim/releases/latest), para usar con [PopTracker](https://github.com/black-sliver/PopTracker/releases)
+
+
+## Configurando BizHawk
+
+Una vez que Bizhawk se haya instalado, abre Emuhawk y cambia las siguientes configuraciones:
+
+- (≤ 2.8) Abrir EmuHawk e ir a Config > Customize. Abrir la pestaña Advanced, y en la opción de Lua Core cambiar desde
+ "NLua+KopiLua" a "Lua+LuaInterface". Luego reinicia EmuHawk. Esto es fundamental para que el script de Lua funcione
+ correctamente.
+ **NOTA: Incluso si "Lua+LuaInterface" ya estaba seleccionado, cambia entre las opciones y vuelvelo a seleccionar.
+ **Algunas instalaciones de versiones nuevas de EmuHawk tienen una tendencia a mostrar "Lua+LuaInterface" por defecto
+ **pero siguen cargando "NLua+KopiLua" hasta completar este paso.**
+- Aun en la pestaña Advanced, asegurate que la casilla de AutoSaveRAM este marcada, y selecciona también la casilla 5s.
+ Esto reduce la posibilidad de que se pierdan datos guardados en el caso de que el emulador deje de funcionar (crash).
+- En Config > Customize, pestaña General, marcar la casilla "Run in background". Esto evitará que te desconectes del
+ cliente mientras EmuHawk se esta ejecutando en segundo plano.
+
+Es muy recomendado asociar los archivos GB (\*.gb) al emulador EmuHawk que se acaba de instalar.
+Para hacerlo, simplemente busca uno de los ROMs de gameboy, presiona con el click derecho sobre el y selecciona
+"Abrir con...", despliega la lista que aparece y selecciona la opción al final de la lista "Buscar otra aplicación en"
+"el equipo", luego navega a la carpeta de Bizhawk y selecciona EmuHawk.exe.
+
+## Configura tu archivo YAML
+
+### Que es un archivo YAML y por qué necesito uno?
+
+Tu archivo YAML contiene un número de opciones que proveen al generador con información sobre como debe generar tu
+juego. Cada jugador de un multiworld entregara su propio archivo YAML. Esto permite que cada jugador disfrute de una
+experiencia personalizada a su manera, y que diferentes jugadores dentro del mismo multiworld pueden tener diferentes
+opciones.
+
+### Donde puedo obtener un archivo YAML?
+
+Puedes generar un archivo YAML or descargar su plantilla en la [pagina de configuración de jugador de Pokemon Red and Blue](/games/Pokemon%20Red%20and%20Blue/player-settings)
+
+Es importante tener en cuenta que la opción `game_version` determina el ROM que será parcheado.
+Tanto el jugador como la persona que genera (si está generando localmente) necesitarán el archivo del ROM
+correspondiente.
+
+Para las opciones `trainer_name` y `rival_name`, los siguientes caracteres normales son permitidos:
+
+* `‘’“”·… ABCDEFGHIJKLMNOPQRSTUVWXYZ():;[]abcdefghijklmnopqrstuvwxyzé'-?!.♂$×/,♀0123456789`
+
+Y los siguientes caracteres especiales (cada uno ocupa un caracter):
+* `<'d>`
+* `<'l>`
+* `<'t>`
+* `<'v>`
+* `<'r>`
+* `<'m>`
+* ``
+* ``
+* `` alias para `♂`
+* `` alias para `♀`
+
+## Unirse a un juego MultiWorld
+
+### Obtener tu parche de Pokémon
+
+Cuando te unes a un juego multiworld, se te pedirá que entregues tu archivo YAML a quien lo este organizando.
+Una vez que la generación acabe, el anfitrión te dará un enlace a tu archivo, o un .zip con los archivos de
+todos. Tu archivo tiene una extensión `.apred` o `.apblue`.
+
+Haz doble click en tu archivo `.apred` o `.apblue` para que se ejecute el cliente y realize el parcheado de la ROM.
+Una vez acabe ese proceso (esto puede tardar un poco), el cliente y el emulador se abrirán automaticamente (si es que se
+ha asociado la extensión al emulador tal como fue recomendado)
+
+### Conectarse al multiserver
+
+Una vez ejecutado tanto el cliente como el emulador, hay que conectarlos. Abre la carpeta de instalación de Archipelago,
+luego abre `data/lua`, y simplemente arrastra el archivo `connector_pkmn_rb.lua` a la ventana principal de Emuhawk.
+(Alternativamente, puedes abrir la consola de Lua manualmente. En Emuhawk ir a Tools > Lua Console, luego ir al menú
+`Script` 〉 `Open Script`, navegar a la ubicación de `connector_pkmn_rb.lua` y seleccionarlo.)
+
+Para conectar el cliente con el servidor, simplemente pon `:` en la caja de texto superior y presiona
+enter (si el servidor tiene contraseña, en la caja de texto inferior escribir `/connect : [contraseña]`)
+
+Ahora ya estás listo para tu aventura en Kanto.
+
+## Auto-Tracking
+
+Pokémon Red and Blue tiene un mapa completamente funcional que soporta seguimiento automático.
+
+1. Descarga el [Tracker de mapa para Pokémon Red and Blue Archipelago](https://github.com/j-imbo/pkmnrb_jim/releases/latest) y [PopTracker](https://github.com/black-sliver/PopTracker/releases).
+2. Abre PopTracker, y carga el pack de Pokémon Red and Blue.
+3. Haz click en el símbolo "AP" en la parte superior.
+4. Ingresa la dirección de AP, nombre del slot y contraseña (si es que hay).
+
+Y ya, el resto debería hacerse solo! Los items y checks seran marcados automaticamente, e incluso reconocerá tus
+configuraciones - Ocultará checks y ajustará la logica segun corresponda.
diff --git a/worlds/pokemon_rb/logic.py b/worlds/pokemon_rb/logic.py
index 87398c7267fe..cbe28e0ddb47 100644
--- a/worlds/pokemon_rb/logic.py
+++ b/worlds/pokemon_rb/logic.py
@@ -53,7 +53,7 @@ def has_key_items(state, count, player):
"Hideout Key", "Card Key 2F", "Card Key 3F", "Card Key 4F", "Card Key 5F",
"Card Key 6F", "Card Key 7F", "Card Key 8F", "Card Key 9F", "Card Key 10F",
"Card Key 11F", "Exp. All", "Fire Stone", "Thunder Stone", "Water Stone",
- "Leaf Stone"] if state.has(item, player)])
+ "Leaf Stone", "Moon Stone"] if state.has(item, player)])
+ min(state.count("Progressive Card Key", player), 10))
return key_items >= count
diff --git a/worlds/pokemon_rb/rom.py b/worlds/pokemon_rb/rom.py
index 0757d3343510..4b191d91765b 100644
--- a/worlds/pokemon_rb/rom.py
+++ b/worlds/pokemon_rb/rom.py
@@ -238,18 +238,19 @@ def generate_output(self, output_directory: str):
data[address] = 0 if "Elevator" in connected_map_name else warp_to_ids[i]
data[address + 1] = map_ids[connected_map_name]
- for i, gym_leader in enumerate(("Pewter Gym - Brock TM", "Cerulean Gym - Misty TM",
- "Vermilion Gym - Lt. Surge TM", "Celadon Gym - Erika TM",
- "Fuchsia Gym - Koga TM", "Saffron Gym - Sabrina TM",
- "Cinnabar Gym - Blaine TM", "Viridian Gym - Giovanni TM")):
- item_name = self.multiworld.get_location(gym_leader, self.player).item.name
- if item_name.startswith("TM"):
- try:
- tm = int(item_name[2:4])
- move = poke_data.moves[self.local_tms[tm - 1]]["id"]
- data[rom_addresses["Gym_Leader_Moves"] + (2 * i)] = move
- except KeyError:
- pass
+ if not self.multiworld.key_items_only[self.player]:
+ for i, gym_leader in enumerate(("Pewter Gym - Brock TM", "Cerulean Gym - Misty TM",
+ "Vermilion Gym - Lt. Surge TM", "Celadon Gym - Erika TM",
+ "Fuchsia Gym - Koga TM", "Saffron Gym - Sabrina TM",
+ "Cinnabar Gym - Blaine TM", "Viridian Gym - Giovanni TM")):
+ item_name = self.multiworld.get_location(gym_leader, self.player).item.name
+ if item_name.startswith("TM"):
+ try:
+ tm = int(item_name[2:4])
+ move = poke_data.moves[self.local_tms[tm - 1]]["id"]
+ data[rom_addresses["Gym_Leader_Moves"] + (2 * i)] = move
+ except KeyError:
+ pass
def set_trade_mon(address, loc):
mon = self.multiworld.get_location(loc, self.player).item.name
diff --git a/worlds/raft/__init__.py b/worlds/raft/__init__.py
index d00b5faa9e90..fec60c3bd51b 100644
--- a/worlds/raft/__init__.py
+++ b/worlds/raft/__init__.py
@@ -138,6 +138,8 @@ def create_resourcePack(self, rpName: str) -> Item:
return RaftItem(rpName, ItemClassification.filler, self.item_name_to_id[rpName], player=self.player)
def collect_item(self, state, item, remove=False):
+ if item.advancement is False:
+ return None
if item.name in progressive_item_list:
prog_table = progressive_item_list[item.name]
if remove:
diff --git a/worlds/ror2/docs/en_Risk of Rain 2.md b/worlds/ror2/docs/en_Risk of Rain 2.md
index ca22d1a44d70..d30edf888944 100644
--- a/worlds/ror2/docs/en_Risk of Rain 2.md
+++ b/worlds/ror2/docs/en_Risk of Rain 2.md
@@ -8,7 +8,7 @@ config file.
## What does randomization do to this game?
Risk of Rain is already a random game, by virtue of being a roguelite. The Archipelago mod implements pure multiworld
-functionality in which certain chests (made clear via a location check progress bar) will send an item out to the
+functionality in which certain chests will send an item out to the
multiworld. The items that _would have been_ in those chests will be returned to the Risk of Rain player via grants by
other players in other worlds.
@@ -16,28 +16,30 @@ There are two modes in risk of rain. Classic Mode and Explore Mode
Classic Mode:
- - Classic mode implements pure multiworld
-functionality in which certain chests (made clear via a location check progress bar) will send an item out to the
-multiworld. The items that _would have been_ in those chests will be returned to the Risk of Rain player via grants by
-other players in other worlds.
+ - Certain chests (made clear via a location check progress bar) will send an item out to the
+ multiworld. The location of these chests do not matter, since all environments share a unified location pool.
Explore Mode:
- - Just like in Classic mode chests will send out an item to the multiworld. The difference is that each environment
- will have a set amount that can be sent out and shrines along with other things that will need to be checked.
- Also, each environment is an item and, you'll need it to be able to access it.
+ - Chests will continue to work as they did in Classic Mode, the difference being that each environment
+ will have a set amount of items that can be sent out. In addition, shrines, radio scanners, newt altars,
+ and scavenger bags will need to be checked, depending on your settings.
+ This mode also makes each environment an item. In order to access a particular stage, you'll need it to be
+ sent in the multiworld.
## What is the goal of Risk of Rain 2 in Archipelago?
-Just like in the original game, any way to "beat the game" counts as a win. Alternatively, if you are new to the game and
+Just like in the original game, any way to "beat the game" counts as a win. This means beating one of the bosses
+on Commencement, The Planetarium, or A Moment, Whole. Alternatively, if you are new to the game and
aren't very confident in being able to "beat the game", you can set **Final Stage Death is Win** to true
-(You can turn this on in your player settings.) This will make it so if you die on either Commencement or The Planetarium,
-it will count as your goal, and **Obliterating yourself** will count as well.
+(You can turn this on in your player settings.) This will make it so dying on either Commencement or The Planetarium,
+or **obliterating yourself in A Moment, Fractured** will count as your goal.
**You do not need to complete all the location checks** to win; any item you don't collect may be released if the
server options allow.
If you die before you accomplish your goal, you can start a new run. You will start the run with any items that you
-received from other players. Any items that you picked up the "normal" way will be lost.
+received from other players. However, these items will be randomized within their rarity at the start of each run.
+Any items that you picked up the "normal" way will be lost.
Note, you can play Simulacrum mode as part of an Archipelago, but you can't achieve any of the victory conditions in
Simulacrum. So you could, for example, collect most of your items through a Simulacrum run(only works in classic mode),
@@ -72,10 +74,10 @@ The Risk of Rain items are:
Each item grants you a random in-game item from the category it belongs to.
-When an item is granted by another world to the Risk of Rain player (one of the items listed above) then a random
+When an item is granted by another world to the Risk of Rain player then a random
in-game item of that tier will appear in the Risk of Rain player's inventory. If the item grant is an `Equipment` and
-the player already has an equipment item equipped then the _item that was equipped_ will be dropped on the ground and _
-the new equipment_ will take it's place. (If you want the old one back, pick it up.)
+the player already has an equipment item equipped then the _item that was equipped_ will be dropped on the ground and
+_the new equipment_ will take it's place.
Explore Mode items are:
@@ -93,10 +95,10 @@ Dlc_Sotv items
* `Sulfur Pools`
* `Void Locus`
-When a explore item is granted it will unlock that environment and will now be accessible to progress to victory! The
-game will still pick randomly which environment is next but it will first check to see if they are available. If you have
-them unlocked it will weight the game to have a ***higher chance*** to go to one you have checks versus one you have
-already completed. You will still not be able to goto a stage 3 environment from a stage 1 environment.
+When an explore item is granted, it will unlock that environment and will now be accessible! The
+game will still pick randomly which environment is next, but it will first check to see if they are available. If you have
+multiple of the next environments unlocked, it will weight the game to have a ***higher chance*** to go to one you
+have checks in versus one you have already completed. You will still be unable to go to a stage 3 environment from a stage 1 environment.
@@ -108,9 +110,9 @@ to 250** items. The number of items will be randomized between all players, so y
item pickup step based on how many items the other players in the multiworld have. (Around 100 seems to be a good
ballpark if you want to have a similar number of items to most other games.)
-In explore mode the amount of checks base on how many **chests, shrines, scavengers, radio scanners and, newt altars**
-are in the pool. With just the base game the numbers are **52 to 516** and with the dlc its **60 to 660** with
-everything on default being **216**
+In explore mode, the amount of checks are based on how many **chests, shrines, scavengers, radio scanners, and newt altars**
+are in the pool. With just the base game, checks can range from **52 to 516**, with the DLC expanding it to **60 to 660**.
+Leaving everything on default, the total number of checks comes out to **216** locations.
After you have completed the specified number of checks, you won't send anything else to the multiworld. You can
receive up to the specified number of randomized items from the multiworld as the players find them. In either case,
@@ -120,12 +122,15 @@ you can continue to collect items as normal in Risk of Rain 2 if you've already
When the Risk of Rain player fills up their location check bar then the next spawned item will become an item grant for
another player's world (or possibly get sent back to yourself). The item in Risk of Rain will disappear in a poof of
-smoke and the grant will automatically go out to the multiworld.
+smoke and the grant will automatically go out to the multiworld. Additionally, you will see a message in the chat saying
+what item you sent out. If the message does not appear, this likely means that another game has collected their items from you.
## What is the item pickup step?
-The item pickup step is a YAML setting which allows you to set how many items you need to spawn before the _next_ item
-that is spawned disappears (in a poof of smoke) and goes out to the multiworld.
+The item pickup step is a setting in the YAML which allows you to set how many items you need to spawn before the _next_ item
+that is spawned disappears (in a poof of smoke) and goes out to the multiworld. For instance, an item step of **1** means that
+every other chest will send an item to the multiworld. An item step of **2** means that every third chest sends out an item
+just as an item step of **0** would send an item on **each chest.**
## Is Archipelago compatible with other Risk of Rain 2 mods?
diff --git a/worlds/sc2wol/Client.py b/worlds/sc2wol/Client.py
new file mode 100644
index 000000000000..a9bb826b7447
--- /dev/null
+++ b/worlds/sc2wol/Client.py
@@ -0,0 +1,1211 @@
+from __future__ import annotations
+
+import asyncio
+import copy
+import ctypes
+import json
+import logging
+import multiprocessing
+import os.path
+import re
+import sys
+import typing
+import queue
+import zipfile
+import io
+import random
+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
+
+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 worlds.sc2wol import SC2WoLWorld
+from worlds.sc2wol.Items import lookup_id_to_name, get_full_item_list, ItemData, type_flaggroups, upgrade_numbers
+from worlds.sc2wol.Locations import SC2WOL_LOC_ID_OFFSET
+from worlds.sc2wol.MissionTables import lookup_id_to_mission
+from worlds.sc2wol.Regions import MissionInfo
+
+import colorama
+from NetUtils import ClientStatus, NetworkItem, RawJSONtoTextParser, JSONtoTextParser, JSONMessagePart
+from MultiServer import mark_raw
+
+loop = asyncio.get_event_loop_policy().new_event_loop()
+nest_asyncio.apply(loop)
+max_bonus: int = 13
+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 = "API2"
+
+
+# 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():
+ return os.environ["SC2PATH"] + os.sep + "ArchipelagoSC2Metadata.txt"
+
+
+class StarcraftClientProcessor(ClientCommandProcessor):
+ ctx: SC2Context
+
+ 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
+
+ def _cmd_color(self, color: str = "") -> bool:
+ player_colors = [
+ "White", "Red", "Blue", "Teal",
+ "Purple", "Yellow", "Orange", "Green",
+ "LightPink", "Violet", "LightGrey", "DarkGreen",
+ "Brown", "LightGreen", "DarkGrey", "Pink",
+ "Rainbow", "Random", "Default"
+ ]
+ match_colors = [player_color.lower() for player_color in player_colors]
+ if color:
+ if color.lower() not in match_colors:
+ self.output(color + " is not a valid color. Available colors: " + ', '.join(player_colors))
+ return False
+ if color.lower() == "random":
+ color = random.choice(player_colors[:16])
+ self.ctx.player_color = match_colors.index(color.lower())
+ self.output("Color set to " + player_colors[self.ctx.player_color])
+ else:
+ self.output("Current player color: " + player_colors[self.ctx.player_color])
+ self.output("To change your colors, add the name of the color after the command.")
+ self.output("Available colors: " + ', '.join(player_colors))
+
+ 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."""
+ 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.")
+ 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):
+ self.handlers = {
+ "ItemSend": self._handle_color,
+ "ItemCheat": self._handle_color,
+ "Hint": self._handle_color,
+ }
+ super().__init__(ctx)
+
+ def _handle_color(self, node: JSONMessagePart):
+ 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):
+ return ''
+
+
+class SC2Context(CommonContext):
+ command_processor = StarcraftClientProcessor
+ game = "Starcraft 2 Wings of Liberty"
+ items_handling = 0b111
+ difficulty = -1
+ game_speed = -1
+ all_in_choice = 0
+ mission_order = 0
+ player_color = 2
+ mission_req_table: typing.Dict[str, MissionInfo] = {}
+ final_mission: int = 29
+ announcements = queue.Queue()
+ sc2_run_task: typing.Optional[asyncio.Task] = None
+ missions_unlocked: bool = False # allow launching missions ignoring requirements
+ generic_upgrade_missions = 0
+ generic_upgrade_research = 0
+ generic_upgrade_items = 0
+ current_tooltip = None
+ last_loc_list = None
+ difficulty_override = -1
+ game_speed_override = -1
+ mission_id_to_location_ids: typing.Dict[int, typing.List[int]] = {}
+ last_bot: typing.Optional[ArchipelagoBot] = None
+
+ def __init__(self, *args, **kwargs):
+ super(SC2Context, self).__init__(*args, **kwargs)
+ self.raw_text_parser = SC2JSONtoTextParser(self)
+
+ async def server_auth(self, password_requested: bool = False):
+ if password_requested and not self.password:
+ await super(SC2Context, self).server_auth(password_requested)
+ await self.get_username()
+ await self.send_connect()
+
+ def on_package(self, cmd: str, args: dict):
+ if cmd in {"Connected"}:
+ self.difficulty = args["slot_data"]["game_difficulty"]
+ if "game_speed" in args["slot_data"]:
+ self.game_speed = args["slot_data"]["game_speed"]
+ else:
+ self.game_speed = 0
+ self.all_in_choice = args["slot_data"]["all_in_map"]
+ slot_req_table = args["slot_data"]["mission_req"]
+ # Maintaining backwards compatibility with older slot data
+ self.mission_req_table = {
+ mission: MissionInfo(
+ **{field: value for field, value in mission_info.items() if field in MissionInfo._fields}
+ )
+ for mission, mission_info in slot_req_table.items()
+ }
+ self.mission_order = args["slot_data"].get("mission_order", 0)
+ self.final_mission = args["slot_data"].get("final_mission", 29)
+ self.player_color = args["slot_data"].get("player_color", 2)
+ self.generic_upgrade_missions = args["slot_data"].get("generic_upgrade_missions", 0)
+ self.generic_upgrade_items = args["slot_data"].get("generic_upgrade_items", 0)
+ self.generic_upgrade_research = args["slot_data"].get("generic_upgrade_research", 0)
+
+ 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.")
+
+
+ def on_print_json(self, args: dict):
+ # 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):
+ from kvui import GameManager, HoverBehavior, ServerToolTip
+ from kivy.app import App
+ from kivy.clock import Clock
+ from kivy.uix.tabbedpanel import TabbedPanelItem
+ from kivy.uix.gridlayout import GridLayout
+ from kivy.lang import Builder
+ from kivy.uix.label import Label
+ from kivy.uix.button import Button
+ from kivy.uix.floatlayout import FloatLayout
+ from kivy.properties import StringProperty
+
+ class HoverableButton(HoverBehavior, Button):
+ pass
+
+ class MissionButton(HoverableButton):
+ tooltip_text = StringProperty("Test")
+ ctx: SC2Context
+
+ def __init__(self, *args, **kwargs):
+ super(HoverableButton, self).__init__(*args, **kwargs)
+ self.layout = FloatLayout()
+ self.popuplabel = ServerToolTip(text=self.text)
+ self.layout.add_widget(self.popuplabel)
+
+ def on_enter(self):
+ self.popuplabel.text = self.tooltip_text
+
+ if self.ctx.current_tooltip:
+ App.get_running_app().root.remove_widget(self.ctx.current_tooltip)
+
+ if self.tooltip_text == "":
+ self.ctx.current_tooltip = None
+ else:
+ App.get_running_app().root.add_widget(self.layout)
+ self.ctx.current_tooltip = self.layout
+
+ def on_leave(self):
+ self.ctx.ui.clear_tooltip()
+
+ @property
+ def ctx(self) -> CommonContext:
+ return App.get_running_app().ctx
+
+ class MissionLayout(GridLayout):
+ pass
+
+ class MissionCategory(GridLayout):
+ pass
+
+ class SC2Manager(GameManager):
+ logging_pairs = [
+ ("Client", "Archipelago"),
+ ("Starcraft2", "Starcraft2"),
+ ]
+ base_title = "Archipelago Starcraft 2 Client"
+
+ mission_panel = None
+ last_checked_locations = {}
+ mission_id_to_button = {}
+ launching: typing.Union[bool, int] = False # if int -> mission ID
+ refresh_from_launching = True
+ first_check = True
+ ctx: SC2Context
+
+ def __init__(self, ctx):
+ super().__init__(ctx)
+
+ def clear_tooltip(self):
+ if self.ctx.current_tooltip:
+ App.get_running_app().root.remove_widget(self.ctx.current_tooltip)
+
+ self.ctx.current_tooltip = None
+
+ def build(self):
+ container = super().build()
+
+ panel = TabbedPanelItem(text="Starcraft 2 Launcher")
+ self.mission_panel = panel.content = MissionLayout()
+
+ self.tabs.add_widget(panel)
+
+ Clock.schedule_interval(self.build_mission_table, 0.5)
+
+ return container
+
+ def build_mission_table(self, dt):
+ if (not self.launching and (not self.last_checked_locations == self.ctx.checked_locations or
+ not self.refresh_from_launching)) or self.first_check:
+ self.refresh_from_launching = True
+
+ self.mission_panel.clear_widgets()
+ if self.ctx.mission_req_table:
+ self.last_checked_locations = self.ctx.checked_locations.copy()
+ self.first_check = False
+
+ self.mission_id_to_button = {}
+ categories = {}
+ available_missions, unfinished_missions = calc_unfinished_missions(self.ctx)
+
+ # separate missions into categories
+ for mission in self.ctx.mission_req_table:
+ if not self.ctx.mission_req_table[mission].category in categories:
+ categories[self.ctx.mission_req_table[mission].category] = []
+
+ categories[self.ctx.mission_req_table[mission].category].append(mission)
+
+ for category in categories:
+ category_panel = MissionCategory()
+ if category.startswith('_'):
+ category_display_name = ''
+ else:
+ category_display_name = category
+ category_panel.add_widget(
+ Label(text=category_display_name, size_hint_y=None, height=50, outline_width=1))
+
+ for mission in categories[category]:
+ text: str = mission
+ tooltip: str = ""
+ mission_id: int = self.ctx.mission_req_table[mission].id
+ # Map has uncollected locations
+ if mission in unfinished_missions:
+ text = f"[color=6495ED]{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 self.ctx.mission_req_table[mission].required_world:
+ tooltip += ", ".join(list(self.ctx.mission_req_table)[req_mission - 1] for
+ req_mission in
+ self.ctx.mission_req_table[mission].required_world)
+
+ if self.ctx.mission_req_table[mission].number:
+ tooltip += " and "
+ if self.ctx.mission_req_table[mission].number:
+ tooltip += f"{self.ctx.mission_req_table[mission].number} missions completed"
+ remaining_location_names: typing.List[str] = [
+ self.ctx.location_names[loc] for loc in self.ctx.locations_for_mission(mission)
+ if loc in self.ctx.missing_locations]
+
+ 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_location_names:
+ if tooltip:
+ tooltip += "\n"
+ tooltip += f"Uncollected locations:\n"
+ tooltip += "\n".join(remaining_location_names)
+
+ mission_button = MissionButton(text=text, size_hint_y=None, height=50)
+ 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=""))
+ self.mission_panel.add_widget(category_panel)
+
+ elif self.launching:
+ self.refresh_from_launching = False
+
+ self.mission_panel.clear_widgets()
+ self.mission_panel.add_widget(Label(text="Launching Mission: " +
+ lookup_id_to_mission[self.launching]))
+ if self.ctx.ui:
+ self.ctx.ui.clear_tooltip()
+
+ def mission_callback(self, button):
+ 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
+
+ self.ui = SC2Manager(self)
+ self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
+ import pkgutil
+ data = pkgutil.get_data(SC2WoLWorld.__module__, "Starcraft2.kv").decode()
+ Builder.load_string(data)
+
+ async def shutdown(self):
+ 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]} is not currently unlocked. "
+ f"Use /unfinished or /available to see what is available.")
+ return False
+
+ def build_location_to_mission_mapping(self):
+ mission_id_to_location_ids: typing.Dict[int, typing.Set[int]] = {
+ mission_info.id: set() for mission_info in self.mission_req_table.values()
+ }
+
+ for loc in self.server_locations:
+ mission_id, objective = divmod(loc - SC2WOL_LOC_ID_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: str):
+ mission_id: int = self.mission_req_table[mission].id
+ objectives = self.mission_id_to_location_ids[self.mission_req_table[mission].id]
+ for objective in objectives:
+ yield SC2WOL_LOC_ID_OFFSET + mission_id * 100 + objective
+
+
+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()
+
+
+maps_table = [
+ "ap_liberation_day", "ap_the_outlaws", "ap_zero_hour",
+ "ap_evacuation", "ap_outbreak", "ap_safe_haven", "ap_havens_fall",
+ "ap_smash_and_grab", "ap_the_dig", "ap_the_moebius_factor", "ap_supernova", "ap_maw_of_the_void",
+ "ap_devils_playground", "ap_welcome_to_the_jungle", "ap_breakout", "ap_ghost_of_a_chance",
+ "ap_the_great_train_robbery", "ap_cutthroat", "ap_engine_of_destruction", "ap_media_blitz", "ap_piercing_the_shroud",
+ "ap_whispers_of_doom", "ap_a_sinister_turn", "ap_echoes_of_the_future", "ap_in_utter_darkness",
+ "ap_gates_of_hell", "ap_belly_of_the_beast", "ap_shatter_the_sky", "ap_all_in"
+]
+
+wol_default_categories = [
+ "Mar Sara", "Mar Sara", "Mar Sara", "Colonist", "Colonist", "Colonist", "Colonist",
+ "Artifact", "Artifact", "Artifact", "Artifact", "Artifact", "Covert", "Covert", "Covert", "Covert",
+ "Rebellion", "Rebellion", "Rebellion", "Rebellion", "Rebellion", "Prophecy", "Prophecy", "Prophecy", "Prophecy",
+ "Char", "Char", "Char", "Char"
+]
+wol_default_category_names = [
+ "Mar Sara", "Colonist", "Artifact", "Covert", "Rebellion", "Prophecy", "Char"
+]
+
+
+def calculate_items(ctx: SC2Context) -> typing.List[int]:
+ items = ctx.items_received
+ network_item: NetworkItem
+ accumulators: typing.List[int] = [0 for _ in type_flaggroups]
+
+ for network_item in items:
+ name: str = lookup_id_to_name[network_item.item]
+ item_data: ItemData = get_full_item_list()[name]
+
+ # exists exactly once
+ if item_data.quantity == 1:
+ accumulators[type_flaggroups[item_data.type]] |= 1 << item_data.number
+
+ # exists multiple times
+ elif item_data.type == "Upgrade" or item_data.type == "Progressive Upgrade":
+ flaggroup = type_flaggroups[item_data.type]
+
+ # Generic upgrades apply only to Weapon / Armor upgrades
+ if item_data.type != "Upgrade" or ctx.generic_upgrade_items == 0:
+ accumulators[flaggroup] += 1 << item_data.number
+ else:
+ for bundled_number in upgrade_numbers[item_data.number]:
+ accumulators[flaggroup] += 1 << bundled_number
+
+ # sum
+ else:
+ accumulators[type_flaggroups[item_data.type]] += item_data.number
+
+ # Upgrades from completed missions
+ if ctx.generic_upgrade_missions > 0:
+ upgrade_flaggroup = type_flaggroups["Upgrade"]
+ num_missions = ctx.generic_upgrade_missions * len(ctx.mission_req_table)
+ 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 SC2WOL_LOC_ID_OFFSET + 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[5]:
+ accumulators[upgrade_flaggroup] += upgrade_count << bundled_number
+
+ return accumulators
+
+
+def calc_difficulty(difficulty):
+ if difficulty == 0:
+ return 'C'
+ elif difficulty == 1:
+ return 'N'
+ elif difficulty == 2:
+ return 'H'
+ elif difficulty == 3:
+ return 'B'
+
+ return 'X'
+
+
+async def starcraft_launch(ctx: SC2Context, mission_id: int):
+ sc2_logger.info(f"Launching {lookup_id_to_mission[mission_id]}. If game does not launch check log file for errors.")
+
+ with DllDirectory(None):
+ run_game(bot.maps.get(maps_table[mission_id - 1]), [Bot(Race.Terran, ArchipelagoBot(ctx, mission_id),
+ name="Archipelago", fullscreen=True)], realtime=True)
+
+
+class ArchipelagoBot(bot.bot_ai.BotAI):
+ game_running: bool = False
+ mission_completed: bool = False
+ boni: typing.List[bool]
+ setup_done: bool
+ ctx: SC2Context
+ mission_id: int
+ want_close: bool = False
+ can_read_game = False
+ last_received_update: int = 0
+
+ def __init__(self, ctx: SC2Context, mission_id):
+ 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)
+ 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
+ ))
+ await self.chat_send("?GiveResources {} {} {}".format(
+ start_items[8],
+ start_items[9],
+ start_items[10]
+ ))
+ await self.chat_send("?GiveTerranTech {} {} {} {} {} {} {} {} {} {}".format(
+ start_items[0], start_items[1], start_items[2], start_items[3], start_items[4],
+ start_items[5], start_items[6], start_items[12], start_items[13], start_items[14]))
+ await self.chat_send("?GiveProtossTech {}".format(start_items[7]))
+ await self.chat_send("?SetColor rr " + str(self.ctx.player_color)) # TODO: Add faction color options
+ await self.chat_send("?LoadFinished")
+ self.last_received_update = len(self.ctx.items_received)
+
+ else:
+ 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
+ for unit in self.all_own_units():
+ if unit.health_max == 38281:
+ game_state = int(38281 - unit.health)
+ self.can_read_game = True
+
+ 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)
+ await self.chat_send("?GiveTerranTech {} {} {} {} {} {} {} {} {} {}".format(
+ current_items[0], current_items[1], current_items[2], current_items[3], current_items[4],
+ current_items[5], current_items[6], current_items[12], current_items[13], current_items[14]))
+ await self.chat_send("?GiveProtossTech {}".format(current_items[7]))
+ 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": [SC2WOL_LOC_ID_OFFSET + 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": [SC2WOL_LOC_ID_OFFSET + victory_modulo * self.mission_id + x + 1]}])
+ self.boni[x] = True
+
+ else:
+ await self.chat_send("?SendMessage LostConnection - Lost connection to game.")
+
+
+def request_unfinished_missions(ctx: SC2Context):
+ if ctx.mission_req_table:
+ message = "Unfinished Missions: "
+ unlocks = initialize_blank_mission_dict(ctx.mission_req_table)
+ unfinished_locations = initialize_blank_mission_dict(ctx.mission_req_table)
+
+ _, unfinished_missions = calc_unfinished_missions(ctx, unlocks=unlocks)
+
+ # 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[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=None):
+ unfinished_missions = []
+ locations_completed = []
+
+ 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):
+ unfinished_missions = calc_available_missions(ctx)
+
+ return any(mission_id_to_check == ctx.mission_req_table[mission].id for mission in unfinished_missions)
+
+
+def mark_up_mission_name(ctx: SC2Context, mission, unlock_table):
+ """Checks if the mission is required for game completion and adds '*' to the name to mark that."""
+
+ if ctx.mission_req_table[mission].completion_critical:
+ if ctx.ui:
+ message = "[color=AF99EF]" + mission + "[/color]"
+ else:
+ message = "*" + mission + "*"
+ else:
+ message = mission
+
+ if ctx.ui:
+ unlocks = unlock_table[mission]
+
+ if len(unlocks) > 0:
+ pre_message = f"[ref={list(ctx.mission_req_table).index(mission)}|Unlocks: "
+ pre_message += ", ".join(f"{unlock}({ctx.mission_req_table[unlock].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]
+
+ pre_message = f"[ref={list(ctx.mission_req_table).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[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=None):
+ available_missions = []
+ missions_complete = 0
+
+ # Get number of missions completed
+ for loc in ctx.checked_locations:
+ if loc % victory_modulo == 0:
+ missions_complete += 1
+
+ for name in ctx.mission_req_table:
+ # Go through the required missions for each mission and fill up unlock table used later for hover-over tooltips
+ if unlocks:
+ for unlock in ctx.mission_req_table[name].required_world:
+ unlocks[list(ctx.mission_req_table)[unlock - 1]].append(name)
+
+ if mission_reqs_completed(ctx, name, missions_complete):
+ available_missions.append(name)
+
+ return available_missions
+
+
+def mission_reqs_completed(ctx: SC2Context, mission_name: str, missions_complete: int):
+ """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
+"""
+ if len(ctx.mission_req_table[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[mission_name].required_world:
+ req_success = True
+
+ # Check if required mission has been completed
+ if not (ctx.mission_req_table[list(ctx.mission_req_table)[req_mission - 1]].id *
+ victory_modulo + SC2WOL_LOC_ID_OFFSET) in ctx.checked_locations:
+ if not ctx.mission_req_table[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 (3, 4):
+ if req_success:
+ return True
+ else:
+ if req_mission is ctx.mission_req_table[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)[req_mission - 1], missions_complete):
+ if not ctx.mission_req_table[mission_name].or_requirements:
+ return False
+ else:
+ req_success = False
+
+ # If requirement check succeeded mark or as satisfied
+ if ctx.mission_req_table[mission_name].or_requirements and req_success:
+ or_success = True
+
+ if ctx.mission_req_table[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[mission_name].number:
+ return True
+ else:
+ return False
+ else:
+ return True
+
+
+def initialize_blank_mission_dict(location_table):
+ unlocks = {}
+
+ 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 = 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:
+ try:
+ base = re.search(r" = (.*)Versions", content).group(1)
+ except AttributeError:
+ sc2_logger.warning(f"Found {einfo}, but it was empty. Run SC2 through the Blizzard launcher, then "
+ f"try again.")
+ return False
+ 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()
+
+ mapdir = os.environ['SC2PATH'] / Path('Maps/ArchipelagoCampaign')
+ mods = ["ArchipelagoCore", "ArchipelagoPlayer", "ArchipelagoPlayerWoL", "ArchipelagoTriggers"]
+ modfiles = [os.environ["SC2PATH"] / Path("Mods/" + mod + ".SC2Mod") for mod in mods]
+ wol_required_maps = ["WoL" + os.sep + map_name + ".SC2Map" for map_name in maps_table]
+ needs_files = False
+
+ # Check for maps.
+ missing_maps = []
+ for mapfile in wol_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: str = None, force_download=False) -> (str, 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)):
+ with open(f"{repo}.zip", "wb") as fh:
+ fh.write(r2.content)
+ sc2_logger.info(f"Successfully downloaded {repo}.zip.")
+ return f"{repo}.zip", 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):
+ 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 launch():
+ colorama.init()
+ asyncio.run(main())
+ colorama.deinit()
diff --git a/worlds/sc2wol/Items.py b/worlds/sc2wol/Items.py
index ea495adf79bc..971a75375fe4 100644
--- a/worlds/sc2wol/Items.py
+++ b/worlds/sc2wol/Items.py
@@ -12,6 +12,7 @@ class ItemData(typing.NamedTuple):
classification: ItemClassification = ItemClassification.useful
quantity: int = 1
parent_item: str = None
+ origin: typing.Set[str] = {"wol"}
class StarcraftWoLItem(Item):
@@ -43,23 +44,36 @@ def get_full_item_list():
"Ghost": ItemData(15 + SC2WOL_ITEM_ID_OFFSET, "Unit", 15, classification=ItemClassification.progression),
"Spectre": ItemData(16 + SC2WOL_ITEM_ID_OFFSET, "Unit", 16, classification=ItemClassification.progression),
"Thor": ItemData(17 + SC2WOL_ITEM_ID_OFFSET, "Unit", 17, classification=ItemClassification.progression),
+ # EE units
+ "Liberator": ItemData(18 + SC2WOL_ITEM_ID_OFFSET, "Unit", 18, classification=ItemClassification.progression, origin={"nco", "ext"}),
+ "Valkyrie": ItemData(19 + SC2WOL_ITEM_ID_OFFSET, "Unit", 19, classification=ItemClassification.progression, origin={"bw"}),
+ "Widow Mine": ItemData(20 + SC2WOL_ITEM_ID_OFFSET, "Unit", 20, classification=ItemClassification.progression, origin={"ext"}),
+ "Cyclone": ItemData(21 + SC2WOL_ITEM_ID_OFFSET, "Unit", 21, classification=ItemClassification.progression, origin={"ext"}),
+ # Some other items are moved to Upgrade group because of the way how the bot message is parsed
"Progressive Infantry Weapon": ItemData(100 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 0, quantity=3),
"Progressive Infantry Armor": ItemData(102 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 2, quantity=3),
"Progressive Vehicle Weapon": ItemData(103 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 4, quantity=3),
"Progressive Vehicle Armor": ItemData(104 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 6, quantity=3),
"Progressive Ship Weapon": ItemData(105 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 8, quantity=3),
"Progressive Ship Armor": ItemData(106 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 10, quantity=3),
+ # Upgrade bundle 'number' values are used as indices to get affected 'number's
+ "Progressive Weapon Upgrade": ItemData(107 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 0, quantity=3),
+ "Progressive Armor Upgrade": ItemData(108 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 1, quantity=3),
+ "Progressive Infantry Upgrade": ItemData(109 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 2, quantity=3),
+ "Progressive Vehicle Upgrade": ItemData(110 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 3, quantity=3),
+ "Progressive Ship Upgrade": ItemData(111 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 4, quantity=3),
+ "Progressive Weapon/Armor Upgrade": ItemData(112 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 5, quantity=3),
"Projectile Accelerator (Bunker)": ItemData(200 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 0, parent_item="Bunker"),
"Neosteel Bunker (Bunker)": ItemData(201 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 1, parent_item="Bunker"),
"Titanium Housing (Missile Turret)": ItemData(202 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 2, classification=ItemClassification.filler, parent_item="Missile Turret"),
"Hellstorm Batteries (Missile Turret)": ItemData(203 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 3, parent_item="Missile Turret"),
- "Advanced Construction (SCV)": ItemData(204 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 4, parent_item="SCV"),
- "Dual-Fusion Welders (SCV)": ItemData(205 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 5, parent_item="SCV"),
+ "Advanced Construction (SCV)": ItemData(204 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 4),
+ "Dual-Fusion Welders (SCV)": ItemData(205 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 5),
"Fire-Suppression System (Building)": ItemData(206 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 6),
"Orbital Command (Building)": ItemData(207 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 7),
- "Stimpack (Marine)": ItemData(208 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 8, parent_item="Marine"),
+ "Progressive Stimpack (Marine)": ItemData(208 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 0, parent_item="Marine", quantity=2),
"Combat Shield (Marine)": ItemData(209 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 9, classification=ItemClassification.progression, parent_item="Marine"),
"Advanced Medic Facilities (Medic)": ItemData(210 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 10, classification=ItemClassification.filler, parent_item="Medic"),
"Stabilizer Medpacks (Medic)": ItemData(211 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 11, classification=ItemClassification.progression, parent_item="Medic"),
@@ -69,10 +83,59 @@ def get_full_item_list():
"Kinetic Foam (Marauder)": ItemData(215 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 15, parent_item="Marauder"),
"U-238 Rounds (Reaper)": ItemData(216 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 16, parent_item="Reaper"),
"G-4 Clusterbomb (Reaper)": ItemData(217 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 17, classification=ItemClassification.progression, parent_item="Reaper"),
+ # Items from EE
+ "Mag-Field Accelerators (Cyclone)": ItemData(218 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 18, parent_item="Cyclone", origin={"ext"}),
+ "Mag-Field Launchers (Cyclone)": ItemData(219 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 19, parent_item="Cyclone", origin={"ext"}),
+ # Items from new mod
+ "Laser Targeting System (Marine)": ItemData(220 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 8, classification=ItemClassification.filler, parent_item="Marine", origin={"nco"}), # Freed slot from Stimpack
+ "Magrail Munitions (Marine)": ItemData(221 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 20, parent_item="Marine", origin={"nco"}),
+ "Optimized Logistics (Marine)": ItemData(222 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 21, classification=ItemClassification.filler, parent_item="Marine", origin={"nco"}),
+ "Restoration (Medic)": ItemData(223 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 22, classification=ItemClassification.filler, parent_item="Medic", origin={"bw"}),
+ "Optical Flare (Medic)": ItemData(224 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 23, classification=ItemClassification.filler, parent_item="Medic", origin={"bw"}),
+ "Optimized Logistics (Medic)": ItemData(225 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 24, classification=ItemClassification.filler, parent_item="Medic", origin={"bw"}),
+ "Progressive Stimpack (Firebat)": ItemData(226 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 6, parent_item="Firebat", quantity=2, origin={"bw"}),
+ "Optimized Logistics (Firebat)": ItemData(227 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 25, parent_item="Firebat", origin={"bw"}),
+ "Progressive Stimpack (Marauder)": ItemData(228 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 8, parent_item="Marauder", quantity=2, origin={"nco"}),
+ "Laser Targeting System (Marauder)": ItemData(229 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 26, classification=ItemClassification.filler, parent_item="Marauder", origin={"nco"}),
+ "Magrail Munitions (Marauder)": ItemData(230 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 27, classification=ItemClassification.filler, parent_item="Marauder", origin={"nco"}),
+ "Internal Tech Module (Marauder)": ItemData(231 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 28, classification=ItemClassification.filler, parent_item="Marauder", origin={"nco"}),
+
+ # Items from new mod
+ "Progressive Stimpack (Reaper)": ItemData(250 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 10, parent_item="Reaper", quantity=2, origin={"nco"}),
+ "Laser Targeting System (Reaper)": ItemData(251 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 0, classification=ItemClassification.filler, parent_item="Reaper", origin={"nco"}),
+ "Advanced Cloaking Field (Reaper)": ItemData(252 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 1, parent_item="Reaper", origin={"nco"}),
+ "Spider Mines (Reaper)": ItemData(253 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 2, classification=ItemClassification.filler, parent_item="Reaper", origin={"nco"}),
+ "Combat Drugs (Reaper)": ItemData(254 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 3, classification=ItemClassification.filler, parent_item="Reaper", origin={"ext"}),
+ "Hellbat Aspect (Hellion)": ItemData(255 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 4, parent_item="Hellion", origin={"nco"}),
+ "Smart Servos (Hellion)": ItemData(256 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 5, parent_item="Hellion", origin={"nco"}),
+ "Optimized Logistics (Hellion)": ItemData(257 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 6, classification=ItemClassification.filler, parent_item="Hellion", origin={"nco"}),
+ "Jump Jets (Hellion)": ItemData(258 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 7, classification=ItemClassification.filler, parent_item="Hellion", origin={"nco"}),
+ "Progressive Stimpack (Hellion)": ItemData(259 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 12, parent_item="Hellion", quantity=2, origin={"nco"}),
+ "Ion Thrusters (Vulture)": ItemData(260 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 8, classification=ItemClassification.filler, parent_item="Vulture", origin={"bw"}),
+ "Auto Launchers (Vulture)": ItemData(261 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 9, parent_item="Vulture", origin={"bw"}),
+ "High Explosive Munition (Spider Mine)": ItemData(262 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 10, origin={"bw"}),
+ "Jump Jets (Goliath)": ItemData(263 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 11, classification=ItemClassification.filler, parent_item="Goliath", origin={"nco"}),
+ "Optimized Logistics (Goliath)": ItemData(264 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 12, classification=ItemClassification.filler, parent_item="Goliath", origin={"nco"}),
+ "Hyperfluxor (Diamondback)": ItemData(265 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 13, parent_item="Diamondback", origin={"ext"}),
+ "Burst Capacitors (Diamondback)": ItemData(266 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 14, classification=ItemClassification.filler, parent_item="Diamondback", origin={"ext"}),
+ "Optimized Logistics (Diamondback)": ItemData(267 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 15, parent_item="Diamondback", origin={"ext"}),
+ "Jump Jets (Siege Tank)": ItemData(268 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 16, parent_item="Siege Tank", origin={"nco"}),
+ "Spider Mines (Siege Tank)": ItemData(269 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 17, classification=ItemClassification.filler, parent_item="Siege Tank", origin={"nco"}),
+ "Smart Servos (Siege Tank)": ItemData(270 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 18, classification=ItemClassification.filler, parent_item="Siege Tank", origin={"nco"}),
+ "Graduating Range (Siege Tank)": ItemData(271 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 19, classification=ItemClassification.progression, parent_item="Siege Tank", origin={"ext"}),
+ "Laser Targeting System (Siege Tank)": ItemData(272 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 20, parent_item="Siege Tank", origin={"nco"}),
+ "Advanced Siege Tech (Siege Tank)": ItemData(273 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 21, parent_item="Siege Tank", origin={"ext"}),
+ "Internal Tech Module (Siege Tank)": ItemData(274 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 22, classification=ItemClassification.filler, parent_item="Siege Tank", origin={"nco"}),
+ "Optimized Logistics (Predator)": ItemData(275 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 23, classification=ItemClassification.filler, parent_item="Predator", origin={"ext"}),
+ "Expanded Hull (Medivac)": ItemData(276 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 24, classification=ItemClassification.filler, parent_item="Medivac", origin={"ext"}),
+ "Afterburners (Medivac)": ItemData(277 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 25, classification=ItemClassification.filler, parent_item="Medivac", origin={"ext"}),
+ "Advanced Laser Technology (Wraith)": ItemData(278 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 26, classification=ItemClassification.progression, parent_item="Wraith", origin={"ext"}),
+ "Smart Servos (Viking)": ItemData(279 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 27, parent_item="Viking", origin={"ext"}),
+ "Magrail Munitions (Viking)": ItemData(280 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 28, parent_item="Viking", origin={"ext"}),
"Twin-Linked Flamethrower (Hellion)": ItemData(300 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 0, classification=ItemClassification.filler, parent_item="Hellion"),
"Thermite Filaments (Hellion)": ItemData(301 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 1, parent_item="Hellion"),
- "Cerberus Mine (Vulture)": ItemData(302 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 2, classification=ItemClassification.filler, parent_item="Vulture"),
+ "Cerberus Mine (Spider Mine)": ItemData(302 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 2, classification=ItemClassification.filler),
"Replenishable Magazine (Vulture)": ItemData(303 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 3, classification=ItemClassification.filler, parent_item="Vulture"),
"Multi-Lock Weapons System (Goliath)": ItemData(304 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 4, parent_item="Goliath"),
"Ares-Class Targeting System (Goliath)": ItemData(305 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 5, parent_item="Goliath"),
@@ -86,7 +149,7 @@ def get_full_item_list():
"Displacement Field (Wraith)": ItemData(313 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 13, classification=ItemClassification.filler, parent_item="Wraith"),
"Ripwave Missiles (Viking)": ItemData(314 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 14, parent_item="Viking"),
"Phobos-Class Weapons System (Viking)": ItemData(315 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 15, parent_item="Viking"),
- "Cross-Spectrum Dampeners (Banshee)": ItemData(316 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 16, classification=ItemClassification.filler, parent_item="Banshee"),
+ "Progressive Cross-Spectrum Dampeners (Banshee)": ItemData(316 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 2, classification=ItemClassification.filler, parent_item="Banshee", quantity=2),
"Shockwave Missile Battery (Banshee)": ItemData(317 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 17, parent_item="Banshee"),
"Missile Pods (Battlecruiser)": ItemData(318 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 18, classification=ItemClassification.filler, parent_item="Battlecruiser"),
"Defensive Matrix (Battlecruiser)": ItemData(319 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 19, classification=ItemClassification.filler, parent_item="Battlecruiser"),
@@ -96,6 +159,47 @@ def get_full_item_list():
"Nyx-Class Cloaking Module (Spectre)": ItemData(323 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 23, parent_item="Spectre"),
"330mm Barrage Cannon (Thor)": ItemData(324 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 24, classification=ItemClassification.filler, parent_item="Thor"),
"Immortality Protocol (Thor)": ItemData(325 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 25, classification=ItemClassification.filler, parent_item="Thor"),
+ # Items from EE
+ "Advanced Ballistics (Liberator)": ItemData(326 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 26, parent_item="Liberator", origin={"ext"}),
+ "Raid Artillery (Liberator)": ItemData(327 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 27, classification=ItemClassification.progression, parent_item="Liberator", origin={"nco"}),
+ "Drilling Claws (Widow Mine)": ItemData(328 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 28, classification=ItemClassification.filler, parent_item="Widow Mine", origin={"ext"}),
+ "Concealment (Widow Mine)": ItemData(329 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 29, classification=ItemClassification.progression, parent_item="Widow Mine", origin={"ext"}),
+
+ #Items from new mod
+ "Hyperflight Rotors (Banshee)": ItemData(350 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 0, classification=ItemClassification.filler, parent_item="Banshee", origin={"ext"}),
+ "Laser Targeting System (Banshee)": ItemData(351 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 1, classification=ItemClassification.filler, parent_item="Banshee", origin={"nco"}),
+ "Internal Tech Module (Banshee)": ItemData(352 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 2, classification=ItemClassification.filler, parent_item="Banshee", origin={"nco"}),
+ "Tactical Jump (Battlecruiser)": ItemData(353 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 3, parent_item="Battlecruiser", origin={"nco", "ext"}),
+ "Cloak (Battlecruiser)": ItemData(354 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 4, parent_item="Battlecruiser", origin={"nco"}),
+ "ATX Laser Battery (Battlecruiser)": ItemData(355 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 5, classification=ItemClassification.progression, parent_item="Battlecruiser", origin={"nco"}),
+ "Optimized Logistics (Battlecruiser)": ItemData(356 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 6, classification=ItemClassification.filler, parent_item="Battlecruiser", origin={"ext"}),
+ "Internal Tech Module (Battlecruiser)": ItemData(357 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 7, classification=ItemClassification.filler, parent_item="Battlecruiser", origin={"nco"}),
+ "EMP Rounds (Ghost)": ItemData(358 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 8, parent_item="Ghost", origin={"ext"}),
+ "Lockdown (Ghost)": ItemData(359 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 9, parent_item="Ghost", origin={"bw"}),
+ "Impaler Rounds (Spectre)": ItemData(360 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 10, parent_item="Spectre", origin={"ext"}),
+ "Progressive High Impact Payload (Thor)": ItemData(361 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 14, parent_item="Thor", quantity=2, origin={"ext"}), # L2 is Smart Servos
+ "Bio Mechanical Repair Drone (Raven)": ItemData(363 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 13, parent_item="Raven", origin={"nco"}),
+ "Spider Mines (Raven)": ItemData(364 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 14, parent_item="Raven", origin={"nco"}),
+ "Railgun Turret (Raven)": ItemData(365 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 15, parent_item="Raven", origin={"nco"}),
+ "Hunter-Seeker Weapon (Raven)": ItemData(366 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 16, parent_item="Raven", origin={"nco"}),
+ "Interference Matrix (Raven)": ItemData(367 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 17, parent_item="Raven", origin={"ext"}),
+ "Anti-Armor Missile (Raven)": ItemData(368 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 18, classification=ItemClassification.filler, parent_item="Raven", origin={"ext"}),
+ "Internal Tech Module (Raven)": ItemData(369 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 19, classification=ItemClassification.filler, parent_item="Raven", origin={"nco"}),
+ "EMP Shockwave (Science Vessel)": ItemData(370 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 20, parent_item="Science Vessel", origin={"bw"}),
+ "Defensive Matrix (Science Vessel)": ItemData(371 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 21, parent_item="Science Vessel", origin={"bw"}),
+ "Targeting Optics (Cyclone)": ItemData(372 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 22, parent_item="Cyclone", origin={"ext"}),
+ "Rapid Fire Launchers (Cyclone)": ItemData(373 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 23, parent_item="Cyclone", origin={"ext"}),
+ "Cloak (Liberator)": ItemData(374 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 24, classification=ItemClassification.filler, parent_item="Liberator", origin={"nco"}),
+ "Laser Targeting System (Liberator)": ItemData(375 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 25, classification=ItemClassification.filler, parent_item="Liberator", origin={"ext"}),
+ "Optimized Logistics (Liberator)": ItemData(376 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 26, classification=ItemClassification.filler, parent_item="Liberator", origin={"nco"}),
+ "Black Market Launchers (Widow Mine)": ItemData(377 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 27, classification=ItemClassification.filler, parent_item="Widow Mine", origin={"ext"}),
+ "Executioner Missiles (Widow Mine)": ItemData(378 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 28, parent_item="Widow Mine", origin={"ext"}),
+
+ # Just lazy to create a new group for one unit
+ "Enhanced Cluster Launchers (Valkyrie)": ItemData(379 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 17, parent_item="Valkyrie", origin={"ext"}),
+ "Shaped Hull (Valkyrie)": ItemData(380 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 20, classification=ItemClassification.filler, parent_item="Valkyrie", origin={"ext"}),
+ "Burst Lasers (Valkyrie)": ItemData(381 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 21, parent_item="Valkyrie", origin={"ext"}),
+ "Afterburners (Valkyrie)": ItemData(382 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 22, classification=ItemClassification.filler, parent_item="Valkyrie", origin={"ext"}),
"Bunker": ItemData(400 + SC2WOL_ITEM_ID_OFFSET, "Building", 0, classification=ItemClassification.progression),
"Missile Turret": ItemData(401 + SC2WOL_ITEM_ID_OFFSET, "Building", 1, classification=ItemClassification.progression),
@@ -120,14 +224,14 @@ def get_full_item_list():
"Science Vessel": ItemData(607 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 7, classification=ItemClassification.progression),
"Tech Reactor": ItemData(608 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 8),
"Orbital Strike": ItemData(609 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 9),
- "Shrike Turret": ItemData(610 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 10, parent_item="Bunker"),
- "Fortified Bunker": ItemData(611 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 11, parent_item="Bunker"),
+ "Shrike Turret (Bunker)": ItemData(610 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 10, parent_item="Bunker"),
+ "Fortified Bunker (Bunker)": ItemData(611 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 11, parent_item="Bunker"),
"Planetary Fortress": ItemData(612 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 12, classification=ItemClassification.progression),
"Perdition Turret": ItemData(613 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 13, classification=ItemClassification.progression),
"Predator": ItemData(614 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 14, classification=ItemClassification.filler),
"Hercules": ItemData(615 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 15, classification=ItemClassification.progression),
"Cellular Reactor": ItemData(616 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 16),
- "Regenerative Bio-Steel": ItemData(617 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 17),
+ "Progressive Regenerative Bio-Steel": ItemData(617 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 4, quantity=2),
"Hive Mind Emulator": ItemData(618 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 18, ItemClassification.progression),
"Psi Disrupter": ItemData(619 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 19, classification=ItemClassification.progression),
@@ -136,18 +240,24 @@ def get_full_item_list():
"High Templar": ItemData(702 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 2, classification=ItemClassification.progression),
"Dark Templar": ItemData(703 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 3, classification=ItemClassification.progression),
"Immortal": ItemData(704 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 4, classification=ItemClassification.progression),
- "Colossus": ItemData(705 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 5, classification=ItemClassification.progression),
- "Phoenix": ItemData(706 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 6, classification=ItemClassification.progression),
+ "Colossus": ItemData(705 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 5),
+ "Phoenix": ItemData(706 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 6, classification=ItemClassification.filler),
"Void Ray": ItemData(707 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 7, classification=ItemClassification.progression),
"Carrier": ItemData(708 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 8, classification=ItemClassification.progression),
+ # Filler items to fill remaining spots
"+15 Starting Minerals": ItemData(800 + SC2WOL_ITEM_ID_OFFSET, "Minerals", 15, quantity=0, classification=ItemClassification.filler),
"+15 Starting Vespene": ItemData(801 + SC2WOL_ITEM_ID_OFFSET, "Vespene", 15, quantity=0, classification=ItemClassification.filler),
+ # This Filler item isn't placed by the generator yet unless plando'd
"+2 Starting Supply": ItemData(802 + SC2WOL_ITEM_ID_OFFSET, "Supply", 2, quantity=0, classification=ItemClassification.filler),
+ # This item is used to "remove" location from the game. Never placed unless plando'd
+ "Nothing": ItemData(803 + SC2WOL_ITEM_ID_OFFSET, "Nothing Group", 2, quantity=0, classification=ItemClassification.trap),
# "Keystone Piece": ItemData(850 + SC2WOL_ITEM_ID_OFFSET, "Goal", 0, quantity=0, classification=ItemClassification.progression_skip_balancing)
}
+def get_item_table(multiworld: MultiWorld, player: int):
+ return item_table
basic_units = {
'Marine',
@@ -172,10 +282,49 @@ def get_basic_units(multiworld: MultiWorld, player: int) -> typing.Set[str]:
item_name_groups = {}
-for item, data in item_table.items():
+for item, data in get_full_item_list().items():
item_name_groups.setdefault(data.type, []).append(item)
+ if data.type in ("Armory 1", "Armory 2") and '(' in item:
+ short_name = item[:item.find(' (')]
+ item_name_groups[short_name] = [item]
item_name_groups["Missions"] = ["Beat " + mission_name for mission_name in vanilla_mission_req_table]
+
+# Items that can be placed before resources if not already in
+# General upgrades and Mercs
+second_pass_placeable_items: typing.Tuple[str, ...] = (
+ # Buildings without upgrades
+ "Sensor Tower",
+ "Hive Mind Emulator",
+ "Psi Disrupter",
+ "Perdition Turret",
+ # General upgrades without any dependencies
+ "Advanced Construction (SCV)",
+ "Dual-Fusion Welders (SCV)",
+ "Fire-Suppression System (Building)",
+ "Orbital Command (Building)",
+ "Ultra-Capacitors",
+ "Vanadium Plating",
+ "Orbital Depots",
+ "Micro-Filtering",
+ "Automated Refinery",
+ "Command Center Reactor",
+ "Tech Reactor",
+ "Planetary Fortress",
+ "Cellular Reactor",
+ "Progressive Regenerative Bio-Steel", # Place only L1
+ # Mercenaries
+ "War Pigs",
+ "Devil Dogs",
+ "Hammer Securities",
+ "Spartan Company",
+ "Siege Breakers",
+ "Hel's Angel",
+ "Dusk Wings",
+ "Jackson's Revenge"
+)
+
+
filler_items: typing.Tuple[str, ...] = (
'+15 Starting Minerals',
'+15 Starting Vespene'
@@ -190,7 +339,10 @@ def get_basic_units(multiworld: MultiWorld, player: int) -> typing.Set[str]:
# Bunker w/ Marine/Marauder: 3,
"Perdition Turret": 2,
"Missile Turret": 2,
- "Vulture": 2
+ "Vulture": 2,
+ "Liberator": 2,
+ "Widow Mine": 2
+ # "Concealment (Widow Mine)": 1
}
zerg_defense_ratings = {
"Perdition Turret": 2,
@@ -199,14 +351,61 @@ def get_basic_units(multiworld: MultiWorld, player: int) -> typing.Set[str]:
"Psi Disruptor": 3
}
+spider_mine_sources = {
+ "Vulture",
+ "Spider Mines (Reaper)",
+ "Spider Mines (Siege Tank)",
+ "Spider Mines (Raven)"
+}
+
+progressive_if_nco = {
+ "Progressive Stimpack (Marine)",
+ "Progressive Stimpack (Firebat)",
+ "Progressive Cross-Spectrum Dampeners (Banshee)",
+ "Progressive Regenerative Bio-Steel"
+}
+
+# 'number' values of upgrades for upgrade bundle items
+upgrade_numbers = [
+ {0, 4, 8}, # Weapon
+ {2, 6, 10}, # Armor
+ {0, 2}, # Infantry
+ {4, 6}, # Vehicle
+ {8, 10}, # Starship
+ {0, 2, 4, 6, 8, 10} # All
+]
+# Names of upgrades to be included for different options
+upgrade_included_names = [
+ { # Individual Items
+ "Progressive Infantry Weapon",
+ "Progressive Infantry Armor",
+ "Progressive Vehicle Weapon",
+ "Progressive Vehicle Armor",
+ "Progressive Ship Weapon",
+ "Progressive Ship Armor"
+ },
+ { # Bundle Weapon And Armor
+ "Progressive Weapon Upgrade",
+ "Progressive Armor Upgrade"
+ },
+ { # Bundle Unit Class
+ "Progressive Infantry Upgrade",
+ "Progressive Vehicle Upgrade",
+ "Progressive Starship Upgrade"
+ },
+ { # Bundle All
+ "Progressive Weapon/Armor Upgrade"
+ }
+]
+
lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in get_full_item_list().items() if
data.code}
# Map type to expected int
type_flaggroups: typing.Dict[str, int] = {
"Unit": 0,
- "Upgrade": 1,
- "Armory 1": 2,
- "Armory 2": 3,
+ "Upgrade": 1, # Weapon / Armor upgrades
+ "Armory 1": 2, # Unit upgrades
+ "Armory 2": 3, # Unit upgrades
"Building": 4,
"Mercenary": 5,
"Laboratory": 6,
@@ -214,5 +413,9 @@ def get_basic_units(multiworld: MultiWorld, player: int) -> typing.Set[str]:
"Minerals": 8,
"Vespene": 9,
"Supply": 10,
- "Goal": 11
+ "Goal": 11,
+ "Armory 3": 12, # Unit upgrades
+ "Armory 4": 13, # Unit upgrades
+ "Progressive Upgrade": 14, # Unit upgrades that exist multiple times (Stimpack / Super Stimpack)
+ "Nothing Group": 15
}
diff --git a/worlds/sc2wol/Locations.py b/worlds/sc2wol/Locations.py
index e91068c4f220..ae31fa8eaadd 100644
--- a/worlds/sc2wol/Locations.py
+++ b/worlds/sc2wol/Locations.py
@@ -1,3 +1,4 @@
+from enum import IntEnum
from typing import List, Tuple, Optional, Callable, NamedTuple
from BaseClasses import MultiWorld
from .Options import get_option_value
@@ -11,10 +12,18 @@ class SC2WoLLocation(Location):
game: str = "Starcraft2WoL"
+class LocationType(IntEnum):
+ VICTORY = 0 # Winning a mission
+ MISSION_PROGRESS = 1 # All tasks done for progressing the mission normally towards victory. All cleaning of expansion bases falls here
+ BONUS = 2 # Bonus objective, getting a campaign or mission bonus in vanilla (credits, research, bonus units or resources)
+ CHALLENGE = 3 # Challenging objectives, often harder than just completing a mission
+ OPTIONAL_BOSS = 4 # Any boss that's not required to win the mission. All Brutalisks, Loki, etc.
+
class LocationData(NamedTuple):
region: str
name: str
code: Optional[int]
+ type: LocationType
rule: Callable = lambda state: True
@@ -22,256 +31,473 @@ def get_locations(multiworld: Optional[MultiWorld], player: Optional[int]) -> Tu
# Note: rules which are ended with or True are rules identified as needed later when restricted units is an option
logic_level = get_option_value(multiworld, player, 'required_tactics')
location_table: List[LocationData] = [
- LocationData("Liberation Day", "Liberation Day: Victory", SC2WOL_LOC_ID_OFFSET + 100),
- LocationData("Liberation Day", "Liberation Day: First Statue", SC2WOL_LOC_ID_OFFSET + 101),
- LocationData("Liberation Day", "Liberation Day: Second Statue", SC2WOL_LOC_ID_OFFSET + 102),
- LocationData("Liberation Day", "Liberation Day: Third Statue", SC2WOL_LOC_ID_OFFSET + 103),
- LocationData("Liberation Day", "Liberation Day: Fourth Statue", SC2WOL_LOC_ID_OFFSET + 104),
- LocationData("Liberation Day", "Liberation Day: Fifth Statue", SC2WOL_LOC_ID_OFFSET + 105),
- LocationData("Liberation Day", "Liberation Day: Sixth Statue", SC2WOL_LOC_ID_OFFSET + 106),
- LocationData("The Outlaws", "The Outlaws: Victory", SC2WOL_LOC_ID_OFFSET + 200,
+ LocationData("Liberation Day", "Liberation Day: Victory", SC2WOL_LOC_ID_OFFSET + 100, LocationType.VICTORY),
+ LocationData("Liberation Day", "Liberation Day: First Statue", SC2WOL_LOC_ID_OFFSET + 101, LocationType.BONUS),
+ LocationData("Liberation Day", "Liberation Day: Second Statue", SC2WOL_LOC_ID_OFFSET + 102, LocationType.BONUS),
+ LocationData("Liberation Day", "Liberation Day: Third Statue", SC2WOL_LOC_ID_OFFSET + 103, LocationType.BONUS),
+ LocationData("Liberation Day", "Liberation Day: Fourth Statue", SC2WOL_LOC_ID_OFFSET + 104, LocationType.BONUS),
+ LocationData("Liberation Day", "Liberation Day: Fifth Statue", SC2WOL_LOC_ID_OFFSET + 105, LocationType.BONUS),
+ LocationData("Liberation Day", "Liberation Day: Sixth Statue", SC2WOL_LOC_ID_OFFSET + 106, LocationType.BONUS),
+ LocationData("Liberation Day", "Liberation Day: Special Delivery", SC2WOL_LOC_ID_OFFSET + 107, LocationType.MISSION_PROGRESS),
+ LocationData("The Outlaws", "The Outlaws: Victory", SC2WOL_LOC_ID_OFFSET + 200, LocationType.VICTORY,
+ lambda state: state._sc2wol_has_common_unit(multiworld, player)),
+ LocationData("The Outlaws", "The Outlaws: Rebel Base", SC2WOL_LOC_ID_OFFSET + 201, LocationType.BONUS,
lambda state: state._sc2wol_has_common_unit(multiworld, player)),
- LocationData("The Outlaws", "The Outlaws: Rebel Base", SC2WOL_LOC_ID_OFFSET + 201,
+ LocationData("The Outlaws", "The Outlaws: North Resource Pickups", SC2WOL_LOC_ID_OFFSET + 202, LocationType.BONUS),
+ LocationData("The Outlaws", "The Outlaws: Bunker", SC2WOL_LOC_ID_OFFSET + 203, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_common_unit(multiworld, player)),
- LocationData("Zero Hour", "Zero Hour: Victory", SC2WOL_LOC_ID_OFFSET + 300,
+ LocationData("Zero Hour", "Zero Hour: Victory", SC2WOL_LOC_ID_OFFSET + 300, LocationType.VICTORY,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
state._sc2wol_defense_rating(multiworld, player, True) >= 2 and
(logic_level > 0 or state._sc2wol_has_anti_air(multiworld, player))),
- LocationData("Zero Hour", "Zero Hour: First Group Rescued", SC2WOL_LOC_ID_OFFSET + 301),
- LocationData("Zero Hour", "Zero Hour: Second Group Rescued", SC2WOL_LOC_ID_OFFSET + 302,
+ LocationData("Zero Hour", "Zero Hour: First Group Rescued", SC2WOL_LOC_ID_OFFSET + 301, LocationType.BONUS),
+ LocationData("Zero Hour", "Zero Hour: Second Group Rescued", SC2WOL_LOC_ID_OFFSET + 302, LocationType.BONUS,
lambda state: state._sc2wol_has_common_unit(multiworld, player)),
- LocationData("Zero Hour", "Zero Hour: Third Group Rescued", SC2WOL_LOC_ID_OFFSET + 303,
+ LocationData("Zero Hour", "Zero Hour: Third Group Rescued", SC2WOL_LOC_ID_OFFSET + 303, LocationType.BONUS,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
state._sc2wol_defense_rating(multiworld, player, True) >= 2),
- LocationData("Evacuation", "Evacuation: Victory", SC2WOL_LOC_ID_OFFSET + 400,
+ LocationData("Zero Hour", "Zero Hour: First Hatchery", SC2WOL_LOC_ID_OFFSET + 304, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
+ LocationData("Zero Hour", "Zero Hour: Second Hatchery", SC2WOL_LOC_ID_OFFSET + 305, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
+ LocationData("Zero Hour", "Zero Hour: Third Hatchery", SC2WOL_LOC_ID_OFFSET + 306, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
+ LocationData("Zero Hour", "Zero Hour: Fourth Hatchery", SC2WOL_LOC_ID_OFFSET + 307, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
+ LocationData("Evacuation", "Evacuation: Victory", SC2WOL_LOC_ID_OFFSET + 400, LocationType.VICTORY,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
(logic_level > 0 and state._sc2wol_has_anti_air(multiworld, player)
or state._sc2wol_has_competent_anti_air(multiworld, player))),
- LocationData("Evacuation", "Evacuation: First Chysalis", SC2WOL_LOC_ID_OFFSET + 401),
- LocationData("Evacuation", "Evacuation: Second Chysalis", SC2WOL_LOC_ID_OFFSET + 402,
+ LocationData("Evacuation", "Evacuation: First Chrysalis", SC2WOL_LOC_ID_OFFSET + 401, LocationType.BONUS),
+ LocationData("Evacuation", "Evacuation: Second Chrysalis", SC2WOL_LOC_ID_OFFSET + 402, LocationType.BONUS,
lambda state: state._sc2wol_has_common_unit(multiworld, player)),
- LocationData("Evacuation", "Evacuation: Third Chysalis", SC2WOL_LOC_ID_OFFSET + 403,
+ LocationData("Evacuation", "Evacuation: Third Chrysalis", SC2WOL_LOC_ID_OFFSET + 403, LocationType.BONUS,
lambda state: state._sc2wol_has_common_unit(multiworld, player)),
- LocationData("Outbreak", "Outbreak: Victory", SC2WOL_LOC_ID_OFFSET + 500,
+ LocationData("Evacuation", "Evacuation: Reach Hanson", SC2WOL_LOC_ID_OFFSET + 404, LocationType.MISSION_PROGRESS),
+ LocationData("Evacuation", "Evacuation: Secret Resource Stash", SC2WOL_LOC_ID_OFFSET + 405, LocationType.BONUS),
+ LocationData("Evacuation", "Evacuation: Flawless", SC2WOL_LOC_ID_OFFSET + 406, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_has_common_unit(multiworld, player) and
+ state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and
+ (logic_level > 0 and state._sc2wol_has_anti_air(multiworld, player)
+ or state._sc2wol_has_competent_anti_air(multiworld, player))),
+ LocationData("Outbreak", "Outbreak: Victory", SC2WOL_LOC_ID_OFFSET + 500, LocationType.VICTORY,
lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 4 and
(state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
- LocationData("Outbreak", "Outbreak: Left Infestor", SC2WOL_LOC_ID_OFFSET + 501,
+ LocationData("Outbreak", "Outbreak: Left Infestor", SC2WOL_LOC_ID_OFFSET + 501, LocationType.BONUS,
+ lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and
+ (state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
+ LocationData("Outbreak", "Outbreak: Right Infestor", SC2WOL_LOC_ID_OFFSET + 502, LocationType.BONUS,
+ lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and
+ (state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
+ LocationData("Outbreak", "Outbreak: North Infested Command Center", SC2WOL_LOC_ID_OFFSET + 503, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and
(state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
- LocationData("Outbreak", "Outbreak: Right Infestor", SC2WOL_LOC_ID_OFFSET + 502,
+ LocationData("Outbreak", "Outbreak: South Infested Command Center", SC2WOL_LOC_ID_OFFSET + 504, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and
(state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
- LocationData("Safe Haven", "Safe Haven: Victory", SC2WOL_LOC_ID_OFFSET + 600,
+ LocationData("Outbreak", "Outbreak: Northwest Bar", SC2WOL_LOC_ID_OFFSET + 505, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and
+ (state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
+ LocationData("Outbreak", "Outbreak: North Bar", SC2WOL_LOC_ID_OFFSET + 506, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and
+ (state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
+ LocationData("Outbreak", "Outbreak: South Bar", SC2WOL_LOC_ID_OFFSET + 507, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and
+ (state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
+ LocationData("Safe Haven", "Safe Haven: Victory", SC2WOL_LOC_ID_OFFSET + 600, LocationType.VICTORY,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
state._sc2wol_has_competent_anti_air(multiworld, player)),
- LocationData("Safe Haven", "Safe Haven: North Nexus", SC2WOL_LOC_ID_OFFSET + 601,
+ LocationData("Safe Haven", "Safe Haven: North Nexus", SC2WOL_LOC_ID_OFFSET + 601, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
state._sc2wol_has_competent_anti_air(multiworld, player)),
- LocationData("Safe Haven", "Safe Haven: East Nexus", SC2WOL_LOC_ID_OFFSET + 602,
+ LocationData("Safe Haven", "Safe Haven: East Nexus", SC2WOL_LOC_ID_OFFSET + 602, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
state._sc2wol_has_competent_anti_air(multiworld, player)),
- LocationData("Safe Haven", "Safe Haven: South Nexus", SC2WOL_LOC_ID_OFFSET + 603,
+ LocationData("Safe Haven", "Safe Haven: South Nexus", SC2WOL_LOC_ID_OFFSET + 603, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
state._sc2wol_has_competent_anti_air(multiworld, player)),
- LocationData("Haven's Fall", "Haven's Fall: Victory", SC2WOL_LOC_ID_OFFSET + 700,
+ LocationData("Safe Haven", "Safe Haven: First Terror Fleet", SC2WOL_LOC_ID_OFFSET + 604, LocationType.BONUS,
+ lambda state: state._sc2wol_has_common_unit(multiworld, player) and
+ state._sc2wol_has_competent_anti_air(multiworld, player)),
+ LocationData("Safe Haven", "Safe Haven: Second Terror Fleet", SC2WOL_LOC_ID_OFFSET + 605, LocationType.BONUS,
+ lambda state: state._sc2wol_has_common_unit(multiworld, player) and
+ state._sc2wol_has_competent_anti_air(multiworld, player)),
+ LocationData("Safe Haven", "Safe Haven: Third Terror Fleet", SC2WOL_LOC_ID_OFFSET + 606, LocationType.BONUS,
+ lambda state: state._sc2wol_has_common_unit(multiworld, player) and
+ state._sc2wol_has_competent_anti_air(multiworld, player)),
+ LocationData("Haven's Fall", "Haven's Fall: Victory", SC2WOL_LOC_ID_OFFSET + 700, LocationType.VICTORY,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
state._sc2wol_has_competent_anti_air(multiworld, player) and
state._sc2wol_defense_rating(multiworld, player, True) >= 3),
- LocationData("Haven's Fall", "Haven's Fall: North Hive", SC2WOL_LOC_ID_OFFSET + 701,
+ LocationData("Haven's Fall", "Haven's Fall: North Hive", SC2WOL_LOC_ID_OFFSET + 701, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
state._sc2wol_has_competent_anti_air(multiworld, player) and
state._sc2wol_defense_rating(multiworld, player, True) >= 3),
- LocationData("Haven's Fall", "Haven's Fall: East Hive", SC2WOL_LOC_ID_OFFSET + 702,
+ LocationData("Haven's Fall", "Haven's Fall: East Hive", SC2WOL_LOC_ID_OFFSET + 702, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
state._sc2wol_has_competent_anti_air(multiworld, player) and
state._sc2wol_defense_rating(multiworld, player, True) >= 3),
- LocationData("Haven's Fall", "Haven's Fall: South Hive", SC2WOL_LOC_ID_OFFSET + 703,
+ LocationData("Haven's Fall", "Haven's Fall: South Hive", SC2WOL_LOC_ID_OFFSET + 703, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
state._sc2wol_has_competent_anti_air(multiworld, player) and
state._sc2wol_defense_rating(multiworld, player, True) >= 3),
- LocationData("Smash and Grab", "Smash and Grab: Victory", SC2WOL_LOC_ID_OFFSET + 800,
+ LocationData("Haven's Fall", "Haven's Fall: Northeast Colony Base", SC2WOL_LOC_ID_OFFSET + 704, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_can_respond_to_colony_infestations),
+ LocationData("Haven's Fall", "Haven's Fall: East Colony Base", SC2WOL_LOC_ID_OFFSET + 705, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_can_respond_to_colony_infestations),
+ LocationData("Haven's Fall", "Haven's Fall: Middle Colony Base", SC2WOL_LOC_ID_OFFSET + 706, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_can_respond_to_colony_infestations),
+ LocationData("Haven's Fall", "Haven's Fall: Southeast Colony Base", SC2WOL_LOC_ID_OFFSET + 707, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_can_respond_to_colony_infestations),
+ LocationData("Haven's Fall", "Haven's Fall: Southwest Colony Base", SC2WOL_LOC_ID_OFFSET + 708, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_can_respond_to_colony_infestations),
+ LocationData("Smash and Grab", "Smash and Grab: Victory", SC2WOL_LOC_ID_OFFSET + 800, LocationType.VICTORY,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
(logic_level > 0 and state._sc2wol_has_anti_air(multiworld, player)
or state._sc2wol_has_competent_anti_air(multiworld, player))),
- LocationData("Smash and Grab", "Smash and Grab: First Relic", SC2WOL_LOC_ID_OFFSET + 801),
- LocationData("Smash and Grab", "Smash and Grab: Second Relic", SC2WOL_LOC_ID_OFFSET + 802),
- LocationData("Smash and Grab", "Smash and Grab: Third Relic", SC2WOL_LOC_ID_OFFSET + 803,
+ LocationData("Smash and Grab", "Smash and Grab: First Relic", SC2WOL_LOC_ID_OFFSET + 801, LocationType.BONUS),
+ LocationData("Smash and Grab", "Smash and Grab: Second Relic", SC2WOL_LOC_ID_OFFSET + 802, LocationType.BONUS),
+ LocationData("Smash and Grab", "Smash and Grab: Third Relic", SC2WOL_LOC_ID_OFFSET + 803, LocationType.BONUS,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
(logic_level > 0 and state._sc2wol_has_anti_air(multiworld, player)
or state._sc2wol_has_competent_anti_air(multiworld, player))),
- LocationData("Smash and Grab", "Smash and Grab: Fourth Relic", SC2WOL_LOC_ID_OFFSET + 804,
+ LocationData("Smash and Grab", "Smash and Grab: Fourth Relic", SC2WOL_LOC_ID_OFFSET + 804, LocationType.BONUS,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
(logic_level > 0 and state._sc2wol_has_anti_air(multiworld, player)
or state._sc2wol_has_competent_anti_air(multiworld, player))),
- LocationData("The Dig", "The Dig: Victory", SC2WOL_LOC_ID_OFFSET + 900,
+ LocationData("Smash and Grab", "Smash and Grab: First Forcefield Area Busted", SC2WOL_LOC_ID_OFFSET + 805, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_common_unit(multiworld, player) and
+ (logic_level > 0 and state._sc2wol_has_anti_air(multiworld, player)
+ or state._sc2wol_has_competent_anti_air(multiworld, player))),
+ LocationData("Smash and Grab", "Smash and Grab: Second Forcefield Area Busted", SC2WOL_LOC_ID_OFFSET + 806, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_common_unit(multiworld, player) and
+ (logic_level > 0 and state._sc2wol_has_anti_air(multiworld, player)
+ or state._sc2wol_has_competent_anti_air(multiworld, player))),
+ LocationData("The Dig", "The Dig: Victory", SC2WOL_LOC_ID_OFFSET + 900, LocationType.VICTORY,
lambda state: state._sc2wol_has_anti_air(multiworld, player) and
state._sc2wol_defense_rating(multiworld, player, False) >= 7),
- LocationData("The Dig", "The Dig: Left Relic", SC2WOL_LOC_ID_OFFSET + 901,
+ LocationData("The Dig", "The Dig: Left Relic", SC2WOL_LOC_ID_OFFSET + 901, LocationType.BONUS,
lambda state: state._sc2wol_defense_rating(multiworld, player, False) >= 5),
- LocationData("The Dig", "The Dig: Right Ground Relic", SC2WOL_LOC_ID_OFFSET + 902,
+ LocationData("The Dig", "The Dig: Right Ground Relic", SC2WOL_LOC_ID_OFFSET + 902, LocationType.BONUS,
lambda state: state._sc2wol_defense_rating(multiworld, player, False) >= 5),
- LocationData("The Dig", "The Dig: Right Cliff Relic", SC2WOL_LOC_ID_OFFSET + 903,
+ LocationData("The Dig", "The Dig: Right Cliff Relic", SC2WOL_LOC_ID_OFFSET + 903, LocationType.BONUS,
lambda state: state._sc2wol_defense_rating(multiworld, player, False) >= 5),
- LocationData("The Moebius Factor", "The Moebius Factor: Victory", SC2WOL_LOC_ID_OFFSET + 1000,
+ LocationData("The Dig", "The Dig: Moebius Base", SC2WOL_LOC_ID_OFFSET + 904, LocationType.MISSION_PROGRESS),
+ LocationData("The Moebius Factor", "The Moebius Factor: Victory", SC2WOL_LOC_ID_OFFSET + 1000, LocationType.VICTORY,
lambda state: state._sc2wol_has_anti_air(multiworld, player) and
(state._sc2wol_has_air(multiworld, player)
or state.has_any({'Medivac', 'Hercules'}, player)
and state._sc2wol_has_common_unit(multiworld, player))),
- LocationData("The Moebius Factor", "The Moebius Factor: South Rescue", SC2WOL_LOC_ID_OFFSET + 1003,
+ LocationData("The Moebius Factor", "The Moebius Factor: 1st Data Core", SC2WOL_LOC_ID_OFFSET + 1001, LocationType.MISSION_PROGRESS),
+ LocationData("The Moebius Factor", "The Moebius Factor: 2nd Data Core", SC2WOL_LOC_ID_OFFSET + 1002, LocationType.MISSION_PROGRESS,
+ lambda state: (state._sc2wol_has_air(multiworld, player)
+ or state.has_any({'Medivac', 'Hercules'}, player)
+ and state._sc2wol_has_common_unit(multiworld, player))),
+ LocationData("The Moebius Factor", "The Moebius Factor: South Rescue", SC2WOL_LOC_ID_OFFSET + 1003, LocationType.BONUS,
lambda state: state._sc2wol_able_to_rescue(multiworld, player)),
- LocationData("The Moebius Factor", "The Moebius Factor: Wall Rescue", SC2WOL_LOC_ID_OFFSET + 1004,
+ LocationData("The Moebius Factor", "The Moebius Factor: Wall Rescue", SC2WOL_LOC_ID_OFFSET + 1004, LocationType.BONUS,
lambda state: state._sc2wol_able_to_rescue(multiworld, player)),
- LocationData("The Moebius Factor", "The Moebius Factor: Mid Rescue", SC2WOL_LOC_ID_OFFSET + 1005,
+ LocationData("The Moebius Factor", "The Moebius Factor: Mid Rescue", SC2WOL_LOC_ID_OFFSET + 1005, LocationType.BONUS,
lambda state: state._sc2wol_able_to_rescue(multiworld, player)),
- LocationData("The Moebius Factor", "The Moebius Factor: Nydus Roof Rescue", SC2WOL_LOC_ID_OFFSET + 1006,
+ LocationData("The Moebius Factor", "The Moebius Factor: Nydus Roof Rescue", SC2WOL_LOC_ID_OFFSET + 1006, LocationType.BONUS,
lambda state: state._sc2wol_able_to_rescue(multiworld, player)),
- LocationData("The Moebius Factor", "The Moebius Factor: Alive Inside Rescue", SC2WOL_LOC_ID_OFFSET + 1007,
+ LocationData("The Moebius Factor", "The Moebius Factor: Alive Inside Rescue", SC2WOL_LOC_ID_OFFSET + 1007, LocationType.BONUS,
lambda state: state._sc2wol_able_to_rescue(multiworld, player)),
- LocationData("The Moebius Factor", "The Moebius Factor: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1008,
+ LocationData("The Moebius Factor", "The Moebius Factor: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1008, LocationType.OPTIONAL_BOSS,
lambda state: state._sc2wol_has_anti_air(multiworld, player) and
(state._sc2wol_has_air(multiworld, player)
or state.has_any({'Medivac', 'Hercules'}, player)
and state._sc2wol_has_common_unit(multiworld, player))),
- LocationData("Supernova", "Supernova: Victory", SC2WOL_LOC_ID_OFFSET + 1100,
+ LocationData("The Moebius Factor", "The Moebius Factor: 3rd Data Core", SC2WOL_LOC_ID_OFFSET + 1009, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_anti_air(multiworld, player) and
+ (state._sc2wol_has_air(multiworld, player)
+ or state.has_any({'Medivac', 'Hercules'}, player)
+ and state._sc2wol_has_common_unit(multiworld, player))),
+ LocationData("Supernova", "Supernova: Victory", SC2WOL_LOC_ID_OFFSET + 1100, LocationType.VICTORY,
+ lambda state: state._sc2wol_beats_protoss_deathball(multiworld, player)),
+ LocationData("Supernova", "Supernova: West Relic", SC2WOL_LOC_ID_OFFSET + 1101, LocationType.BONUS),
+ LocationData("Supernova", "Supernova: North Relic", SC2WOL_LOC_ID_OFFSET + 1102, LocationType.BONUS),
+ LocationData("Supernova", "Supernova: South Relic", SC2WOL_LOC_ID_OFFSET + 1103, LocationType.BONUS,
+ lambda state: state._sc2wol_beats_protoss_deathball(multiworld, player)),
+ LocationData("Supernova", "Supernova: East Relic", SC2WOL_LOC_ID_OFFSET + 1104, LocationType.BONUS,
lambda state: state._sc2wol_beats_protoss_deathball(multiworld, player)),
- LocationData("Supernova", "Supernova: West Relic", SC2WOL_LOC_ID_OFFSET + 1101),
- LocationData("Supernova", "Supernova: North Relic", SC2WOL_LOC_ID_OFFSET + 1102),
- LocationData("Supernova", "Supernova: South Relic", SC2WOL_LOC_ID_OFFSET + 1103,
+ LocationData("Supernova", "Supernova: Landing Zone Cleared", SC2WOL_LOC_ID_OFFSET + 1105, LocationType.MISSION_PROGRESS),
+ LocationData("Supernova", "Supernova: Middle Base", SC2WOL_LOC_ID_OFFSET + 1106, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_beats_protoss_deathball(multiworld, player)),
- LocationData("Supernova", "Supernova: East Relic", SC2WOL_LOC_ID_OFFSET + 1104,
+ LocationData("Supernova", "Supernova: Southeast Base", SC2WOL_LOC_ID_OFFSET + 1107, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_beats_protoss_deathball(multiworld, player)),
- LocationData("Maw of the Void", "Maw of the Void: Victory", SC2WOL_LOC_ID_OFFSET + 1200,
+ LocationData("Maw of the Void", "Maw of the Void: Victory", SC2WOL_LOC_ID_OFFSET + 1200, LocationType.VICTORY,
lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
- LocationData("Maw of the Void", "Maw of the Void: Landing Zone Cleared", SC2WOL_LOC_ID_OFFSET + 1201),
- LocationData("Maw of the Void", "Maw of the Void: Expansion Prisoners", SC2WOL_LOC_ID_OFFSET + 1202,
+ LocationData("Maw of the Void", "Maw of the Void: Landing Zone Cleared", SC2WOL_LOC_ID_OFFSET + 1201, LocationType.MISSION_PROGRESS),
+ LocationData("Maw of the Void", "Maw of the Void: Expansion Prisoners", SC2WOL_LOC_ID_OFFSET + 1202, LocationType.BONUS,
lambda state: logic_level > 0 or state._sc2wol_survives_rip_field(multiworld, player)),
- LocationData("Maw of the Void", "Maw of the Void: South Close Prisoners", SC2WOL_LOC_ID_OFFSET + 1203,
+ LocationData("Maw of the Void", "Maw of the Void: South Close Prisoners", SC2WOL_LOC_ID_OFFSET + 1203, LocationType.BONUS,
lambda state: logic_level > 0 or state._sc2wol_survives_rip_field(multiworld, player)),
- LocationData("Maw of the Void", "Maw of the Void: South Far Prisoners", SC2WOL_LOC_ID_OFFSET + 1204,
+ LocationData("Maw of the Void", "Maw of the Void: South Far Prisoners", SC2WOL_LOC_ID_OFFSET + 1204, LocationType.BONUS,
lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
- LocationData("Maw of the Void", "Maw of the Void: North Prisoners", SC2WOL_LOC_ID_OFFSET + 1205,
+ LocationData("Maw of the Void", "Maw of the Void: North Prisoners", SC2WOL_LOC_ID_OFFSET + 1205, LocationType.BONUS,
lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
- LocationData("Devil's Playground", "Devil's Playground: Victory", SC2WOL_LOC_ID_OFFSET + 1300,
+ LocationData("Maw of the Void", "Maw of the Void: Mothership", SC2WOL_LOC_ID_OFFSET + 1206, LocationType.OPTIONAL_BOSS,
+ lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
+ LocationData("Maw of the Void", "Maw of the Void: Expansion Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1207, LocationType.MISSION_PROGRESS,
+ lambda state: logic_level > 0 or state._sc2wol_survives_rip_field(multiworld, player)),
+ LocationData("Maw of the Void", "Maw of the Void: Middle Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1208, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
+ LocationData("Maw of the Void", "Maw of the Void: Southeast Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1209, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
+ LocationData("Maw of the Void", "Maw of the Void: Stargate Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1210, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
+ LocationData("Maw of the Void", "Maw of the Void: Northwest Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1211, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
+ LocationData("Maw of the Void", "Maw of the Void: West Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1212, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
+ LocationData("Maw of the Void", "Maw of the Void: Southwest Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1213, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
+ LocationData("Devil's Playground", "Devil's Playground: Victory", SC2WOL_LOC_ID_OFFSET + 1300, LocationType.VICTORY,
lambda state: logic_level > 0 or
state._sc2wol_has_anti_air(multiworld, player) and (
state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
- LocationData("Devil's Playground", "Devil's Playground: Tosh's Miners", SC2WOL_LOC_ID_OFFSET + 1301),
- LocationData("Devil's Playground", "Devil's Playground: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1302,
+ LocationData("Devil's Playground", "Devil's Playground: Tosh's Miners", SC2WOL_LOC_ID_OFFSET + 1301, LocationType.BONUS),
+ LocationData("Devil's Playground", "Devil's Playground: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1302, LocationType.OPTIONAL_BOSS,
lambda state: logic_level > 0 or state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player)),
- LocationData("Welcome to the Jungle", "Welcome to the Jungle: Victory", SC2WOL_LOC_ID_OFFSET + 1400,
- lambda state: state._sc2wol_has_common_unit(multiworld, player) and
- state._sc2wol_has_competent_anti_air(multiworld, player)),
- LocationData("Welcome to the Jungle", "Welcome to the Jungle: Close Relic", SC2WOL_LOC_ID_OFFSET + 1401),
- LocationData("Welcome to the Jungle", "Welcome to the Jungle: West Relic", SC2WOL_LOC_ID_OFFSET + 1402,
- lambda state: state._sc2wol_has_common_unit(multiworld, player) and
- state._sc2wol_has_competent_anti_air(multiworld, player)),
- LocationData("Welcome to the Jungle", "Welcome to the Jungle: North-East Relic", SC2WOL_LOC_ID_OFFSET + 1403,
- lambda state: state._sc2wol_has_common_unit(multiworld, player) and
- state._sc2wol_has_competent_anti_air(multiworld, player)),
- LocationData("Breakout", "Breakout: Victory", SC2WOL_LOC_ID_OFFSET + 1500),
- LocationData("Breakout", "Breakout: Diamondback Prison", SC2WOL_LOC_ID_OFFSET + 1501),
- LocationData("Breakout", "Breakout: Siegetank Prison", SC2WOL_LOC_ID_OFFSET + 1502),
- LocationData("Ghost of a Chance", "Ghost of a Chance: Victory", SC2WOL_LOC_ID_OFFSET + 1600),
- LocationData("Ghost of a Chance", "Ghost of a Chance: Terrazine Tank", SC2WOL_LOC_ID_OFFSET + 1601),
- LocationData("Ghost of a Chance", "Ghost of a Chance: Jorium Stockpile", SC2WOL_LOC_ID_OFFSET + 1602),
- LocationData("Ghost of a Chance", "Ghost of a Chance: First Island Spectres", SC2WOL_LOC_ID_OFFSET + 1603),
- LocationData("Ghost of a Chance", "Ghost of a Chance: Second Island Spectres", SC2WOL_LOC_ID_OFFSET + 1604),
- LocationData("Ghost of a Chance", "Ghost of a Chance: Third Island Spectres", SC2WOL_LOC_ID_OFFSET + 1605),
- LocationData("The Great Train Robbery", "The Great Train Robbery: Victory", SC2WOL_LOC_ID_OFFSET + 1700,
+ LocationData("Devil's Playground", "Devil's Playground: North Reapers", SC2WOL_LOC_ID_OFFSET + 1303, LocationType.BONUS),
+ LocationData("Devil's Playground", "Devil's Playground: Middle Reapers", SC2WOL_LOC_ID_OFFSET + 1304, LocationType.BONUS,
+ lambda state: logic_level > 0 or state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player)),
+ LocationData("Devil's Playground", "Devil's Playground: Southwest Reapers", SC2WOL_LOC_ID_OFFSET + 1305, LocationType.BONUS,
+ lambda state: logic_level > 0 or state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player)),
+ LocationData("Devil's Playground", "Devil's Playground: Southeast Reapers", SC2WOL_LOC_ID_OFFSET + 1306, LocationType.BONUS,
+ lambda state: logic_level > 0 or
+ state._sc2wol_has_anti_air(multiworld, player) and (
+ state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
+ LocationData("Devil's Playground", "Devil's Playground: East Reapers", SC2WOL_LOC_ID_OFFSET + 1307, LocationType.BONUS,
+ lambda state: state._sc2wol_has_anti_air(multiworld, player) and
+ (logic_level > 0 or
+ state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
+ LocationData("Welcome to the Jungle", "Welcome to the Jungle: Victory", SC2WOL_LOC_ID_OFFSET + 1400, LocationType.VICTORY,
+ lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)),
+ LocationData("Welcome to the Jungle", "Welcome to the Jungle: Close Relic", SC2WOL_LOC_ID_OFFSET + 1401, LocationType.BONUS),
+ LocationData("Welcome to the Jungle", "Welcome to the Jungle: West Relic", SC2WOL_LOC_ID_OFFSET + 1402, LocationType.BONUS,
+ lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)),
+ LocationData("Welcome to the Jungle", "Welcome to the Jungle: North-East Relic", SC2WOL_LOC_ID_OFFSET + 1403, LocationType.BONUS,
+ lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)),
+ LocationData("Welcome to the Jungle", "Welcome to the Jungle: Middle Base", SC2WOL_LOC_ID_OFFSET + 1404, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)),
+ LocationData("Welcome to the Jungle", "Welcome to the Jungle: Main Base", SC2WOL_LOC_ID_OFFSET + 1405, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)
+ and state._sc2wol_beats_protoss_deathball(multiworld, player)),
+ LocationData("Welcome to the Jungle", "Welcome to the Jungle: No Terrazine Nodes Sealed", SC2WOL_LOC_ID_OFFSET + 1406, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)
+ and state._sc2wol_has_competent_ground_to_air(multiworld, player)
+ and state._sc2wol_beats_protoss_deathball(multiworld, player)),
+ LocationData("Welcome to the Jungle", "Welcome to the Jungle: Up to 1 Terrazine Node Sealed", SC2WOL_LOC_ID_OFFSET + 1407, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)
+ and state._sc2wol_has_competent_ground_to_air(multiworld, player)
+ and state._sc2wol_beats_protoss_deathball(multiworld, player)),
+ LocationData("Welcome to the Jungle", "Welcome to the Jungle: Up to 2 Terrazine Nodes Sealed", SC2WOL_LOC_ID_OFFSET + 1408, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)
+ and state._sc2wol_beats_protoss_deathball(multiworld, player)),
+ LocationData("Welcome to the Jungle", "Welcome to the Jungle: Up to 3 Terrazine Nodes Sealed", SC2WOL_LOC_ID_OFFSET + 1409, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)
+ and state._sc2wol_has_competent_comp(multiworld, player)),
+ LocationData("Welcome to the Jungle", "Welcome to the Jungle: Up to 4 Terrazine Nodes Sealed", SC2WOL_LOC_ID_OFFSET + 1410, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)),
+ LocationData("Welcome to the Jungle", "Welcome to the Jungle: Up to 5 Terrazine Nodes Sealed", SC2WOL_LOC_ID_OFFSET + 1411, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)),
+ LocationData("Breakout", "Breakout: Victory", SC2WOL_LOC_ID_OFFSET + 1500, LocationType.VICTORY),
+ LocationData("Breakout", "Breakout: Diamondback Prison", SC2WOL_LOC_ID_OFFSET + 1501, LocationType.BONUS),
+ LocationData("Breakout", "Breakout: Siege Tank Prison", SC2WOL_LOC_ID_OFFSET + 1502, LocationType.BONUS),
+ LocationData("Breakout", "Breakout: First Checkpoint", SC2WOL_LOC_ID_OFFSET + 1503, LocationType.MISSION_PROGRESS),
+ LocationData("Breakout", "Breakout: Second Checkpoint", SC2WOL_LOC_ID_OFFSET + 1504, LocationType.MISSION_PROGRESS),
+ LocationData("Ghost of a Chance", "Ghost of a Chance: Victory", SC2WOL_LOC_ID_OFFSET + 1600, LocationType.VICTORY),
+ LocationData("Ghost of a Chance", "Ghost of a Chance: Terrazine Tank", SC2WOL_LOC_ID_OFFSET + 1601, LocationType.MISSION_PROGRESS),
+ LocationData("Ghost of a Chance", "Ghost of a Chance: Jorium Stockpile", SC2WOL_LOC_ID_OFFSET + 1602, LocationType.MISSION_PROGRESS),
+ LocationData("Ghost of a Chance", "Ghost of a Chance: First Island Spectres", SC2WOL_LOC_ID_OFFSET + 1603, LocationType.BONUS),
+ LocationData("Ghost of a Chance", "Ghost of a Chance: Second Island Spectres", SC2WOL_LOC_ID_OFFSET + 1604, LocationType.BONUS),
+ LocationData("Ghost of a Chance", "Ghost of a Chance: Third Island Spectres", SC2WOL_LOC_ID_OFFSET + 1605, LocationType.BONUS),
+ LocationData("The Great Train Robbery", "The Great Train Robbery: Victory", SC2WOL_LOC_ID_OFFSET + 1700, LocationType.VICTORY,
lambda state: state._sc2wol_has_train_killers(multiworld, player) and
state._sc2wol_has_anti_air(multiworld, player)),
- LocationData("The Great Train Robbery", "The Great Train Robbery: North Defiler", SC2WOL_LOC_ID_OFFSET + 1701),
- LocationData("The Great Train Robbery", "The Great Train Robbery: Mid Defiler", SC2WOL_LOC_ID_OFFSET + 1702),
- LocationData("The Great Train Robbery", "The Great Train Robbery: South Defiler", SC2WOL_LOC_ID_OFFSET + 1703),
- LocationData("Cutthroat", "Cutthroat: Victory", SC2WOL_LOC_ID_OFFSET + 1800,
+ LocationData("The Great Train Robbery", "The Great Train Robbery: North Defiler", SC2WOL_LOC_ID_OFFSET + 1701, LocationType.BONUS),
+ LocationData("The Great Train Robbery", "The Great Train Robbery: Mid Defiler", SC2WOL_LOC_ID_OFFSET + 1702, LocationType.BONUS),
+ LocationData("The Great Train Robbery", "The Great Train Robbery: South Defiler", SC2WOL_LOC_ID_OFFSET + 1703, LocationType.BONUS),
+ LocationData("The Great Train Robbery", "The Great Train Robbery: Close Diamondback", SC2WOL_LOC_ID_OFFSET + 1704, LocationType.BONUS),
+ LocationData("The Great Train Robbery", "The Great Train Robbery: Northwest Diamondback", SC2WOL_LOC_ID_OFFSET + 1705, LocationType.BONUS),
+ LocationData("The Great Train Robbery", "The Great Train Robbery: North Diamondback", SC2WOL_LOC_ID_OFFSET + 1706, LocationType.BONUS),
+ LocationData("The Great Train Robbery", "The Great Train Robbery: Northeast Diamondback", SC2WOL_LOC_ID_OFFSET + 1707, LocationType.BONUS),
+ LocationData("The Great Train Robbery", "The Great Train Robbery: Southwest Diamondback", SC2WOL_LOC_ID_OFFSET + 1708, LocationType.BONUS),
+ LocationData("The Great Train Robbery", "The Great Train Robbery: Southeast Diamondback", SC2WOL_LOC_ID_OFFSET + 1709, LocationType.BONUS),
+ LocationData("The Great Train Robbery", "The Great Train Robbery: Kill Team", SC2WOL_LOC_ID_OFFSET + 1710, LocationType.CHALLENGE,
+ lambda state: (logic_level > 0 or state._sc2wol_has_common_unit(multiworld, player)) and
+ state._sc2wol_has_train_killers(multiworld, player) and
+ state._sc2wol_has_anti_air(multiworld, player)),
+ LocationData("Cutthroat", "Cutthroat: Victory", SC2WOL_LOC_ID_OFFSET + 1800, LocationType.VICTORY,
lambda state: state._sc2wol_has_common_unit(multiworld, player) and
(logic_level > 0 or state._sc2wol_has_anti_air)),
- LocationData("Cutthroat", "Cutthroat: Mira Han", SC2WOL_LOC_ID_OFFSET + 1801,
+ LocationData("Cutthroat", "Cutthroat: Mira Han", SC2WOL_LOC_ID_OFFSET + 1801, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_common_unit(multiworld, player)),
+ LocationData("Cutthroat", "Cutthroat: North Relic", SC2WOL_LOC_ID_OFFSET + 1802, LocationType.BONUS,
+ lambda state: state._sc2wol_has_common_unit(multiworld, player)),
+ LocationData("Cutthroat", "Cutthroat: Mid Relic", SC2WOL_LOC_ID_OFFSET + 1803, LocationType.BONUS),
+ LocationData("Cutthroat", "Cutthroat: Southwest Relic", SC2WOL_LOC_ID_OFFSET + 1804, LocationType.BONUS,
+ lambda state: state._sc2wol_has_common_unit(multiworld, player)),
+ LocationData("Cutthroat", "Cutthroat: North Command Center", SC2WOL_LOC_ID_OFFSET + 1805, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_common_unit(multiworld, player)),
- LocationData("Cutthroat", "Cutthroat: North Relic", SC2WOL_LOC_ID_OFFSET + 1802,
+ LocationData("Cutthroat", "Cutthroat: South Command Center", SC2WOL_LOC_ID_OFFSET + 1806, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_common_unit(multiworld, player)),
- LocationData("Cutthroat", "Cutthroat: Mid Relic", SC2WOL_LOC_ID_OFFSET + 1803),
- LocationData("Cutthroat", "Cutthroat: Southwest Relic", SC2WOL_LOC_ID_OFFSET + 1804,
+ LocationData("Cutthroat", "Cutthroat: West Command Center", SC2WOL_LOC_ID_OFFSET + 1807, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_common_unit(multiworld, player)),
- LocationData("Engine of Destruction", "Engine of Destruction: Victory", SC2WOL_LOC_ID_OFFSET + 1900,
+ LocationData("Engine of Destruction", "Engine of Destruction: Victory", SC2WOL_LOC_ID_OFFSET + 1900, LocationType.VICTORY,
lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and
state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)),
- LocationData("Engine of Destruction", "Engine of Destruction: Odin", SC2WOL_LOC_ID_OFFSET + 1901),
- LocationData("Engine of Destruction", "Engine of Destruction: Loki", SC2WOL_LOC_ID_OFFSET + 1902,
+ LocationData("Engine of Destruction", "Engine of Destruction: Odin", SC2WOL_LOC_ID_OFFSET + 1901, LocationType.MISSION_PROGRESS),
+ LocationData("Engine of Destruction", "Engine of Destruction: Loki", SC2WOL_LOC_ID_OFFSET + 1902, LocationType.OPTIONAL_BOSS,
lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and
state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)),
- LocationData("Engine of Destruction", "Engine of Destruction: Lab Devourer", SC2WOL_LOC_ID_OFFSET + 1903),
- LocationData("Engine of Destruction", "Engine of Destruction: North Devourer", SC2WOL_LOC_ID_OFFSET + 1904,
+ LocationData("Engine of Destruction", "Engine of Destruction: Lab Devourer", SC2WOL_LOC_ID_OFFSET + 1903, LocationType.BONUS),
+ LocationData("Engine of Destruction", "Engine of Destruction: North Devourer", SC2WOL_LOC_ID_OFFSET + 1904, LocationType.BONUS,
lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and
state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)),
- LocationData("Engine of Destruction", "Engine of Destruction: Southeast Devourer", SC2WOL_LOC_ID_OFFSET + 1905,
+ LocationData("Engine of Destruction", "Engine of Destruction: Southeast Devourer", SC2WOL_LOC_ID_OFFSET + 1905, LocationType.BONUS,
lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and
state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)),
- LocationData("Media Blitz", "Media Blitz: Victory", SC2WOL_LOC_ID_OFFSET + 2000,
+ LocationData("Engine of Destruction", "Engine of Destruction: West Base", SC2WOL_LOC_ID_OFFSET + 1906, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and
+ state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)),
+ LocationData("Engine of Destruction", "Engine of Destruction: Northwest Base", SC2WOL_LOC_ID_OFFSET + 1907, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and
+ state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)),
+ LocationData("Engine of Destruction", "Engine of Destruction: Northeast Base", SC2WOL_LOC_ID_OFFSET + 1908, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and
+ state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)),
+ LocationData("Engine of Destruction", "Engine of Destruction: Southeast Base", SC2WOL_LOC_ID_OFFSET + 1909, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and
+ state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)),
+ LocationData("Media Blitz", "Media Blitz: Victory", SC2WOL_LOC_ID_OFFSET + 2000, LocationType.VICTORY,
+ lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
+ LocationData("Media Blitz", "Media Blitz: Tower 1", SC2WOL_LOC_ID_OFFSET + 2001, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Media Blitz", "Media Blitz: Tower 1", SC2WOL_LOC_ID_OFFSET + 2001,
+ LocationData("Media Blitz", "Media Blitz: Tower 2", SC2WOL_LOC_ID_OFFSET + 2002, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Media Blitz", "Media Blitz: Tower 2", SC2WOL_LOC_ID_OFFSET + 2002,
+ LocationData("Media Blitz", "Media Blitz: Tower 3", SC2WOL_LOC_ID_OFFSET + 2003, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Media Blitz", "Media Blitz: Tower 3", SC2WOL_LOC_ID_OFFSET + 2003,
+ LocationData("Media Blitz", "Media Blitz: Science Facility", SC2WOL_LOC_ID_OFFSET + 2004, LocationType.BONUS),
+ LocationData("Media Blitz", "Media Blitz: All Barracks", SC2WOL_LOC_ID_OFFSET + 2005, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Media Blitz", "Media Blitz: Science Facility", SC2WOL_LOC_ID_OFFSET + 2004),
- LocationData("Piercing the Shroud", "Piercing the Shroud: Victory", SC2WOL_LOC_ID_OFFSET + 2100,
+ LocationData("Media Blitz", "Media Blitz: All Factories", SC2WOL_LOC_ID_OFFSET + 2006, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
+ LocationData("Media Blitz", "Media Blitz: All Starports", SC2WOL_LOC_ID_OFFSET + 2007, LocationType.MISSION_PROGRESS,
+ lambda state: logic_level > 0 or state._sc2wol_has_competent_comp(multiworld, player)),
+ LocationData("Media Blitz", "Media Blitz: Odin Not Trashed", SC2WOL_LOC_ID_OFFSET + 2008, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
+ LocationData("Piercing the Shroud", "Piercing the Shroud: Victory", SC2WOL_LOC_ID_OFFSET + 2100, LocationType.VICTORY,
+ lambda state: state._sc2wol_has_mm_upgrade(multiworld, player)),
+ LocationData("Piercing the Shroud", "Piercing the Shroud: Holding Cell Relic", SC2WOL_LOC_ID_OFFSET + 2101, LocationType.BONUS),
+ LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk Relic", SC2WOL_LOC_ID_OFFSET + 2102, LocationType.BONUS,
lambda state: state._sc2wol_has_mm_upgrade(multiworld, player)),
- LocationData("Piercing the Shroud", "Piercing the Shroud: Holding Cell Relic", SC2WOL_LOC_ID_OFFSET + 2101),
- LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk Relic", SC2WOL_LOC_ID_OFFSET + 2102,
+ LocationData("Piercing the Shroud", "Piercing the Shroud: First Escape Relic", SC2WOL_LOC_ID_OFFSET + 2103,LocationType.BONUS,
lambda state: state._sc2wol_has_mm_upgrade(multiworld, player)),
- LocationData("Piercing the Shroud", "Piercing the Shroud: First Escape Relic", SC2WOL_LOC_ID_OFFSET + 2103,
+ LocationData("Piercing the Shroud", "Piercing the Shroud: Second Escape Relic", SC2WOL_LOC_ID_OFFSET + 2104, LocationType.BONUS,
lambda state: state._sc2wol_has_mm_upgrade(multiworld, player)),
- LocationData("Piercing the Shroud", "Piercing the Shroud: Second Escape Relic", SC2WOL_LOC_ID_OFFSET + 2104,
+ LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk", SC2WOL_LOC_ID_OFFSET + 2105, LocationType.OPTIONAL_BOSS,
lambda state: state._sc2wol_has_mm_upgrade(multiworld, player)),
- LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk", SC2WOL_LOC_ID_OFFSET + 2105,
+ LocationData("Piercing the Shroud", "Piercing the Shroud: Fusion Reactor", SC2WOL_LOC_ID_OFFSET + 2106, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_mm_upgrade(multiworld, player)),
- LocationData("Whispers of Doom", "Whispers of Doom: Victory", SC2WOL_LOC_ID_OFFSET + 2200),
- LocationData("Whispers of Doom", "Whispers of Doom: First Hatchery", SC2WOL_LOC_ID_OFFSET + 2201),
- LocationData("Whispers of Doom", "Whispers of Doom: Second Hatchery", SC2WOL_LOC_ID_OFFSET + 2202),
- LocationData("Whispers of Doom", "Whispers of Doom: Third Hatchery", SC2WOL_LOC_ID_OFFSET + 2203),
- LocationData("A Sinister Turn", "A Sinister Turn: Victory", SC2WOL_LOC_ID_OFFSET + 2300,
+ LocationData("Whispers of Doom", "Whispers of Doom: Victory", SC2WOL_LOC_ID_OFFSET + 2200, LocationType.VICTORY),
+ LocationData("Whispers of Doom", "Whispers of Doom: First Hatchery", SC2WOL_LOC_ID_OFFSET + 2201, LocationType.BONUS),
+ LocationData("Whispers of Doom", "Whispers of Doom: Second Hatchery", SC2WOL_LOC_ID_OFFSET + 2202, LocationType.BONUS),
+ LocationData("Whispers of Doom", "Whispers of Doom: Third Hatchery", SC2WOL_LOC_ID_OFFSET + 2203, LocationType.BONUS),
+ LocationData("Whispers of Doom", "Whispers of Doom: First Prophecy Fragment", SC2WOL_LOC_ID_OFFSET + 2204, LocationType.MISSION_PROGRESS),
+ LocationData("Whispers of Doom", "Whispers of Doom: Second Prophecy Fragment", SC2WOL_LOC_ID_OFFSET + 2205, LocationType.MISSION_PROGRESS),
+ LocationData("Whispers of Doom", "Whispers of Doom: Third Prophecy Fragment", SC2WOL_LOC_ID_OFFSET + 2206, LocationType.MISSION_PROGRESS),
+ LocationData("A Sinister Turn", "A Sinister Turn: Victory", SC2WOL_LOC_ID_OFFSET + 2300, LocationType.VICTORY,
lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)),
- LocationData("A Sinister Turn", "A Sinister Turn: Robotics Facility", SC2WOL_LOC_ID_OFFSET + 2301,
+ LocationData("A Sinister Turn", "A Sinister Turn: Robotics Facility", SC2WOL_LOC_ID_OFFSET + 2301, LocationType.BONUS,
lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(multiworld, player)),
- LocationData("A Sinister Turn", "A Sinister Turn: Dark Shrine", SC2WOL_LOC_ID_OFFSET + 2302,
+ LocationData("A Sinister Turn", "A Sinister Turn: Dark Shrine", SC2WOL_LOC_ID_OFFSET + 2302, LocationType.BONUS,
lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(multiworld, player)),
- LocationData("A Sinister Turn", "A Sinister Turn: Templar Archives", SC2WOL_LOC_ID_OFFSET + 2303,
- lambda state: state._sc2wol_has_protoss_common_units(multiworld, player)),
- LocationData("Echoes of the Future", "Echoes of the Future: Victory", SC2WOL_LOC_ID_OFFSET + 2400,
+ LocationData("A Sinister Turn", "A Sinister Turn: Templar Archives", SC2WOL_LOC_ID_OFFSET + 2303, LocationType.BONUS,
+ lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)),
+ LocationData("A Sinister Turn", "A Sinister Turn: Northeast Base", SC2WOL_LOC_ID_OFFSET + 2304, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)),
+ LocationData("A Sinister Turn", "A Sinister Turn: Southeast Base", SC2WOL_LOC_ID_OFFSET + 2305, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)),
+ LocationData("A Sinister Turn", "A Sinister Turn: Maar", SC2WOL_LOC_ID_OFFSET + 2306, LocationType.MISSION_PROGRESS,
lambda state: logic_level > 0 or state._sc2wol_has_protoss_medium_units(multiworld, player)),
- LocationData("Echoes of the Future", "Echoes of the Future: Close Obelisk", SC2WOL_LOC_ID_OFFSET + 2401),
- LocationData("Echoes of the Future", "Echoes of the Future: West Obelisk", SC2WOL_LOC_ID_OFFSET + 2402,
+ LocationData("A Sinister Turn", "A Sinister Turn: Northwest Preserver", SC2WOL_LOC_ID_OFFSET + 2307, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)),
+ LocationData("A Sinister Turn", "A Sinister Turn: Southwest Preserver", SC2WOL_LOC_ID_OFFSET + 2308, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)),
+ LocationData("A Sinister Turn", "A Sinister Turn: East Preserver", SC2WOL_LOC_ID_OFFSET + 2309, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)),
+ LocationData("Echoes of the Future", "Echoes of the Future: Victory", SC2WOL_LOC_ID_OFFSET + 2400, LocationType.VICTORY,
+ lambda state: logic_level > 0 or state._sc2wol_has_protoss_medium_units(multiworld, player)),
+ LocationData("Echoes of the Future", "Echoes of the Future: Close Obelisk", SC2WOL_LOC_ID_OFFSET + 2401, LocationType.BONUS),
+ LocationData("Echoes of the Future", "Echoes of the Future: West Obelisk", SC2WOL_LOC_ID_OFFSET + 2402, LocationType.BONUS,
+ lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(multiworld, player)),
+ LocationData("Echoes of the Future", "Echoes of the Future: Base", SC2WOL_LOC_ID_OFFSET + 2403, LocationType.MISSION_PROGRESS),
+ LocationData("Echoes of the Future", "Echoes of the Future: Southwest Tendril", SC2WOL_LOC_ID_OFFSET + 2404, LocationType.MISSION_PROGRESS),
+ LocationData("Echoes of the Future", "Echoes of the Future: Southeast Tendril", SC2WOL_LOC_ID_OFFSET + 2405, LocationType.MISSION_PROGRESS,
+ lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(multiworld, player)),
+ LocationData("Echoes of the Future", "Echoes of the Future: Northeast Tendril", SC2WOL_LOC_ID_OFFSET + 2406, LocationType.MISSION_PROGRESS,
lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(multiworld, player)),
- LocationData("In Utter Darkness", "In Utter Darkness: Defeat", SC2WOL_LOC_ID_OFFSET + 2500),
- LocationData("In Utter Darkness", "In Utter Darkness: Protoss Archive", SC2WOL_LOC_ID_OFFSET + 2501,
+ LocationData("Echoes of the Future", "Echoes of the Future: Northwest Tendril", SC2WOL_LOC_ID_OFFSET + 2407, LocationType.MISSION_PROGRESS,
+ lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(multiworld, player)),
+ LocationData("In Utter Darkness", "In Utter Darkness: Defeat", SC2WOL_LOC_ID_OFFSET + 2500, LocationType.VICTORY),
+ LocationData("In Utter Darkness", "In Utter Darkness: Protoss Archive", SC2WOL_LOC_ID_OFFSET + 2501, LocationType.BONUS,
lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)),
- LocationData("In Utter Darkness", "In Utter Darkness: Kills", SC2WOL_LOC_ID_OFFSET + 2502,
+ LocationData("In Utter Darkness", "In Utter Darkness: Kills", SC2WOL_LOC_ID_OFFSET + 2502, LocationType.CHALLENGE,
+ lambda state: state._sc2wol_has_protoss_common_units(multiworld, player)),
+ LocationData("In Utter Darkness", "In Utter Darkness: Urun", SC2WOL_LOC_ID_OFFSET + 2503, LocationType.MISSION_PROGRESS),
+ LocationData("In Utter Darkness", "In Utter Darkness: Mohandar", SC2WOL_LOC_ID_OFFSET + 2504, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_protoss_common_units(multiworld, player)),
+ LocationData("In Utter Darkness", "In Utter Darkness: Selendis", SC2WOL_LOC_ID_OFFSET + 2505, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_protoss_common_units(multiworld, player)),
- LocationData("Gates of Hell", "Gates of Hell: Victory", SC2WOL_LOC_ID_OFFSET + 2600,
+ LocationData("In Utter Darkness", "In Utter Darkness: Artanis", SC2WOL_LOC_ID_OFFSET + 2506, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_protoss_common_units(multiworld, player)),
+ LocationData("Gates of Hell", "Gates of Hell: Victory", SC2WOL_LOC_ID_OFFSET + 2600, LocationType.VICTORY,
+ lambda state: state._sc2wol_has_competent_comp(multiworld, player) and
+ state._sc2wol_defense_rating(multiworld, player, True) > 6),
+ LocationData("Gates of Hell", "Gates of Hell: Large Army", SC2WOL_LOC_ID_OFFSET + 2601, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_competent_comp(multiworld, player) and
+ state._sc2wol_defense_rating(multiworld, player, True) > 6),
+ LocationData("Gates of Hell", "Gates of Hell: 2 Drop Pods", SC2WOL_LOC_ID_OFFSET + 2602, LocationType.BONUS,
+ lambda state: state._sc2wol_has_competent_comp(multiworld, player) and
+ state._sc2wol_defense_rating(multiworld, player, True) > 6),
+ LocationData("Gates of Hell", "Gates of Hell: 4 Drop Pods", SC2WOL_LOC_ID_OFFSET + 2603, LocationType.BONUS,
+ lambda state: state._sc2wol_has_competent_comp(multiworld, player) and
+ state._sc2wol_defense_rating(multiworld, player, True) > 6),
+ LocationData("Gates of Hell", "Gates of Hell: 6 Drop Pods", SC2WOL_LOC_ID_OFFSET + 2604, LocationType.BONUS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player) and
state._sc2wol_defense_rating(multiworld, player, True) > 6),
- LocationData("Gates of Hell", "Gates of Hell: Large Army", SC2WOL_LOC_ID_OFFSET + 2601,
+ LocationData("Gates of Hell", "Gates of Hell: 8 Drop Pods", SC2WOL_LOC_ID_OFFSET + 2605, LocationType.BONUS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player) and
state._sc2wol_defense_rating(multiworld, player, True) > 6),
- LocationData("Belly of the Beast", "Belly of the Beast: Victory", SC2WOL_LOC_ID_OFFSET + 2700),
- LocationData("Belly of the Beast", "Belly of the Beast: First Charge", SC2WOL_LOC_ID_OFFSET + 2701),
- LocationData("Belly of the Beast", "Belly of the Beast: Second Charge", SC2WOL_LOC_ID_OFFSET + 2702),
- LocationData("Belly of the Beast", "Belly of the Beast: Third Charge", SC2WOL_LOC_ID_OFFSET + 2703),
- LocationData("Shatter the Sky", "Shatter the Sky: Victory", SC2WOL_LOC_ID_OFFSET + 2800,
+ LocationData("Belly of the Beast", "Belly of the Beast: Victory", SC2WOL_LOC_ID_OFFSET + 2700, LocationType.VICTORY),
+ LocationData("Belly of the Beast", "Belly of the Beast: First Charge", SC2WOL_LOC_ID_OFFSET + 2701, LocationType.MISSION_PROGRESS),
+ LocationData("Belly of the Beast", "Belly of the Beast: Second Charge", SC2WOL_LOC_ID_OFFSET + 2702, LocationType.MISSION_PROGRESS),
+ LocationData("Belly of the Beast", "Belly of the Beast: Third Charge", SC2WOL_LOC_ID_OFFSET + 2703, LocationType.MISSION_PROGRESS),
+ LocationData("Belly of the Beast", "Belly of the Beast: First Group Rescued", SC2WOL_LOC_ID_OFFSET + 2704, LocationType.BONUS),
+ LocationData("Belly of the Beast", "Belly of the Beast: Second Group Rescued", SC2WOL_LOC_ID_OFFSET + 2705, LocationType.BONUS),
+ LocationData("Belly of the Beast", "Belly of the Beast: Third Group Rescued", SC2WOL_LOC_ID_OFFSET + 2706, LocationType.BONUS),
+ LocationData("Shatter the Sky", "Shatter the Sky: Victory", SC2WOL_LOC_ID_OFFSET + 2800, LocationType.VICTORY,
+ lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
+ LocationData("Shatter the Sky", "Shatter the Sky: Close Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2801, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
+ LocationData("Shatter the Sky", "Shatter the Sky: Northwest Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2802, LocationType.MISSION_PROGRESS,
+ lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
+ LocationData("Shatter the Sky", "Shatter the Sky: Southeast Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2803, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Shatter the Sky", "Shatter the Sky: Close Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2801,
+ LocationData("Shatter the Sky", "Shatter the Sky: Southwest Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2804, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Shatter the Sky", "Shatter the Sky: Northwest Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2802,
+ LocationData("Shatter the Sky", "Shatter the Sky: Leviathan", SC2WOL_LOC_ID_OFFSET + 2805, LocationType.OPTIONAL_BOSS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Shatter the Sky", "Shatter the Sky: Southeast Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2803,
+ LocationData("Shatter the Sky", "Shatter the Sky: East Hatchery", SC2WOL_LOC_ID_OFFSET + 2806, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Shatter the Sky", "Shatter the Sky: Southwest Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2804,
+ LocationData("Shatter the Sky", "Shatter the Sky: North Hatchery", SC2WOL_LOC_ID_OFFSET + 2807, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Shatter the Sky", "Shatter the Sky: Leviathan", SC2WOL_LOC_ID_OFFSET + 2805,
+ LocationData("Shatter the Sky", "Shatter the Sky: Mid Hatchery", SC2WOL_LOC_ID_OFFSET + 2808, LocationType.MISSION_PROGRESS,
lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("All-In", "All-In: Victory", None,
+ LocationData("All-In", "All-In: Victory", None, LocationType.VICTORY,
lambda state: state._sc2wol_final_mission_requirements(multiworld, player))
]
diff --git a/worlds/sc2wol/LogicMixin.py b/worlds/sc2wol/LogicMixin.py
index 8c7182e0e877..112302beb207 100644
--- a/worlds/sc2wol/LogicMixin.py
+++ b/worlds/sc2wol/LogicMixin.py
@@ -9,22 +9,38 @@ def _sc2wol_has_common_unit(self, multiworld: MultiWorld, player: int) -> bool:
return self.has_any(get_basic_units(multiworld, player), player)
def _sc2wol_has_air(self, multiworld: MultiWorld, player: int) -> bool:
- return self.has_any({'Viking', 'Wraith', 'Banshee'}, player) or get_option_value(multiworld, player, 'required_tactics') > 0 \
+ return self.has_any({'Viking', 'Wraith', 'Banshee', 'Battlecruiser'}, player) or get_option_value(multiworld, player, 'required_tactics') > 0 \
and self.has_any({'Hercules', 'Medivac'}, player) and self._sc2wol_has_common_unit(multiworld, player)
def _sc2wol_has_air_anti_air(self, multiworld: MultiWorld, player: int) -> bool:
return self.has('Viking', player) \
- or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has('Wraith', player)
+ or self.has_all({'Wraith', 'Advanced Laser Technology (Wraith)'}, player) \
+ or self.has_all({'Battlecruiser', 'ATX Laser Battery (Battlecruiser)'}, player) \
+ or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has_any({'Wraith', 'Valkyrie', 'Battlecruiser'}, player)
- def _sc2wol_has_competent_anti_air(self, multiworld: MultiWorld, player: int) -> bool:
+ def _sc2wol_has_competent_ground_to_air(self, multiworld: MultiWorld, player: int) -> bool:
return self.has('Goliath', player) \
or self.has('Marine', player) and self.has_any({'Medic', 'Medivac'}, player) \
+ or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has('Cyclone', player)
+
+ def _sc2wol_has_competent_anti_air(self, multiworld: MultiWorld, player: int) -> bool:
+ return self._sc2wol_has_competent_ground_to_air(multiworld, player) \
or self._sc2wol_has_air_anti_air(multiworld, player)
+ def _sc2wol_welcome_to_the_jungle_requirement(self, multiworld: MultiWorld, player: int) -> bool:
+ return (
+ self._sc2wol_has_common_unit(multiworld, player)
+ and self._sc2wol_has_competent_ground_to_air(multiworld, player)
+ ) or (
+ get_option_value(multiworld, player, 'required_tactics') > 0
+ and self.has_any({'Marine', 'Vulture'}, player)
+ and self._sc2wol_has_air_anti_air(multiworld, player)
+ )
+
def _sc2wol_has_anti_air(self, multiworld: MultiWorld, player: int) -> bool:
- return self.has_any({'Missile Turret', 'Thor', 'War Pigs', 'Spartan Company', "Hel's Angel", 'Battlecruiser', 'Marine', 'Wraith'}, player) \
+ return self.has_any({'Missile Turret', 'Thor', 'War Pigs', 'Spartan Company', "Hel's Angel", 'Battlecruiser', 'Marine', 'Wraith', 'Valkyrie', 'Cyclone'}, player) \
or self._sc2wol_has_competent_anti_air(multiworld, player) \
- or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has_any({'Ghost', 'Spectre'}, player)
+ or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has_any({'Ghost', 'Spectre', 'Widow Mine', 'Liberator'}, player)
def _sc2wol_defense_rating(self, multiworld: MultiWorld, player: int, zerg_enemy: bool, air_enemy: bool = True) -> bool:
defense_score = sum((defense_ratings[item] for item in defense_ratings if self.has(item, player)))
@@ -32,6 +48,10 @@ def _sc2wol_defense_rating(self, multiworld: MultiWorld, player: int, zerg_enemy
defense_score += 3
if self.has_all({'Siege Tank', 'Maelstrom Rounds (Siege Tank)'}, player):
defense_score += 2
+ if self.has_all({'Siege Tank', 'Graduating Range (Siege Tank)'}, player):
+ defense_score += 1
+ if self.has_all({'Widow Mine', 'Concealment (Widow Mine)'}, player):
+ defense_score += 1
if zerg_enemy:
defense_score += sum((zerg_defense_ratings[item] for item in zerg_defense_ratings if self.has(item, player)))
if self.has('Firebat', player) and self.has('Bunker', player):
@@ -44,20 +64,27 @@ def _sc2wol_defense_rating(self, multiworld: MultiWorld, player: int, zerg_enemy
return defense_score
def _sc2wol_has_competent_comp(self, multiworld: MultiWorld, player: int) -> bool:
- return (self.has('Marine', player) or self.has('Marauder', player) and
- self._sc2wol_has_competent_anti_air(multiworld, player)) and self.has_any({'Medivac', 'Medic'}, player) or \
- self.has('Thor', player) or self.has("Banshee", player) and self._sc2wol_has_competent_anti_air(multiworld, player) or \
- self.has('Battlecruiser', player) and self._sc2wol_has_common_unit(multiworld, player) or \
- self.has('Siege Tank', player) and self._sc2wol_has_competent_anti_air(multiworld, player)
+ return \
+ (
+ (
+ self.has_any({'Marine', 'Marauder'}, player) and self.has_any({'Medivac', 'Medic'}, player)
+ or self.has_any({'Thor', 'Banshee', 'Siege Tank'}, player)
+ or self.has_all({'Liberator', 'Raid Artillery (Liberator)'}, player)
+ ) and self._sc2wol_has_competent_anti_air(multiworld, player)
+ ) \
+ or \
+ (
+ self.has('Battlecruiser', player) and self._sc2wol_has_common_unit(multiworld, player)
+ )
def _sc2wol_has_train_killers(self, multiworld: MultiWorld, player: int) -> bool:
return (
- self.has_any({'Siege Tank', 'Diamondback', 'Marauder'}, player)
+ self.has_any({'Siege Tank', 'Diamondback', 'Marauder', 'Cyclone'}, player)
or get_option_value(multiworld, player, 'required_tactics') > 0
and (
self.has_all({'Reaper', "G-4 Clusterbomb"}, player)
or self.has_all({'Spectre', 'Psionic Lash'}, player)
- or self.has('Vulture', player)
+ or self.has_any({'Vulture', 'Liberator'}, player)
)
)
@@ -66,16 +93,18 @@ def _sc2wol_able_to_rescue(self, multiworld: MultiWorld, player: int) -> bool:
def _sc2wol_has_protoss_common_units(self, multiworld: MultiWorld, player: int) -> bool:
return self.has_any({'Zealot', 'Immortal', 'Stalker', 'Dark Templar'}, player) \
- or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has_any({'High Templar', 'Dark Templar'}, player)
+ or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has('High Templar', player)
def _sc2wol_has_protoss_medium_units(self, multiworld: MultiWorld, player: int) -> bool:
return self._sc2wol_has_protoss_common_units(multiworld, player) and \
- self.has_any({'Stalker', 'Void Ray', 'Phoenix', 'Carrier'}, player) \
- or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has_any({'High Templar', 'Dark Templar'}, player)
+ self.has_any({'Stalker', 'Void Ray', 'Carrier'}, player) \
+ or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has('Dark Templar', player)
def _sc2wol_beats_protoss_deathball(self, multiworld: MultiWorld, player: int) -> bool:
- return self.has_any({'Banshee', 'Battlecruiser'}, player) and self._sc2wol_has_competent_anti_air(multiworld, player) or \
- self._sc2wol_has_competent_comp(multiworld, player) and self._sc2wol_has_air_anti_air(multiworld, player)
+ return (self.has_any({'Banshee', 'Battlecruiser'}, player) or
+ self.has_all({'Liberator', 'Raid Artillery (Liberator)'}, player)) \
+ and self._sc2wol_has_competent_anti_air(multiworld, player) or \
+ self._sc2wol_has_competent_comp(multiworld, player) and self._sc2wol_has_air_anti_air(multiworld, player)
def _sc2wol_has_mm_upgrade(self, multiworld: MultiWorld, player: int) -> bool:
return self.has_any({"Combat Shield (Marine)", "Stabilizer Medpacks (Medic)"}, player)
@@ -89,6 +118,17 @@ def _sc2wol_survives_rip_field(self, multiworld: MultiWorld, player: int) -> boo
def _sc2wol_has_nukes(self, multiworld: MultiWorld, player: int) -> bool:
return get_option_value(multiworld, player, 'required_tactics') > 0 and self.has_any({'Ghost', 'Spectre'}, player)
+ def _sc2wol_can_respond_to_colony_infestations(self, multiworld: MultiWorld, player: int) -> bool:
+ return self._sc2wol_has_common_unit(multiworld, player) \
+ and self._sc2wol_has_competent_anti_air(multiworld, player) \
+ and \
+ (
+ self._sc2wol_has_air_anti_air(multiworld, player) or
+ self.has_any({'Battlecruiser', 'Valkyrie'}), player
+ ) \
+ and \
+ self._sc2wol_defense_rating(multiworld, player, True) >= 3
+
def _sc2wol_final_mission_requirements(self, multiworld: MultiWorld, player: int):
beats_kerrigan = self.has_any({'Marine', 'Banshee', 'Ghost'}, player) or get_option_value(multiworld, player, 'required_tactics') > 0
if get_option_value(multiworld, player, 'all_in_map') == 0:
@@ -101,7 +141,7 @@ def _sc2wol_final_mission_requirements(self, multiworld: MultiWorld, player: int
# Air
defense_rating = self._sc2wol_defense_rating(multiworld, player, True, True)
return defense_rating >= 8 and beats_kerrigan \
- and self.has_any({'Viking', 'Battlecruiser'}, player) \
+ and self.has_any({'Viking', 'Battlecruiser', 'Valkyrie'}, player) \
and self.has_any({'Hive Mind Emulator', 'Psi Disruptor', 'Missile Turret'}, player)
def _sc2wol_cleared_missions(self, multiworld: MultiWorld, player: int, mission_count: int) -> bool:
diff --git a/worlds/sc2wol/MissionTables.py b/worlds/sc2wol/MissionTables.py
index 6db93547689b..298cd7a978a6 100644
--- a/worlds/sc2wol/MissionTables.py
+++ b/worlds/sc2wol/MissionTables.py
@@ -49,8 +49,8 @@ class FillMission(NamedTuple):
FillMission(MissionPools.EASY, [2], "Artifact", completion_critical=True),
FillMission(MissionPools.MEDIUM, [7], "Artifact", number=8, completion_critical=True),
FillMission(MissionPools.HARD, [8], "Artifact", number=11, completion_critical=True),
- FillMission(MissionPools.HARD, [9], "Artifact", number=14, completion_critical=True),
- FillMission(MissionPools.HARD, [10], "Artifact", completion_critical=True),
+ FillMission(MissionPools.HARD, [9], "Artifact", number=14, completion_critical=True, removal_priority=11),
+ FillMission(MissionPools.HARD, [10], "Artifact", completion_critical=True, removal_priority=10),
FillMission(MissionPools.MEDIUM, [2], "Covert", number=4),
FillMission(MissionPools.MEDIUM, [12], "Covert"),
FillMission(MissionPools.HARD, [13], "Covert", number=8, removal_priority=3),
@@ -58,7 +58,7 @@ class FillMission(NamedTuple):
FillMission(MissionPools.MEDIUM, [2], "Rebellion", number=6),
FillMission(MissionPools.HARD, [16], "Rebellion"),
FillMission(MissionPools.HARD, [17], "Rebellion"),
- FillMission(MissionPools.HARD, [18], "Rebellion"),
+ FillMission(MissionPools.HARD, [18], "Rebellion", removal_priority=12),
FillMission(MissionPools.HARD, [19], "Rebellion", removal_priority=5),
FillMission(MissionPools.MEDIUM, [8], "Prophecy", removal_priority=9),
FillMission(MissionPools.HARD, [21], "Prophecy", removal_priority=8),
@@ -98,6 +98,13 @@ class FillMission(NamedTuple):
FillMission(MissionPools.FINAL, [5], "Final", completion_critical=True)
]
+mini_gauntlet_order = [
+ FillMission(MissionPools.STARTER, [-1], "I", completion_critical=True),
+ FillMission(MissionPools.EASY, [0], "II", completion_critical=True),
+ FillMission(MissionPools.MEDIUM, [1], "III", completion_critical=True),
+ FillMission(MissionPools.FINAL, [2], "Final", completion_critical=True)
+]
+
grid_order = [
FillMission(MissionPools.STARTER, [-1], "_1"),
FillMission(MissionPools.EASY, [0], "_1"),
@@ -129,6 +136,13 @@ class FillMission(NamedTuple):
FillMission(MissionPools.FINAL, [5, 7], "_3", or_requirements=True)
]
+tiny_grid_order = [
+ FillMission(MissionPools.STARTER, [-1], "_1"),
+ FillMission(MissionPools.MEDIUM, [0], "_1"),
+ FillMission(MissionPools.EASY, [0], "_2"),
+ FillMission(MissionPools.FINAL, [1, 2], "_2", or_requirements=True),
+]
+
blitz_order = [
FillMission(MissionPools.STARTER, [-1], "I"),
FillMission(MissionPools.EASY, [-1], "I"),
@@ -144,7 +158,17 @@ class FillMission(NamedTuple):
FillMission(MissionPools.FINAL, [0, 1], "Final", number=5, or_requirements=True)
]
-mission_orders = [vanilla_shuffle_order, vanilla_shuffle_order, mini_campaign_order, grid_order, mini_grid_order, blitz_order, gauntlet_order]
+mission_orders = [
+ vanilla_shuffle_order,
+ vanilla_shuffle_order,
+ mini_campaign_order,
+ grid_order,
+ mini_grid_order,
+ blitz_order,
+ gauntlet_order,
+ mini_gauntlet_order,
+ tiny_grid_order
+]
vanilla_mission_req_table = {
@@ -190,7 +214,7 @@ class FillMission(NamedTuple):
"Whispers of Doom": "Whispers of Doom: Victory",
"Belly of the Beast": "Belly of the Beast: Victory",
"Zero Hour": "Zero Hour: First Group Rescued",
- "Evacuation": "Evacuation: First Chysalis",
+ "Evacuation": "Evacuation: Reach Hanson",
"Devil's Playground": "Devil's Playground: Tosh's Miners",
"Smash and Grab": "Smash and Grab: First Relic",
"The Great Train Robbery": "The Great Train Robbery: North Defiler"
diff --git a/worlds/sc2wol/Options.py b/worlds/sc2wol/Options.py
index 4f2032d662e8..13b01c42a22c 100644
--- a/worlds/sc2wol/Options.py
+++ b/worlds/sc2wol/Options.py
@@ -3,31 +3,49 @@
from Options import Choice, Option, Toggle, DefaultOnToggle, ItemSet, OptionSet, Range
from .MissionTables import vanilla_mission_req_table
+ORDER_VANILLA = 0
+ORDER_VANILLA_SHUFFLED = 1
class GameDifficulty(Choice):
- """The difficulty of the campaign, affects enemy AI, starting units, and game speed."""
+ """
+ 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
-
-
-class UpgradeBonus(Choice):
- """Determines what lab upgrade to use, whether it is Ultra-Capacitors which boost attack speed with every weapon
- upgrade or Vanadium Plating which boosts life with every armor upgrade."""
- display_name = "Upgrade Bonus"
- option_ultra_capacitors = 0
- option_vanadium_plating = 1
-
-
-class BunkerUpgrade(Choice):
- """Determines what bunker lab upgrade to use, whether it is Shrike Turret which outfits bunkers with an automated
- turret or Fortified Bunker which boosts the life of bunkers."""
- display_name = "Bunker Upgrade"
- option_shrike_turret = 0
- option_fortified_bunker = 1
-
+ default = 1
+
+class GameSpeed(Choice):
+ """Optional setting to override difficulty-based game speed."""
+ display_name = "Game Speed"
+ option_default = 0
+ option_slower = 1
+ option_slow = 2
+ option_normal = 3
+ option_fast = 4
+ option_faster = 5
+ default = option_default
+
+class FinalMap(Choice):
+ """
+ Determines if the final map and goal of the campaign.
+ All in: You need to beat All-in map
+ Random Hard: A random hard mission is selected as a goal.
+ Beat this mission in order to complete the game.
+ All-in map won't be in the campaign
+
+ Vanilla mission order always ends with All in mission!
+
+ This option is short-lived. It may be changed in the future
+ """
+ display_name = "Final Map"
+ option_all_in = 0
+ option_random_hard = 1
class AllInMap(Choice):
"""Determines what version of All-In (final map) that will be generated for the campaign."""
@@ -37,14 +55,18 @@ class AllInMap(Choice):
class MissionOrder(Choice):
- """Determines the order the missions are played in. The last three mission orders end in a random mission.
+ """
+ Determines the order the missions are played in. The last three mission orders end in a random mission.
Vanilla (29): Keeps the standard mission order and branching from the WoL Campaign.
Vanilla Shuffled (29): Keeps same branching paths from the WoL Campaign but randomizes the order of missions within.
Mini Campaign (15): Shorter version of the campaign with randomized missions and optional branches.
- Grid (16): A 4x4 grid of random missions. Start at the top-left and forge a path towards All-In.
+ 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."""
+ Gauntlet (7): Linear series of 7 random missions to complete the campaign.
+ Mini Gauntlet (4): Linear series of 4 random missions to complete the campaign.
+ Tiny Grid (4): A 2x2 version of Grid. Complete the bottom-right mission to win.
+ """
display_name = "Mission Order"
option_vanilla = 0
option_vanilla_shuffled = 1
@@ -53,27 +75,53 @@ class MissionOrder(Choice):
option_mini_grid = 4
option_blitz = 5
option_gauntlet = 6
+ option_mini_gauntlet = 7
+ option_tiny_grid = 8
+
+
+class PlayerColor(Choice):
+ """Determines in-game team color."""
+ display_name = "Player Color"
+ option_white = 0
+ option_red = 1
+ option_blue = 2
+ option_teal = 3
+ option_purple = 4
+ option_yellow = 5
+ option_orange = 6
+ option_green = 7
+ option_light_pink = 8
+ option_violet = 9
+ option_light_grey = 10
+ option_dark_green = 11
+ option_brown = 12
+ option_light_green = 13
+ option_dark_grey = 14
+ option_pink = 15
+ option_rainbow = 16
+ option_default = 17
+ default = option_default
class ShuffleProtoss(DefaultOnToggle):
"""Determines if the 3 protoss missions are included in the shuffle if Vanilla mission order is not enabled.
- If turned off with Vanilla Shuffled, the 3 protoss missions will be in their normal position on the Prophecy chain
- if not shuffled.
- If turned off with reduced mission settings, the 3 protoss missions will not appear and Protoss units are removed
- from the pool."""
+ If turned off, the 3 protoss missions will not appear and Protoss units are removed from the pool."""
display_name = "Shuffle Protoss Missions"
class ShuffleNoBuild(DefaultOnToggle):
"""Determines if the 5 no-build missions are included in the shuffle if Vanilla mission order is not enabled.
- If turned off with Vanilla Shuffled, one no-build mission will be placed as the first mission and the rest will be
- placed at the end of optional routes.
- If turned off with reduced mission settings, the 5 no-build missions will not appear."""
+ If turned off, the 5 no-build missions will not appear."""
display_name = "Shuffle No-Build Missions"
class EarlyUnit(DefaultOnToggle):
- """Guarantees that the first mission will contain a unit."""
+ """
+ Guarantees that the first mission will contain a unit.
+
+ Each mission available to be the first mission has a pre-defined location where the unit should spawn.
+ This location gets overriden over any exclusion. It's guaranteed to be reachable with an empty inventory.
+ """
display_name = "Early Unit"
@@ -91,11 +139,97 @@ class RequiredTactics(Choice):
class UnitsAlwaysHaveUpgrades(DefaultOnToggle):
- """If turned on, both upgrades will be present for each unit and structure in the seed.
- This usually results in fewer units."""
+ """
+ If turned on, all upgrades will be present for each unit and structure in the seed.
+ This usually results in fewer units.
+
+ See also: Max Number of Upgrades
+ """
display_name = "Units Always Have Upgrades"
+class GenericUpgradeMissions(Range):
+ """Determines the percentage of missions in the mission order that must be completed before
+ level 1 of all weapon and armor upgrades is unlocked. Level 2 upgrades require double the amount of missions,
+ and level 3 requires triple the amount. The required amounts are always rounded down.
+ If set to 0, upgrades are instead added to the item pool and must be found to be used."""
+ display_name = "Generic Upgrade Missions"
+ range_start = 0
+ range_end = 100
+ default = 0
+
+
+class GenericUpgradeResearch(Choice):
+ """Determines how weapon and armor upgrades affect missions once unlocked.
+
+ Vanilla: Upgrades must be researched as normal.
+ Auto In No-Build: In No-Build missions, upgrades are automatically researched.
+ In all other missions, upgrades must be researched as normal.
+ Auto In Build: In No-Build missions, upgrades are unavailable as normal.
+ In all other missions, upgrades are automatically researched.
+ Always Auto: Upgrades are automatically researched in all missions."""
+ display_name = "Generic Upgrade Research"
+ option_vanilla = 0
+ option_auto_in_no_build = 1
+ option_auto_in_build = 2
+ option_always_auto = 3
+
+
+class GenericUpgradeItems(Choice):
+ """Determines how weapon and armor upgrades are split into items. All options produce 3 levels of each item.
+ Does nothing if upgrades are unlocked by completed mission counts.
+
+ Individual Items: All weapon and armor upgrades are each an item,
+ resulting in 18 total upgrade items.
+ Bundle Weapon And Armor: All types of weapon upgrades are one item,
+ and all types of armor upgrades are one item,
+ resulting in 6 total items.
+ Bundle Unit Class: Weapon and armor upgrades are merged,
+ but Infantry, Vehicle, and Starship upgrades are bundled separately,
+ resulting in 9 total items.
+ Bundle All: All weapon and armor upgrades are one item,
+ resulting in 3 total items."""
+ display_name = "Generic Upgrade Items"
+ option_individual_items = 0
+ option_bundle_weapon_and_armor = 1
+ option_bundle_unit_class = 2
+ option_bundle_all = 3
+
+
+class NovaCovertOpsItems(Toggle):
+ """If turned on, the equipment upgrades from Nova Covert Ops may be present in the world."""
+ display_name = "Nova Covert Ops Items"
+ default = Toggle.option_true
+
+
+class BroodWarItems(Toggle):
+ """If turned on, returning items from StarCraft: Brood War may appear in the world."""
+ display_name = "Brood War Items"
+ default = Toggle.option_true
+
+
+class ExtendedItems(Toggle):
+ """If turned on, original items that did not appear in Campaign mode may appear in the world."""
+ display_name = "Extended Items"
+ default = Toggle.option_true
+
+
+class MaxNumberOfUpgrades(Range):
+ """
+ Set a maximum to the number of upgrades a unit/structure can have. -1 is used to define unlimited.
+ Note that most unit have 4 or 6 upgrades.
+
+ If used with Units Always Have Upgrades, each unit has this given amount of upgrades (if there enough upgrades exist)
+
+ See also: Units Always Have Upgrades
+ """
+ display_name = "Maximum number of upgrades per unit/structure"
+ range_start = -1
+ # Do not know the maximum, but it is less than 123!
+ range_end = 123
+ default = -1
+
+
class LockedItems(ItemSet):
"""Guarantees that these items will be unlockable"""
display_name = "Locked Items"
@@ -108,27 +242,114 @@ class ExcludedItems(ItemSet):
class ExcludedMissions(OptionSet):
"""Guarantees that these missions will not appear in the campaign
- Only applies on shortened mission orders.
+ Doesn't apply to vanilla mission order.
It may be impossible to build a valid campaign if too many missions are excluded."""
display_name = "Excluded Missions"
valid_keys = {mission_name for mission_name in vanilla_mission_req_table.keys() if mission_name != 'All-In'}
+class LocationInclusion(Choice):
+ option_enabled = 0
+ option_trash = 1
+ option_nothing = 2
+
+
+class MissionProgressLocations(LocationInclusion):
+ """
+ Enables or disables item rewards for progressing (not finishing) a mission.
+ Progressing a mission is usually a task of completing or progressing into a main objective.
+ Clearing an expansion base also counts here.
+
+ Enabled: All locations fitting into this do their normal rewards
+ Trash: Forces a trash item in
+ Nothing: No rewards for this type of tasks, effectively disabling such locations
+
+ Note: Individual locations subject to plando are always enabled, so the plando can be placed properly.
+ Warning: The generation may fail if too many locations are excluded by this way.
+ See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando)
+ """
+ display_name = "Mission Progress Locations"
+
+
+class BonusLocations(LocationInclusion):
+ """
+ Enables or disables item rewards for completing bonus tasks.
+ Bonus tasks are those giving you a campaign-wide or mission-wide bonus in vanilla game:
+ Research, credits, bonus units or resources, etc.
+
+ Enabled: All locations fitting into this do their normal rewards
+ Trash: Forces a trash item in
+ Nothing: No rewards for this type of tasks, effectively disabling such locations
+
+ Note: Individual locations subject to plando are always enabled, so the plando can be placed properly.
+ Warning: The generation may fail if too many locations are excluded by this way.
+ See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando)
+ """
+ display_name = "Bonus Locations"
+
+
+class ChallengeLocations(LocationInclusion):
+ """
+ Enables or disables item rewards for completing challenge tasks.
+ Challenges are tasks that have usually higher requirements to be completed
+ than to complete the mission they're in successfully.
+ You might be required to visit the same mission later when getting stronger in order to finish these tasks.
+
+ Enabled: All locations fitting into this do their normal rewards
+ Trash: Forces a trash item in
+ Nothing: No rewards for this type of tasks, effectively disabling such locations
+
+ Note: Individual locations subject to plando are always enabled, so the plando can be placed properly.
+ Warning: The generation may fail if too many locations are excluded by this way.
+ See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando)
+ """
+ display_name = "Challenge Locations"
+
+
+class OptionalBossLocations(LocationInclusion):
+ """
+ Enables or disables item rewards for defeating optional bosses.
+ An optional boss is any boss that's not required to kill in order to finish the mission successfully.
+ All Brutalisks, Loki, etc. belongs here.
+
+ Enabled: All locations fitting into this do their normal rewards
+ Trash: Forces a trash item in
+ Nothing: No rewards for this type of tasks, effectively disabling such locations
+
+ Note: Individual locations subject to plando are always enabled, so the plando can be placed properly.
+ Warning: The generation may fail if too many locations are excluded by this way.
+ See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando)
+ """
+ display_name = "Optional Boss Locations"
+
+
# noinspection PyTypeChecker
sc2wol_options: Dict[str, Option] = {
"game_difficulty": GameDifficulty,
- "upgrade_bonus": UpgradeBonus,
- "bunker_upgrade": BunkerUpgrade,
+ "game_speed": GameSpeed,
"all_in_map": AllInMap,
+ "final_map": FinalMap,
"mission_order": MissionOrder,
+ "player_color": PlayerColor,
"shuffle_protoss": ShuffleProtoss,
"shuffle_no_build": ShuffleNoBuild,
"early_unit": EarlyUnit,
"required_tactics": RequiredTactics,
"units_always_have_upgrades": UnitsAlwaysHaveUpgrades,
+ "max_number_of_upgrades": MaxNumberOfUpgrades,
+ "generic_upgrade_missions": GenericUpgradeMissions,
+ "generic_upgrade_research": GenericUpgradeResearch,
+ "generic_upgrade_items": GenericUpgradeItems,
"locked_items": LockedItems,
"excluded_items": ExcludedItems,
- "excluded_missions": ExcludedMissions
+ "excluded_missions": ExcludedMissions,
+ "nco_items": NovaCovertOpsItems,
+ "bw_items": BroodWarItems,
+ "ext_items": ExtendedItems,
+ "mission_progress_locations": MissionProgressLocations,
+ "bonus_locations": BonusLocations,
+ "challenge_locations": ChallengeLocations,
+ "optional_boss_locations": OptionalBossLocations
}
diff --git a/worlds/sc2wol/PoolFilter.py b/worlds/sc2wol/PoolFilter.py
index 16cc51f24372..4a19e2dbb305 100644
--- a/worlds/sc2wol/PoolFilter.py
+++ b/worlds/sc2wol/PoolFilter.py
@@ -1,22 +1,22 @@
from typing import Callable, Dict, List, Set
from BaseClasses import MultiWorld, ItemClassification, Item, Location
-from .Items import item_table
+from .Items import get_full_item_list, spider_mine_sources, second_pass_placeable_items, filler_items
from .MissionTables import no_build_regions_list, easy_regions_list, medium_regions_list, hard_regions_list,\
mission_orders, MissionInfo, alt_final_mission_locations, MissionPools
-from .Options import get_option_value
+from .Options import get_option_value, MissionOrder, FinalMap, MissionProgressLocations, LocationInclusion
from .LogicMixin import SC2WoLLogic
# Items with associated upgrades
UPGRADABLE_ITEMS = [
"Marine", "Medic", "Firebat", "Marauder", "Reaper", "Ghost", "Spectre",
- "Hellion", "Vulture", "Goliath", "Diamondback", "Siege Tank", "Thor",
- "Medivac", "Wraith", "Viking", "Banshee", "Battlecruiser",
+ "Hellion", "Vulture", "Goliath", "Diamondback", "Siege Tank", "Thor", "Predator", "Widow Mine", "Cyclone",
+ "Medivac", "Wraith", "Viking", "Banshee", "Battlecruiser", "Raven", "Science Vessel", "Liberator", "Valkyrie",
"Bunker", "Missile Turret"
]
BARRACKS_UNITS = {"Marine", "Medic", "Firebat", "Marauder", "Reaper", "Ghost", "Spectre"}
-FACTORY_UNITS = {"Hellion", "Vulture", "Goliath", "Diamondback", "Siege Tank", "Thor", "Predator"}
-STARPORT_UNITS = {"Medivac", "Wraith", "Viking", "Banshee", "Battlecruiser", "Hercules", "Science Vessel", "Raven"}
+FACTORY_UNITS = {"Hellion", "Vulture", "Goliath", "Diamondback", "Siege Tank", "Thor", "Predator", "Widow Mine"}
+STARPORT_UNITS = {"Medivac", "Wraith", "Viking", "Banshee", "Battlecruiser", "Hercules", "Science Vessel", "Raven", "Liberator", "Valkyrie"}
PROTOSS_REGIONS = {"A Sinister Turn", "Echoes of the Future", "In Utter Darkness"}
@@ -30,7 +30,7 @@ def filter_missions(multiworld: MultiWorld, player: int) -> Dict[int, List[str]]
shuffle_no_build = get_option_value(multiworld, player, "shuffle_no_build")
shuffle_protoss = get_option_value(multiworld, player, "shuffle_protoss")
excluded_missions = get_option_value(multiworld, player, "excluded_missions")
- mission_count = len(mission_orders[mission_order_type]) - 1
+ final_map = get_option_value(multiworld, player, "final_map")
mission_pools = {
MissionPools.STARTER: no_build_regions_list[:],
MissionPools.EASY: easy_regions_list[:],
@@ -38,21 +38,18 @@ def filter_missions(multiworld: MultiWorld, player: int) -> Dict[int, List[str]]
MissionPools.HARD: hard_regions_list[:],
MissionPools.FINAL: []
}
- if mission_order_type == 0:
+ if mission_order_type == MissionOrder.option_vanilla:
# Vanilla uses the entire mission pool
mission_pools[MissionPools.FINAL] = ['All-In']
return mission_pools
- elif mission_order_type == 1:
- # Vanilla Shuffled ignores the player-provided excluded missions
- excluded_missions = set()
# Omitting No-Build missions if not shuffling no-build
if not shuffle_no_build:
excluded_missions = excluded_missions.union(no_build_regions_list)
# Omitting Protoss missions if not shuffling protoss
if not shuffle_protoss:
excluded_missions = excluded_missions.union(PROTOSS_REGIONS)
- # Replacing All-In on low mission counts
- if mission_count < 14:
+ # Replacing All-In with alternate ending depending on option
+ if final_map == FinalMap.option_random_hard:
final_mission = multiworld.random.choice([mission for mission in alt_final_mission_locations.keys() if mission not in excluded_missions])
excluded_missions.add(final_mission)
else:
@@ -92,10 +89,18 @@ def get_item_upgrades(inventory: List[Item], parent_item: Item or str):
item_name = parent_item.name if isinstance(parent_item, Item) else parent_item
return [
inv_item for inv_item in inventory
- if item_table[inv_item.name].parent_item == item_name
+ if get_full_item_list()[inv_item.name].parent_item == item_name
]
+def get_item_quantity(item):
+ return get_full_item_list()[item.name].quantity
+
+
+def copy_item(item: Item):
+ return Item(item.name, item.classification, item.code, item.player)
+
+
class ValidInventory:
def has(self, item: str, player: int):
@@ -124,22 +129,6 @@ def generate_reduced_inventory(self, inventory_size: int, mission_requirements:
cascade_keys = self.cascade_removal_map.keys()
units_always_have_upgrades = get_option_value(self.multiworld, self.player, "units_always_have_upgrades")
- # Locking associated items for items that have already been placed when units_always_have_upgrades is on
- if units_always_have_upgrades:
- existing_items = self.existing_items[:]
- while existing_items:
- existing_item = existing_items.pop()
- items_to_lock = self.cascade_removal_map.get(existing_item, [existing_item])
- for item in items_to_lock:
- if item in inventory:
- inventory.remove(item)
- locked_items.append(item)
- if item in existing_items:
- existing_items.remove(item)
-
- if self.min_units_per_structure > 0 and self.has_units_per_structure():
- requirements.append(lambda state: state.has_units_per_structure())
-
def attempt_removal(item: Item) -> bool:
# If item can be removed and has associated items, remove them as well
inventory.remove(item)
@@ -149,9 +138,77 @@ def attempt_removal(item: Item) -> bool:
if not all(requirement(self) for requirement in requirements):
# If item cannot be removed, lock or revert
self.logical_inventory.add(item.name)
- locked_items.append(item)
+ for _ in range(get_item_quantity(item)):
+ locked_items.append(copy_item(item))
return False
return True
+
+ # Limit the maximum number of upgrades
+ maxUpgrad = get_option_value(self.multiworld, self.player,
+ "max_number_of_upgrades")
+ if maxUpgrad != -1:
+ unit_avail_upgrades = {}
+ # Needed to take into account locked/existing items
+ unit_nb_upgrades = {}
+ for item in inventory:
+ cItem = get_full_item_list()[item.name]
+ if cItem.type in UPGRADABLE_ITEMS and item.name not in unit_avail_upgrades:
+ unit_avail_upgrades[item.name] = []
+ unit_nb_upgrades[item.name] = 0
+ elif cItem.parent_item is not None:
+ if cItem.parent_item not in unit_avail_upgrades:
+ unit_avail_upgrades[cItem.parent_item] = [item]
+ unit_nb_upgrades[cItem.parent_item] = 1
+ else:
+ unit_avail_upgrades[cItem.parent_item].append(item)
+ unit_nb_upgrades[cItem.parent_item] += 1
+ # For those two categories, we count them but dont include them in removal
+ for item in locked_items + self.existing_items:
+ cItem = get_full_item_list()[item.name]
+ if cItem.type in UPGRADABLE_ITEMS and item.name not in unit_avail_upgrades:
+ unit_avail_upgrades[item.name] = []
+ unit_nb_upgrades[item.name] = 0
+ elif cItem.parent_item is not None:
+ if cItem.parent_item not in unit_avail_upgrades:
+ unit_nb_upgrades[cItem.parent_item] = 1
+ else:
+ unit_nb_upgrades[cItem.parent_item] += 1
+ # Making sure that the upgrades being removed is random
+ # Currently, only for combat shield vs Stabilizer Medpacks...
+ shuffled_unit_upgrade_list = list(unit_avail_upgrades.keys())
+ self.multiworld.random.shuffle(shuffled_unit_upgrade_list)
+ for unit in shuffled_unit_upgrade_list:
+ while (unit_nb_upgrades[unit] > maxUpgrad) \
+ and (len(unit_avail_upgrades[unit]) > 0):
+ itemCandidate = self.multiworld.random.choice(unit_avail_upgrades[unit])
+ _ = attempt_removal(itemCandidate)
+ # Whatever it succeed to remove the iventory or it fails and thus
+ # lock it, the upgrade is no longer available for removal
+ unit_avail_upgrades[unit].remove(itemCandidate)
+ unit_nb_upgrades[unit] -= 1
+
+ # Locking associated items for items that have already been placed when units_always_have_upgrades is on
+ if units_always_have_upgrades:
+ existing_items = set(self.existing_items[:] + locked_items)
+ while existing_items:
+ existing_item = existing_items.pop()
+ items_to_lock = self.cascade_removal_map.get(existing_item, [existing_item])
+ if get_full_item_list()[existing_item.name].type != "Upgrade":
+ # Don't process general upgrades, they may have been pre-locked per-level
+ for item in items_to_lock:
+ if item in inventory:
+ # Unit upgrades, lock all levels
+ for _ in range(inventory.count(item)):
+ inventory.remove(item)
+ if item not in locked_items:
+ # Lock all the associated items if not already locked
+ for _ in range(get_item_quantity(item)):
+ locked_items.append(copy_item(item))
+ if item in existing_items:
+ existing_items.remove(item)
+
+ if self.min_units_per_structure > 0 and self.has_units_per_structure():
+ requirements.append(lambda state: state.has_units_per_structure())
# Determining if the full-size inventory can complete campaign
if not all(requirement(self) for requirement in requirements):
@@ -185,21 +242,47 @@ def attempt_removal(item: Item) -> bool:
if cascade_failure:
for transient_item in transient_items:
if transient_item in inventory:
- inventory.remove(transient_item)
+ for _ in range(inventory.count(transient_item)):
+ inventory.remove(transient_item)
if transient_item not in locked_items:
- locked_items.append(transient_item)
+ for _ in range(get_item_quantity(transient_item)):
+ locked_items.append(copy_item(transient_item))
if transient_item.classification in (ItemClassification.progression, ItemClassification.progression_skip_balancing):
self.logical_inventory.add(transient_item.name)
else:
attempt_removal(item)
- return inventory + locked_items
+ if not spider_mine_sources & self.logical_inventory:
+ inventory = [item for item in inventory if not item.name.endswith("(Spider Mine)")]
+ if not BARRACKS_UNITS & self.logical_inventory:
+ inventory = [item for item in inventory if
+ not (item.name.startswith("Progressive Infantry") or item.name == "Orbital Strike")]
+ if not FACTORY_UNITS & self.logical_inventory:
+ inventory = [item for item in inventory if not item.name.startswith("Progressive Vehicle")]
+ if not STARPORT_UNITS & self.logical_inventory:
+ inventory = [item for item in inventory if not item.name.startswith("Progressive Ship")]
+
+ # Cull finished, adding locked items back into inventory
+ inventory += locked_items
+
+ # Replacing empty space with generically useful items
+ replacement_items = [item for item in self.item_pool
+ if (item not in inventory
+ and item not in self.locked_items
+ and item.name in second_pass_placeable_items)]
+ self.multiworld.random.shuffle(replacement_items)
+ while len(inventory) < inventory_size and len(replacement_items) > 0:
+ item = replacement_items.pop()
+ inventory.append(item)
+
+ return inventory
def _read_logic(self):
self._sc2wol_has_common_unit = lambda world, player: SC2WoLLogic._sc2wol_has_common_unit(self, world, player)
self._sc2wol_has_air = lambda world, player: SC2WoLLogic._sc2wol_has_air(self, world, player)
self._sc2wol_has_air_anti_air = lambda world, player: SC2WoLLogic._sc2wol_has_air_anti_air(self, world, player)
self._sc2wol_has_competent_anti_air = lambda world, player: SC2WoLLogic._sc2wol_has_competent_anti_air(self, world, player)
+ self._sc2wol_has_competent_ground_to_air = lambda world, player: SC2WoLLogic._sc2wol_has_competent_ground_to_air(self, world, player)
self._sc2wol_has_anti_air = lambda world, player: SC2WoLLogic._sc2wol_has_anti_air(self, world, player)
self._sc2wol_defense_rating = lambda world, player, zerg_enemy, air_enemy=False: SC2WoLLogic._sc2wol_defense_rating(self, world, player, zerg_enemy, air_enemy)
self._sc2wol_has_competent_comp = lambda world, player: SC2WoLLogic._sc2wol_has_competent_comp(self, world, player)
@@ -210,6 +293,8 @@ def _read_logic(self):
self._sc2wol_has_protoss_common_units = lambda world, player: SC2WoLLogic._sc2wol_has_protoss_common_units(self, world, player)
self._sc2wol_has_protoss_medium_units = lambda world, player: SC2WoLLogic._sc2wol_has_protoss_medium_units(self, world, player)
self._sc2wol_has_mm_upgrade = lambda world, player: SC2WoLLogic._sc2wol_has_mm_upgrade(self, world, player)
+ self._sc2wol_welcome_to_the_jungle_requirement = lambda world, player: SC2WoLLogic._sc2wol_welcome_to_the_jungle_requirement(self, world, player)
+ self._sc2wol_can_respond_to_colony_infestations = lambda world, player: SC2WoLLogic._sc2wol_can_respond_to_colony_infestations(self, world, player)
self._sc2wol_final_mission_requirements = lambda world, player: SC2WoLLogic._sc2wol_final_mission_requirements(self, world, player)
def __init__(self, multiworld: MultiWorld, player: int,
@@ -230,7 +315,7 @@ def __init__(self, multiworld: MultiWorld, player: int,
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 = item_table[item.name]
+ item_info = get_full_item_list()[item.name]
if item_info.type == "Upgrade":
# Locking upgrades based on mission duration
if item.name not in item_quantities:
diff --git a/worlds/sc2wol/Regions.py b/worlds/sc2wol/Regions.py
index 033636662b59..f588ce7e982e 100644
--- a/worlds/sc2wol/Regions.py
+++ b/worlds/sc2wol/Regions.py
@@ -1,10 +1,14 @@
from typing import List, Set, Dict, Tuple, Optional, Callable
from BaseClasses import MultiWorld, Region, Entrance, Location
from .Locations import LocationData
-from .Options import get_option_value
-from .MissionTables import MissionInfo, mission_orders, vanilla_mission_req_table, alt_final_mission_locations, MissionPools
+from .Options import get_option_value, MissionOrder
+from .MissionTables import MissionInfo, mission_orders, vanilla_mission_req_table, alt_final_mission_locations, \
+ MissionPools, vanilla_shuffle_order
from .PoolFilter import filter_missions
+PROPHECY_CHAIN_MISSION_COUNT = 4
+
+VANILLA_SHUFFLED_FIRST_PROPHECY_MISSION = 21
def create_regions(multiworld: MultiWorld, player: int, locations: Tuple[LocationData, ...], location_cache: List[Location])\
-> Tuple[Dict[str, MissionInfo], int, str]:
@@ -19,7 +23,7 @@ def create_regions(multiworld: MultiWorld, player: int, locations: Tuple[Locatio
names: Dict[str, int] = {}
- if mission_order_type == 0:
+ if mission_order_type == MissionOrder.option_vanilla:
# Generating all regions and locations
for region_name in vanilla_mission_req_table.keys():
@@ -108,12 +112,17 @@ def create_regions(multiworld: MultiWorld, player: int, locations: Tuple[Locatio
removals = len(mission_order) - mission_pool_size
# Removing entire Prophecy chain on vanilla shuffled when not shuffling protoss
if remove_prophecy:
- removals -= 4
+ removals -= PROPHECY_CHAIN_MISSION_COUNT
# Initial fill out of mission list and marking all-in mission
for mission in mission_order:
# Removing extra missions if mission pool is too small
- if 0 < mission.removal_priority <= removals or mission.category == 'Prophecy' and remove_prophecy:
+ # Also handle lower removal priority than Prophecy
+ if 0 < mission.removal_priority <= removals or mission.category == 'Prophecy' and remove_prophecy \
+ or (remove_prophecy and mission_order_type == MissionOrder.option_vanilla_shuffled
+ and mission.removal_priority > vanilla_shuffle_order[
+ VANILLA_SHUFFLED_FIRST_PROPHECY_MISSION].removal_priority
+ and 0 < mission.removal_priority <= removals + PROPHECY_CHAIN_MISSION_COUNT):
missions.append(None)
elif mission.type == MissionPools.FINAL:
missions.append(final_mission)
@@ -191,22 +200,38 @@ def create_regions(multiworld: MultiWorld, player: int, locations: Tuple[Locatio
# TODO: Handle 'and' connections
mission_req_table = {}
+ def build_connection_rule(mission_names: List[str], missions_req: int) -> Callable:
+ if len(mission_names) > 1:
+ return lambda state: state.has_all({f"Beat {name}" for name in mission_names}, player) and \
+ state._sc2wol_cleared_missions(multiworld, player, missions_req)
+ else:
+ return lambda state: state.has(f"Beat {mission_names[0]}", player) and \
+ state._sc2wol_cleared_missions(multiworld, player, missions_req)
+
for i, mission in enumerate(missions):
if mission is None:
continue
connections = []
+ all_connections = []
+ for connection in mission_order[i].connect_to:
+ if connection == -1:
+ continue
+ while missions[connection] is None:
+ connection -= 1
+ all_connections.append(missions[connection])
for connection in mission_order[i].connect_to:
required_mission = missions[connection]
if connection == -1:
connect(multiworld, player, names, "Menu", mission)
- elif required_mission is None:
- continue
else:
+ if required_mission is None and not mission_order[i].completion_critical: # Drop non-critical null slots
+ continue
+ while required_mission is None: # Substituting null slot with prior slot
+ connection -= 1
+ required_mission = missions[connection]
+ required_missions = [required_mission] if mission_order[i].or_requirements else all_connections
connect(multiworld, player, names, required_mission, mission,
- (lambda name, missions_req: (lambda state: state.has(f"Beat {name}", player) and
- state._sc2wol_cleared_missions(multiworld, player,
- missions_req)))
- (missions[connection], mission_order[i].number))
+ build_connection_rule(required_missions, mission_order[i].number))
connections.append(slot_map[connection])
mission_req_table.update({mission: MissionInfo(
diff --git a/worlds/sc2wol/__init__.py b/worlds/sc2wol/__init__.py
index 490524290c8a..93aebb7ad15a 100644
--- a/worlds/sc2wol/__init__.py
+++ b/worlds/sc2wol/__init__.py
@@ -3,11 +3,11 @@
from typing import List, Set, Tuple, Dict
from BaseClasses import Item, MultiWorld, Location, Tutorial, ItemClassification
from worlds.AutoWorld import WebWorld, World
-from .Items import StarcraftWoLItem, item_table, filler_items, item_name_groups, get_full_item_list, \
- get_basic_units
-from .Locations import get_locations
+from .Items import StarcraftWoLItem, filler_items, item_name_groups, get_item_table, get_full_item_list, \
+ get_basic_units, ItemData, upgrade_included_names, progressive_if_nco
+from .Locations import get_locations, LocationType
from .Regions import create_regions
-from .Options import sc2wol_options, get_option_value
+from .Options import sc2wol_options, get_option_value, LocationInclusion
from .LogicMixin import SC2WoLLogic
from .PoolFilter import filter_missions, filter_items, get_item_upgrades
from .MissionTables import starting_mission_locations, MissionInfo
@@ -36,7 +36,7 @@ class SC2WoLWorld(World):
web = Starcraft2WoLWebWorld()
data_version = 4
- item_name_to_id = {name: data.code for name, data in item_table.items()}
+ item_name_to_id = {name: data.code for name, data in get_full_item_list().items()}
location_name_to_id = {location.name: location.code for location in get_locations(None, None)}
option_definitions = sc2wol_options
@@ -69,6 +69,8 @@ def create_items(self):
starter_items = assign_starter_items(self.multiworld, self.player, excluded_items, self.locked_locations)
+ filter_locations(self.multiworld, self.player, self.locked_locations, self.location_cache)
+
pool = get_item_pool(self.multiworld, self.player, self.mission_req_table, starter_items, excluded_items, self.location_cache)
fill_item_pool_with_dummy_items(self, self.multiworld, self.player, self.locked_locations, self.location_cache, pool)
@@ -109,16 +111,6 @@ def setup_events(player: int, locked_locations: typing.List[str], location_cache
def get_excluded_items(multiworld: MultiWorld, player: int) -> Set[str]:
excluded_items: Set[str] = set()
- if get_option_value(multiworld, player, "upgrade_bonus") == 1:
- excluded_items.add("Ultra-Capacitors")
- else:
- excluded_items.add("Vanadium Plating")
-
- if get_option_value(multiworld, player, "bunker_upgrade") == 1:
- excluded_items.add("Shrike Turret")
- else:
- excluded_items.add("Fortified Bunker")
-
for item in multiworld.precollected_items[player]:
excluded_items.add(item.name)
@@ -167,7 +159,7 @@ def assign_starter_item(multiworld: MultiWorld, player: int, excluded_items: Set
def get_item_pool(multiworld: MultiWorld, player: int, mission_req_table: Dict[str, MissionInfo],
- starter_items: List[str], excluded_items: Set[str], location_cache: List[Location]) -> List[Item]:
+ 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
@@ -176,17 +168,43 @@ def get_item_pool(multiworld: MultiWorld, player: int, mission_req_table: Dict[s
# YAML items
yaml_locked_items = get_option_value(multiworld, player, 'locked_items')
- for name, data in item_table.items():
- if name not in excluded_items:
- for _ in range(data.quantity):
- item = create_item_with_correct_settings(player, name)
- if name in yaml_locked_items:
- locked_items.append(item)
- else:
- pool.append(item)
+ # Adjust generic upgrade availability based on options
+ include_upgrades = get_option_value(multiworld, player, 'generic_upgrade_missions') == 0
+ upgrade_items = get_option_value(multiworld, player, 'generic_upgrade_items')
+
+ # Include items from outside Wings of Liberty
+ item_sets = {'wol'}
+ if get_option_value(multiworld, player, 'nco_items'):
+ item_sets.add('nco')
+ if get_option_value(multiworld, player, 'bw_items'):
+ item_sets.add('bw')
+ if get_option_value(multiworld, player, 'ext_items'):
+ item_sets.add('ext')
+
+ def allowed_quantity(name: str, data: ItemData) -> int:
+ if name in excluded_items \
+ or data.type == "Upgrade" and (not include_upgrades or name not in upgrade_included_names[upgrade_items]) \
+ or not data.origin.intersection(item_sets):
+ return 0
+ elif name in progressive_if_nco and 'nco' not in item_sets:
+ return 1
+ else:
+ return data.quantity
+
+ for name, data in get_item_table(multiworld, player).items():
+ for i in range(allowed_quantity(name, data)):
+ item = create_item_with_correct_settings(player, name)
+ if name in yaml_locked_items:
+ locked_items.append(item)
+ else:
+ pool.append(item)
existing_items = starter_items + [item for item in multiworld.precollected_items[player]]
existing_names = [item.name for item in existing_items]
+
+ # Check the parent item integrity, exclude items
+ pool[:] = [item for item in pool if pool_contains_parent(item, pool + locked_items + existing_items)]
+
# Removing upgrades for excluded items
for item_name in excluded_items:
if item_name in existing_names:
@@ -207,8 +225,100 @@ def fill_item_pool_with_dummy_items(self: SC2WoLWorld, multiworld: MultiWorld, p
def create_item_with_correct_settings(player: int, name: str) -> Item:
- data = item_table[name]
+ data = get_full_item_list()[name]
item = Item(name, data.classification, data.code, player)
return item
+
+
+def pool_contains_parent(item: Item, pool: [Item]):
+ item_data = get_full_item_list().get(item.name)
+ if item_data.parent_item is None:
+ # The item has not associated parent, the item is valid
+ return True
+ parent_item = item_data.parent_item
+ # Check if the pool contains the parent item
+ return parent_item in [pool_item.name for pool_item in pool]
+
+
+def filter_locations(multiworld: MultiWorld, player, locked_locations: List[str], location_cache: List[Location]):
+ """
+ Filters the locations in the world using a trash or Nothing item
+ :param multiworld:
+ :param player:
+ :param locked_locations:
+ :param location_cache:
+ :return:
+ """
+ open_locations = [location for location in location_cache if location.item is None]
+ plando_locations = get_plando_locations(multiworld, player)
+ mission_progress_locations = get_option_value(multiworld, player, "mission_progress_locations")
+ bonus_locations = get_option_value(multiworld, player, "bonus_locations")
+ challenge_locations = get_option_value(multiworld, player, "challenge_locations")
+ optional_boss_locations = get_option_value(multiworld, player, "optional_boss_locations")
+ location_data = get_locations(multiworld, player)
+ for location in open_locations:
+ # Go through the locations that aren't locked yet (early unit, etc)
+ if location.name not in plando_locations:
+ # The location is not plando'd
+ sc2_location = [sc2_location for sc2_location in location_data if sc2_location.name == location.name][0]
+ location_type = sc2_location.type
+
+ if location_type == LocationType.MISSION_PROGRESS \
+ and mission_progress_locations != LocationInclusion.option_enabled:
+ item_name = get_exclusion_item(multiworld, mission_progress_locations)
+ place_exclusion_item(item_name, location, locked_locations, player)
+
+ if location_type == LocationType.BONUS \
+ and bonus_locations != LocationInclusion.option_enabled:
+ item_name = get_exclusion_item(multiworld, bonus_locations)
+ place_exclusion_item(item_name, location, locked_locations, player)
+
+ if location_type == LocationType.CHALLENGE \
+ and challenge_locations != LocationInclusion.option_enabled:
+ item_name = get_exclusion_item(multiworld, challenge_locations)
+ place_exclusion_item(item_name, location, locked_locations, player)
+
+ if location_type == LocationType.OPTIONAL_BOSS \
+ and optional_boss_locations != LocationInclusion.option_enabled:
+ item_name = get_exclusion_item(multiworld, optional_boss_locations)
+ place_exclusion_item(item_name, location, locked_locations, player)
+
+
+def place_exclusion_item(item_name, location, locked_locations, player):
+ item = create_item_with_correct_settings(player, item_name)
+ location.place_locked_item(item)
+ locked_locations.append(location.name)
+
+
+def get_exclusion_item(multiworld: MultiWorld, option) -> str:
+ """
+ Gets the exclusion item according to settings (trash/nothing)
+ :param multiworld:
+ :param option:
+ :return: Item used for location exclusion
+ """
+ if option == LocationInclusion.option_nothing:
+ return "Nothing"
+ elif option == LocationInclusion.option_trash:
+ index = multiworld.random.randint(0, len(filler_items) - 1)
+ return filler_items[index]
+ raise Exception(f"Unsupported option type: {option}")
+
+
+def get_plando_locations(multiworld: MultiWorld, player) -> List[str]:
+ """
+
+ :param multiworld:
+ :param player:
+ :return: A list of locations affected by a plando in a world
+ """
+ plando_locations = []
+ for plando_setting in multiworld.plando_items[player]:
+ plando_locations += plando_setting.get("locations", [])
+ plando_setting_location = plando_setting.get("location", None)
+ if plando_setting_location is not None:
+ plando_locations.append(plando_setting_location)
+
+ return plando_locations
diff --git a/worlds/sc2wol/docs/setup_en.md b/worlds/sc2wol/docs/setup_en.md
index 13c7cb91e3aa..9bfeb3d235bc 100644
--- a/worlds/sc2wol/docs/setup_en.md
+++ b/worlds/sc2wol/docs/setup_en.md
@@ -7,7 +7,7 @@ to obtain a config file for StarCraft 2.
- [StarCraft 2](https://starcraft2.com/en-us/)
- [The most recent Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases)
-- [StarCraft 2 AP Maps and Data](https://github.com/TheCondor07/Starcraft2ArchipelagoData)
+- [StarCraft 2 AP Maps and Data](https://github.com/Ziktofel/Archipelago-SC2-data/releases)
## How do I install this randomizer?
@@ -49,7 +49,7 @@ specific description of what's going wrong and attach your log file to your mess
## Running in macOS
-To run StarCraft 2 through Archipelago in macOS, you will need to run the client via source as seen here: [macOS Guide](https://archipelago.gg/tutorial/Archipelago/mac/en). Note: when running the client, you will need to run the command `python3 Starcraft2Client.py`. This is done to make sure that `/download_data` works correctly.
+To run StarCraft 2 through Archipelago in macOS, you will need to run the client via source as seen here: [macOS Guide](https://archipelago.gg/tutorial/Archipelago/mac/en). Note: when running the client, you will need to run the command `python3 Starcraft2Client.py`.
## Running in Linux
diff --git a/worlds/sm/data/SMBasepatch_prebuilt/multiworld-basepatch.ips b/worlds/sm/data/SMBasepatch_prebuilt/multiworld-basepatch.ips
index 0455364d8a7b..7854ca333287 100644
Binary files a/worlds/sm/data/SMBasepatch_prebuilt/multiworld-basepatch.ips and b/worlds/sm/data/SMBasepatch_prebuilt/multiworld-basepatch.ips differ
diff --git a/worlds/sm/data/SMBasepatch_prebuilt/multiworld.sym b/worlds/sm/data/SMBasepatch_prebuilt/multiworld.sym
index 2d231bc89961..0a76c2071484 100644
--- a/worlds/sm/data/SMBasepatch_prebuilt/multiworld.sym
+++ b/worlds/sm/data/SMBasepatch_prebuilt/multiworld.sym
@@ -2,7 +2,7 @@
; generated by asar
[labels]
-82:FA23 :neg_1_1
+82:FA40 :neg_1_1
B8:83C1 :neg_1_2
B8:85DA :neg_1_3
B8:85F9 :neg_1_4
@@ -177,7 +177,7 @@ A0:FE00 setup_music
A0:FE0B setup_music_quick
A0:FE94 setup_samus
A0:FEA5 setup_samus_normal
-82:FA15 sm_fix_checksum
+82:FA2A sm_fix_checksum
B8:8800 sm_item_graphics
B8:882E sm_item_plm_pickup_sequence_pointers
B8:847B sm_save_done_hook
@@ -192,7 +192,7 @@ B8:83F4 write_repeated_memory_loop
[source files]
0000 fe019f55 main.asm
-0001 91a745e3 ../common/fast_reload.asm
+0001 62a29254 ../common/fast_reload.asm
0002 06780555 ../common/nofanfare.asm
0003 7a8904b6 ../common/multiworld.asm
0004 f7e9db95 ../common/itemextras.asm
@@ -200,163 +200,177 @@ B8:83F4 write_repeated_memory_loop
0006 dbfcb38d ../common/startitem.asm
[rom checksum]
-f95f09a7
+e50233eb
[addr-to-line mapping]
ff:ffff 0000:00000001
-ce:ff0a 0001:00000012
-82:f990 0001:00000016
-82:f991 0001:00000017
-82:f993 0001:00000018
-82:f994 0001:00000019
-82:f996 0001:0000001a
-82:f999 0001:0000001b
-82:f99c 0001:0000001c
-82:f99e 0001:0000001d
-82:f9a2 0001:0000001e
-82:f9a4 0001:0000001f
-82:f9a7 0001:00000020
-82:f9a9 0001:00000021
-82:f9ac 0001:00000022
-82:f9af 0001:00000023
-82:f9b1 0001:00000024
-82:f9b5 0001:00000025
-82:f9b8 0001:00000026
-82:f9ba 0001:00000027
-82:f9bd 0001:00000028
-82:f9c1 0001:00000029
-82:f9c3 0001:0000002a
-82:f9c4 0001:0000002b
-82:f9c5 0001:0000002c
-82:f9c8 0001:0000002d
-82:f9c9 0001:0000002f
-82:f9ca 0001:00000030
-82:f9cb 0001:00000031
-82:f9cc 0001:00000034
-82:f9cd 0001:00000035
-82:f9cf 0001:00000036
-82:f9d2 0001:00000037
-82:f9d5 0001:00000039
-82:f9d9 0001:0000003a
-82:f9dc 0001:0000003a
-82:f9de 0001:0000003b
-82:f9e1 0001:0000003d
-82:f9e2 0001:0000003e
-82:f9e5 0001:0000003e
-82:f9e6 0001:0000003e
-82:f9e9 0001:0000003f
-82:f9ea 0001:0000003f
-82:f9eb 0001:00000040
-82:f9ee 0001:00000040
-82:f9f1 0001:00000041
-82:f9f2 0001:00000042
-82:f9f6 0001:00000043
-82:f9fa 0001:00000044
-82:f9fe 0001:00000046
-82:fa02 0001:00000047
-82:fa05 0001:00000048
-82:fa09 0001:00000049
-82:fa0d 0001:0000004a
-82:fa10 0001:0000004b
-82:fa13 0001:0000004c
-82:fa14 0001:0000004d
-82:fa15 0001:00000050
-82:fa16 0001:00000051
+ce:ff0a 0001:00000013
+82:f990 0001:00000017
+82:f991 0001:00000018
+82:f993 0001:00000019
+82:f994 0001:0000001a
+82:f996 0001:0000001b
+82:f999 0001:0000001c
+82:f99c 0001:0000001d
+82:f99e 0001:0000001e
+82:f9a2 0001:0000001f
+82:f9a4 0001:00000020
+82:f9a7 0001:00000021
+82:f9a9 0001:00000022
+82:f9ac 0001:00000023
+82:f9af 0001:00000024
+82:f9b1 0001:00000025
+82:f9b5 0001:00000026
+82:f9b8 0001:00000027
+82:f9ba 0001:00000028
+82:f9bd 0001:00000029
+82:f9c1 0001:0000002a
+82:f9c3 0001:0000002b
+82:f9c4 0001:0000002c
+82:f9c5 0001:0000002d
+82:f9c8 0001:0000002e
+82:f9c9 0001:00000030
+82:f9ca 0001:00000031
+82:f9cb 0001:00000032
+82:f9cc 0001:00000035
+82:f9cd 0001:00000036
+82:f9cf 0001:00000037
+82:f9d2 0001:00000038
+82:f9d5 0001:0000003a
+82:f9d9 0001:0000003b
+82:f9dc 0001:0000003b
+82:f9de 0001:0000003c
+82:f9e1 0001:0000003e
+82:f9e2 0001:0000003f
+82:f9e6 0001:00000040
+82:f9ea 0001:00000042
+82:f9ee 0001:00000043
+82:f9f1 0001:00000044
+82:f9f2 0001:00000045
+82:f9f4 0001:00000046
+82:f9f5 0001:00000047
+82:f9f9 0001:00000048
+82:f9fc 0001:00000049
+82:f9fd 0001:0000004a
+82:f9fe 0001:0000004b
+82:f9ff 0001:0000004c
+82:fa02 0001:0000004c
+82:fa03 0001:0000004c
+82:fa07 0001:0000004d
+82:fa08 0001:0000004e
+82:fa0b 0001:0000004e
+82:fa0f 0001:0000004f
+82:fa13 0001:00000051
82:fa17 0001:00000052
-82:fa18 0001:00000053
-82:fa19 0000:00000013
-82:fa1b 0001:00000057
-82:fa1d 0001:00000058
-82:fa1e 0001:00000059
-82:fa20 0001:0000005a
-82:fa23 0001:0000005c
-82:fa27 0001:0000005d
-82:fa28 0001:0000005e
-82:fa2a 0001:0000005f
-82:fa2c 0001:00000060
-82:fa2d 0001:00000061
-82:fa2e 0001:00000062
-82:fa31 0001:00000063
-82:fa33 0001:00000065
-82:fa36 0001:00000066
+82:fa1a 0001:00000053
+82:fa1e 0001:00000054
+82:fa22 0001:00000055
+82:fa25 0001:00000056
+82:fa28 0001:00000057
+82:fa29 0001:00000058
+82:fa2a 0001:0000005b
+82:fa2b 0001:0000005c
+82:fa2c 0001:0000005d
+82:fa2d 0001:0000005e
+82:fa2e 0000:00000013
+82:fa30 0001:00000062
+82:fa32 0001:00000063
+82:fa33 0001:00000064
+82:fa35 0001:00000065
+82:fa37 0001:00000066
82:fa38 0001:00000067
82:fa3c 0001:00000068
-82:fa40 0001:00000069
-82:fa43 0001:0000006a
-82:fa47 0001:0000006b
-82:fa4b 0001:0000006c
-82:fa4c 0001:0000006d
-82:fa4e 0001:0000006f
-82:fa4f 0001:00000070
-82:fa50 0001:00000071
-82:fa51 0001:00000072
-82:fa52 0001:00000073
-80:a088 0001:00000079
-80:a08c 0001:00000079
-80:a08d 0001:00000079
-80:a095 0001:0000007c
-80:a0ce 0001:0000007f
-80:a113 0001:00000082
-91:e164 0001:00000085
-91:e168 0001:00000085
-91:e169 0001:00000085
-a0:fe00 0001:0000008a
-a0:fe03 0001:0000008b
-a0:fe05 0001:0000008c
-a0:fe08 0001:0000008d
-a0:fe0b 0001:0000008f
-a0:fe0c 0001:00000092
-a0:fe10 0001:00000093
-a0:fe13 0001:00000094
-a0:fe15 0001:00000095
-a0:fe18 0001:00000096
-a0:fe1b 0001:00000097
-a0:fe1f 0001:00000099
-a0:fe23 0001:0000009a
-a0:fe27 0001:0000009b
-a0:fe2b 0001:0000009c
-a0:fe2f 0001:0000009f
-a0:fe33 0001:000000a0
-a0:fe36 0001:000000a1
-a0:fe38 0001:000000a2
-a0:fe3c 0001:000000a3
-a0:fe40 0001:000000a5
-a0:fe44 0001:000000a8
-a0:fe48 0001:000000a9
-a0:fe49 0001:000000aa
-a0:fe4c 0001:000000ab
-a0:fe4e 0001:000000ac
-a0:fe4f 0001:000000ad
-a0:fe53 0001:000000ae
-a0:fe57 0001:000000b0
-a0:fe58 0001:000000b1
-a0:fe5c 0001:000000b2
-a0:fe5f 0001:000000b3
-a0:fe62 0001:000000b4
-a0:fe65 0001:000000b5
-a0:fe67 0001:000000b6
-a0:fe6a 0001:000000b7
-a0:fe6d 0001:000000b8
-a0:fe6f 0001:000000b9
-a0:fe73 0001:000000bc
-a0:fe76 0001:000000bd
-a0:fe79 0001:000000be
-a0:fe7c 0001:000000bf
-a0:fe7f 0001:000000c1
-a0:fe82 0001:000000c2
-a0:fe85 0001:000000c3
-a0:fe89 0001:000000c4
-a0:fe8c 0001:000000c5
-a0:fe90 0001:000000c7
-a0:fe94 0001:000000ca
-a0:fe97 0001:000000cb
-a0:fe99 0001:000000cc
-a0:fe9c 0001:000000cd
-a0:fe9f 0001:000000ce
-a0:fea2 0001:000000cf
-a0:fea5 0001:000000d1
-a0:fea8 0001:000000d2
-a0:feab 0001:000000d3
+82:fa3d 0001:00000069
+82:fa40 0001:0000006b
+82:fa44 0001:0000006c
+82:fa45 0001:0000006d
+82:fa47 0001:0000006e
+82:fa49 0001:0000006f
+82:fa4a 0001:00000070
+82:fa4b 0001:00000071
+82:fa4c 0001:00000072
+82:fa4d 0001:00000073
+82:fa50 0001:00000074
+82:fa52 0001:00000076
+82:fa54 0001:00000077
+82:fa56 0001:00000078
+82:fa5a 0001:00000079
+82:fa5e 0001:0000007a
+82:fa61 0001:0000007b
+82:fa65 0001:0000007c
+82:fa69 0001:0000007d
+82:fa6a 0001:0000007e
+82:fa6c 0001:00000080
+82:fa6d 0001:00000081
+82:fa6e 0001:00000082
+82:fa6f 0001:00000083
+82:fa70 0001:00000084
+80:a088 0001:0000008a
+80:a08c 0001:0000008a
+80:a08d 0001:0000008a
+80:a095 0001:0000008d
+80:a0ce 0001:00000090
+80:a113 0001:00000093
+91:e164 0001:00000096
+91:e168 0001:00000096
+91:e169 0001:00000096
+a0:fe00 0001:0000009b
+a0:fe03 0001:0000009c
+a0:fe05 0001:0000009d
+a0:fe08 0001:0000009e
+a0:fe0b 0001:000000a0
+a0:fe0c 0001:000000a3
+a0:fe10 0001:000000a4
+a0:fe13 0001:000000a5
+a0:fe15 0001:000000a6
+a0:fe18 0001:000000a7
+a0:fe1b 0001:000000a8
+a0:fe1f 0001:000000aa
+a0:fe23 0001:000000ab
+a0:fe27 0001:000000ac
+a0:fe2b 0001:000000ad
+a0:fe2f 0001:000000b0
+a0:fe33 0001:000000b1
+a0:fe36 0001:000000b2
+a0:fe38 0001:000000b3
+a0:fe3c 0001:000000b4
+a0:fe40 0001:000000b6
+a0:fe44 0001:000000b9
+a0:fe48 0001:000000ba
+a0:fe49 0001:000000bb
+a0:fe4c 0001:000000bc
+a0:fe4e 0001:000000bd
+a0:fe4f 0001:000000be
+a0:fe53 0001:000000bf
+a0:fe57 0001:000000c1
+a0:fe58 0001:000000c2
+a0:fe5c 0001:000000c3
+a0:fe5f 0001:000000c4
+a0:fe62 0001:000000c5
+a0:fe65 0001:000000c6
+a0:fe67 0001:000000c7
+a0:fe6a 0001:000000c8
+a0:fe6d 0001:000000c9
+a0:fe6f 0001:000000ca
+a0:fe73 0001:000000cd
+a0:fe76 0001:000000ce
+a0:fe79 0001:000000cf
+a0:fe7c 0001:000000d0
+a0:fe7f 0001:000000d2
+a0:fe82 0001:000000d3
+a0:fe85 0001:000000d4
+a0:fe89 0001:000000d5
+a0:fe8c 0001:000000d6
+a0:fe90 0001:000000d8
+a0:fe94 0001:000000db
+a0:fe97 0001:000000dc
+a0:fe99 0001:000000dd
+a0:fe9c 0001:000000de
+a0:fe9f 0001:000000df
+a0:fea2 0001:000000e0
+a0:fea5 0001:000000e2
+a0:fea8 0001:000000e3
+a0:feab 0001:000000e4
85:ff00 0002:0000010b
85:ff03 0002:0000010c
85:ff06 0002:0000010d
diff --git a/worlds/sm/data/SMBasepatch_prebuilt/sm-basepatch-symbols.json b/worlds/sm/data/SMBasepatch_prebuilt/sm-basepatch-symbols.json
index 1e5dcccb6efb..0be5a05715b1 100644
--- a/worlds/sm/data/SMBasepatch_prebuilt/sm-basepatch-symbols.json
+++ b/worlds/sm/data/SMBasepatch_prebuilt/sm-basepatch-symbols.json
@@ -158,7 +158,7 @@
"setup_music_quick": "A0:FE0B",
"setup_samus": "A0:FE94",
"setup_samus_normal": "A0:FEA5",
- "sm_fix_checksum": "82:FA15",
+ "sm_fix_checksum": "82:FA2A",
"sm_item_graphics": "B8:8800",
"sm_item_plm_pickup_sequence_pointers": "B8:882E",
"sm_save_done_hook": "B8:847B",
@@ -172,10 +172,11 @@
"write_repeated_memory_loop": "B8:83F4",
"deathhook82": "82:DDC7",
"freespace82_start": "82:F990",
- "freespace82_end": "82:FA53",
+ "freespace82_end": "82:FA71",
"freespacea0": "a0:fe00",
"SRAM_SAVING": "70:2604",
"current_save_slot": "7e:0952",
+ "sram_save_slot_addresses": "81:812B",
"ITEM_RAM": "7E:09A2",
"SRAM_MW_ITEMS_RECV": "70:2000",
"SRAM_MW_ITEMS_RECV_WCOUNT": "70:2602",
diff --git a/worlds/sm/data/SMBasepatch_prebuilt/variapatches.ips b/worlds/sm/data/SMBasepatch_prebuilt/variapatches.ips
index 37139ae5785d..efc38e262f19 100644
Binary files a/worlds/sm/data/SMBasepatch_prebuilt/variapatches.ips and b/worlds/sm/data/SMBasepatch_prebuilt/variapatches.ips differ
diff --git a/worlds/sm/docs/en_Super Metroid.md b/worlds/sm/docs/en_Super Metroid.md
index c177582e221e..5c87e026f634 100644
--- a/worlds/sm/docs/en_Super Metroid.md
+++ b/worlds/sm/docs/en_Super Metroid.md
@@ -34,5 +34,6 @@ When the player receives an item, a text box will appear to show which item was
It can happen that a required item is in a place where you cant get back from. While in normal gameplay state, by holding
Start+Select+L+R at the same time, the game will save your progress and put you back at your original starting position.
-This can be required by the logic.
+This can be required by the logic. Since the addition of that feature, VARIA's automatic backup saves are disabled since
+you can't softlock anymore.
diff --git a/worlds/sm/docs/multiworld_en.md b/worlds/sm/docs/multiworld_en.md
index 20c055bc91bf..ce91e7a7e403 100644
--- a/worlds/sm/docs/multiworld_en.md
+++ b/worlds/sm/docs/multiworld_en.md
@@ -87,8 +87,7 @@ first time launching, you may be prompted to allow it to communicate through the
3. Click on **New Lua Script Window...**
4. In the new window, click **Browse...**
5. Select the connector lua file included with your client
- - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
- emulator is 64-bit or 32-bit.
+ - Look in the Archipelago folder for `/SNI/lua/Connector.lua`.
6. If you see an error while loading the script that states `socket.dll missing` or similar, navigate to the folder of
the lua you are using in your file explorer and copy the `socket.dll` to the base folder of your snes9x install.
@@ -100,8 +99,7 @@ the lua you are using in your file explorer and copy the `socket.dll` to the bas
2. Load your ROM file if it hasn't already been loaded.
If you changed your core preference after loading the ROM, don't forget to reload it (default hotkey: Ctrl+R).
3. Drag+drop the `Connector.lua` file included with your client onto the main EmuHawk window.
- - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
- emulator is 64-bit or 32-bit. Please note the most recent versions of BizHawk are 64-bit only.
+ - Look in the Archipelago folder for `/SNI/lua/Connector.lua`.
- You could instead open the Lua Console manually, click `Script` 〉 `Open Script`, and navigate to `Connector.lua`
with the file picker.
diff --git a/worlds/sm/variaRandomizer/rando/GraphBuilder.py b/worlds/sm/variaRandomizer/rando/GraphBuilder.py
index 3577baff0880..7bee33ec82e0 100644
--- a/worlds/sm/variaRandomizer/rando/GraphBuilder.py
+++ b/worlds/sm/variaRandomizer/rando/GraphBuilder.py
@@ -150,7 +150,6 @@ def escapeTrigger(self, emptyContainer, graph, maxDiff, escapeTrigger):
# update item% objectives
accessibleItems = [il.Item for il in allItemLocs if ilCheck(il)]
majorUpgrades = [item.Type for item in accessibleItems if item.BeamBits != 0 or item.ItemBits != 0]
- sm.objectives.setItemPercentFuncs(len(accessibleItems), majorUpgrades)
if split == "Scavenger":
# update escape access for scav with last scav loc
lastScavItemLoc = progItemLocs[-1]
@@ -163,6 +162,7 @@ def escapeTrigger(self, emptyContainer, graph, maxDiff, escapeTrigger):
if ilCheck(itemLoc) and (split.startswith("Full") or itemLoc.Location.isClass(split)):
availLocsByArea[itemLoc.Location.GraphArea].append(itemLoc.Location.Name)
self.log.debug("escapeTrigger. availLocsByArea="+str(availLocsByArea))
+ sm.objectives.setItemPercentFuncs(len(accessibleItems), majorUpgrades, container)
sm.objectives.setAreaFuncs({area:lambda sm,ap:SMBool(len(container.getLocs(lambda loc: loc.Name in availLocsByArea[area]))==0) for area in availLocsByArea})
self.log.debug("escapeTrigger. collect locs until G4 access")
# collect all item/locations up until we can pass G4 (the escape triggers)
diff --git a/worlds/sm/variaRandomizer/rom/rompatcher.py b/worlds/sm/variaRandomizer/rom/rompatcher.py
index 913d0d8df8ef..2dcf554a0065 100644
--- a/worlds/sm/variaRandomizer/rom/rompatcher.py
+++ b/worlds/sm/variaRandomizer/rom/rompatcher.py
@@ -25,7 +25,9 @@ class RomPatcher:
# faster MB cutscene transitions
'Mother_Brain_Cutscene_Edits',
# "Balanced" suit mode
- 'Removes_Gravity_Suit_heat_protection'
+ 'Removes_Gravity_Suit_heat_protection',
+ # new PLMs for indicating the color of the door on the other side
+ 'door_indicators_plms.ips'
],
# VARIA tweaks
'VariaTweaks' : ['WS_Etank', 'LN_Chozo_SpaceJump_Check_Disable', 'ln_chozo_platform.ips', 'bomb_torizo.ips'],
@@ -236,10 +238,12 @@ def applyIPSPatches(self):
plms.append('WS_Save_Blinking_Door')
if self.settings["boss"] == True:
stdPatches.append("Phantoon_Eye_Door")
- if (self.settings["area"] == True
- or self.settings["doorsColorsRando"] == True
- or not GraphUtils.isStandardStart(self.settings["startLocation"])):
- stdPatches.append("Enable_Backup_Saves")
+ # rolling saves is not required anymore since the addition of fast_save_reload
+ # also, both arent completely compatible as-is
+ #if (self.settings["area"] == True
+ # or self.settings["doorsColorsRando"] == True
+ # or not GraphUtils.isStandardStart(self.settings["startLocation"])):
+ # stdPatches.append("Enable_Backup_Saves")
if 'varia_hud.ips' in self.settings["optionalPatches"]:
# varia hud can make demos glitch out
self.applyIPSPatch("no_demo.ips")
diff --git a/worlds/sm/variaRandomizer/utils/objectives.py b/worlds/sm/variaRandomizer/utils/objectives.py
index 8c886674fd2c..67cdb9a1c132 100644
--- a/worlds/sm/variaRandomizer/utils/objectives.py
+++ b/worlds/sm/variaRandomizer/utils/objectives.py
@@ -511,16 +511,18 @@ def updateItemPercentEscapeAccess(self, collectedLocsAccessPoints):
def setScavengerHuntFunc(self, scavClearFunc):
self.goals["finish scavenger hunt"].clearFunc = scavClearFunc
- def setItemPercentFuncs(self, totalItemsCount=None, allUpgradeTypes=None):
- def getPctFunc(pct, totalItemsCount):
+ def setItemPercentFuncs(self, totalItemsCount=None, allUpgradeTypes=None, container=None):
+ def getPctFunc(total_needed, container):
def f(sm, ap):
- nonlocal pct, totalItemsCount
- return sm.hasItemsPercent(pct, totalItemsCount)
+ nonlocal total_needed, container
+ locs_checked = len(container.getUsedLocs(lambda loc: True))
+ return SMBool(locs_checked >= total_needed)
return f
+ # AP: now based on location checks instead of local item
for pct in [25,50,75,100]:
goal = 'collect %d%% items' % pct
- self.goals[goal].clearFunc = getPctFunc(pct, totalItemsCount)
+ self.goals[goal].clearFunc = getPctFunc(totalItemsCount * pct / 100, container)
if allUpgradeTypes is not None:
self.goals["collect all upgrades"].clearFunc = lambda sm, ap: sm.haveItems(allUpgradeTypes)
diff --git a/worlds/sm64ex/docs/en_Super Mario 64.md b/worlds/sm64ex/docs/en_Super Mario 64.md
index 4586369e5ec7..def6e2a37536 100644
--- a/worlds/sm64ex/docs/en_Super Mario 64.md
+++ b/worlds/sm64ex/docs/en_Super Mario 64.md
@@ -14,7 +14,7 @@ as different Items from within SM64.
As in most Mario Games, save the Princess!
## Which items can be in another player's world?
-Any of the 120 Stars, and the two Caste Keys. Additionally, Cap Switches are also considered "Items" and the "!"-Boxes will only be active
+Any of the 120 Stars, and the two Castle Keys. Additionally, Cap Switches are also considered "Items" and the "!"-Boxes will only be active
when someone collects the corresponding Cap Switch Item.
## What does another world's item look like in SM64EX?
@@ -25,4 +25,4 @@ and who will receive it.
When you receive an Item, a Message will pop up to inform you where you received the Item from,
and which one it is.
-NOTE: The Secret Star count in the Menu is broken.
\ No newline at end of file
+NOTE: The Secret Star count in the Menu is broken.
diff --git a/worlds/smw/docs/setup_en.md b/worlds/smw/docs/setup_en.md
index 2a9435c95d42..9ca8bdf58a16 100644
--- a/worlds/smw/docs/setup_en.md
+++ b/worlds/smw/docs/setup_en.md
@@ -77,8 +77,7 @@ first time launching, you may be prompted to allow it to communicate through the
3. Click on **New Lua Script Window...**
4. In the new window, click **Browse...**
5. Select the connector lua file included with your client
- - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
- emulator is 64-bit or 32-bit.
+ - Look in the Archipelago folder for `/SNI/lua/Connector.lua`.
6. If you see an error while loading the script that states `socket.dll missing` or similar, navigate to the folder of
the lua you are using in your file explorer and copy the `socket.dll` to the base folder of your snes9x install.
@@ -90,8 +89,7 @@ the lua you are using in your file explorer and copy the `socket.dll` to the bas
2. Load your ROM file if it hasn't already been loaded.
If you changed your core preference after loading the ROM, don't forget to reload it (default hotkey: Ctrl+R).
3. Drag+drop the `Connector.lua` file included with your client onto the main EmuHawk window.
- - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
- emulator is 64-bit or 32-bit. Please note the most recent versions of BizHawk are 64-bit only.
+ - Look in the Archipelago folder for `/SNI/lua/Connector.lua`.
- You could instead open the Lua Console manually, click `Script` 〉 `Open Script`, and navigate to `Connector.lua`
with the file picker.
diff --git a/worlds/smz3/TotalSMZ3/Item.py b/worlds/smz3/TotalSMZ3/Item.py
index b4fc9d592550..28e9658ce1d0 100644
--- a/worlds/smz3/TotalSMZ3/Item.py
+++ b/worlds/smz3/TotalSMZ3/Item.py
@@ -181,6 +181,7 @@ class Item:
keycard = re.compile("^Card")
smMap = re.compile("^SmMap")
+ def IsNameDungeonItem(item_name): return Item.dungeon.match(item_name)
def IsDungeonItem(self): return self.dungeon.match(self.Type.name)
def IsBigKey(self): return self.bigKey.match(self.Type.name)
def IsKey(self): return self.key.match(self.Type.name)
diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py
index 79ba17db82b1..e2eb2ac80a13 100644
--- a/worlds/smz3/__init__.py
+++ b/worlds/smz3/__init__.py
@@ -221,7 +221,9 @@ def create_items(self):
if (self.smz3World.Config.Keysanity):
progressionItems = self.progression + self.dungeon + self.keyCardsItems + self.SmMapsItems
else:
- progressionItems = self.progression
+ progressionItems = self.progression
+ # Dungeons items here are not in the itempool and will be prefilled locally so they must stay local
+ self.multiworld.non_local_items[self.player].value -= frozenset(item_name for item_name in self.item_names if TotalSMZ3Item.Item.IsNameDungeonItem(item_name))
for item in self.keyCardsItems:
self.multiworld.push_precollected(SMZ3Item(item.Type.name, ItemClassification.filler, item.Type, self.item_name_to_id[item.Type.name], self.player, item))
@@ -548,11 +550,8 @@ def write_spoiler(self, spoiler_handle: TextIO):
def JunkFillGT(self, factor):
poolLength = len(self.multiworld.itempool)
- playerGroups = self.multiworld.get_player_groups(self.player)
- playerGroups.add(self.player)
junkPoolIdx = [i for i in range(0, poolLength)
- if self.multiworld.itempool[i].classification in (ItemClassification.filler, ItemClassification.trap) and
- self.multiworld.itempool[i].player in playerGroups]
+ if self.multiworld.itempool[i].classification in (ItemClassification.filler, ItemClassification.trap)]
toRemove = []
for loc in self.locations.values():
# commenting this for now since doing a partial GT pre fill would allow for non SMZ3 progression in GT
@@ -563,6 +562,7 @@ def JunkFillGT(self, factor):
poolLength = len(junkPoolIdx)
# start looking at a random starting index and loop at start if no match found
start = self.multiworld.random.randint(0, poolLength)
+ itemFromPool = None
for off in range(0, poolLength):
i = (start + off) % poolLength
candidate = self.multiworld.itempool[junkPoolIdx[i]]
@@ -570,6 +570,7 @@ def JunkFillGT(self, factor):
itemFromPool = candidate
toRemove.append(junkPoolIdx[i])
break
+ assert itemFromPool is not None, "Can't find anymore item(s) to pre fill GT"
self.multiworld.push_item(loc, itemFromPool, False)
loc.event = False
toRemove.sort(reverse = True)
diff --git a/worlds/smz3/docs/multiworld_en.md b/worlds/smz3/docs/multiworld_en.md
index 27c8a507e3df..da6e29ab6923 100644
--- a/worlds/smz3/docs/multiworld_en.md
+++ b/worlds/smz3/docs/multiworld_en.md
@@ -85,8 +85,7 @@ first time launching, you may be prompted to allow it to communicate through the
3. Click on **New Lua Script Window...**
4. In the new window, click **Browse...**
5. Select the connector lua file included with your client
- - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
- emulator is 64-bit or 32-bit.
+ - Look in the Archipelago folder for `/SNI/lua/Connector.lua`.
6. If you see an error while loading the script that states `socket.dll missing` or similar, navigate to the folder of
the lua you are using in your file explorer and copy the `socket.dll` to the base folder of your snes9x install.
@@ -98,8 +97,7 @@ the lua you are using in your file explorer and copy the `socket.dll` to the bas
2. Load your ROM file if it hasn't already been loaded.
If you changed your core preference after loading the ROM, don't forget to reload it (default hotkey: Ctrl+R).
3. Drag+drop the `Connector.lua` file included with your client onto the main EmuHawk window.
- - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
- emulator is 64-bit or 32-bit. Please note the most recent versions of BizHawk are 64-bit only.
+ - Look in the Archipelago folder for `/SNI/lua/Connector.lua`.
- You could instead open the Lua Console manually, click `Script` 〉 `Open Script`, and navigate to `Connector.lua`
with the file picker.
diff --git a/worlds/stardew_valley/data/villagers_data.py b/worlds/stardew_valley/data/villagers_data.py
index 330d5eb955fb..e858d46f34a3 100644
--- a/worlds/stardew_valley/data/villagers_data.py
+++ b/worlds/stardew_valley/data/villagers_data.py
@@ -39,6 +39,7 @@ def __repr__(self):
sewers = (Region.sewer,)
island = (Region.island_east,)
secret_woods = (Region.secret_woods,)
+wizard_tower = (Region.wizard_tower,)
golden_pumpkin = ("Golden Pumpkin",)
# magic_rock_candy = ("Magic Rock Candy",)
@@ -314,7 +315,7 @@ def villager(name: str, bachelor: bool, locations: Tuple[str, ...], birthday: st
sandy = villager(NPC.sandy, False, oasis, Season.fall, universal_loves + sandy_loves, False)
vincent = villager(NPC.vincent, False, town, Season.spring, universal_loves + vincent_loves, True)
willy = villager(NPC.willy, False, beach, Season.summer, universal_loves + willy_loves, True)
-wizard = villager(NPC.wizard, False, forest, Season.winter, universal_loves + wizard_loves, True)
+wizard = villager(NPC.wizard, False, wizard_tower, Season.winter, universal_loves + wizard_loves, True)
# Custom NPCs
alec = villager(ModNPC.alec, True, forest, Season.winter, universal_loves + trilobite, True, ModNames.alec)
diff --git a/worlds/stardew_valley/docs/en_Stardew Valley.md b/worlds/stardew_valley/docs/en_Stardew Valley.md
index 042163343eb9..a880a40b971a 100644
--- a/worlds/stardew_valley/docs/en_Stardew Valley.md
+++ b/worlds/stardew_valley/docs/en_Stardew Valley.md
@@ -7,7 +7,7 @@ config file.
## What does randomization do to this game?
-A vast number of optional objectives in stardew valley can be shuffled around the multiworld. Most of these are optional, and the player can customize their experience in their YAML file.
+A vast number of objectives in Stardew Valley can be shuffled around the multiworld. Most of these are optional, and the player can customize their experience in their YAML file.
For these objectives, if they have a vanilla reward, this reward will instead be an item in the multiworld. For the remaining number of such objectives, there are a number of "Resource Pack" items, which are simply an item or a stack of items that may be useful to the player.
@@ -28,24 +28,24 @@ The player can choose from a number of goals, using their YAML settings.
Location checks in Stardew Valley always include:
- [Community Center Bundles](https://stardewvalleywiki.com/Bundles)
-- [Mineshaft chest rewards](https://stardewvalleywiki.com/The_Mines#Remixed_Rewards)
+- [Mineshaft Chest Rewards](https://stardewvalleywiki.com/The_Mines#Remixed_Rewards)
- [Story Quests](https://stardewvalleywiki.com/Quests#List_of_Story_Quests)
-- [Traveling Merchant items](https://stardewvalleywiki.com/Traveling_Cart)
+- [Traveling Merchant Items](https://stardewvalleywiki.com/Traveling_Cart)
- Isolated objectives such as the [beach bridge](https://stardewvalleywiki.com/The_Beach#Tide_Pools), [Old Master Cannoli](https://stardewvalleywiki.com/Secret_Woods#Old_Master_Cannoli), [Grim Reaper Statue](https://stardewvalleywiki.com/Golden_Scythe), etc
There also are a number of location checks that are optional, and individual players choose to include them or not in their shuffling:
- Tools and Fishing Rod Upgrades
- Carpenter Buildings
- Backpack Upgrades
-- Mine elevator levels
+- Mine Elevator Levels
- Skill Levels
- Arcade Machines
-- Help Wanted quests
+- Help Wanted Quests
- Participating in Festivals
- Special Orders from the town board, or from Mr Qi
-- Cropsanity: Growing and harvesting individual crop types
+- Cropsanity: Growing and Harvesting individual crop types
- Fishsanity: Catching individual fish
-- Museumsanity: Donating individual items to the museum, or reaching the museum milestones for donations
+- Museumsanity: Donating individual items, or reaching milestones for museum donations
- Friendsanity: Reaching specific friendship levels with NPCs
## Which items can be in another player's world?
diff --git a/worlds/stardew_valley/logic.py b/worlds/stardew_valley/logic.py
index b12e02fff989..377fa0d03ba3 100644
--- a/worlds/stardew_valley/logic.py
+++ b/worlds/stardew_valley/logic.py
@@ -492,35 +492,39 @@ def __post_init__(self):
})
self.special_order_rules.update({
- SpecialOrder.island_ingredients: self.has_island_transport() & self.can_farm_perfectly() &
- self.has(Vegetable.taro_root) & self.has(Fruit.pineapple) & self.has(Forageable.ginger),
- SpecialOrder.cave_patrol: self.can_mine_perfectly() & self.can_mine_to_floor(120),
- SpecialOrder.aquatic_overpopulation: self.can_fish_perfectly(),
- SpecialOrder.biome_balance: self.can_fish_perfectly(),
- SpecialOrder.rock_rejuivenation: self.has(Mineral.ruby) & self.has(Mineral.topaz) & self.has(Mineral.emerald) &
- self.has(Mineral.jade) & self.has(Mineral.amethyst) & self.has_relationship(NPC.emily, 4) &
- self.has(ArtisanGood.cloth) & self.can_reach_region(Region.haley_house),
- SpecialOrder.gifts_for_george: self.has_season(Season.spring) & self.has(Forageable.leek),
- SpecialOrder.fragments_of_the_past: self.can_reach_region(Region.dig_site),
- SpecialOrder.gus_famous_omelet: self.has(AnimalProduct.any_egg),
- SpecialOrder.crop_order: self.can_farm_perfectly(),
- SpecialOrder.community_cleanup: self.can_crab_pot(),
- SpecialOrder.the_strong_stuff: self.can_keg(Vegetable.potato),
- SpecialOrder.pierres_prime_produce: self.can_farm_perfectly(),
- SpecialOrder.robins_project: self.can_chop_perfectly() & self.has(Material.hardwood),
- SpecialOrder.robins_resource_rush: self.can_chop_perfectly() & self.has(Fertilizer.tree) & self.can_mine_perfectly(),
- SpecialOrder.juicy_bugs_wanted_yum: self.has(Loot.bug_meat),
- SpecialOrder.tropical_fish: self.has_island_transport() & self.has(Fish.stingray) & self.has(Fish.blue_discus) & self.has(Fish.lionfish),
- SpecialOrder.a_curious_substance: self.can_mine_perfectly() & self.can_mine_to_floor(80),
- SpecialOrder.prismatic_jelly: self.can_mine_perfectly() & self.can_mine_to_floor(40),
+ SpecialOrder.island_ingredients: self.can_meet(NPC.caroline) & self.has_island_transport() & self.can_farm_perfectly() &
+ self.can_ship(Vegetable.taro_root) & self.can_ship(Fruit.pineapple) & self.can_ship(Forageable.ginger),
+ SpecialOrder.cave_patrol: self.can_meet(NPC.clint) & self.can_mine_perfectly() & self.can_mine_to_floor(120),
+ SpecialOrder.aquatic_overpopulation: self.can_meet(NPC.demetrius) & self.can_fish_perfectly(),
+ SpecialOrder.biome_balance: self.can_meet(NPC.demetrius) & self.can_fish_perfectly(),
+ SpecialOrder.rock_rejuivenation: self.has_relationship(NPC.emily, 4) & self.has(Mineral.ruby) & self.has(Mineral.topaz) &
+ self.has(Mineral.emerald) & self.has(Mineral.jade) & self.has(Mineral.amethyst) &
+ self.has(ArtisanGood.cloth) & self.can_reach_region(Region.haley_house),
+ SpecialOrder.gifts_for_george: self.can_reach_region(Region.alex_house) & self.has_season(Season.spring) & self.has(Forageable.leek),
+ SpecialOrder.fragments_of_the_past: self.can_reach_region(Region.museum) & self.can_reach_region(Region.dig_site) & self.has_tool(Tool.pickaxe),
+ SpecialOrder.gus_famous_omelet: self.can_reach_region(Region.saloon) & self.has(AnimalProduct.any_egg),
+ SpecialOrder.crop_order: self.can_farm_perfectly() & self.can_ship(),
+ SpecialOrder.community_cleanup: self.can_reach_region(Region.railroad) & self.can_crab_pot(),
+ SpecialOrder.the_strong_stuff: self.can_reach_region(Region.trailer) & self.can_keg(Vegetable.potato),
+ SpecialOrder.pierres_prime_produce: self.can_reach_region(Region.pierre_store) & self.can_farm_perfectly(),
+ SpecialOrder.robins_project: self.can_meet(NPC.robin) & self.can_reach_region(Region.carpenter) & self.can_chop_perfectly() &
+ self.has(Material.hardwood),
+ SpecialOrder.robins_resource_rush: self.can_meet(NPC.robin) & self.can_reach_region(Region.carpenter) & self.can_chop_perfectly() &
+ self.has(Fertilizer.tree) & self.can_mine_perfectly(),
+ SpecialOrder.juicy_bugs_wanted_yum: self.can_reach_region(Region.beach) & self.has(Loot.bug_meat),
+ SpecialOrder.tropical_fish: self.can_meet(NPC.willy) & self.received("Island Resort") & self.has_island_transport() &
+ self.has(Fish.stingray) & self.has(Fish.blue_discus) & self.has(Fish.lionfish),
+ SpecialOrder.a_curious_substance: self.can_reach_region(Region.wizard_tower) & self.can_mine_perfectly() & self.can_mine_to_floor(80),
+ SpecialOrder.prismatic_jelly: self.can_reach_region(Region.wizard_tower) & self.can_mine_perfectly() & self.can_mine_to_floor(40),
SpecialOrder.qis_crop: self.can_farm_perfectly() & self.can_reach_region(Region.greenhouse) &
self.can_reach_region(Region.island_west) & self.has_total_skill_level(50) &
- self.has(Machine.seed_maker),
+ self.has(Machine.seed_maker) & self.has_building(Building.shipping_bin),
SpecialOrder.lets_play_a_game: self.has_junimo_kart_max_level(),
SpecialOrder.four_precious_stones: self.has_lived_months(MAX_MONTHS) & self.has("Prismatic Shard") &
self.can_mine_perfectly_in_the_skull_cavern(),
SpecialOrder.qis_hungry_challenge: self.can_mine_perfectly_in_the_skull_cavern() & self.has_max_buffs(),
- SpecialOrder.qis_cuisine: self.can_cook() & (self.can_spend_money_at(Region.saloon, 205000) | self.can_spend_money_at(Region.pierre_store, 170000)),
+ SpecialOrder.qis_cuisine: self.can_cook() & (self.can_spend_money_at(Region.saloon, 205000) | self.can_spend_money_at(Region.pierre_store, 170000)) &
+ self.can_ship(),
SpecialOrder.qis_kindness: self.can_give_loved_gifts_to_everyone(),
SpecialOrder.extended_family: self.can_fish_perfectly() & self.has(Fish.angler) & self.has(Fish.glacierfish) &
self.has(Fish.crimsonfish) & self.has(Fish.mutant_carp) & self.has(Fish.legend),
@@ -1039,11 +1043,12 @@ def can_reproduce(self, number_children: int = 1) -> StardewRule:
def has_relationship(self, npc: str, hearts: int = 1) -> StardewRule:
if hearts <= 0:
return True_()
- if self.options[options.Friendsanity] == options.Friendsanity.option_none:
+ friendsanity = self.options[options.Friendsanity]
+ if friendsanity == options.Friendsanity.option_none:
return self.can_earn_relationship(npc, hearts)
if npc not in all_villagers_by_name:
if npc == NPC.pet:
- if self.options[options.Friendsanity] == options.Friendsanity.option_bachelors:
+ if friendsanity == options.Friendsanity.option_bachelors:
return self.can_befriend_pet(hearts)
return self.received_hearts(NPC.pet, hearts)
if npc == Generic.any or npc == Generic.bachelor:
@@ -1073,12 +1078,12 @@ def has_relationship(self, npc: str, hearts: int = 1) -> StardewRule:
if not self.npc_is_in_current_slot(npc):
return True_()
villager = all_villagers_by_name[npc]
- if self.options[options.Friendsanity] == options.Friendsanity.option_bachelors and not villager.bachelor:
+ if friendsanity == options.Friendsanity.option_bachelors and not villager.bachelor:
return self.can_earn_relationship(npc, hearts)
- if self.options[options.Friendsanity] == options.Friendsanity.option_starting_npcs and not villager.available:
+ if friendsanity == options.Friendsanity.option_starting_npcs and not villager.available:
return self.can_earn_relationship(npc, hearts)
- if self.options[
- options.Friendsanity] != options.Friendsanity.option_all_with_marriage and villager.bachelor and hearts > 8:
+ is_capped_at_8 = villager.bachelor and friendsanity != options.Friendsanity.option_all_with_marriage
+ if is_capped_at_8 and hearts > 8:
return self.received_hearts(villager, 8) & self.can_earn_relationship(npc, hearts)
return self.received_hearts(villager, hearts)
@@ -1132,11 +1137,22 @@ def can_earn_relationship(self, npc: str, hearts: int = 0) -> StardewRule:
rule_if_birthday = self.has_season(villager.birthday) & self.has_any_universal_love() & self.has_lived_months(hearts // 2)
rule_if_not_birthday = self.has_lived_months(hearts)
earn_rule = self.can_meet(npc) & (rule_if_birthday | rule_if_not_birthday)
+ if villager.bachelor:
+ if hearts > 8:
+ earn_rule = earn_rule & self.can_date(npc)
+ if hearts > 10:
+ earn_rule = earn_rule & self.can_marry(npc)
else:
earn_rule = self.has_lived_months(min(hearts // 2, 8))
return previous_heart_rule & earn_rule
+ def can_date(self, npc: str) -> StardewRule:
+ return self.has_relationship(npc, 8) & self.has(Gift.bouquet)
+
+ def can_marry(self, npc: str) -> StardewRule:
+ return self.has_relationship(npc, 10) & self.has(Gift.mermaid_pendant)
+
def can_befriend_pet(self, hearts: int):
if hearts <= 0:
return True_()
@@ -1150,14 +1166,15 @@ def can_befriend_pet(self, hearts: int):
def can_complete_bundle(self, bundle_requirements: List[BundleItem], number_required: int) -> StardewRule:
item_rules = []
highest_quality_yet = 0
+ can_speak_junimo = self.can_reach_region(Region.wizard_tower)
for bundle_item in bundle_requirements:
if bundle_item.item.item_id == -1:
- return self.can_spend_money(bundle_item.amount)
+ return can_speak_junimo & self.can_spend_money(bundle_item.amount)
else:
item_rules.append(bundle_item.item.name)
if bundle_item.quality > highest_quality_yet:
highest_quality_yet = bundle_item.quality
- return self.has(item_rules, number_required) & self.can_grow_gold_quality(highest_quality_yet)
+ return can_speak_junimo & self.has(item_rules, number_required) & self.can_grow_gold_quality(highest_quality_yet)
def can_grow_gold_quality(self, quality: int) -> StardewRule:
if quality <= 0:
@@ -1605,3 +1622,9 @@ def has_all_rarecrows(self) -> StardewRule:
rules.append(self.received(f"Rarecrow #{rarecrow_number}"))
return And(rules)
+ def can_ship(self, item: str = "") -> StardewRule:
+ shipping_bin_rule = self.has_building(Building.shipping_bin)
+ if item == "":
+ return shipping_bin_rule
+ return shipping_bin_rule & self.has(item)
+
diff --git a/worlds/stardew_valley/test/TestGeneration.py b/worlds/stardew_valley/test/TestGeneration.py
index a80f334d45d3..0142ad007908 100644
--- a/worlds/stardew_valley/test/TestGeneration.py
+++ b/worlds/stardew_valley/test/TestGeneration.py
@@ -214,7 +214,7 @@ def test_minimal_location_maximal_items_still_valid(self):
self.assertGreaterEqual(len(valid_locations), len(multiworld.itempool))
def test_allsanity_without_mods_has_at_least_locations(self):
- expected_locations = 993
+ expected_locations = 994
allsanity_options = self.allsanity_options_without_mods()
multiworld = setup_solo_multiworld(allsanity_options)
number_locations = len(get_real_locations(self, multiworld))
@@ -227,7 +227,7 @@ def test_allsanity_without_mods_has_at_least_locations(self):
f"\n\t\tActual: {number_locations}")
def test_allsanity_with_mods_has_at_least_locations(self):
- expected_locations = 1245
+ expected_locations = 1246
allsanity_options = self.allsanity_options_with_mods()
multiworld = setup_solo_multiworld(allsanity_options)
number_locations = len(get_real_locations(self, multiworld))
diff --git a/worlds/stardew_valley/test/TestRules.py b/worlds/stardew_valley/test/TestRules.py
index 8556dac1d89a..0847d8a63b95 100644
--- a/worlds/stardew_valley/test/TestRules.py
+++ b/worlds/stardew_valley/test/TestRules.py
@@ -444,3 +444,63 @@ def collect_all_except(multiworld, item_to_not_collect: str):
for item in multiworld.get_items():
if item.name != item_to_not_collect:
multiworld.state.collect(item)
+
+
+class TestFriendsanityDatingRules(SVTestBase):
+ options = {
+ options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized_not_winter,
+ options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage,
+ options.FriendsanityHeartSize.internal_name: 3
+ }
+
+ def test_earning_dating_heart_requires_dating(self):
+ month_name = "Month End"
+ for i in range(12):
+ month_item = self.world.create_item(month_name)
+ self.multiworld.state.collect(month_item, event=True)
+ self.multiworld.state.collect(self.world.create_item("Beach Bridge"), event=False)
+ self.multiworld.state.collect(self.world.create_item("Progressive House"), event=False)
+ self.multiworld.state.collect(self.world.create_item("Adventurer's Guild"), event=False)
+ self.multiworld.state.collect(self.world.create_item("Galaxy Hammer"), event=False)
+ for i in range(3):
+ self.multiworld.state.collect(self.world.create_item("Progressive Pickaxe"), event=False)
+ self.multiworld.state.collect(self.world.create_item("Progressive Axe"), event=False)
+ self.multiworld.state.collect(self.world.create_item("Progressive Barn"), event=False)
+ for i in range(10):
+ self.multiworld.state.collect(self.world.create_item("Foraging Level"), event=False)
+ self.multiworld.state.collect(self.world.create_item("Farming Level"), event=False)
+ self.multiworld.state.collect(self.world.create_item("Mining Level"), event=False)
+ self.multiworld.state.collect(self.world.create_item("Combat Level"), event=False)
+ self.multiworld.state.collect(self.world.create_item("Progressive Mine Elevator"), event=False)
+ self.multiworld.state.collect(self.world.create_item("Progressive Mine Elevator"), event=False)
+
+ npc = "Abigail"
+ heart_name = f"{npc} <3"
+ step = 3
+
+ self.assert_can_reach_heart_up_to(npc, 3, step)
+ self.multiworld.state.collect(self.world.create_item(heart_name), event=False)
+ self.assert_can_reach_heart_up_to(npc, 6, step)
+ self.multiworld.state.collect(self.world.create_item(heart_name), event=False)
+ self.assert_can_reach_heart_up_to(npc, 8, step)
+ self.multiworld.state.collect(self.world.create_item(heart_name), event=False)
+ self.assert_can_reach_heart_up_to(npc, 10, step)
+ self.multiworld.state.collect(self.world.create_item(heart_name), event=False)
+ self.assert_can_reach_heart_up_to(npc, 14, step)
+
+ def assert_can_reach_heart_up_to(self, npc: str, max_reachable: int, step: int):
+ prefix = "Friendsanity: "
+ suffix = " <3"
+ for i in range(1, max_reachable + 1):
+ if i % step != 0 and i != 14:
+ continue
+ location = f"{prefix}{npc} {i}{suffix}"
+ can_reach = self.world.logic.can_reach_location(location)(self.multiworld.state)
+ self.assertTrue(can_reach, f"Should be able to earn relationship up to {i} hearts")
+ for i in range(max_reachable + 1, 14 + 1):
+ if i % step != 0 and i != 14:
+ continue
+ location = f"{prefix}{npc} {i}{suffix}"
+ can_reach = self.world.logic.can_reach_location(location)(self.multiworld.state)
+ self.assertFalse(can_reach, f"Should not be able to earn relationship up to {i} hearts")
+
diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py
index 1684260cc1a0..2d4cf2faf602 100644
--- a/worlds/subnautica/__init__.py
+++ b/worlds/subnautica/__init__.py
@@ -116,15 +116,16 @@ def create_items(self):
# list of high-count important fragments as priority filler
[
"Cyclops Engine Fragment",
- "Modification Station Fragment",
- "Mobile Vehicle Bay Fragment",
- "Seamoth Fragment",
"Cyclops Hull Fragment",
"Cyclops Bridge Fragment",
+ "Seamoth Fragment",
"Prawn Suit Fragment",
+ "Mobile Vehicle Bay Fragment",
+ "Modification Station Fragment",
"Moonpool Fragment",
+ "Laser Cutter Fragment",
],
- k=min(extras, 8)):
+ k=min(extras, 9)):
item = self.create_item(item_name)
pool.append(item)
extras -= 1
diff --git a/worlds/subnautica/docs/en_Subnautica.md b/worlds/subnautica/docs/en_Subnautica.md
index 9a112aa596ec..5e99208b5f44 100644
--- a/worlds/subnautica/docs/en_Subnautica.md
+++ b/worlds/subnautica/docs/en_Subnautica.md
@@ -12,7 +12,7 @@ awarded from scanning those items have been shuffled into location checks throug
## What is the goal of Subnautica when randomized?
-The goal remains unchanged. Cure the plague, build the Neptune Escape Rocket, and escape into space.
+There are four goals currently available. The Launch goal has you leave the planet. The Free goal has you cure the plague. Infected is achieved at maximum infection level. Drive asks you to repair the Aurora Drive Core.
## What items and locations get shuffled?
@@ -34,5 +34,5 @@ player's world.
## When the player receives a technology, what happens?
-When the player receives a technology, the chat log displays a notification the technology has been received.
+When the player receives a technology, the chat log displays a notification that the technology has been received.
diff --git a/worlds/terraria/__init__.py b/worlds/terraria/__init__.py
index a56f47608b87..306a65ef9186 100644
--- a/worlds/terraria/__init__.py
+++ b/worlds/terraria/__init__.py
@@ -338,5 +338,6 @@ def check(state: CollectionState, location=location):
def fill_slot_data(self) -> Dict[str, object]:
return {
"goal": list(self.goal_locations),
+ "achievements": self.multiworld.achievements[self.player].value,
"deathlink": bool(self.multiworld.death_link[self.player]),
}
diff --git a/worlds/terraria/docs/setup_en.md b/worlds/terraria/docs/setup_en.md
index d0833b748452..84744a4a337c 100644
--- a/worlds/terraria/docs/setup_en.md
+++ b/worlds/terraria/docs/setup_en.md
@@ -3,11 +3,23 @@
## Required Software
Download and install [Terraria](https://store.steampowered.com/app/105600/Terraria/)
-and [TModLoader](https://store.steampowered.com/app/1281930/tModLoader/) on Steam
+and [tModLoader](https://store.steampowered.com/app/1281930/tModLoader/) on Steam
## Installing the Archipelago Mod
-Subscribe to [the mod](https://steamcommunity.com/sharedfiles/filedetails/?id=2922217554) on Steam.
+1. Subscribe to [the mod](https://steamcommunity.com/sharedfiles/filedetails/?id=2922217554) on Steam
+2. Open tModLoader
+3. Go to **Workshop -> Manage Mods** and enable the Archipelago mod
+ - If tModLoader states that you need version 1.4.3, follow the following steps
+ 1. Close tModLoader
+ 2. Right-Click tModLoader in Steam and select **Properties**
+ 3. Navigate to **Betas -> Beta Participation**
+ 4. Select **1.4.3-legacy - Legacy - Stable tModLoader for Terraria 1.4.3**
+ 5. Update tModLoader through Steam
+ 6. Open tModLoader and navigate back to the **Manage Mods** menu
+4. tModLoader will say that it needs to refresh; exit this menu, and it will do this automatically
+5. Once tModLoader finishes loading, the Archipelago mod is finished installing; you can now
+[connect to an Archipelago game](#joining-an-archipelago-game-in-terraria).
This mod might not work with mods that significantly alter progression or vanilla features. It is
highly recommended to use utility mods and features to speed up gameplay, such as:
@@ -16,7 +28,7 @@ highly recommended to use utility mods and features to speed up gameplay, such a
- Ore Excavator
- Magic Storage
- Alchemist NPC Lite
- - (May be used to break progression)
+ - (Can be used to break progression)
- Reduced Grinding
- Upgraded Research
@@ -24,8 +36,8 @@ highly recommended to use utility mods and features to speed up gameplay, such a
### What is a YAML and why do I need one?
-You can see the [basic multiworld setup guide](/tutorial/Archipelago/setup/en) here
-on the Archipelago website to learn about why Archipelago uses YAML files and what they're for.
+The [basic multiworld setup guide](/tutorial/Archipelago/setup/en) can be found on Archipelago's website. Among other things, it explains what .yaml
+files are, and how they are used.
### Where do I get a YAML?
@@ -34,17 +46,15 @@ on the Archipelago website to generate a YAML using a graphical interface.
## Joining an Archipelago Game in Terraria
-1. Launch TModLoader
-2. In Workshop > Manage Mods, edit Archipelago Randomizer's settings
- - "Name" should be the player name you set when creating your YAML file
- - "Port" should be the port number associated with the Archipelago server. It will be a 4 or 5
- digit number.
- - If you're not hosting your game on the Archipelago website, change "Address" to the server's
- URL or IP address
-3. Create a new character and world as normal (or use an existing one if you prefer). Terraria is
-usually significantly more difficult with this mod, so it is recommended to choose a lower
-difficulty than you normally would.
-4. Open the world in single player or multiplayer
+1. Launch tModLoader
+2. In **Workshop > Manage Mods**, edit Archipelago Randomizer's settings
+ - **Name** should be the player name you set when creating your YAML file.
+ - **Port** should be the port number associated with the Archipelago server. It will be a 4 or 5-digit number.
+ - If you're not hosting your game on the Archipelago website, change **Address** to the server's URL or IP address
+3. Create a new character and world as normal (or use an existing one if you prefer). Terraria usually becomes
+significantly more difficult with this mod, so it is recommended to choose a lower difficulty than you normally would
+play on.
+4. Open the world in single player or multiplayer.
5. When you're ready, open chat, and enter `/apstart` to start the game.
## Commands
diff --git a/worlds/timespinner/Options.py b/worlds/timespinner/Options.py
index 5f4d230688bd..8b111849442c 100644
--- a/worlds/timespinner/Options.py
+++ b/worlds/timespinner/Options.py
@@ -60,7 +60,7 @@ class BossRando(Toggle):
class BossScaling(DefaultOnToggle):
- "When Boss Rando is enabled, scales the bosses' HP, XP, and ATK to the stats of the location they replace (Reccomended)"
+ "When Boss Rando is enabled, scales the bosses' HP, XP, and ATK to the stats of the location they replace (Recommended)"
display_name = "Scale Random Boss Stats"
diff --git a/worlds/undertale/Rules.py b/worlds/undertale/Rules.py
index 02c21f53f73c..648152c50414 100644
--- a/worlds/undertale/Rules.py
+++ b/worlds/undertale/Rules.py
@@ -113,7 +113,7 @@ def set_rules(multiworld: MultiWorld, player: int):
set_rule(multiworld.get_location("Hush Trade", player),
lambda state: state.can_reach("News Show", "Region", player) and state.has("Hot Dog...?", player, 1))
set_rule(multiworld.get_location("Letter Quest", player),
- lambda state: state.can_reach("New Home Exit", "Entrance", player))
+ lambda state: state.can_reach("New Home Exit", "Entrance", player) and state.has("Undyne Date", player))
if (not _undertale_is_route(multiworld.state, player, 2)) or _undertale_is_route(multiworld.state, player, 3):
set_rule(multiworld.get_location("Nicecream Punch Card", player),
lambda state: state.has("Punch Card", player, 3) and state.can_reach("Waterfall", "Region", player))
diff --git a/worlds/undertale/__init__.py b/worlds/undertale/__init__.py
index 3a34a162c478..5e3634470394 100644
--- a/worlds/undertale/__init__.py
+++ b/worlds/undertale/__init__.py
@@ -33,8 +33,8 @@ class UndertaleWeb(WebWorld):
"A guide to setting up the Archipelago Undertale software on your computer. This guide covers "
"single-player, multiworld, and related software.",
"English",
- "undertale_en.md",
- "undertale/en",
+ "setup_en.md",
+ "setup/en",
["Mewlif"]
)]
diff --git a/worlds/undertale/data/patch.bsdiff b/worlds/undertale/data/patch.bsdiff
index 8d7dcbf43a96..5d137537be83 100644
Binary files a/worlds/undertale/data/patch.bsdiff and b/worlds/undertale/data/patch.bsdiff differ
diff --git a/worlds/undertale/docs/en_Undertale.md b/worlds/undertale/docs/en_Undertale.md
index 79ca21681e58..3905d3bc3ead 100644
--- a/worlds/undertale/docs/en_Undertale.md
+++ b/worlds/undertale/docs/en_Undertale.md
@@ -24,24 +24,29 @@ every major route in the game, those being `Pacifist`, `Neutral`, and `Genocide`
There are some major differences between vanilla and the randomizer.
-There are now doors to every major area in the underground located in the flower room (The first room of the game), those being Ruins, Snowdin, Waterfall, Hotland, and Core.
+There are now doors to every major area in the underground located in the flower room (the first room of the game.)
+These doors lead to Ruins, Snowdin, Waterfall, Hotland, and Core from left to right.
Each door needs their respective key from the pool to enter.
-You start with one key for a random door. (Core will never be given to start with.)
-The rest of the keys will be in the item pool.
+You start with one key for a random door and the rest of the keys will be in the item pool to be found by other players.
+(Core will never be given to start with, unless otherwise specified.)
-Genocide works a little differently in terms of the requirements.
-You now only need to get through Core and fight Mettaton NEO, and then beat Sans, to win.
-If you choose to fight other major bosses, you will still need to grind out the area before fighting them like normal.
-Pacifist is mostly the same, except you are not required to go to the Ruins to spare Toriel,
-you only need to spare Papyrus, Undyne, and Mettaton EX. Although you still cannot kill anyone.
-You are also still required to do the date/hangout with Papyrus, the hangout with Undyne, and the date with Alphys,
-in that order, before entering the True Lab.
+**Genocide** works a little differently in terms of the requirements.
-You now require custom items to Hangout with Papyrus, Undyne, to enter the True Lab, and to fight Mettaton EX/NEO.
-Those being `Complete Skeleton`, `Fish`, `DT Extractor`, and `Mettaton Plush`.
+In order to win with the genocide route, you only need to get through Core, fight Mettaton NEO, and beat Sans to win.
+If you choose to fight other major bosses, you will still need to progress the area like normal before fighting them.
-The Riverperson will only take you to locations you have actually seen the Riverperson at.
-Meaning they will only take you to, for example, Waterfall, if you have seen the Riverperson at Waterfall at least once.
+**Pacifist** remains mostly the same.
+
+In the Pacifist run, you are not required to go to the Ruins to spare Toriel. The only necessary spares are Papyrus,
+Undyne, and Mettaton EX. Just as it is in the vanilla game, you cannot kill anyone. You are also required to complete
+the date/hangout with Papyrus, Undyne, and Alphys, in that order, before entering the True Lab.
+
+Additionally, custom items are required to hang out with Papyrus, Undyne, to enter the True Lab, and to fight
+Mettaton EX/NEO. The respective items for each interaction are `Complete Skeleton`, `Fish`, `DT Extractor`,
+and `Mettaton Plush`.
+
+The Riverperson will only take you to locations you have seen them at, meaning they will only take you to
+Waterfall if you have seen them at Waterfall at least once.
If you press `W` while in the save menu, you will teleport back to the flower room, for quick access to the other areas.
\ No newline at end of file
diff --git a/worlds/undertale/docs/setup_en.md b/worlds/undertale/docs/setup_en.md
new file mode 100644
index 000000000000..f82105c26916
--- /dev/null
+++ b/worlds/undertale/docs/setup_en.md
@@ -0,0 +1,65 @@
+# Undertale Randomizer Setup Guide
+
+### Required Software
+
+- Undertale from the [Steam page](https://store.steampowered.com/app/391540)
+- Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases)
+ - (select `Undertale Client` during installation.)
+
+### First time setup
+
+Start the Undertale client from your Archipelago folder and input `/auto_patch ` at the bottom.
+
+This directory is usually located at `C:\Program Files\Steam\steamapps\Undertale`, but it can be different depending on
+your installation. You can easily find the directory by opening the Undertale directory through Steam by right-clicking
+Undertale in your library and selecting `Manage -> Browse local files`. Then, on Windows you can see the directory that
+you need at the top of the window that opens.
+
+After using the `/auto_patch` command, **Archipelago will make an Undertale folder within the Archipelago install
+location.** That folder contains the version of Undertale you will use for Archipelago. (If you update Archipelago,
+you will need to redo this set-up.)
+
+**Linux Users**: The Linux installation is mostly similar, however, Undertale will be installed on Steam as the Linux
+variant. Since this randomizer only supports the Windows version, we must fix this, by right-click the game in Steam,
+going to `Properties -> Compatibility`, and checking `Force the use of a specific Steam Play compatibility tool`. This
+downloads the Windows version of Undertale to use instead of the Linux version. If the play button is greyed out in
+Steam, be sure to go to `Settings -> Compatibility` and toggle `Enable Steam Play for all other titles`.
+
+### Connect to the MultiServer
+
+Make sure both Undertale **from the Archipelago folder** and its client are running. (Undertale will ask for a save slot
+to play on. Archipelago Undertale does not overwrite vanilla saves, but you may want to back up your save as a precaution.)
+
+In the top text box of the client, type the `IP Address` (or `Hostname`) and `Port` separated with a `:` symbol.
+(Ex. `archipelago.gg:38281`)
+
+The client will then ask for the slot name, input your slot name chosen during YAML creation in the text box at the
+bottom of the client.
+
+**Linux Users**: When you start the client, it is likely that the save data path is incorrect, and how the game
+is played depends on where the save data folder is located.
+
+**On Steam (via Proton)**: This assumes the game is in a Steam Library folder. Right-click Undertale, go to `Manage ->
+Browse Local Files`. Go up the directories to the `steamapps` folder, open `compatdata/391540` (391540 is the "magic number" for
+Undertale in Steam). Save data from here is at `/pfx/drive_c/users/steamuser/AppData/Local/UNDERTALE`.
+
+**Through WINE directly**: This depends on the prefix used. If it is default, then the save data is located at
+`/home/USERNAME/.wine/drive_c/users/USERNAME/AppData/Local/UNDERTALE`.
+
+Once the save data folder is located, run the `/savepath` command to redirect the client to the correct save data folder
+before connecting.
+
+### Play the game
+
+When the console tells you that you have joined the room, you're all set. Congratulations on successfully joining a
+multi-world game!
+
+### PLEASE READ!
+
+Please read this page in its entirety before asking questions! Most importantly, there is a list of
+gameplay differences at the bottom.
+[Undertale Game Info Page](/games/Undertale/info/en)
+
+### Where do I get a YAML file?
+
+You can customize your settings by visiting the [Undertale Player Settings Page](/games/Undertale/player-settings)
diff --git a/worlds/undertale/docs/undertale_en.md b/worlds/undertale/docs/undertale_en.md
deleted file mode 100644
index a2f3d2579a94..000000000000
--- a/worlds/undertale/docs/undertale_en.md
+++ /dev/null
@@ -1,59 +0,0 @@
-# Undertale Randomizer Setup Guide
-
-### Required Software
-
-- Undertale from the [Steam page](https://store.steampowered.com/app/391540)
-- Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases)
- - (select `Undertale Client` during installation.)
-
-### First time setup
-
-Start the Undertale client, and in the bottom text box, input `/auto_patch (Input your Undertale install directory here)` (It is usually located at `C:\Program Files\Steam\steamapps\Undertale`, but it can be different, you can more easily find the directory
-by opening the Undertale directory through Steam), it will then make an Undertale folder that will be created in the
-Archipelago install location. That contains the version of Undertale you will use for Archipelago. (You will need to
-redo this step when updating Archipelago.)
-
-**Linux Users**: This guide is mostly similar; however, when Undertale is installed on Steam, it defaults to a Linux
-supported variant; this randomizer only supports the Windows version. To fix this, right-click the game in Steam, go to
-Properties -> Compatibility, and check "Force the use of a specific Steam Play compatibility tool". This
-downloads the Windows version instead. If the play button is greyed out in Steam, be sure to go to
-Settings -> Compatibility and toggle "Enable Steam Play for all other titles".
-
-### Connect to the MultiServer
-
-Make sure both Undertale and its client are running. (Undertale will ask for a saveslot, it can be 1 through 99, none
-of the slots will overwrite your vanilla save, although you may want to make a backup just in case.)
-
-In the top text box of the client, type the
-`Ip Address` (or `Hostname`) and `Port` separated with a `:` symbol. (Ex. `archipelago.gg:38281`)
-
-The client will then ask for the slot name, input that in the text box at the bottom of the client.
-
-**Linux Users**: When you start the client, it is likely that the save data path is incorrect, and how the game
-is played depends on where the save data folder is located.
-
-*On Steam (via Proton)*: This assumes the game is in a Steam Library folder. Right-click Undertale, go to Manage ->
-Browse Local Files. Move back to the steamapps folder, open compatdata/391540 (391540 is the "magic number" for
-Undertale in Steam and can be confirmed by visiting its store page and looking at the URL). Save data from here is at
-/pfx/drive_c/users/steamuser/AppData/Local/UNDERTALE.
-
-*Through WINE directly*: This depends on the prefix used. If it is default, then the save data is located at
-/home/USERNAME/.wine/drive_c/users/USERNAME/AppData/Local/UNDERTALE.
-
-Once the save data folder is located, run the /savepath command to redirect the client to the correct save data folder
-before connecting.
-
-### Play the game
-
-When the console tells you that you have joined the room, you're all set. Congratulations on successfully joining a
-multiworld game!
-
-### PLEASE READ!
-
-Please read this page in its entirety before asking questions! Most importantly, there is a list of
-gameplay differences at the bottom.
-[Undertale Game Info Page](/games/Undertale/info/en)
-
-### Where do I get a YAML file?
-
-You can customize your settings by visiting the [Undertale Player Settings Page](/games/Undertale/player-settings)
diff --git a/worlds/witness/WitnessLogicExpert.txt b/worlds/witness/WitnessLogicExpert.txt
index a55c22441e28..581167cc450d 100644
--- a/worlds/witness/WitnessLogicExpert.txt
+++ b/worlds/witness/WitnessLogicExpert.txt
@@ -14,7 +14,7 @@ Tutorial (Tutorial) - Outside Tutorial - True:
158005 - 0x0A3B5 (Back Left) - True - Dots & Full Dots
158006 - 0x0A3B2 (Back Right) - True - Dots & Full Dots
158007 - 0x03629 (Gate Open) - 0x002C2 - Symmetry & Dots
-158008 - 0x03505 (Gate Close) - 0x2FAF6 & 0x03629 - False
+158008 - 0x03505 (Gate Close) - 0x2FAF6 & 0x03629 - True
158009 - 0x0C335 (Pillar) - True - Triangles
158010 - 0x0C373 (Patio Floor) - 0x0C335 - Dots
159512 - 0x33530 (Cloud EP) - True - True
@@ -703,7 +703,7 @@ Swamp Between Bridges Far (Swamp) - Swamp Red Underwater - 0x183F2 - Swamp Rotat
158322 - 0x0000A (Between Bridges Far Row 4) - 0x00009 - Rotated Shapers & Shapers & Dots & Full Dots
Door - 0x183F2 (Red Water Pump) - 0x00596
-Swamp Red Underwater (Swamp) - Swamp Maze - 0x014D1:
+Swamp Red Underwater (Swamp) - Swamp Maze - 0x305D5:
158323 - 0x00001 (Red Underwater 1) - True - Shapers & Negative Shapers & Dots & Full Dots
158324 - 0x014D2 (Red Underwater 2) - True - Shapers & Negative Shapers & Dots & Full Dots
158325 - 0x014D4 (Red Underwater 3) - True - Shapers & Negative Shapers & Dots & Full Dots
diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py
index f05202751fdb..faaafd598b51 100644
--- a/worlds/witness/__init__.py
+++ b/worlds/witness/__init__.py
@@ -248,6 +248,9 @@ def create_item(self, item_name: str) -> Item:
return WitnessItem(item_name, item_data.classification, item_data.ap_code, player=self.player)
+ def get_filler_item_name(self) -> str:
+ return "Speed Boost"
+
class WitnessLocation(Location):
"""
diff --git a/worlds/witness/items.py b/worlds/witness/items.py
index 7e083534c9c6..82c79047f3fb 100644
--- a/worlds/witness/items.py
+++ b/worlds/witness/items.py
@@ -152,7 +152,7 @@ def get_mandatory_items(self) -> Dict[str, int]:
"""
Returns the list of items that must be in the pool for the game to successfully generate.
"""
- return self._mandatory_items
+ return self._mandatory_items.copy()
def get_filler_items(self, quantity: int) -> Dict[str, int]:
"""
diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py
index 2ea7ffdea5cd..7c927c10eb92 100644
--- a/worlds/zillion/__init__.py
+++ b/worlds/zillion/__init__.py
@@ -10,6 +10,7 @@
from BaseClasses import ItemClassification, LocationProgressType, \
MultiWorld, Item, CollectionState, Entrance, Tutorial
+from .config import detect_test
from .logic import cs_to_zz_locs
from .region import ZillionLocation, ZillionRegion
from .options import ZillionStartChar, zillion_options, validate
@@ -145,8 +146,7 @@ def generate_early(self) -> None:
self._item_counts = item_counts
- import __main__
- rom_dir_name = "" if "test" in __main__.__file__ else os.path.dirname(get_base_rom_path())
+ rom_dir_name = "" if detect_test() else os.path.dirname(get_base_rom_path())
with redirect_stdout(self.lsi): # type: ignore
self.zz_system.make_patcher(rom_dir_name)
self.zz_system.make_randomizer(zz_op)
diff --git a/worlds/zillion/config.py b/worlds/zillion/config.py
index ca02f9a99f41..db61d0c45347 100644
--- a/worlds/zillion/config.py
+++ b/worlds/zillion/config.py
@@ -2,3 +2,20 @@
base_id = 8675309
zillion_map = os.path.join(os.path.dirname(__file__), "empty-zillion-map-row-col-labels-281.png")
+
+
+def detect_test() -> bool:
+ """
+ Parts of generation that are in unit tests need the rom.
+ This is to detect whether we are running unit tests
+ so we can work around the need for the rom.
+ """
+ import __main__
+ try:
+ if "test" in __main__.__file__:
+ return True
+ except AttributeError:
+ # In some environments, __main__ doesn't have __file__
+ # We'll assume that's not unit tests.
+ pass
+ return False
diff --git a/worlds/zillion/requirements.txt b/worlds/zillion/requirements.txt
index 626579ab1139..2af057decef9 100644
--- a/worlds/zillion/requirements.txt
+++ b/worlds/zillion/requirements.txt
@@ -1 +1 @@
-zilliandomizer @ git+https://github.com/beauxq/zilliandomizer@cd6a940ad7b585c75a560b91468d6b9eee030559#0.5.2
+zilliandomizer @ git+https://github.com/beauxq/zilliandomizer@4b27d115269db25fe73b0471b73495f41df1323c#0.5.3