Archipelago/worlds/witness/player_items.py

224 lines
9.9 KiB
Python

"""
Defines progression, junk and event items for The Witness
"""
import copy
from typing import TYPE_CHECKING, Dict, List, Set, cast
from BaseClasses import Item, ItemClassification, MultiWorld
from .data import static_items as static_witness_items
from .data import static_logic as static_witness_logic
from .data.item_definition_classes import (
DoorItemDefinition,
ItemCategory,
ItemData,
ItemDefinition,
ProgressiveItemDefinition,
WeightedItemDefinition,
)
from .data.utils import build_weighted_int_list
from .locations import WitnessPlayerLocations
from .player_logic import WitnessPlayerLogic
if TYPE_CHECKING:
from . import WitnessWorld
NUM_ENERGY_UPGRADES = 4
class WitnessItem(Item):
"""
Item from the game The Witness
"""
game: str = "The Witness"
class WitnessPlayerItems:
"""
Class that defines Items for a single world
"""
def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic,
player_locations: WitnessPlayerLocations) -> None:
"""Adds event items after logic changes due to options"""
self._world: "WitnessWorld" = world
self._multiworld: MultiWorld = world.multiworld
self._player_id: int = world.player
self._logic: WitnessPlayerLogic = player_logic
self._locations: WitnessPlayerLocations = player_locations
# Duplicate the static item data, then make any player-specific adjustments to classification.
self.item_data: Dict[str, ItemData] = copy.deepcopy(static_witness_items.ITEM_DATA)
# 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 not in
{ItemClassification.progression, ItemClassification.progression_skip_balancing}
or name in player_logic.PROG_ITEMS_ACTUALLY_IN_THE_GAME
}
# Downgrade door items
for item_name, item_data in self.item_data.items():
if not isinstance(item_data.definition, DoorItemDefinition):
continue
if all(not self._logic.solvability_guaranteed(e_hex) for e_hex in item_data.definition.panel_id_hexes):
item_data.classification = ItemClassification.useful
# Build the mandatory item list.
self._mandatory_items: Dict[str, int] = {}
# Add progression items to the mandatory item list.
progression_dict = {
name: data for (name, data) in self.item_data.items()
if data.classification in {ItemClassification.progression, ItemClassification.progression_skip_balancing}
}
for item_name, item_data in progression_dict.items():
if isinstance(item_data.definition, ProgressiveItemDefinition):
num_progression = len(self._logic.MULTI_LISTS[item_name])
self._mandatory_items[item_name] = num_progression
else:
self._mandatory_items[item_name] = 1
# 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 static_witness_items._special_usefuls:
continue
if 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 = player_logic.EVENT_ITEM_PAIRS[event_location]
self.item_data[location_name] = ItemData(None, ItemDefinition(0, ItemCategory.EVENT),
ItemClassification.progression, False)
def get_mandatory_items(self) -> Dict[str, int]:
"""
Returns the list of items that must be in the pool for the game to successfully generate.
"""
return self._mandatory_items.copy()
def get_filler_items(self, quantity: int) -> Dict[str, int]:
"""
Generates a list of filler items of the given length.
"""
if quantity <= 0:
return {}
output: Dict[str, int] = {}
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 = self._world.options.trap_percentage / 100
trap_items = self._world.options.trap_weights.value
if not sum(trap_items.values()):
trap_weight = 0
# Add filler items to the list.
filler_weight = 1 - trap_weight
filler_items: Dict[str, float]
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:
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.
int_weights: List[int] = build_weighted_int_list(filler_items.values(), remaining_quantity)
output.update(zip(filler_items.keys(), int_weights))
return output
def get_early_items(self) -> List[str]:
"""
Returns items that are ideal for placing on extremely early checks, like the tutorial gate.
"""
output: Set[str] = set()
if self._world.options.shuffle_symbols:
output = {"Dots", "Black/White Squares", "Symmetry", "Shapers", "Stars"}
if self._world.options.shuffle_discarded_panels:
if self._world.options.puzzle_randomization == "sigma_expert":
output.add("Arrows")
else:
output.add("Triangles")
# Replace progressive items with their parents.
output = {static_witness_logic.get_parent_progressive_item(item) for item in output}
# 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._multiworld.plando_items[self._player_id]:
if plando_setting.get("from_pool", True):
for item_setting_key in [key for key in ["item", "items"] if key in plando_setting]:
if isinstance(plando_setting[item_setting_key], str):
output -= {plando_setting[item_setting_key]}
elif isinstance(plando_setting[item_setting_key], dict):
output -= {item for item, weight in plando_setting[item_setting_key].items() if weight}
else:
# Assume this is some other kind of iterable.
for inner_item in plando_setting[item_setting_key]:
if isinstance(inner_item, str):
output -= {inner_item}
elif isinstance(inner_item, dict):
output -= {item for item, weight in inner_item.items() if weight}
# Sort the output for consistency across versions if the implementation changes but the logic does not.
return sorted(output)
def get_door_ids_in_pool(self) -> List[int]:
"""
Returns the total set of all door IDs that are controlled by items in the pool.
"""
output: List[int] = []
for item_name, item_data in dict(self.item_data.items()).items():
if not isinstance(item_data.definition, DoorItemDefinition):
continue
output += [int(hex_string, 16) for hex_string in item_data.definition.panel_id_hexes]
return output
def get_symbol_ids_not_in_pool(self) -> List[int]:
"""
Returns the item IDs of symbol items that were defined in the configuration file but are not in the pool.
"""
return [
# data.ap_code is guaranteed for a symbol definition
cast(int, data.ap_code) for name, data in static_witness_items.ITEM_DATA.items()
if name not in self.item_data.keys() and data.definition.category is ItemCategory.SYMBOL
]
def get_progressive_item_ids_in_pool(self) -> Dict[int, List[int]]:
output: Dict[int, List[int]] = {}
for item_name, quantity in dict(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[cast(int, item.ap_code)] = [cast(int, static_witness_items.ITEM_DATA[child_item].ap_code)
for child_item in item.definition.child_item_names]
return output