forked from mirror/Archipelago
SC2: Required Tactics and Unit Upgrade options, better connected item handling
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user