| 
									
										
										
										
											2022-04-29 00:42:11 +02:00
										 |  |  | """
 | 
					
						
							|  |  |  | Archipelago init file for The Witness | 
					
						
							|  |  |  | """
 | 
					
						
							| 
									
										
										
										
											2023-11-24 06:27:03 +01:00
										 |  |  | import dataclasses | 
					
						
							| 
									
										
										
										
											2024-04-12 00:27:42 +02:00
										 |  |  | from logging import error, warning | 
					
						
							|  |  |  | from typing import Any, Dict, List, Optional, cast | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | from BaseClasses import CollectionState, Entrance, Location, Region, Tutorial | 
					
						
							| 
									
										
										
										
											2022-04-29 00:42:11 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-17 10:07:38 +02:00
										 |  |  | from Options import OptionError, PerGameCommonOptions, Toggle | 
					
						
							| 
									
										
										
										
											2024-04-12 00:27:42 +02:00
										 |  |  | from worlds.AutoWorld import WebWorld, World | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | from .data import static_items as static_witness_items | 
					
						
							| 
									
										
										
										
											2024-07-02 23:59:26 +02:00
										 |  |  | from .data import static_locations as static_witness_locations | 
					
						
							| 
									
										
										
										
											2024-04-12 00:27:42 +02:00
										 |  |  | from .data import static_logic as static_witness_logic | 
					
						
							|  |  |  | from .data.item_definition_classes import DoorItemDefinition, ItemData | 
					
						
							|  |  |  | from .data.utils import get_audio_logs | 
					
						
							| 
									
										
										
										
											2024-08-20 01:16:35 +02:00
										 |  |  | from .hints import CompactHintData, create_all_hints, make_compact_hint_data, make_laser_hints | 
					
						
							| 
									
										
										
										
											2024-07-02 23:59:26 +02:00
										 |  |  | from .locations import WitnessPlayerLocations | 
					
						
							| 
									
										
										
										
											2024-05-22 00:17:12 +02:00
										 |  |  | from .options import TheWitnessOptions, witness_option_groups | 
					
						
							| 
									
										
										
										
											2024-04-12 00:27:42 +02:00
										 |  |  | from .player_items import WitnessItem, WitnessPlayerItems | 
					
						
							| 
									
										
										
										
											2022-10-09 04:13:52 +02:00
										 |  |  | from .player_logic import WitnessPlayerLogic | 
					
						
							| 
									
										
										
										
											2024-04-12 00:27:42 +02:00
										 |  |  | from .presets import witness_option_presets | 
					
						
							|  |  |  | from .regions import WitnessPlayerRegions | 
					
						
							| 
									
										
										
										
											2023-11-24 06:27:03 +01:00
										 |  |  | from .rules import set_rules | 
					
						
							| 
									
										
										
										
											2022-04-29 00:42:11 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class WitnessWebWorld(WebWorld): | 
					
						
							|  |  |  |     theme = "jungle" | 
					
						
							| 
									
										
										
										
											2022-05-11 13:05:53 -05:00
										 |  |  |     tutorials = [Tutorial( | 
					
						
							|  |  |  |         "Multiworld Setup Guide", | 
					
						
							|  |  |  |         "A guide to playing The Witness with Archipelago.", | 
					
						
							|  |  |  |         "English", | 
					
						
							|  |  |  |         "setup_en.md", | 
					
						
							|  |  |  |         "setup/en", | 
					
						
							|  |  |  |         ["NewSoupVi", "Jarno"] | 
					
						
							|  |  |  |     )] | 
					
						
							| 
									
										
										
										
											2022-04-29 00:42:11 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-16 15:14:06 +01:00
										 |  |  |     options_presets = witness_option_presets | 
					
						
							| 
									
										
										
										
											2024-05-22 00:17:12 +02:00
										 |  |  |     option_groups = witness_option_groups | 
					
						
							| 
									
										
										
										
											2024-01-16 15:14:06 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-29 00:42:11 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | class WitnessWorld(World): | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     The Witness is an open-world puzzle game with dozens of locations | 
					
						
							|  |  |  |     to explore and over 500 puzzles. Play the popular puzzle randomizer | 
					
						
							|  |  |  |     by sigma144, with an added layer of progression randomization! | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     game = "The Witness" | 
					
						
							|  |  |  |     topology_present = False | 
					
						
							|  |  |  |     web = WitnessWebWorld() | 
					
						
							| 
									
										
										
										
											2023-11-24 06:27:03 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     options_dataclass = TheWitnessOptions | 
					
						
							|  |  |  |     options: TheWitnessOptions | 
					
						
							| 
									
										
										
										
											2022-04-29 00:42:11 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     item_name_to_id = { | 
					
						
							| 
									
										
										
										
											2024-07-02 23:59:26 +02:00
										 |  |  |         # ITEM_DATA doesn't have any event items in it | 
					
						
							|  |  |  |         name: cast(int, data.ap_code) for name, data in static_witness_items.ITEM_DATA.items() | 
					
						
							| 
									
										
										
										
											2022-04-29 00:42:11 +02:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2024-04-12 00:27:42 +02:00
										 |  |  |     location_name_to_id = static_witness_locations.ALL_LOCATIONS_TO_ID | 
					
						
							|  |  |  |     item_name_groups = static_witness_items.ITEM_GROUPS | 
					
						
							|  |  |  |     location_name_groups = static_witness_locations.AREA_LOCATION_GROUPS | 
					
						
							| 
									
										
										
										
											2022-04-29 00:42:11 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-05 22:53:00 +01:00
										 |  |  |     required_client_version = (0, 4, 5) | 
					
						
							| 
									
										
										
										
											2022-11-06 21:26:56 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-12 00:27:42 +02:00
										 |  |  |     player_logic: WitnessPlayerLogic | 
					
						
							|  |  |  |     player_locations: WitnessPlayerLocations | 
					
						
							|  |  |  |     player_items: WitnessPlayerItems | 
					
						
							|  |  |  |     player_regions: WitnessPlayerRegions | 
					
						
							| 
									
										
										
										
											2023-07-18 20:02:57 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-20 01:16:35 +02:00
										 |  |  |     log_ids_to_hints: Dict[int, CompactHintData] | 
					
						
							|  |  |  |     laser_ids_to_hints: Dict[int, CompactHintData] | 
					
						
							| 
									
										
										
										
											2023-07-18 20:02:57 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-12 00:27:42 +02:00
										 |  |  |     items_placed_early: List[str] | 
					
						
							|  |  |  |     own_itempool: List[WitnessItem] | 
					
						
							| 
									
										
										
										
											2023-11-24 06:27:03 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-20 01:16:35 +02:00
										 |  |  |     panel_hunt_required_count: int | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-12 00:27:42 +02:00
										 |  |  |     def _get_slot_data(self) -> Dict[str, Any]: | 
					
						
							| 
									
										
										
										
											2022-04-29 00:42:11 +02:00
										 |  |  |         return { | 
					
						
							| 
									
										
										
										
											2024-04-12 00:27:42 +02:00
										 |  |  |             "seed": self.random.randrange(0, 1000000), | 
					
						
							|  |  |  |             "victory_location": int(self.player_logic.VICTORY_LOCATION, 16), | 
					
						
							|  |  |  |             "panelhex_to_id": self.player_locations.CHECK_PANELHEX_TO_ID, | 
					
						
							|  |  |  |             "item_id_to_door_hexes": static_witness_items.get_item_to_door_mappings(), | 
					
						
							|  |  |  |             "door_hexes_in_the_pool": self.player_items.get_door_ids_in_pool(), | 
					
						
							|  |  |  |             "symbols_not_in_the_game": self.player_items.get_symbol_ids_not_in_pool(), | 
					
						
							|  |  |  |             "disabled_entities": [int(h, 16) for h in self.player_logic.COMPLETELY_DISABLED_ENTITIES], | 
					
						
							| 
									
										
										
										
											2024-08-20 01:16:35 +02:00
										 |  |  |             "hunt_entities": [int(h, 16) for h in self.player_logic.HUNT_ENTITIES], | 
					
						
							| 
									
										
										
										
											2024-04-12 00:27:42 +02:00
										 |  |  |             "log_ids_to_hints": self.log_ids_to_hints, | 
					
						
							|  |  |  |             "laser_ids_to_hints": self.laser_ids_to_hints, | 
					
						
							|  |  |  |             "progressive_item_lists": self.player_items.get_progressive_item_ids_in_pool(), | 
					
						
							|  |  |  |             "obelisk_side_id_to_EPs": static_witness_logic.OBELISK_SIDE_ID_TO_EP_HEXES, | 
					
						
							| 
									
										
										
										
											2024-08-23 00:23:05 +02:00
										 |  |  |             "precompleted_puzzles": [int(h, 16) for h in self.player_logic.EXCLUDED_ENTITIES], | 
					
						
							| 
									
										
										
										
											2024-04-12 00:27:42 +02:00
										 |  |  |             "entity_to_name": static_witness_logic.ENTITY_ID_TO_NAME, | 
					
						
							| 
									
										
										
										
											2024-08-20 01:16:35 +02:00
										 |  |  |             "panel_hunt_required_absolute": self.panel_hunt_required_count | 
					
						
							| 
									
										
										
										
											2022-04-29 00:42:11 +02:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-12 00:27:42 +02:00
										 |  |  |     def determine_sufficient_progression(self) -> None: | 
					
						
							| 
									
										
										
										
											2024-03-12 20:04:13 +01:00
										 |  |  |         """
 | 
					
						
							|  |  |  |         Determine whether there are enough progression items in this world to consider it "interactive". | 
					
						
							|  |  |  |         In the case of singleplayer, this just outputs a warning. | 
					
						
							|  |  |  |         In the case of multiplayer, the requirements are a bit stricter and an Exception is raised. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # A note on Obelisk Keys: | 
					
						
							|  |  |  |         # Obelisk Keys are never relevant in singleplayer, because the locations they lock are irrelevant to in-game | 
					
						
							|  |  |  |         # progress and irrelevant to all victory conditions. Thus, I consider them "fake progression" for singleplayer. | 
					
						
							|  |  |  |         # However, those locations could obviously contain big items needed for other players, so I consider | 
					
						
							|  |  |  |         # "Obelisk Keys only" valid for multiworld. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # A note on Laser Shuffle: | 
					
						
							|  |  |  |         # In singleplayer, I don't mind "Ice Rod Hunt" type gameplay, so "laser shuffle only" is valid. | 
					
						
							|  |  |  |         # However, I do not want to allow "Ice Rod Hunt" style gameplay in multiworld, so "laser shuffle only" is | 
					
						
							|  |  |  |         # not considered interactive enough for multiworld. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         interacts_sufficiently_with_multiworld = ( | 
					
						
							|  |  |  |             self.options.shuffle_symbols | 
					
						
							|  |  |  |             or self.options.shuffle_doors | 
					
						
							|  |  |  |             or self.options.obelisk_keys and self.options.shuffle_EPs | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         has_locally_relevant_progression = ( | 
					
						
							|  |  |  |             self.options.shuffle_symbols | 
					
						
							|  |  |  |             or self.options.shuffle_doors | 
					
						
							|  |  |  |             or self.options.shuffle_lasers | 
					
						
							|  |  |  |             or self.options.shuffle_boat | 
					
						
							|  |  |  |             or self.options.early_caves == "add_to_pool" and self.options.victory_condition == "challenge" | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if not has_locally_relevant_progression and self.multiworld.players == 1: | 
					
						
							| 
									
										
										
										
											2024-08-24 02:08:46 +02:00
										 |  |  |             warning(f"{self.player_name}'s Witness world doesn't have any progression" | 
					
						
							| 
									
										
										
										
											2024-03-12 20:04:13 +01:00
										 |  |  |                     f" items. Please turn on Symbol Shuffle, Door Shuffle or Laser Shuffle if that doesn't seem right.") | 
					
						
							|  |  |  |         elif not interacts_sufficiently_with_multiworld and self.multiworld.players > 1: | 
					
						
							| 
									
										
										
										
											2024-08-24 02:08:46 +02:00
										 |  |  |             raise OptionError(f"{self.player_name}'s Witness world doesn't have enough" | 
					
						
							| 
									
										
										
										
											2024-05-17 10:07:38 +02:00
										 |  |  |                               f" progression items that can be placed in other players' worlds. Please turn on Symbol" | 
					
						
							|  |  |  |                               f" Shuffle, Door Shuffle, or Obelisk Keys.") | 
					
						
							| 
									
										
										
										
											2024-03-12 20:04:13 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-12 00:27:42 +02:00
										 |  |  |     def generate_early(self) -> None: | 
					
						
							| 
									
										
										
										
											2024-01-16 15:33:34 +01:00
										 |  |  |         disabled_locations = self.options.exclude_locations.value | 
					
						
							| 
									
										
										
										
											2023-07-18 20:02:57 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |         self.player_logic = WitnessPlayerLogic( | 
					
						
							| 
									
										
										
										
											2024-01-16 15:33:34 +01:00
										 |  |  |             self, disabled_locations, self.options.start_inventory.value | 
					
						
							| 
									
										
										
										
											2023-07-18 20:02:57 -07:00
										 |  |  |         ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-12 00:27:42 +02:00
										 |  |  |         self.player_locations: WitnessPlayerLocations = WitnessPlayerLocations(self, self.player_logic) | 
					
						
							|  |  |  |         self.player_items: WitnessPlayerItems = WitnessPlayerItems( | 
					
						
							|  |  |  |             self, self.player_logic, self.player_locations | 
					
						
							| 
									
										
										
										
											2023-11-24 06:27:03 +01:00
										 |  |  |         ) | 
					
						
							| 
									
										
										
										
											2024-04-12 00:27:42 +02:00
										 |  |  |         self.player_regions: WitnessPlayerRegions = WitnessPlayerRegions(self.player_locations, self) | 
					
						
							| 
									
										
										
										
											2023-07-18 20:02:57 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-02 23:59:26 +02:00
										 |  |  |         self.log_ids_to_hints = {} | 
					
						
							| 
									
										
										
										
											2024-01-16 15:14:06 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-12 20:04:13 +01:00
										 |  |  |         self.determine_sufficient_progression() | 
					
						
							| 
									
										
										
										
											2024-01-16 15:14:06 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |         if self.options.shuffle_lasers == "local": | 
					
						
							|  |  |  |             self.options.local_items.value |= self.item_name_groups["Lasers"] | 
					
						
							| 
									
										
										
										
											2022-07-17 12:56:22 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-20 01:16:35 +02:00
										 |  |  |         if self.options.victory_condition == "panel_hunt": | 
					
						
							|  |  |  |             total_panels = self.options.panel_hunt_total | 
					
						
							|  |  |  |             required_percentage = self.options.panel_hunt_required_percentage | 
					
						
							|  |  |  |             self.panel_hunt_required_count = round(total_panels * required_percentage / 100) | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             self.panel_hunt_required_count = 0 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-12 00:27:42 +02:00
										 |  |  |     def create_regions(self) -> None: | 
					
						
							|  |  |  |         self.player_regions.create_regions(self, self.player_logic) | 
					
						
							| 
									
										
										
										
											2023-03-03 00:08:24 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-24 06:27:03 +01:00
										 |  |  |         # Set rules early so extra locations can be created based on the results of exploring collection states | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         set_rules(self) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-12 00:27:42 +02:00
										 |  |  |         # Start creating items | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         self.items_placed_early = [] | 
					
						
							|  |  |  |         self.own_itempool = [] | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-24 06:27:03 +01:00
										 |  |  |         # Add event items and tie them to event locations (e.g. laser activations). | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         event_locations = [] | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-12 00:27:42 +02:00
										 |  |  |         for event_location in self.player_locations.EVENT_LOCATION_TABLE: | 
					
						
							| 
									
										
										
										
											2023-11-24 06:27:03 +01:00
										 |  |  |             item_obj = self.create_item( | 
					
						
							| 
									
										
										
										
											2024-08-20 01:16:35 +02:00
										 |  |  |                 self.player_logic.EVENT_ITEM_PAIRS[event_location][0] | 
					
						
							| 
									
										
										
										
											2023-11-24 06:27:03 +01:00
										 |  |  |             ) | 
					
						
							| 
									
										
										
										
											2024-04-12 00:27:42 +02:00
										 |  |  |             location_obj = self.get_location(event_location) | 
					
						
							| 
									
										
										
										
											2023-11-24 06:27:03 +01:00
										 |  |  |             location_obj.place_locked_item(item_obj) | 
					
						
							|  |  |  |             self.own_itempool.append(item_obj) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             event_locations.append(location_obj) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Place other locked items | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-24 02:08:04 +02:00
										 |  |  |         if self.options.shuffle_dog == "puzzle_skip": | 
					
						
							|  |  |  |             dog_puzzle_skip = self.create_item("Puzzle Skip") | 
					
						
							|  |  |  |             self.get_location("Town Pet the Dog").place_locked_item(dog_puzzle_skip) | 
					
						
							| 
									
										
										
										
											2023-11-24 06:27:03 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-24 02:08:04 +02:00
										 |  |  |             self.own_itempool.append(dog_puzzle_skip) | 
					
						
							|  |  |  |             self.items_placed_early.append("Puzzle Skip") | 
					
						
							| 
									
										
										
										
											2023-11-24 06:27:03 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-06 13:40:55 +02:00
										 |  |  |         if self.options.early_symbol_item: | 
					
						
							|  |  |  |             # Pick an early item to place on the tutorial gate. | 
					
						
							|  |  |  |             early_items = [ | 
					
						
							|  |  |  |                 item for item in self.player_items.get_early_items() if item in self.player_items.get_mandatory_items() | 
					
						
							|  |  |  |             ] | 
					
						
							|  |  |  |             if early_items: | 
					
						
							|  |  |  |                 random_early_item = self.random.choice(early_items) | 
					
						
							| 
									
										
										
										
											2024-08-28 18:31:49 +02:00
										 |  |  |                 if ( | 
					
						
							|  |  |  |                     self.options.puzzle_randomization == "sigma_expert" | 
					
						
							|  |  |  |                     or self.options.victory_condition == "panel_hunt" | 
					
						
							|  |  |  |                 ): | 
					
						
							|  |  |  |                     # In Expert and Panel Hunt, only tag the item as early, rather than forcing it onto the gate. | 
					
						
							| 
									
										
										
										
											2024-07-06 13:40:55 +02:00
										 |  |  |                     self.multiworld.local_early_items[self.player][random_early_item] = 1 | 
					
						
							|  |  |  |                 else: | 
					
						
							|  |  |  |                     # Force the item onto the tutorial gate check and remove it from our random pool. | 
					
						
							|  |  |  |                     gate_item = self.create_item(random_early_item) | 
					
						
							|  |  |  |                     self.get_location("Tutorial Gate Open").place_locked_item(gate_item) | 
					
						
							|  |  |  |                     self.own_itempool.append(gate_item) | 
					
						
							|  |  |  |                     self.items_placed_early.append(random_early_item) | 
					
						
							| 
									
										
										
										
											2023-11-24 06:27:03 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-24 02:08:04 +02:00
										 |  |  |         # There are some really restrictive options in The Witness. | 
					
						
							| 
									
										
										
										
											2023-11-24 06:27:03 +01:00
										 |  |  |         # They are rarely played, but when they are, we add some extra sphere 1 locations. | 
					
						
							|  |  |  |         # This is done both to prevent generation failures, but also to make the early game less linear. | 
					
						
							|  |  |  |         # Only sweeps for events because having this behavior be random based on Tutorial Gate would be strange. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         state = CollectionState(self.multiworld) | 
					
						
							| 
									
										
										
										
											2024-08-23 01:15:05 +02:00
										 |  |  |         state.sweep_for_advancements(locations=event_locations) | 
					
						
							| 
									
										
										
										
											2023-11-24 06:27:03 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-24 02:08:04 +02:00
										 |  |  |         num_early_locs = sum( | 
					
						
							|  |  |  |             1 for loc in self.multiworld.get_reachable_locations(state, self.player) | 
					
						
							|  |  |  |             if loc.address and not loc.item | 
					
						
							|  |  |  |         ) | 
					
						
							| 
									
										
										
										
											2023-11-24 06:27:03 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-24 02:08:04 +02:00
										 |  |  |         # Adjust the needed size for sphere 1 based on how restrictive the options are in terms of items | 
					
						
							| 
									
										
										
										
											2023-07-18 20:02:57 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-24 02:08:04 +02:00
										 |  |  |         needed_size = 2 | 
					
						
							| 
									
										
										
										
											2024-02-11 02:25:03 +01:00
										 |  |  |         needed_size += self.options.puzzle_randomization == "sigma_expert" | 
					
						
							| 
									
										
										
										
											2023-11-24 06:27:03 +01:00
										 |  |  |         needed_size += self.options.shuffle_symbols | 
					
						
							|  |  |  |         needed_size += self.options.shuffle_doors > 0 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Then, add checks in order until the required amount of sphere 1 checks is met. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         extra_checks = [ | 
					
						
							| 
									
										
										
										
											2024-02-28 04:44:22 +01:00
										 |  |  |             ("Tutorial First Hallway Room", "Tutorial First Hallway Bend"), | 
					
						
							|  |  |  |             ("Tutorial First Hallway", "Tutorial First Hallway Straight"), | 
					
						
							| 
									
										
										
										
											2024-01-16 15:11:52 +01:00
										 |  |  |             ("Desert Outside", "Desert Surface 1"), | 
					
						
							|  |  |  |             ("Desert Outside", "Desert Surface 2"), | 
					
						
							| 
									
										
										
										
											2023-11-24 06:27:03 +01:00
										 |  |  |         ] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         for i in range(num_early_locs, needed_size): | 
					
						
							|  |  |  |             if not extra_checks: | 
					
						
							|  |  |  |                 break | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             region, loc = extra_checks.pop(0) | 
					
						
							| 
									
										
										
										
											2024-04-12 00:27:42 +02:00
										 |  |  |             self.player_locations.add_location_late(loc) | 
					
						
							|  |  |  |             self.get_region(region).add_locations({loc: self.location_name_to_id[loc]}) | 
					
						
							| 
									
										
										
										
											2023-11-24 06:27:03 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-24 02:08:46 +02:00
										 |  |  |             warning( | 
					
						
							|  |  |  |                 f"""Location "{loc}" had to be added to {self.player_name}'s world 
 | 
					
						
							|  |  |  |                 due to insufficient sphere 1 size."""
 | 
					
						
							|  |  |  |             ) | 
					
						
							| 
									
										
										
										
											2023-11-24 06:27:03 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-12 00:27:42 +02:00
										 |  |  |     def create_items(self) -> None: | 
					
						
							| 
									
										
										
										
											2023-11-24 06:27:03 +01:00
										 |  |  |         # Determine pool size. | 
					
						
							| 
									
										
										
										
											2024-04-12 00:27:42 +02:00
										 |  |  |         pool_size = len(self.player_locations.CHECK_LOCATION_TABLE) - len(self.player_locations.EVENT_LOCATION_TABLE) | 
					
						
							| 
									
										
										
										
											2023-07-18 20:02:57 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |         # Fill mandatory items and remove precollected and/or starting items from the pool. | 
					
						
							| 
									
										
										
										
											2024-04-12 00:27:42 +02:00
										 |  |  |         item_pool = self.player_items.get_mandatory_items() | 
					
						
							| 
									
										
										
										
											2023-07-18 20:02:57 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-24 06:27:03 +01:00
										 |  |  |         # Remove one copy of each item that was placed early | 
					
						
							|  |  |  |         for already_placed in self.items_placed_early: | 
					
						
							|  |  |  |             pool_size -= 1 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if already_placed not in item_pool: | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if item_pool[already_placed] == 1: | 
					
						
							|  |  |  |                 item_pool.pop(already_placed) | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 item_pool[already_placed] -= 1 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-07-18 20:02:57 -07:00
										 |  |  |         for precollected_item_name in [item.name for item in self.multiworld.precollected_items[self.player]]: | 
					
						
							|  |  |  |             if precollected_item_name in item_pool: | 
					
						
							|  |  |  |                 if item_pool[precollected_item_name] == 1: | 
					
						
							|  |  |  |                     item_pool.pop(precollected_item_name) | 
					
						
							|  |  |  |                 else: | 
					
						
							|  |  |  |                     item_pool[precollected_item_name] -= 1 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         for inventory_item_name in self.player_logic.STARTING_INVENTORY: | 
					
						
							|  |  |  |             if inventory_item_name in item_pool: | 
					
						
							|  |  |  |                 if item_pool[inventory_item_name] == 1: | 
					
						
							|  |  |  |                     item_pool.pop(inventory_item_name) | 
					
						
							|  |  |  |                 else: | 
					
						
							|  |  |  |                     item_pool[inventory_item_name] -= 1 | 
					
						
							| 
									
										
										
										
											2023-07-28 09:39:56 +02:00
										 |  |  |             self.multiworld.push_precollected(self.create_item(inventory_item_name)) | 
					
						
							| 
									
										
										
										
											2023-07-18 20:02:57 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |         if len(item_pool) > pool_size: | 
					
						
							| 
									
										
										
										
											2024-08-24 02:08:46 +02:00
										 |  |  |             error(f"{self.player_name}'s Witness world has too few locations ({pool_size})" | 
					
						
							| 
									
										
										
										
											2023-11-24 06:27:03 +01:00
										 |  |  |                   f" to place its necessary items ({len(item_pool)}).") | 
					
						
							| 
									
										
										
										
											2023-07-18 20:02:57 -07:00
										 |  |  |             return | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         remaining_item_slots = pool_size - sum(item_pool.values()) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Add puzzle skips. | 
					
						
							| 
									
										
										
										
											2024-07-02 23:59:26 +02:00
										 |  |  |         num_puzzle_skips = self.options.puzzle_skip_amount.value | 
					
						
							| 
									
										
										
										
											2023-11-24 06:27:03 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-07-18 20:02:57 -07:00
										 |  |  |         if num_puzzle_skips > remaining_item_slots: | 
					
						
							| 
									
										
										
										
											2024-08-24 02:08:46 +02:00
										 |  |  |             warning(f"{self.player_name}'s Witness world has insufficient locations" | 
					
						
							| 
									
										
										
										
											2023-11-24 06:27:03 +01:00
										 |  |  |                     f" to place all requested puzzle skips.") | 
					
						
							| 
									
										
										
										
											2023-07-18 20:02:57 -07:00
										 |  |  |             num_puzzle_skips = remaining_item_slots | 
					
						
							|  |  |  |         item_pool["Puzzle Skip"] = num_puzzle_skips | 
					
						
							|  |  |  |         remaining_item_slots -= num_puzzle_skips | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Add junk items. | 
					
						
							|  |  |  |         if remaining_item_slots > 0: | 
					
						
							| 
									
										
										
										
											2024-04-12 00:27:42 +02:00
										 |  |  |             item_pool.update(self.player_items.get_filler_items(remaining_item_slots)) | 
					
						
							| 
									
										
										
										
											2023-07-18 20:02:57 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |         # Generate the actual items. | 
					
						
							| 
									
										
										
										
											2023-07-20 01:20:59 +02:00
										 |  |  |         for item_name, quantity in sorted(item_pool.items()): | 
					
						
							| 
									
										
										
										
											2023-11-24 06:27:03 +01:00
										 |  |  |             new_items = [self.create_item(item_name) for _ in range(0, quantity)] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             self.own_itempool += new_items | 
					
						
							|  |  |  |             self.multiworld.itempool += new_items | 
					
						
							| 
									
										
										
										
											2024-04-12 00:27:42 +02:00
										 |  |  |             if self.player_items.item_data[item_name].local_only: | 
					
						
							| 
									
										
										
										
											2024-01-16 15:33:34 +01:00
										 |  |  |                 self.options.local_items.value.add(item_name) | 
					
						
							| 
									
										
										
										
											2023-07-18 20:02:57 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-02 23:59:26 +02:00
										 |  |  |     def fill_slot_data(self) -> Dict[str, Any]: | 
					
						
							| 
									
										
										
										
											2024-08-20 01:16:35 +02:00
										 |  |  |         self.log_ids_to_hints: Dict[int, CompactHintData] = {} | 
					
						
							|  |  |  |         self.laser_ids_to_hints: Dict[int, CompactHintData] = {} | 
					
						
							| 
									
										
										
										
											2024-04-12 00:27:42 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-05 22:54:02 +01:00
										 |  |  |         already_hinted_locations = set() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Laser hints | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if self.options.laser_hints: | 
					
						
							| 
									
										
										
										
											2024-07-02 23:59:26 +02:00
										 |  |  |             laser_hints = make_laser_hints(self, sorted(static_witness_items.ITEM_GROUPS["Lasers"])) | 
					
						
							| 
									
										
										
										
											2024-03-05 22:54:02 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |             for item_name, hint in laser_hints.items(): | 
					
						
							| 
									
										
										
										
											2024-04-12 00:27:42 +02:00
										 |  |  |                 item_def = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name]) | 
					
						
							| 
									
										
										
										
											2024-03-05 22:54:02 +01:00
										 |  |  |                 self.laser_ids_to_hints[int(item_def.panel_id_hexes[0], 16)] = make_compact_hint_data(hint, self.player) | 
					
						
							| 
									
										
										
										
											2024-07-02 23:59:26 +02:00
										 |  |  |                 already_hinted_locations.add(cast(Location, hint.location)) | 
					
						
							| 
									
										
										
										
											2024-03-05 22:54:02 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |         # Audio Log Hints | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-24 06:27:03 +01:00
										 |  |  |         hint_amount = self.options.hint_amount.value | 
					
						
							| 
									
										
										
										
											2022-10-09 04:13:52 +02:00
										 |  |  |         audio_logs = get_audio_logs().copy() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-11 02:25:03 +01:00
										 |  |  |         if hint_amount: | 
					
						
							| 
									
										
										
										
											2024-02-28 04:44:22 +01:00
										 |  |  |             area_hints = round(self.options.area_hint_percentage / 100 * hint_amount) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-05 22:54:02 +01:00
										 |  |  |             generated_hints = create_all_hints(self, hint_amount, area_hints, already_hinted_locations) | 
					
						
							| 
									
										
										
										
											2022-10-09 04:13:52 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-24 06:27:03 +01:00
										 |  |  |             self.random.shuffle(audio_logs) | 
					
						
							| 
									
										
										
										
											2022-04-29 00:42:11 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-21 00:45:26 +02:00
										 |  |  |             duplicates = min(3, len(audio_logs) // hint_amount) | 
					
						
							| 
									
										
										
										
											2022-10-09 04:13:52 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-28 04:44:22 +01:00
										 |  |  |             for hint in generated_hints: | 
					
						
							| 
									
										
										
										
											2024-03-05 22:54:02 +01:00
										 |  |  |                 compact_hint_data = make_compact_hint_data(hint, self.player) | 
					
						
							| 
									
										
										
										
											2022-10-09 04:13:52 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |                 for _ in range(0, duplicates): | 
					
						
							|  |  |  |                     audio_log = audio_logs.pop() | 
					
						
							| 
									
										
										
										
											2024-03-05 22:54:02 +01:00
										 |  |  |                     self.log_ids_to_hints[int(audio_log, 16)] = compact_hint_data | 
					
						
							| 
									
										
										
										
											2022-10-09 04:13:52 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-13 17:46:11 -05:00
										 |  |  |         # Client will generate joke hints for these. | 
					
						
							|  |  |  |         self.log_ids_to_hints.update({int(audio_log, 16): ("", -1, -1) for audio_log in audio_logs}) | 
					
						
							| 
									
										
										
										
											2022-10-09 04:13:52 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-05 22:54:02 +01:00
										 |  |  |         # Options for the client & auto-tracker | 
					
						
							| 
									
										
										
										
											2022-10-09 04:13:52 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         slot_data = self._get_slot_data() | 
					
						
							| 
									
										
										
										
											2022-04-29 00:42:11 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-24 06:27:03 +01:00
										 |  |  |         for option_name in (attr.name for attr in dataclasses.fields(TheWitnessOptions) | 
					
						
							|  |  |  |                             if attr not in dataclasses.fields(PerGameCommonOptions)): | 
					
						
							|  |  |  |             option = getattr(self.options, option_name) | 
					
						
							|  |  |  |             slot_data[option_name] = bool(option.value) if isinstance(option, Toggle) else option.value | 
					
						
							| 
									
										
										
										
											2022-04-29 00:42:11 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         return slot_data | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-12 00:27:42 +02:00
										 |  |  |     def create_item(self, item_name: str) -> WitnessItem: | 
					
						
							| 
									
										
										
										
											2023-08-16 07:03:41 -07:00
										 |  |  |         # If the player's plando options are malformed, the item_name parameter could be a dictionary containing the | 
					
						
							|  |  |  |         #   name of the item, rather than the item itself. This is a workaround to prevent a crash. | 
					
						
							| 
									
										
										
										
											2024-04-12 00:27:42 +02:00
										 |  |  |         if isinstance(item_name, dict): | 
					
						
							|  |  |  |             item_name = next(iter(item_name)) | 
					
						
							| 
									
										
										
										
											2023-08-16 07:03:41 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-29 00:42:11 +02:00
										 |  |  |         # this conditional is purely for unit tests, which need to be able to create an item before generate_early | 
					
						
							| 
									
										
										
										
											2023-07-18 20:02:57 -07:00
										 |  |  |         item_data: ItemData | 
					
						
							| 
									
										
										
										
											2024-04-12 00:27:42 +02:00
										 |  |  |         if hasattr(self, "player_items") and self.player_items and item_name in self.player_items.item_data: | 
					
						
							|  |  |  |             item_data = self.player_items.item_data[item_name] | 
					
						
							| 
									
										
										
										
											2022-06-17 03:23:27 +02:00
										 |  |  |         else: | 
					
						
							| 
									
										
										
										
											2024-04-12 00:27:42 +02:00
										 |  |  |             item_data = static_witness_items.ITEM_DATA[item_name] | 
					
						
							| 
									
										
										
										
											2022-04-29 00:42:11 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-07-18 20:02:57 -07:00
										 |  |  |         return WitnessItem(item_name, item_data.classification, item_data.ap_code, player=self.player) | 
					
						
							| 
									
										
										
										
											2022-04-29 00:42:11 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-24 01:48:20 +02:00
										 |  |  |     def get_filler_item_name(self) -> str: | 
					
						
							|  |  |  |         return "Speed Boost" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-29 00:42:11 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | class WitnessLocation(Location): | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Archipelago Location for The Witness | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     game: str = "The Witness" | 
					
						
							| 
									
										
										
										
											2023-11-24 06:27:03 +01:00
										 |  |  |     entity_hex: int = -1 | 
					
						
							| 
									
										
										
										
											2022-04-29 00:42:11 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-02 23:59:26 +02:00
										 |  |  |     def __init__(self, player: int, name: str, address: Optional[int], parent: Region, ch_hex: int = -1) -> None: | 
					
						
							| 
									
										
										
										
											2022-04-29 00:42:11 +02:00
										 |  |  |         super().__init__(player, name, address, parent) | 
					
						
							| 
									
										
										
										
											2023-11-24 06:27:03 +01:00
										 |  |  |         self.entity_hex = ch_hex | 
					
						
							| 
									
										
										
										
											2022-04-29 00:42:11 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-12 00:27:42 +02:00
										 |  |  | def create_region(world: WitnessWorld, name: str, player_locations: WitnessPlayerLocations, | 
					
						
							| 
									
										
										
										
											2024-07-02 23:59:26 +02:00
										 |  |  |                   region_locations: Optional[List[str]] = None, exits: Optional[List[str]] = None) -> Region: | 
					
						
							| 
									
										
										
										
											2022-04-29 00:42:11 +02:00
										 |  |  |     """
 | 
					
						
							|  |  |  |     Create an Archipelago Region for The Witness | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-24 06:27:03 +01:00
										 |  |  |     ret = Region(name, world.player, world.multiworld) | 
					
						
							| 
									
										
										
										
											2022-04-29 00:42:11 +02:00
										 |  |  |     if region_locations: | 
					
						
							|  |  |  |         for location in region_locations: | 
					
						
							| 
									
										
										
										
											2024-04-12 00:27:42 +02:00
										 |  |  |             loc_id = player_locations.CHECK_LOCATION_TABLE[location] | 
					
						
							| 
									
										
										
										
											2022-04-29 00:42:11 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-24 06:27:03 +01:00
										 |  |  |             entity_hex = -1 | 
					
						
							| 
									
										
										
										
											2024-04-12 00:27:42 +02:00
										 |  |  |             if location in static_witness_logic.ENTITIES_BY_NAME: | 
					
						
							| 
									
										
										
										
											2023-11-24 06:27:03 +01:00
										 |  |  |                 entity_hex = int( | 
					
						
							| 
									
										
										
										
											2024-04-12 00:27:42 +02:00
										 |  |  |                     static_witness_logic.ENTITIES_BY_NAME[location]["entity_hex"], 0 | 
					
						
							| 
									
										
										
										
											2022-04-29 00:42:11 +02:00
										 |  |  |                 ) | 
					
						
							| 
									
										
										
										
											2024-07-02 23:59:26 +02:00
										 |  |  |             location_obj = WitnessLocation( | 
					
						
							| 
									
										
										
										
											2023-11-24 06:27:03 +01:00
										 |  |  |                 world.player, location, loc_id, ret, entity_hex | 
					
						
							| 
									
										
										
										
											2022-04-29 00:42:11 +02:00
										 |  |  |             ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-02 23:59:26 +02:00
										 |  |  |             ret.locations.append(location_obj) | 
					
						
							| 
									
										
										
										
											2022-04-29 00:42:11 +02:00
										 |  |  |     if exits: | 
					
						
							|  |  |  |         for single_exit in exits: | 
					
						
							| 
									
										
										
										
											2023-11-24 06:27:03 +01:00
										 |  |  |             ret.exits.append(Entrance(world.player, single_exit, ret)) | 
					
						
							| 
									
										
										
										
											2022-04-29 00:42:11 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     return ret |