SC2: New Settings, Logic improvements (#1110)

* Switched mission item group to a list comprehension to fix missile shuffle errors

* Logic for reducing mission and item counts

* SC2: Piercing the Shroud/Maw of the Void requirements now DRY

* SC2: Logic for All-In, may need further refinement

* SC2: Additional mission orders and starting locations

* SC2: New Mission Order options for shorter campaigns and smaller item pools

* Using location table for hardcoded starter unit

* SC2: Options to curate random item pool and control early unit placement

* SC2: Proper All-In logic

* SC2: Grid, Mini Grid and Blitz mission orders

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

* SC2: Client compatibility with Grid settings

* SC2: Mission rando now uses world random

* SC2: Alternate final missions, new logic, fixes

* SC2: Handling alternate final missions, identifying final mission on client

* SC2: Minor changes to handle edge-case generation failures

* SC2: Removed invalid type hints for Python 3.8

* Revert "SC2: Removed invalid type hints for Python 3.8"

This reverts commit 7851b9f7a39396c8ee1d85d4e4e46e61e8dc80f6.

* SC2: Removed invalid type hints for Python 3.8

* SC2: Removed invalid type hints for Python 3.8

* SC2: Removed invalid type hints for Python 3.8

* SC2: Removed invalid type hints for Python 3.8

* SC2: Changed location loop to enumerate

* SC2: Passing category names through slot data

* SC2: Cleaned up unnecessary _create_items method

* SC2: Removed vestigial extra_locations field from MissionInfo

* SC2: Client backwards compatibility

* SC2: Fixed item generation issue where item is present in both locked and unlocked inventories

* SC2: Removed Missile Turret from defense rating on maps without air

* SC2: No logic locations point to same access rule

Co-authored-by: michaelasantiago <michael.alec.santiago@gmail.com>
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
This commit is contained in:
Magnemania 2022-10-26 06:24:54 -04:00 committed by GitHub
parent d5efc71344
commit 700fe8b75e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 872 additions and 279 deletions

View File

@ -155,7 +155,9 @@ class SC2Context(CommonContext):
items_handling = 0b111 items_handling = 0b111
difficulty = -1 difficulty = -1
all_in_choice = 0 all_in_choice = 0
mission_order = 0
mission_req_table: typing.Dict[str, MissionInfo] = {} mission_req_table: typing.Dict[str, MissionInfo] = {}
final_mission: int = 29
announcements = queue.Queue() announcements = queue.Queue()
sc2_run_task: typing.Optional[asyncio.Task] = None sc2_run_task: typing.Optional[asyncio.Task] = None
missions_unlocked: bool = False # allow launching missions ignoring requirements missions_unlocked: bool = False # allow launching missions ignoring requirements
@ -180,9 +182,15 @@ class SC2Context(CommonContext):
self.difficulty = args["slot_data"]["game_difficulty"] self.difficulty = args["slot_data"]["game_difficulty"]
self.all_in_choice = args["slot_data"]["all_in_map"] self.all_in_choice = args["slot_data"]["all_in_map"]
slot_req_table = args["slot_data"]["mission_req"] slot_req_table = args["slot_data"]["mission_req"]
# Maintaining backwards compatibility with older slot data
self.mission_req_table = { self.mission_req_table = {
mission: MissionInfo(**slot_req_table[mission]) for mission in slot_req_table mission: MissionInfo(
**{field: value for field, value in mission_info.items() if field in MissionInfo._fields}
)
for mission, mission_info in slot_req_table.items()
} }
self.mission_order = args["slot_data"].get("mission_order", 0)
self.final_mission = args["slot_data"].get("final_mission", 29)
self.build_location_to_mission_mapping() self.build_location_to_mission_mapping()
@ -304,7 +312,6 @@ class SC2Context(CommonContext):
self.refresh_from_launching = True self.refresh_from_launching = True
self.mission_panel.clear_widgets() self.mission_panel.clear_widgets()
if self.ctx.mission_req_table: if self.ctx.mission_req_table:
self.last_checked_locations = self.ctx.checked_locations.copy() self.last_checked_locations = self.ctx.checked_locations.copy()
self.first_check = False self.first_check = False
@ -322,17 +329,20 @@ class SC2Context(CommonContext):
for category in categories: for category in categories:
category_panel = MissionCategory() category_panel = MissionCategory()
if category.startswith('_'):
category_display_name = ''
else:
category_display_name = category
category_panel.add_widget( category_panel.add_widget(
Label(text=category, size_hint_y=None, height=50, outline_width=1)) Label(text=category_display_name, size_hint_y=None, height=50, outline_width=1))
for mission in categories[category]: for mission in categories[category]:
text: str = mission text: str = mission
tooltip: str = "" tooltip: str = ""
mission_id: int = self.ctx.mission_req_table[mission].id
# Map has uncollected locations # Map has uncollected locations
if mission in unfinished_missions: if mission in unfinished_missions:
text = f"[color=6495ED]{text}[/color]" text = f"[color=6495ED]{text}[/color]"
elif mission in available_missions: elif mission in available_missions:
text = f"[color=FFFFFF]{text}[/color]" text = f"[color=FFFFFF]{text}[/color]"
# Map requirements not met # Map requirements not met
@ -351,6 +361,16 @@ class SC2Context(CommonContext):
remaining_location_names: typing.List[str] = [ remaining_location_names: typing.List[str] = [
self.ctx.location_names[loc] for loc in self.ctx.locations_for_mission(mission) self.ctx.location_names[loc] for loc in self.ctx.locations_for_mission(mission)
if loc in self.ctx.missing_locations] if loc in self.ctx.missing_locations]
if mission_id == self.ctx.final_mission:
if mission in available_missions:
text = f"[color=FFBC95]{mission}[/color]"
else:
text = f"[color=D0C0BE]{mission}[/color]"
if tooltip:
tooltip += "\n"
tooltip += "Final Mission"
if remaining_location_names: if remaining_location_names:
if tooltip: if tooltip:
tooltip += "\n" tooltip += "\n"
@ -360,7 +380,7 @@ class SC2Context(CommonContext):
mission_button = MissionButton(text=text, size_hint_y=None, height=50) mission_button = MissionButton(text=text, size_hint_y=None, height=50)
mission_button.tooltip_text = tooltip mission_button.tooltip_text = tooltip
mission_button.bind(on_press=self.mission_callback) mission_button.bind(on_press=self.mission_callback)
self.mission_id_to_button[self.ctx.mission_req_table[mission].id] = mission_button self.mission_id_to_button[mission_id] = mission_button
category_panel.add_widget(mission_button) category_panel.add_widget(mission_button)
category_panel.add_widget(Label(text="")) category_panel.add_widget(Label(text=""))
@ -469,6 +489,9 @@ wol_default_categories = [
"Rebellion", "Rebellion", "Rebellion", "Rebellion", "Rebellion", "Prophecy", "Prophecy", "Prophecy", "Prophecy", "Rebellion", "Rebellion", "Rebellion", "Rebellion", "Rebellion", "Prophecy", "Prophecy", "Prophecy", "Prophecy",
"Char", "Char", "Char", "Char" "Char", "Char", "Char", "Char"
] ]
wol_default_category_names = [
"Mar Sara", "Colonist", "Artifact", "Covert", "Rebellion", "Prophecy", "Char"
]
def calculate_items(items: typing.List[NetworkItem]) -> typing.List[int]: def calculate_items(items: typing.List[NetworkItem]) -> typing.List[int]:
@ -586,7 +609,7 @@ class ArchipelagoBot(sc2.bot_ai.BotAI):
if self.can_read_game: if self.can_read_game:
if game_state & (1 << 1) and not self.mission_completed: if game_state & (1 << 1) and not self.mission_completed:
if self.mission_id != 29: if self.mission_id != self.ctx.final_mission:
print("Mission Completed") print("Mission Completed")
await self.ctx.send_msgs( await self.ctx.send_msgs(
[{"cmd": 'LocationChecks', [{"cmd": 'LocationChecks',
@ -742,13 +765,14 @@ def calc_available_missions(ctx: SC2Context, unlocks=None):
return available_missions return available_missions
def mission_reqs_completed(ctx: SC2Context, mission_name: str, missions_complete): def mission_reqs_completed(ctx: SC2Context, mission_name: str, missions_complete: int):
"""Returns a bool signifying if the mission has all requirements complete and can be done """Returns a bool signifying if the mission has all requirements complete and can be done
Arguments: Arguments:
ctx -- instance of SC2Context ctx -- instance of SC2Context
locations_to_check -- the mission string name to check locations_to_check -- the mission string name to check
missions_complete -- an int of how many missions have been completed missions_complete -- an int of how many missions have been completed
mission_path -- a list of missions that have already been checked
""" """
if len(ctx.mission_req_table[mission_name].required_world) >= 1: if len(ctx.mission_req_table[mission_name].required_world) >= 1:
# A check for when the requirements are being or'd # A check for when the requirements are being or'd
@ -766,7 +790,18 @@ def mission_reqs_completed(ctx: SC2Context, mission_name: str, missions_complete
else: else:
req_success = False req_success = False
# Grid-specific logic (to avoid long path checks and infinite recursion)
if ctx.mission_order in (3, 4):
if req_success:
return True
else:
if req_mission is ctx.mission_req_table[mission_name].required_world[-1]:
return False
else:
continue
# Recursively check required mission to see if it's requirements are met, in case !collect has been done # Recursively check required mission to see if it's requirements are met, in case !collect has been done
# Skipping recursive check on Grid settings to speed up checks and avoid infinite recursion
if not mission_reqs_completed(ctx, list(ctx.mission_req_table)[req_mission - 1], missions_complete): if not mission_reqs_completed(ctx, list(ctx.mission_req_table)[req_mission - 1], missions_complete):
if not ctx.mission_req_table[mission_name].or_requirements: if not ctx.mission_req_table[mission_name].or_requirements:
return False return False

View File

@ -1,5 +1,7 @@
from BaseClasses import Item, ItemClassification from BaseClasses import Item, ItemClassification, MultiWorld
import typing import typing
from .Options import get_option_value
from .MissionTables import vanilla_mission_req_table from .MissionTables import vanilla_mission_req_table
@ -9,6 +11,7 @@ class ItemData(typing.NamedTuple):
number: typing.Optional[int] number: typing.Optional[int]
classification: ItemClassification = ItemClassification.useful classification: ItemClassification = ItemClassification.useful
quantity: int = 1 quantity: int = 1
parent_item: str = None
class StarcraftWoLItem(Item): class StarcraftWoLItem(Item):
@ -48,51 +51,51 @@ item_table = {
"Progressive Ship Weapon": ItemData(105 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 8, quantity=3), "Progressive Ship Weapon": ItemData(105 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 8, quantity=3),
"Progressive Ship Armor": ItemData(106 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 10, quantity=3), "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), "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), "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), "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), "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), "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), "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), "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), "Orbital Command (Building)": ItemData(207 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 7),
"Stimpack (Marine)": ItemData(208 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 8), "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), "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), "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), "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), "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), "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), "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), "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), "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), "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), "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), "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), "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), "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), "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), "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), "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), "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), "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), "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), "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), "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), "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), "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), "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), "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), "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), "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), "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), "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), "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), "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), "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), "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), "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), "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), "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), "Missile Turret": ItemData(401 + SC2WOL_ITEM_ID_OFFSET, "Building", 1, classification=ItemClassification.progression),
@ -117,16 +120,16 @@ item_table = {
"Science Vessel": ItemData(607 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 7, classification=ItemClassification.progression), "Science Vessel": ItemData(607 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 7, classification=ItemClassification.progression),
"Tech Reactor": ItemData(608 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 8), "Tech Reactor": ItemData(608 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 8),
"Orbital Strike": ItemData(609 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 9), "Orbital Strike": ItemData(609 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 9),
"Shrike Turret": ItemData(610 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 10), "Shrike Turret": ItemData(610 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 10, parent_item="Bunker"),
"Fortified Bunker": ItemData(611 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 11), "Fortified Bunker": ItemData(611 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 11, parent_item="Bunker"),
"Planetary Fortress": ItemData(612 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 12), "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), "Predator": ItemData(614 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 14, classification=ItemClassification.filler),
"Hercules": ItemData(615 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 15, classification=ItemClassification.progression), "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), "Cellular Reactor": ItemData(616 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 16, classification=ItemClassification.filler),
"Regenerative Bio-Steel": ItemData(617 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 17, classification=ItemClassification.filler), "Regenerative Bio-Steel": ItemData(617 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 17, classification=ItemClassification.filler),
"Hive Mind Emulator": ItemData(618 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 18), "Hive Mind Emulator": ItemData(618 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 18, ItemClassification.progression),
"Psi Disrupter": ItemData(619 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 19, classification=ItemClassification.filler), "Psi Disrupter": ItemData(619 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 19, classification=ItemClassification.progression),
"Zealot": ItemData(700 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 0, classification=ItemClassification.progression), "Zealot": ItemData(700 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 0, classification=ItemClassification.progression),
"Stalker": ItemData(701 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 1, classification=ItemClassification.progression), "Stalker": ItemData(701 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 1, 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 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), "+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), "+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', 'Marine',
'Marauder', 'Marauder',
'Firebat', 'Firebat',
'Hellion', 'Hellion',
'Vulture' 'Vulture'
) }
advanced_basic_units = {
'Reaper',
'Goliath',
'Diamondback',
'Viking'
}
def get_basic_units(world: MultiWorld, player: int) -> typing.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 = {} item_name_groups = {}
for item, data in item_table.items(): for item, data in item_table.items():
@ -161,6 +182,22 @@ filler_items: typing.Tuple[str, ...] = (
'+15 Starting Vespene' '+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 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} data.code}
# Map type to expected int # Map type to expected int
@ -176,4 +213,5 @@ type_flaggroups: typing.Dict[str, int] = {
"Minerals": 8, "Minerals": 8,
"Vespene": 9, "Vespene": 9,
"Supply": 10, "Supply": 10,
"Goal": 11
} }

View File

@ -1,5 +1,6 @@
from typing import List, Tuple, Optional, Callable, NamedTuple from typing import List, Tuple, Optional, Callable, NamedTuple
from BaseClasses import MultiWorld from BaseClasses import MultiWorld
from .Options import get_option_value
from BaseClasses import Location from BaseClasses import Location
@ -19,6 +20,7 @@ class LocationData(NamedTuple):
def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[LocationData, ...]: 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 # 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] = [ location_table: List[LocationData] = [
LocationData("Liberation Day", "Liberation Day: Victory", SC2WOL_LOC_ID_OFFSET + 100), LocationData("Liberation Day", "Liberation Day: Victory", SC2WOL_LOC_ID_OFFSET + 100),
LocationData("Liberation Day", "Liberation Day: First Statue", SC2WOL_LOC_ID_OFFSET + 101), LocationData("Liberation Day", "Liberation Day: First Statue", SC2WOL_LOC_ID_OFFSET + 101),
@ -32,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, LocationData("The Outlaws", "The Outlaws: Rebel Base", SC2WOL_LOC_ID_OFFSET + 201,
lambda state: state._sc2wol_has_common_unit(world, player)), lambda state: state._sc2wol_has_common_unit(world, player)),
LocationData("Zero Hour", "Zero Hour: Victory", SC2WOL_LOC_ID_OFFSET + 300, 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: First Group Rescued", SC2WOL_LOC_ID_OFFSET + 301),
LocationData("Zero Hour", "Zero Hour: Second Group Rescued", SC2WOL_LOC_ID_OFFSET + 302, LocationData("Zero Hour", "Zero Hour: Second Group Rescued", SC2WOL_LOC_ID_OFFSET + 302,
lambda state: state._sc2wol_has_common_unit(world, player)), lambda state: state._sc2wol_has_common_unit(world, player)),
LocationData("Zero Hour", "Zero Hour: Third Group Rescued", SC2WOL_LOC_ID_OFFSET + 303, LocationData("Zero Hour", "Zero Hour: Third Group Rescued", SC2WOL_LOC_ID_OFFSET + 303,
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, LocationData("Evacuation", "Evacuation: Victory", SC2WOL_LOC_ID_OFFSET + 400,
lambda state: state._sc2wol_has_common_unit(world, player) and 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: First Chysalis", SC2WOL_LOC_ID_OFFSET + 401),
LocationData("Evacuation", "Evacuation: Second Chysalis", SC2WOL_LOC_ID_OFFSET + 402, LocationData("Evacuation", "Evacuation: Second Chysalis", SC2WOL_LOC_ID_OFFSET + 402,
lambda state: state._sc2wol_has_common_unit(world, player)), lambda state: state._sc2wol_has_common_unit(world, player)),
LocationData("Evacuation", "Evacuation: Third Chysalis", SC2WOL_LOC_ID_OFFSET + 403, LocationData("Evacuation", "Evacuation: Third Chysalis", SC2WOL_LOC_ID_OFFSET + 403,
lambda state: state._sc2wol_has_common_unit(world, player)), lambda state: state._sc2wol_has_common_unit(world, player)),
LocationData("Outbreak", "Outbreak: Victory", SC2WOL_LOC_ID_OFFSET + 500, 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, False) >= 4 and
(state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player))),
LocationData("Outbreak", "Outbreak: Left Infestor", SC2WOL_LOC_ID_OFFSET + 501, LocationData("Outbreak", "Outbreak: Left Infestor", SC2WOL_LOC_ID_OFFSET + 501,
lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)), lambda state: state._sc2wol_defense_rating(world, player, True, False) >= 2 and
(state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player))),
LocationData("Outbreak", "Outbreak: Right Infestor", SC2WOL_LOC_ID_OFFSET + 502, 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, False) >= 2 and
(state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player))),
LocationData("Safe Haven", "Safe Haven: Victory", SC2WOL_LOC_ID_OFFSET + 600, LocationData("Safe Haven", "Safe Haven: Victory", SC2WOL_LOC_ID_OFFSET + 600,
lambda state: state._sc2wol_has_common_unit(world, player) and 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)),
@ -66,38 +75,48 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
state._sc2wol_has_competent_anti_air(world, player)), state._sc2wol_has_competent_anti_air(world, player)),
LocationData("Haven's Fall", "Haven's Fall: Victory", SC2WOL_LOC_ID_OFFSET + 700, LocationData("Haven's Fall", "Haven's Fall: Victory", SC2WOL_LOC_ID_OFFSET + 700,
lambda state: state._sc2wol_has_common_unit(world, player) and 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, LocationData("Haven's Fall", "Haven's Fall: North Hive", SC2WOL_LOC_ID_OFFSET + 701,
lambda state: state._sc2wol_has_common_unit(world, player) and 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, LocationData("Haven's Fall", "Haven's Fall: East Hive", SC2WOL_LOC_ID_OFFSET + 702,
lambda state: state._sc2wol_has_common_unit(world, player) and 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, LocationData("Haven's Fall", "Haven's Fall: South Hive", SC2WOL_LOC_ID_OFFSET + 703,
lambda state: state._sc2wol_has_common_unit(world, player) and 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, LocationData("Smash and Grab", "Smash and Grab: Victory", SC2WOL_LOC_ID_OFFSET + 800,
lambda state: state._sc2wol_has_common_unit(world, player) and 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: 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: Second Relic", SC2WOL_LOC_ID_OFFSET + 802),
LocationData("Smash and Grab", "Smash and Grab: Third Relic", SC2WOL_LOC_ID_OFFSET + 803, LocationData("Smash and Grab", "Smash and Grab: 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, LocationData("Smash and Grab", "Smash and Grab: Fourth Relic", SC2WOL_LOC_ID_OFFSET + 804,
lambda state: state._sc2wol_has_common_unit(world, player) and 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, LocationData("The Dig", "The Dig: Victory", SC2WOL_LOC_ID_OFFSET + 900,
lambda state: state._sc2wol_has_common_unit(world, player) and lambda state: state._sc2wol_has_anti_air(world, player) and
state._sc2wol_has_anti_air(world, player) and state._sc2wol_defense_rating(world, player, False) >= 7),
state._sc2wol_has_heavy_defense(world, player)),
LocationData("The Dig", "The Dig: Left Relic", SC2WOL_LOC_ID_OFFSET + 901, 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, 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, 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, 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, LocationData("The Moebius Factor", "The Moebius Factor: South Rescue", SC2WOL_LOC_ID_OFFSET + 1003,
lambda state: state._sc2wol_able_to_rescue(world, player)), lambda state: state._sc2wol_able_to_rescue(world, player)),
LocationData("The Moebius Factor", "The Moebius Factor: Wall Rescue", SC2WOL_LOC_ID_OFFSET + 1004, LocationData("The Moebius Factor", "The Moebius Factor: Wall Rescue", SC2WOL_LOC_ID_OFFSET + 1004,
@ -109,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, LocationData("The Moebius Factor", "The Moebius Factor: Alive Inside Rescue", SC2WOL_LOC_ID_OFFSET + 1007,
lambda state: state._sc2wol_able_to_rescue(world, player)), lambda state: state._sc2wol_able_to_rescue(world, player)),
LocationData("The Moebius Factor", "The Moebius Factor: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1008, LocationData("The Moebius Factor", "The Moebius Factor: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1008,
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, LocationData("Supernova", "Supernova: Victory", SC2WOL_LOC_ID_OFFSET + 1100,
lambda state: state._sc2wol_beats_protoss_deathball(world, player)), lambda state: state._sc2wol_beats_protoss_deathball(world, player)),
LocationData("Supernova", "Supernova: West Relic", SC2WOL_LOC_ID_OFFSET + 1101), LocationData("Supernova", "Supernova: West Relic", SC2WOL_LOC_ID_OFFSET + 1101),
@ -119,37 +141,23 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
LocationData("Supernova", "Supernova: East Relic", SC2WOL_LOC_ID_OFFSET + 1104, LocationData("Supernova", "Supernova: East Relic", SC2WOL_LOC_ID_OFFSET + 1104,
lambda state: state._sc2wol_beats_protoss_deathball(world, player)), lambda state: state._sc2wol_beats_protoss_deathball(world, player)),
LocationData("Maw of the Void", "Maw of the Void: Victory", SC2WOL_LOC_ID_OFFSET + 1200, LocationData("Maw of the Void", "Maw of the Void: Victory", SC2WOL_LOC_ID_OFFSET + 1200,
lambda state: state.has('Battlecruiser', player) or lambda state: state._sc2wol_survives_rip_field(world, player)),
state._sc2wol_has_air(world, player) and
state._sc2wol_has_competent_anti_air(world, player) and
state.has('Science Vessel', 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: Landing Zone Cleared", SC2WOL_LOC_ID_OFFSET + 1201),
LocationData("Maw of the Void", "Maw of the Void: Expansion Prisoners", SC2WOL_LOC_ID_OFFSET + 1202, LocationData("Maw of the Void", "Maw of the Void: Expansion Prisoners", SC2WOL_LOC_ID_OFFSET + 1202,
lambda state: state.has('Battlecruiser', player) or lambda state: logic_level > 0 or state._sc2wol_survives_rip_field(world, player)),
state._sc2wol_has_air(world, player) and
state._sc2wol_has_competent_anti_air(world, player) and
state.has('Science Vessel', player)),
LocationData("Maw of the Void", "Maw of the Void: South Close Prisoners", SC2WOL_LOC_ID_OFFSET + 1203, LocationData("Maw of the Void", "Maw of the Void: South Close Prisoners", SC2WOL_LOC_ID_OFFSET + 1203,
lambda state: state.has('Battlecruiser', player) or lambda state: logic_level > 0 or state._sc2wol_survives_rip_field(world, player)),
state._sc2wol_has_air(world, player) and
state._sc2wol_has_competent_anti_air(world, player) and
state.has('Science Vessel', player)),
LocationData("Maw of the Void", "Maw of the Void: South Far Prisoners", SC2WOL_LOC_ID_OFFSET + 1204, LocationData("Maw of the Void", "Maw of the Void: South Far Prisoners", SC2WOL_LOC_ID_OFFSET + 1204,
lambda state: state.has('Battlecruiser', player) or lambda state: state._sc2wol_survives_rip_field(world, player)),
state._sc2wol_has_air(world, player) and
state._sc2wol_has_competent_anti_air(world, player) and
state.has('Science Vessel', player)),
LocationData("Maw of the Void", "Maw of the Void: North Prisoners", SC2WOL_LOC_ID_OFFSET + 1205, LocationData("Maw of the Void", "Maw of the Void: North Prisoners", SC2WOL_LOC_ID_OFFSET + 1205,
lambda state: state.has('Battlecruiser', player) or lambda state: state._sc2wol_survives_rip_field(world, player)),
state._sc2wol_has_air(world, player) and
state._sc2wol_has_competent_anti_air(world, player) and
state.has('Science Vessel', player)),
LocationData("Devil's Playground", "Devil's Playground: Victory", SC2WOL_LOC_ID_OFFSET + 1300, LocationData("Devil's Playground", "Devil's Playground: Victory", SC2WOL_LOC_ID_OFFSET + 1300,
lambda state: state._sc2wol_has_anti_air(world, player) and ( 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))), 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: Tosh's Miners", SC2WOL_LOC_ID_OFFSET + 1301),
LocationData("Devil's Playground", "Devil's Playground: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1302, LocationData("Devil's Playground", "Devil's Playground: 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, 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 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)),
@ -176,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: Mid Defiler", SC2WOL_LOC_ID_OFFSET + 1702),
LocationData("The Great Train Robbery", "The Great Train Robbery: South Defiler", SC2WOL_LOC_ID_OFFSET + 1703), LocationData("The Great Train Robbery", "The Great Train Robbery: South Defiler", SC2WOL_LOC_ID_OFFSET + 1703),
LocationData("Cutthroat", "Cutthroat: Victory", SC2WOL_LOC_ID_OFFSET + 1800, LocationData("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, LocationData("Cutthroat", "Cutthroat: Mira Han", SC2WOL_LOC_ID_OFFSET + 1801,
lambda state: state._sc2wol_has_common_unit(world, player)), lambda state: state._sc2wol_has_common_unit(world, player)),
LocationData("Cutthroat", "Cutthroat: North Relic", SC2WOL_LOC_ID_OFFSET + 1802, LocationData("Cutthroat", "Cutthroat: North Relic", SC2WOL_LOC_ID_OFFSET + 1802,
@ -208,40 +217,44 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
lambda state: state._sc2wol_has_competent_comp(world, player)), lambda state: state._sc2wol_has_competent_comp(world, player)),
LocationData("Media Blitz", "Media Blitz: Science Facility", SC2WOL_LOC_ID_OFFSET + 2004), LocationData("Media Blitz", "Media Blitz: Science Facility", SC2WOL_LOC_ID_OFFSET + 2004),
LocationData("Piercing the Shroud", "Piercing the Shroud: Victory", SC2WOL_LOC_ID_OFFSET + 2100, LocationData("Piercing the Shroud", "Piercing the Shroud: Victory", SC2WOL_LOC_ID_OFFSET + 2100,
lambda state: state.has_any({'Combat Shield (Marine)', 'Stabilizer Medpacks (Medic)'}, player)), lambda state: state._sc2wol_has_mm_upgrade(world, player)),
LocationData("Piercing the Shroud", "Piercing the Shroud: Holding Cell Relic", SC2WOL_LOC_ID_OFFSET + 2101), LocationData("Piercing the Shroud", "Piercing the Shroud: Holding Cell Relic", SC2WOL_LOC_ID_OFFSET + 2101),
LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk Relic", SC2WOL_LOC_ID_OFFSET + 2102, LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk Relic", SC2WOL_LOC_ID_OFFSET + 2102,
lambda state: state.has_any({'Combat Shield (Marine)', 'Stabilizer Medpacks (Medic)'}, player)), lambda state: state._sc2wol_has_mm_upgrade(world, player)),
LocationData("Piercing the Shroud", "Piercing the Shroud: First Escape Relic", SC2WOL_LOC_ID_OFFSET + 2103, LocationData("Piercing the Shroud", "Piercing the Shroud: First Escape Relic", SC2WOL_LOC_ID_OFFSET + 2103,
lambda state: state.has_any({'Combat Shield (Marine)', 'Stabilizer Medpacks (Medic)'}, player)), lambda state: state._sc2wol_has_mm_upgrade(world, player)),
LocationData("Piercing the Shroud", "Piercing the Shroud: Second Escape Relic", SC2WOL_LOC_ID_OFFSET + 2104, LocationData("Piercing the Shroud", "Piercing the Shroud: Second Escape Relic", SC2WOL_LOC_ID_OFFSET + 2104,
lambda state: state.has_any({'Combat Shield (Marine)', 'Stabilizer Medpacks (Medic)'}, player)), lambda state: state._sc2wol_has_mm_upgrade(world, player)),
LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk ", SC2WOL_LOC_ID_OFFSET + 2105, LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk ", SC2WOL_LOC_ID_OFFSET + 2105,
lambda state: state.has_any({'Combat Shield (Marine)', 'Stabilizer Medpacks (Medic)'}, player)), lambda state: state._sc2wol_has_mm_upgrade(world, player)),
LocationData("Whispers of Doom", "Whispers of Doom: Victory", SC2WOL_LOC_ID_OFFSET + 2200), LocationData("Whispers of Doom", "Whispers of Doom: Victory", SC2WOL_LOC_ID_OFFSET + 2200),
LocationData("Whispers of Doom", "Whispers of Doom: First Hatchery", SC2WOL_LOC_ID_OFFSET + 2201), LocationData("Whispers of Doom", "Whispers of Doom: First Hatchery", SC2WOL_LOC_ID_OFFSET + 2201),
LocationData("Whispers of Doom", "Whispers of Doom: Second Hatchery", SC2WOL_LOC_ID_OFFSET + 2202), LocationData("Whispers of Doom", "Whispers of Doom: Second Hatchery", SC2WOL_LOC_ID_OFFSET + 2202),
LocationData("Whispers of Doom", "Whispers of Doom: Third Hatchery", SC2WOL_LOC_ID_OFFSET + 2203), LocationData("Whispers of Doom", "Whispers of Doom: Third Hatchery", SC2WOL_LOC_ID_OFFSET + 2203),
LocationData("A Sinister Turn", "A Sinister Turn: Victory", SC2WOL_LOC_ID_OFFSET + 2300, LocationData("A Sinister Turn", "A Sinister Turn: Victory", SC2WOL_LOC_ID_OFFSET + 2300,
lambda state: state._sc2wol_has_protoss_medium_units(world, player)), 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: Robotics Facility", SC2WOL_LOC_ID_OFFSET + 2301,
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: 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, LocationData("A Sinister Turn", "A Sinister Turn: Templar Archives", SC2WOL_LOC_ID_OFFSET + 2303,
lambda state: state._sc2wol_has_protoss_common_units(world, player)), 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, 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: Close Obelisk", SC2WOL_LOC_ID_OFFSET + 2401),
LocationData("Echoes of the Future", "Echoes of the Future: West Obelisk", SC2WOL_LOC_ID_OFFSET + 2402, 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: Defeat", SC2WOL_LOC_ID_OFFSET + 2500),
LocationData("In Utter Darkness", "In Utter Darkness: Protoss Archive", SC2WOL_LOC_ID_OFFSET + 2501, LocationData("In Utter Darkness", "In Utter Darkness: Protoss Archive", SC2WOL_LOC_ID_OFFSET + 2501,
lambda state: state._sc2wol_has_protoss_medium_units(world, player)), lambda state: state._sc2wol_has_protoss_medium_units(world, player)),
LocationData("In Utter Darkness", "In Utter Darkness: Kills", SC2WOL_LOC_ID_OFFSET + 2502, LocationData("In Utter Darkness", "In Utter Darkness: Kills", SC2WOL_LOC_ID_OFFSET + 2502,
lambda state: state._sc2wol_has_protoss_common_units(world, player)), lambda state: state._sc2wol_has_protoss_common_units(world, player)),
LocationData("Gates of Hell", "Gates of Hell: Victory", SC2WOL_LOC_ID_OFFSET + 2600, 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) > 6),
LocationData("Gates of Hell", "Gates of Hell: Large Army", SC2WOL_LOC_ID_OFFSET + 2601, 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) > 6),
LocationData("Belly of the Beast", "Belly of the Beast: Victory", SC2WOL_LOC_ID_OFFSET + 2700), LocationData("Belly of the Beast", "Belly of the Beast: 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: First Charge", SC2WOL_LOC_ID_OFFSET + 2701),
LocationData("Belly of the Beast", "Belly of the Beast: Second Charge", SC2WOL_LOC_ID_OFFSET + 2702), LocationData("Belly of the Beast", "Belly of the Beast: Second Charge", SC2WOL_LOC_ID_OFFSET + 2702),
@ -258,15 +271,19 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
lambda state: state._sc2wol_has_competent_comp(world, player)), lambda state: state._sc2wol_has_competent_comp(world, player)),
LocationData("Shatter the Sky", "Shatter the Sky: Leviathan", SC2WOL_LOC_ID_OFFSET + 2805, LocationData("Shatter the Sky", "Shatter the Sky: Leviathan", SC2WOL_LOC_ID_OFFSET + 2805,
lambda state: state._sc2wol_has_competent_comp(world, player)), lambda state: state._sc2wol_has_competent_comp(world, player)),
LocationData("All-In", "All-In: Victory", None) LocationData("All-In", "All-In: Victory", None,
lambda state: state._sc2wol_final_mission_requirements(world, player))
] ]
beat_events = [] beat_events = []
for location_data in location_table: for i, location_data in enumerate(location_table):
# Removing all item-based logic on No Logic
if logic_level == 2:
location_table[i] = location_data._replace(rule=Location.access_rule)
# Generating Beat event locations
if location_data.name.endswith((": Victory", ": Defeat")): if location_data.name.endswith((": Victory", ": Defeat")):
beat_events.append( beat_events.append(
location_data._replace(name="Beat " + location_data.name.rsplit(": ", 1)[0], code=None) location_data._replace(name="Beat " + location_data.name.rsplit(": ", 1)[0], code=None)
) )
return tuple(location_table + beat_events) return tuple(location_table + beat_events)

View File

@ -1,31 +1,43 @@
from BaseClasses import MultiWorld from BaseClasses import MultiWorld
from worlds.AutoWorld import LogicMixin from worlds.AutoWorld import LogicMixin
from .Options import get_option_value
from .Items import get_basic_units, defense_ratings, zerg_defense_ratings
class SC2WoLLogic(LogicMixin): class SC2WoLLogic(LogicMixin):
def _sc2wol_has_common_unit(self, world: MultiWorld, player: int) -> bool: 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_bunker_unit(self, world: MultiWorld, player: int) -> bool:
return self.has_any({'Marine', 'Marauder'}, player)
def _sc2wol_has_air(self, world: MultiWorld, player: int) -> bool: def _sc2wol_has_air(self, world: MultiWorld, player: int) -> bool:
return self.has_any({'Viking', 'Wraith', 'Banshee'}, player) or \ return self.has_any({'Viking', 'Wraith', 'Banshee'}, player) or get_option_value(world, player, 'required_tactics') > 0 \
self.has_any({'Hercules', 'Medivac'}, player) and self._sc2wol_has_common_unit(world, player) 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: 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: 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) 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: 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', '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'}, player)
def _sc2wol_has_heavy_defense(self, world: MultiWorld, player: int) -> bool: def _sc2wol_defense_rating(self, world: MultiWorld, player: int, zerg_enemy: bool, air_enemy: bool = True) -> bool:
return (self.has_any({'Siege Tank', 'Vulture'}, player) or defense_score = sum((defense_ratings[item] for item in defense_ratings if self.has(item, player)))
self.has('Bunker', player) and self._sc2wol_has_bunker_unit(world, player)) and \ if self.has_any({'Marine', 'Marauder'}, player) and self.has('Bunker', player):
self._sc2wol_has_anti_air(world, 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
if not air_enemy and self.has('Missile Turret', player):
defense_score -= defense_ratings['Missile Turret']
# 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: def _sc2wol_has_competent_comp(self, world: MultiWorld, player: int) -> bool:
return (self.has('Marine', player) or self.has('Marauder', player) and return (self.has('Marine', player) or self.has('Marauder', player) and
@ -35,25 +47,50 @@ class SC2WoLLogic(LogicMixin):
self.has('Siege Tank', player) and self._sc2wol_has_competent_anti_air(world, player) 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: def _sc2wol_has_train_killers(self, world: MultiWorld, player: int) -> bool:
return (self.has_any({'Siege Tank', 'Diamondback'}, player) or return (self.has_any({'Siege Tank', 'Diamondback', 'Marauder'}, player) or get_option_value(world, player, 'required_tactics') > 0
self.has_all({'Reaper', "G-4 Clusterbomb"}, player) or self.has_all({'Spectre', 'Psionic Lash'}, player) and self.has_all({'Reaper', "G-4 Clusterbomb"}, player) or self.has_all({'Spectre', 'Psionic Lash'}, player))
or self.has('Marauders', player))
def _sc2wol_able_to_rescue(self, world: MultiWorld, player: int) -> bool: 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: 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: def _sc2wol_has_protoss_medium_units(self, world: MultiWorld, player: int) -> bool:
return self._sc2wol_has_protoss_common_units(world, player) and \ 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: 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 \ return self.has_any({'Banshee', 'Battlecruiser'}, player) and self._sc2wol_has_competent_anti_air or \
self._sc2wol_has_competent_comp(world, player) and self._sc2wol_has_air_anti_air(world, player) self._sc2wol_has_competent_comp(world, player) and self._sc2wol_has_air_anti_air(world, player)
def _sc2wol_has_mm_upgrade(self, world: MultiWorld, player: int) -> bool:
return self.has_any({"Combat Shield (Marine)", "Stabilizer Medpacks (Medic)"}, player)
def _sc2wol_survives_rip_field(self, world: MultiWorld, player: int) -> bool:
return self.has("Battlecruiser", player) or \
self._sc2wol_has_air(world, player) and \
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):
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
if self.has_any({'Battlecruiser', 'Banshee'}, player):
defense_rating += 3
return defense_rating >= 12 and beats_kerrigan
else:
# Air
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: def _sc2wol_cleared_missions(self, world: MultiWorld, player: int, mission_count: int) -> bool:
return self.has_group("Missions", player, mission_count) return self.has_group("Missions", player, mission_count)

View File

@ -1,4 +1,7 @@
from typing import NamedTuple, Dict, List from typing import NamedTuple, Dict, List, Set
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", no_build_regions_list = ["Liberation Day", "Breakout", "Ghost of a Chance", "Piercing the Shroud", "Whispers of Doom",
"Belly of the Beast"] "Belly of the Beast"]
@ -12,7 +15,6 @@ hard_regions_list = ["Maw of the Void", "Engine of Destruction", "In Utter Darkn
class MissionInfo(NamedTuple): class MissionInfo(NamedTuple):
id: int id: int
extra_locations: int
required_world: List[int] required_world: List[int]
category: str category: str
number: int = 0 # number of worlds need beaten number: int = 0 # number of worlds need beaten
@ -62,38 +64,156 @@ vanilla_shuffle_order = [
FillMission("all_in", [26, 27], "Char", completion_critical=True, or_requirements=True) FillMission("all_in", [26, 27], "Char", completion_critical=True, or_requirements=True)
] ]
mini_campaign_order = [
FillMission("no_build", [-1], "Mar Sara", completion_critical=True),
FillMission("easy", [0], "Colonist"),
FillMission("medium", [1], "Colonist"),
FillMission("medium", [0], "Artifact", completion_critical=True),
FillMission("medium", [3], "Artifact", number=4, completion_critical=True),
FillMission("hard", [4], "Artifact", number=8, completion_critical=True),
FillMission("medium", [0], "Covert", number=2),
FillMission("hard", [6], "Covert"),
FillMission("medium", [0], "Rebellion", number=3),
FillMission("hard", [8], "Rebellion"),
FillMission("medium", [4], "Prophecy"),
FillMission("hard", [10], "Prophecy"),
FillMission("hard", [5], "Char", completion_critical=True),
FillMission("hard", [5], "Char", completion_critical=True),
FillMission("all_in", [12, 13], "Char", completion_critical=True, or_requirements=True)
]
gauntlet_order = [
FillMission("no_build", [-1], "I", completion_critical=True),
FillMission("easy", [0], "II", completion_critical=True),
FillMission("medium", [1], "III", completion_critical=True),
FillMission("medium", [2], "IV", completion_critical=True),
FillMission("hard", [3], "V", completion_critical=True),
FillMission("hard", [4], "VI", completion_critical=True),
FillMission("all_in", [5], "Final", completion_critical=True)
]
grid_order = [
FillMission("no_build", [-1], "_1"),
FillMission("medium", [0], "_1"),
FillMission("medium", [1, 6, 3], "_1", or_requirements=True),
FillMission("hard", [2, 7], "_1", or_requirements=True),
FillMission("easy", [0], "_2"),
FillMission("medium", [1, 4], "_2", or_requirements=True),
FillMission("hard", [2, 5, 10, 7], "_2", or_requirements=True),
FillMission("hard", [3, 6, 11], "_2", or_requirements=True),
FillMission("medium", [4, 9, 12], "_3", or_requirements=True),
FillMission("hard", [5, 8, 10, 13], "_3", or_requirements=True),
FillMission("hard", [6, 9, 11, 14], "_3", or_requirements=True),
FillMission("hard", [7, 10], "_3", or_requirements=True),
FillMission("hard", [8, 13], "_4", or_requirements=True),
FillMission("hard", [9, 12, 14], "_4", or_requirements=True),
FillMission("hard", [10, 13], "_4", or_requirements=True),
FillMission("all_in", [11, 14], "_4", or_requirements=True)
]
mini_grid_order = [
FillMission("no_build", [-1], "_1"),
FillMission("medium", [0], "_1"),
FillMission("medium", [1, 5], "_1", or_requirements=True),
FillMission("easy", [0], "_2"),
FillMission("medium", [1, 3], "_2", or_requirements=True),
FillMission("hard", [2, 4], "_2", or_requirements=True),
FillMission("medium", [3, 7], "_3", or_requirements=True),
FillMission("hard", [4, 6], "_3", or_requirements=True),
FillMission("all_in", [5, 7], "_3", or_requirements=True)
]
blitz_order = [
FillMission("no_build", [-1], "I"),
FillMission("easy", [-1], "I"),
FillMission("medium", [0, 1], "II", number=1, or_requirements=True),
FillMission("medium", [0, 1], "II", number=1, or_requirements=True),
FillMission("medium", [0, 1], "III", number=2, or_requirements=True),
FillMission("medium", [0, 1], "III", number=2, or_requirements=True),
FillMission("hard", [0, 1], "IV", number=3, or_requirements=True),
FillMission("hard", [0, 1], "IV", number=3, or_requirements=True),
FillMission("hard", [0, 1], "V", number=4, or_requirements=True),
FillMission("hard", [0, 1], "V", number=4, or_requirements=True),
FillMission("hard", [0, 1], "Final", number=5, or_requirements=True),
FillMission("all_in", [0, 1], "Final", number=5, or_requirements=True)
]
mission_orders = [vanilla_shuffle_order, vanilla_shuffle_order, mini_campaign_order, grid_order, mini_grid_order, blitz_order, gauntlet_order]
vanilla_mission_req_table = { vanilla_mission_req_table = {
"Liberation Day": MissionInfo(1, 7, [], "Mar Sara", completion_critical=True), "Liberation Day": MissionInfo(1, [], "Mar Sara", completion_critical=True),
"The Outlaws": MissionInfo(2, 2, [1], "Mar Sara", completion_critical=True), "The Outlaws": MissionInfo(2, [1], "Mar Sara", completion_critical=True),
"Zero Hour": MissionInfo(3, 4, [2], "Mar Sara", completion_critical=True), "Zero Hour": MissionInfo(3, [2], "Mar Sara", completion_critical=True),
"Evacuation": MissionInfo(4, 4, [3], "Colonist"), "Evacuation": MissionInfo(4, [3], "Colonist"),
"Outbreak": MissionInfo(5, 3, [4], "Colonist"), "Outbreak": MissionInfo(5, [4], "Colonist"),
"Safe Haven": MissionInfo(6, 4, [5], "Colonist", number=7), "Safe Haven": MissionInfo(6, [5], "Colonist", number=7),
"Haven's Fall": MissionInfo(7, 4, [5], "Colonist", number=7), "Haven's Fall": MissionInfo(7, [5], "Colonist", number=7),
"Smash and Grab": MissionInfo(8, 5, [3], "Artifact", completion_critical=True), "Smash and Grab": MissionInfo(8, [3], "Artifact", completion_critical=True),
"The Dig": MissionInfo(9, 4, [8], "Artifact", number=8, completion_critical=True), "The Dig": MissionInfo(9, [8], "Artifact", number=8, completion_critical=True),
"The Moebius Factor": MissionInfo(10, 9, [9], "Artifact", number=11, completion_critical=True), "The Moebius Factor": MissionInfo(10, [9], "Artifact", number=11, completion_critical=True),
"Supernova": MissionInfo(11, 5, [10], "Artifact", number=14, completion_critical=True), "Supernova": MissionInfo(11, [10], "Artifact", number=14, completion_critical=True),
"Maw of the Void": MissionInfo(12, 6, [11], "Artifact", completion_critical=True), "Maw of the Void": MissionInfo(12, [11], "Artifact", completion_critical=True),
"Devil's Playground": MissionInfo(13, 3, [3], "Covert", number=4), "Devil's Playground": MissionInfo(13, [3], "Covert", number=4),
"Welcome to the Jungle": MissionInfo(14, 4, [13], "Covert"), "Welcome to the Jungle": MissionInfo(14, [13], "Covert"),
"Breakout": MissionInfo(15, 3, [14], "Covert", number=8), "Breakout": MissionInfo(15, [14], "Covert", number=8),
"Ghost of a Chance": MissionInfo(16, 6, [14], "Covert", number=8), "Ghost of a Chance": MissionInfo(16, [14], "Covert", number=8),
"The Great Train Robbery": MissionInfo(17, 4, [3], "Rebellion", number=6), "The Great Train Robbery": MissionInfo(17, [3], "Rebellion", number=6),
"Cutthroat": MissionInfo(18, 5, [17], "Rebellion"), "Cutthroat": MissionInfo(18, [17], "Rebellion"),
"Engine of Destruction": MissionInfo(19, 6, [18], "Rebellion"), "Engine of Destruction": MissionInfo(19, [18], "Rebellion"),
"Media Blitz": MissionInfo(20, 5, [19], "Rebellion"), "Media Blitz": MissionInfo(20, [19], "Rebellion"),
"Piercing the Shroud": MissionInfo(21, 6, [20], "Rebellion"), "Piercing the Shroud": MissionInfo(21, [20], "Rebellion"),
"Whispers of Doom": MissionInfo(22, 4, [9], "Prophecy"), "Whispers of Doom": MissionInfo(22, [9], "Prophecy"),
"A Sinister Turn": MissionInfo(23, 4, [22], "Prophecy"), "A Sinister Turn": MissionInfo(23, [22], "Prophecy"),
"Echoes of the Future": MissionInfo(24, 3, [23], "Prophecy"), "Echoes of the Future": MissionInfo(24, [23], "Prophecy"),
"In Utter Darkness": MissionInfo(25, 3, [24], "Prophecy"), "In Utter Darkness": MissionInfo(25, [24], "Prophecy"),
"Gates of Hell": MissionInfo(26, 2, [12], "Char", completion_critical=True), "Gates of Hell": MissionInfo(26, [12], "Char", completion_critical=True),
"Belly of the Beast": MissionInfo(27, 4, [26], "Char", completion_critical=True), "Belly of the Beast": MissionInfo(27, [26], "Char", completion_critical=True),
"Shatter the Sky": MissionInfo(28, 5, [26], "Char", completion_critical=True), "Shatter the Sky": MissionInfo(28, [26], "Char", completion_critical=True),
"All-In": MissionInfo(29, -1, [27, 28], "Char", completion_critical=True, or_requirements=True) "All-In": MissionInfo(29, [27, 28], "Char", completion_critical=True, or_requirements=True)
} }
lookup_id_to_mission: Dict[int, str] = { lookup_id_to_mission: Dict[int, str] = {
data.id: mission_name for mission_name, data in vanilla_mission_req_table.items() if data.id} data.id: mission_name for mission_name, data in vanilla_mission_req_table.items() if data.id}
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"
}
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, '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:
# 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

@ -1,6 +1,6 @@
from typing import Dict from typing import Dict
from BaseClasses import MultiWorld from BaseClasses import MultiWorld
from Options import Choice, Option, DefaultOnToggle from Options import Choice, Option, Toggle, DefaultOnToggle, ItemSet, OptionSet, Range
class GameDifficulty(Choice): class GameDifficulty(Choice):
@ -36,25 +36,75 @@ class AllInMap(Choice):
class MissionOrder(Choice): class MissionOrder(Choice):
"""Determines the order the missions are played in. """Determines the order the missions are played in. The last three mission orders end in a random mission.
Vanilla: Keeps the standard mission order and branching from the WoL Campaign. Vanilla (29): 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.""" 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 (12): 12 random missions that open up very quickly. Complete the bottom-right mission to win.
Gauntlet (7): Linear series of 7 random missions to complete the campaign."""
display_name = "Mission Order" display_name = "Mission Order"
option_vanilla = 0 option_vanilla = 0
option_vanilla_shuffled = 1 option_vanilla_shuffled = 1
option_mini_campaign = 2
option_grid = 3
option_mini_grid = 4
option_blitz = 5
option_gauntlet = 6
class ShuffleProtoss(DefaultOnToggle): class ShuffleProtoss(DefaultOnToggle):
"""Determines if the 3 protoss missions are included in the shuffle if Vanilla Shuffled is enabled. If this is """Determines if the 3 protoss missions are included in the shuffle if Vanilla mission order is not enabled.
not the 3 protoss missions will stay in their vanilla order in the mission order making them optional to complete If turned off with Vanilla Shuffled, the 3 protoss missions will be in their normal position on the Prophecy chain if not shuffled.
the game.""" 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" display_name = "Shuffle Protoss Missions"
class RelegateNoBuildMissions(DefaultOnToggle): class ShuffleNoBuild(DefaultOnToggle):
"""If enabled, all no build missions besides the needed first one will be placed at the end of optional routes so """Determines if the 5 no-build missions are included in the shuffle if Vanilla mission order is not enabled.
that none of them become required to complete the game. Only takes effect if mission order is not set to vanilla.""" 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.
display_name = "Relegate No-Build Missions" If turned off with reduced mission settings, the 5 no-build missions will not appear."""
display_name = "Shuffle No-Build Missions"
class EarlyUnit(DefaultOnToggle):
"""Guarantees that the first mission will contain a unit."""
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 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
option_advanced = 1
option_no_logic = 2
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"
class LockedItems(ItemSet):
"""Guarantees that these items will be unlockable"""
display_name = "Locked Items"
class ExcludedItems(ItemSet):
"""Guarantees that these items will not be unlockable"""
display_name = "Excluded Items"
class ExcludedMissions(OptionSet):
"""Guarantees that these missions will not appear in the campaign
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 # noinspection PyTypeChecker
@ -65,14 +115,29 @@ sc2wol_options: Dict[str, Option] = {
"all_in_map": AllInMap, "all_in_map": AllInMap,
"mission_order": MissionOrder, "mission_order": MissionOrder,
"shuffle_protoss": ShuffleProtoss, "shuffle_protoss": ShuffleProtoss,
"relegate_no_build": RelegateNoBuildMissions "shuffle_no_build": ShuffleNoBuild,
"early_unit": EarlyUnit,
"required_tactics": RequiredTactics,
"units_always_have_upgrades": UnitsAlwaysHaveUpgrades,
"locked_items": LockedItems,
"excluded_items": ExcludedItems,
"excluded_missions": ExcludedMissions
} }
def get_option_value(world: MultiWorld, player: int, name: str) -> int: def get_option_value(world: MultiWorld, player: int, name: str) -> int:
option = getattr(world, name, None) option = getattr(world, name, None)
if option == None: if option is None:
return 0 return 0
return int(option[player].value) return int(option[player].value)
def get_option_set_value(world: MultiWorld, player: int, name: str) -> set:
option = getattr(world, name, None)
if option is None:
return set()
return option[player].value

257
worlds/sc2wol/PoolFilter.py Normal file
View File

@ -0,0 +1,257 @@
from typing import Callable, Dict, List, Set
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, alt_final_mission_locations
from .Options import get_option_value, get_option_set_value
from .LogicMixin import SC2WoLLogic
# Items with associated upgrades
UPGRADABLE_ITEMS = [
"Marine", "Medic", "Firebat", "Marauder", "Reaper", "Ghost", "Spectre",
"Hellion", "Vulture", "Goliath", "Diamondback", "Siege Tank", "Thor",
"Medivac", "Wraith", "Viking", "Banshee", "Battlecruiser",
"Bunker", "Missile Turret"
]
BARRACKS_UNITS = {"Marine", "Medic", "Firebat", "Marauder", "Reaper", "Ghost", "Spectre"}
FACTORY_UNITS = {"Hellion", "Vulture", "Goliath", "Diamondback", "Siege Tank", "Thor", "Predator"}
STARPORT_UNITS = {"Medivac", "Wraith", "Viking", "Banshee", "Battlecruiser", "Hercules", "Science Vessel", "Raven"}
PROTOSS_REGIONS = {"A Sinister Turn", "Echoes of the Future", "In Utter Darkness"}
def filter_missions(world: MultiWorld, player: int) -> Dict[str, List[str]]:
"""
Returns a semi-randomly pruned tuple of no-build, easy, medium, and hard mission sets
"""
mission_order_type = get_option_value(world, player, "mission_order")
shuffle_protoss = get_option_value(world, player, "shuffle_protoss")
excluded_missions = 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))
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[:],
"all_in": ["All-In"]
}
mission_pools = [
[],
easy_regions_list,
medium_regions_list,
hard_regions_list
]
# Omitting Protoss missions if not shuffling protoss
if not shuffle_protoss:
excluded_missions = excluded_missions.union(PROTOSS_REGIONS)
# Replacing All-In on low mission counts
if mission_count < 14:
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])]
# If the first mission is a build mission, there may not be enough locations to reach Outbreak as a second mission
if not get_option_value(world, player, 'shuffle_no_build'):
# Swapping Outbreak and The Great Train Robbery
if "Outbreak" in mission_pools[1]:
mission_pools[1].remove("Outbreak")
mission_pools[2].append("Outbreak")
if "The Great Train Robbery" in mission_pools[2]:
mission_pools[2].remove("The Great Train Robbery")
mission_pools[1].append("The Great Train Robbery")
# Removing random missions from each difficulty set in a cycle
set_cycle = 0
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.")
while current_count > mission_count:
if set_cycle == 4:
set_cycle = 0
# Must contain at least one mission per set
mission_pool = mission_pools[set_cycle]
if len(mission_pool) <= 1:
if all(len(mission_pool) <= 1 for mission_pool in mission_pools):
raise Exception("Not enough missions available to fill the campaign on current settings. Please exclude fewer missions.")
else:
mission_pool.remove(world.random.choice(mission_pool))
current_count -= 1
set_cycle += 1
return {
"no_build": mission_pools[0],
"easy": mission_pools[1],
"medium": mission_pools[2],
"hard": mission_pools[3],
"all_in": [final_mission]
}
def get_item_upgrades(inventory: List[Item], parent_item: Item or str):
item_name = parent_item.name if isinstance(parent_item, Item) else parent_item
return [
inv_item for inv_item in inventory
if item_table[inv_item.name].parent_item == item_name
]
class ValidInventory:
def has(self, item: str, player: int):
return item in self.logical_inventory
def has_any(self, items: Set[str], player: int):
return any(item in self.logical_inventory for item in items)
def has_all(self, items: Set[str], player: int):
return all(item in self.logical_inventory for item in items)
def has_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 + 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")
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
inventory.remove(item)
# Only run logic checks when removing logic items
if item.name in self.logical_inventory:
self.logical_inventory.remove(item.name)
if not all(requirement(self) for requirement in 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
for transient_item in transient_items:
if transient_item not in inventory and transient_item not in locked_items:
locked_items += transient_item
if transient_item.classification in (ItemClassification.progression, ItemClassification.progression_skip_balancing):
self.logical_inventory.add(transient_item.name)
break
else:
attempt_removal(item)
return inventory + locked_items
def _read_logic(self):
self._sc2wol_has_common_unit = lambda world, player: SC2WoLLogic._sc2wol_has_common_unit(self, world, player)
self._sc2wol_has_air = lambda world, player: SC2WoLLogic._sc2wol_has_air(self, world, player)
self._sc2wol_has_air_anti_air = lambda world, player: SC2WoLLogic._sc2wol_has_air_anti_air(self, world, player)
self._sc2wol_has_competent_anti_air = lambda world, player: SC2WoLLogic._sc2wol_has_competent_anti_air(self, world, player)
self._sc2wol_has_anti_air = lambda world, player: SC2WoLLogic._sc2wol_has_anti_air(self, world, player)
self._sc2wol_defense_rating = lambda world, player, zerg_enemy, air_enemy=False: SC2WoLLogic._sc2wol_defense_rating(self, world, player, zerg_enemy, air_enemy)
self._sc2wol_has_competent_comp = lambda world, player: SC2WoLLogic._sc2wol_has_competent_comp(self, world, player)
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)
self._sc2wol_beats_protoss_deathball = lambda world, player: SC2WoLLogic._sc2wol_beats_protoss_deathball(self, world, player)
self._sc2wol_survives_rip_field = lambda world, player: SC2WoLLogic._sc2wol_survives_rip_field(self, world, player)
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_final_mission_requirements = lambda world, player: SC2WoLLogic._sc2wol_final_mission_requirements(self, world, player)
def __init__(self, world: MultiWorld, player: int,
item_pool: List[Item], existing_items: List[Item], locked_items: List[Item],
has_protoss: bool):
self.world = world
self.player = player
self.logical_inventory = set()
self.locked_items = locked_items[:]
self.existing_items = existing_items
self._read_logic()
# 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":
# Locking upgrades based on mission duration
if item.name not in item_quantities:
item_quantities[item.name] = 0
item_quantities[item.name] += 1
if item_quantities[item.name] < min_upgrades:
self.locked_items.append(item)
else:
self.item_pool.append(item)
elif item_info.type == "Goal":
locked_items.append(item)
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],
item_pool: List[Item], existing_items: List[Item], locked_items: List[Item]) -> List[Item]:
"""
Returns a semi-randomly pruned set of items based on number of available locations.
The returned inventory must be capable of logically accessing every location in the world.
"""
open_locations = [location for location in location_cache if location.item is None]
inventory_size = len(open_locations)
has_protoss = bool(PROTOSS_REGIONS.intersection(mission_req_table.keys()))
mission_requirements = [location.access_rule for location in location_cache]
valid_inventory = ValidInventory(world, player, item_pool, existing_items, locked_items, has_protoss)
valid_items = valid_inventory.generate_reduced_inventory(inventory_size, mission_requirements)
return valid_items

View File

@ -2,55 +2,47 @@ from typing import List, Set, Dict, Tuple, Optional, Callable
from BaseClasses import MultiWorld, Region, Entrance, Location, RegionType from BaseClasses import MultiWorld, Region, Entrance, Location, RegionType
from .Locations import LocationData from .Locations import LocationData
from .Options import get_option_value from .Options import get_option_value
from .MissionTables import MissionInfo, vanilla_shuffle_order, vanilla_mission_req_table, \ from .MissionTables import MissionInfo, mission_orders, vanilla_mission_req_table, alt_final_mission_locations
no_build_regions_list, easy_regions_list, medium_regions_list, hard_regions_list from .PoolFilter import filter_missions
import random 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) locations_per_region = get_locations_per_region(locations)
regions = [ mission_order_type = get_option_value(world, player, "mission_order")
create_region(world, player, locations_per_region, location_cache, "Menu"), mission_order = mission_orders[mission_order_type]
create_region(world, player, locations_per_region, location_cache, "Liberation Day"),
create_region(world, player, locations_per_region, location_cache, "The Outlaws"), mission_pools = filter_missions(world, player)
create_region(world, player, locations_per_region, location_cache, "Zero Hour"), final_mission = mission_pools['all_in'][0]
create_region(world, player, locations_per_region, location_cache, "Evacuation"),
create_region(world, player, locations_per_region, location_cache, "Outbreak"), used_regions = [mission for mission_pool in mission_pools.values() for mission in mission_pool]
create_region(world, player, locations_per_region, location_cache, "Safe Haven"), regions = [create_region(world, player, locations_per_region, location_cache, "Menu")]
create_region(world, player, locations_per_region, location_cache, "Haven's Fall"), for region_name in used_regions:
create_region(world, player, locations_per_region, location_cache, "Smash and Grab"), regions.append(create_region(world, player, locations_per_region, location_cache, region_name))
create_region(world, player, locations_per_region, location_cache, "The Dig"), # Changing the completion condition for alternate final missions into an event
create_region(world, player, locations_per_region, location_cache, "The Moebius Factor"), if final_mission != 'All-In':
create_region(world, player, locations_per_region, location_cache, "Supernova"), final_location = alt_final_mission_locations[final_mission]
create_region(world, player, locations_per_region, location_cache, "Maw of the Void"), # Final location should be near the end of the cache
create_region(world, player, locations_per_region, location_cache, "Devil's Playground"), for i in range(len(location_cache) - 1, -1, -1):
create_region(world, player, locations_per_region, location_cache, "Welcome to the Jungle"), if location_cache[i].name == final_location:
create_region(world, player, locations_per_region, location_cache, "Breakout"), location_cache[i].locked = True
create_region(world, player, locations_per_region, location_cache, "Ghost of a Chance"), location_cache[i].event = True
create_region(world, player, locations_per_region, location_cache, "The Great Train Robbery"), location_cache[i].address = None
create_region(world, player, locations_per_region, location_cache, "Cutthroat"), break
create_region(world, player, locations_per_region, location_cache, "Engine of Destruction"), else:
create_region(world, player, locations_per_region, location_cache, "Media Blitz"), final_location = 'All-In: Victory'
create_region(world, player, locations_per_region, location_cache, "Piercing the Shroud"),
create_region(world, player, locations_per_region, location_cache, "Whispers of Doom"),
create_region(world, player, locations_per_region, location_cache, "A Sinister Turn"),
create_region(world, player, locations_per_region, location_cache, "Echoes of the Future"),
create_region(world, player, locations_per_region, location_cache, "In Utter Darkness"),
create_region(world, player, locations_per_region, location_cache, "Gates of Hell"),
create_region(world, player, locations_per_region, location_cache, "Belly of the Beast"),
create_region(world, player, locations_per_region, location_cache, "Shatter the Sky"),
create_region(world, player, locations_per_region, location_cache, "All-In")
]
if __debug__: if __debug__:
if mission_order_type in (0, 1):
throwIfAnyLocationIsNotAssignedToARegion(regions, locations_per_region.keys()) throwIfAnyLocationIsNotAssignedToARegion(regions, locations_per_region.keys())
world.regions += regions world.regions += regions
names: Dict[str, int] = {} names: Dict[str, int] = {}
if get_option_value(world, player, "mission_order") == 0: if mission_order_type == 0:
connect(world, player, names, 'Menu', 'Liberation Day'), connect(world, player, names, 'Menu', 'Liberation Day'),
connect(world, player, names, 'Liberation Day', 'The Outlaws', connect(world, player, names, 'Liberation Day', 'The Outlaws',
lambda state: state.has("Beat Liberation Day", player)), lambda state: state.has("Beat Liberation Day", player)),
@ -119,32 +111,30 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
lambda state: state.has('Beat Gates of Hell', player) and ( 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))) 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
elif get_option_value(world, player, "mission_order") == 1: else:
missions = [] missions = []
no_build_pool = no_build_regions_list[:]
easy_pool = easy_regions_list[:]
medium_pool = medium_regions_list[:]
hard_pool = hard_regions_list[:]
# Initial fill out of mission list and marking all-in mission # Initial fill out of mission list and marking all-in mission
for mission in vanilla_shuffle_order: for mission in mission_order:
if mission.type == "all_in": if mission is None:
missions.append("All-In") missions.append(None)
elif get_option_value(world, player, "relegate_no_build") and mission.relegate: elif mission.type == "all_in":
missions.append(final_mission)
elif mission.relegate and not get_option_value(world, player, "shuffle_no_build"):
missions.append("no_build") missions.append("no_build")
else: else:
missions.append(mission.type) missions.append(mission.type)
# Place Protoss Missions if we are not using ShuffleProtoss # Place Protoss Missions if we are not using ShuffleProtoss and are in Vanilla Shuffled
if get_option_value(world, player, "shuffle_protoss") == 0: if get_option_value(world, player, "shuffle_protoss") == 0 and mission_order_type == 1:
missions[22] = "A Sinister Turn" missions[22] = "A Sinister Turn"
medium_pool.remove("A Sinister Turn") mission_pools['medium'].remove("A Sinister Turn")
missions[23] = "Echoes of the Future" 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" missions[24] = "In Utter Darkness"
hard_pool.remove("In Utter Darkness") mission_pools['hard'].remove("In Utter Darkness")
no_build_slots = [] no_build_slots = []
easy_slots = [] easy_slots = []
@ -153,6 +143,8 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
# Search through missions to find slots needed to fill # Search through missions to find slots needed to fill
for i in range(len(missions)): for i in range(len(missions)):
if missions[i] is None:
continue
if missions[i] == "no_build": if missions[i] == "no_build":
no_build_slots.append(i) no_build_slots.append(i)
elif missions[i] == "easy": elif missions[i] == "easy":
@ -163,30 +155,30 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
hard_slots.append(i) hard_slots.append(i)
# Add no_build missions to the pool and fill in no_build slots # Add no_build missions to the pool and fill in no_build slots
missions_to_add = no_build_pool missions_to_add = mission_pools['no_build']
for slot in no_build_slots: for slot in no_build_slots:
filler = random.randint(0, len(missions_to_add)-1) filler = world.random.randint(0, len(missions_to_add)-1)
missions[slot] = missions_to_add.pop(filler) missions[slot] = missions_to_add.pop(filler)
# Add easy missions into pool and fill in easy slots # Add easy missions into pool and fill in easy slots
missions_to_add = missions_to_add + easy_pool missions_to_add = missions_to_add + mission_pools['easy']
for slot in easy_slots: for slot in easy_slots:
filler = random.randint(0, len(missions_to_add) - 1) filler = world.random.randint(0, len(missions_to_add) - 1)
missions[slot] = missions_to_add.pop(filler) missions[slot] = missions_to_add.pop(filler)
# Add medium missions into pool and fill in medium slots # Add medium missions into pool and fill in medium slots
missions_to_add = missions_to_add + medium_pool missions_to_add = missions_to_add + mission_pools['medium']
for slot in medium_slots: for slot in medium_slots:
filler = random.randint(0, len(missions_to_add) - 1) filler = world.random.randint(0, len(missions_to_add) - 1)
missions[slot] = missions_to_add.pop(filler) missions[slot] = missions_to_add.pop(filler)
# Add hard missions into pool and fill in hard slots # Add hard missions into pool and fill in hard slots
missions_to_add = missions_to_add + hard_pool missions_to_add = missions_to_add + mission_pools['hard']
for slot in hard_slots: for slot in hard_slots:
filler = random.randint(0, len(missions_to_add) - 1) filler = world.random.randint(0, len(missions_to_add) - 1)
missions[slot] = missions_to_add.pop(filler) missions[slot] = missions_to_add.pop(filler)
@ -195,7 +187,7 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
mission_req_table = {} mission_req_table = {}
for i in range(len(missions)): for i in range(len(missions)):
connections = [] connections = []
for connection in vanilla_shuffle_order[i].connect_to: for connection in mission_order[i].connect_to:
if connection == -1: if connection == -1:
connect(world, player, names, "Menu", missions[i]) connect(world, player, names, "Menu", missions[i])
else: else:
@ -203,16 +195,17 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
(lambda name, missions_req: (lambda state: state.has(f"Beat {name}", player) and (lambda name, missions_req: (lambda state: state.has(f"Beat {name}", player) and
state._sc2wol_cleared_missions(world, player, state._sc2wol_cleared_missions(world, player,
missions_req))) missions_req)))
(missions[connection], vanilla_shuffle_order[i].number)) (missions[connection], mission_order[i].number))
connections.append(connection + 1) connections.append(connection + 1)
mission_req_table.update({missions[i]: MissionInfo( mission_req_table.update({missions[i]: MissionInfo(
vanilla_mission_req_table[missions[i]].id, vanilla_mission_req_table[missions[i]].extra_locations, vanilla_mission_req_table[missions[i]].id, connections, mission_order[i].category,
connections, vanilla_shuffle_order[i].category, number=vanilla_shuffle_order[i].number, number=mission_order[i].number,
completion_critical=vanilla_shuffle_order[i].completion_critical, completion_critical=mission_order[i].completion_critical,
or_requirements=vanilla_shuffle_order[i].or_requirements)}) 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]): def throwIfAnyLocationIsNotAssignedToARegion(regions: List[Region], regionNames: Set[str]):

View File

@ -1,14 +1,16 @@
import typing import typing
from typing import List, Set, Tuple from typing import List, Set, Tuple, Dict
from BaseClasses import Item, MultiWorld, Location, Tutorial, ItemClassification from BaseClasses import Item, MultiWorld, Location, Tutorial, ItemClassification
from worlds.AutoWorld import WebWorld, World from worlds.AutoWorld import WebWorld, World
from .Items import StarcraftWoLItem, item_table, filler_items, item_name_groups, get_full_item_list, \ 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 .Locations import get_locations
from .Regions import create_regions 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 .LogicMixin import SC2WoLLogic
from .PoolFilter import filter_missions, filter_items, get_item_upgrades
from .MissionTables import get_starting_mission_locations, MissionInfo
class Starcraft2WoLWebWorld(WebWorld): class Starcraft2WoLWebWorld(WebWorld):
@ -42,6 +44,8 @@ class SC2WoLWorld(World):
locked_locations: typing.List[str] locked_locations: typing.List[str]
location_cache: typing.List[Location] location_cache: typing.List[Location]
mission_req_table = {} mission_req_table = {}
final_mission_id: int
victory_item: str
required_client_version = 0, 3, 5 required_client_version = 0, 3, 5
def __init__(self, world: MultiWorld, player: int): def __init__(self, world: MultiWorld, player: int):
@ -49,24 +53,21 @@ class SC2WoLWorld(World):
self.location_cache = [] self.location_cache = []
self.locked_locations = [] self.locked_locations = []
def _create_items(self, name: str):
data = get_full_item_list()[name]
return [self.create_item(name) for _ in range(data.quantity)]
def create_item(self, name: str) -> Item: def create_item(self, name: str) -> Item:
data = get_full_item_list()[name] data = get_full_item_list()[name]
return StarcraftWoLItem(name, data.classification, data.code, self.player) return StarcraftWoLItem(name, data.classification, data.code, self.player)
def create_regions(self): def create_regions(self):
self.mission_req_table = create_regions(self.world, self.player, get_locations(self.world, self.player), self.mission_req_table, self.final_mission_id, self.victory_item = create_regions(
self.location_cache) self.world, self.player, get_locations(self.world, self.player), self.location_cache
)
def generate_basic(self): def generate_basic(self):
excluded_items = get_excluded_items(self, self.world, self.player) excluded_items = get_excluded_items(self, self.world, self.player)
assign_starter_items(self.world, self.player, excluded_items, self.locked_locations) starter_items = assign_starter_items(self.world, self.player, excluded_items, self.locked_locations)
pool = get_item_pool(self.world, self.player, excluded_items) pool = get_item_pool(self.world, self.player, self.mission_req_table, starter_items, excluded_items, self.location_cache)
fill_item_pool_with_dummy_items(self, self.world, self.player, self.locked_locations, self.location_cache, pool) fill_item_pool_with_dummy_items(self, self.world, self.player, self.locked_locations, self.location_cache, pool)
@ -74,8 +75,7 @@ class SC2WoLWorld(World):
def set_rules(self): def set_rules(self):
setup_events(self.world, self.player, self.locked_locations, self.location_cache) setup_events(self.world, self.player, self.locked_locations, self.location_cache)
self.world.completion_condition[self.player] = lambda state: state.has(self.victory_item, self.player)
self.world.completion_condition[self.player] = lambda state: state.has('All-In: Victory', self.player)
def get_filler_item_name(self) -> str: def get_filler_item_name(self) -> str:
return self.world.random.choice(filler_items) return self.world.random.choice(filler_items)
@ -91,6 +91,7 @@ class SC2WoLWorld(World):
slot_req_table[mission] = self.mission_req_table[mission]._asdict() slot_req_table[mission] = self.mission_req_table[mission]._asdict()
slot_data["mission_req"] = slot_req_table slot_data["mission_req"] = slot_req_table
slot_data["final_mission"] = self.final_mission_id
return slot_data return slot_data
@ -120,30 +121,37 @@ def get_excluded_items(self: SC2WoLWorld, world: MultiWorld, player: int) -> Set
for item in world.precollected_items[player]: for item in world.precollected_items[player]:
excluded_items.add(item.name) excluded_items.add(item.name)
excluded_items_option = getattr(world, 'excluded_items', [])
excluded_items.update(excluded_items_option[player].value)
return excluded_items return excluded_items
def assign_starter_items(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]): 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 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: if not local_basic_unit:
raise Exception("At least one basic unit must be local") raise Exception("At least one basic unit must be local")
# The first world should also be the starting world # The first world should also be the starting world
first_location = list(world.worlds[player].mission_req_table)[0] first_mission = list(world.worlds[player].mission_req_table)[0]
starting_mission_locations = get_starting_mission_locations(world, player)
if first_location == "In Utter Darkness": if first_mission in starting_mission_locations:
first_location = first_location + ": Defeat" first_location = starting_mission_locations[first_mission]
elif first_mission == "In Utter Darkness":
first_location = first_mission + ": Defeat"
else: else:
first_location = first_location + ": Victory" first_location = first_mission + ": Victory"
assign_starter_item(world, player, excluded_items, locked_locations, first_location, return [assign_starter_item(world, player, excluded_items, locked_locations, first_location, local_basic_unit)]
local_basic_unit) else:
return []
def assign_starter_item(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str], def assign_starter_item(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str],
location: str, item_list: Tuple[str, ...]): location: str, item_list: Tuple[str, ...]) -> Item:
item_name = world.random.choice(item_list) item_name = world.random.choice(item_list)
@ -155,17 +163,40 @@ def assign_starter_item(world: MultiWorld, player: int, excluded_items: Set[str]
locked_locations.append(location) locked_locations.append(location)
return item
def get_item_pool(world: MultiWorld, player: int, excluded_items: Set[str]) -> List[Item]:
def get_item_pool(world: MultiWorld, player: int, mission_req_table: Dict[str, MissionInfo],
starter_items: List[str], excluded_items: Set[str], location_cache: List[Location]) -> List[Item]:
pool: List[Item] = [] pool: List[Item] = []
# For the future: goal items like Artifact Shards go here
locked_items = []
# YAML items
yaml_locked_items = get_option_set_value(world, player, 'locked_items')
for name, data in item_table.items(): for name, data in item_table.items():
if name not in excluded_items: if name not in excluded_items:
for _ in range(data.quantity): for _ in range(data.quantity):
item = create_item_with_correct_settings(world, player, name) item = create_item_with_correct_settings(world, player, name)
if name in yaml_locked_items:
locked_items.append(item)
else:
pool.append(item) pool.append(item)
return pool 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:
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
def fill_item_pool_with_dummy_items(self: SC2WoLWorld, world: MultiWorld, player: int, locked_locations: List[str], def fill_item_pool_with_dummy_items(self: SC2WoLWorld, world: MultiWorld, player: int, locked_locations: List[str],