The Messenger: Add Shop Rando (#1834)
* add shop shuffle options and items * add logic for the shop slots * write cost tests * start on shop item logic * make strike and second wind early items * some cleanup * remove 5 shards * double cost requirement for really expensive items and raise the rates * add test for shop shuffle with minimum other locations * put power seal in front of shards * rename locations and items * update rules, regions, and shop * update tests and misc fixes * minor cleanup * implement money wrench and figurines * clean out now unneeded info from slot_data * docs update and fix a failure when not shuffling shops * remove shop shuffle option * Finish out shop rules * make seals generation easier to read and fix tests * rule adjustments * oop * adjust the prices to be a bit more generous * add max price to slot data for tracker * update the hard rules a bit * remove unnecessary test * update data_version * bump version and remove info for fixed issues * remove now unneeded assert * review updates * minor bug fix * add a test for minimum locations shop costing * minor optimizations and cleanup * remove whitespace
This commit is contained in:
		
							parent
							
								
									8c2584f872
								
							
						
					
					
						commit
						332eab9569
					
				| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
# items
 | 
			
		||||
# listing individual groups first for easy lookup
 | 
			
		||||
from .Shop import SHOP_ITEMS, FIGURINES
 | 
			
		||||
 | 
			
		||||
NOTES = [
 | 
			
		||||
    "Key of Hope",
 | 
			
		||||
| 
						 | 
				
			
			@ -13,15 +14,16 @@ NOTES = [
 | 
			
		|||
PROG_ITEMS = [
 | 
			
		||||
    "Wingsuit",
 | 
			
		||||
    "Rope Dart",
 | 
			
		||||
    "Ninja Tabi",
 | 
			
		||||
    "Lightfoot Tabi",
 | 
			
		||||
    "Power Thistle",
 | 
			
		||||
    "Demon King Crown",
 | 
			
		||||
    "Ruxxtin's Amulet",
 | 
			
		||||
    "Fairy Bottle",
 | 
			
		||||
    "Magic Firefly",
 | 
			
		||||
    "Sun Crest",
 | 
			
		||||
    "Moon Crest",
 | 
			
		||||
    # "Astral Seed",
 | 
			
		||||
    # "Astral Tea Leaves",
 | 
			
		||||
    "Money Wrench",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
PHOBEKINS = [
 | 
			
		||||
| 
						 | 
				
			
			@ -35,13 +37,22 @@ USEFUL_ITEMS = [
 | 
			
		|||
    "Windmill Shuriken",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
FILLER = {
 | 
			
		||||
    "Time Shard": 5,
 | 
			
		||||
    "Time Shard (10)": 10,
 | 
			
		||||
    "Time Shard (50)": 20,
 | 
			
		||||
    "Time Shard (100)": 20,
 | 
			
		||||
    "Time Shard (300)": 10,
 | 
			
		||||
    "Time Shard (500)": 5,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# item_name_to_id needs to be deterministic and match upstream
 | 
			
		||||
ALL_ITEMS = [
 | 
			
		||||
    *NOTES,
 | 
			
		||||
    "Windmill Shuriken",
 | 
			
		||||
    "Wingsuit",
 | 
			
		||||
    "Rope Dart",
 | 
			
		||||
    "Ninja Tabi",
 | 
			
		||||
    "Lightfoot Tabi",
 | 
			
		||||
    # "Astral Seed",
 | 
			
		||||
    # "Astral Tea Leaves",
 | 
			
		||||
    "Candle",
 | 
			
		||||
| 
						 | 
				
			
			@ -49,12 +60,15 @@ ALL_ITEMS = [
 | 
			
		|||
    "Power Thistle",
 | 
			
		||||
    "Demon King Crown",
 | 
			
		||||
    "Ruxxtin's Amulet",
 | 
			
		||||
    "Fairy Bottle",
 | 
			
		||||
    "Magic Firefly",
 | 
			
		||||
    "Sun Crest",
 | 
			
		||||
    "Moon Crest",
 | 
			
		||||
    *PHOBEKINS,
 | 
			
		||||
    "Power Seal",
 | 
			
		||||
    "Time Shard",  # there's 45 separate instances of this in the client lookup, but hopefully we don't care?
 | 
			
		||||
    *FILLER,
 | 
			
		||||
    *SHOP_ITEMS,
 | 
			
		||||
    *FIGURINES,
 | 
			
		||||
    "Money Wrench",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
# locations
 | 
			
		||||
| 
						 | 
				
			
			@ -62,100 +76,38 @@ ALL_ITEMS = [
 | 
			
		|||
# order must be exactly the same as upstream
 | 
			
		||||
ALWAYS_LOCATIONS = [
 | 
			
		||||
    # notes
 | 
			
		||||
    "Key of Love",
 | 
			
		||||
    "Key of Courage",
 | 
			
		||||
    "Key of Chaos",
 | 
			
		||||
    "Key of Symbiosis",
 | 
			
		||||
    "Key of Strength",
 | 
			
		||||
    "Key of Hope",
 | 
			
		||||
    "Sunken Shrine - Key of Love",
 | 
			
		||||
    "Corrupted Future - Key of Courage",
 | 
			
		||||
    "Underworld - Key of Chaos",
 | 
			
		||||
    "Elemental Skylands - Key of Symbiosis",
 | 
			
		||||
    "Searing Crags - Key of Strength",
 | 
			
		||||
    "Autumn Hills - Key of Hope",
 | 
			
		||||
    # upgrades
 | 
			
		||||
    "Wingsuit",
 | 
			
		||||
    "Rope Dart",
 | 
			
		||||
    "Ninja Tabi",
 | 
			
		||||
    "Climbing Claws",
 | 
			
		||||
    "Howling Grotto - Wingsuit",
 | 
			
		||||
    "Searing Crags - Rope Dart",
 | 
			
		||||
    "Sunken Shrine - Lightfoot Tabi",
 | 
			
		||||
    "Autumn Hills - Climbing Claws",
 | 
			
		||||
    # quest items
 | 
			
		||||
    "Astral Seed",
 | 
			
		||||
    "Astral Tea Leaves",
 | 
			
		||||
    "Candle",
 | 
			
		||||
    "Seashell",
 | 
			
		||||
    "Power Thistle",
 | 
			
		||||
    "Demon King Crown",
 | 
			
		||||
    "Ruxxtin's Amulet",
 | 
			
		||||
    "Fairy Bottle",
 | 
			
		||||
    "Sun Crest",
 | 
			
		||||
    "Moon Crest",
 | 
			
		||||
    "Ninja Village - Astral Seed",
 | 
			
		||||
    "Searing Crags - Astral Tea Leaves",
 | 
			
		||||
    "Ninja Village - Candle",
 | 
			
		||||
    "Quillshroom Marsh - Seashell",
 | 
			
		||||
    "Searing Crags - Power Thistle",
 | 
			
		||||
    "Forlorn Temple - Demon King",
 | 
			
		||||
    "Catacombs - Ruxxtin's Amulet",
 | 
			
		||||
    "Riviere Turquoise - Butterfly Matriarch",
 | 
			
		||||
    "Sunken Shrine - Sun Crest",
 | 
			
		||||
    "Sunken Shrine - Moon Crest",
 | 
			
		||||
    # phobekins
 | 
			
		||||
    "Necro",
 | 
			
		||||
    "Pyro",
 | 
			
		||||
    "Claustro",
 | 
			
		||||
    "Acro",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
SEALS = [
 | 
			
		||||
    "Ninja Village Seal - Tree House",
 | 
			
		||||
 | 
			
		||||
    "Autumn Hills Seal - Trip Saws",
 | 
			
		||||
    "Autumn Hills Seal - Double Swing Saws",
 | 
			
		||||
    "Autumn Hills Seal - Spike Ball Swing",
 | 
			
		||||
    "Autumn Hills Seal - Spike Ball Darts",
 | 
			
		||||
 | 
			
		||||
    "Catacombs Seal - Triple Spike Crushers",
 | 
			
		||||
    "Catacombs Seal - Crusher Gauntlet",
 | 
			
		||||
    "Catacombs Seal - Dirty Pond",
 | 
			
		||||
 | 
			
		||||
    "Bamboo Creek Seal - Spike Crushers and Doors",
 | 
			
		||||
    "Bamboo Creek Seal - Spike Ball Pits",
 | 
			
		||||
    "Bamboo Creek Seal - Spike Crushers and Doors v2",
 | 
			
		||||
 | 
			
		||||
    "Howling Grotto Seal - Windy Saws and Balls",
 | 
			
		||||
    "Howling Grotto Seal - Crushing Pits",
 | 
			
		||||
    "Howling Grotto Seal - Breezy Crushers",
 | 
			
		||||
 | 
			
		||||
    "Quillshroom Marsh Seal - Spikey Window",
 | 
			
		||||
    "Quillshroom Marsh Seal - Sand Trap",
 | 
			
		||||
    "Quillshroom Marsh Seal - Do the Spike Wave",
 | 
			
		||||
 | 
			
		||||
    "Searing Crags Seal - Triple Ball Spinner",
 | 
			
		||||
    "Searing Crags Seal - Raining Rocks",
 | 
			
		||||
    "Searing Crags Seal - Rhythm Rocks",
 | 
			
		||||
 | 
			
		||||
    "Glacial Peak Seal - Ice Climbers",
 | 
			
		||||
    "Glacial Peak Seal - Projectile Spike Pit",
 | 
			
		||||
    "Glacial Peak Seal - Glacial Air Swag",
 | 
			
		||||
 | 
			
		||||
    "Tower of Time Seal - Time Waster Seal",
 | 
			
		||||
    "Tower of Time Seal - Lantern Climb",
 | 
			
		||||
    "Tower of Time Seal - Arcane Orbs",
 | 
			
		||||
 | 
			
		||||
    "Cloud Ruins Seal - Ghost Pit",
 | 
			
		||||
    "Cloud Ruins Seal - Toothbrush Alley",
 | 
			
		||||
    "Cloud Ruins Seal - Saw Pit",
 | 
			
		||||
    "Cloud Ruins Seal - Money Farm Room",
 | 
			
		||||
 | 
			
		||||
    "Underworld Seal - Sharp and Windy Climb",
 | 
			
		||||
    "Underworld Seal - Spike Wall",
 | 
			
		||||
    "Underworld Seal - Fireball Wave",
 | 
			
		||||
    "Underworld Seal - Rising Fanta",
 | 
			
		||||
 | 
			
		||||
    "Forlorn Temple Seal - Rocket Maze",
 | 
			
		||||
    "Forlorn Temple Seal - Rocket Sunset",
 | 
			
		||||
 | 
			
		||||
    "Sunken Shrine Seal - Ultra Lifeguard",
 | 
			
		||||
    "Sunken Shrine Seal - Waterfall Paradise",
 | 
			
		||||
    "Sunken Shrine Seal - Tabi Gauntlet",
 | 
			
		||||
 | 
			
		||||
    "Riviere Turquoise Seal - Bounces and Balls",
 | 
			
		||||
    "Riviere Turquoise Seal - Launch of Faith",
 | 
			
		||||
    "Riviere Turquoise Seal - Flower Power",
 | 
			
		||||
 | 
			
		||||
    "Elemental Skylands Seal - Air",
 | 
			
		||||
    "Elemental Skylands Seal - Water",
 | 
			
		||||
    "Elemental Skylands Seal - Fire",
 | 
			
		||||
    "Catacombs - Necro",
 | 
			
		||||
    "Searing Crags - Pyro",
 | 
			
		||||
    "Bamboo Creek - Claustro",
 | 
			
		||||
    "Cloud Ruins - Acro",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
BOSS_LOCATIONS = [
 | 
			
		||||
    "Leaf Golem",
 | 
			
		||||
    "Ruxxtin",
 | 
			
		||||
    "Emerald Golem",
 | 
			
		||||
    "Queen of Quills",
 | 
			
		||||
    "Autumn Hills - Leaf Golem",
 | 
			
		||||
    "Catacombs - Ruxxtin",
 | 
			
		||||
    "Howling Grotto - Emerald Golem",
 | 
			
		||||
    "Quillshroom Marsh - Queen of Quills",
 | 
			
		||||
]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,7 @@
 | 
			
		|||
from Options import DefaultOnToggle, DeathLink, Range, Accessibility, Choice, Toggle, StartInventoryPool
 | 
			
		||||
from typing import Dict
 | 
			
		||||
from schema import Schema, Or, And, Optional
 | 
			
		||||
 | 
			
		||||
from Options import DefaultOnToggle, DeathLink, Range, Accessibility, Choice, Toggle, OptionDict, StartInventoryPool
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MessengerAccessibility(Accessibility):
 | 
			
		||||
| 
						 | 
				
			
			@ -10,16 +13,16 @@ class MessengerAccessibility(Accessibility):
 | 
			
		|||
class Logic(Choice):
 | 
			
		||||
    """
 | 
			
		||||
    The level of logic to use when determining what locations in your world are accessible.
 | 
			
		||||
    Normal can require damage boosts, but otherwise approachable for someone who has beaten the game.
 | 
			
		||||
    Hard has some easier speedrunning tricks in logic. May need to leash.
 | 
			
		||||
    Challenging contains more medium and hard difficulty speedrunning tricks.
 | 
			
		||||
    OoB places everything with the minimum amount of rules possible. Expect to do OoB. Not guaranteed completable.
 | 
			
		||||
 | 
			
		||||
    Normal: can require damage boosts, but otherwise approachable for someone who has beaten the game.
 | 
			
		||||
    Hard: has leashing, normal clips, time warps and turtle boosting in logic.
 | 
			
		||||
    OoB: places everything with the minimum amount of rules possible. Expect to do OoB. Not guaranteed completable.
 | 
			
		||||
    """
 | 
			
		||||
    display_name = "Logic Level"
 | 
			
		||||
    option_normal = 0
 | 
			
		||||
    option_hard = 1
 | 
			
		||||
    option_challenging = 2
 | 
			
		||||
    option_oob = 3
 | 
			
		||||
    option_oob = 2
 | 
			
		||||
    alias_challenging = 1
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PowerSeals(DefaultOnToggle):
 | 
			
		||||
| 
						 | 
				
			
			@ -68,6 +71,64 @@ class RequiredSeals(Range):
 | 
			
		|||
    default = range_end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ShopPrices(Range):
 | 
			
		||||
    """Percentage modifier for shuffled item prices in shops"""
 | 
			
		||||
    display_name = "Shop Prices Modifier"
 | 
			
		||||
    range_start = 25
 | 
			
		||||
    range_end = 400
 | 
			
		||||
    default = 100
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def planned_price(location: str) -> Dict[Optional, Or]:
 | 
			
		||||
    return {
 | 
			
		||||
        Optional(location): Or(
 | 
			
		||||
            And(int, lambda n: n >= 0),
 | 
			
		||||
            {
 | 
			
		||||
                Optional(And(int, lambda n: n >= 0)): And(int, lambda n: n >= 0)
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PlannedShopPrices(OptionDict):
 | 
			
		||||
    """Plan specific prices on shop slots. Supports weighting"""
 | 
			
		||||
    display_name = "Shop Price Plando"
 | 
			
		||||
    schema = Schema({
 | 
			
		||||
        **planned_price("Karuta Plates"),
 | 
			
		||||
        **planned_price("Serendipitous Bodies"),
 | 
			
		||||
        **planned_price("Path of Resilience"),
 | 
			
		||||
        **planned_price("Kusari Jacket"),
 | 
			
		||||
        **planned_price("Energy Shuriken"),
 | 
			
		||||
        **planned_price("Serendipitous Minds"),
 | 
			
		||||
        **planned_price("Prepared Mind"),
 | 
			
		||||
        **planned_price("Meditation"),
 | 
			
		||||
        **planned_price("Rejuvenative Spirit"),
 | 
			
		||||
        **planned_price("Centered Mind"),
 | 
			
		||||
        **planned_price("Strike of the Ninja"),
 | 
			
		||||
        **planned_price("Second Wind"),
 | 
			
		||||
        **planned_price("Currents Master"),
 | 
			
		||||
        **planned_price("Aerobatics Warrior"),
 | 
			
		||||
        **planned_price("Demon's Bane"),
 | 
			
		||||
        **planned_price("Devil's Due"),
 | 
			
		||||
        **planned_price("Time Sense"),
 | 
			
		||||
        **planned_price("Power Sense"),
 | 
			
		||||
        **planned_price("Focused Power Sense"),
 | 
			
		||||
        **planned_price("Green Kappa Figurine"),
 | 
			
		||||
        **planned_price("Blue Kappa Figurine"),
 | 
			
		||||
        **planned_price("Ountarde Figurine"),
 | 
			
		||||
        **planned_price("Red Kappa Figurine"),
 | 
			
		||||
        **planned_price("Demon King Figurine"),
 | 
			
		||||
        **planned_price("Quillshroom Figurine"),
 | 
			
		||||
        **planned_price("Jumping Quillshroom Figurine"),
 | 
			
		||||
        **planned_price("Scurubu Figurine"),
 | 
			
		||||
        **planned_price("Jumping Scurubu Figurine"),
 | 
			
		||||
        **planned_price("Wallaxer Figurine"),
 | 
			
		||||
        **planned_price("Barmath'azel Figurine"),
 | 
			
		||||
        **planned_price("Queen of Quills Figurine"),
 | 
			
		||||
        **planned_price("Demon Hive Figurine"),
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
messenger_options = {
 | 
			
		||||
    "accessibility": MessengerAccessibility,
 | 
			
		||||
    "start_inventory": StartInventoryPool,
 | 
			
		||||
| 
						 | 
				
			
			@ -79,5 +140,7 @@ messenger_options = {
 | 
			
		|||
    "notes_needed": NotesNeeded,
 | 
			
		||||
    "total_seals": AmountSeals,
 | 
			
		||||
    "percent_seals_required": RequiredSeals,
 | 
			
		||||
    "shop_price": ShopPrices,
 | 
			
		||||
    "shop_price_plan": PlannedShopPrices,
 | 
			
		||||
    "death_link": DeathLink,
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,27 +5,60 @@ REGIONS: Dict[str, List[str]] = {
 | 
			
		|||
    "Tower HQ": [],
 | 
			
		||||
    "The Shop": [],
 | 
			
		||||
    "Tower of Time": [],
 | 
			
		||||
    "Ninja Village": ["Candle", "Astral Seed"],
 | 
			
		||||
    "Autumn Hills": ["Climbing Claws", "Key of Hope", "Leaf Golem"],
 | 
			
		||||
    "Forlorn Temple": ["Demon King Crown"],
 | 
			
		||||
    "Catacombs": ["Necro", "Ruxxtin's Amulet", "Ruxxtin"],
 | 
			
		||||
    "Bamboo Creek": ["Claustro"],
 | 
			
		||||
    "Howling Grotto": ["Wingsuit", "Emerald Golem"],
 | 
			
		||||
    "Quillshroom Marsh": ["Seashell", "Queen of Quills"],
 | 
			
		||||
    "Searing Crags": ["Rope Dart"],
 | 
			
		||||
    "Searing Crags Upper": ["Power Thistle", "Key of Strength", "Astral Tea Leaves"],
 | 
			
		||||
    "Ninja Village": ["Ninja Village - Candle", "Ninja Village - Astral Seed"],
 | 
			
		||||
    "Autumn Hills": ["Autumn Hills - Climbing Claws", "Autumn Hills - Key of Hope", "Autumn Hills - Leaf Golem"],
 | 
			
		||||
    "Forlorn Temple": ["Forlorn Temple - Demon King"],
 | 
			
		||||
    "Catacombs": ["Catacombs - Necro", "Catacombs - Ruxxtin's Amulet", "Catacombs - Ruxxtin"],
 | 
			
		||||
    "Bamboo Creek": ["Bamboo Creek - Claustro"],
 | 
			
		||||
    "Howling Grotto": ["Howling Grotto - Wingsuit", "Howling Grotto - Emerald Golem"],
 | 
			
		||||
    "Quillshroom Marsh": ["Quillshroom Marsh - Seashell", "Quillshroom Marsh - Queen of Quills"],
 | 
			
		||||
    "Searing Crags": ["Searing Crags - Rope Dart"],
 | 
			
		||||
    "Searing Crags Upper": ["Searing Crags - Power Thistle", "Searing Crags - Key of Strength",
 | 
			
		||||
                            "Searing Crags - Astral Tea Leaves"],
 | 
			
		||||
    "Glacial Peak": [],
 | 
			
		||||
    "Cloud Ruins": [],
 | 
			
		||||
    "Cloud Ruins Right": ["Acro"],
 | 
			
		||||
    "Underworld": ["Pyro", "Key of Chaos"],
 | 
			
		||||
    "Cloud Ruins Right": ["Cloud Ruins - Acro"],
 | 
			
		||||
    "Underworld": ["Searing Crags - Pyro", "Underworld - Key of Chaos"],
 | 
			
		||||
    "Dark Cave": [],
 | 
			
		||||
    "Riviere Turquoise": ["Fairy Bottle"],
 | 
			
		||||
    "Sunken Shrine": ["Ninja Tabi", "Sun Crest", "Moon Crest", "Key of Love"],
 | 
			
		||||
    "Elemental Skylands": ["Key of Symbiosis"],
 | 
			
		||||
    "Corrupted Future": ["Key of Courage"],
 | 
			
		||||
    "Riviere Turquoise Entrance": [],
 | 
			
		||||
    "Riviere Turquoise": ["Riviere Turquoise - Butterfly Matriarch"],
 | 
			
		||||
    "Sunken Shrine": ["Sunken Shrine - Lightfoot Tabi", "Sunken Shrine - Sun Crest", "Sunken Shrine - Moon Crest",
 | 
			
		||||
                      "Sunken Shrine - Key of Love"],
 | 
			
		||||
    "Elemental Skylands": ["Elemental Skylands - Key of Symbiosis"],
 | 
			
		||||
    "Corrupted Future": ["Corrupted Future - Key of Courage"],
 | 
			
		||||
    "Music Box": ["Rescue Phantom"],
 | 
			
		||||
}
 | 
			
		||||
"""seal locations have the region in their name and may not need to be created so skip them here"""
 | 
			
		||||
 | 
			
		||||
SEALS: Dict[str, List[str]] = {
 | 
			
		||||
    "Ninja Village": ["Ninja Village Seal - Tree House"],
 | 
			
		||||
    "Autumn Hills": ["Autumn Hills Seal - Trip Saws", "Autumn Hills Seal - Double Swing Saws",
 | 
			
		||||
                     "Autumn Hills Seal - Spike Ball Swing", "Autumn Hills Seal - Spike Ball Darts"],
 | 
			
		||||
    "Catacombs": ["Catacombs Seal - Triple Spike Crushers", "Catacombs Seal - Crusher Gauntlet",
 | 
			
		||||
                  "Catacombs Seal - Dirty Pond"],
 | 
			
		||||
    "Bamboo Creek": ["Bamboo Creek Seal - Spike Crushers and Doors", "Bamboo Creek Seal - Spike Ball Pits",
 | 
			
		||||
                     "Bamboo Creek Seal - Spike Crushers and Doors v2"],
 | 
			
		||||
    "Howling Grotto": ["Howling Grotto Seal - Windy Saws and Balls", "Howling Grotto Seal - Crushing Pits",
 | 
			
		||||
                       "Howling Grotto Seal - Breezy Crushers"],
 | 
			
		||||
    "Quillshroom Marsh": ["Quillshroom Marsh Seal - Spikey Window", "Quillshroom Marsh Seal - Sand Trap",
 | 
			
		||||
                          "Quillshroom Marsh Seal - Do the Spike Wave"],
 | 
			
		||||
    "Searing Crags": ["Searing Crags Seal - Triple Ball Spinner"],
 | 
			
		||||
    "Searing Crags Upper": ["Searing Crags Seal - Raining Rocks", "Searing Crags Seal - Rhythm Rocks"],
 | 
			
		||||
    "Glacial Peak": ["Glacial Peak Seal - Ice Climbers", "Glacial Peak Seal - Projectile Spike Pit",
 | 
			
		||||
                     "Glacial Peak Seal - Glacial Air Swag"],
 | 
			
		||||
    "Tower of Time": ["Tower of Time Seal - Time Waster", "Tower of Time Seal - Lantern Climb",
 | 
			
		||||
                      "Tower of Time Seal - Arcane Orbs"],
 | 
			
		||||
    "Cloud Ruins Right": ["Cloud Ruins Seal - Ghost Pit", "Cloud Ruins Seal - Toothbrush Alley",
 | 
			
		||||
                          "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room"],
 | 
			
		||||
    "Underworld": ["Underworld Seal - Sharp and Windy Climb", "Underworld Seal - Spike Wall",
 | 
			
		||||
                   "Underworld Seal - Fireball Wave", "Underworld Seal - Rising Fanta"],
 | 
			
		||||
    "Forlorn Temple": ["Forlorn Temple Seal - Rocket Maze", "Forlorn Temple Seal - Rocket Sunset"],
 | 
			
		||||
    "Sunken Shrine": ["Sunken Shrine Seal - Ultra Lifeguard", "Sunken Shrine Seal - Waterfall Paradise",
 | 
			
		||||
                      "Sunken Shrine Seal - Tabi Gauntlet"],
 | 
			
		||||
    "Riviere Turquoise Entrance": ["Riviere Turquoise Seal - Bounces and Balls"],
 | 
			
		||||
    "Riviere Turquoise": ["Riviere Turquoise Seal - Launch of Faith", "Riviere Turquoise Seal - Flower Power"],
 | 
			
		||||
    "Elemental Skylands": ["Elemental Skylands Seal - Air", "Elemental Skylands Seal - Water",
 | 
			
		||||
                           "Elemental Skylands Seal - Fire"]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
MEGA_SHARDS: Dict[str, List[str]] = {
 | 
			
		||||
    "Autumn Hills": ["Autumn Hills Mega Shard", "Hidden Entrance Mega Shard"],
 | 
			
		||||
| 
						 | 
				
			
			@ -41,15 +74,16 @@ MEGA_SHARDS: Dict[str, List[str]] = {
 | 
			
		|||
    "Underworld": ["Under Entrance Mega Shard", "Hot Tub Mega Shard", "Projectile Pit Mega Shard"],
 | 
			
		||||
    "Forlorn Temple": ["Sunny Day Mega Shard", "Down Under Mega Shard"],
 | 
			
		||||
    "Sunken Shrine": ["Mega Shard of the Moon", "Beginner's Mega Shard", "Mega Shard of the Stars", "Mega Shard of the Sun"],
 | 
			
		||||
    "Riviere Turquoise": ["Waterfall Mega Shard", "Quick Restock Mega Shard 1", "Quick Restock Mega Shard 2"],
 | 
			
		||||
    "RIviere Turquoise Entrance": ["Waterfall Mega Shard"],
 | 
			
		||||
    "Riviere Turquoise": ["Quick Restock Mega Shard 1", "Quick Restock Mega Shard 2"],
 | 
			
		||||
    "Elemental Skylands": ["Earth Mega Shard", "Water Mega Shard"],
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
REGION_CONNECTIONS: Dict[str, Set[str]] = {
 | 
			
		||||
    "Menu": {"Tower HQ"},
 | 
			
		||||
    "Tower HQ": {"Autumn Hills", "Howling Grotto", "Searing Crags", "Glacial Peak", "Tower of Time", "Riviere Turquoise",
 | 
			
		||||
                 "Sunken Shrine", "Corrupted Future", "The Shop", "Music Box"},
 | 
			
		||||
    "Tower HQ": {"Autumn Hills", "Howling Grotto", "Searing Crags", "Glacial Peak", "Tower of Time",
 | 
			
		||||
                 "Riviere Turquoise Entrance", "Sunken Shrine", "Corrupted Future", "The Shop", "Music Box"},
 | 
			
		||||
    "Tower of Time": set(),
 | 
			
		||||
    "Ninja Village": set(),
 | 
			
		||||
    "Autumn Hills": {"Ninja Village", "Forlorn Temple", "Catacombs"},
 | 
			
		||||
| 
						 | 
				
			
			@ -64,7 +98,8 @@ REGION_CONNECTIONS: Dict[str, Set[str]] = {
 | 
			
		|||
    "Cloud Ruins": {"Cloud Ruins Right"},
 | 
			
		||||
    "Cloud Ruins Right": {"Underworld"},
 | 
			
		||||
    "Underworld": set(),
 | 
			
		||||
    "Dark Cave": {"Catacombs", "Riviere Turquoise"},
 | 
			
		||||
    "Dark Cave": {"Catacombs", "Riviere Turquoise Entrance"},
 | 
			
		||||
    "Riviere Turquoise Entrance": {"Riviere Turquoise"},
 | 
			
		||||
    "Riviere Turquoise": set(),
 | 
			
		||||
    "Sunken Shrine": {"Howling Grotto"},
 | 
			
		||||
    "Elemental Skylands": set(),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,6 +4,7 @@ from BaseClasses import CollectionState, MultiWorld
 | 
			
		|||
from worlds.generic.Rules import set_rule, allow_self_locking_items, add_rule
 | 
			
		||||
from .Options import MessengerAccessibility, Goal
 | 
			
		||||
from .Constants import NOTES, PHOBEKINS
 | 
			
		||||
from .SubClasses import MessengerShopLocation
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from . import MessengerWorld
 | 
			
		||||
| 
						 | 
				
			
			@ -28,62 +29,73 @@ class MessengerRules:
 | 
			
		|||
            "Bamboo Creek": self.has_wingsuit,
 | 
			
		||||
            "Searing Crags Upper": self.has_vertical,
 | 
			
		||||
            "Cloud Ruins": lambda state: self.has_vertical(state) and state.has("Ruxxtin's Amulet", self.player),
 | 
			
		||||
            "Cloud Ruins Right": self.has_wingsuit,
 | 
			
		||||
            "Cloud Ruins Right": lambda state: self.has_wingsuit(state) and
 | 
			
		||||
                                               (self.has_dart(state) or self.can_dboost(state)),
 | 
			
		||||
            "Underworld": self.has_tabi,
 | 
			
		||||
            "Forlorn Temple": lambda state: state.has_all({"Wingsuit", *PHOBEKINS}, self.player),
 | 
			
		||||
            "Riviere Turquoise": lambda state: self.has_dart(state) or
 | 
			
		||||
                                               (self.has_wingsuit(state) and self.can_destroy_projectiles(state)),
 | 
			
		||||
            "Forlorn Temple": lambda state: state.has_all({"Wingsuit", *PHOBEKINS}, self.player) and self.can_dboost(state),
 | 
			
		||||
            "Glacial Peak": self.has_vertical,
 | 
			
		||||
            "Elemental Skylands": lambda state: state.has("Fairy Bottle", self.player),
 | 
			
		||||
            "Music Box": lambda state: state.has_all(set(NOTES), self.player) and self.has_vertical(state),
 | 
			
		||||
            "Elemental Skylands": lambda state: state.has("Magic Firefly", self.player) and self.has_wingsuit(state),
 | 
			
		||||
            "Music Box": lambda state: state.has_all(set(NOTES), self.player) and self.has_dart(state),
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        self.location_rules = {
 | 
			
		||||
            # ninja village
 | 
			
		||||
            "Ninja Village Seal - Tree House": self.has_dart,
 | 
			
		||||
            # autumn hills
 | 
			
		||||
            "Key of Hope": self.has_dart,
 | 
			
		||||
            "Autumn Hills - Key of Hope": self.has_dart,
 | 
			
		||||
            "Autumn Hills Seal - Spike Ball Darts": self.is_aerobatic,
 | 
			
		||||
            # bamboo creek
 | 
			
		||||
            "Bamboo Creek - Claustro": lambda state: self.has_dart(state) or self.can_dboost(state),
 | 
			
		||||
            # howling grotto
 | 
			
		||||
            "Howling Grotto Seal - Windy Saws and Balls": self.has_wingsuit,
 | 
			
		||||
            "Howling Grotto Seal - Crushing Pits": lambda state: self.has_wingsuit(state) and self.has_dart(state),
 | 
			
		||||
            "Emerald Golem": self.has_wingsuit,
 | 
			
		||||
            "Howling Grotto - Emerald Golem": self.has_wingsuit,
 | 
			
		||||
            # searing crags
 | 
			
		||||
            "Astral Tea Leaves": lambda state: state.can_reach("Astral Seed", "Location", self.player),
 | 
			
		||||
            "Key of Strength": lambda state: state.has("Power Thistle", self.player),
 | 
			
		||||
            "Searing Crags Seal - Triple Ball Spinner": self.has_vertical,
 | 
			
		||||
            "Searing Crags - Astral Tea Leaves":
 | 
			
		||||
                lambda state: state.can_reach("Ninja Village - Astral Seed", "Location", self.player),
 | 
			
		||||
            "Searing Crags - Key of Strength": lambda state: state.has("Power Thistle", self.player),
 | 
			
		||||
            # glacial peak
 | 
			
		||||
            "Glacial Peak Seal - Ice Climbers": self.has_dart,
 | 
			
		||||
            "Glacial Peak Seal - Projectile Spike Pit": self.has_vertical,
 | 
			
		||||
            "Glacial Peak Seal - Glacial Air Swag": self.has_vertical,
 | 
			
		||||
            "Glacial Peak Seal - Projectile Spike Pit": self.can_destroy_projectiles,
 | 
			
		||||
            # cloud ruins
 | 
			
		||||
            "Cloud Ruins Seal - Ghost Pit": self.has_dart,
 | 
			
		||||
            # tower of time
 | 
			
		||||
            "Tower of Time Seal - Time Waster Seal": self.has_dart,
 | 
			
		||||
            "Tower of Time Seal - Lantern Climb": self.has_wingsuit,
 | 
			
		||||
            "Tower of Time Seal - Time Waster": self.has_dart,
 | 
			
		||||
            "Tower of Time Seal - Lantern Climb": lambda state: self.has_wingsuit(state) and self.has_dart(state),
 | 
			
		||||
            "Tower of Time Seal - Arcane Orbs": lambda state: self.has_wingsuit(state) and self.has_dart(state),
 | 
			
		||||
            # underworld
 | 
			
		||||
            "Underworld Seal - Sharp and Windy Climb": self.has_wingsuit,
 | 
			
		||||
            "Underworld Seal - Fireball Wave": self.has_wingsuit,
 | 
			
		||||
            "Underworld Seal - Fireball Wave": self.is_aerobatic,
 | 
			
		||||
            "Underworld Seal - Rising Fanta": self.has_dart,
 | 
			
		||||
            # sunken shrine
 | 
			
		||||
            "Sun Crest": self.has_tabi,
 | 
			
		||||
            "Moon Crest": self.has_tabi,
 | 
			
		||||
            "Key of Love": lambda state: state.has_all({"Sun Crest", "Moon Crest"}, self.player),
 | 
			
		||||
            "Sunken Shrine - Sun Crest": self.has_tabi,
 | 
			
		||||
            "Sunken Shrine - Moon Crest": self.has_tabi,
 | 
			
		||||
            "Sunken Shrine - Key of Love": lambda state: state.has_all({"Sun Crest", "Moon Crest"}, self.player),
 | 
			
		||||
            "Sunken Shrine Seal - Waterfall Paradise": self.has_tabi,
 | 
			
		||||
            "Sunken Shrine Seal - Tabi Gauntlet": self.has_tabi,
 | 
			
		||||
            "Mega Shard of the Moon": self.has_tabi,
 | 
			
		||||
            "Mega Shard of the Sun": self.has_tabi,
 | 
			
		||||
            # riviere turquoise
 | 
			
		||||
            "Fairy Bottle": self.has_vertical,
 | 
			
		||||
            "Riviere Turquoise Seal - Flower Power": self.has_vertical,
 | 
			
		||||
            "Quick Restock Mega Shard 1": self.has_vertical,
 | 
			
		||||
            "Quick Restock Mega Shard 2": self.has_vertical,
 | 
			
		||||
            "Riviere Turquoise Seal - Bounces and Balls": self.can_dboost,
 | 
			
		||||
            "Riviere Turquoise Seal - Launch of Faith": lambda state: self.can_dboost(state) or self.has_dart(state),
 | 
			
		||||
            # elemental skylands
 | 
			
		||||
            "Key of Symbiosis": self.has_dart,
 | 
			
		||||
            "Elemental Skylands - Key of Symbiosis": self.has_dart,
 | 
			
		||||
            "Elemental Skylands Seal - Air": self.has_wingsuit,
 | 
			
		||||
            "Elemental Skylands Seal - Water": self.has_dart,
 | 
			
		||||
            "Elemental Skylands Seal - Fire": self.has_dart,
 | 
			
		||||
            "Elemental Skylands Seal - Water": lambda state: self.has_dart(state) and
 | 
			
		||||
                                                             state.has("Currents Master", self.player),
 | 
			
		||||
            "Elemental Skylands Seal - Fire": lambda state: self.has_dart(state) and self.can_destroy_projectiles(state),
 | 
			
		||||
            "Earth Mega Shard": self.has_dart,
 | 
			
		||||
            "Water Mega Shard": self.has_dart,
 | 
			
		||||
            # corrupted future
 | 
			
		||||
            "Key of Courage": lambda state: state.has_all({"Demon King Crown", "Fairy Bottle"}, self.player),
 | 
			
		||||
            "Corrupted Future - Key of Courage": lambda state: state.has_all({"Demon King Crown", "Magic Firefly"},
 | 
			
		||||
                                                                             self.player),
 | 
			
		||||
            # the shop
 | 
			
		||||
            "Shop Chest": self.has_enough_seals,
 | 
			
		||||
            # tower hq
 | 
			
		||||
            "Money Wrench": self.can_shop,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def has_wingsuit(self, state: CollectionState) -> bool:
 | 
			
		||||
| 
						 | 
				
			
			@ -93,7 +105,7 @@ class MessengerRules:
 | 
			
		|||
        return state.has("Rope Dart", self.player)
 | 
			
		||||
 | 
			
		||||
    def has_tabi(self, state: CollectionState) -> bool:
 | 
			
		||||
        return state.has("Ninja Tabi", self.player)
 | 
			
		||||
        return state.has("Lightfoot Tabi", self.player)
 | 
			
		||||
 | 
			
		||||
    def has_vertical(self, state: CollectionState) -> bool:
 | 
			
		||||
        return self.has_wingsuit(state) or self.has_dart(state)
 | 
			
		||||
| 
						 | 
				
			
			@ -101,10 +113,25 @@ class MessengerRules:
 | 
			
		|||
    def has_enough_seals(self, state: CollectionState) -> bool:
 | 
			
		||||
        return not self.world.required_seals or state.has("Power Seal", self.player, self.world.required_seals)
 | 
			
		||||
 | 
			
		||||
    def can_destroy_projectiles(self, state: CollectionState) -> bool:
 | 
			
		||||
        return state.has("Strike of the Ninja", self.player)
 | 
			
		||||
 | 
			
		||||
    def can_dboost(self, state: CollectionState) -> bool:
 | 
			
		||||
        return state.has_any({"Path of Resilience", "Meditation"}, self.player) and \
 | 
			
		||||
            state.has("Second Wind", self.player)
 | 
			
		||||
    
 | 
			
		||||
    def is_aerobatic(self, state: CollectionState) -> bool:
 | 
			
		||||
        return self.has_wingsuit(state) and state.has("Aerobatics Warrior", self.player)
 | 
			
		||||
 | 
			
		||||
    def true(self, state: CollectionState) -> bool:
 | 
			
		||||
        """I know this is stupid, but it's easier to read in the dicts."""
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    def can_shop(self, state: CollectionState) -> bool:
 | 
			
		||||
        prices = self.world.shop_prices
 | 
			
		||||
        most_expensive_loc = max(prices, key=prices.get)
 | 
			
		||||
        return state.can_reach(f"The Shop - {most_expensive_loc}", "Location", self.player)
 | 
			
		||||
 | 
			
		||||
    def set_messenger_rules(self) -> None:
 | 
			
		||||
        multiworld = self.world.multiworld
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -115,6 +142,9 @@ class MessengerRules:
 | 
			
		|||
            for loc in region.locations:
 | 
			
		||||
                if loc.name in self.location_rules:
 | 
			
		||||
                    loc.access_rule = self.location_rules[loc.name]
 | 
			
		||||
            if region.name == "The Shop":
 | 
			
		||||
                for loc in [location for location in region.locations if isinstance(location, MessengerShopLocation)]:
 | 
			
		||||
                    loc.access_rule = loc.can_afford
 | 
			
		||||
        if multiworld.goal[self.player] == Goal.option_power_seal_hunt:
 | 
			
		||||
            set_rule(multiworld.get_entrance("Tower HQ -> Music Box", self.player),
 | 
			
		||||
                     lambda state: state.has("Shop Chest", self.player))
 | 
			
		||||
| 
						 | 
				
			
			@ -135,29 +165,45 @@ class MessengerHardRules(MessengerRules):
 | 
			
		|||
            "Autumn Hills": self.has_vertical,
 | 
			
		||||
            "Catacombs": self.has_vertical,
 | 
			
		||||
            "Bamboo Creek": self.has_vertical,
 | 
			
		||||
            "Riviere Turquoise": self.true,
 | 
			
		||||
            "Forlorn Temple": lambda state: self.has_vertical(state) and state.has_all(set(PHOBEKINS), self.player),
 | 
			
		||||
            "Searing Crags Upper": self.true,
 | 
			
		||||
            "Glacial Peak": self.true,
 | 
			
		||||
            "Elemental Skylands": lambda state: state.has("Fairy Bottle", self.player) or self.has_windmill(state),
 | 
			
		||||
            "Searing Crags Upper": lambda state: self.can_destroy_projectiles(state) or self.has_windmill(state)
 | 
			
		||||
                                                 or self.has_vertical(state),
 | 
			
		||||
            "Glacial Peak": lambda state: self.can_destroy_projectiles(state) or self.has_windmill(state)
 | 
			
		||||
                                          or self.has_vertical(state),
 | 
			
		||||
            "Elemental Skylands": lambda state: state.has("Magic Firefly", self.player) or
 | 
			
		||||
                                                self.has_windmill(state) or
 | 
			
		||||
                                                self.has_dart(state),
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        self.location_rules.update({
 | 
			
		||||
            "Howling Grotto Seal - Windy Saws and Balls": self.true,
 | 
			
		||||
            "Searing Crags Seal - Triple Ball Spinner": self.true,
 | 
			
		||||
            "Searing Crags Seal - Raining Rocks": lambda state: self.has_vertical(state) or self.can_destroy_projectiles(state),
 | 
			
		||||
            "Searing Crags Seal - Rhythm Rocks": lambda state: self.has_vertical(state) or self.can_destroy_projectiles(state),
 | 
			
		||||
            "Searing Crags - Power Thistle": lambda state: self.has_vertical(state) or self.can_destroy_projectiles(state),
 | 
			
		||||
            "Glacial Peak Seal - Ice Climbers": self.has_vertical,
 | 
			
		||||
            "Glacial Peak Seal - Projectile Spike Pit": self.true,
 | 
			
		||||
            "Claustro": self.has_wingsuit,
 | 
			
		||||
            "Elemental Skylands Seal - Water": self.true,
 | 
			
		||||
            "Elemental Skylands Seal - Fire": self.true,
 | 
			
		||||
            "Earth Mega Shard": self.true,
 | 
			
		||||
            "Water Mega Shard": self.true,
 | 
			
		||||
            "Cloud Ruins Seal - Ghost Pit": self.true,
 | 
			
		||||
            "Bamboo Creek - Claustro": self.has_wingsuit,
 | 
			
		||||
            "Tower of Time Seal - Lantern Climb": self.has_wingsuit,
 | 
			
		||||
            "Elemental Skylands Seal - Water": lambda state: self.has_dart(state) or self.can_dboost(state)
 | 
			
		||||
                                                             or self.has_windmill(state),
 | 
			
		||||
            "Elemental Skylands Seal - Fire": lambda state: (self.has_dart(state) or self.can_dboost(state)
 | 
			
		||||
                                                             or self.has_windmill(state)) and
 | 
			
		||||
                                                            self.can_destroy_projectiles(state),
 | 
			
		||||
            "Earth Mega Shard": lambda state: self.has_dart(state) or self.can_dboost(state) or self.has_windmill(state),
 | 
			
		||||
            "Water Mega Shard": lambda state: self.has_dart(state) or self.can_dboost(state) or self.has_windmill(state),
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        self.extra_rules = {
 | 
			
		||||
            "Key of Strength": lambda state: self.has_dart(state) or self.has_windmill(state),
 | 
			
		||||
            "Key of Symbiosis": self.has_windmill,
 | 
			
		||||
            "Searing Crags - Key of Strength": lambda state: self.has_dart(state) or self.has_windmill(state),
 | 
			
		||||
            "Elemental Skylands - Key of Symbiosis": lambda state: self.has_windmill(state) or self.can_dboost(state),
 | 
			
		||||
            "Autumn Hills Seal - Spike Ball Darts": lambda state: (self.has_dart(state) and self.has_windmill(state))
 | 
			
		||||
                                                                  or self.has_wingsuit(state),
 | 
			
		||||
            "Glacial Peak Seal - Glacial Air Swag": self.has_windmill,
 | 
			
		||||
            "Underworld Seal - Fireball Wave": lambda state: state.has_all({"Ninja Tabi", "Windmill Shuriken"},
 | 
			
		||||
            "Glacial Peak Seal - Ice Climbers": lambda state: self.has_wingsuit(state) or self.can_dboost(state),
 | 
			
		||||
            "Underworld Seal - Fireball Wave": lambda state: state.has_all({"Lightfoot Tabi", "Windmill Shuriken"},
 | 
			
		||||
                                                                           self.player),
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -174,53 +220,31 @@ class MessengerHardRules(MessengerRules):
 | 
			
		|||
            add_rule(self.world.multiworld.get_location(loc, self.player), rule, "or")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MessengerChallengeRules(MessengerHardRules):
 | 
			
		||||
    def __init__(self, world: MessengerWorld) -> None:
 | 
			
		||||
        super().__init__(world)
 | 
			
		||||
 | 
			
		||||
        self.region_rules.update({
 | 
			
		||||
            "Forlorn Temple": lambda state: (self.has_vertical(state) and state.has_all(set(PHOBEKINS), self.player))
 | 
			
		||||
                                            or state.has_all({"Wingsuit", "Windmill Shuriken"}, self.player),
 | 
			
		||||
            "Elemental Skylands": lambda state: self.has_wingsuit(state) or state.has("Fairy Bottle", self.player)
 | 
			
		||||
                                                or self.has_windmill(state),
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        self.location_rules.update({
 | 
			
		||||
            "Fairy Bottle": self.true,
 | 
			
		||||
            "Howling Grotto Seal - Crushing Pits": self.true,
 | 
			
		||||
            "Underworld Seal - Sharp and Windy Climb": self.true,
 | 
			
		||||
            "Riviere Turquoise Seal - Flower Power": self.true,
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        self.extra_rules.update({
 | 
			
		||||
            "Key of Hope": self.has_vertical,
 | 
			
		||||
            "Key of Symbiosis": lambda state: self.has_vertical(state) or self.has_windmill(state),
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MessengerOOBRules(MessengerRules):
 | 
			
		||||
    def __init__(self, world: MessengerWorld) -> None:
 | 
			
		||||
        self.world = world
 | 
			
		||||
        self.player = world.player
 | 
			
		||||
 | 
			
		||||
        self.region_rules = {
 | 
			
		||||
            "Elemental Skylands": lambda state: state.has_any({"Wingsuit", "Rope Dart", "Fairy Bottle"}, self.player),
 | 
			
		||||
            "Music Box": lambda state: state.has_all(set(NOTES), self.player),
 | 
			
		||||
            "Elemental Skylands":
 | 
			
		||||
                lambda state: state.has_any({"Windmill Shuriken", "Wingsuit", "Rope Dart", "Magic Firefly"}, self.player),
 | 
			
		||||
            "Music Box": lambda state: state.has_all(set(NOTES), self.player)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        self.location_rules = {
 | 
			
		||||
            "Claustro": self.has_wingsuit,
 | 
			
		||||
            "Key of Strength": lambda state: self.has_vertical(state) or state.has("Power Thistle", self.player),
 | 
			
		||||
            "Key of Love": lambda state: state.has_all({"Sun Crest", "Moon Crest"}, self.player),
 | 
			
		||||
            "Pyro": self.has_tabi,
 | 
			
		||||
            "Key of Chaos": self.has_tabi,
 | 
			
		||||
            "Key of Courage": lambda state: state.has_all({"Demon King Crown", "Fairy Bottle"}, self.player),
 | 
			
		||||
            "Bamboo Creek - Claustro": self.has_wingsuit,
 | 
			
		||||
            "Searing Crags - Key of Strength": self.has_wingsuit,
 | 
			
		||||
            "Sunken Shrine - Key of Love": lambda state: state.has_all({"Sun Crest", "Moon Crest"}, self.player),
 | 
			
		||||
            "Searing Crags - Pyro": self.has_tabi,
 | 
			
		||||
            "Underworld - Key of Chaos": self.has_tabi,
 | 
			
		||||
            "Corrupted Future - Key of Courage":
 | 
			
		||||
                lambda state: state.has_all({"Demon King Crown", "Magic Firefly"}, self.player),
 | 
			
		||||
            "Autumn Hills Seal - Spike Ball Darts": self.has_dart,
 | 
			
		||||
            "Ninja Village Seal - Tree House": self.has_dart,
 | 
			
		||||
            "Underworld Seal - Fireball Wave": lambda state: state.has_any({"Wingsuit", "Windmill Shuriken"},
 | 
			
		||||
                                                                           self.player),
 | 
			
		||||
            "Tower of Time Seal - Time Waster Seal": self.has_dart,
 | 
			
		||||
            "Shop Chest": self.has_enough_seals,
 | 
			
		||||
            "Tower of Time Seal - Time Waster": self.has_dart,
 | 
			
		||||
            "Shop Chest": self.has_enough_seals
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def set_messenger_rules(self) -> None:
 | 
			
		||||
| 
						 | 
				
			
			@ -231,11 +255,14 @@ class MessengerOOBRules(MessengerRules):
 | 
			
		|||
 | 
			
		||||
def set_self_locking_items(multiworld: MultiWorld, player: int) -> None:
 | 
			
		||||
    # do the ones for seal shuffle on and off first
 | 
			
		||||
    allow_self_locking_items(multiworld.get_location("Key of Strength", player), "Power Thistle")
 | 
			
		||||
    allow_self_locking_items(multiworld.get_location("Key of Love", player), "Sun Crest", "Moon Crest")
 | 
			
		||||
    allow_self_locking_items(multiworld.get_location("Key of Courage", player), "Demon King Crown")
 | 
			
		||||
    allow_self_locking_items(multiworld.get_location("Searing Crags - Key of Strength", player), "Power Thistle")
 | 
			
		||||
    allow_self_locking_items(multiworld.get_location("Sunken Shrine - Key of Love", player), "Sun Crest", "Moon Crest")
 | 
			
		||||
    allow_self_locking_items(multiworld.get_location("Corrupted Future - Key of Courage", player), "Demon King Crown")
 | 
			
		||||
 | 
			
		||||
    # add these locations when seals aren't shuffled
 | 
			
		||||
    if not multiworld.shuffle_seals[player] and not multiworld.shuffle_shards[player]:
 | 
			
		||||
    # add these locations when seals are shuffled
 | 
			
		||||
    if multiworld.shuffle_seals[player]:
 | 
			
		||||
        allow_self_locking_items(multiworld.get_location("Elemental Skylands Seal - Water", player), "Currents Master")
 | 
			
		||||
    # add these locations when seals and shards aren't shuffled
 | 
			
		||||
    elif not multiworld.shuffle_shards[player]:
 | 
			
		||||
        allow_self_locking_items(multiworld.get_region("Cloud Ruins Right", player), "Ruxxtin's Amulet")
 | 
			
		||||
        allow_self_locking_items(multiworld.get_region("Forlorn Temple", player), *PHOBEKINS)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,100 @@
 | 
			
		|||
from random import Random
 | 
			
		||||
from typing import Dict, TYPE_CHECKING, NamedTuple, Tuple, List
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from . import MessengerWorld
 | 
			
		||||
else:
 | 
			
		||||
    MessengerWorld = object
 | 
			
		||||
 | 
			
		||||
PROG_SHOP_ITEMS: List[str] = [
 | 
			
		||||
    "Path of Resilience",
 | 
			
		||||
    "Meditation",
 | 
			
		||||
    "Strike of the Ninja",
 | 
			
		||||
    "Second Wind",
 | 
			
		||||
    "Currents Master",
 | 
			
		||||
    "Aerobatics Warrior",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
USEFUL_SHOP_ITEMS: List[str] = [
 | 
			
		||||
    "Karuta Plates",
 | 
			
		||||
    "Serendipitous Bodies",
 | 
			
		||||
    "Kusari Jacket",
 | 
			
		||||
    "Energy Shuriken",
 | 
			
		||||
    "Serendipitous Minds",
 | 
			
		||||
    "Rejuvenate Spirit",
 | 
			
		||||
    "Demon's Bane",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ShopData(NamedTuple):
 | 
			
		||||
    internal_name: str
 | 
			
		||||
    min_price: int
 | 
			
		||||
    max_price: int
 | 
			
		||||
    default_price: int = 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
SHOP_ITEMS: Dict[str, ShopData] = {
 | 
			
		||||
    "Karuta Plates":        ShopData("HP_UPGRADE_1", 20, 200),
 | 
			
		||||
    "Serendipitous Bodies": ShopData("ENEMY_DROP_HP", 20, 300),
 | 
			
		||||
    "Path of Resilience":   ShopData("DAMAGE_REDUCTION", 100, 500),
 | 
			
		||||
    "Kusari Jacket":        ShopData("HP_UPGRADE_2", 100, 500),
 | 
			
		||||
    "Energy Shuriken":      ShopData("SHURIKEN", 20, 200),
 | 
			
		||||
    "Serendipitous Minds":  ShopData("ENEMY_DROP_MANA", 20, 300),
 | 
			
		||||
    "Prepared Mind":        ShopData("SHURIKEN_UPGRADE_1", 100, 600),
 | 
			
		||||
    "Meditation":           ShopData("CHECKPOINT_FULL", 100, 600),
 | 
			
		||||
    "Rejuvenative Spirit":  ShopData("POTION_FULL_HEAL_AND_HP", 300, 800),
 | 
			
		||||
    "Centered Mind":        ShopData("SHURIKEN_UPGRADE_2", 300, 800),
 | 
			
		||||
    "Strike of the Ninja":  ShopData("ATTACK_PROJECTILE", 20, 200),
 | 
			
		||||
    "Second Wind":          ShopData("AIR_RECOVER", 20, 350),
 | 
			
		||||
    "Currents Master":      ShopData("SWIM_DASH", 100, 600),
 | 
			
		||||
    "Aerobatics Warrior":   ShopData("GLIDE_ATTACK", 300, 800),
 | 
			
		||||
    "Demon's Bane":         ShopData("CHARGED_ATTACK", 400, 1000),
 | 
			
		||||
    "Devil's Due":          ShopData("QUARBLE_DISCOUNT_50", 20, 200),
 | 
			
		||||
    "Time Sense":           ShopData("TIME_WARP", 20, 300),
 | 
			
		||||
    "Power Sense":          ShopData("POWER_SEAL", 100, 800),
 | 
			
		||||
    "Focused Power Sense":  ShopData("POWER_SEAL_WORLD_MAP", 300, 600),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
FIGURINES: Dict[str, ShopData] = {
 | 
			
		||||
    "Green Kappa Figurine":         ShopData("GREEN_KAPPA", 100, 500, 450),
 | 
			
		||||
    "Blue Kappa Figurine":          ShopData("BLUE_KAPPA", 100, 500, 450),
 | 
			
		||||
    "Ountarde Figurine":            ShopData("OUNTARDE", 100, 500, 450),
 | 
			
		||||
    "Red Kappa Figurine":           ShopData("RED_KAPPA", 100, 500, 450),
 | 
			
		||||
    "Demon King Figurine":          ShopData("DEMON_KING", 600, 2000, 2000),
 | 
			
		||||
    "Quillshroom Figurine":         ShopData("QUILLSHROOM", 100, 500, 450),
 | 
			
		||||
    "Jumping Quillshroom Figurine": ShopData("JUMPING_QUILLSHROOM", 100, 500, 450),
 | 
			
		||||
    "Scurubu Figurine":             ShopData("SCURUBU", 100, 500, 450),
 | 
			
		||||
    "Jumping Scurubu Figurine":     ShopData("JUMPING_SCURUBU", 100, 500, 450),
 | 
			
		||||
    "Wallaxer Figurine":            ShopData("WALLAXER", 100, 500, 450),
 | 
			
		||||
    "Barmath'azel Figurine":        ShopData("BARMATHAZEL", 600, 2000, 2000),
 | 
			
		||||
    "Queen of Quills Figurine":     ShopData("QUEEN_OF_QUILLS", 400, 1000, 2000),
 | 
			
		||||
    "Demon Hive Figurine":          ShopData("DEMON_HIVE", 100, 500, 450),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def shuffle_shop_prices(world: MessengerWorld) -> Tuple[Dict[str, int], Dict[str, int]]:
 | 
			
		||||
    shop_price_mod = world.multiworld.shop_price[world.player].value
 | 
			
		||||
    shop_price_planned = world.multiworld.shop_price_plan[world.player]
 | 
			
		||||
    local_random: Random = world.multiworld.per_slot_randoms[world.player]
 | 
			
		||||
 | 
			
		||||
    shop_prices: Dict[str, int] = {}
 | 
			
		||||
    figurine_prices: Dict[str, int] = {}
 | 
			
		||||
    for item, price in shop_price_planned.value.items():
 | 
			
		||||
        if not isinstance(price, int):
 | 
			
		||||
            price = local_random.choices(list(price.keys()), weights=list(price.values()))[0]
 | 
			
		||||
        if "Figurine" in item:
 | 
			
		||||
            figurine_prices[item] = price
 | 
			
		||||
        else:
 | 
			
		||||
            shop_prices[item] = price
 | 
			
		||||
 | 
			
		||||
    remaining_slots = [item for item in [*SHOP_ITEMS, *FIGURINES] if item not in shop_price_planned.value]
 | 
			
		||||
    for shop_item in remaining_slots:
 | 
			
		||||
        shop_data = SHOP_ITEMS.get(shop_item, FIGURINES.get(shop_item))
 | 
			
		||||
        price = local_random.randint(shop_data.min_price, shop_data.max_price)
 | 
			
		||||
        adjusted_price = min(int(price * shop_price_mod / 100), 5000)
 | 
			
		||||
        if "Figurine" in shop_item:
 | 
			
		||||
            figurine_prices[shop_item] = adjusted_price
 | 
			
		||||
        else:
 | 
			
		||||
            shop_prices[shop_item] = adjusted_price
 | 
			
		||||
 | 
			
		||||
    return shop_prices, figurine_prices
 | 
			
		||||
| 
						 | 
				
			
			@ -1,9 +1,10 @@
 | 
			
		|||
from typing import Set, TYPE_CHECKING, Optional, Dict
 | 
			
		||||
 | 
			
		||||
from BaseClasses import Region, Location, Item, ItemClassification, Entrance
 | 
			
		||||
from .Constants import SEALS, NOTES, PROG_ITEMS, PHOBEKINS, USEFUL_ITEMS
 | 
			
		||||
from BaseClasses import Region, Location, Item, ItemClassification, Entrance, CollectionState
 | 
			
		||||
from .Constants import NOTES, PROG_ITEMS, PHOBEKINS, USEFUL_ITEMS
 | 
			
		||||
from .Options import Goal
 | 
			
		||||
from .Regions import REGIONS, MEGA_SHARDS
 | 
			
		||||
from .Regions import REGIONS, SEALS, MEGA_SHARDS
 | 
			
		||||
from .Shop import SHOP_ITEMS, PROG_SHOP_ITEMS, USEFUL_SHOP_ITEMS, FIGURINES
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from . import MessengerWorld
 | 
			
		||||
| 
						 | 
				
			
			@ -20,14 +21,21 @@ class MessengerRegion(Region):
 | 
			
		|||
    def add_locations(self, name_to_id: Dict[str, int]) -> None:
 | 
			
		||||
        for loc in REGIONS[self.name]:
 | 
			
		||||
            self.locations.append(MessengerLocation(loc, self, name_to_id.get(loc, None)))
 | 
			
		||||
        if self.name == "The Shop" and self.multiworld.goal[self.player] > Goal.option_open_music_box:
 | 
			
		||||
            self.locations.append(MessengerLocation("Shop Chest", self, name_to_id.get("Shop Chest", None)))
 | 
			
		||||
        # putting some dumb special case for searing crags and ToT so i can split them into 2 regions
 | 
			
		||||
        if self.multiworld.shuffle_seals[self.player] and self.name not in {"Searing Crags", "Tower HQ", "Cloud Ruins"}:
 | 
			
		||||
            self.locations += [MessengerLocation(seal_loc, self, name_to_id.get(seal_loc, None))
 | 
			
		||||
                               for seal_loc in SEALS if seal_loc.startswith(self.name.split(" ")[0])]
 | 
			
		||||
        if self.name == "The Shop":
 | 
			
		||||
            if self.multiworld.goal[self.player] > Goal.option_open_music_box:
 | 
			
		||||
                self.locations.append(MessengerLocation("Shop Chest", self, None))
 | 
			
		||||
            self.locations += [MessengerShopLocation(f"The Shop - {shop_loc}", self,
 | 
			
		||||
                                                     name_to_id[f"The Shop - {shop_loc}"])
 | 
			
		||||
                               for shop_loc in SHOP_ITEMS]
 | 
			
		||||
            self.locations += [MessengerShopLocation(figurine, self, name_to_id[figurine])
 | 
			
		||||
                               for figurine in FIGURINES]
 | 
			
		||||
        elif self.name == "Tower HQ":
 | 
			
		||||
            self.locations.append(MessengerLocation("Money Wrench", self, name_to_id["Money Wrench"]))
 | 
			
		||||
        if self.multiworld.shuffle_seals[self.player] and self.name in SEALS:
 | 
			
		||||
            self.locations += [MessengerLocation(seal_loc, self, name_to_id[seal_loc])
 | 
			
		||||
                               for seal_loc in SEALS[self.name]]
 | 
			
		||||
        if self.multiworld.shuffle_shards[self.player] and self.name in MEGA_SHARDS:
 | 
			
		||||
            self.locations += [MessengerLocation(shard, self, name_to_id.get(shard, None))
 | 
			
		||||
            self.locations += [MessengerLocation(shard, self, name_to_id[shard])
 | 
			
		||||
                               for shard in MEGA_SHARDS[self.name]]
 | 
			
		||||
 | 
			
		||||
    def add_exits(self, exits: Set[str]) -> None:
 | 
			
		||||
| 
						 | 
				
			
			@ -46,13 +54,33 @@ class MessengerLocation(Location):
 | 
			
		|||
            self.place_locked_item(MessengerItem(name, parent.player, None))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MessengerShopLocation(MessengerLocation):
 | 
			
		||||
    def cost(self) -> int:
 | 
			
		||||
        name = self.name.replace("The Shop - ", "")  # TODO use `remove_prefix` when 3.8 finally gets dropped
 | 
			
		||||
        world: MessengerWorld = self.parent_region.multiworld.worlds[self.player]
 | 
			
		||||
        return world.shop_prices.get(name, world.figurine_prices.get(name))
 | 
			
		||||
 | 
			
		||||
    def can_afford(self, state: CollectionState) -> bool:
 | 
			
		||||
        world: MessengerWorld = state.multiworld.worlds[self.player]
 | 
			
		||||
        cost = self.cost() * 2
 | 
			
		||||
        if cost >= 1000:
 | 
			
		||||
            cost *= 2
 | 
			
		||||
        can_afford = state.has("Shards", self.player, min(cost, world.total_shards))
 | 
			
		||||
        if "Figurine" in self.name:
 | 
			
		||||
            return state.has("Money Wrench", self.player) and can_afford
 | 
			
		||||
        return can_afford
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MessengerItem(Item):
 | 
			
		||||
    game = "The Messenger"
 | 
			
		||||
 | 
			
		||||
    def __init__(self, name: str, player: int, item_id: Optional[int] = None, override_progression: bool = False) -> None:
 | 
			
		||||
        if name in {*NOTES, *PROG_ITEMS, *PHOBEKINS} or item_id is None or override_progression:
 | 
			
		||||
    def __init__(self, name: str, player: int, item_id: Optional[int] = None, override_progression: bool = False,
 | 
			
		||||
                 count: int = 0) -> None:
 | 
			
		||||
        if count:
 | 
			
		||||
            item_class = ItemClassification.progression_skip_balancing
 | 
			
		||||
        elif item_id is None or override_progression or name in {*NOTES, *PROG_ITEMS, *PHOBEKINS, *PROG_SHOP_ITEMS}:
 | 
			
		||||
            item_class = ItemClassification.progression
 | 
			
		||||
        elif name in USEFUL_ITEMS:
 | 
			
		||||
        elif name in {*USEFUL_ITEMS, *USEFUL_SHOP_ITEMS}:
 | 
			
		||||
            item_class = ItemClassification.useful
 | 
			
		||||
        else:
 | 
			
		||||
            item_class = ItemClassification.filler
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +1,12 @@
 | 
			
		|||
import logging
 | 
			
		||||
from typing import Dict, Any, Optional, List
 | 
			
		||||
from typing import Dict, Any, Optional
 | 
			
		||||
 | 
			
		||||
from BaseClasses import Tutorial, ItemClassification, MultiWorld
 | 
			
		||||
from BaseClasses import Tutorial, ItemClassification, CollectionState, Item, MultiWorld
 | 
			
		||||
from worlds.AutoWorld import World, WebWorld
 | 
			
		||||
from .Constants import NOTES, PHOBEKINS, ALL_ITEMS, ALWAYS_LOCATIONS, SEALS, BOSS_LOCATIONS
 | 
			
		||||
from .Constants import NOTES, PHOBEKINS, ALL_ITEMS, ALWAYS_LOCATIONS, BOSS_LOCATIONS, FILLER
 | 
			
		||||
from .Options import messenger_options, NotesNeeded, Goal, PowerSeals, Logic
 | 
			
		||||
from .Regions import REGIONS, REGION_CONNECTIONS, MEGA_SHARDS
 | 
			
		||||
from .Regions import REGIONS, REGION_CONNECTIONS, SEALS, MEGA_SHARDS
 | 
			
		||||
from .Shop import SHOP_ITEMS, shuffle_shop_prices, FIGURINES
 | 
			
		||||
from .SubClasses import MessengerRegion, MessengerItem
 | 
			
		||||
from . import Rules
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -41,7 +42,6 @@ class MessengerWorld(World):
 | 
			
		|||
        "Crest": {"Sun Crest", "Moon Crest"},
 | 
			
		||||
        "Phobe": set(PHOBEKINS),
 | 
			
		||||
        "Phobekin": set(PHOBEKINS),
 | 
			
		||||
        "Shuriken": {"Windmill Shuriken"},
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    option_definitions = messenger_options
 | 
			
		||||
| 
						 | 
				
			
			@ -49,30 +49,35 @@ class MessengerWorld(World):
 | 
			
		|||
    base_offset = 0xADD_000
 | 
			
		||||
    item_name_to_id = {item: item_id
 | 
			
		||||
                       for item_id, item in enumerate(ALL_ITEMS, base_offset)}
 | 
			
		||||
    mega_shard_locs = [shard for region in MEGA_SHARDS for shard in MEGA_SHARDS[region]]
 | 
			
		||||
    seal_locs = [seal for seals in SEALS.values() for seal in seals]
 | 
			
		||||
    mega_shard_locs = [shard for shards in MEGA_SHARDS.values() for shard in shards]
 | 
			
		||||
    shop_locs = [f"The Shop - {shop_loc}" for shop_loc in SHOP_ITEMS]
 | 
			
		||||
    location_name_to_id = {location: location_id
 | 
			
		||||
                           for location_id, location in
 | 
			
		||||
                           enumerate([
 | 
			
		||||
                               *ALWAYS_LOCATIONS,
 | 
			
		||||
                               *SEALS,
 | 
			
		||||
                               *seal_locs,
 | 
			
		||||
                               *mega_shard_locs,
 | 
			
		||||
                               *BOSS_LOCATIONS,
 | 
			
		||||
                               *shop_locs,
 | 
			
		||||
                               *FIGURINES,
 | 
			
		||||
                               "Money Wrench",
 | 
			
		||||
                           ], base_offset)}
 | 
			
		||||
 | 
			
		||||
    data_version = 2
 | 
			
		||||
    required_client_version = (0, 3, 9)
 | 
			
		||||
    data_version = 3
 | 
			
		||||
    required_client_version = (0, 4, 0)
 | 
			
		||||
 | 
			
		||||
    web = MessengerWeb()
 | 
			
		||||
 | 
			
		||||
    total_seals: int = 0
 | 
			
		||||
    required_seals: int = 0
 | 
			
		||||
    total_shards: int
 | 
			
		||||
    shop_prices: Dict[str, int]
 | 
			
		||||
    figurine_prices: Dict[str, int]
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def stage_assert_generate(cls, multiworld: MultiWorld) -> None:
 | 
			
		||||
        for player in multiworld.get_game_players(cls.game):
 | 
			
		||||
            player_name = multiworld.player_name[player] = multiworld.get_player_name(player).replace("_", " ")
 | 
			
		||||
            if not all(c.isalnum() or c in "- " for c in player_name):
 | 
			
		||||
                raise ValueError(f"Player name {player_name} is not alpha-numeric.")
 | 
			
		||||
    def __init__(self, multiworld: MultiWorld, player: int):
 | 
			
		||||
        super().__init__(multiworld, player)
 | 
			
		||||
        self.total_shards = 0
 | 
			
		||||
 | 
			
		||||
    def generate_early(self) -> None:
 | 
			
		||||
        if self.multiworld.goal[self.player] == Goal.option_power_seal_hunt:
 | 
			
		||||
| 
						 | 
				
			
			@ -85,27 +90,32 @@ class MessengerWorld(World):
 | 
			
		|||
                region.add_exits(REGION_CONNECTIONS[region.name])
 | 
			
		||||
 | 
			
		||||
    def create_items(self) -> None:
 | 
			
		||||
        itempool: List[MessengerItem] = []
 | 
			
		||||
        if self.multiworld.goal[self.player] == Goal.option_open_music_box:
 | 
			
		||||
            notes = self.multiworld.random.sample(NOTES, k=len(NOTES))
 | 
			
		||||
            precollected_notes_amount = NotesNeeded.range_end - self.multiworld.notes_needed[self.player]
 | 
			
		||||
            if precollected_notes_amount:
 | 
			
		||||
                for note in notes[:precollected_notes_amount]:
 | 
			
		||||
                    self.multiworld.push_precollected(self.create_item(note))
 | 
			
		||||
            itempool += [self.create_item(note) for note in notes[precollected_notes_amount:]]
 | 
			
		||||
 | 
			
		||||
        itempool += [self.create_item(item)
 | 
			
		||||
        # create items that are always in the item pool
 | 
			
		||||
        itempool = [
 | 
			
		||||
            self.create_item(item)
 | 
			
		||||
            for item in self.item_name_to_id
 | 
			
		||||
            if item not in
 | 
			
		||||
            {
 | 
			
		||||
                         "Power Seal", "Time Shard", *NOTES,
 | 
			
		||||
                "Power Seal", *NOTES,
 | 
			
		||||
                *{collected_item.name for collected_item in self.multiworld.precollected_items[self.player]},
 | 
			
		||||
                         # this is a set and currently won't create items for anything that appears in here at all
 | 
			
		||||
                         # if we get in a position where this can have duplicates of items that aren't Power Seals
 | 
			
		||||
                         # or Time shards, this will need to be redone.
 | 
			
		||||
                     }]
 | 
			
		||||
            } and "Time Shard" not in item
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        if self.multiworld.goal[self.player] == Goal.option_power_seal_hunt:
 | 
			
		||||
        if self.multiworld.goal[self.player] == Goal.option_open_music_box:
 | 
			
		||||
            # make a list of all notes except those in the player's defined starting inventory, and adjust the
 | 
			
		||||
            # amount we need to put in the itempool and precollect based on that
 | 
			
		||||
            notes = [note for note in NOTES if note not in self.multiworld.precollected_items[self.player]]
 | 
			
		||||
            self.multiworld.per_slot_randoms[self.player].shuffle(notes)
 | 
			
		||||
            precollected_notes_amount = NotesNeeded.range_end - \
 | 
			
		||||
                self.multiworld.notes_needed[self.player] - \
 | 
			
		||||
                (len(NOTES) - len(notes))
 | 
			
		||||
            if precollected_notes_amount:
 | 
			
		||||
                for note in notes[:precollected_notes_amount]:
 | 
			
		||||
                    self.multiworld.push_precollected(self.create_item(note))
 | 
			
		||||
                notes = notes[precollected_notes_amount:]
 | 
			
		||||
            itempool += [self.create_item(note) for note in notes]
 | 
			
		||||
 | 
			
		||||
        elif self.multiworld.goal[self.player] == Goal.option_power_seal_hunt:
 | 
			
		||||
            total_seals = min(len(self.multiworld.get_unfilled_locations(self.player)) - len(itempool),
 | 
			
		||||
                              self.multiworld.total_seals[self.player].value)
 | 
			
		||||
            if total_seals < self.total_seals:
 | 
			
		||||
| 
						 | 
				
			
			@ -118,39 +128,41 @@ class MessengerWorld(World):
 | 
			
		|||
                seals[i].classification = ItemClassification.progression_skip_balancing
 | 
			
		||||
            itempool += seals
 | 
			
		||||
 | 
			
		||||
        itempool += [self.create_filler()
 | 
			
		||||
                     for _ in range(len(self.multiworld.get_unfilled_locations(self.player)) - len(itempool))]
 | 
			
		||||
        itempool += [self.create_item(filler_item)
 | 
			
		||||
                     for filler_item in
 | 
			
		||||
                     self.multiworld.random.choices(
 | 
			
		||||
                         list(FILLER),
 | 
			
		||||
                         weights=list(FILLER.values()),
 | 
			
		||||
                         k=len(self.multiworld.get_unfilled_locations(self.player)) - len(itempool)
 | 
			
		||||
                     )]
 | 
			
		||||
 | 
			
		||||
        self.multiworld.itempool += itempool
 | 
			
		||||
 | 
			
		||||
    def set_rules(self) -> None:
 | 
			
		||||
        self.shop_prices, self.figurine_prices = shuffle_shop_prices(self)
 | 
			
		||||
 | 
			
		||||
        logic = self.multiworld.logic_level[self.player]
 | 
			
		||||
        if logic == Logic.option_normal:
 | 
			
		||||
            Rules.MessengerRules(self).set_messenger_rules()
 | 
			
		||||
        elif logic == Logic.option_hard:
 | 
			
		||||
            Rules.MessengerHardRules(self).set_messenger_rules()
 | 
			
		||||
        elif logic == Logic.option_challenging:
 | 
			
		||||
            Rules.MessengerChallengeRules(self).set_messenger_rules()
 | 
			
		||||
        else:
 | 
			
		||||
            Rules.MessengerOOBRules(self).set_messenger_rules()
 | 
			
		||||
 | 
			
		||||
    def fill_slot_data(self) -> Dict[str, Any]:
 | 
			
		||||
        locations: Dict[int, List[str]] = {}
 | 
			
		||||
        for loc in self.multiworld.get_filled_locations(self.player):
 | 
			
		||||
            if loc.item.code:
 | 
			
		||||
                locations[loc.address] = [loc.item.name, self.multiworld.player_name[loc.item.player]]
 | 
			
		||||
        shop_prices = {SHOP_ITEMS[item].internal_name: price for item, price in self.shop_prices.items()}
 | 
			
		||||
        figure_prices = {FIGURINES[item].internal_name: price for item, price in self.figurine_prices.items()}
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            "deathlink": self.multiworld.death_link[self.player].value,
 | 
			
		||||
            "goal": self.multiworld.goal[self.player].current_key,
 | 
			
		||||
            "music_box": self.multiworld.music_box[self.player].value,
 | 
			
		||||
            "required_seals": self.required_seals,
 | 
			
		||||
            "locations": locations,
 | 
			
		||||
            "settings": {
 | 
			
		||||
                "Difficulty": "Basic" if not self.multiworld.shuffle_seals[self.player] else "Advanced",
 | 
			
		||||
                "Mega Shards": self.multiworld.shuffle_shards[self.player].value
 | 
			
		||||
            },
 | 
			
		||||
            "mega_shards": self.multiworld.shuffle_shards[self.player].value,
 | 
			
		||||
            "logic": self.multiworld.logic_level[self.player].current_key,
 | 
			
		||||
            "shop": shop_prices,
 | 
			
		||||
            "figures": figure_prices,
 | 
			
		||||
            "max_price": self.total_shards,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def get_filler_item_name(self) -> str:
 | 
			
		||||
| 
						 | 
				
			
			@ -158,6 +170,21 @@ class MessengerWorld(World):
 | 
			
		|||
 | 
			
		||||
    def create_item(self, name: str) -> MessengerItem:
 | 
			
		||||
        item_id: Optional[int] = self.item_name_to_id.get(name, None)
 | 
			
		||||
        override_prog = name in {"Windmill Shuriken"} and getattr(self, "multiworld") is not None \
 | 
			
		||||
            and self.multiworld.logic_level[self.player] > Logic.option_normal
 | 
			
		||||
        return MessengerItem(name, self.player, item_id, override_prog)
 | 
			
		||||
        override_prog = getattr(self, "multiworld") is not None and \
 | 
			
		||||
            name in {"Windmill Shuriken"} and \
 | 
			
		||||
            self.multiworld.logic_level[self.player] > Logic.option_normal
 | 
			
		||||
        count = 0
 | 
			
		||||
        if "Time Shard " in name:
 | 
			
		||||
            count = int(name.strip("Time Shard ()"))
 | 
			
		||||
            count = count if count >= 100 else 0
 | 
			
		||||
            self.total_shards += count
 | 
			
		||||
        return MessengerItem(name, self.player, item_id, override_prog, count)
 | 
			
		||||
 | 
			
		||||
    def collect_item(self, state: "CollectionState", item: "Item", remove: bool = False) -> Optional[str]:
 | 
			
		||||
        if item.advancement and "Time Shard" in item.name:
 | 
			
		||||
            shard_count = int(item.name.strip("Time Shard ()"))
 | 
			
		||||
            if remove:
 | 
			
		||||
                shard_count = -shard_count
 | 
			
		||||
            state.prog_items["Shards", self.player] += shard_count
 | 
			
		||||
 | 
			
		||||
        return super().collect_item(state, item, remove)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,8 +13,7 @@
 | 
			
		|||
 | 
			
		||||
All items and upgrades that can be picked up by the player in the game are randomized. The player starts in the Tower of
 | 
			
		||||
Time HQ with the past section finished, all area portals open, and with the cloud step, and climbing claws already
 | 
			
		||||
obtained. You'll be forced to do sections of the game in different ways with your current abilities. Currently, logic
 | 
			
		||||
assumes you already have all shop upgrades.
 | 
			
		||||
obtained. You'll be forced to do sections of the game in different ways with your current abilities.
 | 
			
		||||
 | 
			
		||||
## What items can appear in other players' worlds?
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -23,6 +22,7 @@ assumes you already have all shop upgrades.
 | 
			
		|||
* Music Box notes
 | 
			
		||||
* The Phobekins
 | 
			
		||||
* Time shards
 | 
			
		||||
* Shop Upgrades
 | 
			
		||||
* Power Seals
 | 
			
		||||
 | 
			
		||||
## Where can I find items?
 | 
			
		||||
| 
						 | 
				
			
			@ -33,6 +33,7 @@ You can find items wherever items can be picked up in the original game. This in
 | 
			
		|||
* Music Box notes
 | 
			
		||||
* Phobekins
 | 
			
		||||
* Bosses
 | 
			
		||||
* Shop Upgrades, Money Wrench, and Figurine Purchases
 | 
			
		||||
* Power seals
 | 
			
		||||
* Mega Time Shards
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -46,7 +47,6 @@ for it. The groups you can use for The Messenger are:
 | 
			
		|||
* Crest - The Sun and Moon Crests
 | 
			
		||||
* Phobekin - Any of the Phobekins
 | 
			
		||||
* Phobe - An alternative name for the Phobekins
 | 
			
		||||
* Shuriken - The windmill shuriken
 | 
			
		||||
 | 
			
		||||
## Other changes
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -60,11 +60,13 @@ for it. The groups you can use for The Messenger are:
 | 
			
		|||
* Toggle Windmill Shuriken button is added to option menu once the item is received
 | 
			
		||||
* The mod option menu will also have a hint item button, as well as a release and collect button that are all placed when
 | 
			
		||||
  the player fulfills the necessary conditions.
 | 
			
		||||
* After running the game with the mod, a config file (APConfig.toml) will be generated in your game folder that can be
 | 
			
		||||
  used to modify certain settings such as text size and color. This can also be used to specify a player name that can't
 | 
			
		||||
  be entered in game.
 | 
			
		||||
 | 
			
		||||
## Currently known issues
 | 
			
		||||
* Necro cutscene will sometimes not play correctly, but will still reward the item
 | 
			
		||||
## Known issues
 | 
			
		||||
* Ruxxtin Coffin cutscene will sometimes not play correctly, but will still reward the item
 | 
			
		||||
* If you receive the Fairy Bottle while in Quillshroom Marsh, The De-curse Queen cutscene will not play. You can exit
 | 
			
		||||
* If you receive the Magic Firefly while in Quillshroom Marsh, The De-curse Queen cutscene will not play. You can exit
 | 
			
		||||
  to Searing Crags and re-enter to get it to play correctly.
 | 
			
		||||
* Sometimes upon teleporting back to HQ, Ninja will run left and enter a different portal than the one entered by the
 | 
			
		||||
  player. This may also cause a softlock.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -38,10 +38,11 @@
 | 
			
		|||
   * This will set up all of your save slots with new randomizer save files. You can have up to 3 randomizer files at a
 | 
			
		||||
     time, but must do this step again to start new runs afterward.
 | 
			
		||||
4. Enter connection info using the relevant option buttons
 | 
			
		||||
   * **The game is limited to alphanumerical characters and `-` so when entering the host name replace `.` with ` `.
 | 
			
		||||
     Ensure that your player name when generating a settings file follows these constrictions**
 | 
			
		||||
   * **The game is limited to alphanumerical characters and `-` so when entering the host name replace `.` with ` `.**
 | 
			
		||||
   * This defaults to `archipelago.gg` and does not need to be manually changed if connecting to a game hosted on the
 | 
			
		||||
     website.
 | 
			
		||||
   * If using a name that cannot be entered in the in game menus, there is a config file (APConfig.toml) in the game
 | 
			
		||||
     directory. When using this, all connection information must be entered in the file. 
 | 
			
		||||
5. Select the `Connect to Archipelago` button
 | 
			
		||||
6. Navigate to save file selection
 | 
			
		||||
7. Select a new valid randomizer save
 | 
			
		||||
| 
						 | 
				
			
			@ -55,11 +56,3 @@ MultiWorld.
 | 
			
		|||
If the reconnection fails, the message on screen will state you are disconnected. If this happens, you can return to the
 | 
			
		||||
main menu and connect to the server as in [Joining a Multiworld Game](#joining-a-multiworld-game), then load the correct
 | 
			
		||||
save file.
 | 
			
		||||
 | 
			
		||||
## Troubleshooting
 | 
			
		||||
 | 
			
		||||
If you launch the game, and it hangs on the splash screen for more than 30 seconds try these steps:
 | 
			
		||||
1. Close the game and remove `TheMessengerRandomizerAP` from the `Mods` folder.
 | 
			
		||||
2. Launch The Messenger
 | 
			
		||||
3. Delete any save slot
 | 
			
		||||
4. Reinstall the randomizer mod following step 2 of the installation.
 | 
			
		||||
| 
						 | 
				
			
			@ -8,115 +8,131 @@ class AccessTest(MessengerTestBase):
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    def testTabi(self) -> None:
 | 
			
		||||
        """locations that hard require the Ninja Tabi"""
 | 
			
		||||
        locations = ["Pyro", "Key of Chaos", "Underworld Seal - Sharp and Windy Climb", "Underworld Seal - Spike Wall",
 | 
			
		||||
                     "Underworld Seal - Fireball Wave", "Underworld Seal - Rising Fanta", "Sun Crest", "Moon Crest",
 | 
			
		||||
                     "Sunken Shrine Seal - Waterfall Paradise", "Sunken Shrine Seal - Tabi Gauntlet",
 | 
			
		||||
                     "Mega Shard of the Moon", "Mega Shard of the Sun", "Under Entrance Mega Shard",
 | 
			
		||||
                     "Hot Tub Mega Shard", "Projectile Pit Mega Shard"]
 | 
			
		||||
        items = [["Ninja Tabi"]]
 | 
			
		||||
        """locations that hard require the Lightfoot Tabi"""
 | 
			
		||||
        locations = [
 | 
			
		||||
            "Searing Crags - Pyro", "Underworld - Key of Chaos", "Underworld Seal - Sharp and Windy Climb",
 | 
			
		||||
            "Underworld Seal - Spike Wall", "Underworld Seal - Fireball Wave", "Underworld Seal - Rising Fanta",
 | 
			
		||||
            "Sunken Shrine - Sun Crest", "Sunken Shrine - Moon Crest", "Sunken Shrine Seal - Waterfall Paradise",
 | 
			
		||||
            "Sunken Shrine Seal - Tabi Gauntlet", "Mega Shard of the Moon", "Mega Shard of the Sun",
 | 
			
		||||
            "Under Entrance Mega Shard", "Hot Tub Mega Shard", "Projectile Pit Mega Shard"
 | 
			
		||||
        ]
 | 
			
		||||
        items = [["Lightfoot Tabi"]]
 | 
			
		||||
        self.assertAccessDependency(locations, items)
 | 
			
		||||
 | 
			
		||||
    def testDart(self) -> None:
 | 
			
		||||
        """locations that hard require the Rope Dart"""
 | 
			
		||||
        locations = ["Ninja Village Seal - Tree House", "Key of Hope", "Howling Grotto Seal - Crushing Pits",
 | 
			
		||||
                     "Glacial Peak Seal - Ice Climbers", "Tower of Time Seal - Time Waster Seal",
 | 
			
		||||
                     "Tower of Time Seal - Arcane Orbs", "Underworld Seal - Rising Fanta", "Key of Symbiosis",
 | 
			
		||||
                     "Elemental Skylands Seal - Water", "Elemental Skylands Seal - Fire", "Earth Mega Shard",
 | 
			
		||||
                     "Water Mega Shard"]
 | 
			
		||||
        locations = [
 | 
			
		||||
            "Ninja Village Seal - Tree House", "Autumn Hills - Key of Hope", "Howling Grotto Seal - Crushing Pits",
 | 
			
		||||
            "Glacial Peak Seal - Ice Climbers", "Tower of Time Seal - Time Waster", "Tower of Time Seal - Lantern Climb",
 | 
			
		||||
            "Tower of Time Seal - Arcane Orbs", "Cloud Ruins Seal - Ghost Pit", "Underworld Seal - Rising Fanta",
 | 
			
		||||
            "Elemental Skylands - Key of Symbiosis", "Elemental Skylands Seal - Water",
 | 
			
		||||
            "Elemental Skylands Seal - Fire", "Earth Mega Shard", "Water Mega Shard", "Rescue Phantom",
 | 
			
		||||
        ]
 | 
			
		||||
        items = [["Rope Dart"]]
 | 
			
		||||
        self.assertAccessDependency(locations, items)
 | 
			
		||||
 | 
			
		||||
    def testWingsuit(self) -> None:
 | 
			
		||||
        """locations that hard require the Wingsuit"""
 | 
			
		||||
        locations = ["Candle", "Ninja Village Seal - Tree House", "Climbing Claws", "Key of Hope",
 | 
			
		||||
                     "Autumn Hills Seal - Trip Saws", "Autumn Hills Seal - Double Swing Saws",
 | 
			
		||||
                     "Autumn Hills Seal - Spike Ball Swing", "Autumn Hills Seal - Spike Ball Darts", "Necro",
 | 
			
		||||
                     "Ruxxtin's Amulet", "Catacombs Seal - Triple Spike Crushers", "Catacombs Seal - Crusher Gauntlet",
 | 
			
		||||
                     "Catacombs Seal - Dirty Pond", "Claustro", "Acro", "Bamboo Creek Seal - Spike Crushers and Doors",
 | 
			
		||||
                     "Bamboo Creek Seal - Spike Ball Pits", "Bamboo Creek Seal - Spike Crushers and Doors v2",
 | 
			
		||||
                     "Howling Grotto Seal - Crushing Pits", "Howling Grotto Seal - Windy Saws and Balls",
 | 
			
		||||
                     "Tower of Time Seal - Lantern Climb", "Demon King Crown", "Cloud Ruins Seal - Ghost Pit",
 | 
			
		||||
                     "Cloud Ruins Seal - Toothbrush Alley", "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room",
 | 
			
		||||
                     "Tower of Time Seal - Lantern Climb", "Tower of Time Seal - Arcane Orbs",
 | 
			
		||||
                     "Underworld Seal - Sharp and Windy Climb", "Underworld Seal - Fireball Wave",
 | 
			
		||||
                     "Elemental Skylands Seal - Air", "Forlorn Temple Seal - Rocket Maze",
 | 
			
		||||
                     "Forlorn Temple Seal - Rocket Sunset", "Astral Seed", "Astral Tea Leaves",
 | 
			
		||||
                     "Autumn Hills Mega Shard", "Hidden Entrance Mega Shard", "Sunny Day Mega Shard",
 | 
			
		||||
                     "Down Under Mega Shard", "Catacombs Mega Shard", "Above Entrance Mega Shard",
 | 
			
		||||
                     "Abandoned Mega Shard", "Time Loop Mega Shard", "Money Farm Room Mega Shard 1",
 | 
			
		||||
                     "Money Farm Room Mega Shard 2", "Leaf Golem", "Ruxxtin", "Emerald Golem"]
 | 
			
		||||
        locations = [
 | 
			
		||||
            "Ninja Village - Candle", "Ninja Village Seal - Tree House", "Autumn Hills - Climbing Claws",
 | 
			
		||||
            "Autumn Hills - Key of Hope", "Autumn Hills Seal - Trip Saws", "Autumn Hills Seal - Double Swing Saws",
 | 
			
		||||
            "Autumn Hills Seal - Spike Ball Swing", "Autumn Hills Seal - Spike Ball Darts", "Catacombs - Necro",
 | 
			
		||||
            "Catacombs - Ruxxtin's Amulet", "Catacombs Seal - Triple Spike Crushers",
 | 
			
		||||
            "Catacombs Seal - Crusher Gauntlet", "Catacombs Seal - Dirty Pond", "Bamboo Creek - Claustro",
 | 
			
		||||
            "Cloud Ruins - Acro", "Bamboo Creek Seal - Spike Crushers and Doors", "Bamboo Creek Seal - Spike Ball Pits",
 | 
			
		||||
            "Bamboo Creek Seal - Spike Crushers and Doors v2", "Howling Grotto Seal - Crushing Pits",
 | 
			
		||||
            "Howling Grotto Seal - Windy Saws and Balls", "Tower of Time Seal - Lantern Climb",
 | 
			
		||||
            "Forlorn Temple - Demon King", "Cloud Ruins Seal - Ghost Pit", "Cloud Ruins Seal - Toothbrush Alley",
 | 
			
		||||
            "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room", "Tower of Time Seal - Lantern Climb",
 | 
			
		||||
            "Tower of Time Seal - Arcane Orbs", "Underworld Seal - Sharp and Windy Climb",
 | 
			
		||||
            "Underworld Seal - Fireball Wave", "Elemental Skylands Seal - Air", "Elemental Skylands Seal - Water",
 | 
			
		||||
            "Elemental Skylands Seal - Fire", "Elemental Skylands - Key of Symbiosis",
 | 
			
		||||
            "Forlorn Temple Seal - Rocket Maze", "Forlorn Temple Seal - Rocket Sunset", "Ninja Village - Astral Seed",
 | 
			
		||||
            "Searing Crags - Astral Tea Leaves", "Autumn Hills Mega Shard", "Hidden Entrance Mega Shard",
 | 
			
		||||
            "Sunny Day Mega Shard", "Down Under Mega Shard", "Catacombs Mega Shard", "Above Entrance Mega Shard",
 | 
			
		||||
            "Abandoned Mega Shard", "Time Loop Mega Shard", "Earth Mega Shard", "Water Mega Shard",
 | 
			
		||||
            "Money Farm Room Mega Shard 1", "Money Farm Room Mega Shard 2",
 | 
			
		||||
            "Autumn Hills - Leaf Golem", "Catacombs - Ruxxtin", "Howling Grotto - Emerald Golem"
 | 
			
		||||
        ]
 | 
			
		||||
        items = [["Wingsuit"]]
 | 
			
		||||
        self.assertAccessDependency(locations, items)
 | 
			
		||||
 | 
			
		||||
    def testVertical(self) -> None:
 | 
			
		||||
        """locations that require either the Rope Dart or the Wingsuit"""
 | 
			
		||||
        locations = ["Ninja Village Seal - Tree House", "Key of Hope", "Howling Grotto Seal - Crushing Pits",
 | 
			
		||||
                     "Glacial Peak Seal - Ice Climbers", "Tower of Time Seal - Time Waster Seal",
 | 
			
		||||
                     "Underworld Seal - Rising Fanta", "Key of Symbiosis",
 | 
			
		||||
                     "Elemental Skylands Seal - Water", "Elemental Skylands Seal - Fire", "Candle",
 | 
			
		||||
                     "Climbing Claws", "Key of Hope", "Autumn Hills Seal - Trip Saws",
 | 
			
		||||
        locations = [
 | 
			
		||||
            "Ninja Village Seal - Tree House", "Howling Grotto Seal - Crushing Pits",
 | 
			
		||||
            "Glacial Peak Seal - Ice Climbers", "Tower of Time Seal - Time Waster",
 | 
			
		||||
            "Underworld Seal - Rising Fanta", "Elemental Skylands - Key of Symbiosis",
 | 
			
		||||
            "Elemental Skylands Seal - Water", "Elemental Skylands Seal - Fire", "Ninja Village - Candle",
 | 
			
		||||
            "Autumn Hills - Climbing Claws", "Autumn Hills - Key of Hope", "Autumn Hills Seal - Trip Saws",
 | 
			
		||||
            "Autumn Hills Seal - Double Swing Saws", "Autumn Hills Seal - Spike Ball Swing",
 | 
			
		||||
                     "Autumn Hills Seal - Spike Ball Darts", "Necro", "Ruxxtin's Amulet",
 | 
			
		||||
            "Autumn Hills Seal - Spike Ball Darts", "Catacombs - Necro", "Catacombs - Ruxxtin's Amulet",
 | 
			
		||||
            "Catacombs Seal - Triple Spike Crushers", "Catacombs Seal - Crusher Gauntlet",
 | 
			
		||||
                     "Catacombs Seal - Dirty Pond", "Claustro", "Acro", "Bamboo Creek Seal - Spike Crushers and Doors",
 | 
			
		||||
                     "Bamboo Creek Seal - Spike Ball Pits", "Bamboo Creek Seal - Spike Crushers and Doors v2",
 | 
			
		||||
                     "Howling Grotto Seal - Crushing Pits", "Howling Grotto Seal - Windy Saws and Balls",
 | 
			
		||||
                     "Demon King Crown", "Cloud Ruins Seal - Ghost Pit", "Cloud Ruins Seal - Toothbrush Alley",
 | 
			
		||||
                     "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room",
 | 
			
		||||
            "Catacombs Seal - Dirty Pond", "Bamboo Creek - Claustro", "Cloud Ruins - Acro",
 | 
			
		||||
            "Bamboo Creek Seal - Spike Crushers and Doors", "Bamboo Creek Seal - Spike Ball Pits",
 | 
			
		||||
            "Bamboo Creek Seal - Spike Crushers and Doors v2", "Howling Grotto Seal - Crushing Pits",
 | 
			
		||||
            "Howling Grotto Seal - Windy Saws and Balls", "Forlorn Temple - Demon King", "Cloud Ruins Seal - Ghost Pit",
 | 
			
		||||
            "Cloud Ruins Seal - Toothbrush Alley", "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room",
 | 
			
		||||
            "Tower of Time Seal - Lantern Climb", "Tower of Time Seal - Arcane Orbs",
 | 
			
		||||
            "Underworld Seal - Sharp and Windy Climb", "Underworld Seal - Fireball Wave",
 | 
			
		||||
                     "Elemental Skylands Seal - Air", "Forlorn Temple Seal - Rocket Maze",
 | 
			
		||||
                     "Forlorn Temple Seal - Rocket Sunset", "Power Thistle", "Key of Strength",
 | 
			
		||||
            "Elemental Skylands Seal - Air", "Forlorn Temple Seal - Rocket Maze", "Forlorn Temple Seal - Rocket Sunset",
 | 
			
		||||
            "Searing Crags - Power Thistle", "Searing Crags - Key of Strength",
 | 
			
		||||
            "Glacial Peak Seal - Projectile Spike Pit", "Glacial Peak Seal - Glacial Air Swag",
 | 
			
		||||
                     "Fairy Bottle", "Riviere Turquoise Seal - Flower Power", "Searing Crags Seal - Triple Ball Spinner",
 | 
			
		||||
                     "Searing Crags Seal - Raining Rocks", "Searing Crags Seal - Rhythm Rocks", "Astral Seed",
 | 
			
		||||
                     "Astral Tea Leaves", "Rescue Phantom", "Autumn Hills Mega Shard", "Hidden Entrance Mega Shard",
 | 
			
		||||
                     "Sunny Day Mega Shard", "Down Under Mega Shard", "Catacombs Mega Shard",
 | 
			
		||||
                     "Above Entrance Mega Shard", "Abandoned Mega Shard", "Time Loop Mega Shard",
 | 
			
		||||
                     "Searing Crags Mega Shard", "Glacial Peak Mega Shard", "Cloud Entrance Mega Shard",
 | 
			
		||||
            "Riviere Turquoise - Butterfly Matriarch", "Riviere Turquoise Seal - Flower Power",
 | 
			
		||||
            "Riviere Turquoise Seal - Launch of Faith",
 | 
			
		||||
            "Searing Crags Seal - Triple Ball Spinner", "Searing Crags Seal - Raining Rocks",
 | 
			
		||||
            "Searing Crags Seal - Rhythm Rocks", "Ninja Village - Astral Seed", "Searing Crags - Astral Tea Leaves",
 | 
			
		||||
            "Rescue Phantom", "Autumn Hills Mega Shard", "Hidden Entrance Mega Shard", "Sunny Day Mega Shard",
 | 
			
		||||
            "Down Under Mega Shard", "Catacombs Mega Shard", "Above Entrance Mega Shard", "Abandoned Mega Shard",
 | 
			
		||||
            "Time Loop Mega Shard", "Searing Crags Mega Shard", "Glacial Peak Mega Shard", "Cloud Entrance Mega Shard",
 | 
			
		||||
            "Time Warp Mega Shard", "Money Farm Room Mega Shard 1", "Money Farm Room Mega Shard 2",
 | 
			
		||||
            "Quick Restock Mega Shard 1", "Quick Restock Mega Shard 2", "Earth Mega Shard", "Water Mega Shard",
 | 
			
		||||
                     "Leaf Golem", "Ruxxtin", "Emerald Golem"]
 | 
			
		||||
            "Autumn Hills - Leaf Golem", "Catacombs - Ruxxtin", "Howling Grotto - Emerald Golem"
 | 
			
		||||
        ]
 | 
			
		||||
        items = [["Wingsuit", "Rope Dart"]]
 | 
			
		||||
        self.assertAccessDependency(locations, items)
 | 
			
		||||
 | 
			
		||||
    def testAmulet(self) -> None:
 | 
			
		||||
        """Locations that require Ruxxtin's Amulet"""
 | 
			
		||||
        locations = ["Acro", "Cloud Ruins Seal - Ghost Pit", "Cloud Ruins Seal - Toothbrush Alley",
 | 
			
		||||
        locations = [
 | 
			
		||||
            "Cloud Ruins - Acro", "Cloud Ruins Seal - Ghost Pit", "Cloud Ruins Seal - Toothbrush Alley",
 | 
			
		||||
            "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room", "Cloud Entrance Mega Shard",
 | 
			
		||||
                     "Time Warp Mega Shard", "Money Farm Room Mega Shard 1", "Money Farm Room Mega Shard 2"]
 | 
			
		||||
            "Time Warp Mega Shard", "Money Farm Room Mega Shard 1", "Money Farm Room Mega Shard 2"
 | 
			
		||||
        ]
 | 
			
		||||
        # Cloud Ruins requires Ruxxtin's Amulet
 | 
			
		||||
        items = [["Ruxxtin's Amulet"]]
 | 
			
		||||
        self.assertAccessDependency(locations, items)
 | 
			
		||||
 | 
			
		||||
    def testBottle(self) -> None:
 | 
			
		||||
        """Elemental Skylands and Corrupted Future require the Fairy Bottle"""
 | 
			
		||||
        locations = ["Key of Symbiosis", "Elemental Skylands Seal - Air", "Elemental Skylands Seal - Fire",
 | 
			
		||||
                     "Elemental Skylands Seal - Water", "Key of Courage", "Earth Mega Shard", "Water Mega Shard"]
 | 
			
		||||
        items = [["Fairy Bottle"]]
 | 
			
		||||
    def testFirefly(self) -> None:
 | 
			
		||||
        """Elemental Skylands and Corrupted Future require the Magic Firefly"""
 | 
			
		||||
        locations = [
 | 
			
		||||
            "Elemental Skylands - Key of Symbiosis", "Elemental Skylands Seal - Air", "Elemental Skylands Seal - Fire",
 | 
			
		||||
            "Elemental Skylands Seal - Water", "Corrupted Future - Key of Courage", "Earth Mega Shard",
 | 
			
		||||
            "Water Mega Shard"
 | 
			
		||||
        ]
 | 
			
		||||
        items = [["Magic Firefly"]]
 | 
			
		||||
        self.assertAccessDependency(locations, items)
 | 
			
		||||
 | 
			
		||||
    def testCrests(self) -> None:
 | 
			
		||||
        """Test Key of Love nonsense"""
 | 
			
		||||
        locations = ["Key of Love"]
 | 
			
		||||
        locations = ["Sunken Shrine - Key of Love"]
 | 
			
		||||
        items = [["Sun Crest", "Moon Crest"]]
 | 
			
		||||
        self.assertAccessDependency(locations, items)
 | 
			
		||||
        self.collect_all_but("Sun Crest")
 | 
			
		||||
        self.assertEqual(self.can_reach_location("Key of Love"), False)
 | 
			
		||||
        self.assertEqual(self.can_reach_location("Sunken Shrine - Key of Love"), False)
 | 
			
		||||
        self.remove(self.get_item_by_name("Moon Crest"))
 | 
			
		||||
        self.collect_by_name("Sun Crest")
 | 
			
		||||
        self.assertEqual(self.can_reach_location("Key of Love"), False)
 | 
			
		||||
        self.assertEqual(self.can_reach_location("Sunken Shrine - Key of Love"), False)
 | 
			
		||||
 | 
			
		||||
    def testThistle(self) -> None:
 | 
			
		||||
        """I'm a chuckster!"""
 | 
			
		||||
        locations = ["Key of Strength"]
 | 
			
		||||
        locations = ["Searing Crags - Key of Strength"]
 | 
			
		||||
        items = [["Power Thistle"]]
 | 
			
		||||
        self.assertAccessDependency(locations, items)
 | 
			
		||||
 | 
			
		||||
    def testCrown(self) -> None:
 | 
			
		||||
        """Crocomire but not"""
 | 
			
		||||
        locations = ["Key of Courage"]
 | 
			
		||||
        locations = ["Corrupted Future - Key of Courage"]
 | 
			
		||||
        items = [["Demon King Crown"]]
 | 
			
		||||
        self.assertAccessDependency(locations, items)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -140,11 +156,11 @@ class ItemsAccessTest(MessengerTestBase):
 | 
			
		|||
    def testSelfLockingItems(self) -> None:
 | 
			
		||||
        """Force items that can be self locked to ensure it's valid placement."""
 | 
			
		||||
        location_lock_pairs = {
 | 
			
		||||
            "Key of Strength": ["Power Thistle"],
 | 
			
		||||
            "Key of Love": ["Sun Crest", "Moon Crest"],
 | 
			
		||||
            "Key of Courage": ["Demon King Crown"],
 | 
			
		||||
            "Acro": ["Ruxxtin's Amulet"],
 | 
			
		||||
            "Demon King Crown": PHOBEKINS
 | 
			
		||||
            "Searing Crags - Key of Strength": ["Power Thistle"],
 | 
			
		||||
            "Sunken Shrine - Key of Love": ["Sun Crest", "Moon Crest"],
 | 
			
		||||
            "Corrupted Future - Key of Courage": ["Demon King Crown"],
 | 
			
		||||
            "Cloud Ruins - Acro": ["Ruxxtin's Amulet"],
 | 
			
		||||
            "Forlorn Temple - Demon King": PHOBEKINS
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for loc in location_lock_pairs:
 | 
			
		||||
| 
						 | 
				
			
			@ -152,4 +168,3 @@ class ItemsAccessTest(MessengerTestBase):
 | 
			
		|||
                item = self.get_item_by_name(item_name)
 | 
			
		||||
                with self.subTest("Fulfills Accessibility", location=loc, item=item_name):
 | 
			
		||||
                    self.assertTrue(self.multiworld.get_location(loc, self.player).can_fill(self.multiworld.state, item, True))
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,35 +11,33 @@ class HardLogicTest(MessengerTestBase):
 | 
			
		|||
        """Test the locations that still require wingsuit or rope dart."""
 | 
			
		||||
        locations = [
 | 
			
		||||
            # tower of time
 | 
			
		||||
            "Tower of Time Seal - Time Waster Seal", "Tower of Time Seal - Lantern Climb",
 | 
			
		||||
            "Tower of Time Seal - Time Waster", "Tower of Time Seal - Lantern Climb",
 | 
			
		||||
            "Tower of Time Seal - Arcane Orbs",
 | 
			
		||||
            # ninja village
 | 
			
		||||
            "Candle", "Astral Seed", "Ninja Village Seal - Tree House", "Astral Tea Leaves",
 | 
			
		||||
            "Ninja Village - Candle", "Ninja Village - Astral Seed", "Ninja Village Seal - Tree House",
 | 
			
		||||
            # autumn hills
 | 
			
		||||
            "Climbing Claws", "Key of Hope", "Leaf Golem",
 | 
			
		||||
            "Autumn Hills - Climbing Claws", "Autumn Hills - Key of Hope", "Autumn Hills - Leaf Golem",
 | 
			
		||||
            "Autumn Hills Seal - Trip Saws", "Autumn Hills Seal - Double Swing Saws",
 | 
			
		||||
            "Autumn Hills Seal - Spike Ball Swing", "Autumn Hills Seal - Spike Ball Darts",
 | 
			
		||||
            # forlorn temple
 | 
			
		||||
            "Demon King Crown",
 | 
			
		||||
            "Forlorn Temple - Demon King",
 | 
			
		||||
            "Forlorn Temple Seal - Rocket Maze", "Forlorn Temple Seal - Rocket Sunset",
 | 
			
		||||
            # catacombs
 | 
			
		||||
            "Necro", "Ruxxtin's Amulet", "Ruxxtin",
 | 
			
		||||
            "Catacombs - Necro", "Catacombs - Ruxxtin's Amulet", "Catacombs - Ruxxtin",
 | 
			
		||||
            "Catacombs Seal - Triple Spike Crushers", "Catacombs Seal - Crusher Gauntlet", "Catacombs Seal - Dirty Pond",
 | 
			
		||||
            # bamboo creek
 | 
			
		||||
            "Claustro",
 | 
			
		||||
            "Bamboo Creek - Claustro",
 | 
			
		||||
            "Bamboo Creek Seal - Spike Crushers and Doors", "Bamboo Creek Seal - Spike Ball Pits",
 | 
			
		||||
            "Bamboo Creek Seal - Spike Crushers and Doors v2",
 | 
			
		||||
            # howling grotto
 | 
			
		||||
            "Emerald Golem", "Howling Grotto Seal - Crushing Pits", "Howling Grotto Seal - Crushing Pits",
 | 
			
		||||
            # glacial peak
 | 
			
		||||
            "Glacial Peak Seal - Ice Climbers",
 | 
			
		||||
            "Howling Grotto - Emerald Golem", "Howling Grotto Seal - Crushing Pits", "Howling Grotto Seal - Crushing Pits",
 | 
			
		||||
            # searing crags
 | 
			
		||||
            "Searing Crags - Astral Tea Leaves",
 | 
			
		||||
            # cloud ruins
 | 
			
		||||
            "Acro", "Cloud Ruins Seal - Ghost Pit",
 | 
			
		||||
            "Cloud Ruins - Acro", "Cloud Ruins Seal - Ghost Pit",
 | 
			
		||||
            "Cloud Ruins Seal - Toothbrush Alley", "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room",
 | 
			
		||||
            # underworld
 | 
			
		||||
            "Underworld Seal - Rising Fanta", "Underworld Seal - Sharp and Windy Climb",
 | 
			
		||||
            # riviere turquoise
 | 
			
		||||
            "Fairy Bottle", "Riviere Turquoise Seal - Flower Power",
 | 
			
		||||
            # elemental skylands
 | 
			
		||||
            "Elemental Skylands Seal - Air",
 | 
			
		||||
            # phantom
 | 
			
		||||
| 
						 | 
				
			
			@ -52,15 +50,15 @@ class HardLogicTest(MessengerTestBase):
 | 
			
		|||
        """Windmill Shuriken isn't progression on normal difficulty, so test it's marked correctly and required."""
 | 
			
		||||
        self.assertEqual(ItemClassification.progression, self.get_item_by_name("Windmill Shuriken").classification)
 | 
			
		||||
        windmill_locs = [
 | 
			
		||||
            "Key of Strength",
 | 
			
		||||
            "Key of Symbiosis",
 | 
			
		||||
            "Searing Crags - Key of Strength",
 | 
			
		||||
            "Elemental Skylands - Key of Symbiosis",
 | 
			
		||||
            "Underworld Seal - Fireball Wave",
 | 
			
		||||
        ]
 | 
			
		||||
        for loc in windmill_locs:
 | 
			
		||||
            with self.subTest("can't reach location with nothing", location=loc):
 | 
			
		||||
                self.assertFalse(self.can_reach_location(loc))
 | 
			
		||||
 | 
			
		||||
        items = self.get_items_by_name(["Windmill Shuriken", "Ninja Tabi", "Fairy Bottle"])
 | 
			
		||||
        items = self.get_items_by_name(["Windmill Shuriken", "Lightfoot Tabi", "Magic Firefly"])
 | 
			
		||||
        self.collect(items)
 | 
			
		||||
        for loc in windmill_locs:
 | 
			
		||||
            with self.subTest("can reach with Windmill", location=loc):
 | 
			
		||||
| 
						 | 
				
			
			@ -77,13 +75,6 @@ class HardLogicTest(MessengerTestBase):
 | 
			
		|||
        self.assertTrue(self.can_reach_location(special_loc))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ChallengingLogicTest(MessengerTestBase):
 | 
			
		||||
    options = {
 | 
			
		||||
        "shuffle_seals": "false",
 | 
			
		||||
        "logic_level": "challenging",
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NoLogicTest(MessengerTestBase):
 | 
			
		||||
    options = {
 | 
			
		||||
        "logic_level": "oob",
 | 
			
		||||
| 
						 | 
				
			
			@ -92,17 +83,14 @@ class NoLogicTest(MessengerTestBase):
 | 
			
		|||
    def testAccess(self) -> None:
 | 
			
		||||
        """Test the locations with rules still require things."""
 | 
			
		||||
        all_locations = [
 | 
			
		||||
            "Claustro", "Key of Strength", "Key of Symbiosis", "Key of Love", "Pyro", "Key of Chaos", "Key of Courage",
 | 
			
		||||
            "Autumn Hills Seal - Spike Ball Darts", "Ninja Village Seal - Tree House", "Underworld Seal - Fireball Wave",
 | 
			
		||||
            "Tower of Time Seal - Time Waster Seal", "Rescue Phantom", "Elemental Skylands Seal - Air",
 | 
			
		||||
            "Elemental Skylands Seal - Water", "Elemental Skylands Seal - Fire",
 | 
			
		||||
            "Bamboo Creek - Claustro", "Searing Crags - Key of Strength", "Elemental Skylands - Key of Symbiosis",
 | 
			
		||||
            "Sunken Shrine - Key of Love", "Searing Crags - Pyro", "Underworld - Key of Chaos",
 | 
			
		||||
            "Corrupted Future - Key of Courage", "Autumn Hills Seal - Spike Ball Darts",
 | 
			
		||||
            "Ninja Village Seal - Tree House", "Underworld Seal - Fireball Wave", "Tower of Time Seal - Time Waster",
 | 
			
		||||
            "Rescue Phantom", "Elemental Skylands Seal - Air", "Elemental Skylands Seal - Water",
 | 
			
		||||
            "Elemental Skylands Seal - Fire",
 | 
			
		||||
        ]
 | 
			
		||||
        for loc in all_locations:
 | 
			
		||||
            with self.subTest("Default unreachables", location=loc):
 | 
			
		||||
                self.assertFalse(self.can_reach_location(loc))
 | 
			
		||||
 | 
			
		||||
    def testNoLogic(self) -> None:
 | 
			
		||||
        """Test some funny locations to make sure they aren't reachable, but we can still win"""
 | 
			
		||||
        self.assertEqual(self.can_reach_location("Pyro"), False)
 | 
			
		||||
        self.assertEqual(self.can_reach_location("Rescue Phantom"), False)
 | 
			
		||||
        self.assertBeatable(True)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,101 @@
 | 
			
		|||
from typing import Dict
 | 
			
		||||
 | 
			
		||||
from . import MessengerTestBase
 | 
			
		||||
from ..Shop import SHOP_ITEMS, FIGURINES
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ShopCostTest(MessengerTestBase):
 | 
			
		||||
    options = {
 | 
			
		||||
        "shop_price": "random",
 | 
			
		||||
        "shuffle_shards": "true",
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    def testShopRules(self) -> None:
 | 
			
		||||
        for loc in SHOP_ITEMS:
 | 
			
		||||
            loc = f"The Shop - {loc}"
 | 
			
		||||
            with self.subTest("has cost", loc=loc):
 | 
			
		||||
                self.assertFalse(self.can_reach_location(loc))
 | 
			
		||||
 | 
			
		||||
        prices: Dict[str, int] = self.multiworld.worlds[self.player].shop_prices
 | 
			
		||||
        for loc, price in prices.items():
 | 
			
		||||
            with self.subTest("prices", loc=loc):
 | 
			
		||||
                self.assertEqual(price, self.multiworld.get_location(f"The Shop - {loc}", self.player).cost())
 | 
			
		||||
                self.assertTrue(loc in SHOP_ITEMS)
 | 
			
		||||
        self.assertEqual(len(prices), len(SHOP_ITEMS))
 | 
			
		||||
 | 
			
		||||
    def testDBoost(self) -> None:
 | 
			
		||||
        locations = [
 | 
			
		||||
            "Riviere Turquoise Seal - Bounces and Balls",
 | 
			
		||||
            "Forlorn Temple - Demon King", "Forlorn Temple Seal - Rocket Maze", "Forlorn Temple Seal - Rocket Sunset",
 | 
			
		||||
            "Sunny Day Mega Shard", "Down Under Mega Shard",
 | 
			
		||||
        ]
 | 
			
		||||
        items = [["Path of Resilience", "Meditation", "Second Wind"]]
 | 
			
		||||
        self.assertAccessDependency(locations, items)
 | 
			
		||||
 | 
			
		||||
    def testCurrents(self) -> None:
 | 
			
		||||
        self.assertAccessDependency(["Elemental Skylands Seal - Water"], [["Currents Master"]])
 | 
			
		||||
 | 
			
		||||
    def testStrike(self) -> None:
 | 
			
		||||
        locations = [
 | 
			
		||||
            "Glacial Peak Seal - Projectile Spike Pit", "Elemental Skylands Seal - Fire",
 | 
			
		||||
        ]
 | 
			
		||||
        items = [["Strike of the Ninja"]]
 | 
			
		||||
        self.assertAccessDependency(locations, items)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ShopCostMinTest(ShopCostTest):
 | 
			
		||||
    options = {
 | 
			
		||||
        "shop_price": "random",
 | 
			
		||||
        "shuffle_seals": "false",
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    def testDBoost(self) -> None:
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def testCurrents(self) -> None:
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def testStrike(self) -> None:
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PlandoTest(MessengerTestBase):
 | 
			
		||||
    options = {
 | 
			
		||||
        "shop_price_plan": {
 | 
			
		||||
            "Karuta Plates": 50,
 | 
			
		||||
            "Serendipitous Bodies": {100: 1, 200: 1, 300: 1},
 | 
			
		||||
            "Barmath'azel Figurine": 500,
 | 
			
		||||
            "Demon Hive Figurine": {100: 1, 200: 2, 300: 1},
 | 
			
		||||
        },
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    def testCosts(self) -> None:
 | 
			
		||||
        for loc in SHOP_ITEMS:
 | 
			
		||||
            loc = f"The Shop - {loc}"
 | 
			
		||||
            with self.subTest("has cost", loc=loc):
 | 
			
		||||
                self.assertFalse(self.can_reach_location(loc))
 | 
			
		||||
 | 
			
		||||
        prices = self.multiworld.worlds[self.player].shop_prices
 | 
			
		||||
        for loc, price in prices.items():
 | 
			
		||||
            with self.subTest("prices", loc=loc):
 | 
			
		||||
                if loc == "Karuta Plates":
 | 
			
		||||
                    self.assertEqual(self.options["shop_price_plan"]["Karuta Plates"], price)
 | 
			
		||||
                elif loc == "Serendipitous Bodies":
 | 
			
		||||
                    self.assertIn(price, self.options["shop_price_plan"]["Serendipitous Bodies"])
 | 
			
		||||
 | 
			
		||||
                loc = f"The Shop - {loc}"
 | 
			
		||||
                self.assertEqual(price, self.multiworld.get_location(loc, self.player).cost())
 | 
			
		||||
                self.assertTrue(loc.replace("The Shop - ", "") in SHOP_ITEMS)
 | 
			
		||||
        self.assertEqual(len(prices), len(SHOP_ITEMS))
 | 
			
		||||
 | 
			
		||||
        figures = self.multiworld.worlds[self.player].figurine_prices
 | 
			
		||||
        for loc, price in figures.items():
 | 
			
		||||
            with self.subTest("figure prices", loc=loc):
 | 
			
		||||
                if loc == "Barmath'azel Figurine":
 | 
			
		||||
                    self.assertEqual(self.options["shop_price_plan"]["Barmath'azel Figurine"], price)
 | 
			
		||||
                elif loc == "Demon Hive Figurine":
 | 
			
		||||
                    self.assertIn(price, self.options["shop_price_plan"]["Demon Hive Figurine"])
 | 
			
		||||
 | 
			
		||||
                self.assertEqual(price, self.multiworld.get_location(loc, self.player).cost())
 | 
			
		||||
                self.assertTrue(loc in FIGURINES)
 | 
			
		||||
        self.assertEqual(len(figures), len(FIGURINES))
 | 
			
		||||
| 
						 | 
				
			
			@ -2,18 +2,6 @@ from BaseClasses import ItemClassification, CollectionState
 | 
			
		|||
from . import MessengerTestBase
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NoLogicTest(MessengerTestBase):
 | 
			
		||||
    options = {
 | 
			
		||||
        "logic_level": "oob",
 | 
			
		||||
        "goal": "power_seal_hunt",
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    def testChestAccess(self) -> None:
 | 
			
		||||
        """Test to make sure we can win even though we can't reach the chest."""
 | 
			
		||||
        self.assertEqual(self.can_reach_location("Shop Chest"), False)
 | 
			
		||||
        self.assertBeatable(True)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AllSealsRequired(MessengerTestBase):
 | 
			
		||||
    options = {
 | 
			
		||||
        "shuffle_seals": "false",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue