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:
		
							parent
							
								
									d5efc71344
								
							
						
					
					
						commit
						700fe8b75e
					
				|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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_common_unit(world, player) or state.has("Reaper", player))), |                                    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: 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) | ||||||
|  |  | ||||||
|  | @ -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) | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|  |  | ||||||
|  | @ -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" | ||||||
|  | } | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  | @ -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__: | ||||||
|         throwIfAnyLocationIsNotAssignedToARegion(regions, locations_per_region.keys()) |         if mission_order_type in (0, 1): | ||||||
|  |             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]): | ||||||
|  |  | ||||||
|  | @ -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 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) |         # The first world should also be the starting world | ||||||
|     if not local_basic_unit: |         first_mission = list(world.worlds[player].mission_req_table)[0] | ||||||
|         raise Exception("At least one basic unit must be local") |         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 |         return [assign_starter_item(world, player, excluded_items, locked_locations, first_location, local_basic_unit)] | ||||||
|     first_location = list(world.worlds[player].mission_req_table)[0] |  | ||||||
| 
 |  | ||||||
|     if first_location == "In Utter Darkness": |  | ||||||
|         first_location = first_location + ": Defeat" |  | ||||||
|     else: |     else: | ||||||
|         first_location = first_location + ": Victory" |         return [] | ||||||
| 
 |  | ||||||
|     assign_starter_item(world, player, excluded_items, locked_locations, first_location, |  | ||||||
|                         local_basic_unit) |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 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) | ||||||
|                 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], | def fill_item_pool_with_dummy_items(self: SC2WoLWorld, world: MultiWorld, player: int, locked_locations: List[str], | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue