2022-04-28 22:42:11 +00:00
|
|
|
"""
|
|
|
|
Defines progression, junk and event items for The Witness
|
|
|
|
"""
|
|
|
|
import copy
|
2022-10-09 02:13:52 +00:00
|
|
|
from collections import defaultdict
|
2022-07-23 10:42:14 +00:00
|
|
|
from typing import Dict, NamedTuple, Optional, Set
|
2022-04-28 22:42:11 +00:00
|
|
|
|
|
|
|
from BaseClasses import Item, MultiWorld
|
|
|
|
from . import StaticWitnessLogic, WitnessPlayerLocations, WitnessPlayerLogic
|
2022-06-16 01:04:45 +00:00
|
|
|
from .Options import get_option_value, is_option_enabled, the_witness_options
|
2022-05-09 05:20:28 +00:00
|
|
|
from fractions import Fraction
|
2022-04-28 22:42:11 +00:00
|
|
|
|
|
|
|
|
|
|
|
class ItemData(NamedTuple):
|
|
|
|
"""
|
|
|
|
ItemData for an item in The Witness
|
|
|
|
"""
|
|
|
|
code: Optional[int]
|
|
|
|
progression: bool
|
|
|
|
event: bool = False
|
|
|
|
trap: bool = False
|
2022-06-16 01:04:45 +00:00
|
|
|
never_exclude: bool = False
|
2022-04-28 22:42:11 +00:00
|
|
|
|
|
|
|
|
|
|
|
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] = {}
|
|
|
|
|
2022-07-23 10:42:14 +00:00
|
|
|
ITEM_NAME_GROUPS: Dict[str, Set[str]] = dict()
|
|
|
|
|
2022-05-09 05:20:28 +00:00
|
|
|
# These should always add up to 1!!!
|
|
|
|
BONUS_WEIGHTS = {
|
|
|
|
"Speed Boost": Fraction(1, 1),
|
2022-04-28 22:42:11 +00:00
|
|
|
}
|
|
|
|
|
2022-05-09 05:20:28 +00:00
|
|
|
# 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())
|
|
|
|
|
2023-02-01 20:18:07 +00:00
|
|
|
ITEM_ID_TO_DOOR_HEX_ALL = dict()
|
|
|
|
|
2022-04-28 22:42:11 +00:00
|
|
|
def __init__(self):
|
|
|
|
item_tab = dict()
|
|
|
|
|
2022-07-17 10:56:22 +00:00
|
|
|
for item in StaticWitnessLogic.ALL_SYMBOL_ITEMS:
|
2022-04-28 22:42:11 +00:00
|
|
|
if item[0] == "11 Lasers" or item == "7 Lasers":
|
|
|
|
continue
|
|
|
|
|
|
|
|
item_tab[item[0]] = ItemData(158000 + item[1], True, False)
|
|
|
|
|
2022-07-23 10:42:14 +00:00
|
|
|
self.ITEM_NAME_GROUPS.setdefault("Symbols", set()).add(item[0])
|
|
|
|
|
2023-02-01 20:18:07 +00:00
|
|
|
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)
|
|
|
|
|
2022-07-17 10:56:22 +00:00
|
|
|
for item in StaticWitnessLogic.ALL_DOOR_ITEMS:
|
|
|
|
item_tab[item[0]] = ItemData(158000 + item[1], True, False)
|
|
|
|
|
2022-07-23 10:42:14 +00:00
|
|
|
# 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])
|
|
|
|
|
2022-04-28 22:42:11 +00:00
|
|
|
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)
|
|
|
|
|
2022-06-16 01:04:45 +00:00
|
|
|
for item in StaticWitnessLogic.ALL_USEFULS:
|
2022-06-24 17:25:23 +00:00
|
|
|
item_tab[item[0]] = ItemData(158000 + item[1], False, False, False, item[2])
|
2022-06-16 01:04:45 +00:00
|
|
|
|
2022-04-28 22:42:11 +00:00
|
|
|
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
|
|
|
|
|
2023-02-01 20:18:07 +00:00
|
|
|
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]}
|
|
|
|
|
2022-04-28 22:42:11 +00:00
|
|
|
|
|
|
|
class WitnessPlayerItems:
|
|
|
|
"""
|
|
|
|
Class that defines Items for a single world
|
|
|
|
"""
|
|
|
|
|
2022-10-09 02:13:52 +00:00
|
|
|
@staticmethod
|
|
|
|
def code(item_name: str):
|
|
|
|
return StaticWitnessItems.ALL_ITEM_TABLE[item_name].code
|
|
|
|
|
2023-02-01 20:18:07 +00:00
|
|
|
@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):
|
2022-04-28 22:42:11 +00:00
|
|
|
"""Adds event items after logic changes due to options"""
|
|
|
|
self.EVENT_ITEM_TABLE = dict()
|
|
|
|
self.ITEM_TABLE = copy.copy(StaticWitnessItems.ALL_ITEM_TABLE)
|
2023-02-01 20:18:07 +00:00
|
|
|
|
2022-06-16 01:04:45 +00:00
|
|
|
self.PROGRESSION_TABLE = dict()
|
|
|
|
|
2022-07-17 10:56:22 +00:00
|
|
|
self.ITEM_ID_TO_DOOR_HEX = dict()
|
|
|
|
self.DOORS = set()
|
|
|
|
|
2022-10-09 02:13:52 +00:00
|
|
|
self.PROG_ITEM_AMOUNTS = defaultdict(lambda: 1)
|
|
|
|
|
2022-07-17 10:56:22 +00:00
|
|
|
self.SYMBOLS_NOT_IN_THE_GAME = set()
|
|
|
|
|
2022-06-16 01:04:45 +00:00
|
|
|
self.EXTRA_AMOUNTS = {
|
|
|
|
"Functioning Brain": 1,
|
2023-02-01 20:18:07 +00:00
|
|
|
"Puzzle Skip": get_option_value(multiworld, player, "puzzle_skip_amount")
|
2022-06-16 01:04:45 +00:00
|
|
|
}
|
|
|
|
|
2023-02-01 20:18:07 +00:00
|
|
|
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)
|
|
|
|
|
2022-06-16 01:04:45 +00:00
|
|
|
for item in StaticWitnessLogic.ALL_SYMBOL_ITEMS.union(StaticWitnessLogic.ALL_DOOR_ITEMS):
|
2023-02-01 20:18:07 +00:00
|
|
|
if item[0] not in logic.PROG_ITEMS_ACTUALLY_IN_THE_GAME:
|
2022-06-16 01:04:45 +00:00
|
|
|
del self.ITEM_TABLE[item[0]]
|
2022-07-17 10:56:22 +00:00
|
|
|
if item in StaticWitnessLogic.ALL_SYMBOL_ITEMS:
|
|
|
|
self.SYMBOLS_NOT_IN_THE_GAME.add(StaticWitnessItems.ALL_ITEM_TABLE[item[0]].code)
|
2022-06-16 01:04:45 +00:00
|
|
|
else:
|
2022-10-09 02:13:52 +00:00
|
|
|
if item[0] in StaticWitnessLogic.PROGRESSIVE_TO_ITEMS:
|
2023-02-01 20:18:07 +00:00
|
|
|
self.PROG_ITEM_AMOUNTS[item[0]] = len(logic.MULTI_LISTS[item[0]])
|
2022-10-09 02:13:52 +00:00
|
|
|
|
2022-06-16 01:04:45 +00:00
|
|
|
self.PROGRESSION_TABLE[item[0]] = self.ITEM_TABLE[item[0]]
|
|
|
|
|
2022-10-09 02:13:52 +00:00
|
|
|
self.MULTI_LISTS_BY_CODE = dict()
|
|
|
|
|
|
|
|
for item in self.PROG_ITEM_AMOUNTS:
|
2023-02-01 20:18:07 +00:00
|
|
|
multi_list = logic.MULTI_LISTS[item]
|
2022-10-09 02:13:52 +00:00
|
|
|
self.MULTI_LISTS_BY_CODE[self.code(item)] = [self.code(single_item) for single_item in multi_list]
|
|
|
|
|
2023-02-01 20:18:07 +00:00
|
|
|
for entity_hex, items in logic.DOOR_ITEMS_BY_ID.items():
|
2022-07-17 10:56:22 +00:00
|
|
|
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)
|
|
|
|
|
2023-02-01 20:18:07 +00:00
|
|
|
symbols = is_option_enabled(multiworld, player, "shuffle_symbols")
|
2022-06-16 01:04:45 +00:00
|
|
|
|
|
|
|
if "shuffle_symbols" not in the_witness_options.keys():
|
|
|
|
symbols = True
|
|
|
|
|
2023-02-01 20:18:07 +00:00
|
|
|
doors = get_option_value(multiworld, player, "shuffle_doors")
|
2022-06-16 01:04:45 +00:00
|
|
|
|
2023-02-01 20:18:07 +00:00
|
|
|
self.GOOD_ITEMS = []
|
|
|
|
|
|
|
|
if symbols:
|
2022-06-16 01:04:45 +00:00
|
|
|
self.GOOD_ITEMS = [
|
2023-02-01 20:18:07 +00:00
|
|
|
"Dots", "Black/White Squares", "Stars",
|
2022-06-16 01:04:45 +00:00
|
|
|
"Shapers", "Symmetry"
|
|
|
|
]
|
2022-04-28 22:42:11 +00:00
|
|
|
|
2023-02-01 20:18:07 +00:00
|
|
|
if doors:
|
|
|
|
self.GOOD_ITEMS = [
|
|
|
|
"Dots", "Black/White Squares", "Symmetry"
|
|
|
|
]
|
|
|
|
|
|
|
|
if is_option_enabled(multiworld, player, "shuffle_discarded_panels"):
|
|
|
|
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")
|
|
|
|
if not is_option_enabled(multiworld, player, "disable_non_randomized_puzzles"):
|
2022-07-17 10:56:22 +00:00
|
|
|
self.GOOD_ITEMS.append("Colored Squares")
|
2022-04-28 22:42:11 +00:00
|
|
|
|
2022-10-09 02:13:52 +00:00
|
|
|
self.GOOD_ITEMS = [
|
|
|
|
StaticWitnessLogic.ITEMS_TO_PROGRESSIVE.get(item, item) for item in self.GOOD_ITEMS
|
|
|
|
]
|
|
|
|
|
2022-04-28 22:42:11 +00:00
|
|
|
for event_location in locat.EVENT_LOCATION_TABLE:
|
2023-02-01 20:18:07 +00:00
|
|
|
location = logic.EVENT_ITEM_PAIRS[event_location]
|
2022-04-28 22:42:11 +00:00
|
|
|
self.EVENT_ITEM_TABLE[location] = ItemData(None, True, True)
|
|
|
|
self.ITEM_TABLE[location] = ItemData(None, True, True)
|
|
|
|
|
2023-02-01 20:18:07 +00:00
|
|
|
trap_percentage = get_option_value(multiworld, player, "trap_percentage")
|
2022-05-09 05:20:28 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2022-04-28 22:42:11 +00:00
|
|
|
self.JUNK_WEIGHTS = {
|
|
|
|
key: value for (key, value)
|
2022-05-09 05:20:28 +00:00
|
|
|
in self.JUNK_WEIGHTS.items()
|
2022-04-28 22:42:11 +00:00
|
|
|
if key in self.ITEM_TABLE.keys()
|
|
|
|
}
|
2022-05-09 05:20:28 +00:00
|
|
|
|
|
|
|
# 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]
|