diff --git a/worlds/messenger/Constants.py b/worlds/messenger/Constants.py index 643c3f832764..121584da0555 100644 --- a/worlds/messenger/Constants.py +++ b/worlds/messenger/Constants.py @@ -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 @@ 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 @@ "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 @@ "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 @@ # 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", ] diff --git a/worlds/messenger/Options.py b/worlds/messenger/Options.py index f45a41878f5f..8e8b61a2049c 100644 --- a/worlds/messenger/Options.py +++ b/worlds/messenger/Options.py @@ -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 @@ class RequiredSeals(Range): "notes_needed": NotesNeeded, "total_seals": AmountSeals, "percent_seals_required": RequiredSeals, + "shop_price": ShopPrices, + "shop_price_plan": PlannedShopPrices, "death_link": DeathLink, } diff --git a/worlds/messenger/Regions.py b/worlds/messenger/Regions.py index 8a9cfffb56ea..2bfd3cab8433 100644 --- a/worlds/messenger/Regions.py +++ b/worlds/messenger/Regions.py @@ -5,27 +5,60 @@ "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 @@ "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 @@ "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(), diff --git a/worlds/messenger/Rules.py b/worlds/messenger/Rules.py index c171eddfdd34..b72d454a7e0f 100644 --- a/worlds/messenger/Rules.py +++ b/worlds/messenger/Rules.py @@ -4,6 +4,7 @@ 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 @@ def __init__(self, world: MessengerWorld) -> None: "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 @@ def has_dart(self, state: CollectionState) -> bool: 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 @@ def has_vertical(self, state: CollectionState) -> bool: 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 @@ def set_messenger_rules(self) -> None: 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 @@ def __init__(self, world: MessengerWorld) -> None: "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 @@ def set_messenger_rules(self) -> None: 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 @@ def set_messenger_rules(self) -> None: 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") - - # add these locations when seals aren't shuffled - if not multiworld.shuffle_seals[player] and not multiworld.shuffle_shards[player]: + 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 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) diff --git a/worlds/messenger/Shop.py b/worlds/messenger/Shop.py new file mode 100644 index 000000000000..99f9a4436119 --- /dev/null +++ b/worlds/messenger/Shop.py @@ -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 diff --git a/worlds/messenger/SubClasses.py b/worlds/messenger/SubClasses.py index 3daf183e0b70..c83f8f5e0892 100644 --- a/worlds/messenger/SubClasses.py +++ b/worlds/messenger/SubClasses.py @@ -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 @@ def __init__(self, name: str, world: MessengerWorld) -> None: 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 @@ def __init__(self, name: str, parent: MessengerRegion, loc_id: Optional[int]) -> 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 diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index 919bdba66f26..a29a6936a900 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -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 - - @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.") + total_shards: int + shop_prices: Dict[str, int] + figurine_prices: Dict[str, int] + + 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 @@ def create_regions(self) -> None: region.add_exits(REGION_CONNECTIONS[region.name]) def create_items(self) -> None: - itempool: List[MessengerItem] = [] + # 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", *NOTES, + *{collected_item.name for collected_item in self.multiworld.precollected_items[self.player]}, + } and "Time Shard" not in item + ] + 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] + # 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)) - itempool += [self.create_item(note) for note in notes[precollected_notes_amount:]] - - itempool += [self.create_item(item) - for item in self.item_name_to_id - if item not in - { - "Power Seal", "Time Shard", *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. - }] + notes = notes[precollected_notes_amount:] + itempool += [self.create_item(note) for note in notes] - if self.multiworld.goal[self.player] == Goal.option_power_seal_hunt: + 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 @@ def create_items(self) -> None: 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 @@ def get_filler_item_name(self) -> str: 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) diff --git a/worlds/messenger/docs/en_The Messenger.md b/worlds/messenger/docs/en_The Messenger.md index 64b0dd73ae15..422195db2dae 100644 --- a/worlds/messenger/docs/en_The Messenger.md +++ b/worlds/messenger/docs/en_The Messenger.md @@ -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. @@ -73,5 +75,5 @@ for it. The groups you can use for The Messenger are: ## What do I do if I have a problem? -If you believe something happened that isn't intended, please get the `log.txt`from the folder of your game installation +If you believe something happened that isn't intended, please get the `log.txt` from the folder of your game installation and send a bug report either on GitHub or the [Archipelago Discord Server](http://archipelago.gg/discord) diff --git a/worlds/messenger/docs/setup_en.md b/worlds/messenger/docs/setup_en.md index 0a57c2b83f31..367256cb3bcc 100644 --- a/worlds/messenger/docs/setup_en.md +++ b/worlds/messenger/docs/setup_en.md @@ -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. \ No newline at end of file diff --git a/worlds/messenger/test/TestAccess.py b/worlds/messenger/test/TestAccess.py index 87bf55f7c9e5..452ed1189f44 100644 --- a/worlds/messenger/test/TestAccess.py +++ b/worlds/messenger/test/TestAccess.py @@ -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", - "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", - "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", "Power Thistle", "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", - "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"] + 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", "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", "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", + "Searing Crags - Power Thistle", "Searing Crags - Key of Strength", + "Glacial Peak Seal - Projectile Spike Pit", "Glacial Peak Seal - Glacial Air Swag", + "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", + "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", - "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"] + 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" + ] # 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 @@ def testSelfLockingItems(self) -> None: 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)) - diff --git a/worlds/messenger/test/TestLogic.py b/worlds/messenger/test/TestLogic.py index 8b3d58541f46..45b0d0dab629 100644 --- a/worlds/messenger/test/TestLogic.py +++ b/worlds/messenger/test/TestLogic.py @@ -11,35 +11,33 @@ def testVertical(self) -> None: """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 @@ def testWindmill(self) -> None: """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 @@ def testWindmill(self) -> None: 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) diff --git a/worlds/messenger/test/TestShop.py b/worlds/messenger/test/TestShop.py new file mode 100644 index 000000000000..cad414e0b7ae --- /dev/null +++ b/worlds/messenger/test/TestShop.py @@ -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)) diff --git a/worlds/messenger/test/TestShopChest.py b/worlds/messenger/test/TestShopChest.py index fea49de80b9a..273c3ea71605 100644 --- a/worlds/messenger/test/TestShopChest.py +++ b/worlds/messenger/test/TestShopChest.py @@ -2,18 +2,6 @@ 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",