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

View File

@ -1,5 +1,7 @@
from BaseClasses import Item, ItemClassification
from BaseClasses import Item, ItemClassification, MultiWorld
import typing
from .Options import get_option_value
from .MissionTables import vanilla_mission_req_table
@ -9,6 +11,7 @@ class ItemData(typing.NamedTuple):
number: typing.Optional[int]
classification: ItemClassification = ItemClassification.useful
quantity: int = 1
parent_item: str = None
class StarcraftWoLItem(Item):
@ -48,51 +51,51 @@ item_table = {
"Progressive Ship Weapon": ItemData(105 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 8, quantity=3),
"Progressive Ship Armor": ItemData(106 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 10, quantity=3),
"Projectile Accelerator (Bunker)": ItemData(200 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 0),
"Neosteel Bunker (Bunker)": ItemData(201 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 1),
"Titanium Housing (Missile Turret)": ItemData(202 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 2, classification=ItemClassification.filler),
"Hellstorm Batteries (Missile Turret)": ItemData(203 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 3),
"Advanced Construction (SCV)": ItemData(204 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 4),
"Dual-Fusion Welders (SCV)": ItemData(205 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 5),
"Projectile Accelerator (Bunker)": ItemData(200 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 0, parent_item="Bunker"),
"Neosteel Bunker (Bunker)": ItemData(201 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 1, parent_item="Bunker"),
"Titanium Housing (Missile Turret)": ItemData(202 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 2, classification=ItemClassification.filler, parent_item="Missile Turret"),
"Hellstorm Batteries (Missile Turret)": ItemData(203 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 3, parent_item="Missile Turret"),
"Advanced Construction (SCV)": ItemData(204 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 4, parent_item="SCV"),
"Dual-Fusion Welders (SCV)": ItemData(205 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 5, parent_item="SCV"),
"Fire-Suppression System (Building)": ItemData(206 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 6, classification=ItemClassification.filler),
"Orbital Command (Building)": ItemData(207 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 7),
"Stimpack (Marine)": ItemData(208 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 8),
"Combat Shield (Marine)": ItemData(209 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 9, classification=ItemClassification.progression),
"Advanced Medic Facilities (Medic)": ItemData(210 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 10, classification=ItemClassification.progression),
"Stabilizer Medpacks (Medic)": ItemData(211 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 11, classification=ItemClassification.progression),
"Incinerator Gauntlets (Firebat)": ItemData(212 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 12, classification=ItemClassification.filler),
"Juggernaut Plating (Firebat)": ItemData(213 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 13),
"Concussive Shells (Marauder)": ItemData(214 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 14),
"Kinetic Foam (Marauder)": ItemData(215 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 15),
"U-238 Rounds (Reaper)": ItemData(216 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 16),
"G-4 Clusterbomb (Reaper)": ItemData(217 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 17, classification=ItemClassification.progression),
"Stimpack (Marine)": ItemData(208 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 8, parent_item="Marine"),
"Combat Shield (Marine)": ItemData(209 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 9, classification=ItemClassification.progression, parent_item="Marine"),
"Advanced Medic Facilities (Medic)": ItemData(210 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 10, classification=ItemClassification.progression, parent_item="Medic"),
"Stabilizer Medpacks (Medic)": ItemData(211 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 11, classification=ItemClassification.progression, parent_item="Medic"),
"Incinerator Gauntlets (Firebat)": ItemData(212 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 12, classification=ItemClassification.filler, parent_item="Firebat"),
"Juggernaut Plating (Firebat)": ItemData(213 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 13, parent_item="Firebat"),
"Concussive Shells (Marauder)": ItemData(214 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 14, parent_item="Marauder"),
"Kinetic Foam (Marauder)": ItemData(215 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 15, parent_item="Marauder"),
"U-238 Rounds (Reaper)": ItemData(216 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 16, parent_item="Reaper"),
"G-4 Clusterbomb (Reaper)": ItemData(217 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 17, classification=ItemClassification.progression, parent_item="Reaper"),
"Twin-Linked Flamethrower (Hellion)": ItemData(300 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 0, classification=ItemClassification.filler),
"Thermite Filaments (Hellion)": ItemData(301 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 1),
"Cerberus Mine (Vulture)": ItemData(302 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 2, classification=ItemClassification.filler),
"Replenishable Magazine (Vulture)": ItemData(303 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 3, classification=ItemClassification.filler),
"Multi-Lock Weapons System (Goliath)": ItemData(304 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 4),
"Ares-Class Targeting System (Goliath)": ItemData(305 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 5),
"Tri-Lithium Power Cell (Diamondback)": ItemData(306 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 6, classification=ItemClassification.filler),
"Shaped Hull (Diamondback)": ItemData(307 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 7, classification=ItemClassification.filler),
"Maelstrom Rounds (Siege Tank)": ItemData(308 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 8),
"Shaped Blast (Siege Tank)": ItemData(309 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 9),
"Rapid Deployment Tube (Medivac)": ItemData(310 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 10, classification=ItemClassification.filler),
"Advanced Healing AI (Medivac)": ItemData(311 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 11, classification=ItemClassification.filler),
"Tomahawk Power Cells (Wraith)": ItemData(312 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 12, classification=ItemClassification.filler),
"Displacement Field (Wraith)": ItemData(313 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 13, classification=ItemClassification.filler),
"Ripwave Missiles (Viking)": ItemData(314 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 14),
"Phobos-Class Weapons System (Viking)": ItemData(315 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 15),
"Cross-Spectrum Dampeners (Banshee)": ItemData(316 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 16, classification=ItemClassification.filler),
"Shockwave Missile Battery (Banshee)": ItemData(317 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 17),
"Missile Pods (Battlecruiser)": ItemData(318 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 18, classification=ItemClassification.filler),
"Defensive Matrix (Battlecruiser)": ItemData(319 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 19, classification=ItemClassification.filler),
"Ocular Implants (Ghost)": ItemData(320 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 20),
"Crius Suit (Ghost)": ItemData(321 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 21),
"Psionic Lash (Spectre)": ItemData(322 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 22, classification=ItemClassification.progression),
"Nyx-Class Cloaking Module (Spectre)": ItemData(323 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 23),
"330mm Barrage Cannon (Thor)": ItemData(324 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 24, classification=ItemClassification.filler),
"Immortality Protocol (Thor)": ItemData(325 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 25, classification=ItemClassification.filler),
"Twin-Linked Flamethrower (Hellion)": ItemData(300 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 0, classification=ItemClassification.filler, parent_item="Hellion"),
"Thermite Filaments (Hellion)": ItemData(301 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 1, parent_item="Hellion"),
"Cerberus Mine (Vulture)": ItemData(302 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 2, classification=ItemClassification.filler, parent_item="Vulture"),
"Replenishable Magazine (Vulture)": ItemData(303 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 3, classification=ItemClassification.filler, parent_item="Vulture"),
"Multi-Lock Weapons System (Goliath)": ItemData(304 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 4, parent_item="Goliath"),
"Ares-Class Targeting System (Goliath)": ItemData(305 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 5, parent_item="Goliath"),
"Tri-Lithium Power Cell (Diamondback)": ItemData(306 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 6, classification=ItemClassification.filler, parent_item="Diamondback"),
"Shaped Hull (Diamondback)": ItemData(307 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 7, classification=ItemClassification.filler, parent_item="Diamondback"),
"Maelstrom Rounds (Siege Tank)": ItemData(308 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 8, classification=ItemClassification.progression, parent_item="Siege Tank"),
"Shaped Blast (Siege Tank)": ItemData(309 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 9, parent_item="Siege Tank"),
"Rapid Deployment Tube (Medivac)": ItemData(310 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 10, classification=ItemClassification.filler, parent_item="Medivac"),
"Advanced Healing AI (Medivac)": ItemData(311 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 11, classification=ItemClassification.filler, parent_item="Medivac"),
"Tomahawk Power Cells (Wraith)": ItemData(312 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 12, classification=ItemClassification.filler, parent_item="Wraith"),
"Displacement Field (Wraith)": ItemData(313 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 13, classification=ItemClassification.filler, parent_item="Wraith"),
"Ripwave Missiles (Viking)": ItemData(314 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 14, parent_item="Viking"),
"Phobos-Class Weapons System (Viking)": ItemData(315 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 15, parent_item="Viking"),
"Cross-Spectrum Dampeners (Banshee)": ItemData(316 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 16, classification=ItemClassification.filler, parent_item="Banshee"),
"Shockwave Missile Battery (Banshee)": ItemData(317 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 17, parent_item="Banshee"),
"Missile Pods (Battlecruiser)": ItemData(318 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 18, classification=ItemClassification.filler, parent_item="Battlecruiser"),
"Defensive Matrix (Battlecruiser)": ItemData(319 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 19, classification=ItemClassification.filler, parent_item="Battlecruiser"),
"Ocular Implants (Ghost)": ItemData(320 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 20, parent_item="Ghost"),
"Crius Suit (Ghost)": ItemData(321 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 21, parent_item="Ghost"),
"Psionic Lash (Spectre)": ItemData(322 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 22, classification=ItemClassification.progression, parent_item="Spectre"),
"Nyx-Class Cloaking Module (Spectre)": ItemData(323 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 23, parent_item="Spectre"),
"330mm Barrage Cannon (Thor)": ItemData(324 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 24, classification=ItemClassification.filler, parent_item="Thor"),
"Immortality Protocol (Thor)": ItemData(325 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 25, classification=ItemClassification.filler, parent_item="Thor"),
"Bunker": ItemData(400 + SC2WOL_ITEM_ID_OFFSET, "Building", 0, classification=ItemClassification.progression),
"Missile Turret": ItemData(401 + SC2WOL_ITEM_ID_OFFSET, "Building", 1, classification=ItemClassification.progression),
@ -117,16 +120,16 @@ item_table = {
"Science Vessel": ItemData(607 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 7, classification=ItemClassification.progression),
"Tech Reactor": ItemData(608 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 8),
"Orbital Strike": ItemData(609 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 9),
"Shrike Turret": ItemData(610 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 10),
"Fortified Bunker": ItemData(611 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 11),
"Planetary Fortress": ItemData(612 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 12),
"Perdition Turret": ItemData(613 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 13),
"Shrike Turret": ItemData(610 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 10, parent_item="Bunker"),
"Fortified Bunker": ItemData(611 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 11, parent_item="Bunker"),
"Planetary Fortress": ItemData(612 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 12, classification=ItemClassification.progression),
"Perdition Turret": ItemData(613 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 13, classification=ItemClassification.progression),
"Predator": ItemData(614 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 14, classification=ItemClassification.filler),
"Hercules": ItemData(615 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 15, classification=ItemClassification.progression),
"Cellular Reactor": ItemData(616 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 16, classification=ItemClassification.filler),
"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),
"Psi Disrupter": ItemData(619 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 19, classification=ItemClassification.filler),
"Hive Mind Emulator": ItemData(618 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 18, ItemClassification.progression),
"Psi Disrupter": ItemData(619 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 19, classification=ItemClassification.progression),
"Zealot": ItemData(700 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 0, 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 Vespene": ItemData(801 + SC2WOL_ITEM_ID_OFFSET, "Vespene", 15, quantity=0, classification=ItemClassification.filler),
"+2 Starting Supply": ItemData(802 + SC2WOL_ITEM_ID_OFFSET, "Supply", 2, quantity=0, classification=ItemClassification.filler),
# "Keystone Piece": ItemData(850 + SC2WOL_ITEM_ID_OFFSET, "Goal", 0, quantity=0, classification=ItemClassification.progression_skip_balancing)
}
basic_unit: typing.Tuple[str, ...] = (
basic_units = {
'Marine',
'Marauder',
'Firebat',
'Hellion',
'Vulture'
)
}
advanced_basic_units = {
'Reaper',
'Goliath',
'Diamondback',
'Viking'
}
def get_basic_units(world: MultiWorld, player: int) -> 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 = {}
for item, data in item_table.items():
@ -161,6 +182,22 @@ filler_items: typing.Tuple[str, ...] = (
'+15 Starting Vespene'
)
defense_ratings = {
"Siege Tank": 5,
"Maelstrom Rounds": 2,
"Planetary Fortress": 3,
# Bunker w/ Marine/Marauder: 3,
"Perdition Turret": 2,
"Missile Turret": 2,
"Vulture": 2
}
zerg_defense_ratings = {
"Perdition Turret": 2,
# Bunker w/ Firebat
"Hive Mind Emulator": 3,
"Psi Disruptor": 3
}
lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in get_full_item_list().items() if
data.code}
# Map type to expected int
@ -176,4 +213,5 @@ type_flaggroups: typing.Dict[str, int] = {
"Minerals": 8,
"Vespene": 9,
"Supply": 10,
"Goal": 11
}

View File

@ -1,5 +1,6 @@
from typing import List, Tuple, Optional, Callable, NamedTuple
from BaseClasses import MultiWorld
from .Options import get_option_value
from BaseClasses import Location
@ -19,6 +20,7 @@ class LocationData(NamedTuple):
def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[LocationData, ...]:
# Note: rules which are ended with or True are rules identified as needed later when restricted units is an option
logic_level = get_option_value(world, player, 'required_tactics')
location_table: List[LocationData] = [
LocationData("Liberation Day", "Liberation Day: Victory", SC2WOL_LOC_ID_OFFSET + 100),
LocationData("Liberation Day", "Liberation Day: First Statue", SC2WOL_LOC_ID_OFFSET + 101),
@ -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,
lambda state: state._sc2wol_has_common_unit(world, player)),
LocationData("Zero Hour", "Zero Hour: Victory", SC2WOL_LOC_ID_OFFSET + 300,
lambda state: state._sc2wol_has_common_unit(world, player)),
lambda state: state._sc2wol_has_common_unit(world, player) and
state._sc2wol_defense_rating(world, player, True) >= 2 and
(logic_level > 0 or state._sc2wol_has_anti_air(world, player))),
LocationData("Zero Hour", "Zero Hour: First Group Rescued", SC2WOL_LOC_ID_OFFSET + 301),
LocationData("Zero Hour", "Zero Hour: Second Group Rescued", SC2WOL_LOC_ID_OFFSET + 302,
lambda state: state._sc2wol_has_common_unit(world, player)),
LocationData("Zero Hour", "Zero Hour: Third Group Rescued", SC2WOL_LOC_ID_OFFSET + 303,
lambda state: state._sc2wol_has_common_unit(world, player)),
lambda state: state._sc2wol_has_common_unit(world, player) and
state._sc2wol_defense_rating(world, player, True) >= 2),
LocationData("Evacuation", "Evacuation: Victory", SC2WOL_LOC_ID_OFFSET + 400,
lambda state: state._sc2wol_has_common_unit(world, player) and
state._sc2wol_has_competent_anti_air(world, player)),
(logic_level > 0 and state._sc2wol_has_anti_air(world, player)
or state._sc2wol_has_competent_anti_air(world, player))),
LocationData("Evacuation", "Evacuation: First Chysalis", SC2WOL_LOC_ID_OFFSET + 401),
LocationData("Evacuation", "Evacuation: Second Chysalis", SC2WOL_LOC_ID_OFFSET + 402,
lambda state: state._sc2wol_has_common_unit(world, player)),
LocationData("Evacuation", "Evacuation: Third Chysalis", SC2WOL_LOC_ID_OFFSET + 403,
lambda state: state._sc2wol_has_common_unit(world, player)),
LocationData("Outbreak", "Outbreak: Victory", SC2WOL_LOC_ID_OFFSET + 500,
lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)),
lambda state: state._sc2wol_defense_rating(world, player, True, 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,
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,
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,
lambda state: state._sc2wol_has_common_unit(world, player) and
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)),
LocationData("Haven's Fall", "Haven's Fall: Victory", SC2WOL_LOC_ID_OFFSET + 700,
lambda state: state._sc2wol_has_common_unit(world, player) and
state._sc2wol_has_competent_anti_air(world, player)),
state._sc2wol_has_competent_anti_air(world, player) and
state._sc2wol_defense_rating(world, player, True) >= 3),
LocationData("Haven's Fall", "Haven's Fall: North Hive", SC2WOL_LOC_ID_OFFSET + 701,
lambda state: state._sc2wol_has_common_unit(world, player) and
state._sc2wol_has_competent_anti_air(world, player)),
state._sc2wol_has_competent_anti_air(world, player) and
state._sc2wol_defense_rating(world, player, True) >= 3),
LocationData("Haven's Fall", "Haven's Fall: East Hive", SC2WOL_LOC_ID_OFFSET + 702,
lambda state: state._sc2wol_has_common_unit(world, player) and
state._sc2wol_has_competent_anti_air(world, player)),
state._sc2wol_has_competent_anti_air(world, player) and
state._sc2wol_defense_rating(world, player, True) >= 3),
LocationData("Haven's Fall", "Haven's Fall: South Hive", SC2WOL_LOC_ID_OFFSET + 703,
lambda state: state._sc2wol_has_common_unit(world, player) and
state._sc2wol_has_competent_anti_air(world, player)),
state._sc2wol_has_competent_anti_air(world, player) and
state._sc2wol_defense_rating(world, player, True) >= 3),
LocationData("Smash and Grab", "Smash and Grab: Victory", SC2WOL_LOC_ID_OFFSET + 800,
lambda state: state._sc2wol_has_common_unit(world, player) and
state._sc2wol_has_competent_anti_air(world, player)),
(logic_level > 0 and state._sc2wol_has_anti_air(world, player)
or state._sc2wol_has_competent_anti_air(world, player))),
LocationData("Smash and Grab", "Smash and Grab: First Relic", SC2WOL_LOC_ID_OFFSET + 801),
LocationData("Smash and Grab", "Smash and Grab: Second Relic", SC2WOL_LOC_ID_OFFSET + 802),
LocationData("Smash and Grab", "Smash and Grab: Third Relic", SC2WOL_LOC_ID_OFFSET + 803,
lambda state: state._sc2wol_has_common_unit(world, player)),
lambda state: state._sc2wol_has_common_unit(world, player) and
(logic_level > 0 and state._sc2wol_has_anti_air(world, player)
or state._sc2wol_has_competent_anti_air(world, player))),
LocationData("Smash and Grab", "Smash and Grab: Fourth Relic", SC2WOL_LOC_ID_OFFSET + 804,
lambda state: state._sc2wol_has_common_unit(world, player) and
state._sc2wol_has_anti_air(world, player)),
(logic_level > 0 and state._sc2wol_has_anti_air(world, player)
or state._sc2wol_has_competent_anti_air(world, player))),
LocationData("The Dig", "The Dig: Victory", SC2WOL_LOC_ID_OFFSET + 900,
lambda state: state._sc2wol_has_common_unit(world, player) and
state._sc2wol_has_anti_air(world, player) and
state._sc2wol_has_heavy_defense(world, player)),
lambda state: state._sc2wol_has_anti_air(world, player) and
state._sc2wol_defense_rating(world, player, False) >= 7),
LocationData("The Dig", "The Dig: Left Relic", SC2WOL_LOC_ID_OFFSET + 901,
lambda state: state._sc2wol_has_common_unit(world, player)),
lambda state: state._sc2wol_defense_rating(world, player, False) >= 5),
LocationData("The Dig", "The Dig: Right Ground Relic", SC2WOL_LOC_ID_OFFSET + 902,
lambda state: state._sc2wol_has_common_unit(world, player)),
lambda state: state._sc2wol_defense_rating(world, player, False) >= 5),
LocationData("The Dig", "The Dig: Right Cliff Relic", SC2WOL_LOC_ID_OFFSET + 903,
lambda state: state._sc2wol_has_common_unit(world, player)),
lambda state: state._sc2wol_defense_rating(world, player, False) >= 5),
LocationData("The Moebius Factor", "The Moebius Factor: Victory", SC2WOL_LOC_ID_OFFSET + 1000,
lambda state: state._sc2wol_has_air(world, player) and state._sc2wol_has_anti_air(world, player)),
lambda state: state._sc2wol_has_anti_air(world, player) and
(state._sc2wol_has_air(world, player)
or state.has_any({'Medivac', 'Hercules'}, player)
and state._sc2wol_has_common_unit(world, player))),
LocationData("The Moebius Factor", "The Moebius Factor: South Rescue", SC2WOL_LOC_ID_OFFSET + 1003,
lambda state: state._sc2wol_able_to_rescue(world, player)),
LocationData("The Moebius Factor", "The Moebius Factor: Wall Rescue", SC2WOL_LOC_ID_OFFSET + 1004,
@ -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,
lambda state: state._sc2wol_able_to_rescue(world, player)),
LocationData("The Moebius Factor", "The Moebius Factor: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1008,
lambda state: state._sc2wol_has_air(world, player)),
lambda state: state._sc2wol_has_anti_air(world, player) and
(state._sc2wol_has_air(world, player)
or state.has_any({'Medivac', 'Hercules'}, player)
and state._sc2wol_has_common_unit(world, player))),
LocationData("Supernova", "Supernova: Victory", SC2WOL_LOC_ID_OFFSET + 1100,
lambda state: state._sc2wol_beats_protoss_deathball(world, player)),
LocationData("Supernova", "Supernova: West Relic", SC2WOL_LOC_ID_OFFSET + 1101),
@ -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,
lambda state: state._sc2wol_beats_protoss_deathball(world, player)),
LocationData("Maw of the Void", "Maw of the Void: Victory", SC2WOL_LOC_ID_OFFSET + 1200,
lambda state: state.has('Battlecruiser', player) or
state._sc2wol_has_air(world, player) and
state._sc2wol_has_competent_anti_air(world, player) and
state.has('Science Vessel', player)),
lambda state: state._sc2wol_survives_rip_field(world, player)),
LocationData("Maw of the Void", "Maw of the Void: Landing Zone Cleared", SC2WOL_LOC_ID_OFFSET + 1201),
LocationData("Maw of the Void", "Maw of the Void: Expansion Prisoners", SC2WOL_LOC_ID_OFFSET + 1202,
lambda state: state.has('Battlecruiser', player) or
state._sc2wol_has_air(world, player) and
state._sc2wol_has_competent_anti_air(world, player) and
state.has('Science Vessel', player)),
lambda state: logic_level > 0 or state._sc2wol_survives_rip_field(world, player)),
LocationData("Maw of the Void", "Maw of the Void: South Close Prisoners", SC2WOL_LOC_ID_OFFSET + 1203,
lambda state: state.has('Battlecruiser', player) or
state._sc2wol_has_air(world, player) and
state._sc2wol_has_competent_anti_air(world, player) and
state.has('Science Vessel', player)),
lambda state: logic_level > 0 or state._sc2wol_survives_rip_field(world, player)),
LocationData("Maw of the Void", "Maw of the Void: South Far Prisoners", SC2WOL_LOC_ID_OFFSET + 1204,
lambda state: state.has('Battlecruiser', player) or
state._sc2wol_has_air(world, player) and
state._sc2wol_has_competent_anti_air(world, player) and
state.has('Science Vessel', player)),
lambda state: state._sc2wol_survives_rip_field(world, player)),
LocationData("Maw of the Void", "Maw of the Void: North Prisoners", SC2WOL_LOC_ID_OFFSET + 1205,
lambda state: state.has('Battlecruiser', player) or
state._sc2wol_has_air(world, player) and
state._sc2wol_has_competent_anti_air(world, player) and
state.has('Science Vessel', player)),
lambda state: state._sc2wol_survives_rip_field(world, player)),
LocationData("Devil's Playground", "Devil's Playground: Victory", SC2WOL_LOC_ID_OFFSET + 1300,
lambda state: state._sc2wol_has_anti_air(world, player) and (
state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player))),
lambda state: logic_level > 0 or
state._sc2wol_has_anti_air(world, player) and (
state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player))),
LocationData("Devil's Playground", "Devil's Playground: Tosh's Miners", SC2WOL_LOC_ID_OFFSET + 1301),
LocationData("Devil's Playground", "Devil's Playground: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1302,
lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)),
lambda state: logic_level > 0 or state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)),
LocationData("Welcome to the Jungle", "Welcome to the Jungle: Victory", SC2WOL_LOC_ID_OFFSET + 1400,
lambda state: state._sc2wol_has_common_unit(world, player) and
state._sc2wol_has_competent_anti_air(world, player)),
@ -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: South Defiler", SC2WOL_LOC_ID_OFFSET + 1703),
LocationData("Cutthroat", "Cutthroat: Victory", SC2WOL_LOC_ID_OFFSET + 1800,
lambda state: state._sc2wol_has_common_unit(world, player)),
lambda state: state._sc2wol_has_common_unit(world, player) and
(logic_level > 0 or state._sc2wol_has_anti_air)),
LocationData("Cutthroat", "Cutthroat: Mira Han", SC2WOL_LOC_ID_OFFSET + 1801,
lambda state: state._sc2wol_has_common_unit(world, player)),
LocationData("Cutthroat", "Cutthroat: North Relic", SC2WOL_LOC_ID_OFFSET + 1802,
@ -208,40 +217,44 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
lambda state: state._sc2wol_has_competent_comp(world, player)),
LocationData("Media Blitz", "Media Blitz: Science Facility", SC2WOL_LOC_ID_OFFSET + 2004),
LocationData("Piercing the Shroud", "Piercing the Shroud: Victory", SC2WOL_LOC_ID_OFFSET + 2100,
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: 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,
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,
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,
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: First Hatchery", SC2WOL_LOC_ID_OFFSET + 2201),
LocationData("Whispers of Doom", "Whispers of Doom: Second Hatchery", SC2WOL_LOC_ID_OFFSET + 2202),
LocationData("Whispers of Doom", "Whispers of Doom: Third Hatchery", SC2WOL_LOC_ID_OFFSET + 2203),
LocationData("A Sinister Turn", "A Sinister Turn: Victory", SC2WOL_LOC_ID_OFFSET + 2300,
lambda state: state._sc2wol_has_protoss_medium_units(world, player)),
LocationData("A Sinister Turn", "A Sinister Turn: Robotics Facility", SC2WOL_LOC_ID_OFFSET + 2301),
LocationData("A Sinister Turn", "A Sinister Turn: Dark Shrine", SC2WOL_LOC_ID_OFFSET + 2302),
LocationData("A Sinister Turn", "A Sinister Turn: Robotics Facility", SC2WOL_LOC_ID_OFFSET + 2301,
lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(world, player)),
LocationData("A Sinister Turn", "A Sinister Turn: Dark Shrine", SC2WOL_LOC_ID_OFFSET + 2302,
lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(world, player)),
LocationData("A Sinister Turn", "A Sinister Turn: Templar Archives", SC2WOL_LOC_ID_OFFSET + 2303,
lambda state: state._sc2wol_has_protoss_common_units(world, player)),
LocationData("Echoes of the Future", "Echoes of the Future: Victory", SC2WOL_LOC_ID_OFFSET + 2400,
lambda state: state._sc2wol_has_protoss_medium_units(world, player)),
lambda state: logic_level > 0 or state._sc2wol_has_protoss_medium_units(world, player)),
LocationData("Echoes of the Future", "Echoes of the Future: Close Obelisk", SC2WOL_LOC_ID_OFFSET + 2401),
LocationData("Echoes of the Future", "Echoes of the Future: West Obelisk", SC2WOL_LOC_ID_OFFSET + 2402,
lambda state: state._sc2wol_has_protoss_common_units(world, player)),
lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(world, player)),
LocationData("In Utter Darkness", "In Utter Darkness: Defeat", SC2WOL_LOC_ID_OFFSET + 2500),
LocationData("In Utter Darkness", "In Utter Darkness: Protoss Archive", SC2WOL_LOC_ID_OFFSET + 2501,
lambda state: state._sc2wol_has_protoss_medium_units(world, player)),
LocationData("In Utter Darkness", "In Utter Darkness: Kills", SC2WOL_LOC_ID_OFFSET + 2502,
lambda state: state._sc2wol_has_protoss_common_units(world, player)),
LocationData("Gates of Hell", "Gates of Hell: Victory", SC2WOL_LOC_ID_OFFSET + 2600,
lambda state: state._sc2wol_has_competent_comp(world, player)),
lambda state: state._sc2wol_has_competent_comp(world, player) and
state._sc2wol_defense_rating(world, player, True) > 6),
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: First Charge", SC2WOL_LOC_ID_OFFSET + 2701),
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)),
LocationData("Shatter the Sky", "Shatter the Sky: Leviathan", SC2WOL_LOC_ID_OFFSET + 2805,
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 = []
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")):
beat_events.append(
location_data._replace(name="Beat " + location_data.name.rsplit(": ", 1)[0], code=None)
)
return tuple(location_table + beat_events)

View File

@ -1,31 +1,43 @@
from BaseClasses import MultiWorld
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):
def _sc2wol_has_common_unit(self, world: MultiWorld, player: int) -> bool:
return self.has_any({'Marine', 'Marauder', 'Firebat', 'Hellion', 'Vulture'}, player)
def _sc2wol_has_bunker_unit(self, world: MultiWorld, player: int) -> bool:
return self.has_any({'Marine', 'Marauder'}, player)
return self.has_any(get_basic_units(world, player), player)
def _sc2wol_has_air(self, world: MultiWorld, player: int) -> bool:
return self.has_any({'Viking', 'Wraith', 'Banshee'}, player) or \
self.has_any({'Hercules', 'Medivac'}, player) and self._sc2wol_has_common_unit(world, player)
return self.has_any({'Viking', 'Wraith', 'Banshee'}, player) or get_option_value(world, player, 'required_tactics') > 0 \
and self.has_any({'Hercules', 'Medivac'}, player) and self._sc2wol_has_common_unit(world, player)
def _sc2wol_has_air_anti_air(self, world: MultiWorld, player: int) -> bool:
return self.has_any({'Viking', 'Wraith'}, player)
return self.has('Viking', player) \
or get_option_value(world, player, 'required_tactics') > 0 and self.has('Wraith', player)
def _sc2wol_has_competent_anti_air(self, world: MultiWorld, player: int) -> bool:
return self.has_any({'Marine', 'Goliath'}, player) or self._sc2wol_has_air_anti_air(world, player)
def _sc2wol_has_anti_air(self, world: MultiWorld, player: int) -> bool:
return self.has_any({'Missile Turret', 'Thor', 'War Pigs', 'Spartan Company', "Hel's Angel", 'Battlecruiser'}, player) 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:
return (self.has_any({'Siege Tank', 'Vulture'}, player) or
self.has('Bunker', player) and self._sc2wol_has_bunker_unit(world, player)) and \
self._sc2wol_has_anti_air(world, player)
def _sc2wol_defense_rating(self, world: MultiWorld, player: int, zerg_enemy: bool, air_enemy: bool = True) -> bool:
defense_score = sum((defense_ratings[item] for item in defense_ratings if self.has(item, player)))
if self.has_any({'Marine', 'Marauder'}, player) and self.has('Bunker', player):
defense_score += 3
if zerg_enemy:
defense_score += sum((zerg_defense_ratings[item] for item in zerg_defense_ratings if self.has(item, player)))
if self.has('Firebat', player) and self.has('Bunker', player):
defense_score += 2
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:
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)
def _sc2wol_has_train_killers(self, world: MultiWorld, player: int) -> bool:
return (self.has_any({'Siege Tank', 'Diamondback'}, player) or
self.has_all({'Reaper', "G-4 Clusterbomb"}, player) or self.has_all({'Spectre', 'Psionic Lash'}, player)
or self.has('Marauders', player))
return (self.has_any({'Siege Tank', 'Diamondback', 'Marauder'}, player) or get_option_value(world, player, 'required_tactics') > 0
and self.has_all({'Reaper', "G-4 Clusterbomb"}, player) or self.has_all({'Spectre', 'Psionic Lash'}, player))
def _sc2wol_able_to_rescue(self, world: MultiWorld, player: int) -> bool:
return self.has_any({'Medivac', 'Hercules', 'Raven', 'Viking'}, player)
return self.has_any({'Medivac', 'Hercules', 'Raven', 'Viking'}, player) or get_option_value(world, player, 'required_tactics') > 0
def _sc2wol_has_protoss_common_units(self, world: MultiWorld, player: int) -> bool:
return self.has_any({'Zealot', 'Immortal', 'Stalker', 'Dark Templar'}, player)
return self.has_any({'Zealot', 'Immortal', 'Stalker', 'Dark Templar'}, player) \
or get_option_value(world, player, 'required_tactics') > 0 and self.has_any({'High Templar', 'Dark Templar'}, player)
def _sc2wol_has_protoss_medium_units(self, world: MultiWorld, player: int) -> bool:
return self._sc2wol_has_protoss_common_units(world, player) and \
self.has_any({'Stalker', 'Void Ray', 'Phoenix', 'Carrier'}, player)
self.has_any({'Stalker', 'Void Ray', 'Phoenix', 'Carrier'}, player) \
or get_option_value(world, player, 'required_tactics') > 0 and self.has_any({'High Templar', 'Dark Templar'}, player)
def _sc2wol_beats_protoss_deathball(self, world: MultiWorld, player: int) -> bool:
return self.has_any({'Banshee', 'Battlecruiser'}, player) and self._sc2wol_has_competent_anti_air or \
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:
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",
"Belly of the Beast"]
@ -12,7 +15,6 @@ hard_regions_list = ["Maw of the Void", "Engine of Destruction", "In Utter Darkn
class MissionInfo(NamedTuple):
id: int
extra_locations: int
required_world: List[int]
category: str
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)
]
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 = {
"Liberation Day": MissionInfo(1, 7, [], "Mar Sara", completion_critical=True),
"The Outlaws": MissionInfo(2, 2, [1], "Mar Sara", completion_critical=True),
"Zero Hour": MissionInfo(3, 4, [2], "Mar Sara", completion_critical=True),
"Evacuation": MissionInfo(4, 4, [3], "Colonist"),
"Outbreak": MissionInfo(5, 3, [4], "Colonist"),
"Safe Haven": MissionInfo(6, 4, [5], "Colonist", number=7),
"Haven's Fall": MissionInfo(7, 4, [5], "Colonist", number=7),
"Smash and Grab": MissionInfo(8, 5, [3], "Artifact", completion_critical=True),
"The Dig": MissionInfo(9, 4, [8], "Artifact", number=8, completion_critical=True),
"The Moebius Factor": MissionInfo(10, 9, [9], "Artifact", number=11, completion_critical=True),
"Supernova": MissionInfo(11, 5, [10], "Artifact", number=14, completion_critical=True),
"Maw of the Void": MissionInfo(12, 6, [11], "Artifact", completion_critical=True),
"Devil's Playground": MissionInfo(13, 3, [3], "Covert", number=4),
"Welcome to the Jungle": MissionInfo(14, 4, [13], "Covert"),
"Breakout": MissionInfo(15, 3, [14], "Covert", number=8),
"Ghost of a Chance": MissionInfo(16, 6, [14], "Covert", number=8),
"The Great Train Robbery": MissionInfo(17, 4, [3], "Rebellion", number=6),
"Cutthroat": MissionInfo(18, 5, [17], "Rebellion"),
"Engine of Destruction": MissionInfo(19, 6, [18], "Rebellion"),
"Media Blitz": MissionInfo(20, 5, [19], "Rebellion"),
"Piercing the Shroud": MissionInfo(21, 6, [20], "Rebellion"),
"Whispers of Doom": MissionInfo(22, 4, [9], "Prophecy"),
"A Sinister Turn": MissionInfo(23, 4, [22], "Prophecy"),
"Echoes of the Future": MissionInfo(24, 3, [23], "Prophecy"),
"In Utter Darkness": MissionInfo(25, 3, [24], "Prophecy"),
"Gates of Hell": MissionInfo(26, 2, [12], "Char", completion_critical=True),
"Belly of the Beast": MissionInfo(27, 4, [26], "Char", completion_critical=True),
"Shatter the Sky": MissionInfo(28, 5, [26], "Char", completion_critical=True),
"All-In": MissionInfo(29, -1, [27, 28], "Char", completion_critical=True, or_requirements=True)
"Liberation Day": MissionInfo(1, [], "Mar Sara", completion_critical=True),
"The Outlaws": MissionInfo(2, [1], "Mar Sara", completion_critical=True),
"Zero Hour": MissionInfo(3, [2], "Mar Sara", completion_critical=True),
"Evacuation": MissionInfo(4, [3], "Colonist"),
"Outbreak": MissionInfo(5, [4], "Colonist"),
"Safe Haven": MissionInfo(6, [5], "Colonist", number=7),
"Haven's Fall": MissionInfo(7, [5], "Colonist", number=7),
"Smash and Grab": MissionInfo(8, [3], "Artifact", completion_critical=True),
"The Dig": MissionInfo(9, [8], "Artifact", number=8, completion_critical=True),
"The Moebius Factor": MissionInfo(10, [9], "Artifact", number=11, completion_critical=True),
"Supernova": MissionInfo(11, [10], "Artifact", number=14, completion_critical=True),
"Maw of the Void": MissionInfo(12, [11], "Artifact", completion_critical=True),
"Devil's Playground": MissionInfo(13, [3], "Covert", number=4),
"Welcome to the Jungle": MissionInfo(14, [13], "Covert"),
"Breakout": MissionInfo(15, [14], "Covert", number=8),
"Ghost of a Chance": MissionInfo(16, [14], "Covert", number=8),
"The Great Train Robbery": MissionInfo(17, [3], "Rebellion", number=6),
"Cutthroat": MissionInfo(18, [17], "Rebellion"),
"Engine of Destruction": MissionInfo(19, [18], "Rebellion"),
"Media Blitz": MissionInfo(20, [19], "Rebellion"),
"Piercing the Shroud": MissionInfo(21, [20], "Rebellion"),
"Whispers of Doom": MissionInfo(22, [9], "Prophecy"),
"A Sinister Turn": MissionInfo(23, [22], "Prophecy"),
"Echoes of the Future": MissionInfo(24, [23], "Prophecy"),
"In Utter Darkness": MissionInfo(25, [24], "Prophecy"),
"Gates of Hell": MissionInfo(26, [12], "Char", completion_critical=True),
"Belly of the Beast": MissionInfo(27, [26], "Char", completion_critical=True),
"Shatter the Sky": MissionInfo(28, [26], "Char", completion_critical=True),
"All-In": MissionInfo(29, [27, 28], "Char", completion_critical=True, or_requirements=True)
}
lookup_id_to_mission: Dict[int, str] = {
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 BaseClasses import MultiWorld
from Options import Choice, Option, DefaultOnToggle
from Options import Choice, Option, Toggle, DefaultOnToggle, ItemSet, OptionSet, Range
class GameDifficulty(Choice):
@ -36,25 +36,75 @@ class AllInMap(Choice):
class MissionOrder(Choice):
"""Determines the order the missions are played in.
Vanilla: Keeps the standard mission order and branching from the WoL Campaign.
Vanilla Shuffled: Keeps same branching paths from the WoL Campaign but randomizes the order of missions within."""
"""Determines the order the missions are played in. The last three mission orders end in a random mission.
Vanilla (29): Keeps the standard mission order and branching from the WoL Campaign.
Vanilla Shuffled (29): Keeps same branching paths from the WoL Campaign but randomizes the order of missions within.
Mini Campaign (15): Shorter version of the campaign with randomized missions and optional branches.
Grid (16): A 4x4 grid of random missions. Start at the top-left and forge a path towards All-In.
Mini Grid (9): A 3x3 version of Grid. Complete the bottom-right mission to win.
Blitz (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"
option_vanilla = 0
option_vanilla_shuffled = 1
option_mini_campaign = 2
option_grid = 3
option_mini_grid = 4
option_blitz = 5
option_gauntlet = 6
class ShuffleProtoss(DefaultOnToggle):
"""Determines if the 3 protoss missions are included in the shuffle if Vanilla Shuffled is enabled. If this is
not the 3 protoss missions will stay in their vanilla order in the mission order making them optional to complete
the game."""
"""Determines if the 3 protoss missions are included in the shuffle if Vanilla mission order is not enabled.
If turned off with Vanilla Shuffled, the 3 protoss missions will be in their normal position on the Prophecy chain if not shuffled.
If turned off with reduced mission settings, the 3 protoss missions will not appear and Protoss units are removed from the pool."""
display_name = "Shuffle Protoss Missions"
class RelegateNoBuildMissions(DefaultOnToggle):
"""If enabled, all no build missions besides the needed first one will be placed at the end of optional routes so
that none of them become required to complete the game. Only takes effect if mission order is not set to vanilla."""
display_name = "Relegate No-Build Missions"
class ShuffleNoBuild(DefaultOnToggle):
"""Determines if the 5 no-build missions are included in the shuffle if Vanilla mission order is not enabled.
If turned off with Vanilla Shuffled, one no-build mission will be placed as the first mission and the rest will be placed at the end of optional routes.
If turned off with reduced mission settings, the 5 no-build missions will not appear."""
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
@ -65,14 +115,29 @@ sc2wol_options: Dict[str, Option] = {
"all_in_map": AllInMap,
"mission_order": MissionOrder,
"shuffle_protoss": ShuffleProtoss,
"relegate_no_build": RelegateNoBuildMissions
"shuffle_no_build": ShuffleNoBuild,
"early_unit": EarlyUnit,
"required_tactics": RequiredTactics,
"units_always_have_upgrades": UnitsAlwaysHaveUpgrades,
"locked_items": LockedItems,
"excluded_items": ExcludedItems,
"excluded_missions": ExcludedMissions
}
def get_option_value(world: MultiWorld, player: int, name: str) -> int:
option = getattr(world, name, None)
if option == None:
if option is None:
return 0
return int(option[player].value)
def get_option_set_value(world: MultiWorld, player: int, name: str) -> 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 .Locations import LocationData
from .Options import get_option_value
from .MissionTables import MissionInfo, vanilla_shuffle_order, vanilla_mission_req_table, \
no_build_regions_list, easy_regions_list, medium_regions_list, hard_regions_list
from .MissionTables import MissionInfo, mission_orders, vanilla_mission_req_table, alt_final_mission_locations
from .PoolFilter import filter_missions
import random
def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData, ...], location_cache: List[Location]):
def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData, ...], location_cache: List[Location])\
-> Tuple[Dict[str, MissionInfo], int, str]:
locations_per_region = get_locations_per_region(locations)
regions = [
create_region(world, player, locations_per_region, location_cache, "Menu"),
create_region(world, player, locations_per_region, location_cache, "Liberation Day"),
create_region(world, player, locations_per_region, location_cache, "The Outlaws"),
create_region(world, player, locations_per_region, location_cache, "Zero Hour"),
create_region(world, player, locations_per_region, location_cache, "Evacuation"),
create_region(world, player, locations_per_region, location_cache, "Outbreak"),
create_region(world, player, locations_per_region, location_cache, "Safe Haven"),
create_region(world, player, locations_per_region, location_cache, "Haven's Fall"),
create_region(world, player, locations_per_region, location_cache, "Smash and Grab"),
create_region(world, player, locations_per_region, location_cache, "The Dig"),
create_region(world, player, locations_per_region, location_cache, "The Moebius Factor"),
create_region(world, player, locations_per_region, location_cache, "Supernova"),
create_region(world, player, locations_per_region, location_cache, "Maw of the Void"),
create_region(world, player, locations_per_region, location_cache, "Devil's Playground"),
create_region(world, player, locations_per_region, location_cache, "Welcome to the Jungle"),
create_region(world, player, locations_per_region, location_cache, "Breakout"),
create_region(world, player, locations_per_region, location_cache, "Ghost of a Chance"),
create_region(world, player, locations_per_region, location_cache, "The Great Train Robbery"),
create_region(world, player, locations_per_region, location_cache, "Cutthroat"),
create_region(world, player, locations_per_region, location_cache, "Engine of Destruction"),
create_region(world, player, locations_per_region, location_cache, "Media Blitz"),
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")
]
mission_order_type = get_option_value(world, player, "mission_order")
mission_order = mission_orders[mission_order_type]
mission_pools = filter_missions(world, player)
final_mission = mission_pools['all_in'][0]
used_regions = [mission for mission_pool in mission_pools.values() for mission in mission_pool]
regions = [create_region(world, player, locations_per_region, location_cache, "Menu")]
for region_name in used_regions:
regions.append(create_region(world, player, locations_per_region, location_cache, region_name))
# Changing the completion condition for alternate final missions into an event
if final_mission != 'All-In':
final_location = alt_final_mission_locations[final_mission]
# Final location should be near the end of the cache
for i in range(len(location_cache) - 1, -1, -1):
if location_cache[i].name == final_location:
location_cache[i].locked = True
location_cache[i].event = True
location_cache[i].address = None
break
else:
final_location = 'All-In: Victory'
if __debug__:
throwIfAnyLocationIsNotAssignedToARegion(regions, locations_per_region.keys())
if mission_order_type in (0, 1):
throwIfAnyLocationIsNotAssignedToARegion(regions, locations_per_region.keys())
world.regions += regions
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, 'Liberation Day', 'The Outlaws',
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 (
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 = []
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
for mission in vanilla_shuffle_order:
if mission.type == "all_in":
missions.append("All-In")
elif get_option_value(world, player, "relegate_no_build") and mission.relegate:
for mission in mission_order:
if mission is None:
missions.append(None)
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")
else:
missions.append(mission.type)
# Place Protoss Missions if we are not using ShuffleProtoss
if get_option_value(world, player, "shuffle_protoss") == 0:
# Place Protoss Missions if we are not using ShuffleProtoss and are in Vanilla Shuffled
if get_option_value(world, player, "shuffle_protoss") == 0 and mission_order_type == 1:
missions[22] = "A Sinister Turn"
medium_pool.remove("A Sinister Turn")
mission_pools['medium'].remove("A Sinister Turn")
missions[23] = "Echoes of the Future"
medium_pool.remove("Echoes of the Future")
mission_pools['medium'].remove("Echoes of the Future")
missions[24] = "In Utter Darkness"
hard_pool.remove("In Utter Darkness")
mission_pools['hard'].remove("In Utter Darkness")
no_build_slots = []
easy_slots = []
@ -153,6 +143,8 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
# Search through missions to find slots needed to fill
for i in range(len(missions)):
if missions[i] is None:
continue
if missions[i] == "no_build":
no_build_slots.append(i)
elif missions[i] == "easy":
@ -163,30 +155,30 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
hard_slots.append(i)
# 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:
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)
# 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:
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)
# 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:
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)
# 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:
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)
@ -195,7 +187,7 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
mission_req_table = {}
for i in range(len(missions)):
connections = []
for connection in vanilla_shuffle_order[i].connect_to:
for connection in mission_order[i].connect_to:
if connection == -1:
connect(world, player, names, "Menu", missions[i])
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
state._sc2wol_cleared_missions(world, player,
missions_req)))
(missions[connection], vanilla_shuffle_order[i].number))
(missions[connection], mission_order[i].number))
connections.append(connection + 1)
mission_req_table.update({missions[i]: MissionInfo(
vanilla_mission_req_table[missions[i]].id, vanilla_mission_req_table[missions[i]].extra_locations,
connections, vanilla_shuffle_order[i].category, number=vanilla_shuffle_order[i].number,
completion_critical=vanilla_shuffle_order[i].completion_critical,
or_requirements=vanilla_shuffle_order[i].or_requirements)})
vanilla_mission_req_table[missions[i]].id, connections, mission_order[i].category,
number=mission_order[i].number,
completion_critical=mission_order[i].completion_critical,
or_requirements=mission_order[i].or_requirements)})
return mission_req_table
final_mission_id = vanilla_mission_req_table[final_mission].id
return mission_req_table, final_mission_id, final_mission + ': Victory'
def throwIfAnyLocationIsNotAssignedToARegion(regions: List[Region], regionNames: Set[str]):

View File

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