SC2: Required Tactics and Unit Upgrade options, better connected item handling

This commit is contained in:
Magnemania
2022-10-13 07:50:39 -04:00
parent cb5ef5c157
commit cf1ae860fb
7 changed files with 259 additions and 145 deletions

View File

@@ -1,5 +1,7 @@
from BaseClasses import Item, ItemClassification
from BaseClasses import Item, ItemClassification, MultiWorld
import typing
from .Options import get_option_value
from .MissionTables import vanilla_mission_req_table
@@ -9,6 +11,7 @@ class ItemData(typing.NamedTuple):
number: typing.Optional[int]
classification: ItemClassification = ItemClassification.useful
quantity: int = 1
parent_item: str = None
class StarcraftWoLItem(Item):
@@ -48,51 +51,51 @@ item_table = {
"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),
"Projectile Accelerator (Bunker)": ItemData(200 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 0),
"Neosteel Bunker (Bunker)": ItemData(201 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 1),
"Titanium Housing (Missile Turret)": ItemData(202 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 2, classification=ItemClassification.filler),
"Hellstorm Batteries (Missile Turret)": ItemData(203 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 3),
"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, 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),
"Combat Shield (Marine)": ItemData(209 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 9, classification=ItemClassification.progression),
"Advanced Medic Facilities (Medic)": ItemData(210 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 10, classification=ItemClassification.progression),
"Stabilizer Medpacks (Medic)": ItemData(211 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 11, classification=ItemClassification.progression),
"Incinerator Gauntlets (Firebat)": ItemData(212 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 12, classification=ItemClassification.filler),
"Juggernaut Plating (Firebat)": ItemData(213 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 13),
"Concussive Shells (Marauder)": ItemData(214 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 14),
"Kinetic Foam (Marauder)": ItemData(215 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 15),
"U-238 Rounds (Reaper)": ItemData(216 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 16),
"G-4 Clusterbomb (Reaper)": ItemData(217 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 17, classification=ItemClassification.progression),
"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"),
"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"),
"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"),
"Stabilizer Medpacks (Medic)": ItemData(211 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 11, classification=ItemClassification.progression, parent_item="Medic"),
"Incinerator Gauntlets (Firebat)": ItemData(212 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 12, classification=ItemClassification.filler, parent_item="Firebat"),
"Juggernaut Plating (Firebat)": ItemData(213 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 13, parent_item="Firebat"),
"Concussive Shells (Marauder)": ItemData(214 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 14, parent_item="Marauder"),
"Kinetic Foam (Marauder)": ItemData(215 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 15, parent_item="Marauder"),
"U-238 Rounds (Reaper)": ItemData(216 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 16, parent_item="Reaper"),
"G-4 Clusterbomb (Reaper)": ItemData(217 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 17, classification=ItemClassification.progression, parent_item="Reaper"),
"Twin-Linked Flamethrower (Hellion)": ItemData(300 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 0, classification=ItemClassification.filler),
"Thermite Filaments (Hellion)": ItemData(301 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 1),
"Cerberus Mine (Vulture)": 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),
"Multi-Lock Weapons System (Goliath)": ItemData(304 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 4),
"Ares-Class Targeting System (Goliath)": ItemData(305 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 5),
"Tri-Lithium Power Cell (Diamondback)": ItemData(306 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 6, classification=ItemClassification.filler),
"Shaped Hull (Diamondback)": ItemData(307 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 7, classification=ItemClassification.filler),
"Maelstrom Rounds (Siege Tank)": ItemData(308 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 8),
"Shaped Blast (Siege Tank)": ItemData(309 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 9),
"Rapid Deployment Tube (Medivac)": ItemData(310 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 10, classification=ItemClassification.filler),
"Advanced Healing AI (Medivac)": ItemData(311 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 11, classification=ItemClassification.filler),
"Tomahawk Power Cells (Wraith)": ItemData(312 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 12, classification=ItemClassification.filler),
"Displacement Field (Wraith)": ItemData(313 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 13, classification=ItemClassification.filler),
"Ripwave Missiles (Viking)": ItemData(314 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 14),
"Phobos-Class Weapons System (Viking)": ItemData(315 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 15),
"Cross-Spectrum Dampeners (Banshee)": ItemData(316 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 16, classification=ItemClassification.filler),
"Shockwave Missile Battery (Banshee)": ItemData(317 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 17),
"Missile Pods (Battlecruiser)": ItemData(318 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 18, classification=ItemClassification.filler),
"Defensive Matrix (Battlecruiser)": ItemData(319 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 19, classification=ItemClassification.filler),
"Ocular Implants (Ghost)": ItemData(320 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 20),
"Crius Suit (Ghost)": ItemData(321 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 21),
"Psionic Lash (Spectre)": ItemData(322 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 22, classification=ItemClassification.progression),
"Nyx-Class Cloaking Module (Spectre)": ItemData(323 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 23),
"330mm Barrage Cannon (Thor)": ItemData(324 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 24, classification=ItemClassification.filler),
"Immortality Protocol (Thor)": ItemData(325 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 25, classification=ItemClassification.filler),
"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"),
"Replenishable Magazine (Vulture)": ItemData(303 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 3, classification=ItemClassification.filler, parent_item="Vulture"),
"Multi-Lock Weapons System (Goliath)": ItemData(304 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 4, parent_item="Goliath"),
"Ares-Class Targeting System (Goliath)": ItemData(305 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 5, parent_item="Goliath"),
"Tri-Lithium Power Cell (Diamondback)": ItemData(306 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 6, classification=ItemClassification.filler, parent_item="Diamondback"),
"Shaped Hull (Diamondback)": ItemData(307 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 7, classification=ItemClassification.filler, parent_item="Diamondback"),
"Maelstrom Rounds (Siege Tank)": ItemData(308 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 8, classification=ItemClassification.progression, parent_item="Siege Tank"),
"Shaped Blast (Siege Tank)": ItemData(309 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 9, parent_item="Siege Tank"),
"Rapid Deployment Tube (Medivac)": ItemData(310 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 10, classification=ItemClassification.filler, parent_item="Medivac"),
"Advanced Healing AI (Medivac)": ItemData(311 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 11, classification=ItemClassification.filler, parent_item="Medivac"),
"Tomahawk Power Cells (Wraith)": ItemData(312 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 12, classification=ItemClassification.filler, parent_item="Wraith"),
"Displacement Field (Wraith)": ItemData(313 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 13, classification=ItemClassification.filler, parent_item="Wraith"),
"Ripwave Missiles (Viking)": ItemData(314 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 14, parent_item="Viking"),
"Phobos-Class Weapons System (Viking)": ItemData(315 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 15, parent_item="Viking"),
"Cross-Spectrum Dampeners (Banshee)": ItemData(316 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 16, classification=ItemClassification.filler, parent_item="Banshee"),
"Shockwave Missile Battery (Banshee)": ItemData(317 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 17, parent_item="Banshee"),
"Missile Pods (Battlecruiser)": ItemData(318 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 18, classification=ItemClassification.filler, parent_item="Battlecruiser"),
"Defensive Matrix (Battlecruiser)": ItemData(319 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 19, classification=ItemClassification.filler, parent_item="Battlecruiser"),
"Ocular Implants (Ghost)": ItemData(320 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 20, parent_item="Ghost"),
"Crius Suit (Ghost)": ItemData(321 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 21, parent_item="Ghost"),
"Psionic Lash (Spectre)": ItemData(322 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 22, classification=ItemClassification.progression, parent_item="Spectre"),
"Nyx-Class Cloaking Module (Spectre)": ItemData(323 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 23, parent_item="Spectre"),
"330mm Barrage Cannon (Thor)": ItemData(324 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 24, classification=ItemClassification.filler, parent_item="Thor"),
"Immortality Protocol (Thor)": ItemData(325 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 25, classification=ItemClassification.filler, parent_item="Thor"),
"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),
@@ -117,9 +120,9 @@ item_table = {
"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),
"Fortified Bunker": ItemData(611 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 11),
"Planetary Fortress": ItemData(612 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 12),
"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),
"Predator": ItemData(614 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 14, classification=ItemClassification.filler),
"Hercules": ItemData(615 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 15, classification=ItemClassification.progression),
@@ -141,15 +144,33 @@ item_table = {
"+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),
"+2 Starting Supply": ItemData(802 + SC2WOL_ITEM_ID_OFFSET, "Supply", 2, quantity=0, classification=ItemClassification.filler),
# "Keystone Piece": ItemData(850 + SC2WOL_ITEM_ID_OFFSET, "Goal", 0, quantity=0, classification=ItemClassification.progression_skip_balancing)
}
basic_unit: typing.Tuple[str, ...] = (
basic_units = {
'Marine',
'Marauder',
'Firebat',
'Hellion',
'Vulture'
)
}
advanced_basic_units = {
'Reaper',
'Goliath',
'Diamondback',
'Viking'
}
def get_basic_units(world: MultiWorld, player: int) -> set[str]:
if get_option_value(world, player, 'required_tactics') > 0:
return basic_units.union(advanced_basic_units)
else:
return basic_units
item_name_groups = {}
for item, data in item_table.items():
@@ -176,4 +197,5 @@ type_flaggroups: typing.Dict[str, int] = {
"Minerals": 8,
"Vespene": 9,
"Supply": 10,
"Goal": 11
}

View File

@@ -1,5 +1,6 @@
from typing import List, Tuple, Optional, Callable, NamedTuple
from BaseClasses import MultiWorld
from .Options import get_option_value
from BaseClasses import Location
@@ -19,6 +20,7 @@ class LocationData(NamedTuple):
def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[LocationData, ...]:
# Note: rules which are ended with or True are rules identified as needed later when restricted units is an option
logic_level = get_option_value(world, 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),
@@ -119,10 +121,7 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
LocationData("Supernova", "Supernova: East Relic", SC2WOL_LOC_ID_OFFSET + 1104,
lambda state: state._sc2wol_beats_protoss_deathball(world, player)),
LocationData("Maw of the Void", "Maw of the Void: Victory", SC2WOL_LOC_ID_OFFSET + 1200,
lambda state: state.has('Battlecruiser', player) or
state._sc2wol_has_air(world, player) and
state._sc2wol_has_competent_anti_air(world, player) and
state.has('Science Vessel', player)),
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)),
@@ -252,9 +251,13 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
beat_events = []
for location_data in location_table:
if location_data.name.endswith((": Victory", ": Defeat")):
for i in range(len(location_table)):
# Removing all item-based logic on No Logic
if logic_level == 2:
location_table[i] = location_table[i]._replace(rule=lambda state: True)
# Generating Beat event locations
if location_table[i].name.endswith((": Victory", ": Defeat")):
beat_events.append(
location_data._replace(name="Beat " + location_data.name.rsplit(": ", 1)[0], code=None)
location_table[i]._replace(name="Beat " + location_table[i].name.rsplit(": ", 1)[0], code=None)
)
return tuple(location_table + beat_events)

View File

@@ -1,11 +1,12 @@
from BaseClasses import MultiWorld
from worlds.AutoWorld import LogicMixin
from .Options import get_option_value
from .Items import get_basic_units
class SC2WoLLogic(LogicMixin):
def _sc2wol_has_common_unit(self, world: MultiWorld, player: int) -> bool:
return self.has_any({'Marine', 'Marauder', 'Firebat', 'Hellion', 'Vulture'}, player)
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)
@@ -21,10 +22,12 @@ class SC2WoLLogic(LogicMixin):
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) or self._sc2wol_has_competent_anti_air(world, player)
return self.has_any({'Missile Turret', 'Thor', 'War Pigs', 'Spartan Company', "Hel's Angel", 'Battlecruiser'}, 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')
def _sc2wol_has_heavy_defense(self, world: MultiWorld, player: int) -> bool:
return (self.has_any({'Siege Tank', 'Vulture'}, player) or
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_has_competent_comp(self, world: MultiWorld, player: int) -> bool:
@@ -37,17 +40,19 @@ class SC2WoLLogic(LogicMixin):
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('Marauders', player))
or self.has('Marauder', player))
def _sc2wol_able_to_rescue(self, world: MultiWorld, player: int) -> bool:
return self.has_any({'Medivac', 'Hercules', 'Raven', 'Viking'}, player)
def _sc2wol_has_protoss_common_units(self, world: MultiWorld, player: int) -> bool:
return self.has_any({'Zealot', 'Immortal', 'Stalker', 'Dark Templar'}, player)
return self.has_any({'Zealot', 'Immortal', 'Stalker', 'Dark Templar'}, player) \
or get_option_value(world, player, 'required_tactics') > 0 and self.has_any({'High Templar', 'Dark Templar'}, player)
def _sc2wol_has_protoss_medium_units(self, world: MultiWorld, player: int) -> bool:
return self._sc2wol_has_protoss_common_units(world, player) and \
self.has_any({'Stalker', 'Void Ray', 'Phoenix', 'Carrier'}, player)
self.has_any({'Stalker', 'Void Ray', 'Phoenix', 'Carrier'}, player) \
or get_option_value(world, player, 'required_tactics') > 0 and self.has_any({'High Templar', 'Dark Templar'}, player)
def _sc2wol_beats_protoss_deathball(self, world: MultiWorld, player: int) -> bool:
return self.has_any({'Banshee', 'Battlecruiser'}, player) and self._sc2wol_has_competent_anti_air or \
@@ -62,12 +67,15 @@ class SC2WoLLogic(LogicMixin):
self._sc2wol_has_competent_anti_air(world, player) and \
self.has("Science Vessel", player)
def _sc2wol_has_nukes(self, world: MultiWorld, player: int) -> bool:
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):
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']
) + self._sc2wol_has_manned_bunkers(world, player) >= 3
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
else:
# Air
version_logic = self.has_any({'Viking', 'Battlecruiser'}, player) \

View File

@@ -1,5 +1,8 @@
from typing import NamedTuple, Dict, List
from BaseClasses import MultiWorld
from .Options import get_option_value
no_build_regions_list = ["Liberation Day", "Breakout", "Ghost of a Chance", "Piercing the Shroud", "Whispers of Doom",
"Belly of the Beast"]
easy_regions_list = ["The Outlaws", "Zero Hour", "Evacuation", "Outbreak", "Smash and Grab", "Devil's Playground"]
@@ -115,9 +118,9 @@ mini_grid_order = [
FillMission("medium", [1, 5], "Colonist", or_requirements=True),
FillMission("easy", [0], "Artifact"),
FillMission("medium", [1, 3], "Artifact", or_requirements=True),
FillMission("hard", [2, 4, 8], "Artifact", or_requirements=True),
FillMission("hard", [2, 4], "Artifact", or_requirements=True),
FillMission("medium", [3, 7], "Covert", or_requirements=True),
FillMission("hard", [4, 6, 8], "Covert", or_requirements=True),
FillMission("hard", [4, 6], "Covert", or_requirements=True),
FillMission("all_in", [5, 7], "Covert", or_requirements=True)
]
@@ -174,7 +177,6 @@ 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 = {
"Liberation Day": "Liberation Day: Victory",
"Breakout": "Breakout: Victory",
@@ -186,3 +188,15 @@ starting_mission_locations = {
"Evacuation": "Evacuation: First Chysalis",
"Devil's Playground": "Devil's Playground: Tosh's Miners"
}
advanced_starting_mission_locations = {
"Smash and Grab": "Smash and Grab: First Relic",
"The Great Train Robbery": "The Great Train Robbery: North Defiler"
}
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}
else:
return starting_mission_locations

View File

@@ -1,6 +1,6 @@
from typing import Dict
from BaseClasses import MultiWorld
from Options import Choice, Option, DefaultOnToggle, ItemSet
from Options import Choice, Option, Toggle, DefaultOnToggle, ItemSet, OptionSet, Range
class GameDifficulty(Choice):
@@ -43,8 +43,7 @@ class MissionOrder(Choice):
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.
"""
Gauntlet: Linear series of 7 random missions to complete the campaign."""
display_name = "Mission Order"
option_vanilla = 0
option_vanilla_shuffled = 1
@@ -57,16 +56,15 @@ class MissionOrder(Choice):
class ShuffleProtoss(DefaultOnToggle):
"""Determines if the 3 protoss missions are included in the shuffle if Vanilla mission order is not enabled.
On Vanilla Shuffled, the 3 protoss missions will be in their normal position on the Prophecy chain if not shuffled.
On reduced mission settings, the 3 protoss missions will not appear and Protoss units are removed from the pool if not shuffled.
"""
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."""
display_name = "Shuffle Protoss Missions"
class RelegateNoBuildMissions(DefaultOnToggle):
"""Determines if the 5 no-build missions are included in the shuffle if Vanilla mission order is not enabled.
On 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.
On reduced mission settings, the 5 no-build missions will not appear."""
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"
@@ -75,16 +73,40 @@ class EarlyUnit(DefaultOnToggle):
display_name = "Early Unit"
class RequiredTactics(Choice):
"""Determines the maximum tactical difficulty of the seed (separate from mission difficulty). Higher settings increase randomness.
Standard: All missions can be completed with good micro and macro.
Advanced: Completing missions may require relying on starting units and difficult-to-use units.
No Logic: Units and upgrades may be placed anywhere. LIKELY TO RENDER THE RUN IMPOSSIBLE ON HARDER DIFFICULTIES!"""
display_name = "Required Tactics"
option_standard = 0
option_advanced = 1
option_no_logic = 2
class UnitsAlwaysHaveUpgrades(Toggle):
"""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"
class LockedItems(ItemSet):
"""Guarantees that these items will appear in your world"""
"""Guarantees that these items will be unlockable"""
display_name = "Locked Items"
class ExcludedItems(ItemSet):
"""Guarantees that these items will not appear in your world"""
"""Guarantees that these items will not be unlockable"""
display_name = "Excluded Items"
class ExcludedMissions(OptionSet):
"""Guarantees that these missions will not appear in the campaign
Only applies on shortened mission orders.
It may be impossible to build a valid campaign if too many missions are excluded."""
display_name = "Excluded Missions"
# noinspection PyTypeChecker
sc2wol_options: Dict[str, Option] = {
"game_difficulty": GameDifficulty,
@@ -95,15 +117,27 @@ sc2wol_options: Dict[str, Option] = {
"shuffle_protoss": ShuffleProtoss,
"relegate_no_build": RelegateNoBuildMissions,
"early_unit": EarlyUnit,
"required_tactics": RequiredTactics,
"units_always_have_upgrades": UnitsAlwaysHaveUpgrades,
"locked_items": LockedItems,
"excluded_items": ExcludedItems
"excluded_items": ExcludedItems,
"excluded_missions": ExcludedMissions
}
def get_option_value(world: MultiWorld, player: int, name: str) -> int:
option = getattr(world, name, None)
if option == None:
if option is None:
return 0
return int(option[player].value)
def get_option_set_value(world: MultiWorld, player: int, name: str) -> int:
option = getattr(world, name, None)
if option is None:
return set()
return option[player].value

View File

@@ -2,8 +2,8 @@ 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, starting_mission_locations, MissionInfo
from .Options import get_option_value
mission_orders, get_starting_mission_locations, MissionInfo, vanilla_mission_req_table
from .Options import get_option_value, get_option_set_value
from .LogicMixin import SC2WoLLogic
# Items with associated upgrades
@@ -17,19 +17,9 @@ UPGRADABLE_ITEMS = [
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"}
MIN_UNITS_PER_STRUCTURE = [
3, # Vanilla
3, # Vanilla Shuffled
2, # Mini Shuffle
0 # Gauntlet
]
PROTOSS_REGIONS = {"A Sinister Turn", "Echoes of the Future", "In Utter Darkness"}
ALWAYS_USEFUL_ARMORY = [
"Combat Shield (Marine)", "Stabilizer Medpacks (Medic)" # Needed for no-build logic
]
def filter_missions(world: MultiWorld, player: int) -> dict[str, list[str]]:
"""
@@ -39,20 +29,19 @@ 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")
mission_count = 0
for mission in mission_orders[mission_order_type]:
if mission.type == 'all_in': # All-In is placed separately
continue
mission_count += 1
excluded_missions: set[str] = 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))
mission_count = len(mission_orders[mission_order_type]) - 1
# Vanilla and Vanilla Shuffled use the entire mission pool
if mission_count == 28:
return {
'no_build': no_build_regions_list[:],
'easy': easy_regions_list[:],
'medium': medium_regions_list[:],
'hard': hard_regions_list[:]
"no_build": no_build_regions_list[:],
"easy": easy_regions_list[:],
"medium": medium_regions_list[:],
"hard": hard_regions_list[:],
"all_in": "all_in"
}
mission_sets = [
@@ -61,24 +50,25 @@ def filter_missions(world: MultiWorld, player: int) -> dict[str, list[str]]:
set(medium_regions_list),
set(hard_regions_list)
]
# Omitting Protoss missions if not shuffling protoss
if not shuffle_protoss:
for mission_set in mission_sets:
mission_set.difference_update(PROTOSS_REGIONS)
# 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(starting_mission_locations.keys())
mission_sets[0] = set(get_starting_mission_locations(world, player).keys())
mission_sets[0].difference_update(no_build_regions_list)
# Future-proofing in case a non-Easy mission is placed in starting_mission_locations
# 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)
# 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.')
raise Exception("Not enough missions available to fill the campaign on current settings. Please exclude fewer missions.")
while current_count > mission_count:
if set_cycle == 4:
set_cycle = 0
@@ -86,22 +76,25 @@ def filter_missions(world: MultiWorld, player: int) -> dict[str, list[str]]:
mission_pool = mission_pools[set_cycle]
set_cycle += 1
if len(mission_pool) == 1:
if all(len(other_pool) == 1 for other_pool in mission_pools):
raise Exception("Not enough missions available to fill the campaign on current settings. Please exclude fewer missions.")
continue
mission_pool.remove(world.random.choice(mission_pool))
current_count -= 1
return {
'no_build': mission_pools[0],
'easy': mission_pools[1],
'medium': mission_pools[2],
'hard': mission_pools[3]
"no_build": mission_pools[0],
"easy": mission_pools[1],
"medium": mission_pools[2],
"hard": mission_pools[3]
}
def filter_upgrades(inventory: list[Item], parent_item: Item or str):
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 inv_item.name in ALWAYS_USEFUL_ARMORY or not inv_item.name.endswith('(' + item_name + ')')
if item_table[inv_item.name].parent_item == item_name
]
@@ -122,34 +115,64 @@ class ValidInventory:
len(STARPORT_UNITS.intersection(self.logical_inventory)) > 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 self.progression_items.union(self.locked_items)}
self.logical_inventory = {
item.name for item in inventory + locked_items
if item.classification in (ItemClassification.progression, ItemClassification.progression_skip_balancing)
}
requirements = mission_requirements
mission_order_type = get_option_value(self.world, self.player, "mission_order")
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))
while len(inventory) + len(locked_items) > inventory_size:
if len(inventory) == 0:
raise Exception('Reduced item pool generation failed - not enough locations available to place items.')
# Select random item from removable items
item = self.world.random.choice(inventory)
def attempt_removal(item: Item) -> bool:
# If item can be removed and has associated items, remove them as well
inventory.remove(item)
# Only run logic checks when removing logic items
if item.name in self.logical_inventory:
self.logical_inventory.remove(item.name)
if all(requirement(self) for requirement in requirements):
# If item can be removed and is a unit, remove armory upgrades
# Some armory upgrades are kept regardless, as they remain logically relevant
if item.name in UPGRADABLE_ITEMS:
inventory = filter_upgrades(inventory, item)
else:
# If item cannot be removed, move it to locked items
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)
return False
return True
while len(inventory) + len(locked_items) > inventory_size:
if len(inventory) == 0:
raise Exception("Reduced item pool generation failed - not enough locations available to place items.")
# Select random item from removable items
item = self.world.random.choice(inventory)
# Cascade removals to associated items
if item in cascade_keys:
items_to_remove = self.cascade_removal_map[item]
transient_items = []
while len(items_to_remove) > 0:
item_to_remove = items_to_remove.pop()
if item_to_remove not in inventory:
continue
success = attempt_removal(item_to_remove)
if success:
transient_items.append(item_to_remove)
elif units_always_have_upgrades:
# Lock all associated items if any of them cannot be removed
transient_items += items_to_remove
locked_items += transient_items
self.logical_inventory = self.logical_inventory.union({
transient_item.name for transient_item in transient_items
if item.classification in (ItemClassification.progression, ItemClassification.progression_skip_balancing)
})
break
else:
attempt_removal(item)
return inventory + locked_items
def _read_logic(self):
@@ -176,7 +199,6 @@ class ValidInventory:
self.world = world
self.player = player
self.logical_inventory = set()
self.progression_items = set()
self.locked_items = locked_items[:]
self.existing_items = existing_items
self._read_logic()
@@ -185,9 +207,7 @@ class ValidInventory:
item_quantities: dict[str, int] = dict()
for item in item_pool:
item_info = item_table[item.name]
if item.classification == ItemClassification.progression:
self.progression_items.add(item)
if item_info.type == 'Upgrade':
if item_info.type == "Upgrade":
# All Upgrades are locked except for the final tier
if item.name not in item_quantities:
item_quantities[item.name] = 0
@@ -196,10 +216,19 @@ class ValidInventory:
self.locked_items.append(item)
else:
self.item_pool.append(item)
elif item_info.type == 'Goal':
elif item_info.type == "Goal":
locked_items.append(item)
elif item_info.type != 'Protoss' or has_protoss:
elif item_info.type != "Protoss" or has_protoss:
self.item_pool.append(item)
self.cascade_removal_map: dict[Item, list[Item]] = dict()
for item in self.item_pool + locked_items + existing_items:
if item.name in UPGRADABLE_ITEMS:
upgrades = get_item_upgrades(self.item_pool, item)
associated_items = [*upgrades, item]
self.cascade_removal_map[item] = associated_items
if get_option_value(world, player, "units_always_have_upgrades"):
for upgrade in upgrades:
self.cascade_removal_map[upgrade] = associated_items
def filter_items(world: MultiWorld, player: int, mission_req_table: dict[str, MissionInfo], location_cache: list[Location],

View File

@@ -4,13 +4,13 @@ from typing import List, Set, Tuple
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, \
basic_unit
get_basic_units
from .Locations import get_locations
from .Regions import create_regions
from .Options import sc2wol_options, get_option_value
from .Options import sc2wol_options, get_option_value, get_option_set_value
from .LogicMixin import SC2WoLLogic
from .PoolFilter import filter_missions, filter_items, filter_upgrades
from .MissionTables import starting_mission_locations, MissionInfo
from .PoolFilter import filter_missions, filter_items, get_item_upgrades
from .MissionTables import get_starting_mission_locations, MissionInfo
class Starcraft2WoLWebWorld(WebWorld):
@@ -132,12 +132,13 @@ def get_excluded_items(self: SC2WoLWorld, world: MultiWorld, player: int) -> Set
def assign_starter_items(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]) -> List[Item]:
non_local_items = world.non_local_items[player].value
if get_option_value(world, player, "early_unit"):
local_basic_unit = tuple(item for item in basic_unit if item not in non_local_items)
local_basic_unit = tuple(item for item in get_basic_units(world, player) if item not in non_local_items)
if not local_basic_unit:
raise Exception("At least one basic unit must be local")
# The first world should also be the starting world
first_mission = list(world.worlds[player].mission_req_table)[0]
starting_mission_locations = get_starting_mission_locations(world, player)
if first_mission in starting_mission_locations:
first_location = starting_mission_locations[first_mission]
elif first_mission == "In Utter Darkness":
@@ -174,8 +175,7 @@ def get_item_pool(world: MultiWorld, player: int, mission_req_table: dict[str, M
locked_items = []
# YAML items
locked_items_option = getattr(world, 'locked_items', [])
yaml_locked_items = locked_items_option[player].value
yaml_locked_items = get_option_set_value(world, player, 'locked_items')
for name, data in item_table.items():
if name not in excluded_items:
@@ -187,10 +187,14 @@ def get_item_pool(world: MultiWorld, player: int, mission_req_table: dict[str, M
pool.append(item)
existing_items = starter_items + [item.name 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:
pool = filter_upgrades(pool, item_name)
if item_name in existing_names:
continue
invalid_upgrades = get_item_upgrades(pool, item_name)
for invalid_upgrade in invalid_upgrades:
pool.remove(invalid_upgrade)
filtered_pool = filter_items(world, player, mission_req_table, location_cache, pool, existing_items, locked_items)
return filtered_pool