mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 20:21:32 -06:00
The Witness: Automatic Postgame & Disabled Panels Calculation (#2698)
* Refactor postgame code to be more readable * Change all references to options to strings * oops * Fix some outdated code related to yaml-disabled EPs * Small fixes to short/longbox stuff (thanks Medic) * comment * fix duplicate * Removed triplicate lmfao * Better comment * added another 'unfun' postgame consideration * comment * more option strings * oops * Remove an unnecessary comparison * another string missed * New classification changes (Credit: Exempt-Medic) * Don't need to pass world * Comments * Replace it with another magic system because why not at this point :DDDDDD * oops * Oops * Another was missed * Make events conditions. Disable_Non_Randomized will no longer just 'have all events' * What the fuck? Has this just always been broken? * Don't have boolean function with 'not' in the name * Another useful classification * slight code refactor * Funny haha booleans * This would create a really bad merge error * I can't believe this actually kind of works * And here's the punchline. + some bugfixes * Comment dat code * Comments galore * LMAO OOPS * so nice I did it twice * debug x2 * Careful * Add more comments * That comment is a bit unnecessary now * Fix overriding region connections * Correct a comment * Correct again * Rename variable * Idk I guess this is in this branch now * More tweaking of postgame & comments * This is commit just exists to fix that grammar error * I think I can just fucking delete this now??? * Forgot to reset something here * Delete dead codepath * Obelisk Keys were getting yote erroneously * More comments * Fix duplicate connections * Oopsington III * performance improvements & cleanup * More rules cleanup and performance improvements * Oh cool I can do this huh * Okay but this is even more swag tho * Lazy eval * remove some implicit checks * Is this too magical yet * more guard magic * Maaaaaaaagiccccccccc * Laaaaaaaaaaaaaaaazzzzzzyyyyyyyyyyy * Make it docstring * Newline bc I like that better * this is a little spooky lol * lol * Wait * spoO * Better variable name and comment * Improved comment again * better API * oops I deleted a deepcopy * lol help * Help??? * player_regionsns lmao * Add some comments * Make doors disabled properly again. I hope this works * Don't disable lasers * Omega oops * Make Floor 2 Exit not exist * Make a fix that's warps compatible * I think this was an oversight, I tested a seed and it seems to have the same result * This is definitely less Violet than before * Does this feel more violet lol * Exception if a laser gets disabled, cleanup * Ruff * >:( * consistent utils import * Make autopostgame more reviewable (hopefully) * more reviewability * WitnessRule * replace another instance of it * lint * style * comment * found the bug * Move comment * Get rid of cache and ugly allow_victory * comments and lint
This commit is contained in:
@@ -17,13 +17,39 @@ When the world has parsed its options, a second function is called to finalize t
|
||||
|
||||
import copy
|
||||
from collections import defaultdict
|
||||
from functools import lru_cache
|
||||
from logging import warning
|
||||
from typing import TYPE_CHECKING, Dict, FrozenSet, List, Set, Tuple, cast
|
||||
from typing import TYPE_CHECKING, Dict, List, Set, Tuple, cast
|
||||
|
||||
from .data import static_logic as static_witness_logic
|
||||
from .data import utils
|
||||
from .data.item_definition_classes import DoorItemDefinition, ItemCategory, ProgressiveItemDefinition
|
||||
from .data.utils import (
|
||||
WitnessRule,
|
||||
define_new_region,
|
||||
get_boat,
|
||||
get_caves_except_path_to_challenge_exclusion_list,
|
||||
get_complex_additional_panels,
|
||||
get_complex_door_panels,
|
||||
get_complex_doors,
|
||||
get_disable_unrandomized_list,
|
||||
get_discard_exclusion_list,
|
||||
get_early_caves_list,
|
||||
get_early_caves_start_list,
|
||||
get_elevators_come_to_you,
|
||||
get_ep_all_individual,
|
||||
get_ep_easy,
|
||||
get_ep_no_eclipse,
|
||||
get_ep_obelisks,
|
||||
get_laser_shuffle,
|
||||
get_obelisk_keys,
|
||||
get_simple_additional_panels,
|
||||
get_simple_doors,
|
||||
get_simple_panels,
|
||||
get_symbol_shuffle_list,
|
||||
get_vault_exclusion_list,
|
||||
logical_and_witness_rules,
|
||||
logical_or_witness_rules,
|
||||
parse_lambda,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import WitnessWorld
|
||||
@@ -32,8 +58,7 @@ if TYPE_CHECKING:
|
||||
class WitnessPlayerLogic:
|
||||
"""WITNESS LOGIC CLASS"""
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def reduce_req_within_region(self, entity_hex: str) -> FrozenSet[FrozenSet[str]]:
|
||||
def reduce_req_within_region(self, entity_hex: str) -> WitnessRule:
|
||||
"""
|
||||
Panels in this game often only turn on when other panels are solved.
|
||||
Those other panels may have different item requirements.
|
||||
@@ -42,35 +67,39 @@ class WitnessPlayerLogic:
|
||||
Panels outside of the same region will still be checked manually.
|
||||
"""
|
||||
|
||||
if entity_hex in self.COMPLETELY_DISABLED_ENTITIES or entity_hex in self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES:
|
||||
if self.is_disabled(entity_hex):
|
||||
return frozenset()
|
||||
|
||||
entity_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity_hex]
|
||||
|
||||
these_items = frozenset({frozenset()})
|
||||
if entity_obj["region"] is not None and entity_obj["region"]["name"] in self.UNREACHABLE_REGIONS:
|
||||
return frozenset()
|
||||
|
||||
if entity_obj["id"]:
|
||||
these_items = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex]["items"]
|
||||
# For the requirement of an entity, we consider two things:
|
||||
# 1. Any items this entity needs (e.g. Symbols or Door Items)
|
||||
these_items = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex].get("items", frozenset({frozenset()}))
|
||||
# 2. Any entities that this entity depends on (e.g. one panel powering on the next panel in a set)
|
||||
these_panels = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex]["entities"]
|
||||
|
||||
# Remove any items that don't actually exist in the settings (e.g. Symbol Shuffle turned off)
|
||||
these_items = frozenset({
|
||||
subset.intersection(self.THEORETICAL_ITEMS_NO_MULTI)
|
||||
for subset in these_items
|
||||
})
|
||||
|
||||
# Update the list of "items that are actually being used by any entity"
|
||||
for subset in these_items:
|
||||
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(subset)
|
||||
|
||||
these_panels = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex]["panels"]
|
||||
|
||||
# If this entity is opened by a door item that exists in the itempool, add that item to its requirements.
|
||||
# Also, remove any original power requirements this entity might have had.
|
||||
if entity_hex in self.DOOR_ITEMS_BY_ID:
|
||||
door_items = frozenset({frozenset([item]) for item in self.DOOR_ITEMS_BY_ID[entity_hex]})
|
||||
|
||||
all_options: Set[FrozenSet[str]] = set()
|
||||
|
||||
for dependent_item in door_items:
|
||||
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(dependent_item)
|
||||
for items_option in these_items:
|
||||
all_options.add(items_option.union(dependent_item))
|
||||
|
||||
all_options = logical_and_witness_rules([door_items, these_items])
|
||||
|
||||
# If this entity is not an EP, and it has an associated door item, ignore the original power dependencies
|
||||
if static_witness_logic.ENTITIES_BY_HEX[entity_hex]["entityType"] != "EP":
|
||||
@@ -90,46 +119,70 @@ class WitnessPlayerLogic:
|
||||
else:
|
||||
these_items = all_options
|
||||
|
||||
disabled_eps = {eHex for eHex in self.COMPLETELY_DISABLED_ENTITIES
|
||||
if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[eHex]["entityType"] == "EP"}
|
||||
# Now that we have item requirements and entity dependencies, it's time for the dependency reduction.
|
||||
|
||||
these_panels = frozenset({panels - disabled_eps
|
||||
for panels in these_panels})
|
||||
|
||||
if these_panels == frozenset({frozenset()}):
|
||||
return these_items
|
||||
|
||||
all_options = set()
|
||||
# For each entity that this entity depends on (e.g. a panel turning on another panel),
|
||||
# Add that entities requirements to this entity.
|
||||
# If there are multiple options, consider each, and then or-chain them.
|
||||
all_options = list()
|
||||
|
||||
for option in these_panels:
|
||||
dependent_items_for_option = frozenset({frozenset()})
|
||||
|
||||
# For each entity in this option, resolve it to its actual requirement.
|
||||
for option_entity in option:
|
||||
dep_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX.get(option_entity)
|
||||
|
||||
if option_entity in self.ALWAYS_EVENT_NAMES_BY_HEX:
|
||||
new_items = frozenset({frozenset([option_entity])})
|
||||
elif (entity_hex, option_entity) in self.CONDITIONAL_EVENTS:
|
||||
new_items = frozenset({frozenset([option_entity])})
|
||||
self.USED_EVENT_NAMES_BY_HEX[option_entity] = self.CONDITIONAL_EVENTS[(entity_hex, option_entity)]
|
||||
elif option_entity in {"7 Lasers", "11 Lasers", "7 Lasers + Redirect", "11 Lasers + Redirect",
|
||||
"PP2 Weirdness", "Theater to Tunnels"}:
|
||||
if option_entity in {"7 Lasers", "11 Lasers", "7 Lasers + Redirect", "11 Lasers + Redirect",
|
||||
"PP2 Weirdness", "Theater to Tunnels"}:
|
||||
new_items = frozenset({frozenset([option_entity])})
|
||||
elif option_entity in self.DISABLE_EVERYTHING_BEHIND:
|
||||
new_items = frozenset()
|
||||
else:
|
||||
new_items = self.reduce_req_within_region(option_entity)
|
||||
if dep_obj["region"] and entity_obj["region"] != dep_obj["region"]:
|
||||
new_items = frozenset(
|
||||
frozenset(possibility | {dep_obj["region"]["name"]})
|
||||
for possibility in new_items
|
||||
)
|
||||
theoretical_new_items = self.get_entity_requirement(option_entity)
|
||||
|
||||
dependent_items_for_option = utils.dnf_and([dependent_items_for_option, new_items])
|
||||
if not theoretical_new_items:
|
||||
# If the dependent entity is unsolvable & it is an EP, the current entity is an Obelisk Side.
|
||||
# In this case, we actually have to skip it because it will just become pre-solved instead.
|
||||
if dep_obj["entityType"] == "EP":
|
||||
continue
|
||||
# If the dependent entity is unsolvable and is NOT an EP, this requirement option is invalid.
|
||||
new_items = frozenset()
|
||||
elif option_entity in self.ALWAYS_EVENT_NAMES_BY_HEX:
|
||||
new_items = frozenset({frozenset([option_entity])})
|
||||
elif (entity_hex, option_entity) in self.CONDITIONAL_EVENTS:
|
||||
new_items = frozenset({frozenset([option_entity])})
|
||||
self.USED_EVENT_NAMES_BY_HEX[option_entity] = self.CONDITIONAL_EVENTS[
|
||||
(entity_hex, option_entity)
|
||||
]
|
||||
else:
|
||||
new_items = theoretical_new_items
|
||||
if dep_obj["region"] and entity_obj["region"] != dep_obj["region"]:
|
||||
new_items = frozenset(
|
||||
frozenset(possibility | {dep_obj["region"]["name"]})
|
||||
for possibility in new_items
|
||||
)
|
||||
|
||||
for items_option in these_items:
|
||||
for dependent_item in dependent_items_for_option:
|
||||
all_options.add(items_option.union(dependent_item))
|
||||
dependent_items_for_option = logical_and_witness_rules([dependent_items_for_option, new_items])
|
||||
|
||||
return utils.dnf_remove_redundancies(frozenset(all_options))
|
||||
# Combine the resolved dependent entity requirements with the item requirements of this entity.
|
||||
all_options.append(logical_and_witness_rules([these_items, dependent_items_for_option]))
|
||||
|
||||
# or-chain all separate dependent entity options.
|
||||
return logical_or_witness_rules(all_options)
|
||||
|
||||
def get_entity_requirement(self, entity_hex: str) -> WitnessRule:
|
||||
"""
|
||||
Get requirement of entity by its hex code.
|
||||
These requirements are cached, with the actual function calculating them being reduce_req_within_region.
|
||||
"""
|
||||
requirement = self.REQUIREMENTS_BY_HEX.get(entity_hex)
|
||||
|
||||
if requirement is None:
|
||||
requirement = self.reduce_req_within_region(entity_hex)
|
||||
self.REQUIREMENTS_BY_HEX[entity_hex] = requirement
|
||||
|
||||
return requirement
|
||||
|
||||
def make_single_adjustment(self, adj_type: str, line: str) -> None:
|
||||
from .data import static_items as static_witness_items
|
||||
@@ -191,11 +244,11 @@ class WitnessPlayerLogic:
|
||||
line_split = line.split(" - ")
|
||||
|
||||
requirement = {
|
||||
"panels": utils.parse_lambda(line_split[1]),
|
||||
"entities": parse_lambda(line_split[1]),
|
||||
}
|
||||
|
||||
if len(line_split) > 2:
|
||||
required_items = utils.parse_lambda(line_split[2])
|
||||
required_items = parse_lambda(line_split[2])
|
||||
items_actually_in_the_game = [
|
||||
item_name for item_name, item_definition in static_witness_logic.ALL_ITEMS.items()
|
||||
if item_definition.category is ItemCategory.SYMBOL
|
||||
@@ -226,9 +279,9 @@ class WitnessPlayerLogic:
|
||||
return
|
||||
|
||||
if adj_type == "Region Changes":
|
||||
new_region_and_options = utils.define_new_region(line + ":")
|
||||
new_region_and_options = define_new_region(line + ":")
|
||||
|
||||
self.CONNECTIONS_BY_REGION_NAME[new_region_and_options[0]["name"]] = new_region_and_options[1]
|
||||
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[new_region_and_options[0]["name"]] = new_region_and_options[1]
|
||||
|
||||
return
|
||||
|
||||
@@ -238,102 +291,99 @@ class WitnessPlayerLogic:
|
||||
target_region = line_split[1]
|
||||
panel_set_string = line_split[2]
|
||||
|
||||
for connection in self.CONNECTIONS_BY_REGION_NAME[source_region]:
|
||||
for connection in self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region]:
|
||||
if connection[0] == target_region:
|
||||
self.CONNECTIONS_BY_REGION_NAME[source_region].remove(connection)
|
||||
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].remove(connection)
|
||||
|
||||
if panel_set_string == "TrueOneWay":
|
||||
self.CONNECTIONS_BY_REGION_NAME[source_region].add(
|
||||
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].add(
|
||||
(target_region, frozenset({frozenset(["TrueOneWay"])}))
|
||||
)
|
||||
else:
|
||||
new_lambda = connection[1] | utils.parse_lambda(panel_set_string)
|
||||
self.CONNECTIONS_BY_REGION_NAME[source_region].add((target_region, new_lambda))
|
||||
new_lambda = logical_or_witness_rules([connection[1], parse_lambda(panel_set_string)])
|
||||
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].add((target_region, new_lambda))
|
||||
break
|
||||
else: # Execute if loop did not break. TIL this is a thing you can do!
|
||||
new_conn = (target_region, utils.parse_lambda(panel_set_string))
|
||||
self.CONNECTIONS_BY_REGION_NAME[source_region].add(new_conn)
|
||||
else:
|
||||
new_conn = (target_region, parse_lambda(panel_set_string))
|
||||
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].add(new_conn)
|
||||
|
||||
if adj_type == "Added Locations":
|
||||
if "0x" in line:
|
||||
line = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[line]["checkName"]
|
||||
self.ADDED_CHECKS.add(line)
|
||||
|
||||
@staticmethod
|
||||
def handle_postgame(world: "WitnessWorld") -> List[List[str]]:
|
||||
# In shuffle_postgame, panels that become accessible "after or at the same time as the goal" are disabled.
|
||||
# This has a lot of complicated considerations, which I'll try my best to explain.
|
||||
def handle_postgame(self, world: "WitnessWorld") -> List[List[str]]:
|
||||
"""
|
||||
In shuffle_postgame, panels that become accessible "after or at the same time as the goal" are disabled.
|
||||
This mostly involves the disabling of key panels (e.g. long box when the goal is short box).
|
||||
These will then hava a cascading effect on other entities that are locked "behind" them.
|
||||
"""
|
||||
|
||||
postgame_adjustments = []
|
||||
|
||||
# Make some quick references to some options
|
||||
doors = world.options.shuffle_doors >= 2 # "Panels" mode has no overarching region accessibility implications.
|
||||
remote_doors = world.options.shuffle_doors >= 2 # "Panels" mode has no region accessibility implications.
|
||||
early_caves = world.options.early_caves
|
||||
victory = world.options.victory_condition
|
||||
mnt_lasers = world.options.mountain_lasers
|
||||
chal_lasers = world.options.challenge_lasers
|
||||
|
||||
# Goal is "short box" but short box requires more lasers than long box
|
||||
reverse_shortbox_goal = victory == "mountain_box_short" and mnt_lasers > chal_lasers
|
||||
|
||||
# Goal is "short box", and long box requires at least as many lasers as short box (as god intended)
|
||||
proper_shortbox_goal = victory == "mountain_box_short" and chal_lasers >= mnt_lasers
|
||||
|
||||
# Goal is "long box", but short box requires at least as many lasers than long box.
|
||||
reverse_longbox_goal = victory == "mountain_box_long" and mnt_lasers >= chal_lasers
|
||||
|
||||
# If goal is shortbox or "reverse longbox", you will never enter the mountain from the top before winning.
|
||||
mountain_enterable_from_top = not (victory == "mountain_box_short" or reverse_longbox_goal)
|
||||
# ||| Section 1: Proper postgame cases |||
|
||||
# When something only comes into logic after the goal, e.g. "longbox is postgame if the goal is shortbox".
|
||||
|
||||
# Caves & Challenge should never have anything if doors are vanilla - definitionally "post-game"
|
||||
# This is technically imprecise, but it matches player expectations better.
|
||||
if not (early_caves or doors):
|
||||
postgame_adjustments.append(utils.get_caves_exclusion_list())
|
||||
postgame_adjustments.append(utils.get_beyond_challenge_exclusion_list())
|
||||
# Disable anything directly locked by the victory panel
|
||||
self.DISABLE_EVERYTHING_BEHIND.add(self.VICTORY_LOCATION)
|
||||
|
||||
# If Challenge is the goal, some panels on the way need to be left on, as well as Challenge Vault box itself
|
||||
if not victory == "challenge":
|
||||
postgame_adjustments.append(utils.get_path_to_challenge_exclusion_list())
|
||||
postgame_adjustments.append(utils.get_challenge_vault_box_exclusion_list())
|
||||
# If we have a long box goal, Challenge is behind the amount of lasers required to just win.
|
||||
# This is technically slightly incorrect as the Challenge Vault Box could contain a *symbol* that is required
|
||||
# to open Mountain Entry (Stars 2). However, since there is a very easy sphere 1 snipe, this is not considered.
|
||||
if victory == "mountain_box_long":
|
||||
postgame_adjustments.append(["Disabled Locations:", "0x0A332 (Challenge Timer Start)"])
|
||||
|
||||
# Challenge can only have something if the goal is not challenge or longbox itself.
|
||||
# In case of shortbox, it'd have to be a "reverse shortbox" situation where shortbox requires *more* lasers.
|
||||
# In that case, it'd also have to be a doors mode, but that's already covered by the previous block.
|
||||
if not (victory == "elevator" or reverse_shortbox_goal):
|
||||
postgame_adjustments.append(utils.get_beyond_challenge_exclusion_list())
|
||||
if not victory == "challenge":
|
||||
postgame_adjustments.append(utils.get_challenge_vault_box_exclusion_list())
|
||||
|
||||
# Mountain can't be reached if the goal is shortbox (or "reverse long box")
|
||||
if not mountain_enterable_from_top:
|
||||
postgame_adjustments.append(utils.get_mountain_upper_exclusion_list())
|
||||
|
||||
# Same goes for lower mountain, but that one *can* be reached in remote doors modes.
|
||||
if not doors:
|
||||
postgame_adjustments.append(utils.get_mountain_lower_exclusion_list())
|
||||
|
||||
# The Mountain Bottom Floor Discard is a bit complicated, so we handle it separately. ("it" == the Discard)
|
||||
# In Elevator Goal, it is definitionally in the post-game, unless remote doors is played.
|
||||
# In Challenge Goal, it is before the Challenge, so it is not post-game.
|
||||
# In Short Box Goal, you can win before turning it on, UNLESS Short Box requires MORE lasers than long box.
|
||||
# In Long Box Goal, it is always in the post-game because solving long box is what turns it on.
|
||||
if not ((victory == "elevator" and doors) or victory == "challenge" or (reverse_shortbox_goal and doors)):
|
||||
# We now know Bottom Floor Discard is in the post-game.
|
||||
# This has different consequences depending on whether remote doors is being played.
|
||||
# If doors are vanilla, Bottom Floor Discard locks a door to an area, which has to be disabled as well.
|
||||
if doors:
|
||||
postgame_adjustments.append(utils.get_bottom_floor_discard_exclusion_list())
|
||||
else:
|
||||
postgame_adjustments.append(utils.get_bottom_floor_discard_nondoors_exclusion_list())
|
||||
|
||||
# In Challenge goal + early_caves + vanilla doors, you could find something important on Bottom Floor Discard,
|
||||
# including the Caves Shortcuts themselves if playing "early_caves: start_inventory".
|
||||
# This is another thing that was deemed "unfun" more than fitting the actual definition of post-game.
|
||||
if victory == "challenge" and early_caves and not doors:
|
||||
postgame_adjustments.append(utils.get_bottom_floor_discard_nondoors_exclusion_list())
|
||||
|
||||
# If we have a proper short box goal, long box will never be activated first.
|
||||
# If we have a proper short box goal, anything based on challenge lasers will never have something required.
|
||||
if proper_shortbox_goal:
|
||||
postgame_adjustments.append(["Disabled Locations:", "0xFFF00 (Mountain Box Long)"])
|
||||
postgame_adjustments.append(["Disabled Locations:", "0x0A332 (Challenge Timer Start)"])
|
||||
|
||||
# In a case where long box can be activated before short box, short box is postgame.
|
||||
if reverse_longbox_goal:
|
||||
postgame_adjustments.append(["Disabled Locations:", "0x09F7F (Mountain Box Short)"])
|
||||
|
||||
# ||| Section 2: "Fun" considerations |||
|
||||
# These are cases in which it was deemed "unfun" to have an "oops, all lasers" situation, especially when
|
||||
# it's for a single possible item.
|
||||
|
||||
mbfd_extra_exclusions = (
|
||||
# Progressive Dots 2 behind 11 lasers in an Elevator seed with vanilla doors = :(
|
||||
victory == "elevator" and not remote_doors
|
||||
|
||||
# Caves Shortcuts / Challenge Entry (Panel) on MBFD in a Challenge seed with vanilla doors = :(
|
||||
or victory == "challenge" and early_caves and not remote_doors
|
||||
)
|
||||
|
||||
if mbfd_extra_exclusions:
|
||||
postgame_adjustments.append(["Disabled Locations:", "0xFFF00 (Mountain Box Long)"])
|
||||
|
||||
# Another big postgame case that is missed is "Desert Laser Redirect (Panel)".
|
||||
# An 11 lasers longbox seed could technically have this item on Challenge Vault Box.
|
||||
# This case is not considered and we will act like Desert Laser Redirect (Panel) is always accessible.
|
||||
# (Which means we do no additional work, this comment just exists to document that case)
|
||||
|
||||
# ||| Section 3: "Post-or-equal-game" cases |||
|
||||
# These are cases in which something comes into logic *at the same time* as your goal and thus also can't
|
||||
# possibly have a required item. These can be a bit awkward.
|
||||
|
||||
# When your victory is Challenge, but you have to get to it the vanilla way, there are no required items
|
||||
# that can show up in the Caves that aren't also needed on the descent through Mountain.
|
||||
# So, we should disable all entities in the Caves and Tunnels *except* for those that are required to enter.
|
||||
if not (early_caves or remote_doors) and victory == "challenge":
|
||||
postgame_adjustments.append(get_caves_except_path_to_challenge_exclusion_list())
|
||||
|
||||
return postgame_adjustments
|
||||
|
||||
@@ -343,7 +393,7 @@ class WitnessPlayerLogic:
|
||||
|
||||
# Make condensed references to some options
|
||||
|
||||
doors = world.options.shuffle_doors >= 2 # "Panels" mode has no overarching region accessibility implications.
|
||||
remote_doors = world.options.shuffle_doors >= 2 # "Panels" mode has no overarching region access implications.
|
||||
lasers = world.options.shuffle_lasers
|
||||
victory = world.options.victory_condition
|
||||
mnt_lasers = world.options.mountain_lasers
|
||||
@@ -357,16 +407,16 @@ class WitnessPlayerLogic:
|
||||
if not world.options.shuffle_discarded_panels:
|
||||
# In disable_non_randomized, the discards are needed for alternate activation triggers, UNLESS both
|
||||
# (remote) doors and lasers are shuffled.
|
||||
if not world.options.disable_non_randomized_puzzles or (doors and lasers):
|
||||
adjustment_linesets_in_order.append(utils.get_discard_exclusion_list())
|
||||
if not world.options.disable_non_randomized_puzzles or (remote_doors and lasers):
|
||||
adjustment_linesets_in_order.append(get_discard_exclusion_list())
|
||||
|
||||
if doors:
|
||||
adjustment_linesets_in_order.append(utils.get_bottom_floor_discard_exclusion_list())
|
||||
if remote_doors:
|
||||
adjustment_linesets_in_order.append(["Disabled Locations:", "0x17FA2"])
|
||||
|
||||
if not world.options.shuffle_vault_boxes:
|
||||
adjustment_linesets_in_order.append(utils.get_vault_exclusion_list())
|
||||
adjustment_linesets_in_order.append(get_vault_exclusion_list())
|
||||
if not victory == "challenge":
|
||||
adjustment_linesets_in_order.append(utils.get_challenge_vault_box_exclusion_list())
|
||||
adjustment_linesets_in_order.append(["Disabled Locations:", "0x0A332"])
|
||||
|
||||
# Victory Condition
|
||||
|
||||
@@ -389,54 +439,54 @@ class WitnessPlayerLogic:
|
||||
])
|
||||
|
||||
if world.options.disable_non_randomized_puzzles:
|
||||
adjustment_linesets_in_order.append(utils.get_disable_unrandomized_list())
|
||||
adjustment_linesets_in_order.append(get_disable_unrandomized_list())
|
||||
|
||||
if world.options.shuffle_symbols:
|
||||
adjustment_linesets_in_order.append(utils.get_symbol_shuffle_list())
|
||||
adjustment_linesets_in_order.append(get_symbol_shuffle_list())
|
||||
|
||||
if world.options.EP_difficulty == "normal":
|
||||
adjustment_linesets_in_order.append(utils.get_ep_easy())
|
||||
adjustment_linesets_in_order.append(get_ep_easy())
|
||||
elif world.options.EP_difficulty == "tedious":
|
||||
adjustment_linesets_in_order.append(utils.get_ep_no_eclipse())
|
||||
adjustment_linesets_in_order.append(get_ep_no_eclipse())
|
||||
|
||||
if world.options.door_groupings == "regional":
|
||||
if world.options.shuffle_doors == "panels":
|
||||
adjustment_linesets_in_order.append(utils.get_simple_panels())
|
||||
adjustment_linesets_in_order.append(get_simple_panels())
|
||||
elif world.options.shuffle_doors == "doors":
|
||||
adjustment_linesets_in_order.append(utils.get_simple_doors())
|
||||
adjustment_linesets_in_order.append(get_simple_doors())
|
||||
elif world.options.shuffle_doors == "mixed":
|
||||
adjustment_linesets_in_order.append(utils.get_simple_doors())
|
||||
adjustment_linesets_in_order.append(utils.get_simple_additional_panels())
|
||||
adjustment_linesets_in_order.append(get_simple_doors())
|
||||
adjustment_linesets_in_order.append(get_simple_additional_panels())
|
||||
else:
|
||||
if world.options.shuffle_doors == "panels":
|
||||
adjustment_linesets_in_order.append(utils.get_complex_door_panels())
|
||||
adjustment_linesets_in_order.append(utils.get_complex_additional_panels())
|
||||
adjustment_linesets_in_order.append(get_complex_door_panels())
|
||||
adjustment_linesets_in_order.append(get_complex_additional_panels())
|
||||
elif world.options.shuffle_doors == "doors":
|
||||
adjustment_linesets_in_order.append(utils.get_complex_doors())
|
||||
adjustment_linesets_in_order.append(get_complex_doors())
|
||||
elif world.options.shuffle_doors == "mixed":
|
||||
adjustment_linesets_in_order.append(utils.get_complex_doors())
|
||||
adjustment_linesets_in_order.append(utils.get_complex_additional_panels())
|
||||
adjustment_linesets_in_order.append(get_complex_doors())
|
||||
adjustment_linesets_in_order.append(get_complex_additional_panels())
|
||||
|
||||
if world.options.shuffle_boat:
|
||||
adjustment_linesets_in_order.append(utils.get_boat())
|
||||
adjustment_linesets_in_order.append(get_boat())
|
||||
|
||||
if world.options.early_caves == "starting_inventory":
|
||||
adjustment_linesets_in_order.append(utils.get_early_caves_start_list())
|
||||
adjustment_linesets_in_order.append(get_early_caves_start_list())
|
||||
|
||||
if world.options.early_caves == "add_to_pool" and not doors:
|
||||
adjustment_linesets_in_order.append(utils.get_early_caves_list())
|
||||
if world.options.early_caves == "add_to_pool" and not remote_doors:
|
||||
adjustment_linesets_in_order.append(get_early_caves_list())
|
||||
|
||||
if world.options.elevators_come_to_you:
|
||||
adjustment_linesets_in_order.append(utils.get_elevators_come_to_you())
|
||||
adjustment_linesets_in_order.append(get_elevators_come_to_you())
|
||||
|
||||
for item in self.YAML_ADDED_ITEMS:
|
||||
adjustment_linesets_in_order.append(["Items:", item])
|
||||
|
||||
if lasers:
|
||||
adjustment_linesets_in_order.append(utils.get_laser_shuffle())
|
||||
adjustment_linesets_in_order.append(get_laser_shuffle())
|
||||
|
||||
if world.options.shuffle_EPs and world.options.obelisk_keys:
|
||||
adjustment_linesets_in_order.append(utils.get_obelisk_keys())
|
||||
adjustment_linesets_in_order.append(get_obelisk_keys())
|
||||
|
||||
if world.options.shuffle_EPs == "obelisk_sides":
|
||||
ep_gen = ((ep_hex, ep_obj) for (ep_hex, ep_obj) in self.REFERENCE_LOGIC.ENTITIES_BY_HEX.items()
|
||||
@@ -448,10 +498,10 @@ class WitnessPlayerLogic:
|
||||
ep_name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[ep_hex]["checkName"]
|
||||
self.ALWAYS_EVENT_NAMES_BY_HEX[ep_hex] = f"{obelisk_name} - {ep_name}"
|
||||
else:
|
||||
adjustment_linesets_in_order.append(["Disabled Locations:"] + utils.get_ep_obelisks()[1:])
|
||||
adjustment_linesets_in_order.append(["Disabled Locations:"] + get_ep_obelisks()[1:])
|
||||
|
||||
if not world.options.shuffle_EPs:
|
||||
adjustment_linesets_in_order.append(["Disabled Locations:"] + utils.get_ep_all_individual()[1:])
|
||||
adjustment_linesets_in_order.append(["Disabled Locations:"] + get_ep_all_individual()[1:])
|
||||
|
||||
for yaml_disabled_location in self.YAML_DISABLED_LOCATIONS:
|
||||
if yaml_disabled_location not in self.REFERENCE_LOGIC.ENTITIES_BY_NAME:
|
||||
@@ -482,16 +532,189 @@ class WitnessPlayerLogic:
|
||||
if entity_id in self.DOOR_ITEMS_BY_ID:
|
||||
del self.DOOR_ITEMS_BY_ID[entity_id]
|
||||
|
||||
def make_dependency_reduced_checklist(self) -> None:
|
||||
def discover_reachable_regions(self):
|
||||
"""
|
||||
Turns dependent check set into semi-independent check set
|
||||
Some options disable panels or remove specific items.
|
||||
This can make entire regions completely unreachable, because all their incoming connections are invalid.
|
||||
This function starts from the Entry region and performs a graph search to discover all reachable regions.
|
||||
"""
|
||||
reachable_regions = {"Entry"}
|
||||
new_regions_found = True
|
||||
|
||||
# This for loop "floods" the region graph until no more new regions are discovered.
|
||||
# Note that connections that rely on disabled entities are considered invalid.
|
||||
# This fact may lead to unreachable regions being discovered.
|
||||
while new_regions_found:
|
||||
new_regions_found = False
|
||||
regions_to_check = reachable_regions.copy()
|
||||
|
||||
# Find new regions through connections from currently reachable regions
|
||||
while regions_to_check:
|
||||
next_region = regions_to_check.pop()
|
||||
|
||||
for region_exit in self.CONNECTIONS_BY_REGION_NAME[next_region]:
|
||||
target = region_exit[0]
|
||||
|
||||
if target in reachable_regions:
|
||||
continue
|
||||
|
||||
# There may be multiple conncetions between two regions. We should check all of them to see if
|
||||
# any of them are valid.
|
||||
for option in region_exit[1]:
|
||||
# If a connection requires having access to a not-yet-reached region, do not consider it.
|
||||
# Otherwise, this connection is valid, and the target region is reachable -> break for loop
|
||||
if not any(req in self.CONNECTIONS_BY_REGION_NAME and req not in reachable_regions
|
||||
for req in option):
|
||||
break
|
||||
# If none of the connections were valid, this region is not reachable this way, for now.
|
||||
else:
|
||||
continue
|
||||
|
||||
new_regions_found = True
|
||||
regions_to_check.add(target)
|
||||
reachable_regions.add(target)
|
||||
|
||||
return reachable_regions
|
||||
|
||||
def find_unsolvable_entities(self, world: "WitnessWorld") -> None:
|
||||
"""
|
||||
Settings like "shuffle_postgame: False" may disable certain panels.
|
||||
This may make panels or regions logically locked by those panels unreachable.
|
||||
We will determine these automatically and disable them as well.
|
||||
"""
|
||||
|
||||
all_regions = set(self.CONNECTIONS_BY_REGION_NAME_THEORETICAL)
|
||||
|
||||
while True:
|
||||
# Re-make the dependency reduced entity requirements dict, which depends on currently
|
||||
self.make_dependency_reduced_checklist()
|
||||
|
||||
# Check if any regions have become unreachable.
|
||||
reachable_regions = self.discover_reachable_regions()
|
||||
new_unreachable_regions = all_regions - reachable_regions - self.UNREACHABLE_REGIONS
|
||||
if new_unreachable_regions:
|
||||
self.UNREACHABLE_REGIONS.update(new_unreachable_regions)
|
||||
|
||||
# Then, discover unreachable entities.
|
||||
newly_discovered_disabled_entities = set()
|
||||
|
||||
# First, entities in unreachable regions are obviously themselves unreachable.
|
||||
for region in new_unreachable_regions:
|
||||
for entity in static_witness_logic.ALL_REGIONS_BY_NAME[region]["physical_entities"]:
|
||||
# Never disable the Victory Location.
|
||||
if entity == self.VICTORY_LOCATION:
|
||||
continue
|
||||
|
||||
# Never disable a laser (They should still function even if you can't walk up to them).
|
||||
if static_witness_logic.ENTITIES_BY_HEX[entity]["entityType"] == "Laser":
|
||||
continue
|
||||
|
||||
newly_discovered_disabled_entities.add(entity)
|
||||
|
||||
# Secondly, any entities that depend on disabled entities are unreachable as well.
|
||||
for entity, req in self.REQUIREMENTS_BY_HEX.items():
|
||||
# If the requirement is empty (unsolvable) and it isn't disabled already, add it to "newly disabled"
|
||||
if not req and not self.is_disabled(entity):
|
||||
# Never disable the Victory Location.
|
||||
if entity == self.VICTORY_LOCATION:
|
||||
continue
|
||||
|
||||
# If we are disabling a laser, something has gone wrong.
|
||||
if static_witness_logic.ENTITIES_BY_HEX[entity]["entityType"] == "Laser":
|
||||
laser_name = static_witness_logic.ENTITIES_BY_HEX[entity]["checkName"]
|
||||
player_name = world.multiworld.get_player_name(world.player)
|
||||
raise RuntimeError(f"Somehow, {laser_name} was disabled for player {player_name}."
|
||||
f" This is not allowed to happen, please report to Violet.")
|
||||
|
||||
newly_discovered_disabled_entities.add(entity)
|
||||
|
||||
# Disable the newly determined unreachable entities.
|
||||
self.COMPLETELY_DISABLED_ENTITIES.update(newly_discovered_disabled_entities)
|
||||
|
||||
# If we didn't find any new unreachable regions or entities this cycle, we are done.
|
||||
# If we did, we need to do another cycle to see if even more regions or entities became unreachable.
|
||||
if not new_unreachable_regions and not newly_discovered_disabled_entities:
|
||||
return
|
||||
|
||||
def reduce_connection_requirement(self, connection: Tuple[str, WitnessRule]) -> WitnessRule:
|
||||
all_possibilities = []
|
||||
|
||||
# Check each traversal option individually
|
||||
for option in connection[1]:
|
||||
individual_entity_requirements = []
|
||||
for entity in option:
|
||||
# If a connection requires solving a disabled entity, it is not valid.
|
||||
if not self.solvability_guaranteed(entity) or entity in self.DISABLE_EVERYTHING_BEHIND:
|
||||
individual_entity_requirements.append(frozenset())
|
||||
# If a connection requires acquiring an event, add that event to its requirements.
|
||||
elif (entity in self.ALWAYS_EVENT_NAMES_BY_HEX
|
||||
or entity not in self.REFERENCE_LOGIC.ENTITIES_BY_HEX):
|
||||
individual_entity_requirements.append(frozenset({frozenset({entity})}))
|
||||
# If a connection requires entities, use their newly calculated independent requirements.
|
||||
else:
|
||||
entity_req = self.get_entity_requirement(entity)
|
||||
|
||||
if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity]["region"]:
|
||||
region_name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity]["region"]["name"]
|
||||
entity_req = logical_and_witness_rules([entity_req, frozenset({frozenset({region_name})})])
|
||||
|
||||
individual_entity_requirements.append(entity_req)
|
||||
|
||||
# Merge all possible requirements into one DNF condition.
|
||||
all_possibilities.append(logical_and_witness_rules(individual_entity_requirements))
|
||||
|
||||
return logical_or_witness_rules(all_possibilities)
|
||||
|
||||
def make_dependency_reduced_checklist(self):
|
||||
"""
|
||||
Every entity has a requirement. This requirement may involve other entities.
|
||||
Example: Solving a panel powers a cable, and that cable turns on the next panel.
|
||||
These dependencies are specified in the logic files (e.g. "WitnessLogic.txt") and may be modified by options.
|
||||
|
||||
Recursively having to check the requirements of every dependent entity would be very slow, so we go through this
|
||||
recursion once and make a single, independent requirement for each entity.
|
||||
|
||||
This requirement may include symbol items, door items, regions, or events.
|
||||
A requirement is saved as a two-dimensional set that represents a disjuntive normal form.
|
||||
"""
|
||||
|
||||
# Requirements are cached per entity. However, we might redo the whole reduction process multiple times.
|
||||
# So, we first clear this cache.
|
||||
self.REQUIREMENTS_BY_HEX = dict()
|
||||
|
||||
# We also clear any data structures that we might have filled in a previous dependency reduction
|
||||
self.REQUIREMENTS_BY_HEX = dict()
|
||||
self.USED_EVENT_NAMES_BY_HEX = dict()
|
||||
self.CONNECTIONS_BY_REGION_NAME = dict()
|
||||
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI = set()
|
||||
|
||||
# Make independent requirements for entities
|
||||
for entity_hex in self.DEPENDENT_REQUIREMENTS_BY_HEX.keys():
|
||||
indep_requirement = self.reduce_req_within_region(entity_hex)
|
||||
indep_requirement = self.get_entity_requirement(entity_hex)
|
||||
|
||||
self.REQUIREMENTS_BY_HEX[entity_hex] = indep_requirement
|
||||
|
||||
# Make independent region connection requirements based on the entities they require
|
||||
for region, connections in self.CONNECTIONS_BY_REGION_NAME_THEORETICAL.items():
|
||||
self.CONNECTIONS_BY_REGION_NAME[region] = []
|
||||
|
||||
new_connections = []
|
||||
|
||||
for connection in connections:
|
||||
overall_requirement = self.reduce_connection_requirement(connection)
|
||||
|
||||
# If there is a way to use this connection, add it.
|
||||
if overall_requirement:
|
||||
new_connections.append((connection[0], overall_requirement))
|
||||
|
||||
# If there are any usable outgoing connections from this region, add them.
|
||||
if new_connections:
|
||||
self.CONNECTIONS_BY_REGION_NAME[region] = new_connections
|
||||
|
||||
def finalize_items(self):
|
||||
"""
|
||||
Finalise which items are used in the world, and handle their progressive versions.
|
||||
"""
|
||||
for item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI:
|
||||
if item not in self.THEORETICAL_ITEMS:
|
||||
progressive_item_name = static_witness_logic.get_parent_progressive_item(item)
|
||||
@@ -505,33 +728,6 @@ class WitnessPlayerLogic:
|
||||
else:
|
||||
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(item)
|
||||
|
||||
for region, connections in self.CONNECTIONS_BY_REGION_NAME.items():
|
||||
new_connections = []
|
||||
|
||||
for connection in connections:
|
||||
overall_requirement = frozenset()
|
||||
|
||||
for option in connection[1]:
|
||||
individual_entity_requirements = []
|
||||
for entity in option:
|
||||
if (entity in self.ALWAYS_EVENT_NAMES_BY_HEX
|
||||
or entity not in self.REFERENCE_LOGIC.ENTITIES_BY_HEX):
|
||||
individual_entity_requirements.append(frozenset({frozenset({entity})}))
|
||||
else:
|
||||
entity_req = self.reduce_req_within_region(entity)
|
||||
|
||||
if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity]["region"]:
|
||||
region_name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity]["region"]["name"]
|
||||
entity_req = utils.dnf_and([entity_req, frozenset({frozenset({region_name})})])
|
||||
|
||||
individual_entity_requirements.append(entity_req)
|
||||
|
||||
overall_requirement |= utils.dnf_and(individual_entity_requirements)
|
||||
|
||||
new_connections.append((connection[0], overall_requirement))
|
||||
|
||||
self.CONNECTIONS_BY_REGION_NAME[region] = new_connections
|
||||
|
||||
def solvability_guaranteed(self, entity_hex: str) -> bool:
|
||||
return not (
|
||||
entity_hex in self.ENTITIES_WITHOUT_ENSURED_SOLVABILITY
|
||||
@@ -539,6 +735,12 @@ class WitnessPlayerLogic:
|
||||
or entity_hex in self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES
|
||||
)
|
||||
|
||||
def is_disabled(self, entity_hex: str) -> bool:
|
||||
return (
|
||||
entity_hex in self.COMPLETELY_DISABLED_ENTITIES
|
||||
or entity_hex in self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES
|
||||
)
|
||||
|
||||
def determine_unrequired_entities(self, world: "WitnessWorld") -> None:
|
||||
"""Figure out which major items are actually useless in this world's settings"""
|
||||
|
||||
@@ -588,7 +790,6 @@ class WitnessPlayerLogic:
|
||||
"0x01BEA": difficulty == "none" and eps_shuffled, # Keep PP2
|
||||
"0x0A0C9": eps_shuffled or discards_shuffled or disable_non_randomized, # Cargo Box Entry Door
|
||||
"0x09EEB": discards_shuffled or mountain_upper_included, # Mountain Floor 2 Elevator Control Panel
|
||||
"0x09EDD": mountain_upper_included, # Mountain Floor 2 Exit Door
|
||||
"0x17CAB": symbols_shuffled or not disable_non_randomized or "0x17CAB" not in self.DOOR_ITEMS_BY_ID,
|
||||
# Jungle Popup Wall Panel
|
||||
}
|
||||
@@ -598,20 +799,24 @@ class WitnessPlayerLogic:
|
||||
item_name for item_name, is_required in is_item_required_dict.items() if not is_required
|
||||
}
|
||||
|
||||
def make_event_item_pair(self, panel: str) -> Tuple[str, str]:
|
||||
def make_event_item_pair(self, entity_hex: str) -> Tuple[str, str]:
|
||||
"""
|
||||
Makes a pair of an event panel and its event item
|
||||
"""
|
||||
action = " Opened" if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[panel]["entityType"] == "Door" else " Solved"
|
||||
action = " Opened" if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity_hex]["entityType"] == "Door" else " Solved"
|
||||
|
||||
name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[panel]["checkName"] + action
|
||||
if panel not in self.USED_EVENT_NAMES_BY_HEX:
|
||||
warning(f'Panel "{name}" does not have an associated event name.')
|
||||
self.USED_EVENT_NAMES_BY_HEX[panel] = name + " Event"
|
||||
pair = (name, self.USED_EVENT_NAMES_BY_HEX[panel])
|
||||
name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity_hex]["checkName"] + action
|
||||
if entity_hex not in self.USED_EVENT_NAMES_BY_HEX:
|
||||
warning(f'Entity "{name}" does not have an associated event name.')
|
||||
self.USED_EVENT_NAMES_BY_HEX[entity_hex] = name + " Event"
|
||||
pair = (name, self.USED_EVENT_NAMES_BY_HEX[entity_hex])
|
||||
return pair
|
||||
|
||||
def make_event_panel_lists(self) -> None:
|
||||
"""
|
||||
Makes event-item pairs for entities with associated events, unless these entities are disabled.
|
||||
"""
|
||||
|
||||
self.ALWAYS_EVENT_NAMES_BY_HEX[self.VICTORY_LOCATION] = "Victory"
|
||||
|
||||
self.USED_EVENT_NAMES_BY_HEX.update(self.ALWAYS_EVENT_NAMES_BY_HEX)
|
||||
@@ -636,6 +841,8 @@ class WitnessPlayerLogic:
|
||||
|
||||
self.ENTITIES_WITHOUT_ENSURED_SOLVABILITY = set()
|
||||
|
||||
self.UNREACHABLE_REGIONS = set()
|
||||
|
||||
self.THEORETICAL_ITEMS = set()
|
||||
self.THEORETICAL_ITEMS_NO_MULTI = set()
|
||||
self.MULTI_AMOUNTS = defaultdict(lambda: 1)
|
||||
@@ -654,14 +861,16 @@ class WitnessPlayerLogic:
|
||||
elif self.DIFFICULTY == "none":
|
||||
self.REFERENCE_LOGIC = static_witness_logic.vanilla
|
||||
|
||||
self.CONNECTIONS_BY_REGION_NAME = copy.deepcopy(self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME)
|
||||
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL = copy.deepcopy(
|
||||
self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME
|
||||
)
|
||||
self.CONNECTIONS_BY_REGION_NAME = dict()
|
||||
self.DEPENDENT_REQUIREMENTS_BY_HEX = copy.deepcopy(self.REFERENCE_LOGIC.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX)
|
||||
self.REQUIREMENTS_BY_HEX = dict()
|
||||
|
||||
# Determining which panels need to be events is a difficult process.
|
||||
# At the end, we will have EVENT_ITEM_PAIRS for all the necessary ones.
|
||||
self.EVENT_ITEM_PAIRS = dict()
|
||||
self.COMPLETELY_DISABLED_ENTITIES = set()
|
||||
self.DISABLE_EVERYTHING_BEHIND = set()
|
||||
self.PRECOMPLETED_LOCATIONS = set()
|
||||
self.EXCLUDED_LOCATIONS = set()
|
||||
self.ADDED_CHECKS = set()
|
||||
@@ -687,7 +896,18 @@ class WitnessPlayerLogic:
|
||||
self.USED_EVENT_NAMES_BY_HEX = {}
|
||||
self.CONDITIONAL_EVENTS = {}
|
||||
|
||||
# The basic requirements to solve each entity come from StaticWitnessLogic.
|
||||
# However, for any given world, the options (e.g. which item shuffles are enabled) affect the requirements.
|
||||
self.make_options_adjustments(world)
|
||||
self.determine_unrequired_entities(world)
|
||||
self.find_unsolvable_entities(world)
|
||||
|
||||
# After we have adjusted the raw requirements, we perform a dependency reduction for the entity requirements.
|
||||
# This will make the access conditions way faster, instead of recursively checking dependent entities each time.
|
||||
self.make_dependency_reduced_checklist()
|
||||
|
||||
# Finalize which items actually exist in the MultiWorld and which get grouped into progressive items.
|
||||
self.finalize_items()
|
||||
|
||||
# Create event-item pairs for specific panels in the game.
|
||||
self.make_event_panel_lists()
|
||||
|
||||
Reference in New Issue
Block a user