2022-04-29 00:42:11 +02:00
|
|
|
"""
|
|
|
|
Defines the rules by which locations can be accessed,
|
|
|
|
depending on the items received
|
|
|
|
"""
|
2024-06-01 23:11:28 +02:00
|
|
|
from typing import TYPE_CHECKING
|
2022-04-29 00:42:11 +02:00
|
|
|
|
2023-11-24 06:27:03 +01:00
|
|
|
from BaseClasses import CollectionState
|
2024-04-12 00:27:42 +02:00
|
|
|
|
|
|
|
from worlds.generic.Rules import CollectionRule, set_rule
|
|
|
|
|
|
|
|
from .data import static_logic as static_witness_logic
|
2024-06-01 23:11:28 +02:00
|
|
|
from .data.utils import WitnessRule
|
2022-04-29 00:42:11 +02:00
|
|
|
from .locations import WitnessPlayerLocations
|
2024-04-12 00:27:42 +02: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
|
2022-04-29 00:42:11 +02:00
|
|
|
|
2023-11-24 06:27:03 +01:00
|
|
|
laser_hexes = [
|
|
|
|
"0x028A4",
|
|
|
|
"0x00274",
|
|
|
|
"0x032F9",
|
|
|
|
"0x01539",
|
|
|
|
"0x181B3",
|
|
|
|
"0x0C2B2",
|
|
|
|
"0x00509",
|
|
|
|
"0x00BF6",
|
|
|
|
"0x014BB",
|
|
|
|
"0x012FB",
|
|
|
|
"0x17C65",
|
|
|
|
]
|
|
|
|
|
|
|
|
|
2024-06-01 23:11:28 +02:00
|
|
|
def _has_laser(laser_hex: str, world: "WitnessWorld", player: int, redirect_required: bool) -> CollectionRule:
|
2024-01-16 13:13:44 +01:00
|
|
|
if laser_hex == "0x012FB" and redirect_required:
|
2023-11-24 06:27:03 +01:00
|
|
|
return lambda state: (
|
2024-04-12 00:27:42 +02:00
|
|
|
_can_solve_panel(laser_hex, world, world.player, world.player_logic, world.player_locations)(state)
|
2023-11-24 06:27:03 +01:00
|
|
|
and state.has("Desert Laser Redirection", player)
|
|
|
|
)
|
2024-07-02 23:59:26 +02:00
|
|
|
|
|
|
|
return _can_solve_panel(laser_hex, world, world.player, world.player_logic, world.player_locations)
|
2023-11-24 06:27:03 +01:00
|
|
|
|
|
|
|
|
2024-04-12 00:27:42 +02:00
|
|
|
def _has_lasers(amount: int, world: "WitnessWorld", redirect_required: bool) -> CollectionRule:
|
2023-11-24 06:27:03 +01:00
|
|
|
laser_lambdas = []
|
|
|
|
|
|
|
|
for laser_hex in laser_hexes:
|
2024-01-16 13:13:44 +01:00
|
|
|
has_laser_lambda = _has_laser(laser_hex, world, world.player, redirect_required)
|
2023-11-24 06:27:03 +01:00
|
|
|
|
|
|
|
laser_lambdas.append(has_laser_lambda)
|
|
|
|
|
|
|
|
return lambda state: sum(laser_lambda(state) for laser_lambda in laser_lambdas) >= amount
|
|
|
|
|
|
|
|
|
|
|
|
def _can_solve_panel(panel: str, world: "WitnessWorld", player: int, player_logic: WitnessPlayerLogic,
|
2024-04-12 00:27:42 +02:00
|
|
|
player_locations: WitnessPlayerLocations) -> CollectionRule:
|
2023-11-24 06:27:03 +01:00
|
|
|
"""
|
|
|
|
Determines whether a panel can be solved
|
|
|
|
"""
|
|
|
|
|
|
|
|
panel_obj = player_logic.REFERENCE_LOGIC.ENTITIES_BY_HEX[panel]
|
|
|
|
entity_name = panel_obj["checkName"]
|
|
|
|
|
2024-04-12 00:27:42 +02:00
|
|
|
if entity_name + " Solved" in player_locations.EVENT_LOCATION_TABLE:
|
2023-11-24 06:27:03 +01:00
|
|
|
return lambda state: state.has(player_logic.EVENT_ITEM_PAIRS[entity_name + " Solved"], player)
|
2024-07-02 23:59:26 +02:00
|
|
|
|
|
|
|
return make_lambda(panel, world)
|
2023-11-24 06:27:03 +01:00
|
|
|
|
|
|
|
|
2024-06-01 23:11:28 +02:00
|
|
|
def _can_do_expert_pp2(state: CollectionState, world: "WitnessWorld") -> bool:
|
|
|
|
"""
|
|
|
|
For Expert PP2, you need a way to access PP2 from the front, and a separate way from the back.
|
|
|
|
This condition is quite complicated. We'll attempt to evaluate it as lazily as possible.
|
|
|
|
"""
|
|
|
|
|
|
|
|
player = world.player
|
2024-08-19 07:49:06 +02:00
|
|
|
two_way_entrance_register = world.player_regions.two_way_entrance_register
|
2023-11-24 06:27:03 +01:00
|
|
|
|
2024-06-01 23:11:28 +02:00
|
|
|
front_access = (
|
2024-08-19 07:49:06 +02:00
|
|
|
any(e.can_reach(state) for e in two_way_entrance_register["Keep 2nd Pressure Plate", "Keep"])
|
2024-06-01 23:11:28 +02:00
|
|
|
and state.can_reach_region("Keep", player)
|
2023-11-24 06:27:03 +01:00
|
|
|
)
|
|
|
|
|
2024-06-01 23:11:28 +02:00
|
|
|
# If we don't have front access, we can't do PP2.
|
|
|
|
if not front_access:
|
|
|
|
return False
|
2023-11-24 06:27:03 +01:00
|
|
|
|
2024-06-01 23:11:28 +02:00
|
|
|
# Front access works. Now, we need to check for the many ways to access PP2 from the back.
|
|
|
|
# All of those ways lead through the PP3 exit door from PP4. So we check this first.
|
|
|
|
|
2024-08-19 07:49:06 +02:00
|
|
|
fourth_to_third = any(e.can_reach(state) for e in two_way_entrance_register[
|
2024-06-01 23:11:28 +02:00
|
|
|
"Keep 3rd Pressure Plate", "Keep 4th Pressure Plate"
|
|
|
|
])
|
|
|
|
|
|
|
|
# If we can't get from PP4 to PP3, we can't do PP2.
|
|
|
|
if not fourth_to_third:
|
|
|
|
return False
|
2023-11-24 06:27:03 +01:00
|
|
|
|
2024-06-01 23:11:28 +02:00
|
|
|
# We can go from PP4 to PP3. We now need to find a way to PP4.
|
|
|
|
# The shadows shortcut is the simplest way.
|
|
|
|
|
|
|
|
shadows_shortcut = (
|
2024-08-19 07:49:06 +02:00
|
|
|
any(e.can_reach(state) for e in two_way_entrance_register["Keep 4th Pressure Plate", "Shadows"])
|
2023-11-24 06:27:03 +01:00
|
|
|
)
|
|
|
|
|
2024-06-01 23:11:28 +02:00
|
|
|
if shadows_shortcut:
|
|
|
|
return True
|
|
|
|
|
|
|
|
# We don't have the Shadows shortcut. This means we need to come in through the PP4 exit door instead.
|
|
|
|
|
2024-08-19 07:49:06 +02:00
|
|
|
tower_to_pp4 = any(e.can_reach(state) for e in two_way_entrance_register["Keep 4th Pressure Plate", "Keep Tower"])
|
2023-11-24 06:27:03 +01:00
|
|
|
|
2024-06-01 23:11:28 +02:00
|
|
|
# If we don't have the PP4 exit door, we've run out of options.
|
|
|
|
if not tower_to_pp4:
|
|
|
|
return False
|
|
|
|
|
|
|
|
# We have the PP4 exit door. If we can get to Keep Tower from behind, we can do PP2.
|
|
|
|
# The simplest way would be the Tower Shortcut.
|
|
|
|
|
2024-08-19 07:49:06 +02:00
|
|
|
tower_shortcut = any(e.can_reach(state) for e in two_way_entrance_register["Keep", "Keep Tower"])
|
2024-06-01 23:11:28 +02:00
|
|
|
|
|
|
|
if tower_shortcut:
|
|
|
|
return True
|
|
|
|
|
|
|
|
# We don't have the Tower shortcut. At this point, there is one possibility remaining:
|
|
|
|
# Getting to Keep Tower through the hedge mazes. This can be done in a multitude of ways.
|
|
|
|
# No matter what, though, we would need Hedge Maze 4 Exit to Keep Tower.
|
|
|
|
|
2024-08-19 07:49:06 +02:00
|
|
|
tower_access_from_hedges = any(e.can_reach(state) for e in two_way_entrance_register["Keep 4th Maze", "Keep Tower"])
|
2023-11-24 06:27:03 +01:00
|
|
|
|
2024-06-01 23:11:28 +02:00
|
|
|
if not tower_access_from_hedges:
|
|
|
|
return False
|
|
|
|
|
|
|
|
# We can reach Keep Tower from Hedge Maze 4. If we now have the Hedge 4 Shortcut, we are immediately good.
|
|
|
|
|
2024-08-19 07:49:06 +02:00
|
|
|
hedge_4_shortcut = any(e.can_reach(state) for e in two_way_entrance_register["Keep 4th Maze", "Keep"])
|
2023-11-24 06:27:03 +01:00
|
|
|
|
2024-06-01 23:11:28 +02:00
|
|
|
# If we have the hedge 4 shortcut, that works.
|
|
|
|
if hedge_4_shortcut:
|
|
|
|
return True
|
|
|
|
|
|
|
|
# We don't have the hedge 4 shortcut. This means we would now need to come through Hedge Maze 3.
|
|
|
|
|
2024-08-19 07:49:06 +02:00
|
|
|
hedge_3_to_4 = any(e.can_reach(state) for e in two_way_entrance_register["Keep 4th Maze", "Keep 3rd Maze"])
|
2023-11-24 06:27:03 +01:00
|
|
|
|
2024-06-01 23:11:28 +02:00
|
|
|
if not hedge_3_to_4:
|
|
|
|
return False
|
|
|
|
|
|
|
|
# We can get to Hedge 4 from Hedge 3. If we have the Hedge 3 Shortcut, we're good.
|
|
|
|
|
2024-08-19 07:49:06 +02:00
|
|
|
hedge_3_shortcut = any(e.can_reach(state) for e in two_way_entrance_register["Keep 3rd Maze", "Keep"])
|
2023-11-24 06:27:03 +01:00
|
|
|
|
2024-06-01 23:11:28 +02:00
|
|
|
if hedge_3_shortcut:
|
|
|
|
return True
|
|
|
|
|
|
|
|
# We don't have Hedge 3 Shortcut. This means we would now need to come through Hedge Maze 2.
|
|
|
|
|
2024-08-19 07:49:06 +02:00
|
|
|
hedge_2_to_3 = any(e.can_reach(state) for e in two_way_entrance_register["Keep 3rd Maze", "Keep 2nd Maze"])
|
2023-11-24 06:27:03 +01:00
|
|
|
|
2024-06-01 23:11:28 +02:00
|
|
|
if not hedge_2_to_3:
|
|
|
|
return False
|
|
|
|
|
|
|
|
# We can get to Hedge 3 from Hedge 2. If we can get from Keep to Hedge 2, we're good.
|
|
|
|
# This covers both Hedge 1 Exit and Hedge 2 Shortcut, because Hedge 1 is just part of the Keep region.
|
|
|
|
|
2024-08-19 07:49:06 +02:00
|
|
|
return any(e.can_reach(state) for e in two_way_entrance_register["Keep 2nd Maze", "Keep"])
|
2023-11-24 06:27:03 +01:00
|
|
|
|
|
|
|
|
|
|
|
def _can_do_theater_to_tunnels(state: CollectionState, world: "WitnessWorld") -> bool:
|
2024-06-01 23:11:28 +02:00
|
|
|
"""
|
|
|
|
To do Tunnels Theater Flowers EP, you need to quickly move from Theater to Tunnels.
|
|
|
|
This condition is a little tricky. We'll attempt to evaluate it as lazily as possible.
|
|
|
|
"""
|
|
|
|
|
|
|
|
# Checking for access to Theater is not necessary, as solvability of Tutorial Video is checked in the other half
|
|
|
|
# of the Theater Flowers EP condition.
|
|
|
|
|
2024-08-19 07:49:06 +02:00
|
|
|
two_way_entrance_register = world.player_regions.two_way_entrance_register
|
2024-06-01 23:11:28 +02:00
|
|
|
|
2023-11-24 06:27:03 +01:00
|
|
|
direct_access = (
|
2024-08-19 07:49:06 +02:00
|
|
|
any(e.can_reach(state) for e in two_way_entrance_register["Tunnels", "Windmill Interior"])
|
|
|
|
and any(e.can_reach(state) for e in two_way_entrance_register["Theater", "Windmill Interior"])
|
2023-11-24 06:27:03 +01:00
|
|
|
)
|
|
|
|
|
2024-06-01 23:11:28 +02:00
|
|
|
if direct_access:
|
|
|
|
return True
|
|
|
|
|
|
|
|
# We don't have direct access through the shortest path.
|
|
|
|
# This means we somehow need to exit Theater to the Main Island, and then enter Tunnels from the Main Island.
|
|
|
|
# Getting to Tunnels through Mountain -> Caves -> Tunnels is way too slow, so we only expect paths through Town.
|
|
|
|
|
|
|
|
# We need a way from Theater to Town. This is actually guaranteed, otherwise we wouldn't be in Theater.
|
|
|
|
# The only ways to Theater are through Town and Tunnels. We just checked the Tunnels way.
|
|
|
|
# This might need to be changed when warps are implemented.
|
|
|
|
|
|
|
|
# We also need a way from Town to Tunnels.
|
2023-11-24 06:27:03 +01:00
|
|
|
|
2024-07-02 23:59:26 +02:00
|
|
|
return (
|
2024-08-19 07:49:06 +02:00
|
|
|
any(e.can_reach(state) for e in two_way_entrance_register["Tunnels", "Windmill Interior"])
|
|
|
|
and any(e.can_reach(state) for e in two_way_entrance_register["Outside Windmill", "Windmill Interior"])
|
|
|
|
or any(e.can_reach(state) for e in two_way_entrance_register["Tunnels", "Town"])
|
2023-11-24 06:27:03 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _has_item(item: str, world: "WitnessWorld", player: int,
|
2024-04-12 00:27:42 +02:00
|
|
|
player_logic: WitnessPlayerLogic, player_locations: WitnessPlayerLocations) -> CollectionRule:
|
2023-11-24 06:27:03 +01:00
|
|
|
if item in player_logic.REFERENCE_LOGIC.ALL_REGIONS_BY_NAME:
|
2024-06-01 23:11:28 +02:00
|
|
|
region = world.get_region(item)
|
|
|
|
return region.can_reach
|
2023-11-24 06:27:03 +01:00
|
|
|
if item == "7 Lasers":
|
|
|
|
laser_req = world.options.mountain_lasers.value
|
2024-01-16 13:13:44 +01:00
|
|
|
return _has_lasers(laser_req, world, False)
|
|
|
|
if item == "7 Lasers + Redirect":
|
|
|
|
laser_req = world.options.mountain_lasers.value
|
|
|
|
return _has_lasers(laser_req, world, True)
|
2023-11-24 06:27:03 +01:00
|
|
|
if item == "11 Lasers":
|
|
|
|
laser_req = world.options.challenge_lasers.value
|
2024-01-16 13:13:44 +01:00
|
|
|
return _has_lasers(laser_req, world, False)
|
|
|
|
if item == "11 Lasers + Redirect":
|
|
|
|
laser_req = world.options.challenge_lasers.value
|
|
|
|
return _has_lasers(laser_req, world, True)
|
2024-07-02 23:59:26 +02:00
|
|
|
if item == "PP2 Weirdness":
|
2023-11-24 06:27:03 +01:00
|
|
|
return lambda state: _can_do_expert_pp2(state, world)
|
2024-07-02 23:59:26 +02:00
|
|
|
if item == "Theater to Tunnels":
|
2023-11-24 06:27:03 +01:00
|
|
|
return lambda state: _can_do_theater_to_tunnels(state, world)
|
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 item in player_logic.USED_EVENT_NAMES_BY_HEX:
|
2024-04-12 00:27:42 +02:00
|
|
|
return _can_solve_panel(item, world, player, player_logic, player_locations)
|
2023-11-24 06:27:03 +01:00
|
|
|
|
2024-04-12 00:27:42 +02:00
|
|
|
prog_item = static_witness_logic.get_parent_progressive_item(item)
|
2023-11-24 06:27:03 +01:00
|
|
|
return lambda state: state.has(prog_item, player, player_logic.MULTI_AMOUNTS[item])
|
|
|
|
|
|
|
|
|
2024-06-01 23:11:28 +02:00
|
|
|
def _meets_item_requirements(requirements: WitnessRule, world: "WitnessWorld") -> CollectionRule:
|
2022-04-29 00:42:11 +02:00
|
|
|
"""
|
2023-11-24 06:27:03 +01:00
|
|
|
Checks whether item and panel requirements are met for
|
|
|
|
a panel
|
2022-04-29 00:42:11 +02:00
|
|
|
"""
|
|
|
|
|
2023-11-24 06:27:03 +01:00
|
|
|
lambda_conversion = [
|
2024-04-12 00:27:42 +02:00
|
|
|
[_has_item(item, world, world.player, world.player_logic, world.player_locations) for item in subset]
|
2023-11-24 06:27:03 +01:00
|
|
|
for subset in requirements
|
|
|
|
]
|
|
|
|
|
|
|
|
return lambda state: any(
|
|
|
|
all(condition(state) for condition in sub_requirement)
|
|
|
|
for sub_requirement in lambda_conversion
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2024-04-12 00:27:42 +02:00
|
|
|
def make_lambda(entity_hex: str, world: "WitnessWorld") -> CollectionRule:
|
2022-04-29 00:42:11 +02:00
|
|
|
"""
|
|
|
|
Lambdas are created in a for loop so values need to be captured
|
|
|
|
"""
|
2023-11-24 06:27:03 +01:00
|
|
|
entity_req = world.player_logic.REQUIREMENTS_BY_HEX[entity_hex]
|
|
|
|
|
|
|
|
return _meets_item_requirements(entity_req, world)
|
2022-04-29 00:42:11 +02:00
|
|
|
|
|
|
|
|
2024-04-12 00:27:42 +02:00
|
|
|
def set_rules(world: "WitnessWorld") -> None:
|
2022-04-29 00:42:11 +02:00
|
|
|
"""
|
|
|
|
Sets all rules for all locations
|
|
|
|
"""
|
|
|
|
|
2024-04-12 00:27:42 +02:00
|
|
|
for location in world.player_locations.CHECK_LOCATION_TABLE:
|
2022-04-29 00:42:11 +02:00
|
|
|
real_location = location
|
|
|
|
|
2024-04-12 00:27:42 +02:00
|
|
|
if location in world.player_locations.EVENT_LOCATION_TABLE:
|
2022-04-29 00:42:11 +02:00
|
|
|
real_location = location[:-7]
|
|
|
|
|
2023-11-24 06:27:03 +01:00
|
|
|
associated_entity = world.player_logic.REFERENCE_LOGIC.ENTITIES_BY_NAME[real_location]
|
|
|
|
entity_hex = associated_entity["entity_hex"]
|
|
|
|
|
|
|
|
rule = make_lambda(entity_hex, world)
|
2022-04-29 00:42:11 +02:00
|
|
|
|
2024-04-12 00:27:42 +02:00
|
|
|
location = world.get_location(location)
|
2022-04-29 00:42:11 +02:00
|
|
|
|
2023-11-24 06:27:03 +01:00
|
|
|
set_rule(location, rule)
|
2022-04-29 00:42:11 +02:00
|
|
|
|
2024-04-12 00:27:42 +02:00
|
|
|
world.multiworld.completion_condition[world.player] = lambda state: state.has("Victory", world.player)
|