2022-04-29 00:42:11 +02:00
|
|
|
"""
|
|
|
|
Defines progression, junk and event items for The Witness
|
|
|
|
"""
|
|
|
|
import copy
|
2024-11-18 02:16:14 +01:00
|
|
|
from typing import TYPE_CHECKING, Dict, List, Set
|
2024-04-12 00:27:42 +02:00
|
|
|
|
|
|
|
from BaseClasses import Item, ItemClassification, MultiWorld
|
|
|
|
|
|
|
|
from .data import static_items as static_witness_items
|
2024-12-10 21:06:06 +01:00
|
|
|
from .data import static_logic as static_witness_logic
|
2024-04-12 00:27:42 +02:00
|
|
|
from .data.item_definition_classes import (
|
|
|
|
DoorItemDefinition,
|
|
|
|
ItemCategory,
|
|
|
|
ItemData,
|
|
|
|
ItemDefinition,
|
|
|
|
ProgressiveItemDefinition,
|
|
|
|
WeightedItemDefinition,
|
|
|
|
)
|
2024-10-02 00:02:17 +02:00
|
|
|
from .data.utils import build_weighted_int_list, cast_not_none
|
2024-04-12 00:27:42 +02:00
|
|
|
from .locations import WitnessPlayerLocations
|
2023-07-18 20:02:57 -07:00
|
|
|
from .player_logic import WitnessPlayerLogic
|
2022-04-29 00:42:11 +02:00
|
|
|
|
2023-11-24 06:27:03 +01:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
from . import WitnessWorld
|
|
|
|
|
2023-07-18 20:02:57 -07:00
|
|
|
NUM_ENERGY_UPGRADES = 4
|
|
|
|
|
|
|
|
|
2022-04-29 00:42:11 +02:00
|
|
|
class WitnessItem(Item):
|
|
|
|
"""
|
|
|
|
Item from the game The Witness
|
|
|
|
"""
|
|
|
|
game: str = "The Witness"
|
2025-03-08 01:44:06 +01:00
|
|
|
eggs: int = 0
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def make_egg_event(cls, item_name: str, player: int):
|
|
|
|
ret = cls(item_name, ItemClassification.progression, None, player)
|
|
|
|
ret.eggs = int(item_name[1:].split(" ", 1)[0])
|
|
|
|
return ret
|
2022-04-29 00:42:11 +02:00
|
|
|
|
|
|
|
|
|
|
|
class WitnessPlayerItems:
|
|
|
|
"""
|
|
|
|
Class that defines Items for a single world
|
|
|
|
"""
|
|
|
|
|
2024-04-12 00:27:42 +02:00
|
|
|
def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic,
|
|
|
|
player_locations: WitnessPlayerLocations) -> None:
|
2022-04-29 00:42:11 +02:00
|
|
|
"""Adds event items after logic changes due to options"""
|
2023-02-01 21:18:07 +01:00
|
|
|
|
2024-08-28 18:31:49 +02:00
|
|
|
self._world: WitnessWorld = world
|
2023-11-24 06:27:03 +01:00
|
|
|
self._multiworld: MultiWorld = world.multiworld
|
|
|
|
self._player_id: int = world.player
|
2024-04-12 00:27:42 +02:00
|
|
|
self._logic: WitnessPlayerLogic = player_logic
|
|
|
|
self._locations: WitnessPlayerLocations = player_locations
|
2023-07-18 20:02:57 -07:00
|
|
|
|
|
|
|
# Duplicate the static item data, then make any player-specific adjustments to classification.
|
2024-04-12 00:27:42 +02:00
|
|
|
self.item_data: Dict[str, ItemData] = copy.deepcopy(static_witness_items.ITEM_DATA)
|
2023-07-18 20:02:57 -07:00
|
|
|
|
|
|
|
# Remove all progression items that aren't actually in the game.
|
2023-11-24 06:27:03 +01:00
|
|
|
self.item_data = {
|
|
|
|
name: data for (name, data) in self.item_data.items()
|
2024-12-10 21:06:06 +01:00
|
|
|
if ItemClassification.progression not in data.classification
|
|
|
|
or name in player_logic.PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME
|
2023-11-24 06:27:03 +01:00
|
|
|
}
|
2023-07-18 20:02:57 -07:00
|
|
|
|
2025-03-08 12:26:59 +01:00
|
|
|
# Downgrade door items and make lasers local if local lasers is on
|
2023-07-18 20:02:57 -07:00
|
|
|
for item_name, item_data in self.item_data.items():
|
The Witness: Event System & Item Classification System revamp (#2652)
Two things have been happening.
**Incorrect Events**
Spoiler logs containing events that just straight up have an incorrect name and shouldn't be there. E.g. "Symmetry Island Yellow 3 solved - Monastery Laser Activation" when playing Laser Shuffle where this event should not exist, because Laser Activations are governed by the Laser items.
Now to be clear - There are no logic issues with it. The event will be in the spoiler log, but it won't actually be used in the way that its name suggests.
Basically, every panel in the game has exactly one event name. If the panel is referenced by another panel, it will reference the event instead. So, the Symmetry Laser Panel location will reference Symmetry Island Yellow 3, and an event is created for Symmetry Island Yellow 3. The only problem is the **name**: The canonical name for the event is related to "Symmetry Island Yellow 3" is "Monastery Laser Activation", because that's another thing that panel does sometimes.
From now on, event names are tied to both the panel referencing and the panel being referenced. Only once the referincing panel actually references the dependent panel (during the dependency reduction process in generate_early), is the event actually created.
This also removes some spoiler log clutter where unused events were just in the location list.
**Item classifications**
When playing shuffle_doors, there are a lot of doors in the game that are logically useless depending on settings. When that happens, they should get downgraded from progression to useful. The previous system for this was jank and terrible. Now there is a better system for it, and many items have been added to it. :)
2024-02-13 22:47:19 +01:00
|
|
|
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):
|
2023-12-10 20:35:46 +01:00
|
|
|
item_data.classification = ItemClassification.useful
|
2023-07-18 20:02:57 -07:00
|
|
|
|
2025-03-08 12:26:59 +01:00
|
|
|
if item_data.definition.category == ItemCategory.LASER and self._world.options.shuffle_lasers == "local":
|
|
|
|
item_data.local_only = True
|
|
|
|
|
2023-07-18 20:02:57 -07:00
|
|
|
# Build the mandatory item list.
|
2023-07-20 02:10:48 +02:00
|
|
|
self._mandatory_items: Dict[str, int] = {}
|
2023-07-18 20:02:57 -07:00
|
|
|
|
|
|
|
# Add progression items to the mandatory item list.
|
2023-11-24 06:27:03 +01:00
|
|
|
progression_dict = {
|
|
|
|
name: data for (name, data) in self.item_data.items()
|
2024-12-10 21:06:06 +01:00
|
|
|
if ItemClassification.progression in data.classification
|
2023-11-24 06:27:03 +01:00
|
|
|
}
|
|
|
|
for item_name, item_data in progression_dict.items():
|
2023-07-18 20:02:57 -07:00
|
|
|
if isinstance(item_data.definition, ProgressiveItemDefinition):
|
2024-11-29 01:37:19 +01:00
|
|
|
num_progression = len(self._logic.PROGRESSIVE_LISTS[item_name])
|
2023-07-18 20:02:57 -07:00
|
|
|
self._mandatory_items[item_name] = num_progression
|
2022-06-16 03:04:45 +02:00
|
|
|
else:
|
2023-07-18 20:02:57 -07:00
|
|
|
self._mandatory_items[item_name] = 1
|
2022-06-16 03:04:45 +02:00
|
|
|
|
2023-07-18 20:02:57 -07: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():
|
2024-04-12 00:27:42 +02:00
|
|
|
if item_name in static_witness_items._special_usefuls:
|
2023-07-18 20:02:57 -07:00
|
|
|
continue
|
2024-07-02 23:59:26 +02:00
|
|
|
|
|
|
|
if item_name == "Energy Capacity":
|
2023-07-18 20:02:57 -07:00
|
|
|
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:
|
2024-08-20 01:16:35 +02:00
|
|
|
location_name = player_logic.EVENT_ITEM_PAIRS[event_location][0]
|
2023-07-18 20:02:57 -07:00
|
|
|
self.item_data[location_name] = ItemData(None, ItemDefinition(0, ItemCategory.EVENT),
|
|
|
|
ItemClassification.progression, False)
|
|
|
|
|
2024-12-10 21:06:06 +01:00
|
|
|
# Determine which items should be progression + useful, if they exist in some capacity.
|
|
|
|
# Note: Some of these may need to be updated for the "independent symbols" PR.
|
|
|
|
self._proguseful_items = {
|
|
|
|
"Dots", "Stars", "Shapers", "Black/White Squares",
|
|
|
|
"Caves Shortcuts", "Caves Mountain Shortcut (Door)", "Caves Swamp Shortcut (Door)",
|
|
|
|
"Boat",
|
|
|
|
}
|
|
|
|
|
|
|
|
if self._world.options.shuffle_EPs == "individual":
|
|
|
|
self._proguseful_items |= {
|
|
|
|
"Town Obelisk Key", # Most checks
|
|
|
|
"Monastery Obelisk Key", # Most sphere 1 checks, and also super dense ("Jackpot" vibes)}
|
|
|
|
}
|
|
|
|
|
|
|
|
if self._world.options.shuffle_discarded_panels:
|
|
|
|
# Discards only give a moderate amount of checks, but are very spread out and a lot of them are in sphere 1.
|
|
|
|
# Thus, you really want to have the discard-unlocking item as quickly as possible.
|
|
|
|
|
|
|
|
if self._world.options.puzzle_randomization in ("none", "sigma_normal"):
|
|
|
|
self._proguseful_items.add("Triangles")
|
|
|
|
elif self._world.options.puzzle_randomization == "sigma_expert":
|
|
|
|
self._proguseful_items.add("Arrows")
|
|
|
|
# Discards require two symbols in Variety, so the "sphere 1 unlocking power" of Arrows is not there.
|
|
|
|
if self._world.options.puzzle_randomization == "sigma_expert":
|
|
|
|
self._proguseful_items.add("Triangles")
|
|
|
|
self._proguseful_items.add("Full Dots")
|
|
|
|
self._proguseful_items.add("Stars + Same Colored Symbol")
|
|
|
|
self._proguseful_items.discard("Stars") # Stars are not that useful on their own.
|
|
|
|
if self._world.options.puzzle_randomization == "umbra_variety":
|
|
|
|
self._proguseful_items.add("Triangles")
|
|
|
|
|
|
|
|
# This needs to be improved when the improved independent&progressive symbols PR is merged
|
|
|
|
for item in list(self._proguseful_items):
|
|
|
|
self._proguseful_items.add(static_witness_logic.get_parent_progressive_item(item))
|
|
|
|
|
|
|
|
for item_name, item_data in self.item_data.items():
|
|
|
|
if item_name in self._proguseful_items:
|
|
|
|
item_data.classification |= ItemClassification.useful
|
|
|
|
|
|
|
|
|
2023-07-20 02:10:48 +02:00
|
|
|
def get_mandatory_items(self) -> Dict[str, int]:
|
2023-07-18 20:02:57 -07:00
|
|
|
"""
|
|
|
|
Returns the list of items that must be in the pool for the game to successfully generate.
|
|
|
|
"""
|
2023-09-10 14:29:42 -07:00
|
|
|
return self._mandatory_items.copy()
|
2023-07-18 20:02:57 -07:00
|
|
|
|
2023-07-20 02:10:48 +02:00
|
|
|
def get_filler_items(self, quantity: int) -> Dict[str, int]:
|
2023-07-18 20:02:57 -07:00
|
|
|
"""
|
|
|
|
Generates a list of filler items of the given length.
|
|
|
|
"""
|
|
|
|
if quantity <= 0:
|
|
|
|
return {}
|
|
|
|
|
2023-07-20 02:10:48 +02:00
|
|
|
output: Dict[str, int] = {}
|
2023-07-18 20:02:57 -07: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.
|
2023-11-24 06:27:03 +01:00
|
|
|
trap_weight = self._world.options.trap_percentage / 100
|
2024-02-29 07:40:08 +01:00
|
|
|
trap_items = self._world.options.trap_weights.value
|
|
|
|
|
|
|
|
if not sum(trap_items.values()):
|
|
|
|
trap_weight = 0
|
2023-07-18 20:02:57 -07:00
|
|
|
|
|
|
|
# Add filler items to the list.
|
2024-02-29 07:40:08 +01:00
|
|
|
filler_weight = 1 - trap_weight
|
|
|
|
|
2023-07-20 02:10:48 +02:00
|
|
|
filler_items: Dict[str, float]
|
2023-07-18 20:02:57 -07: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:
|
|
|
|
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 02:10:48 +02:00
|
|
|
int_weights: List[int] = build_weighted_int_list(filler_items.values(), remaining_quantity)
|
2023-07-18 20:02:57 -07:00
|
|
|
output.update(zip(filler_items.keys(), int_weights))
|
|
|
|
|
|
|
|
return output
|
|
|
|
|
2023-07-20 02:10:48 +02:00
|
|
|
def get_early_items(self) -> List[str]:
|
2023-07-18 20:02:57 -07:00
|
|
|
"""
|
|
|
|
Returns items that are ideal for placing on extremely early checks, like the tutorial gate.
|
|
|
|
"""
|
2023-07-28 09:39:56 +02:00
|
|
|
output: Set[str] = set()
|
2023-11-24 06:27:03 +01:00
|
|
|
if self._world.options.shuffle_symbols:
|
2024-09-05 17:10:09 +02:00
|
|
|
discards_on = self._world.options.shuffle_discarded_panels
|
|
|
|
mode = self._world.options.puzzle_randomization.current_key
|
2023-02-01 21:18:07 +01:00
|
|
|
|
2024-09-05 17:10:09 +02:00
|
|
|
output = static_witness_items.ALWAYS_GOOD_SYMBOL_ITEMS | static_witness_items.MODE_SPECIFIC_GOOD_ITEMS[mode]
|
|
|
|
if discards_on:
|
|
|
|
output |= static_witness_items.MODE_SPECIFIC_GOOD_DISCARD_ITEMS[mode]
|
2023-07-18 20:02:57 -07: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.
|
2025-05-10 17:49:49 -05:00
|
|
|
for plando_setting in self._world.options.plando_items:
|
|
|
|
if plando_setting.from_pool:
|
|
|
|
if isinstance(plando_setting.items, dict):
|
|
|
|
output -= {item for item, weight in plando_setting.items.items() if weight}
|
|
|
|
else:
|
|
|
|
# Assume this is some other kind of iterable.
|
|
|
|
for inner_item in plando_setting.items:
|
|
|
|
if isinstance(inner_item, str):
|
|
|
|
output -= {inner_item}
|
|
|
|
elif isinstance(inner_item, dict):
|
|
|
|
output -= {item for item, weight in inner_item.items() if weight}
|
2023-07-18 20:02:57 -07:00
|
|
|
|
|
|
|
# Sort the output for consistency across versions if the implementation changes but the logic does not.
|
2024-07-02 23:59:26 +02:00
|
|
|
return sorted(output)
|
2023-07-18 20:02:57 -07:00
|
|
|
|
2024-12-25 21:55:15 +01:00
|
|
|
def get_door_item_ids_in_pool(self) -> List[int]:
|
2023-07-18 20:02:57 -07:00
|
|
|
"""
|
2024-12-25 21:55:15 +01:00
|
|
|
Returns the ids of all door items that exist in the pool.
|
2023-07-18 20:02:57 -07:00
|
|
|
"""
|
2024-12-10 21:13:45 +01:00
|
|
|
|
2024-12-25 21:55:15 +01:00
|
|
|
return [
|
|
|
|
cast_not_none(item_data.ap_code) for item_data in self.item_data.values()
|
|
|
|
if isinstance(item_data.definition, DoorItemDefinition)
|
|
|
|
]
|
2023-07-18 20:02:57 -07:00
|
|
|
|
2023-07-20 02:10:48 +02:00
|
|
|
def get_symbol_ids_not_in_pool(self) -> List[int]:
|
2023-07-18 20:02:57 -07:00
|
|
|
"""
|
|
|
|
Returns the item IDs of symbol items that were defined in the configuration file but are not in the pool.
|
|
|
|
"""
|
2024-07-02 23:59:26 +02:00
|
|
|
return [
|
|
|
|
# data.ap_code is guaranteed for a symbol definition
|
2024-10-02 00:02:17 +02:00
|
|
|
cast_not_none(data.ap_code) for name, data in static_witness_items.ITEM_DATA.items()
|
2024-07-02 23:59:26 +02:00
|
|
|
if name not in self.item_data.keys() and data.definition.category is ItemCategory.SYMBOL
|
|
|
|
]
|
2023-07-18 20:02:57 -07:00
|
|
|
|
2023-07-20 02:10:48 +02:00
|
|
|
def get_progressive_item_ids_in_pool(self) -> Dict[int, List[int]]:
|
|
|
|
output: Dict[int, List[int]] = {}
|
2024-07-02 23:59:26 +02:00
|
|
|
for item_name, quantity in dict(self._mandatory_items.items()).items():
|
2023-07-18 20:02:57 -07:00
|
|
|
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
|
2024-08-24 02:08:04 +02:00
|
|
|
# items were removed from the pool when we pruned out all progression items not in the options.
|
2024-10-02 00:02:17 +02:00
|
|
|
output[cast_not_none(item.ap_code)] = [cast_not_none(static_witness_items.ITEM_DATA[child_item].ap_code)
|
|
|
|
for child_item in item.definition.child_item_names]
|
2023-07-18 20:02:57 -07:00
|
|
|
return output
|