The Witness: Obelisk Keys (#2805)

This commit is contained in:
NewSoupVi 2024-03-12 20:04:13 +01:00 committed by GitHub
parent ae6c16bde1
commit 4bf676e588
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 94 additions and 31 deletions

View File

@ -265,6 +265,13 @@ Doors:
2165 - Caves Panels - 0x3369D,0x00FF8,0x0A16E,0x335AB,0x335AC
2170 - Tunnels Panels - 0x09E85,0x039B4
2200 - Desert Obelisk Key - 0x0332B,0x03367,0x28B8A,0x037B6,0x037B2,0x000F7,0x3351D,0x0053C,0x00771,0x335C8,0x335C9,0x337F8,0x037BB,0x220E4,0x220E5,0x334B9,0x334BC,0x22106,0x0A14C,0x0A14D,0x00359
2201 - Monastery Obelisk Key - 0x03ABC,0x03ABE,0x03AC0,0x03AC4,0x03AC5,0x03BE2,0x03BE3,0x0A409,0x006E5,0x006E6,0x006E7,0x034A7,0x034AD,0x034AF,0x03DAB,0x03DAC,0x03DAD,0x03E01,0x289F4,0x289F5,0x00263
2202 - Treehouse Obelisk Key - 0x0053D,0x0053E,0x00769,0x33721,0x220A7,0x220BD,0x03B22,0x03B23,0x03B24,0x03B25,0x03A79,0x28ABD,0x28ABE,0x3388F,0x28B29,0x28B2A,0x018B6,0x033BE,0x033BF,0x033DD,0x033E5,0x28AE9,0x3348F,0x00097
2203 - Mountainside Obelisk Key - 0x001A3,0x335AE,0x000D3,0x035F5,0x09D5D,0x09D5E,0x09D63,0x3370E,0x035DE,0x03601,0x03603,0x03D0D,0x3369A,0x336C8,0x33505,0x03A9E,0x016B2,0x3365F,0x03731,0x036CE,0x03C07,0x03A93,0x03AA6,0x3397C,0x0105D,0x0A304,0x035CB,0x035CF,0x00367
2204 - Quarry Obelisk Key - 0x28A7B,0x005F6,0x00859,0x17CB9,0x28A4A,0x334B6,0x00614,0x0069D,0x28A4C,0x289CF,0x289D1,0x33692,0x03E77,0x03E7C,0x22073
2205 - Town Obelisk Key - 0x035C7,0x01848,0x03D06,0x33530,0x33600,0x28A2F,0x28A37,0x334A3,0x3352F,0x33857,0x33879,0x03C19,0x28B30,0x035C9,0x03335,0x03412,0x038A6,0x038AA,0x03E3F,0x03E40,0x28B8E,0x28B91,0x03BCE,0x03BCF,0x03BD1,0x339B6,0x33A20,0x33A29,0x33A2A,0x33B06,0x0A16C
Lasers:
1500 - Symmetry Laser - 0x00509
1501 - Desert Laser - 0x012FB

View File

@ -89,6 +89,46 @@ class WitnessWorld(World):
'entity_to_name': StaticWitnessLogic.ENTITY_ID_TO_NAME,
}
def determine_sufficient_progression(self):
"""
Determine whether there are enough progression items in this world to consider it "interactive".
In the case of singleplayer, this just outputs a warning.
In the case of multiplayer, the requirements are a bit stricter and an Exception is raised.
"""
# A note on Obelisk Keys:
# Obelisk Keys are never relevant in singleplayer, because the locations they lock are irrelevant to in-game
# progress and irrelevant to all victory conditions. Thus, I consider them "fake progression" for singleplayer.
# However, those locations could obviously contain big items needed for other players, so I consider
# "Obelisk Keys only" valid for multiworld.
# A note on Laser Shuffle:
# In singleplayer, I don't mind "Ice Rod Hunt" type gameplay, so "laser shuffle only" is valid.
# However, I do not want to allow "Ice Rod Hunt" style gameplay in multiworld, so "laser shuffle only" is
# not considered interactive enough for multiworld.
interacts_sufficiently_with_multiworld = (
self.options.shuffle_symbols
or self.options.shuffle_doors
or self.options.obelisk_keys and self.options.shuffle_EPs
)
has_locally_relevant_progression = (
self.options.shuffle_symbols
or self.options.shuffle_doors
or self.options.shuffle_lasers
or self.options.shuffle_boat
or self.options.early_caves == "add_to_pool" and self.options.victory_condition == "challenge"
)
if not has_locally_relevant_progression and self.multiworld.players == 1:
warning(f"{self.multiworld.get_player_name(self.player)}'s Witness world doesn't have any progression"
f" items. Please turn on Symbol Shuffle, Door Shuffle or Laser Shuffle if that doesn't seem right.")
elif not interacts_sufficiently_with_multiworld and self.multiworld.players > 1:
raise Exception(f"{self.multiworld.get_player_name(self.player)}'s Witness world doesn't have enough"
f" progression items that can be placed in other players' worlds. Please turn on Symbol"
f" Shuffle, Door Shuffle or Obelisk Keys.")
def generate_early(self):
disabled_locations = self.options.exclude_locations.value
@ -102,26 +142,9 @@ class WitnessWorld(World):
)
self.regio: WitnessRegions = WitnessRegions(self.locat, self)
interacts_with_multiworld = (
self.options.shuffle_symbols or
self.options.shuffle_doors or
self.options.shuffle_lasers == "anywhere"
)
self.log_ids_to_hints = dict()
has_progression = (
interacts_with_multiworld
or self.options.shuffle_lasers == "local"
or self.options.shuffle_boat
or self.options.early_caves == "add_to_pool"
)
if not has_progression and self.multiworld.players == 1:
warning(f"{self.multiworld.get_player_name(self.player)}'s Witness world doesn't have any progression"
f" items. Please turn on Symbol Shuffle, Door Shuffle or Laser Shuffle if that doesn't seem right.")
elif not interacts_with_multiworld and self.multiworld.players > 1:
raise Exception(f"{self.multiworld.get_player_name(self.player)}'s Witness world doesn't have enough"
f" progression items that can be placed in other players' worlds. Please turn on Symbol"
f" Shuffle, Door Shuffle or non-local Laser Shuffle.")
self.determine_sufficient_progression()
if self.options.shuffle_lasers == "local":
self.options.local_items.value |= self.item_name_groups["Lasers"]

View File

@ -120,6 +120,14 @@ class EnvironmentalPuzzlesDifficulty(Choice):
option_eclipse = 2
class ObeliskKeys(DefaultOnToggle):
"""
Add one Obelisk Key item per Obelisk, locking you out of solving any of the associated Environmental Puzzles.
Does nothing if "Shuffle Environmental Puzzles" is set to "off".
"""
display_name = "Obelisk Keys"
class ShufflePostgame(Toggle):
"""Adds locations into the pool that are guaranteed to become accessible after or at the same time as your goal.
Use this if you don't play with release on victory. IMPORTANT NOTE: The possibility of your second
@ -263,6 +271,7 @@ class TheWitnessOptions(PerGameCommonOptions):
disable_non_randomized_puzzles: DisableNonRandomizedPuzzles
shuffle_discarded_panels: ShuffleDiscardedPanels
shuffle_vault_boxes: ShuffleVaultBoxes
obelisk_keys: ObeliskKeys
shuffle_EPs: ShuffleEnvironmentalPuzzles
EP_difficulty: EnvironmentalPuzzlesDifficulty
shuffle_postgame: ShufflePostgame

View File

@ -63,26 +63,30 @@ class WitnessPlayerLogic:
if panel_hex in self.DOOR_ITEMS_BY_ID:
door_items = frozenset({frozenset([item]) for item in self.DOOR_ITEMS_BY_ID[panel_hex]})
all_options = set()
all_options: Set[FrozenSet[str]] = set()
for dependentItem in door_items:
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(dependentItem)
for items_option in these_items:
all_options.add(items_option.union(dependentItem))
# 0x28A0D depends on another entity for *non-power* reasons -> This dependency needs to be preserved,
# except in Expert, where that dependency doesn't exist, but now there *is* a power dependency.
# In the future, it would be wise to make a distinction between "power dependencies" and other dependencies.
if panel_hex == "0x28A0D" and not any("0x28998" in option for option in these_panels):
these_items = all_options
# If this entity is not an EP, and it has an associated door item, ignore the original power dependencies
if StaticWitnessLogic.ENTITIES_BY_HEX[panel_hex]["entityType"] != "EP":
# 0x28A0D depends on another entity for *non-power* reasons -> This dependency needs to be preserved,
# except in Expert, where that dependency doesn't exist, but now there *is* a power dependency.
# In the future, it'd be wise to make a distinction between "power dependencies" and other dependencies.
if panel_hex == "0x28A0D" and not any("0x28998" in option for option in these_panels):
these_items = all_options
# Another dependency that is not power-based: The Symmetry Island Upper Panel latches
elif panel_hex == "0x1C349":
these_items = all_options
# Another dependency that is not power-based: The Symmetry Island Upper Panel latches
elif panel_hex == "0x1C349":
these_items = all_options
else:
return frozenset(all_options)
# For any other door entity, we just return a set with the item that opens it & disregard power dependencies
else:
return frozenset(all_options)
these_items = all_options
disabled_eps = {eHex for eHex in self.COMPLETELY_DISABLED_ENTITIES
if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[eHex]["entityType"] == "EP"}
@ -429,6 +433,9 @@ class WitnessPlayerLogic:
if lasers:
adjustment_linesets_in_order.append(get_laser_shuffle())
if world.options.shuffle_EPs and world.options.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()
if ep_obj["entityType"] == "EP")
@ -442,7 +449,7 @@ class WitnessPlayerLogic:
adjustment_linesets_in_order.append(["Disabled Locations:"] + get_ep_obelisks()[1:])
if not world.options.shuffle_EPs:
adjustment_linesets_in_order.append(["Irrelevant Locations:"] + 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:

View File

@ -14,6 +14,7 @@ witness_option_presets: Dict[str, Dict[str, Any]] = {
"door_groupings": DoorGroupings.option_off,
"shuffle_boat": True,
"shuffle_lasers": ShuffleLasers.option_local,
"obelisk_keys": ObeliskKeys.option_false,
"disable_non_randomized_puzzles": True,
"shuffle_discarded_panels": False,
@ -35,6 +36,7 @@ witness_option_presets: Dict[str, Dict[str, Any]] = {
"area_hint_percentage": AreaHintPercentage.default,
"laser_hints": LaserHints.default,
"death_link": DeathLink.default,
"death_link_amnesty": DeathLinkAmnesty.default,
},
# For relative beginners who want to move to the next step.
@ -48,6 +50,7 @@ witness_option_presets: Dict[str, Dict[str, Any]] = {
"door_groupings": DoorGroupings.option_regional,
"shuffle_boat": True,
"shuffle_lasers": ShuffleLasers.option_off,
"obelisk_keys": ObeliskKeys.option_false,
"disable_non_randomized_puzzles": False,
"shuffle_discarded_panels": True,
@ -69,6 +72,7 @@ witness_option_presets: Dict[str, Dict[str, Any]] = {
"area_hint_percentage": AreaHintPercentage.default,
"laser_hints": LaserHints.default,
"death_link": DeathLink.default,
"death_link_amnesty": DeathLinkAmnesty.default,
},
# Allsanity but without the BS (no expert, no tedious EPs).
@ -82,6 +86,7 @@ witness_option_presets: Dict[str, Dict[str, Any]] = {
"door_groupings": DoorGroupings.option_off,
"shuffle_boat": True,
"shuffle_lasers": ShuffleLasers.option_anywhere,
"obelisk_keys": ObeliskKeys.option_true,
"disable_non_randomized_puzzles": False,
"shuffle_discarded_panels": True,
@ -103,5 +108,6 @@ witness_option_presets: Dict[str, Dict[str, Any]] = {
"area_hint_percentage": AreaHintPercentage.default,
"laser_hints": LaserHints.default,
"death_link": DeathLink.default,
"death_link_amnesty": DeathLinkAmnesty.default,
},
}

View File

@ -0,0 +1,7 @@
Items:
Desert Obelisk Key
Monastery Obelisk Key
Treehouse Obelisk Key
Mountainside Obelisk Key
Quarry Obelisk Key
Town Obelisk Key

View File

@ -177,6 +177,10 @@ def get_ep_obelisks() -> List[str]:
return get_adjustment_file("settings/EP_Shuffle/EP_Sides.txt")
def get_obelisk_keys() -> List[str]:
return get_adjustment_file("settings/Door_Shuffle/Obelisk_Keys.txt")
def get_ep_easy() -> List[str]:
return get_adjustment_file("settings/EP_Shuffle/EP_Easy.txt")