diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index 471d030d..3bf3661b 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -5,7 +5,7 @@ import dataclasses from logging import error, warning from typing import Any, Dict, List, Optional, cast -from BaseClasses import CollectionState, Entrance, Location, Region, Tutorial +from BaseClasses import CollectionState, Entrance, Location, LocationProgressType, Region, Tutorial from Options import OptionError, PerGameCommonOptions, Toggle from worlds.AutoWorld import WebWorld, World @@ -380,6 +380,10 @@ class WitnessWorld(World): if isinstance(item_name, dict): item_name = next(iter(item_name)) + # Easter Egg events with arbitrary sizes + if item_name.startswith("+") and "Easter Egg" in item_name: + return WitnessItem.make_egg_event(item_name, self.player) + # this conditional is purely for unit tests, which need to be able to create an item before generate_early item_data: ItemData if hasattr(self, "player_items") and self.player_items and item_name in self.player_items.item_data: @@ -389,6 +393,18 @@ class WitnessWorld(World): return WitnessItem(item_name, item_data.classification, item_data.ap_code, player=self.player) + def collect(self, state: "CollectionState", item: WitnessItem) -> bool: + changed = super().collect(state, item) + if changed and item.eggs: + state.prog_items[self.player]["Egg"] += item.eggs + return changed + + def remove(self, state: "CollectionState", item: WitnessItem) -> bool: + changed = super().remove(state, item) + if changed and item.eggs: + state.prog_items[self.player]["Egg"] -= item.eggs + return changed + def get_filler_item_name(self) -> str: return "Speed Boost" @@ -398,11 +414,9 @@ class WitnessLocation(Location): Archipelago Location for The Witness """ game: str = "The Witness" - entity_hex: int = -1 - def __init__(self, player: int, name: str, address: Optional[int], parent: Region, ch_hex: int = -1) -> None: + def __init__(self, player: int, name: str, address: Optional[int], parent: Region) -> None: super().__init__(player, name, address, parent) - self.entity_hex = ch_hex def create_region(world: WitnessWorld, name: str, player_locations: WitnessPlayerLocations, @@ -416,14 +430,13 @@ def create_region(world: WitnessWorld, name: str, player_locations: WitnessPlaye for location in region_locations: loc_id = player_locations.CHECK_LOCATION_TABLE[location] - entity_hex = -1 + location_obj = WitnessLocation(world.player, location, loc_id, ret) + if location in static_witness_logic.ENTITIES_BY_NAME: - entity_hex = int( - static_witness_logic.ENTITIES_BY_NAME[location]["entity_hex"], 0 - ) - location_obj = WitnessLocation( - world.player, location, loc_id, ret, entity_hex - ) + entity_hex = static_witness_logic.ENTITIES_BY_NAME[location]["entity_hex"] + + if entity_hex in world.player_logic.EXCLUDED_ENTITIES: + location_obj.progress_type = LocationProgressType.EXCLUDED ret.locations.append(location_obj) if exits: diff --git a/worlds/witness/data/WitnessLogic.txt b/worlds/witness/data/WitnessLogic.txt index 0dbb88a1..edc45222 100644 --- a/worlds/witness/data/WitnessLogic.txt +++ b/worlds/witness/data/WitnessLogic.txt @@ -206,7 +206,7 @@ Door - 0x0A24B (Flood Room Entry) - 0x0A249 159043 - 0x0A14C (Pond Room Near Reflection EP) - True - True 159044 - 0x0A14D (Pond Room Far Reflection EP) - True - True -Desert Flood Room (Desert) - Desert Elevator Room - 0x0C316: +Desert Flood Room (Desert) - Desert Elevator Room - 0x0C316 - Desert Flood Room Underwater - 0x1C260: 158097 - 0x1C2DF (Reduce Water Level Far Left) - True - True 158098 - 0x1831E (Reduce Water Level Far Right) - True - True 158099 - 0x1C260 (Reduce Water Level Near Left) - True - True @@ -224,6 +224,8 @@ Desert Flood Room (Desert) - Desert Elevator Room - 0x0C316: Door - 0x0C316 (Elevator Room Entry) - 0x18076 159034 - 0x337F8 (Flood Room EP) - 0x1C2DF - True +Desert Flood Room Underwater (Desert): + Desert Elevator Room (Desert) - Desert Behind Elevator - 0x01317: 158111 - 0x17C31 (Elevator Room Transparent) - True - True 158113 - 0x012D7 (Elevator Room Hexagonal) - 0x17C31 & 0x0A015 - True @@ -501,7 +503,7 @@ Laser - 0x17C65 (Laser) - 0x17CA4 159121 - 0x03BE3 (Garden Right EP) - True - True 159122 - 0x0A409 (Wall EP) - True - True -Inside Monastery (Monastery): +Inside Monastery (Monastery) - Monastery North Shutters - 0x09D9B: 158213 - 0x09D9B (Shutters Control) - True - Dots 158214 - 0x193A7 (Inside 1) - 0x00037 - True 158215 - 0x193AA (Inside 2) - 0x193A7 - True @@ -513,6 +515,8 @@ Inside Monastery (Monastery): Monastery Garden (Monastery): +Monastery North Shutters (Monastery): + ==Town== Town Obelisk (Town) - Entry - True: @@ -637,9 +641,13 @@ Door - 0x3CCDF (Exit Right) - 0x33AB2 159556 - 0x33A2A (Door EP) - 0x03553 - True 159558 - 0x33B06 (Church EP) - 0x0354E - True +==Southern Peninsula== + +Southern Peninsula (Southern Peninsula) - Main Island - True: + ==Jungle== -Jungle (Jungle) - Main Island - True - The Ocean - 0x17CDF: +Jungle (Jungle) - Main Island - True - The Ocean - 0x17CDF - Jungle Under Popup Wall - 0x1475B: 158251 - 0x17CDF (Shore Boat Spawn) - True - Boat 158609 - 0x17F9B (Discard) - True - Triangles 158252 - 0x002C4 (First Row 1) - True - True @@ -670,6 +678,8 @@ Door - 0x3873B (Laser Shortcut) - 0x337FA 159350 - 0x035CB (Bamboo CCW EP) - True - True 159351 - 0x035CF (Bamboo CW EP) - True - True +Jungle Under Popup Wall (Jungle): + Outside Jungle River (Jungle) - Main Island - True - Monastery Garden - 0x0CF2A - Jungle Vault - 0x15287: 158267 - 0x17CAA (Monastery Garden Shortcut Panel) - True - True Door - 0x0CF2A (Monastery Garden Shortcut) - 0x17CAA @@ -712,9 +722,11 @@ Bunker Ultraviolet Room (Bunker) - Bunker Elevator Section - 0x0A08D: 158285 - 0x17E67 (UV Room 2) - 0x17E63 & 0x34BC6 - Colored Squares & Black/White Squares Door - 0x0A08D (Elevator Room Entry) - 0x17E67 -Bunker Elevator Section (Bunker) - Bunker Elevator - TrueOneWay: +Bunker Elevator Section (Bunker) - Bunker Elevator - TrueOneWay - Bunker Under Elevator - 0x0A079 | Bunker Green Room | Bunker Cyan Room | Bunker Laser Platform: 159311 - 0x035F5 (Tinted Door EP) - 0x17C79 - True +Bunker Under Elevator (Bunker): + Bunker Elevator (Bunker) - Bunker Elevator Section - 0x0A079 - Bunker Cyan Room - 0x0A079 - Bunker Green Room - 0x0A079 - Bunker Laser Platform - 0x0A079 - Outside Bunker - 0x0A079: 158286 - 0x0A079 (Elevator Control) - True - Colored Squares & Black/White Squares @@ -1005,7 +1017,7 @@ Mountaintop (Mountaintop) - Mountain Floor 1 - 0x17C34: Mountain Floor 1 (Mountain Floor 1) - Mountain Floor 1 Bridge - 0x09E39: 158408 - 0x09E39 (Light Bridge Controller) - True - Black/White Squares & Colored Squares & Eraser -Mountain Floor 1 Bridge (Mountain Floor 1) - Mountain Floor 1 At Door - TrueOneWay: +Mountain Floor 1 Bridge (Mountain Floor 1) - Mountain Floor 1 At Door - TrueOneWay - Mountain Floor 1 Trash Pillar - TrueOneWay - Mountain Floor 1 Back Section - TrueOneWay: 158409 - 0x09E7A (Right Row 1) - True - Black/White Squares & Dots 158410 - 0x09E71 (Right Row 2) - 0x09E7A - Black/White Squares & Dots 158411 - 0x09E72 (Right Row 3) - 0x09E71 - Black/White Squares & Shapers & Dots @@ -1018,11 +1030,15 @@ Mountain Floor 1 Bridge (Mountain Floor 1) - Mountain Floor 1 At Door - TrueOneW 158418 - 0x09E6C (Left Row 5) - 0x09E79 - Stars & Black/White Squares & Stars + Same Colored Symbol 158419 - 0x09E6F (Left Row 6) - 0x09E6C - Stars & Rotated Shapers & Shapers 158420 - 0x09E6B (Left Row 7) - 0x09E6F - Stars & Dots +158424 - 0x09EAD (Trash Pillar 1) - True - Black/White Squares & Shapers +158425 - 0x09EAF (Trash Pillar 2) - 0x09EAD - Black/White Squares & Shaper + +Mountain Floor 1 Trash Pillar (Mountain Floor 1): + +Mountain Floor 1 Back Section (Mountain Floor 1): 158421 - 0x33AF5 (Back Row 1) - True - Black/White Squares & Symmetry 158422 - 0x33AF7 (Back Row 2) - 0x33AF5 - Black/White Squares & Stars 158423 - 0x09F6E (Back Row 3) - 0x33AF7 - Symmetry & Dots -158424 - 0x09EAD (Trash Pillar 1) - True - Black/White Squares & Shapers -158425 - 0x09EAF (Trash Pillar 2) - 0x09EAD - Black/White Squares & Shapers Mountain Floor 1 At Door (Mountain Floor 1) - Mountain Floor 2 - 0x09E54: Door - 0x09E54 (Exit) - 0x09EAF & 0x09F6E & 0x09E6B & 0x09E7B @@ -1098,14 +1114,16 @@ Elevator (Mountain Bottom Floor): Mountain Pink Bridge EP (Mountain Floor 2): 159312 - 0x09D63 (Pink Bridge EP) - 0x09E39 - True -Mountain Path to Caves (Mountain Bottom Floor) - Caves - 0x2D77D: +Mountain Path to Caves (Mountain Bottom Floor) - Caves - 0x2D77D - Caves Entry Door - TrueOneWay: 158447 - 0x00FF8 (Caves Entry Panel) - True - Triangles & Black/White Squares Door - 0x2D77D (Caves Entry) - 0x00FF8 158448 - 0x334E1 (Rock Control) - True - True ==Caves== -Caves (Caves) - Main Island - 0x2D73F | 0x2D859 - Caves Path to Challenge - 0x019A5: +Caves Entry Door (Caves): + +Caves (Caves) - Main Island - 0x2D73F | 0x2D859 - Caves Path to Challenge - 0x019A5 - Caves Entry Door - TrueOneWay: 158451 - 0x335AB (Elevator Inside Control) - True - Dots & Black/White Squares 158452 - 0x335AC (Elevator Upper Outside Control) - 0x335AB - Black/White Squares 158453 - 0x3369D (Elevator Lower Outside Control) - 0x335AB - Black/White Squares & Dots @@ -1219,3 +1237,7 @@ The Ocean (Boat) - Main Island - TrueOneWay - Swamp Near Boat - TrueOneWay - Tre 159521 - 0x33879 (Tutorial Reflection EP) - True - True 159522 - 0x03C19 (Tutorial Moss EP) - True - True 159531 - 0x035C9 (Cargo Box EP) - 0x0A0C9 - True + +==Easter Eggs== + +Easter Eggs (Easter Eggs) - Entry - True: diff --git a/worlds/witness/data/WitnessLogicExpert.txt b/worlds/witness/data/WitnessLogicExpert.txt index 0f601724..23521ddd 100644 --- a/worlds/witness/data/WitnessLogicExpert.txt +++ b/worlds/witness/data/WitnessLogicExpert.txt @@ -206,7 +206,7 @@ Door - 0x0A24B (Flood Room Entry) - 0x0A249 159043 - 0x0A14C (Pond Room Near Reflection EP) - True - True 159044 - 0x0A14D (Pond Room Far Reflection EP) - True - True -Desert Flood Room (Desert) - Desert Elevator Room - 0x0C316: +Desert Flood Room (Desert) - Desert Elevator Room - 0x0C316 - Desert Flood Room Underwater - 0x1C260: 158097 - 0x1C2DF (Reduce Water Level Far Left) - True - True 158098 - 0x1831E (Reduce Water Level Far Right) - True - True 158099 - 0x1C260 (Reduce Water Level Near Left) - True - True @@ -224,6 +224,8 @@ Desert Flood Room (Desert) - Desert Elevator Room - 0x0C316: Door - 0x0C316 (Elevator Room Entry) - 0x18076 159034 - 0x337F8 (Flood Room EP) - 0x1C2DF - True +Desert Flood Room Underwater (Desert): + Desert Elevator Room (Desert) - Desert Behind Elevator - 0x01317: 158111 - 0x17C31 (Elevator Room Transparent) - True - True 158113 - 0x012D7 (Elevator Room Hexagonal) - 0x17C31 & 0x0A015 - True @@ -501,7 +503,7 @@ Laser - 0x17C65 (Laser) - 0x17CA4 159121 - 0x03BE3 (Garden Right EP) - True - True 159122 - 0x0A409 (Wall EP) - True - True -Inside Monastery (Monastery): +Inside Monastery (Monastery) - Monastery North Shutters - 0x09D9B: 158213 - 0x09D9B (Shutters Control) - True - Dots 158214 - 0x193A7 (Inside 1) - 0x00037 - True 158215 - 0x193AA (Inside 2) - 0x193A7 - True @@ -513,6 +515,8 @@ Inside Monastery (Monastery): Monastery Garden (Monastery): +Monastery North Shutters (Monastery): + ==Town== Town Obelisk (Town) - Entry - True: @@ -637,9 +641,13 @@ Door - 0x3CCDF (Exit Right) - 0x33AB2 159556 - 0x33A2A (Door EP) - 0x03553 - True 159558 - 0x33B06 (Church EP) - 0x0354E - True +==Southern Peninsula== + +Southern Peninsula (Southern Peninsula) - Main Island - True: + ==Jungle== -Jungle (Jungle) - Main Island - True - The Ocean - 0x17CDF: +Jungle (Jungle) - Main Island - True - The Ocean - 0x17CDF - Jungle Under Popup Wall - 0x1475B: 158251 - 0x17CDF (Shore Boat Spawn) - True - Boat 158609 - 0x17F9B (Discard) - True - Arrows 158252 - 0x002C4 (First Row 1) - True - True @@ -670,6 +678,8 @@ Door - 0x3873B (Laser Shortcut) - 0x337FA 159350 - 0x035CB (Bamboo CCW EP) - True - True 159351 - 0x035CF (Bamboo CW EP) - True - True +Jungle Under Popup Wall (Jungle): + Outside Jungle River (Jungle) - Main Island - True - Monastery Garden - 0x0CF2A - Jungle Vault - 0x15287: 158267 - 0x17CAA (Monastery Garden Shortcut Panel) - True - True Door - 0x0CF2A (Monastery Garden Shortcut) - 0x17CAA @@ -712,9 +722,11 @@ Bunker Ultraviolet Room (Bunker) - Bunker Elevator Section - 0x0A08D: 158285 - 0x17E67 (UV Room 2) - 0x17E63 & 0x34BC6 - Squares & Colored Squares & Black/White Squares Door - 0x0A08D (Elevator Room Entry) - 0x17E67 -Bunker Elevator Section (Bunker) - Bunker Elevator - TrueOneWay: +Bunker Elevator Section (Bunker) - Bunker Elevator - TrueOneWay - Bunker Under Elevator - 0x0A079 | Bunker Green Room | Bunker Cyan Room | Bunker Laser Platform: 159311 - 0x035F5 (Tinted Door EP) - 0x17C79 - True +Bunker Under Elevator (Bunker): + Bunker Elevator (Bunker) - Bunker Elevator Section - 0x0A079 - Bunker Cyan Room - 0x0A079 - Bunker Green Room - 0x0A079 - Bunker Laser Platform - 0x0A079 - Outside Bunker - 0x0A079: 158286 - 0x0A079 (Elevator Control) - True - Colored Squares & Black/White Squares @@ -1005,7 +1017,7 @@ Mountaintop (Mountaintop) - Mountain Floor 1 - 0x17C34: Mountain Floor 1 (Mountain Floor 1) - Mountain Floor 1 Bridge - 0x09E39: 158408 - 0x09E39 (Light Bridge Controller) - True - Eraser & Triangles -Mountain Floor 1 Bridge (Mountain Floor 1) - Mountain Floor 1 At Door - TrueOneWay: +Mountain Floor 1 Bridge (Mountain Floor 1) - Mountain Floor 1 At Door - TrueOneWay - Mountain Floor 1 Trash Pillar - TrueOneWay - Mountain Floor 1 Back Section - TrueOneWay: 158409 - 0x09E7A (Right Row 1) - True - Black/White Squares & Dots & Stars & Stars + Same Colored Symbol 158410 - 0x09E71 (Right Row 2) - 0x09E7A - Black/White Squares & Triangles 158411 - 0x09E72 (Right Row 3) - 0x09E71 - Black/White Squares & Shapers & Stars & Stars + Same Colored Symbol @@ -1018,11 +1030,15 @@ Mountain Floor 1 Bridge (Mountain Floor 1) - Mountain Floor 1 At Door - TrueOneW 158418 - 0x09E6C (Left Row 5) - 0x09E79 - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol 158419 - 0x09E6F (Left Row 6) - 0x09E6C - Symmetry & Stars & Colored Squares & Black/White Squares & Stars + Same Colored Symbol & Symmetry & Eraser 158420 - 0x09E6B (Left Row 7) - 0x09E6F - Symmetry & Dots & Full Dots & Triangles +158424 - 0x09EAD (Trash Pillar 1) - True - Rotated Shapers & Stars +158425 - 0x09EAF (Trash Pillar 2) - 0x09EAD - Rotated Shapers & Triangles + +Mountain Floor 1 Trash Pillar (Mountain Floor 1): + +Mountain Floor 1 Back Section (Mountain Floor 1): 158421 - 0x33AF5 (Back Row 1) - True - Symmetry & Black/White Squares & Triangles 158422 - 0x33AF7 (Back Row 2) - 0x33AF5 - Symmetry & Stars & Triangles & Stars + Same Colored Symbol 158423 - 0x09F6E (Back Row 3) - 0x33AF7 - Symmetry & Stars & Shapers & Stars + Same Colored Symbol -158424 - 0x09EAD (Trash Pillar 1) - True - Rotated Shapers & Stars -158425 - 0x09EAF (Trash Pillar 2) - 0x09EAD - Rotated Shapers & Triangles Mountain Floor 1 At Door (Mountain Floor 1) - Mountain Floor 2 - 0x09E54: Door - 0x09E54 (Exit) - 0x09EAF & 0x09F6E & 0x09E6B & 0x09E7B @@ -1098,14 +1114,16 @@ Elevator (Mountain Bottom Floor): Mountain Pink Bridge EP (Mountain Floor 2): 159312 - 0x09D63 (Pink Bridge EP) - 0x09E39 - True -Mountain Path to Caves (Mountain Bottom Floor) - Caves - 0x2D77D: +Mountain Path to Caves (Mountain Bottom Floor) - Caves - 0x2D77D - Caves Entry Door - TrueOneWay: 158447 - 0x00FF8 (Caves Entry Panel) - True - Arrows & Black/White Squares Door - 0x2D77D (Caves Entry) - 0x00FF8 158448 - 0x334E1 (Rock Control) - True - True ==Caves== -Caves (Caves) - Main Island - 0x2D73F | 0x2D859 - Caves Path to Challenge - 0x019A5: +Caves Entry Door (Caves): + +Caves (Caves) - Main Island - 0x2D73F | 0x2D859 - Caves Path to Challenge - 0x019A5 - Caves Entry Door - TrueOneWay: 158451 - 0x335AB (Elevator Inside Control) - True - Dots & Squares & Black/White Squares 158452 - 0x335AC (Elevator Upper Outside Control) - 0x335AB - Squares & Black/White Squares 158453 - 0x3369D (Elevator Lower Outside Control) - 0x335AB - Squares & Black/White Squares & Dots @@ -1219,3 +1237,7 @@ The Ocean (Boat) - Main Island - TrueOneWay - Swamp Near Boat - TrueOneWay - Tre 159521 - 0x33879 (Tutorial Reflection EP) - True - True 159522 - 0x03C19 (Tutorial Moss EP) - True - True 159531 - 0x035C9 (Cargo Box EP) - 0x0A0C9 - True + +==Easter Eggs== + +Easter Eggs (Easter Eggs) - Entry - True: diff --git a/worlds/witness/data/WitnessLogicVanilla.txt b/worlds/witness/data/WitnessLogicVanilla.txt index f0c6a869..a967a12e 100644 --- a/worlds/witness/data/WitnessLogicVanilla.txt +++ b/worlds/witness/data/WitnessLogicVanilla.txt @@ -206,7 +206,7 @@ Door - 0x0A24B (Flood Room Entry) - 0x0A249 159043 - 0x0A14C (Pond Room Near Reflection EP) - True - True 159044 - 0x0A14D (Pond Room Far Reflection EP) - True - True -Desert Flood Room (Desert) - Desert Elevator Room - 0x0C316: +Desert Flood Room (Desert) - Desert Elevator Room - 0x0C316 - Desert Flood Room Underwater - 0x1C260: 158097 - 0x1C2DF (Reduce Water Level Far Left) - True - True 158098 - 0x1831E (Reduce Water Level Far Right) - True - True 158099 - 0x1C260 (Reduce Water Level Near Left) - True - True @@ -224,6 +224,8 @@ Desert Flood Room (Desert) - Desert Elevator Room - 0x0C316: Door - 0x0C316 (Elevator Room Entry) - 0x18076 159034 - 0x337F8 (Flood Room EP) - 0x1C2DF - True +Desert Flood Room Underwater (Desert): + Desert Elevator Room (Desert) - Desert Behind Elevator - 0x01317: 158111 - 0x17C31 (Elevator Room Transparent) - True - True 158113 - 0x012D7 (Elevator Room Hexagonal) - 0x17C31 & 0x0A015 - True @@ -501,7 +503,7 @@ Laser - 0x17C65 (Laser) - 0x17CA4 159121 - 0x03BE3 (Garden Right EP) - True - True 159122 - 0x0A409 (Wall EP) - True - True -Inside Monastery (Monastery): +Inside Monastery (Monastery) - Monastery North Shutters - 0x09D9B: 158213 - 0x09D9B (Shutters Control) - True - Dots 158214 - 0x193A7 (Inside 1) - 0x00037 - True 158215 - 0x193AA (Inside 2) - 0x193A7 - True @@ -513,6 +515,8 @@ Inside Monastery (Monastery): Monastery Garden (Monastery): +Monastery North Shutters (Monastery): + ==Town== Town Obelisk (Town) - Entry - True: @@ -637,9 +641,13 @@ Door - 0x3CCDF (Exit Right) - 0x33AB2 159556 - 0x33A2A (Door EP) - 0x03553 - True 159558 - 0x33B06 (Church EP) - 0x0354E - True +==Southern Peninsula== + +Southern Peninsula (Southern Peninsula) - Main Island - True: + ==Jungle== -Jungle (Jungle) - Main Island - True - The Ocean - 0x17CDF: +Jungle (Jungle) - Main Island - True - The Ocean - 0x17CDF - Jungle Under Popup Wall - 0x1475B: 158251 - 0x17CDF (Shore Boat Spawn) - True - Boat 158609 - 0x17F9B (Discard) - True - Triangles 158252 - 0x002C4 (First Row 1) - True - True @@ -670,6 +678,8 @@ Door - 0x3873B (Laser Shortcut) - 0x337FA 159350 - 0x035CB (Bamboo CCW EP) - True - True 159351 - 0x035CF (Bamboo CW EP) - True - True +Jungle Under Popup Wall (Jungle): + Outside Jungle River (Jungle) - Main Island - True - Monastery Garden - 0x0CF2A - Jungle Vault - 0x15287: 158267 - 0x17CAA (Monastery Garden Shortcut Panel) - True - True Door - 0x0CF2A (Monastery Garden Shortcut) - 0x17CAA @@ -712,9 +722,11 @@ Bunker Ultraviolet Room (Bunker) - Bunker Elevator Section - 0x0A08D: 158285 - 0x17E67 (UV Room 2) - 0x17E63 & 0x34BC6 - Colored Squares & Black/White Squares Door - 0x0A08D (Elevator Room Entry) - 0x17E67 -Bunker Elevator Section (Bunker) - Bunker Elevator - TrueOneWay: +Bunker Elevator Section (Bunker) - Bunker Elevator - TrueOneWay - Bunker Under Elevator - 0x0A079 | Bunker Green Room | Bunker Cyan Room | Bunker Laser Platform: 159311 - 0x035F5 (Tinted Door EP) - 0x17C79 - True +Bunker Under Elevator (Bunker): + Bunker Elevator (Bunker) - Bunker Elevator Section - 0x0A079 - Bunker Cyan Room - 0x0A079 - Bunker Green Room - 0x0A079 - Bunker Laser Platform - 0x0A079 - Outside Bunker - 0x0A079: 158286 - 0x0A079 (Elevator Control) - True - Colored Squares & Black/White Squares @@ -1005,7 +1017,7 @@ Mountaintop (Mountaintop) - Mountain Floor 1 - 0x17C34: Mountain Floor 1 (Mountain Floor 1) - Mountain Floor 1 Bridge - 0x09E39: 158408 - 0x09E39 (Light Bridge Controller) - True - Black/White Squares & Rotated Shapers -Mountain Floor 1 Bridge (Mountain Floor 1) - Mountain Floor 1 At Door - TrueOneWay: +Mountain Floor 1 Bridge (Mountain Floor 1) - Mountain Floor 1 At Door - TrueOneWay - Mountain Floor 1 Trash Pillar - TrueOneWay - Mountain Floor 1 Back Section - TrueOneWay: 158409 - 0x09E7A (Right Row 1) - True - Black/White Squares & Dots 158410 - 0x09E71 (Right Row 2) - 0x09E7A - Black/White Squares & Dots 158411 - 0x09E72 (Right Row 3) - 0x09E71 - Black/White Squares & Shapers @@ -1018,11 +1030,15 @@ Mountain Floor 1 Bridge (Mountain Floor 1) - Mountain Floor 1 At Door - TrueOneW 158418 - 0x09E6C (Left Row 5) - 0x09E79 - Stars & Black/White Squares 158419 - 0x09E6F (Left Row 6) - 0x09E6C - Shapers & Dots 158420 - 0x09E6B (Left Row 7) - 0x09E6F - Dots +158424 - 0x09EAD (Trash Pillar 1) - True - Black/White Squares & Shapers +158425 - 0x09EAF (Trash Pillar 2) - 0x09EAD - Black/White Squares & Shapers + +Mountain Floor 1 Trash Pillar (Mountain Floor 1): + +Mountain Floor 1 Back Section (Mountain Floor 1): 158421 - 0x33AF5 (Back Row 1) - True - Black/White Squares & Symmetry 158422 - 0x33AF7 (Back Row 2) - 0x33AF5 - Black/White Squares 158423 - 0x09F6E (Back Row 3) - 0x33AF7 - Symmetry & Dots -158424 - 0x09EAD (Trash Pillar 1) - True - Black/White Squares & Shapers -158425 - 0x09EAF (Trash Pillar 2) - 0x09EAD - Black/White Squares & Shapers Mountain Floor 1 At Door (Mountain Floor 1) - Mountain Floor 2 - 0x09E54: Door - 0x09E54 (Exit) - 0x09EAF & 0x09F6E & 0x09E6B & 0x09E7B @@ -1098,14 +1114,16 @@ Elevator (Mountain Bottom Floor): Mountain Pink Bridge EP (Mountain Floor 2): 159312 - 0x09D63 (Pink Bridge EP) - 0x09E39 - True -Mountain Path to Caves (Mountain Bottom Floor) - Caves - 0x2D77D: +Mountain Path to Caves (Mountain Bottom Floor) - Caves - 0x2D77D - Caves Entry Door - TrueOneWay: 158447 - 0x00FF8 (Caves Entry Panel) - True - Black/White Squares Door - 0x2D77D (Caves Entry) - 0x00FF8 158448 - 0x334E1 (Rock Control) - True - True ==Caves== -Caves (Caves) - Main Island - 0x2D73F | 0x2D859 - Caves Path to Challenge - 0x019A5: +Caves Entry Door (Caves): + +Caves (Caves) - Main Island - 0x2D73F | 0x2D859 - Caves Path to Challenge - 0x019A5 - Caves Entry Door - TrueOneWay: 158451 - 0x335AB (Elevator Inside Control) - True - Dots & Black/White Squares 158452 - 0x335AC (Elevator Upper Outside Control) - 0x335AB - Black/White Squares 158453 - 0x3369D (Elevator Lower Outside Control) - 0x335AB - Black/White Squares & Dots @@ -1219,3 +1237,7 @@ The Ocean (Boat) - Main Island - TrueOneWay - Swamp Near Boat - TrueOneWay - Tre 159521 - 0x33879 (Tutorial Reflection EP) - True - True 159522 - 0x03C19 (Tutorial Moss EP) - True - True 159531 - 0x035C9 (Cargo Box EP) - 0x0A0C9 - True + +==Easter Eggs== + +Easter Eggs (Easter Eggs) - Entry - True: diff --git a/worlds/witness/data/WitnessLogicVariety.txt b/worlds/witness/data/WitnessLogicVariety.txt index b7b705a6..bc9a40f5 100644 --- a/worlds/witness/data/WitnessLogicVariety.txt +++ b/worlds/witness/data/WitnessLogicVariety.txt @@ -206,7 +206,7 @@ Door - 0x0A24B (Flood Room Entry) - 0x0A249 159043 - 0x0A14C (Pond Room Near Reflection EP) - True - True 159044 - 0x0A14D (Pond Room Far Reflection EP) - True - True -Desert Flood Room (Desert) - Desert Elevator Room - 0x0C316: +Desert Flood Room (Desert) - Desert Elevator Room - 0x0C316 - Desert Flood Room Underwater - 0x1C260: 158097 - 0x1C2DF (Reduce Water Level Far Left) - True - True 158098 - 0x1831E (Reduce Water Level Far Right) - True - True 158099 - 0x1C260 (Reduce Water Level Near Left) - True - True @@ -224,6 +224,8 @@ Desert Flood Room (Desert) - Desert Elevator Room - 0x0C316: Door - 0x0C316 (Elevator Room Entry) - 0x18076 159034 - 0x337F8 (Flood Room EP) - 0x1C2DF - True +Desert Flood Room Underwater (Desert): + Desert Elevator Room (Desert) - Desert Behind Elevator - 0x01317: 158111 - 0x17C31 (Elevator Room Transparent) - True - True 158113 - 0x012D7 (Elevator Room Hexagonal) - 0x17C31 & 0x0A015 - True @@ -501,7 +503,7 @@ Laser - 0x17C65 (Laser) - 0x17CA4 159121 - 0x03BE3 (Garden Right EP) - True - True 159122 - 0x0A409 (Wall EP) - True - True -Inside Monastery (Monastery): +Inside Monastery (Monastery) - Monastery North Shutters - 0x09D9B: 158213 - 0x09D9B (Shutters Control) - True - Dots 158214 - 0x193A7 (Inside 1) - 0x00037 - True 158215 - 0x193AA (Inside 2) - 0x193A7 - True @@ -513,6 +515,8 @@ Inside Monastery (Monastery): Monastery Garden (Monastery): +Monastery North Shutters (Monastery): + ==Town== Town Obelisk (Town) - Entry - True: @@ -637,9 +641,13 @@ Door - 0x3CCDF (Exit Right) - 0x33AB2 159556 - 0x33A2A (Door EP) - 0x03553 - True 159558 - 0x33B06 (Church EP) - 0x0354E - True +==Southern Peninsula== + +Southern Peninsula (Southern Peninsula) - Main Island - True: + ==Jungle== -Jungle (Jungle) - Main Island - True - The Ocean - 0x17CDF: +Jungle (Jungle) - Main Island - True - The Ocean - 0x17CDF - Jungle Under Popup Wall - 0x1475B: 158251 - 0x17CDF (Shore Boat Spawn) - True - Boat 158609 - 0x17F9B (Discard) - True - Arrows & Triangles 158252 - 0x002C4 (First Row 1) - True - True @@ -670,6 +678,8 @@ Door - 0x3873B (Laser Shortcut) - 0x337FA 159350 - 0x035CB (Bamboo CCW EP) - True - True 159351 - 0x035CF (Bamboo CW EP) - True - True +Jungle Under Popup Wall (Jungle): + Outside Jungle River (Jungle) - Main Island - True - Monastery Garden - 0x0CF2A - Jungle Vault - 0x15287: 158267 - 0x17CAA (Monastery Garden Shortcut Panel) - True - True Door - 0x0CF2A (Monastery Garden Shortcut) - 0x17CAA @@ -712,9 +722,11 @@ Bunker Ultraviolet Room (Bunker) - Bunker Elevator Section - 0x0A08D: 158285 - 0x17E67 (UV Room 2) - 0x17E63 & 0x34BC6 - Colored Squares & Black/White Squares Door - 0x0A08D (Elevator Room Entry) - 0x17E67 -Bunker Elevator Section (Bunker) - Bunker Elevator - TrueOneWay: +Bunker Elevator Section (Bunker) - Bunker Elevator - TrueOneWay - Bunker Under Elevator - 0x0A079 | Bunker Green Room | Bunker Cyan Room | Bunker Laser Platform: 159311 - 0x035F5 (Tinted Door EP) - 0x17C79 - True +Bunker Under Elevator (Bunker): + Bunker Elevator (Bunker) - Bunker Elevator Section - 0x0A079 - Bunker Cyan Room - 0x0A079 - Bunker Green Room - 0x0A079 - Bunker Laser Platform - 0x0A079 - Outside Bunker - 0x0A079: 158286 - 0x0A079 (Elevator Control) - True - Colored Squares & Black/White Squares @@ -1005,7 +1017,7 @@ Mountaintop (Mountaintop) - Mountain Floor 1 - 0x17C34: Mountain Floor 1 (Mountain Floor 1) - Mountain Floor 1 Bridge - 0x09E39: 158408 - 0x09E39 (Light Bridge Controller) - True - Black/White Squares & Colored Squares & Eraser -Mountain Floor 1 Bridge (Mountain Floor 1) - Mountain Floor 1 At Door - TrueOneWay: +Mountain Floor 1 Bridge (Mountain Floor 1) - Mountain Floor 1 At Door - TrueOneWay - Mountain Floor 1 Trash Pillar - TrueOneWay - Mountain Floor 1 Back Section - TrueOneWay: 158409 - 0x09E7A (Right Row 1) - True - Black/White Squares & Dots 158410 - 0x09E71 (Right Row 2) - 0x09E7A - Black/White Squares & Dots & Stars & Stars + Same Colored Symbol 158411 - 0x09E72 (Right Row 3) - 0x09E71 - Black/White Squares & Shapers & Stars & Stars + Same Colored Symbol @@ -1018,11 +1030,15 @@ Mountain Floor 1 Bridge (Mountain Floor 1) - Mountain Floor 1 At Door - TrueOneW 158418 - 0x09E6C (Left Row 5) - 0x09E79 - Arrows & Black/White Squares & Stars & Stars + Same Colored Symbol 158419 - 0x09E6F (Left Row 6) - 0x09E6C - Arrows & Dots & Full Dots 158420 - 0x09E6B (Left Row 7) - 0x09E6F - Arrows & Dots & Full Dots +158424 - 0x09EAD (Trash Pillar 1) - True - Triangles & Arrows +158425 - 0x09EAF (Trash Pillar 2) - 0x09EAD - Triangles & Arrows + +Mountain Floor 1 Trash Pillar (Mountain Floor 1): + +Mountain Floor 1 Back Section (Mountain Floor 1): 158421 - 0x33AF5 (Back Row 1) - True - Symmetry & Triangles 158422 - 0x33AF7 (Back Row 2) - 0x33AF5 - Triangles 158423 - 0x09F6E (Back Row 3) - 0x33AF7 - Symmetry & Triangles -158424 - 0x09EAD (Trash Pillar 1) - True - Triangles & Arrows -158425 - 0x09EAF (Trash Pillar 2) - 0x09EAD - Triangles & Arrows Mountain Floor 1 At Door (Mountain Floor 1) - Mountain Floor 2 - 0x09E54: Door - 0x09E54 (Exit) - 0x09EAF & 0x09F6E & 0x09E6B & 0x09E7B @@ -1098,14 +1114,16 @@ Elevator (Mountain Bottom Floor): Mountain Pink Bridge EP (Mountain Floor 2): 159312 - 0x09D63 (Pink Bridge EP) - 0x09E39 - True -Mountain Path to Caves (Mountain Bottom Floor) - Caves - 0x2D77D: +Mountain Path to Caves (Mountain Bottom Floor) - Caves - 0x2D77D - Caves Entry Door - TrueOneWay: 158447 - 0x00FF8 (Caves Entry Panel) - True - Black/White Squares & Arrows & Triangles Door - 0x2D77D (Caves Entry) - 0x00FF8 158448 - 0x334E1 (Rock Control) - True - True ==Caves== -Caves (Caves) - Main Island - 0x2D73F | 0x2D859 - Caves Path to Challenge - 0x019A5: +Caves Entry Door (Caves): + +Caves (Caves) - Main Island - 0x2D73F | 0x2D859 - Caves Path to Challenge - 0x019A5 - Caves Entry Door - TrueOneWay: 158451 - 0x335AB (Elevator Inside Control) - True - Dots & Black/White Squares 158452 - 0x335AC (Elevator Upper Outside Control) - 0x335AB - Black/White Squares 158453 - 0x3369D (Elevator Lower Outside Control) - 0x335AB - Black/White Squares & Dots @@ -1219,3 +1237,7 @@ The Ocean (Boat) - Main Island - TrueOneWay - Swamp Near Boat - TrueOneWay - Tre 159521 - 0x33879 (Tutorial Reflection EP) - True - True 159522 - 0x03C19 (Tutorial Moss EP) - True - True 159531 - 0x035C9 (Cargo Box EP) - 0x0A0C9 - True + +==Easter Eggs== + +Easter Eggs (Easter Eggs) - Entry - True: diff --git a/worlds/witness/data/settings/easter_eggs.py b/worlds/witness/data/settings/easter_eggs.py new file mode 100644 index 00000000..248e9427 --- /dev/null +++ b/worlds/witness/data/settings/easter_eggs.py @@ -0,0 +1,75 @@ +MAXIMUM_EASTER_EGG_CHECKS = 50 + +EASTER_EGGS = { + "Tutorial": 1, + "Outside Tutorial": 4, + "Outside Tutorial Path To Outpost": 1, + "Outside Tutorial Outpost": 1, + "Orchard Beyond First Gate": 1, + "Orchard End": 1, + "Inside Glass Factory": 2, + "Symmetry Island Lower": 2, + "Symmetry Island Upper": 1, + "Desert Outside": 6, + "Desert Vault": 1, + "Desert Pond Room": 2, + "Desert Flood Room Underwater": 1, + "Desert Elevator Room": 1, + "Outside Quarry": 2, + "Quarry": 5, + "Quarry Stoneworks Upper Floor": 2, + "Quarry Boathouse": 2, + "Shadows": 2, + "Shadows Ledge": 1, + "Shadows Laser Room": 1, + "Keep": 1, + "Keep 3rd Maze": 3, + "Keep 2nd Pressure Plate": 2, + "Keep 3rd Pressure Plate": 1, + "Keep 4th Pressure Plate": 1, + "Keep Tower": 2, + "Shipwreck": 5, + "Inside Monastery": 1, + "Monastery North Shutters": 1, + "Monastery Garden": 1, + "Town": 5, + "Town Wooden Rooftop": 1, + "Town RGB House": 2, + "Town Tower Top": 1, + "Windmill Interior": 2, + "Theater": 1, + "Southern Peninsula": 5, + "Jungle": 2, + "Jungle Under Popup Wall": 1, + "Jungle Vault": 1, + "Outside Bunker": 3, + "Bunker Glass Room": 1, + "Bunker Under Elevator": 1, + "Bunker Green Room": 1, + "Outside Swamp": 2, + "Swamp Entry Area": 1, + "Swamp Platform": 1, + "Swamp Cyan Underwater": 1, + "Swamp Near Boat": 1, + "Swamp Laser Area": 1, + "Treehouse Beach": 1, + "Treehouse Yellow Bridge": 1, + "Treehouse Junction": 2, + "Treehouse Second Purple Bridge": 1, + "Treehouse Green Bridge Left House": 1, + "Treehouse Laser Room Back Platform": 1, + "Treehouse Burned House": 1, + "Treehouse Drawbridge Platform": 1, + "Mountainside": 4, + "Mountaintop": 1, + "Mountain Floor 1 Trash Pillar": 1, + "Mountain Floor 1 Back Section": 1, + "Mountain Floor 2": 1, + "Mountain Bottom Floor Pillars Room": 1, + "Caves Entry Door": 1, + "Caves": 2, + "Caves Path to Challenge": 1, + "Challenge": 2, + "Tunnels": 2, + "The Ocean": 2, +} diff --git a/worlds/witness/data/static_logic.py b/worlds/witness/data/static_logic.py index 6cc4e143..4f4786a3 100644 --- a/worlds/witness/data/static_logic.py +++ b/worlds/witness/data/static_logic.py @@ -1,4 +1,4 @@ -from collections import defaultdict +from collections import Counter, defaultdict from typing import Any, Dict, List, Optional, Set, Tuple from Utils import cache_argsless @@ -11,6 +11,7 @@ from .item_definition_classes import ( ProgressiveItemDefinition, WeightedItemDefinition, ) +from .settings.easter_eggs import EASTER_EGGS from .utils import ( WitnessRule, define_new_region, @@ -49,6 +50,70 @@ class StaticWitnessLogicObj: self.reverse_connections() self.combine_connections() + def add_easter_eggs(self) -> None: + egg_counter = 0 + area_counts: Dict[str, int] = Counter() + for region_name, entity_amount in EASTER_EGGS.items(): + region_object = self.ALL_REGIONS_BY_NAME[region_name] + correct_area = region_object["area"] + + for _ in range(entity_amount): + location_id = 160200 + egg_counter + entity_hex = hex(0xEE000 + egg_counter) + egg_counter += 1 + + area_counts[correct_area["name"]] += 1 + full_entity_name = f"{correct_area['name']} Easter Egg {area_counts[correct_area['name']]}" + + self.ENTITIES_BY_HEX[entity_hex] = { + "checkName": full_entity_name, + "entity_hex": entity_hex, + "region": region_object, + "id": int(location_id), + "entityType": "Easter Egg", + "locationType": "Easter Egg", + "area": correct_area, + "order": len(self.ENTITIES_BY_HEX), + } + + self.ENTITIES_BY_NAME[self.ENTITIES_BY_HEX[entity_hex]["checkName"]] = self.ENTITIES_BY_HEX[entity_hex] + + self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex] = { + "entities": frozenset({frozenset({})}) + } + region_object["entities"].append(entity_hex) + region_object["physical_entities"].append(entity_hex) + + easter_egg_region = self.ALL_REGIONS_BY_NAME["Easter Eggs"] + easter_egg_area = easter_egg_region["area"] + for i in range(sum(EASTER_EGGS.values())): + location_id = 160000 + i + entity_hex = hex(0xEE200 + i) + + if i == 0: + continue + + full_entity_name = f"{i + 1} Easter Eggs Collected" + + self.ENTITIES_BY_HEX[entity_hex] = { + "checkName": full_entity_name, + "entity_hex": entity_hex, + "region": easter_egg_region, + "id": int(location_id), + "entityType": "Easter Egg Total", + "locationType": "Easter Egg Total", + "area": easter_egg_area, + "order": len(self.ENTITIES_BY_HEX), + } + + self.ENTITIES_BY_NAME[self.ENTITIES_BY_HEX[entity_hex]["checkName"]] = self.ENTITIES_BY_HEX[entity_hex] + + self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex] = { + "entities": frozenset({frozenset({})}) + } + easter_egg_region["entities"].append(entity_hex) + easter_egg_region["physical_entities"].append(entity_hex) + def read_logic_file(self, lines: List[str]) -> None: """ Reads the logic file and does the initial population of data structures @@ -66,7 +131,7 @@ class StaticWitnessLogicObj: continue if line[-1] == ":": - new_region_and_connections = define_new_region(line) + new_region_and_connections = define_new_region(line, current_area) current_region = new_region_and_connections[0] region_name = current_region["name"] self.ALL_REGIONS_BY_NAME[region_name] = current_region @@ -198,6 +263,8 @@ class StaticWitnessLogicObj: current_region["entities"].append(entity_hex) current_region["physical_entities"].append(entity_hex) + self.add_easter_eggs() + def reverse_connection(self, source_region: str, connection: Tuple[str, Set[WitnessRule]]) -> None: target = connection[0] traversal_options = connection[1] diff --git a/worlds/witness/data/utils.py b/worlds/witness/data/utils.py index 190c00dc..aca45738 100644 --- a/worlds/witness/data/utils.py +++ b/worlds/witness/data/utils.py @@ -1,3 +1,4 @@ +from datetime import date from math import floor from pkgutil import get_data from random import Random @@ -61,7 +62,7 @@ def build_weighted_int_list(inputs: Collection[float], total: int) -> List[int]: return rounded_output -def define_new_region(region_string: str) -> Tuple[Dict[str, Any], Set[Tuple[str, WitnessRule]]]: +def define_new_region(region_string: str, area: dict[str, Any]) -> Tuple[Dict[str, Any], Set[Tuple[str, WitnessRule]]]: """ Returns a region object by parsing a line in the logic file """ @@ -91,6 +92,7 @@ def define_new_region(region_string: str) -> Tuple[Dict[str, Any], Set[Tuple[str "shortName": region_name_simple, "entities": [], "physical_entities": [], + "area": area, } return region_obj, options @@ -264,3 +266,15 @@ def logical_and_witness_rules(witness_rules: Iterable[WitnessRule]) -> WitnessRu def logical_or_witness_rules(witness_rules: Iterable[WitnessRule]) -> WitnessRule: return optimize_witness_rule(frozenset.union(*witness_rules)) + + +def is_easter_time() -> bool: + # dateutils would have been nice here, because it has an easter() function. + # But adding it as a requirement seems heavier than necessary. + # Thus, we just take a range from the earliest to latest possible easter dates. + + today = date.today() + earliest_easter_day = date(today.year, 3, 20) # Earliest possible is 3/22 + 2 day buffer for Good Friday + last_easter_day = date(today.year, 4, 26) # Latest possible is 4/25 + 1 day buffer for Easter Monday + + return earliest_easter_day <= today <= last_easter_day diff --git a/worlds/witness/generate_data_file.py b/worlds/witness/generate_data_file.py index 50a63a37..cc05015c 100644 --- a/worlds/witness/generate_data_file.py +++ b/worlds/witness/generate_data_file.py @@ -43,3 +43,12 @@ if __name__ == "__main__": ) ) datafile.write("\n};\n\n") + + datafile.write("inline std::map entityToName = {") + datafile.write( + "\n".join( + "\t{ " + entity_hex + ', "' + entity_object["checkName"] + '" },' + for entity_hex, entity_object in static_witness_logic.ENTITIES_BY_HEX.items() + ) + ) + datafile.write("\n};\n\n") diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index 82837aed..6f274f5e 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -241,7 +241,10 @@ def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> Witnes area = chosen_group # local locations should only ever return a location group, as Witness defines groups for every location. - hint_text = f"{item_name} can be found in the {area} area." + if area == "Easter Eggs": + hint_text = f"{item_name} can be found by collecting Easter Eggs." + else: + hint_text = f"{item_name} can be found in the {area} area." else: player_name = world.multiworld.get_player_name(hint.location.player) @@ -505,10 +508,13 @@ def word_area_hint(world: "WitnessWorld", hinted_area: str, area_items: List[Ite area_progression_word = "Both" if total_progression == 2 else "All" - hint_string = f"In the {hinted_area} area, you will find " + if hinted_area == "Easter Eggs": + hint_string = "Through collecting Easter Eggs, you will find " + else: + hint_string = f"In the {hinted_area} area, you will find " hunt_panels = None - if world.options.victory_condition == "panel_hunt": + if world.options.victory_condition == "panel_hunt" and hinted_area != "Easter Eggs": hunt_panels = sum( static_witness_logic.ENTITIES_BY_HEX[hunt_entity]["area"]["name"] == hinted_area for hunt_entity in world.player_logic.HUNT_ENTITIES diff --git a/worlds/witness/locations.py b/worlds/witness/locations.py index 49a4437c..e7f6f94d 100644 --- a/worlds/witness/locations.py +++ b/worlds/witness/locations.py @@ -19,7 +19,7 @@ class WitnessPlayerLocations: def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic) -> None: """Defines locations AFTER logic changes due to options""" - self.PANEL_TYPES_TO_SHUFFLE = {"General", "Good Boi"} + self.PANEL_TYPES_TO_SHUFFLE = {"General", "Good Boi", "Easter Egg Total"} self.CHECK_LOCATIONS = static_witness_locations.GENERAL_LOCATIONS.copy() if world.options.shuffle_discarded_panels: diff --git a/worlds/witness/options.py b/worlds/witness/options.py index d7395178..c56209b2 100644 --- a/worlds/witness/options.py +++ b/worlds/witness/options.py @@ -1,4 +1,6 @@ from dataclasses import dataclass +from datetime import datetime +from typing import Tuple from schema import And, Schema @@ -18,6 +20,7 @@ from Options import ( from .data import static_logic as static_witness_logic from .data.item_definition_classes import ItemCategory, WeightedItemDefinition +from .data.utils import is_easter_time from .entity_hunt import ALL_HUNTABLE_PANELS @@ -142,6 +145,53 @@ class ShuffleEnvironmentalPuzzles(Choice): option_obelisk_sides = 2 +class EasterEggHunt(Choice): + """ + Adds up to 120 Easter Eggs to the game, placed by NewSoupVi, Exempt-Medic, hatkirby, Scipio, and Rever. + These can be collected by simply clicking on them. + + The difficulty options differ by how many Eggs you need to collect for each check and how many are logically required for each check. + + - "Easy": 3 / 8 + - "Normal": 3 / 6 + - "Hard": 4 / 6 + - "Very Hard": 4 / 5 + - "Extreme": 4 / 4 (You are expected to collect every Easter Egg) + + Checks that require more Eggs than logically available still exist, but are excluded. + For example, on "Easy", the "63 Eggs Collected" check can physically be obtained, but would logically require 125 Easter Eggs, which is impossible. Thus, it is excluded. + + On "Easy", "Normal", and "Hard", you will start with an "Egg Radar" that you can activate using the Puzzle Skip key. + On every difficulty except "Extreme", there will be a message when you've collected all Easter Eggs in an area. + On "Easy", there will be an additional message after every Easter Egg telling you how many Easter Eggs are remaining in the area. + + It is recommended that you play this mode together with Door Shuffle. Without it, more than half of the Easter Eggs will be in sphere 1. + """ + + visibility = Visibility.all if is_easter_time() else Visibility.none + + display_name = "Easter Egg Hunt" + option_off = 0 + # Number represents the amount of eggs needed per check + option_easy = 1 + option_normal = 2 + option_hard = 3 + option_very_hard = 4 + option_extreme = 5 + default = 2 if is_easter_time() else 0 + + def get_step_and_logical_step(self) -> Tuple[int, int]: + if self == "easy": + return 3, 8 + if self == "normal": + return 3, 6 + if self == "hard": + return 4, 6 + if self == "very_hard": + return 4, 5 + return 4, 4 + + class ShuffleDog(Choice): """ Adds petting the dog statue in Town into the location pool. @@ -504,6 +554,7 @@ class TheWitnessOptions(PerGameCommonOptions): death_link_amnesty: DeathLinkAmnesty puzzle_randomization_seed: PuzzleRandomizationSeed shuffle_dog: ShuffleDog + easter_egg_hunt: EasterEggHunt witness_option_groups = [ @@ -561,3 +612,13 @@ witness_option_groups = [ ShuffleDog, ]) ] + +# Make sure that Easter Egg Hunt is VERY visible during easter time (when it's enabled by default) +if is_easter_time(): + easter_special_option_group = OptionGroup("EASTER SPECIAL", [ + EasterEggHunt, + ]) + witness_option_groups = [easter_special_option_group, *witness_option_groups] +else: + silly_options_group = next(group for group in witness_option_groups if group.name == "Silly Options") + silly_options_group.options.append(EasterEggHunt) diff --git a/worlds/witness/player_items.py b/worlds/witness/player_items.py index e40d261d..b98c59e9 100644 --- a/worlds/witness/player_items.py +++ b/worlds/witness/player_items.py @@ -31,6 +31,13 @@ class WitnessItem(Item): Item from the game The Witness """ game: str = "The Witness" + 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 class WitnessPlayerItems: diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index aea2953a..1276d55d 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -24,7 +24,6 @@ from .data.item_definition_classes import DoorItemDefinition, ItemCategory, Prog from .data.static_logic import StaticWitnessLogicObj from .data.utils import ( WitnessRule, - define_new_region, get_boat, get_caves_except_path_to_challenge_exclusion_list, get_complex_additional_panels, @@ -119,6 +118,8 @@ class WitnessPlayerLogic: self.PRE_PICKED_HUNT_ENTITIES: Set[str] = set() self.HUNT_ENTITIES: Set[str] = set() + self.AVAILABLE_EASTER_EGGS: Set[str] = set() + self.AVAILABLE_EASTER_EGGS_PER_REGION: Dict[str, int] = {} self.ALWAYS_EVENT_NAMES_BY_HEX = { "0x00509": "+1 Laser", "0x012FB": "+1 Laser (Unredirected)", @@ -154,6 +155,9 @@ class WitnessPlayerLogic: picker = EntityHuntPicker(self, world, self.PRE_PICKED_HUNT_ENTITIES) self.HUNT_ENTITIES = picker.pick_panel_hunt_panels(world.options.panel_hunt_total.value) + if world.options.easter_egg_hunt: + self.finalize_easter_eggs(world) + # Finalize which items actually exist in the MultiWorld and which get grouped into progressive items. self.finalize_items() @@ -241,6 +245,8 @@ class WitnessPlayerLogic: if option_entity in {"7 Lasers", "11 Lasers", "7 Lasers + Redirect", "11 Lasers + Redirect", "PP2 Weirdness", "Theater to Tunnels", "Entity Hunt"}: new_items = frozenset({frozenset([option_entity])}) + elif "Eggs" in option_entity: + new_items = frozenset({frozenset([option_entity])}) elif option_entity in self.DISABLE_EVERYTHING_BEHIND: new_items = frozenset() else: @@ -387,13 +393,6 @@ class WitnessPlayerLogic: return - if adj_type == "Region Changes": - new_region_and_options = define_new_region(line + ":") - - self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[new_region_and_options[0]["name"]] = new_region_and_options[1] - - return - if adj_type == "New Connections": line_split = line.split(" - ") source_region = line_split[0] @@ -533,6 +532,55 @@ class WitnessPlayerLogic: return postgame_adjustments + def set_easter_egg_requirements(self, world: "WitnessWorld") -> None: + eggs_per_check, logically_required_eggs_per_check = world.options.easter_egg_hunt.get_step_and_logical_step() + + for entity_hex, entity_obj in static_witness_logic.ENTITIES_BY_HEX.items(): + if entity_obj["entityType"] != "Easter Egg Total": + continue + + direct_egg_count = int(entity_obj["checkName"].split(" ")[0]) + + if direct_egg_count % eggs_per_check: + self.COMPLETELY_DISABLED_ENTITIES.add(entity_hex) + + requirement = direct_egg_count // eggs_per_check * logically_required_eggs_per_check + self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex] = { + "entities": frozenset({frozenset({f"{requirement} Eggs"})}) + } + + def finalize_easter_eggs(self, world: "WitnessWorld") -> None: + self.AVAILABLE_EASTER_EGGS = { + entity_hex for entity_hex, entity_obj in static_witness_logic.ENTITIES_BY_HEX.items() + if entity_obj["entityType"] == "Easter Egg" and self.solvability_guaranteed(entity_hex) + } + max_eggs = len(self.AVAILABLE_EASTER_EGGS) + + self.AVAILABLE_EASTER_EGGS_PER_REGION = defaultdict(int) + for entity_hex in self.AVAILABLE_EASTER_EGGS: + region_name = static_witness_logic.ENTITIES_BY_HEX[entity_hex]["region"]["name"] + self.AVAILABLE_EASTER_EGGS_PER_REGION[region_name] += 1 + + eggs_per_check, logically_required_eggs_per_check = world.options.easter_egg_hunt.get_step_and_logical_step() + + for entity_hex, entity_obj in static_witness_logic.ENTITIES_BY_HEX.items(): + if entity_obj["entityType"] != "Easter Egg Total": + continue + if entity_hex in self.COMPLETELY_DISABLED_ENTITIES: + continue + + direct_egg_count = int(entity_obj["checkName"].split(" ", 1)[0]) + logically_required_egg_count = direct_egg_count // eggs_per_check * logically_required_eggs_per_check + if direct_egg_count > max_eggs: + self.COMPLETELY_DISABLED_ENTITIES.add(entity_hex) + continue + + self.ADDED_CHECKS.add(entity_obj["checkName"]) + if logically_required_egg_count > max_eggs: + # Exclude and set logic to require every egg + self.EXCLUDED_ENTITIES.add(entity_hex) + self.REQUIREMENTS_BY_HEX[entity_hex] = frozenset({frozenset({f"{max_eggs} Eggs"})}) + def make_options_adjustments(self, world: "WitnessWorld") -> None: """Makes logic adjustments based on options""" adjustment_linesets_in_order = [] @@ -641,6 +689,8 @@ class WitnessPlayerLogic: adjustment_linesets_in_order.append([ "New Connections:", "Outside Bunker - Bunker Elevator - TrueOneWay", + "Bunker Elevator Section - Bunker Under Elevator - " + "0x0A079 | Bunker Green Room | Bunker Cyan Room | Bunker Laser Platform | Outside Bunker", ]) if "Swamp Long Bridge" in world.options.elevators_come_to_you: adjustment_linesets_in_order.append([ @@ -655,6 +705,14 @@ class WitnessPlayerLogic: # "New Connections:" # "Town Red Rooftop - Town Maze Rooftop - TrueOneWay" + if world.options.easter_egg_hunt: + self.set_easter_egg_requirements(world) + else: + self.COMPLETELY_DISABLED_ENTITIES.update({ + entity_hex for entity_hex, entity_obj in static_witness_logic.ENTITIES_BY_HEX.items() + if "Easter Egg" in entity_obj["entityType"] + }) + if world.options.victory_condition == "panel_hunt": adjustment_linesets_in_order.append(get_entity_hunt()) @@ -691,6 +749,9 @@ class WitnessPlayerLogic: if loc_obj["entityType"] == "EP": self.COMPLETELY_DISABLED_ENTITIES.add(loc_obj["entity_hex"]) + if loc_obj["entityType"] == "Easter Egg": + self.COMPLETELY_DISABLED_ENTITIES.add(loc_obj["entity_hex"]) + elif loc_obj["entityType"] == "Panel": self.EXCLUDED_ENTITIES.add(loc_obj["entity_hex"]) @@ -937,6 +998,7 @@ class WitnessPlayerLogic: doors = world.options.shuffle_doors shortbox_req = world.options.mountain_lasers longbox_req = world.options.challenge_lasers + eggs_exist = world.options.easter_egg_hunt swamp_bridge_comes_to_you = "Swamp Long Bridge" in world.options.elevators_come_to_you quarry_elevator_comes_to_you = "Quarry Elevator" in world.options.elevators_come_to_you @@ -953,17 +1015,17 @@ class WitnessPlayerLogic: # It is easier to think about when these items *are* required, so we make that dict first # If the entity is disabled anyway, we don't need to consider that case is_item_required_dict = { - "0x03750": eps_shuffled, # Monastery Garden Entry Door + "0x03750": eps_shuffled or eggs_exist, # Monastery Garden Entry Door "0x275FA": eps_shuffled, # Boathouse Hook Control "0x17D02": eps_shuffled, # Windmill Turn Control "0x0368A": symbols_shuffled or door_panels, # Quarry Stoneworks Stairs Door "0x3865F": symbols_shuffled or door_panels or eps_shuffled, # Quarry Boathouse 2nd Barrier "0x17CC4": quarry_elevator_comes_to_you or eps_shuffled, # Quarry Elevator Panel "0x17E2B": swamp_bridge_comes_to_you and boat_shuffled or eps_shuffled, # Swamp Long Bridge - "0x0CF2A": False, # Jungle Monastery Garden Shortcut + "0x0CF2A": eggs_exist, # Jungle Monastery Garden Shortcut "0x0364E": False, # Monastery Laser Shortcut Door "0x03713": remote_doors, # Monastery Laser Shortcut Panel - "0x03313": False, # Orchard Second Gate + "0x03313": eggs_exist, # Orchard Second Gate "0x337FA": remote_doors, # Jungle Bamboo Laser Shortcut Panel "0x3873B": False, # Jungle Bamboo Laser Shortcut Door "0x335AB": False, # Caves Elevator Controls @@ -1026,4 +1088,9 @@ class WitnessPlayerLogic: entity_name = entity_obj["checkName"] self.EVENT_ITEM_PAIRS[entity_name + " (Panel Hunt)"] = ("+1 Panel Hunt", entity_hex) + for region_name, easter_egg_count in self.AVAILABLE_EASTER_EGGS_PER_REGION.items(): + plural = "s" if easter_egg_count != 1 else "" + event_name = f"+{easter_egg_count} Easter Egg{plural}" + self.EVENT_ITEM_PAIRS[f"{region_name} Easter Egg{plural}"] = (event_name, region_name) + return diff --git a/worlds/witness/regions.py b/worlds/witness/regions.py index a1f7df8a..8cb3678a 100644 --- a/worlds/witness/regions.py +++ b/worlds/witness/regions.py @@ -117,12 +117,17 @@ class WitnessPlayerRegions: event_locations_per_region = defaultdict(dict) for event_location, event_item_and_entity in player_logic.EVENT_ITEM_PAIRS.items(): - region = static_witness_logic.ENTITIES_BY_HEX[event_item_and_entity[1]]["region"] - if region is None: - region_name = "Entry" + entity_or_region = event_item_and_entity[1] + if entity_or_region in static_witness_logic.ALL_REGIONS_BY_NAME: + region_name = entity_or_region + order = -1 else: - region_name = region["name"] - order = self.reference_logic.ENTITIES_BY_HEX[event_item_and_entity[1]]["order"] + region = static_witness_logic.ENTITIES_BY_HEX[event_item_and_entity[1]]["region"] + if region is None: + region_name = "Entry" + else: + region_name = region["name"] + order = self.reference_logic.ENTITIES_BY_HEX[entity_or_region]["order"] event_locations_per_region[region_name][event_location] = order for region_name, region in regions_to_create.items(): diff --git a/worlds/witness/ruff.toml b/worlds/witness/ruff.toml index a35711cc..6deccd13 100644 --- a/worlds/witness/ruff.toml +++ b/worlds/witness/ruff.toml @@ -2,7 +2,7 @@ line-length = 120 [lint] select = ["C", "E", "F", "R", "W", "I", "N", "Q", "UP", "RUF", "ISC", "T20"] -ignore = ["C9", "RUF012", "RUF100"] +ignore = ["C9", "RUF012", "RUF021", "RUF100", "UP006", "UP035"] [lint.per-file-ignores] # The way options definitions work right now, I am forced to break line length requirements. diff --git a/worlds/witness/rules.py b/worlds/witness/rules.py index dac1556e..866f4690 100644 --- a/worlds/witness/rules.py +++ b/worlds/witness/rules.py @@ -196,6 +196,8 @@ def _has_item(item: str, world: "WitnessWorld", if item == "Entity Hunt": # Right now, panel hunt is the only type of entity hunt. This may need to be changed later return _can_do_panel_hunt(world) + if "Eggs" in item: + return SimpleItemRepresentation("Egg", int(item.split(" ")[0])) if item == "PP2 Weirdness": return lambda state: _can_do_expert_pp2(state, world) if item == "Theater to Tunnels": @@ -303,6 +305,11 @@ def make_lambda(entity_hex: str, world: "WitnessWorld") -> Optional[CollectionRu return _meets_item_requirements(entity_req, world) +def make_region_lambda(region_name: str, world: "WitnessWorld") -> CollectionRule: + region = world.get_region(region_name) + return lambda state: region.can_reach(state) + + def set_rules(world: "WitnessWorld") -> None: """ Sets all rules for all locations @@ -312,8 +319,12 @@ def set_rules(world: "WitnessWorld") -> None: real_location = location if location in world.player_locations.EVENT_LOCATION_TABLE: - entity_hex = world.player_logic.EVENT_ITEM_PAIRS[location][1] - real_location = static_witness_logic.ENTITIES_BY_HEX[entity_hex]["checkName"] + entity_hex_or_region_name = world.player_logic.EVENT_ITEM_PAIRS[location][1] + if entity_hex_or_region_name in static_witness_logic.ALL_REGIONS_BY_NAME: + set_rule(world.get_location(location), make_region_lambda(entity_hex_or_region_name, world)) + continue + + real_location = static_witness_logic.ENTITIES_BY_HEX[entity_hex_or_region_name]["checkName"] associated_entity = world.player_logic.REFERENCE_LOGIC.ENTITIES_BY_NAME[real_location] entity_hex = associated_entity["entity_hex"] diff --git a/worlds/witness/test/test_easter_egg_shuffle.py b/worlds/witness/test/test_easter_egg_shuffle.py new file mode 100644 index 00000000..300d32f9 --- /dev/null +++ b/worlds/witness/test/test_easter_egg_shuffle.py @@ -0,0 +1,155 @@ +from typing import cast + +from BaseClasses import LocationProgressType + +from .. import WitnessWorld +from ..test import WitnessMultiworldTestBase + + +class TestEasterEggShuffle(WitnessMultiworldTestBase): + options_per_world = [ + { + "easter_egg_hunt": "off", + }, + { + "easter_egg_hunt": "easy", + }, + { + "easter_egg_hunt": "normal", + }, + { + "easter_egg_hunt": "hard", + }, + { + "easter_egg_hunt": "very_hard", + }, + { + "easter_egg_hunt": "extreme", + }, + ] + + def test_easter_egg_hunt(self) -> None: + with self.subTest("Test that player without Easter Egg Hunt has no easter egg related locations"): + egg_locations = {location for location in self.multiworld.get_locations(1) if "Egg" in location.name} + self.assertFalse(egg_locations) + + for player, eggs_per_check, logical_eggs_per_check in zip([2, 3, 4, 5, 6], [3, 3, 4, 4, 4], [8, 6, 6, 5, 4]): + world = cast(WitnessWorld, self.multiworld.worlds[player]) + option_name = world.options.easter_egg_hunt + + with self.subTest(f"Test that {option_name} Egg Hunt player starts with 0 eggs"): + self.assertEqual(self.multiworld.state.count("Egg", player), 0) + + with self.subTest(f"Test that the correct Egg Collection locations exist for {option_name} player"): + first_egg_location = f"{eggs_per_check} Easter Eggs Collected" + one_less_location = f"{eggs_per_check - 1} Easter Eggs Collected" + one_more_location = f"{eggs_per_check + 1} Easter Eggs Collected" + self.assert_location_exists(first_egg_location, player) + self.assert_location_does_not_exist(one_less_location, player, strict_check=False) + self.assert_location_does_not_exist(one_more_location, player, strict_check=False) + + one_too_few = logical_eggs_per_check - 1 + with self.subTest(f'Test that "+{one_too_few} Easter Eggs" item adds 4 easter eggs'): + item = world.create_item(f"+{one_too_few} Easter Eggs") + self.multiworld.state.collect(item, prevent_sweep=True) + self.assertEqual(self.multiworld.state.count("Egg", player), one_too_few) + + with self.subTest( + f"Test that {one_too_few} Easter Eggs are not enough for {option_name} player's first location" + ): + self.assertFalse(self.multiworld.state.can_reach_location(first_egg_location, player)) + + with self.subTest( + f"Test that {logical_eggs_per_check} Easter Eggs are enough for {option_name} player's first location" + ): + item = world.create_item("+1 Easter Egg") + self.multiworld.state.collect(item, prevent_sweep=True) + self.assertTrue(self.multiworld.state.can_reach_location(first_egg_location, player)) + + +class TestEggRestrictions(WitnessMultiworldTestBase): + options_per_world = [ + { + "shuffle_postgame": False, + }, + { + "shuffle_postgame": True, + }, + { + "shuffle_postgame": True, + "exclude_locations": frozenset({"Bunker Easter Egg 3"}), + } + ] + + common_options = { + "victory_condition": "mountain_box_short", + "shuffle_doors": "off", + "easter_egg_hunt": "very_hard", + "shuffle_vault_boxes": True, + } + + def test_egg_restrictions(self) -> None: + with self.subTest("Test that locations beyond 108 Easter Eggs don't exist for a seed without Mountain"): + self.assert_location_exists("108 Easter Eggs Collected", 1) + self.assert_location_does_not_exist("112 Easter Eggs Collected", 1) + + with self.subTest( + "Test that locations beyond 86 Easter Eggs, which would logically require more than 108 Eggs, are excluded" + ): + egg_84_location = self.multiworld.get_location("84 Easter Eggs Collected", 1) + egg_88_location = self.multiworld.get_location("88 Easter Eggs Collected", 1) + + self.assertNotEqual(egg_84_location.progress_type, LocationProgressType.EXCLUDED) + self.assertEqual(egg_88_location.progress_type, LocationProgressType.EXCLUDED) + + with self.subTest("Test that in a seed with the whole game included, the 120 egg location exists"): + self.assert_location_exists("120 Easter Eggs Collected", 2) + + with self.subTest( + "Test that locations beyond 96 Easter Eggs, which would logically require more than 120 Eggs, are excluded" + ): + egg_96_location = self.multiworld.get_location("96 Easter Eggs Collected", 2) + egg_100_location = self.multiworld.get_location("100 Easter Eggs Collected", 2) + + self.assertNotEqual(egg_96_location.progress_type, LocationProgressType.EXCLUDED) + self.assertEqual(egg_100_location.progress_type, LocationProgressType.EXCLUDED) + + with self.subTest("Test that you can exclude and egg to disable it"): + self.assert_location_exists("116 Easter Eggs Collected", 3) + self.assert_location_does_not_exist("120 Easter Eggs Collected", 3) + + +class TestBunkerElevatorEgg(WitnessMultiworldTestBase): + options_per_world = [ + { + "elevators_come_to_you": frozenset() + }, + { + "elevators_come_to_you": frozenset({"Bunker Elevator"}) + }, + ] + + common_options = { + "easter_egg_hunt": "normal", + "shuffle_doors": "panels", + "shuffle_symbols": False, + } + + def test_bunker_elevator_egg(self) -> None: + items_to_reach_bunker_elevator = [ + "Bunker Entry (Panel)", + "Bunker Tinted Glass Door (Panel)", + "Bunker Drop-Down Door Controls (Panel)" + ] + + with self.subTest("Test that normally, the egg behind the elevator needs Elevator Control"): + self.assertFalse(self.multiworld.state.can_reach_location("Bunker Under Elevator Easter Egg", 1)) + self.collect_by_name(items_to_reach_bunker_elevator, 1) + self.assertFalse(self.multiworld.state.can_reach_location("Bunker Under Elevator Easter Egg", 1)) + self.collect_by_name(["Bunker Elevator Control (Panel)"], 1) + self.assertTrue(self.multiworld.state.can_reach_location("Bunker Under Elevator Easter Egg", 1)) + + with self.subTest("Test that with auto-elevators, the egg behind the elevator doesn't need Elevator Control"): + self.assertFalse(self.multiworld.state.can_reach_location("Bunker Under Elevator Easter Egg", 2)) + self.collect_by_name(items_to_reach_bunker_elevator, 2) + self.assertTrue(self.multiworld.state.can_reach_location("Bunker Under Elevator Easter Egg", 2))