 805f33c39e
			
		
	
	805f33c39e
	
	
	
		
			
			* Make all Keep Pressure Plates logically required for the Laser Panel * Added more Tutorial checks * Added the remaining two Shipwreck Boat EPs to the exclude list for normal * Improved itempool filling system, added warning if usefuls had to be eaten * Moved creation of said warning string to utils * Fixed logic bug causing broken seeds on Mountain Floor 2 * Hints system change * Expert Logic Fix * Fixed typo * Better wording * Added missing games to junk hints * Made sure Entrance names are unique * Fixed missing Obelisk Side * Disable Non Randomized + EP Shuffle fix * Fixed disable_non_randomized precompleted EPs being 'disabled' instead of 'precompleted' * Fixed if/elif error * Tutorial Gate Open local symbol item becomes local_early_item in expert instead * Bump required client version. There is a beta client that sends 0.3.9. * Removed print statement, oops * Fixed itempool manipulation in pre_fill * Replaced string concats with fstrings * Improved make_warning_string function signature Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Improved performance on removing multiple items from multiworld itempool * Comment * Fixed errors with the code * Made removal from itempool not fail unit test for multiple references * Moved all item creation to create_items, got rid of itempool modifying system * Colored Squares is no longer a good item, that's outdated * Removed double if * React to from_pool: false by removing a junk item * Fixed warning if only Fnc Brain was removed * Make use of string truthiness instead * Made reading of plandoed items safer --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
		
			
				
	
	
		
			247 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			247 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| """
 | |
| Defines progression, junk and event items for The Witness
 | |
| """
 | |
| import copy
 | |
| from collections import defaultdict
 | |
| from typing import Dict, NamedTuple, Optional, Set
 | |
| 
 | |
| from BaseClasses import Item, MultiWorld
 | |
| from . import StaticWitnessLogic, WitnessPlayerLocations, WitnessPlayerLogic
 | |
| from .Options import get_option_value, is_option_enabled, the_witness_options
 | |
| from fractions import Fraction
 | |
| 
 | |
| 
 | |
| class ItemData(NamedTuple):
 | |
|     """
 | |
|     ItemData for an item in The Witness
 | |
|     """
 | |
|     code: Optional[int]
 | |
|     progression: bool
 | |
|     event: bool = False
 | |
|     trap: bool = False
 | |
|     never_exclude: bool = False
 | |
| 
 | |
| 
 | |
| class WitnessItem(Item):
 | |
|     """
 | |
|     Item from the game The Witness
 | |
|     """
 | |
|     game: str = "The Witness"
 | |
| 
 | |
| 
 | |
| class StaticWitnessItems:
 | |
|     """
 | |
|     Class that handles Witness items independent of world settings
 | |
|     """
 | |
| 
 | |
|     ALL_ITEM_TABLE: Dict[str, ItemData] = {}
 | |
| 
 | |
|     ITEM_NAME_GROUPS: Dict[str, Set[str]] = dict()
 | |
| 
 | |
|     # These should always add up to 1!!!
 | |
|     BONUS_WEIGHTS = {
 | |
|         "Speed Boost": Fraction(1, 1),
 | |
|     }
 | |
| 
 | |
|     # These should always add up to 1!!!
 | |
|     TRAP_WEIGHTS = {   
 | |
|         "Slowness": Fraction(8, 10),
 | |
|         "Power Surge": Fraction(2, 10),
 | |
|     }
 | |
| 
 | |
|     ALL_JUNK_ITEMS = set(BONUS_WEIGHTS.keys()) | set(TRAP_WEIGHTS.keys())
 | |
| 
 | |
|     ITEM_ID_TO_DOOR_HEX_ALL = dict()
 | |
| 
 | |
|     def __init__(self):
 | |
|         item_tab = dict()
 | |
| 
 | |
|         for item in StaticWitnessLogic.ALL_SYMBOL_ITEMS:
 | |
|             if item[0] == "11 Lasers" or item == "7 Lasers":
 | |
|                 continue
 | |
| 
 | |
|             item_tab[item[0]] = ItemData(158000 + item[1], True, False)
 | |
| 
 | |
|             self.ITEM_NAME_GROUPS.setdefault("Symbols", set()).add(item[0])
 | |
| 
 | |
|         for progressive, item_list in StaticWitnessLogic.PROGRESSIVE_TO_ITEMS.items():
 | |
|             if not item_list:
 | |
|                 continue
 | |
| 
 | |
|             if item_list[0] in self.ITEM_NAME_GROUPS.setdefault("Symbols", set()):
 | |
|                 self.ITEM_NAME_GROUPS.setdefault("Symbols", set()).add(progressive)
 | |
| 
 | |
|         for item in StaticWitnessLogic.ALL_DOOR_ITEMS:
 | |
|             item_tab[item[0]] = ItemData(158000 + item[1], True, False)
 | |
| 
 | |
|             # 1500 - 1510 are the laser items, which are handled like doors but should be their own separate group.
 | |
|             if item[1] in range(1500, 1511):
 | |
|                 self.ITEM_NAME_GROUPS.setdefault("Lasers", set()).add(item[0])
 | |
|             else:
 | |
|                 self.ITEM_NAME_GROUPS.setdefault("Doors", set()).add(item[0])
 | |
| 
 | |
|         for item in StaticWitnessLogic.ALL_TRAPS:
 | |
|             item_tab[item[0]] = ItemData(
 | |
|                 158000 + item[1], False, False, True
 | |
|             )
 | |
| 
 | |
|         for item in StaticWitnessLogic.ALL_BOOSTS:
 | |
|             item_tab[item[0]] = ItemData(158000 + item[1], False, False)
 | |
| 
 | |
|         for item in StaticWitnessLogic.ALL_USEFULS:
 | |
|             item_tab[item[0]] = ItemData(158000 + item[1], False, False, False, item[2])
 | |
| 
 | |
|         item_tab = dict(sorted(
 | |
|             item_tab.items(),
 | |
|             key=lambda single_item: single_item[1].code
 | |
|             if isinstance(single_item[1].code, int) else 0)
 | |
|         )
 | |
| 
 | |
|         for key, item in item_tab.items():
 | |
|             self.ALL_ITEM_TABLE[key] = item
 | |
| 
 | |
|         for door in StaticWitnessLogic.ALL_DOOR_ITEMS:
 | |
|             self.ITEM_ID_TO_DOOR_HEX_ALL[door[1] + 158000] = {int(door_hex, 16) for door_hex in door[2]}
 | |
| 
 | |
| 
 | |
| class WitnessPlayerItems:
 | |
|     """
 | |
|     Class that defines Items for a single world
 | |
|     """
 | |
| 
 | |
|     @staticmethod
 | |
|     def code(item_name: str):
 | |
|         return StaticWitnessItems.ALL_ITEM_TABLE[item_name].code
 | |
| 
 | |
|     @staticmethod
 | |
|     def is_progression(item_name: str, multiworld: MultiWorld, player: int):
 | |
|         useless_doors = {
 | |
|             "River Monastery Shortcut (Door)",
 | |
|             "Jungle & River Shortcuts",
 | |
|             "Monastery Shortcut (Door)",
 | |
|             "Orchard Second Gate (Door)",
 | |
|         }
 | |
| 
 | |
|         if item_name in useless_doors:
 | |
|             return False
 | |
| 
 | |
|         ep_doors = {
 | |
|             "Monastery Garden Entry (Door)",
 | |
|             "Monastery Shortcuts",
 | |
|         }
 | |
| 
 | |
|         if item_name in ep_doors:
 | |
|             return get_option_value(multiworld, player, "shuffle_EPs") != 0
 | |
| 
 | |
|         return True
 | |
| 
 | |
|     def __init__(self, locat: WitnessPlayerLocations, multiworld: MultiWorld, player: int, logic: WitnessPlayerLogic):
 | |
|         """Adds event items after logic changes due to options"""
 | |
|         self.EVENT_ITEM_TABLE = dict()
 | |
|         self.ITEM_TABLE = copy.copy(StaticWitnessItems.ALL_ITEM_TABLE)
 | |
| 
 | |
|         self.PROGRESSION_TABLE = dict()
 | |
| 
 | |
|         self.ITEM_ID_TO_DOOR_HEX = dict()
 | |
|         self.DOORS = set()
 | |
| 
 | |
|         self.PROG_ITEM_AMOUNTS = defaultdict(lambda: 1)
 | |
| 
 | |
|         self.SYMBOLS_NOT_IN_THE_GAME = set()
 | |
| 
 | |
|         self.EXTRA_AMOUNTS = {
 | |
|             "Functioning Brain": 1,
 | |
|             "Puzzle Skip": get_option_value(multiworld, player, "puzzle_skip_amount")
 | |
|         }
 | |
| 
 | |
|         for k, v in self.ITEM_TABLE.items():
 | |
|             if v.progression and not self.is_progression(k, multiworld, player):
 | |
|                 self.ITEM_TABLE[k] = ItemData(v.code, False, False, never_exclude=True)
 | |
| 
 | |
|         for item in StaticWitnessLogic.ALL_SYMBOL_ITEMS.union(StaticWitnessLogic.ALL_DOOR_ITEMS):
 | |
|             if item[0] not in logic.PROG_ITEMS_ACTUALLY_IN_THE_GAME:
 | |
|                 del self.ITEM_TABLE[item[0]]
 | |
|                 if item in StaticWitnessLogic.ALL_SYMBOL_ITEMS:
 | |
|                     self.SYMBOLS_NOT_IN_THE_GAME.add(StaticWitnessItems.ALL_ITEM_TABLE[item[0]].code)
 | |
|             else:
 | |
|                 if item[0] in StaticWitnessLogic.PROGRESSIVE_TO_ITEMS:
 | |
|                     self.PROG_ITEM_AMOUNTS[item[0]] = len(logic.MULTI_LISTS[item[0]])
 | |
| 
 | |
|                 self.PROGRESSION_TABLE[item[0]] = self.ITEM_TABLE[item[0]]
 | |
| 
 | |
|         self.MULTI_LISTS_BY_CODE = dict()
 | |
| 
 | |
|         for item in self.PROG_ITEM_AMOUNTS:
 | |
|             multi_list = logic.MULTI_LISTS[item]
 | |
|             self.MULTI_LISTS_BY_CODE[self.code(item)] = [self.code(single_item) for single_item in multi_list]
 | |
| 
 | |
|         for entity_hex, items in logic.DOOR_ITEMS_BY_ID.items():
 | |
|             entity_hex_int = int(entity_hex, 16)
 | |
| 
 | |
|             self.DOORS.add(entity_hex_int)
 | |
| 
 | |
|             for item in items:
 | |
|                 item_id = StaticWitnessItems.ALL_ITEM_TABLE[item].code
 | |
|                 self.ITEM_ID_TO_DOOR_HEX.setdefault(item_id, set()).add(entity_hex_int)
 | |
| 
 | |
|         symbols = is_option_enabled(multiworld, player, "shuffle_symbols")
 | |
| 
 | |
|         if "shuffle_symbols" not in the_witness_options.keys():
 | |
|             symbols = True
 | |
| 
 | |
|         doors = get_option_value(multiworld, player, "shuffle_doors")
 | |
| 
 | |
|         self.GOOD_ITEMS = []
 | |
| 
 | |
|         if symbols:
 | |
|             self.GOOD_ITEMS = [
 | |
|                 "Dots", "Black/White Squares", "Stars",
 | |
|                 "Shapers", "Symmetry"
 | |
|             ]
 | |
| 
 | |
|             if doors:
 | |
|                 self.GOOD_ITEMS = [
 | |
|                     "Dots", "Black/White Squares", "Symmetry"
 | |
|                 ]
 | |
| 
 | |
|             if is_option_enabled(multiworld, player, "shuffle_discarded_panels"):
 | |
|                 if get_option_value(multiworld, player, "puzzle_randomization") == 1:
 | |
|                     self.GOOD_ITEMS.append("Arrows")
 | |
|                 else:
 | |
|                     self.GOOD_ITEMS.append("Triangles")
 | |
| 
 | |
|             self.GOOD_ITEMS = [
 | |
|                 StaticWitnessLogic.ITEMS_TO_PROGRESSIVE.get(item, item) for item in self.GOOD_ITEMS
 | |
|             ]
 | |
| 
 | |
|         for event_location in locat.EVENT_LOCATION_TABLE:
 | |
|             location = logic.EVENT_ITEM_PAIRS[event_location]
 | |
|             self.EVENT_ITEM_TABLE[location] = ItemData(None, True, True)
 | |
|             self.ITEM_TABLE[location] = ItemData(None, True, True)
 | |
| 
 | |
|         trap_percentage = get_option_value(multiworld, player, "trap_percentage")
 | |
| 
 | |
|         self.JUNK_WEIGHTS = dict()
 | |
| 
 | |
|         if trap_percentage != 0:
 | |
|             # I'm sure there must be some super "pythonic" way of doing this :D
 | |
| 
 | |
|             for trap_name, trap_weight in StaticWitnessItems.TRAP_WEIGHTS.items():
 | |
|                 self.JUNK_WEIGHTS[trap_name] = (trap_weight * trap_percentage) / 100
 | |
| 
 | |
|         if trap_percentage != 100:
 | |
|             for bonus_name, bonus_weight in StaticWitnessItems.BONUS_WEIGHTS.items():
 | |
|                 self.JUNK_WEIGHTS[bonus_name] = (bonus_weight * (100 - trap_percentage)) / 100
 | |
| 
 | |
|         self.JUNK_WEIGHTS = {
 | |
|             key: value for (key, value)
 | |
|             in self.JUNK_WEIGHTS.items()
 | |
|             if key in self.ITEM_TABLE.keys()
 | |
|         }
 | |
| 
 | |
|         # JUNK_WEIGHTS will add up to 1 if the boosts weights and the trap weights each add up to 1 respectively.
 | |
| 
 | |
|         for junk_item in StaticWitnessItems.ALL_JUNK_ITEMS:
 | |
|             if junk_item not in self.JUNK_WEIGHTS.keys():
 | |
|                 del self.ITEM_TABLE[junk_item]
 |