SC2: Alternate final missions, new logic, fixes

This commit is contained in:
Magnemania
2022-10-16 09:25:13 -04:00
parent 1e5ecadf67
commit 23b1e973b0
8 changed files with 221 additions and 138 deletions

View File

@@ -57,8 +57,8 @@ item_table = {
"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"),
"Fire-Suppression System (Building)": ItemData(206 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 6, classification=ItemClassification.filler, parent_item="Building"),
"Orbital Command (Building)": ItemData(207 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 7, parent_item="Building"),
"Fire-Suppression System (Building)": ItemData(206 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 6, classification=ItemClassification.filler),
"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"),
"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.progression, parent_item="Medic"),
@@ -123,7 +123,7 @@ item_table = {
"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"),
"Planetary Fortress": ItemData(612 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 12, classification=ItemClassification.progression),
"Perdition Turret": ItemData(613 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 13),
"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, classification=ItemClassification.filler),
@@ -182,6 +182,22 @@ filler_items: typing.Tuple[str, ...] = (
'+15 Starting Vespene'
)
defense_ratings = {
"Siege Tank": 5,
"Maelstrom Rounds": 2,
"Planetary Fortress": 3,
# Bunker w/ Marine/Marauder: 3,
"Perdition Turret": 2,
"Missile Turret": 2,
"Vulture": 2
}
zerg_defense_ratings = {
"Perdition Turret": 2,
# Bunker w/ Firebat
"Hive Mind Emulator": 3,
"Psi Disruptor": 3
}
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

View File

@@ -34,26 +34,33 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
LocationData("The Outlaws", "The Outlaws: Rebel Base", SC2WOL_LOC_ID_OFFSET + 201,
lambda state: state._sc2wol_has_common_unit(world, player)),
LocationData("Zero Hour", "Zero Hour: Victory", SC2WOL_LOC_ID_OFFSET + 300,
lambda state: state._sc2wol_has_common_unit(world, player)),
lambda state: state._sc2wol_has_common_unit(world, player) and
state._sc2wol_defense_rating(world, player, True) >= 2 and
(logic_level > 0 or state._sc2wol_has_anti_air(world, 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,
lambda state: state._sc2wol_has_common_unit(world, player)),
LocationData("Zero Hour", "Zero Hour: Third Group Rescued", SC2WOL_LOC_ID_OFFSET + 303,
lambda state: state._sc2wol_has_common_unit(world, player)),
lambda state: state._sc2wol_has_common_unit(world, player) and
state._sc2wol_defense_rating(world, player, True) >= 2),
LocationData("Evacuation", "Evacuation: Victory", SC2WOL_LOC_ID_OFFSET + 400,
lambda state: state._sc2wol_has_common_unit(world, player) and
state._sc2wol_has_competent_anti_air(world, player)),
(logic_level > 0 and state._sc2wol_has_anti_air(world, player)
or state._sc2wol_has_competent_anti_air(world, player))),
LocationData("Evacuation", "Evacuation: First Chysalis", SC2WOL_LOC_ID_OFFSET + 401),
LocationData("Evacuation", "Evacuation: Second Chysalis", SC2WOL_LOC_ID_OFFSET + 402,
lambda state: state._sc2wol_has_common_unit(world, player)),
LocationData("Evacuation", "Evacuation: Third Chysalis", SC2WOL_LOC_ID_OFFSET + 403,
lambda state: state._sc2wol_has_common_unit(world, player)),
LocationData("Outbreak", "Outbreak: Victory", SC2WOL_LOC_ID_OFFSET + 500,
lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)),
lambda state: state._sc2wol_defense_rating(world, player, True) >= 4 and
(state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player))),
LocationData("Outbreak", "Outbreak: Left Infestor", SC2WOL_LOC_ID_OFFSET + 501,
lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)),
lambda state: state._sc2wol_defense_rating(world, player, True) >= 4 and
(state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player))),
LocationData("Outbreak", "Outbreak: Right Infestor", SC2WOL_LOC_ID_OFFSET + 502,
lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)),
lambda state: state._sc2wol_defense_rating(world, player, True) >= 4 and
(state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player))),
LocationData("Safe Haven", "Safe Haven: Victory", SC2WOL_LOC_ID_OFFSET + 600,
lambda state: state._sc2wol_has_common_unit(world, player) and
state._sc2wol_has_competent_anti_air(world, player)),
@@ -68,38 +75,48 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
state._sc2wol_has_competent_anti_air(world, player)),
LocationData("Haven's Fall", "Haven's Fall: Victory", SC2WOL_LOC_ID_OFFSET + 700,
lambda state: state._sc2wol_has_common_unit(world, player) and
state._sc2wol_has_competent_anti_air(world, player)),
state._sc2wol_has_competent_anti_air(world, player) and
state._sc2wol_defense_rating(world, player, True) >= 3),
LocationData("Haven's Fall", "Haven's Fall: North Hive", SC2WOL_LOC_ID_OFFSET + 701,
lambda state: state._sc2wol_has_common_unit(world, player) and
state._sc2wol_has_competent_anti_air(world, player)),
state._sc2wol_has_competent_anti_air(world, player) and
state._sc2wol_defense_rating(world, player, True) >= 3),
LocationData("Haven's Fall", "Haven's Fall: East Hive", SC2WOL_LOC_ID_OFFSET + 702,
lambda state: state._sc2wol_has_common_unit(world, player) and
state._sc2wol_has_competent_anti_air(world, player)),
state._sc2wol_has_competent_anti_air(world, player) and
state._sc2wol_defense_rating(world, player, True) >= 3),
LocationData("Haven's Fall", "Haven's Fall: South Hive", SC2WOL_LOC_ID_OFFSET + 703,
lambda state: state._sc2wol_has_common_unit(world, player) and
state._sc2wol_has_competent_anti_air(world, player)),
state._sc2wol_has_competent_anti_air(world, player) and
state._sc2wol_defense_rating(world, player, True) >= 3),
LocationData("Smash and Grab", "Smash and Grab: Victory", SC2WOL_LOC_ID_OFFSET + 800,
lambda state: state._sc2wol_has_common_unit(world, player) and
state._sc2wol_has_competent_anti_air(world, player)),
(logic_level > 0 and state._sc2wol_has_anti_air(world, player)
or state._sc2wol_has_competent_anti_air(world, 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,
lambda state: state._sc2wol_has_common_unit(world, player)),
lambda state: state._sc2wol_has_common_unit(world, player) and
(logic_level > 0 and state._sc2wol_has_anti_air(world, player)
or state._sc2wol_has_competent_anti_air(world, player))),
LocationData("Smash and Grab", "Smash and Grab: Fourth Relic", SC2WOL_LOC_ID_OFFSET + 804,
lambda state: state._sc2wol_has_common_unit(world, player) and
state._sc2wol_has_anti_air(world, player)),
(logic_level > 0 and state._sc2wol_has_anti_air(world, player)
or state._sc2wol_has_competent_anti_air(world, player))),
LocationData("The Dig", "The Dig: Victory", SC2WOL_LOC_ID_OFFSET + 900,
lambda state: state._sc2wol_has_common_unit(world, player) and
state._sc2wol_has_anti_air(world, player) and
state._sc2wol_has_heavy_defense(world, player)),
lambda state: state._sc2wol_has_anti_air(world, player) and
state._sc2wol_defense_rating(world, player, False) >= 7),
LocationData("The Dig", "The Dig: Left Relic", SC2WOL_LOC_ID_OFFSET + 901,
lambda state: state._sc2wol_has_common_unit(world, player)),
lambda state: state._sc2wol_defense_rating(world, player, False) >= 5),
LocationData("The Dig", "The Dig: Right Ground Relic", SC2WOL_LOC_ID_OFFSET + 902,
lambda state: state._sc2wol_has_common_unit(world, player)),
lambda state: state._sc2wol_defense_rating(world, player, False) >= 5),
LocationData("The Dig", "The Dig: Right Cliff Relic", SC2WOL_LOC_ID_OFFSET + 903,
lambda state: state._sc2wol_has_common_unit(world, player)),
lambda state: state._sc2wol_defense_rating(world, player, False) >= 5),
LocationData("The Moebius Factor", "The Moebius Factor: Victory", SC2WOL_LOC_ID_OFFSET + 1000,
lambda state: state._sc2wol_has_air(world, player) and state._sc2wol_has_anti_air(world, player)),
lambda state: state._sc2wol_has_anti_air(world, player) and
(state._sc2wol_has_air(world, player)
or state.has_any({'Medivac', 'Hercules'}, player)
and state._sc2wol_has_common_unit(world, player))),
LocationData("The Moebius Factor", "The Moebius Factor: South Rescue", SC2WOL_LOC_ID_OFFSET + 1003,
lambda state: state._sc2wol_able_to_rescue(world, player)),
LocationData("The Moebius Factor", "The Moebius Factor: Wall Rescue", SC2WOL_LOC_ID_OFFSET + 1004,
@@ -111,7 +128,10 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
LocationData("The Moebius Factor", "The Moebius Factor: Alive Inside Rescue", SC2WOL_LOC_ID_OFFSET + 1007,
lambda state: state._sc2wol_able_to_rescue(world, player)),
LocationData("The Moebius Factor", "The Moebius Factor: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1008,
lambda state: state._sc2wol_has_air(world, player)),
lambda state: state._sc2wol_has_anti_air(world, player) and
(state._sc2wol_has_air(world, player)
or state.has_any({'Medivac', 'Hercules'}, player)
and state._sc2wol_has_common_unit(world, player))),
LocationData("Supernova", "Supernova: Victory", SC2WOL_LOC_ID_OFFSET + 1100,
lambda state: state._sc2wol_beats_protoss_deathball(world, player)),
LocationData("Supernova", "Supernova: West Relic", SC2WOL_LOC_ID_OFFSET + 1101),
@@ -124,19 +144,20 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
lambda state: state._sc2wol_survives_rip_field(world, 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,
lambda state: state._sc2wol_survives_rip_field(world, player)),
lambda state: logic_level > 0 or state._sc2wol_survives_rip_field(world, player)),
LocationData("Maw of the Void", "Maw of the Void: South Close Prisoners", SC2WOL_LOC_ID_OFFSET + 1203,
lambda state: state._sc2wol_survives_rip_field(world, player)),
lambda state: logic_level > 0 or state._sc2wol_survives_rip_field(world, player)),
LocationData("Maw of the Void", "Maw of the Void: South Far Prisoners", SC2WOL_LOC_ID_OFFSET + 1204,
lambda state: state._sc2wol_survives_rip_field(world, player)),
LocationData("Maw of the Void", "Maw of the Void: North Prisoners", SC2WOL_LOC_ID_OFFSET + 1205,
lambda state: state._sc2wol_survives_rip_field(world, player)),
LocationData("Devil's Playground", "Devil's Playground: Victory", SC2WOL_LOC_ID_OFFSET + 1300,
lambda state: state._sc2wol_has_anti_air(world, player) and (
state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player))),
lambda state: logic_level > 0 or
state._sc2wol_has_anti_air(world, player) and (
state._sc2wol_has_common_unit(world, 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,
lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)),
lambda state: logic_level > 0 or state._sc2wol_has_common_unit(world, 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(world, player) and
state._sc2wol_has_competent_anti_air(world, player)),
@@ -163,7 +184,8 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
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,
lambda state: state._sc2wol_has_common_unit(world, player)),
lambda state: state._sc2wol_has_common_unit(world, player) and
(logic_level > 0 or state._sc2wol_has_anti_air)),
LocationData("Cutthroat", "Cutthroat: Mira Han", SC2WOL_LOC_ID_OFFSET + 1801,
lambda state: state._sc2wol_has_common_unit(world, player)),
LocationData("Cutthroat", "Cutthroat: North Relic", SC2WOL_LOC_ID_OFFSET + 1802,
@@ -211,24 +233,28 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
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,
lambda state: state._sc2wol_has_protoss_medium_units(world, player)),
LocationData("A Sinister Turn", "A Sinister Turn: Robotics Facility", SC2WOL_LOC_ID_OFFSET + 2301),
LocationData("A Sinister Turn", "A Sinister Turn: Dark Shrine", SC2WOL_LOC_ID_OFFSET + 2302),
LocationData("A Sinister Turn", "A Sinister Turn: Robotics Facility", SC2WOL_LOC_ID_OFFSET + 2301,
lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(world, player)),
LocationData("A Sinister Turn", "A Sinister Turn: Dark Shrine", SC2WOL_LOC_ID_OFFSET + 2302,
lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(world, player)),
LocationData("A Sinister Turn", "A Sinister Turn: Templar Archives", SC2WOL_LOC_ID_OFFSET + 2303,
lambda state: state._sc2wol_has_protoss_common_units(world, player)),
LocationData("Echoes of the Future", "Echoes of the Future: Victory", SC2WOL_LOC_ID_OFFSET + 2400,
lambda state: state._sc2wol_has_protoss_medium_units(world, player)),
lambda state: logic_level > 0 or state._sc2wol_has_protoss_medium_units(world, 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,
lambda state: state._sc2wol_has_protoss_common_units(world, player)),
lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(world, 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,
lambda state: state._sc2wol_has_protoss_medium_units(world, player)),
LocationData("In Utter Darkness", "In Utter Darkness: Kills", SC2WOL_LOC_ID_OFFSET + 2502,
lambda state: state._sc2wol_has_protoss_common_units(world, player)),
LocationData("Gates of Hell", "Gates of Hell: Victory", SC2WOL_LOC_ID_OFFSET + 2600,
lambda state: state._sc2wol_has_competent_comp(world, player)),
lambda state: state._sc2wol_has_competent_comp(world, player) and
state._sc2wol_defense_rating(world, player, True) > 5),
LocationData("Gates of Hell", "Gates of Hell: Large Army", SC2WOL_LOC_ID_OFFSET + 2601,
lambda state: state._sc2wol_has_competent_comp(world, player)),
lambda state: state._sc2wol_has_competent_comp(world, player) and
state._sc2wol_defense_rating(world, player, True) > 5),
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),

View File

@@ -1,34 +1,41 @@
from BaseClasses import MultiWorld
from worlds.AutoWorld import LogicMixin
from .Options import get_option_value
from .Items import get_basic_units
from .Items import get_basic_units, defense_ratings, zerg_defense_ratings
class SC2WoLLogic(LogicMixin):
def _sc2wol_has_common_unit(self, world: MultiWorld, player: int) -> bool:
return self.has_any(get_basic_units(world, player), player)
def _sc2wol_has_manned_bunkers(self, world: MultiWorld, player: int) -> bool:
return self.has_any({'Marine', 'Marauder'}, player) and self.has('Bunker', player)
def _sc2wol_has_air(self, world: MultiWorld, player: int) -> bool:
return self.has_any({'Viking', 'Wraith', 'Banshee'}, player) or \
self.has_any({'Hercules', 'Medivac'}, player) and self._sc2wol_has_common_unit(world, player)
return self.has_any({'Viking', 'Wraith', 'Banshee'}, player) or get_option_value(world, player, 'required_tactics') > 0 \
and self.has_any({'Hercules', 'Medivac'}, player) and self._sc2wol_has_common_unit(world, player)
def _sc2wol_has_air_anti_air(self, world: MultiWorld, player: int) -> bool:
return self.has_any({'Viking', 'Wraith'}, player)
return self.has('Viking', player) \
or get_option_value(world, player, 'required_tactics') > 0 and self.has('Wraith', player)
def _sc2wol_has_competent_anti_air(self, world: MultiWorld, player: int) -> bool:
return self.has_any({'Marine', 'Goliath'}, player) or self._sc2wol_has_air_anti_air(world, player)
def _sc2wol_has_anti_air(self, world: MultiWorld, player: int) -> bool:
return self.has_any({'Missile Turret', 'Thor', 'War Pigs', 'Spartan Company', "Hel's Angel", 'Battlecruiser'}, player) \
return self.has_any({'Missile Turret', 'Thor', 'War Pigs', 'Spartan Company', "Hel's Angel", 'Battlecruiser', 'Wraith'}, player) \
or self._sc2wol_has_competent_anti_air(world, player) \
or get_option_value(world, player, 'required_tactics') > 0 and self.has_any('Ghost', 'Spectre')
or get_option_value(world, player, 'required_tactics') > 0 and self.has_any({'Ghost', 'Spectre'}, player)
def _sc2wol_has_heavy_defense(self, world: MultiWorld, player: int) -> bool:
return (self.has_any({'Siege Tank', 'Vulture', 'Planetary Fortress'}, player) or
self._sc2wol_has_manned_bunkers(world, player)) and self._sc2wol_has_anti_air(world, player)
def _sc2wol_defense_rating(self, world: MultiWorld, player: int, zerg_enemy: bool) -> bool:
defense_score = sum((defense_ratings[item] for item in defense_ratings if self.has(item, player)))
if self.has_any({'Marine', 'Marauder'}, player) and self.has('Bunker', player):
defense_score += 3
if zerg_enemy:
defense_score += sum((zerg_defense_ratings[item] for item in zerg_defense_ratings if self.has(item, player)))
if self.has('Firebat', player) and self.has('Bunker', player):
defense_score += 2
# Advanced Tactics bumps defense rating requirements down by 2
if get_option_value(world, player, 'required_tactics') > 0:
defense_score += 2
return defense_score
def _sc2wol_has_competent_comp(self, world: MultiWorld, player: int) -> bool:
return (self.has('Marine', player) or self.has('Marauder', player) and
@@ -38,12 +45,11 @@ class SC2WoLLogic(LogicMixin):
self.has('Siege Tank', player) and self._sc2wol_has_competent_anti_air(world, player)
def _sc2wol_has_train_killers(self, world: MultiWorld, player: int) -> bool:
return (self.has_any({'Siege Tank', 'Diamondback'}, player) or
self.has_all({'Reaper', "G-4 Clusterbomb"}, player) or self.has_all({'Spectre', 'Psionic Lash'}, player)
or self.has('Marauder', player))
return (self.has_any({'Siege Tank', 'Diamondback', 'Marauder'}, player) or get_option_value(world, player, 'required_tactics') > 0
and self.has_all({'Reaper', "G-4 Clusterbomb"}, player) or self.has_all({'Spectre', 'Psionic Lash'}, player))
def _sc2wol_able_to_rescue(self, world: MultiWorld, player: int) -> bool:
return self.has_any({'Medivac', 'Hercules', 'Raven', 'Viking'}, player)
return self.has_any({'Medivac', 'Hercules', 'Raven', 'Viking'}, player) or get_option_value(world, player, 'required_tactics') > 0
def _sc2wol_has_protoss_common_units(self, world: MultiWorld, player: int) -> bool:
return self.has_any({'Zealot', 'Immortal', 'Stalker', 'Dark Templar'}, player) \
@@ -71,18 +77,18 @@ class SC2WoLLogic(LogicMixin):
return get_option_value(world, player, 'required_tactics') > 0 and self.has_any({'Ghost', 'Spectre'}, player)
def _sc2wol_final_mission_requirements(self, world: MultiWorld, player: int):
defense_rating = self._sc2wol_defense_rating(world, player, True)
beats_kerrigan = self.has_any({'Marine', 'Banshee', 'Ghost'}, player) or get_option_value(world, player, 'required_tactics') > 0
if get_option_value(world, player, 'all_in_map') == 0:
# Ground
version_logic = sum(
self.has(item, player) for item in ['Planetary Fortress', 'Siege Tank', 'Psi Disruptor', 'Banshee', 'Battlecruiser', 'Maelstrom Rounds']
) + self._sc2wol_has_manned_bunkers(world, player) >= 4
if self.has_any({'Battlecruiser', 'Banshee'}, player):
defense_rating += 3
return defense_rating >= 12 and beats_kerrigan
else:
# Air
version_logic = self.has_any({'Viking', 'Battlecruiser'}, player) \
and self.has_any({'Hive Mind Emulator', 'Psi Disruptor', 'Missile Turret'}, player)
return self._sc2wol_has_heavy_defense(world, player) and version_logic
return defense_rating >= 8 and beats_kerrigan \
and self.has_any({'Viking', 'Battlecruiser'}, player) \
and self.has_any({'Hive Mind Emulator', 'Psi Disruptor', 'Missile Turret'}, player)
def _sc2wol_cleared_missions(self, world: MultiWorld, player: int, mission_count: int) -> bool:
return self.has_group("Missions", player, mission_count)

View File

@@ -65,7 +65,7 @@ vanilla_shuffle_order = [
FillMission("all_in", [26, 27], "Char", completion_critical=True, or_requirements=True)
]
mini_shuffle_order = [
mini_campaign_order = [
FillMission("no_build", [-1], "Mar Sara", completion_critical=True),
FillMission("easy", [0], "Colonist"),
FillMission("medium", [1], "Colonist"),
@@ -102,13 +102,13 @@ grid_order = [
FillMission("medium", [1, 4], "Artifact", or_requirements=True),
FillMission("hard", [2, 5, 10, 7], "Artifact", or_requirements=True),
FillMission("hard", [3, 6, 11], "Artifact", or_requirements=True),
FillMission("medium", [4, 9], "Covert", or_requirements=True),
FillMission("hard", [5, 8, 10], "Covert", or_requirements=True),
FillMission("hard", [6, 9, 11], "Covert", or_requirements=True),
FillMission("medium", [4, 9, 12], "Covert", or_requirements=True),
FillMission("hard", [5, 8, 10, 13], "Covert", or_requirements=True),
FillMission("hard", [6, 9, 11, 14], "Covert", or_requirements=True),
FillMission("hard", [7, 10], "Covert", or_requirements=True),
FillMission("hard", [8, 13], "Rebellion", or_requirements=True),
FillMission("hard", [9, 12, 14], "Rebellion", or_requirements=True),
FillMission("hard", [10, 13, 15], "Rebellion", or_requirements=True),
FillMission("hard", [10, 13], "Rebellion", or_requirements=True),
FillMission("all_in", [11, 14], "Rebellion", or_requirements=True)
]
@@ -139,7 +139,7 @@ blitz_order = [
FillMission("all_in", [0, 1], "Char", number=5, or_requirements=True)
]
mission_orders = [vanilla_shuffle_order, vanilla_shuffle_order, mini_shuffle_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]
vanilla_mission_req_table = {
@@ -177,13 +177,16 @@ vanilla_mission_req_table = {
lookup_id_to_mission: Dict[int, str] = {
data.id: mission_name for mission_name, data in vanilla_mission_req_table.items() if data.id}
starting_mission_locations = {
no_build_starting_mission_locations = {
"Liberation Day": "Liberation Day: Victory",
"Breakout": "Breakout: Victory",
"Ghost of a Chance": "Ghost of a Chance: Victory",
"Piercing the Shroud": "Piercing the Shroud: Victory",
"Whispers of Doom": "Whispers of Doom: Victory",
"Belly of the Beast": "Belly of the Beast: Victory",
}
build_starting_mission_locations = {
"Zero Hour": "Zero Hour: First Group Rescued",
"Evacuation": "Evacuation: First Chysalis",
"Devil's Playground": "Devil's Playground: Tosh's Miners"
@@ -196,7 +199,22 @@ advanced_starting_mission_locations = {
def get_starting_mission_locations(world: MultiWorld, player: int) -> set[str]:
if get_option_value(world, player, 'required_tactics') > 0:
return {**starting_mission_locations, **advanced_starting_mission_locations}
if get_option_value(world, player, 'shuffle_no_build') or get_option_value(world, player, 'mission_order') < 2:
# Always start with a no-build mission unless explicitly relegating them
# Vanilla and Vanilla Shuffled always start with a no-build even when relegated
return no_build_starting_mission_locations
elif get_option_value(world, player, 'required_tactics') > 0:
# Advanced Tactics/No Logic add more starting missions to the pool
return {**build_starting_mission_locations, **advanced_starting_mission_locations}
else:
return starting_mission_locations
# Standard starting missions when relegate is on
return build_starting_mission_locations
alt_final_mission_locations = {
"Maw of the Void": "Maw of the Void: Victory",
"Engine of Destruction": "Engine of Destruction: Victory",
"Supernova": "Supernova: Victory",
"Gates of Hell": "Gates of Hell: Victory",
"Shatter the Sky": "Shatter the Sky: Victory"
}

View File

@@ -36,18 +36,18 @@ class AllInMap(Choice):
class MissionOrder(Choice):
"""Determines the order the missions are played in.
Vanilla: Keeps the standard mission order and branching from the WoL Campaign.
Vanilla Shuffled: Keeps same branching paths from the WoL Campaign but randomizes the order of missions within.
Mini Shuffle: Shorter version of the campaign with randomized missions and optional branches.
Grid: A 4x4 grid of random missions. Start at the top left and forge a path towards All-In.
Mini Grid: A 3x3 version of Grid.
Blitz: 10 random missions that open up very quickly.
Gauntlet: Linear series of 7 random missions to complete the campaign."""
"""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.
Mini Grid (9): A 3x3 version of Grid. Complete the bottom-right mission to win.
Blitz (14): 14 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."""
display_name = "Mission Order"
option_vanilla = 0
option_vanilla_shuffled = 1
option_mini_shuffle = 2
option_mini_campaign = 2
option_grid = 3
option_mini_grid = 4
option_blitz = 5
@@ -61,11 +61,11 @@ class ShuffleProtoss(DefaultOnToggle):
display_name = "Shuffle Protoss Missions"
class RelegateNoBuildMissions(DefaultOnToggle):
class ShuffleNoBuild(DefaultOnToggle):
"""Determines if the 5 no-build missions are included in the shuffle if Vanilla mission order is not enabled.
If turned on 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 on with reduced mission settings, the 5 no-build missions will not appear."""
display_name = "Relegate No-Build Missions"
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."""
display_name = "Shuffle No-Build Missions"
class EarlyUnit(DefaultOnToggle):
@@ -76,7 +76,7 @@ class EarlyUnit(DefaultOnToggle):
class RequiredTactics(Choice):
"""Determines the maximum tactical difficulty of the seed (separate from mission difficulty). Higher settings increase randomness.
Standard: All missions can be completed with good micro and macro.
Advanced: Completing missions may require relying on starting units and difficult-to-use units.
Advanced: Completing missions may require relying on starting units and micro-heavy units.
No Logic: Units and upgrades may be placed anywhere. LIKELY TO RENDER THE RUN IMPOSSIBLE ON HARDER DIFFICULTIES!"""
display_name = "Required Tactics"
option_standard = 0
@@ -84,7 +84,7 @@ class RequiredTactics(Choice):
option_no_logic = 2
class UnitsAlwaysHaveUpgrades(Toggle):
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."""
display_name = "Units Always Have Upgrades"
@@ -115,7 +115,7 @@ sc2wol_options: Dict[str, Option] = {
"all_in_map": AllInMap,
"mission_order": MissionOrder,
"shuffle_protoss": ShuffleProtoss,
"relegate_no_build": RelegateNoBuildMissions,
"shuffle_no_build": ShuffleNoBuild,
"early_unit": EarlyUnit,
"required_tactics": RequiredTactics,
"units_always_have_upgrades": UnitsAlwaysHaveUpgrades,
@@ -134,7 +134,7 @@ def get_option_value(world: MultiWorld, player: int, name: str) -> int:
return int(option[player].value)
def get_option_set_value(world: MultiWorld, player: int, name: str) -> int:
def get_option_set_value(world: MultiWorld, player: int, name: str) -> set:
option = getattr(world, name, None)
if option is None:

View File

@@ -2,7 +2,7 @@ from typing import Callable
from BaseClasses import MultiWorld, ItemClassification, Item, Location
from .Items import item_table
from .MissionTables import no_build_regions_list, easy_regions_list, medium_regions_list, hard_regions_list,\
mission_orders, get_starting_mission_locations, MissionInfo, vanilla_mission_req_table
mission_orders, get_starting_mission_locations, MissionInfo, vanilla_mission_req_table, alt_final_mission_locations
from .Options import get_option_value, get_option_set_value
from .LogicMixin import SC2WoLLogic
@@ -28,8 +28,7 @@ def filter_missions(world: MultiWorld, player: int) -> dict[str, list[str]]:
mission_order_type = get_option_value(world, player, "mission_order")
shuffle_protoss = get_option_value(world, player, "shuffle_protoss")
relegate_no_build = get_option_value(world, player, "relegate_no_build")
excluded_missions: set[str] = get_option_set_value(world, player, "excluded_missions")
excluded_missions: set[str] = set(get_option_set_value(world, player, "excluded_missions"))
invalid_mission_names = excluded_missions.difference(vanilla_mission_req_table.keys())
if invalid_mission_names:
raise Exception("Error in locked_missions - the following are not valid mission names: " + ", ".join(invalid_mission_names))
@@ -41,31 +40,31 @@ def filter_missions(world: MultiWorld, player: int) -> dict[str, list[str]]:
"easy": easy_regions_list[:],
"medium": medium_regions_list[:],
"hard": hard_regions_list[:],
"all_in": "all_in"
"all_in": ["All-In"]
}
mission_sets = [
set(no_build_regions_list),
set(easy_regions_list),
set(medium_regions_list),
set(hard_regions_list)
mission_pools = [
no_build_regions_list,
easy_regions_list,
medium_regions_list,
hard_regions_list
]
# Omitting No Build missions if relegating no-build
if relegate_no_build:
# The build missions in starting_mission_locations become the new "no build missions"
mission_sets[0] = set(get_starting_mission_locations(world, player).keys())
mission_sets[0].difference_update(no_build_regions_list)
# Removing the new no-build missions from their original sets
for mission_set in mission_sets[1:]:
mission_set.difference_update(mission_sets[0])
# Omitting Protoss missions if not shuffling protoss
if not shuffle_protoss:
excluded_missions = excluded_missions.union(PROTOSS_REGIONS)
for mission_set in mission_sets:
mission_set.difference_update(excluded_missions)
# Replacing All-In on low mission counts
if mission_count < 14:
final_mission = world.random.choice([mission for mission in alt_final_mission_locations.keys() if mission not in excluded_missions])
excluded_missions.add(final_mission)
else:
final_mission = 'All-In'
# Yaml settings determine which missions can be placed in the first slot
mission_pools[0] = [mission for mission in get_starting_mission_locations(world, player).keys() if mission not in excluded_missions]
# Removing the new no-build missions from their original sets
for i in range(1, len(mission_pools)):
mission_pools[i] = [mission for mission in mission_pools[i] if mission not in excluded_missions.union(mission_pools[0])]
# Removing random missions from each difficulty set in a cycle
set_cycle = 0
mission_pools = [list(mission_set) for mission_set in mission_sets]
current_count = sum(len(mission_pool) for mission_pool in mission_pools)
if current_count < mission_count:
raise Exception("Not enough missions available to fill the campaign on current settings. Please exclude fewer missions.")
@@ -86,7 +85,8 @@ def filter_missions(world: MultiWorld, player: int) -> dict[str, list[str]]:
"no_build": mission_pools[0],
"easy": mission_pools[1],
"medium": mission_pools[2],
"hard": mission_pools[3]
"hard": mission_pools[3],
"all_in": [final_mission]
}
@@ -109,28 +109,24 @@ class ValidInventory:
def has_all(self, items: set[str], player: int):
return all(item in self.logical_inventory for item in items)
def has_units_per_structure(self, min_units_per_structure) -> bool:
return len(BARRACKS_UNITS.intersection(self.logical_inventory)) > min_units_per_structure and \
len(FACTORY_UNITS.intersection(self.logical_inventory)) > min_units_per_structure and \
len(STARPORT_UNITS.intersection(self.logical_inventory)) > min_units_per_structure
def has_units_per_structure(self) -> bool:
return len(BARRACKS_UNITS.intersection(self.logical_inventory)) > self.min_units_per_structure and \
len(FACTORY_UNITS.intersection(self.logical_inventory)) > self.min_units_per_structure and \
len(STARPORT_UNITS.intersection(self.logical_inventory)) > self.min_units_per_structure
def generate_reduced_inventory(self, inventory_size: int, mission_requirements: list[Callable]) -> list[Item]:
"""Attempts to generate a reduced inventory that can fulfill the mission requirements."""
inventory = list(self.item_pool)
locked_items = list(self.locked_items)
self.logical_inventory = {
item.name for item in inventory + locked_items
item.name for item in inventory + locked_items + self.existing_items
if item.classification in (ItemClassification.progression, ItemClassification.progression_skip_balancing)
}
requirements = mission_requirements
cascade_keys = self.cascade_removal_map.keys()
units_always_have_upgrades = get_option_value(self.world, self.player, "units_always_have_upgrades")
# Inventory restrictiveness based on number of missions with checks
mission_order_type = get_option_value(self.world, self.player, "mission_order")
mission_count = len(mission_orders[mission_order_type]) - 1
min_units_per_structure = int(mission_count / 7)
if min_units_per_structure > 0:
requirements.append(lambda state: state.has_units_per_structure(min_units_per_structure))
if self.min_units_per_structure > 0:
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
@@ -181,7 +177,7 @@ class ValidInventory:
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_anti_air = lambda world, player: SC2WoLLogic._sc2wol_has_anti_air(self, world, player)
self._sc2wol_has_heavy_defense = lambda world, player: SC2WoLLogic._sc2wol_has_heavy_defense(self, world, player)
self._sc2wol_defense_rating = lambda world, player, zerg_enemy: SC2WoLLogic._sc2wol_defense_rating(self, world, player, zerg_enemy)
self._sc2wol_has_competent_comp = lambda world, player: SC2WoLLogic._sc2wol_has_competent_comp(self, world, player)
self._sc2wol_has_train_killers = lambda world, player: SC2WoLLogic._sc2wol_has_train_killers(self, world, player)
self._sc2wol_able_to_rescue = lambda world, player: SC2WoLLogic._sc2wol_able_to_rescue(self, world, player)
@@ -190,7 +186,6 @@ class ValidInventory:
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_has_manned_bunkers = lambda world, player: SC2WoLLogic._sc2wol_has_manned_bunkers(self, world, player)
self._sc2wol_final_mission_requirements = lambda world, player: SC2WoLLogic._sc2wol_final_mission_requirements(self, world, player)
def __init__(self, world: MultiWorld, player: int,
@@ -205,14 +200,19 @@ class ValidInventory:
# Initial filter of item pool
self.item_pool = []
item_quantities: dict[str, int] = dict()
# Inventory restrictiveness based on number of missions with checks
mission_order_type = get_option_value(self.world, self.player, "mission_order")
mission_count = len(mission_orders[mission_order_type]) - 1
self.min_units_per_structure = int(mission_count / 7)
min_upgrades = 1 if mission_count < 10 else 2
for item in item_pool:
item_info = item_table[item.name]
if item_info.type == "Upgrade":
# All Upgrades are locked except for the final tier
# Locking upgrades based on mission duration
if item.name not in item_quantities:
item_quantities[item.name] = 0
item_quantities[item.name] += 1
if item_quantities[item.name] < item_info.quantity:
if item_quantities[item.name] < min_upgrades:
self.locked_items.append(item)
else:
self.item_pool.append(item)

View File

@@ -2,24 +2,37 @@ from typing import List, Set, Dict, Tuple, Optional, Callable
from BaseClasses import MultiWorld, Region, Entrance, Location, RegionType
from .Locations import LocationData
from .Options import get_option_value
from .MissionTables import MissionInfo, mission_orders, vanilla_mission_req_table
from .MissionTables import MissionInfo, mission_orders, vanilla_mission_req_table, alt_final_mission_locations
from .PoolFilter import filter_missions
import random
def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData, ...], location_cache: List[Location]):
def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData, ...], location_cache: List[Location])\
-> tuple[dict[str, MissionInfo], int, str]:
locations_per_region = get_locations_per_region(locations)
mission_order_type = get_option_value(world, player, "mission_order")
mission_order = mission_orders[mission_order_type]
mission_pools = filter_missions(world, player)
final_mission = mission_pools['all_in'][0]
used_regions = [mission for mission_pool in mission_pools.values() for mission in mission_pool]
used_regions += ['All-In']
regions = [create_region(world, player, locations_per_region, location_cache, "Menu")]
for region_name in used_regions:
regions.append(create_region(world, player, locations_per_region, location_cache, region_name))
# Changing the completion condition for alternate final missions into an event
if final_mission != 'All-In':
final_location = alt_final_mission_locations[final_mission]
# Final location should be near the end of the cache
for i in range(len(location_cache) - 1, -1, -1):
if location_cache[i].name == final_location:
location_cache[i].locked = True
location_cache[i].event = True
location_cache[i].address = None
break
else:
final_location = 'All-In: Victory'
if __debug__:
if mission_order_type in (0, 1):
@@ -98,7 +111,7 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
lambda state: state.has('Beat Gates of Hell', player) and (
state.has('Beat Shatter the Sky', player) or state.has('Beat Belly of the Beast', player)))
return vanilla_mission_req_table
return vanilla_mission_req_table, 29, final_location
else:
missions = []
@@ -108,8 +121,8 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
if mission is None:
missions.append(None)
elif mission.type == "all_in":
missions.append("All-In")
elif get_option_value(world, player, "relegate_no_build") and mission.relegate:
missions.append(final_mission)
elif mission.relegate and not get_option_value(world, player, "shuffle_no_build"):
missions.append("no_build")
else:
missions.append(mission.type)
@@ -117,11 +130,11 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
# Place Protoss Missions if we are not using ShuffleProtoss and are in Vanilla Shuffled
if get_option_value(world, player, "shuffle_protoss") == 0 and mission_order_type == 1:
missions[22] = "A Sinister Turn"
medium_pool.remove("A Sinister Turn")
mission_pools['medium'].remove("A Sinister Turn")
missions[23] = "Echoes of the Future"
medium_pool.remove("Echoes of the Future")
mission_pools['medium'].remove("Echoes of the Future")
missions[24] = "In Utter Darkness"
hard_pool.remove("In Utter Darkness")
mission_pools['hard'].remove("In Utter Darkness")
no_build_slots = []
easy_slots = []
@@ -191,7 +204,8 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
completion_critical=mission_order[i].completion_critical,
or_requirements=mission_order[i].or_requirements)})
return mission_req_table
final_mission_id = vanilla_mission_req_table[final_mission].id
return mission_req_table, final_mission_id, final_mission + ': Victory'
def throwIfAnyLocationIsNotAssignedToARegion(regions: List[Region], regionNames: Set[str]):

View File

@@ -44,6 +44,8 @@ class SC2WoLWorld(World):
locked_locations: typing.List[str]
location_cache: typing.List[Location]
mission_req_table = {}
final_mission_id: int
victory_item: str
required_client_version = 0, 3, 5
def __init__(self, world: MultiWorld, player: int):
@@ -60,8 +62,9 @@ class SC2WoLWorld(World):
return StarcraftWoLItem(name, data.classification, data.code, self.player)
def create_regions(self):
self.mission_req_table = create_regions(self.world, self.player, get_locations(self.world, self.player),
self.location_cache)
self.mission_req_table, self.final_mission_id, self.victory_item = create_regions(
self.world, self.player, get_locations(self.world, self.player), self.location_cache
)
def generate_basic(self):
excluded_items = get_excluded_items(self, self.world, self.player)
@@ -76,8 +79,7 @@ class SC2WoLWorld(World):
def set_rules(self):
setup_events(self.world, self.player, self.locked_locations, self.location_cache)
self.world.completion_condition[self.player] = lambda state: state.has('All-In: Victory', self.player)
self.world.completion_condition[self.player] = lambda state: state.has(self.victory_item, self.player)
def get_filler_item_name(self) -> str:
return self.world.random.choice(filler_items)
@@ -93,6 +95,7 @@ class SC2WoLWorld(World):
slot_req_table[mission] = self.mission_req_table[mission]._asdict()
slot_data["mission_req"] = slot_req_table
slot_data["final_mission"] = self.final_mission_id
return slot_data
@@ -186,7 +189,7 @@ def get_item_pool(world: MultiWorld, player: int, mission_req_table: dict[str, M
else:
pool.append(item)
existing_items = starter_items + [item.name for item in world.precollected_items[player]]
existing_items = starter_items + [item for item in world.precollected_items[player]]
existing_names = [item.name for item in existing_items]
# Removing upgrades for excluded items
for item_name in excluded_items: