2022-04-28 22:42:11 +00:00
|
|
|
"""
|
|
|
|
Defines progression, junk and event items for The Witness
|
|
|
|
"""
|
|
|
|
import copy
|
2023-07-19 03:02:57 +00:00
|
|
|
from dataclasses import dataclass
|
2023-07-28 07:39:56 +00:00
|
|
|
from typing import Optional, Dict, List, Set
|
2022-04-28 22:42:11 +00:00
|
|
|
|
2023-07-19 03:02:57 +00:00
|
|
|
from BaseClasses import Item, MultiWorld, ItemClassification
|
2022-06-16 01:04:45 +00:00
|
|
|
from .Options import get_option_value, is_option_enabled, the_witness_options
|
2022-04-28 22:42:11 +00:00
|
|
|
|
2023-07-19 03:02:57 +00:00
|
|
|
from .locations import ID_START, WitnessPlayerLocations
|
|
|
|
from .player_logic import WitnessPlayerLogic
|
|
|
|
from .static_logic import ItemDefinition, DoorItemDefinition, ProgressiveItemDefinition, ItemCategory, \
|
|
|
|
StaticWitnessLogic, WeightedItemDefinition
|
|
|
|
from .utils import build_weighted_int_list
|
2022-04-28 22:42:11 +00:00
|
|
|
|
2023-07-19 03:02:57 +00:00
|
|
|
NUM_ENERGY_UPGRADES = 4
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass()
|
|
|
|
class ItemData:
|
2022-04-28 22:42:11 +00:00
|
|
|
"""
|
|
|
|
ItemData for an item in The Witness
|
|
|
|
"""
|
2023-07-19 03:02:57 +00:00
|
|
|
ap_code: Optional[int]
|
|
|
|
definition: ItemDefinition
|
|
|
|
classification: ItemClassification
|
|
|
|
local_only: 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
|
|
|
|
"""
|
2023-07-20 00:10:48 +00:00
|
|
|
item_data: Dict[str, ItemData] = {}
|
|
|
|
item_groups: Dict[str, List[str]] = {}
|
2022-04-28 22:42:11 +00:00
|
|
|
|
2023-07-19 03:02:57 +00:00
|
|
|
# Useful items that are treated specially at generation time and should not be automatically added to the player's
|
|
|
|
# item list during get_progression_items.
|
2023-07-20 00:10:48 +00:00
|
|
|
special_usefuls: List[str] = ["Puzzle Skip"]
|
2023-02-01 20:18:07 +00:00
|
|
|
|
2022-04-28 22:42:11 +00:00
|
|
|
def __init__(self):
|
2023-07-19 03:02:57 +00:00
|
|
|
for item_name, definition in StaticWitnessLogic.all_items.items():
|
|
|
|
ap_item_code = definition.local_code + ID_START
|
|
|
|
classification: ItemClassification = ItemClassification.filler
|
|
|
|
local_only: bool = False
|
|
|
|
|
|
|
|
if definition.category is ItemCategory.SYMBOL:
|
|
|
|
classification = ItemClassification.progression
|
|
|
|
StaticWitnessItems.item_groups.setdefault("Symbols", []).append(item_name)
|
|
|
|
elif definition.category is ItemCategory.DOOR:
|
|
|
|
classification = ItemClassification.progression
|
|
|
|
StaticWitnessItems.item_groups.setdefault("Doors", []).append(item_name)
|
|
|
|
elif definition.category is ItemCategory.LASER:
|
|
|
|
classification = ItemClassification.progression
|
|
|
|
StaticWitnessItems.item_groups.setdefault("Lasers", []).append(item_name)
|
|
|
|
elif definition.category is ItemCategory.USEFUL:
|
|
|
|
classification = ItemClassification.useful
|
|
|
|
elif definition.category is ItemCategory.FILLER:
|
|
|
|
if item_name in ["Energy Fill (Small)"]:
|
|
|
|
local_only = True
|
|
|
|
classification = ItemClassification.filler
|
|
|
|
elif definition.category is ItemCategory.TRAP:
|
|
|
|
classification = ItemClassification.trap
|
|
|
|
elif definition.category is ItemCategory.JOKE:
|
|
|
|
classification = ItemClassification.filler
|
|
|
|
|
|
|
|
StaticWitnessItems.item_data[item_name] = ItemData(ap_item_code, definition,
|
|
|
|
classification, local_only)
|
2023-02-01 20:18:07 +00:00
|
|
|
|
2023-07-19 03:02:57 +00:00
|
|
|
@staticmethod
|
2023-07-20 00:10:48 +00:00
|
|
|
def get_item_to_door_mappings() -> Dict[int, List[int]]:
|
|
|
|
output: Dict[int, List[int]] = {}
|
2023-07-19 03:02:57 +00:00
|
|
|
for item_name, item_data in {name: data for name, data in StaticWitnessItems.item_data.items()
|
|
|
|
if isinstance(data.definition, DoorItemDefinition)}.items():
|
|
|
|
item = StaticWitnessItems.item_data[item_name]
|
|
|
|
output[item.ap_code] = [int(hex_string, 16) for hex_string in item_data.definition.panel_id_hexes]
|
|
|
|
return output
|
2023-02-01 20:18:07 +00:00
|
|
|
|
2022-04-28 22:42:11 +00:00
|
|
|
|
|
|
|
class WitnessPlayerItems:
|
|
|
|
"""
|
|
|
|
Class that defines Items for a single world
|
|
|
|
"""
|
|
|
|
|
2023-07-19 03:02:57 +00:00
|
|
|
def __init__(self, multiworld: MultiWorld, player: int, logic: WitnessPlayerLogic, locat: WitnessPlayerLocations):
|
2022-04-28 22:42:11 +00:00
|
|
|
"""Adds event items after logic changes due to options"""
|
2023-02-01 20:18:07 +00:00
|
|
|
|
2023-07-19 03:02:57 +00:00
|
|
|
self._world: MultiWorld = multiworld
|
|
|
|
self._player_id: int = player
|
|
|
|
self._logic: WitnessPlayerLogic = logic
|
|
|
|
self._locations: WitnessPlayerLocations = locat
|
|
|
|
|
|
|
|
# Duplicate the static item data, then make any player-specific adjustments to classification.
|
2023-07-25 03:54:23 +00:00
|
|
|
self.item_data: Dict[str, ItemData] = copy.deepcopy(StaticWitnessItems.item_data)
|
2023-07-19 03:02:57 +00:00
|
|
|
|
|
|
|
# Remove all progression items that aren't actually in the game.
|
|
|
|
self.item_data = {name: data for (name, data) in self.item_data.items()
|
|
|
|
if data.classification is not ItemClassification.progression or
|
|
|
|
name in logic.PROG_ITEMS_ACTUALLY_IN_THE_GAME}
|
|
|
|
|
|
|
|
# Adjust item classifications based on game settings.
|
|
|
|
eps_shuffled = get_option_value(self._world, self._player_id, "shuffle_EPs") != 0
|
|
|
|
for item_name, item_data in self.item_data.items():
|
|
|
|
if not eps_shuffled and item_name in ["Monastery Garden Entry (Door)", "Monastery Shortcuts"]:
|
|
|
|
# Downgrade doors that only gate progress in EP shuffle.
|
|
|
|
item_data.classification = ItemClassification.useful
|
|
|
|
elif item_name in ["River Monastery Shortcut (Door)", "Jungle & River Shortcuts",
|
|
|
|
"Monastery Shortcut (Door)",
|
|
|
|
"Orchard Second Gate (Door)"]:
|
|
|
|
# Downgrade doors that don't gate progress.
|
|
|
|
item_data.classification = ItemClassification.useful
|
|
|
|
|
|
|
|
# Build the mandatory item list.
|
2023-07-20 00:10:48 +00:00
|
|
|
self._mandatory_items: Dict[str, int] = {}
|
2023-07-19 03:02:57 +00:00
|
|
|
|
|
|
|
# Add progression items to the mandatory item list.
|
|
|
|
for item_name, item_data in {name: data for (name, data) in self.item_data.items()
|
|
|
|
if data.classification == ItemClassification.progression}.items():
|
|
|
|
if isinstance(item_data.definition, ProgressiveItemDefinition):
|
|
|
|
num_progression = len(self._logic.MULTI_LISTS[item_name])
|
|
|
|
self._mandatory_items[item_name] = num_progression
|
2022-06-16 01:04:45 +00:00
|
|
|
else:
|
2023-07-19 03:02:57 +00:00
|
|
|
self._mandatory_items[item_name] = 1
|
2022-06-16 01:04:45 +00:00
|
|
|
|
2023-07-19 03:02:57 +00:00
|
|
|
# Add setting-specific useful items to the mandatory item list.
|
|
|
|
for item_name, item_data in {name: data for (name, data) in self.item_data.items()
|
|
|
|
if data.classification == ItemClassification.useful}.items():
|
|
|
|
if item_name in StaticWitnessItems.special_usefuls:
|
|
|
|
continue
|
|
|
|
elif item_name == "Energy Capacity":
|
|
|
|
self._mandatory_items[item_name] = NUM_ENERGY_UPGRADES
|
|
|
|
elif isinstance(item_data.classification, ProgressiveItemDefinition):
|
|
|
|
self._mandatory_items[item_name] = len(item_data.mappings)
|
|
|
|
else:
|
|
|
|
self._mandatory_items[item_name] = 1
|
|
|
|
|
|
|
|
# Add event items to the item definition list for later lookup.
|
|
|
|
for event_location in self._locations.EVENT_LOCATION_TABLE:
|
|
|
|
location_name = logic.EVENT_ITEM_PAIRS[event_location]
|
|
|
|
self.item_data[location_name] = ItemData(None, ItemDefinition(0, ItemCategory.EVENT),
|
|
|
|
ItemClassification.progression, False)
|
|
|
|
|
2023-07-20 00:10:48 +00:00
|
|
|
def get_mandatory_items(self) -> Dict[str, int]:
|
2023-07-19 03:02:57 +00:00
|
|
|
"""
|
|
|
|
Returns the list of items that must be in the pool for the game to successfully generate.
|
|
|
|
"""
|
|
|
|
return self._mandatory_items
|
|
|
|
|
2023-07-20 00:10:48 +00:00
|
|
|
def get_filler_items(self, quantity: int) -> Dict[str, int]:
|
2023-07-19 03:02:57 +00:00
|
|
|
"""
|
|
|
|
Generates a list of filler items of the given length.
|
|
|
|
"""
|
|
|
|
if quantity <= 0:
|
|
|
|
return {}
|
|
|
|
|
2023-07-20 00:10:48 +00:00
|
|
|
output: Dict[str, int] = {}
|
2023-07-19 03:02:57 +00:00
|
|
|
remaining_quantity = quantity
|
|
|
|
|
|
|
|
# Add joke items.
|
|
|
|
output.update({name: 1 for (name, data) in self.item_data.items()
|
|
|
|
if data.definition.category is ItemCategory.JOKE})
|
|
|
|
remaining_quantity -= len(output)
|
|
|
|
|
|
|
|
# Read trap configuration data.
|
|
|
|
trap_weight = get_option_value(self._world, self._player_id, "trap_percentage") / 100
|
|
|
|
filler_weight = 1 - trap_weight
|
|
|
|
|
|
|
|
# Add filler items to the list.
|
2023-07-20 00:10:48 +00:00
|
|
|
filler_items: Dict[str, float]
|
2023-07-19 03:02:57 +00:00
|
|
|
filler_items = {name: data.definition.weight if isinstance(data.definition, WeightedItemDefinition) else 1
|
|
|
|
for (name, data) in self.item_data.items() if data.definition.category is ItemCategory.FILLER}
|
|
|
|
filler_items = {name: base_weight * filler_weight / sum(filler_items.values())
|
|
|
|
for name, base_weight in filler_items.items() if base_weight > 0}
|
|
|
|
|
|
|
|
# Add trap items.
|
|
|
|
if trap_weight > 0:
|
|
|
|
trap_items = {name: data.definition.weight if isinstance(data.definition, WeightedItemDefinition) else 1
|
|
|
|
for (name, data) in self.item_data.items() if data.definition.category is ItemCategory.TRAP}
|
|
|
|
filler_items.update({name: base_weight * trap_weight / sum(trap_items.values())
|
|
|
|
for name, base_weight in trap_items.items() if base_weight > 0})
|
|
|
|
|
|
|
|
# Get the actual number of each item by scaling the float weight values to match the target quantity.
|
2023-07-20 00:10:48 +00:00
|
|
|
int_weights: List[int] = build_weighted_int_list(filler_items.values(), remaining_quantity)
|
2023-07-19 03:02:57 +00:00
|
|
|
output.update(zip(filler_items.keys(), int_weights))
|
|
|
|
|
|
|
|
return output
|
|
|
|
|
2023-07-20 00:10:48 +00:00
|
|
|
def get_early_items(self) -> List[str]:
|
2023-07-19 03:02:57 +00:00
|
|
|
"""
|
|
|
|
Returns items that are ideal for placing on extremely early checks, like the tutorial gate.
|
|
|
|
"""
|
2023-07-28 07:39:56 +00:00
|
|
|
output: Set[str] = set()
|
2023-07-19 03:02:57 +00:00
|
|
|
if "shuffle_symbols" not in the_witness_options.keys() \
|
|
|
|
or is_option_enabled(self._world, self._player_id, "shuffle_symbols"):
|
|
|
|
if get_option_value(self._world, self._player_id, "shuffle_doors") > 0:
|
2023-07-28 07:39:56 +00:00
|
|
|
output = {"Dots", "Black/White Squares", "Symmetry"}
|
2023-07-19 03:02:57 +00:00
|
|
|
else:
|
2023-07-28 07:39:56 +00:00
|
|
|
output = {"Dots", "Black/White Squares", "Symmetry", "Shapers", "Stars"}
|
2023-02-01 20:18:07 +00:00
|
|
|
|
2023-07-19 03:02:57 +00:00
|
|
|
if is_option_enabled(self._world, self._player_id, "shuffle_discarded_panels"):
|
|
|
|
if get_option_value(self._world, self._player_id, "puzzle_randomization") == 1:
|
2023-07-28 07:39:56 +00:00
|
|
|
output.add("Arrows")
|
2023-03-02 23:08:24 +00:00
|
|
|
else:
|
2023-07-28 07:39:56 +00:00
|
|
|
output.add("Triangles")
|
2023-07-19 03:02:57 +00:00
|
|
|
|
|
|
|
# Replace progressive items with their parents.
|
2023-07-28 07:39:56 +00:00
|
|
|
output = {StaticWitnessLogic.get_parent_progressive_item(item) for item in output}
|
2023-07-19 03:02:57 +00:00
|
|
|
|
|
|
|
# Remove items that are mentioned in any plando options. (Hopefully, in the future, plando will get resolved
|
|
|
|
# before create_items so that we'll be able to check placed items instead of just removing all items mentioned
|
|
|
|
# regardless of whether or not they actually wind up being manually placed.
|
|
|
|
for plando_setting in self._world.plando_items[self._player_id]:
|
|
|
|
if plando_setting.get("from_pool", True):
|
2023-07-28 07:39:56 +00:00
|
|
|
for item_setting_key in (key for key in ["item", "items"] if key in plando_setting):
|
|
|
|
if type(plando_setting[item_setting_key]) is str:
|
|
|
|
output.remove(plando_setting[item_setting_key])
|
|
|
|
elif type(plando_setting[item_setting_key]) is dict:
|
|
|
|
output -= {item for item, weight in plando_setting[item_setting_key].items() if weight}
|
2023-07-19 03:02:57 +00:00
|
|
|
else:
|
|
|
|
# Assume this is some other kind of iterable.
|
2023-07-28 07:39:56 +00:00
|
|
|
output -= plando_setting[item_setting_key]
|
2023-07-19 03:02:57 +00:00
|
|
|
|
|
|
|
# Sort the output for consistency across versions if the implementation changes but the logic does not.
|
|
|
|
return sorted(output)
|
|
|
|
|
2023-07-20 00:10:48 +00:00
|
|
|
def get_door_ids_in_pool(self) -> List[int]:
|
2023-07-19 03:02:57 +00:00
|
|
|
"""
|
|
|
|
Returns the total set of all door IDs that are controlled by items in the pool.
|
|
|
|
"""
|
2023-07-20 00:10:48 +00:00
|
|
|
output: List[int] = []
|
2023-07-19 03:02:57 +00:00
|
|
|
for item_name, item_data in {name: data for name, data in self.item_data.items()
|
|
|
|
if isinstance(data.definition, DoorItemDefinition)}.items():
|
|
|
|
output += [int(hex_string, 16) for hex_string in item_data.definition.panel_id_hexes]
|
|
|
|
return output
|
|
|
|
|
2023-07-20 00:10:48 +00:00
|
|
|
def get_symbol_ids_not_in_pool(self) -> List[int]:
|
2023-07-19 03:02:57 +00:00
|
|
|
"""
|
|
|
|
Returns the item IDs of symbol items that were defined in the configuration file but are not in the pool.
|
|
|
|
"""
|
|
|
|
return [data.ap_code for name, data in StaticWitnessItems.item_data.items()
|
|
|
|
if name not in self.item_data.keys() and data.definition.category is ItemCategory.SYMBOL]
|
|
|
|
|
2023-07-20 00:10:48 +00:00
|
|
|
def get_progressive_item_ids_in_pool(self) -> Dict[int, List[int]]:
|
|
|
|
output: Dict[int, List[int]] = {}
|
2023-07-19 03:02:57 +00:00
|
|
|
for item_name, quantity in {name: quantity for name, quantity in self._mandatory_items.items()}.items():
|
|
|
|
item = self.item_data[item_name]
|
|
|
|
if isinstance(item.definition, ProgressiveItemDefinition):
|
|
|
|
# Note: we need to reference the static table here rather than the player-specific one because the child
|
|
|
|
# items were removed from the pool when we pruned out all progression items not in the settings.
|
|
|
|
output[item.ap_code] = [StaticWitnessItems.item_data[child_item].ap_code
|
|
|
|
for child_item in item.definition.child_item_names]
|
|
|
|
return output
|