Files
Grinch-AP/worlds/witness/items.py
NewSoupVi 805f33c39e Witness: Bugfixes in response to beta tests (#1473)
* 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>
2023-03-03 00:08:24 +01:00

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]