diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index 304b43cf5316..f12687361b70 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -62,8 +62,7 @@ class MessengerWorld(World): "Money Wrench", ], base_offset)} - data_version = 3 - required_client_version = (0, 4, 0) + required_client_version = (0, 4, 1) web = MessengerWeb() @@ -148,19 +147,12 @@ def set_rules(self) -> None: MessengerOOBRules(self).set_messenger_rules() def fill_slot_data(self) -> Dict[str, Any]: - 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.options.death_link.value, - "goal": self.options.goal.current_key, - "music_box": self.options.music_box.value, - "required_seals": self.required_seals, - "mega_shards": self.options.shuffle_shards.value, - "logic": self.options.logic_level.current_key, - "shop": shop_prices, - "figures": figure_prices, + "shop": {SHOP_ITEMS[item].internal_name: price for item, price in self.shop_prices.items()}, + "figures": {FIGURINES[item].internal_name: price for item, price in self.figurine_prices.items()}, "max_price": self.total_shards, + "required_seals": self.required_seals, + **self.options.as_dict("music_box", "death_link", "logic_level"), } def get_filler_item_name(self) -> str: diff --git a/worlds/messenger/regions.py b/worlds/messenger/regions.py index 3a6c95bff5a2..43de4dd1f6d0 100644 --- a/worlds/messenger/regions.py +++ b/worlds/messenger/regions.py @@ -4,6 +4,7 @@ "Menu": [], "Tower HQ": [], "The Shop": [], + "The Craftsman's Corner": [], "Tower of Time": [], "Ninja Village": ["Ninja Village - Candle", "Ninja Village - Astral Seed"], "Autumn Hills": ["Autumn Hills - Climbing Claws", "Autumn Hills - Key of Hope", "Autumn Hills - Leaf Golem"], @@ -82,7 +83,8 @@ REGION_CONNECTIONS: Dict[str, Set[str]] = { "Menu": {"Tower HQ"}, "Tower HQ": {"Autumn Hills", "Howling Grotto", "Searing Crags", "Glacial Peak", "Tower of Time", - "Riviere Turquoise Entrance", "Sunken Shrine", "Corrupted Future", "The Shop", "Music Box"}, + "Riviere Turquoise Entrance", "Sunken Shrine", "Corrupted Future", "The Shop", + "The Craftsman's Corner", "Music Box"}, "Autumn Hills": {"Ninja Village", "Forlorn Temple", "Catacombs"}, "Forlorn Temple": {"Catacombs", "Bamboo Creek"}, "Catacombs": {"Autumn Hills", "Bamboo Creek", "Dark Cave"}, diff --git a/worlds/messenger/rules.py b/worlds/messenger/rules.py index 876acd42c108..793de50afb70 100644 --- a/worlds/messenger/rules.py +++ b/worlds/messenger/rules.py @@ -1,26 +1,32 @@ -from typing import Callable, Dict, TYPE_CHECKING +from typing import Dict, TYPE_CHECKING from BaseClasses import CollectionState -from worlds.generic.Rules import add_rule, allow_self_locking_items +from worlds.generic.Rules import add_rule, allow_self_locking_items, CollectionRule from .constants import NOTES, PHOBEKINS from .options import MessengerAccessibility if TYPE_CHECKING: from . import MessengerWorld -else: - MessengerWorld = object class MessengerRules: player: int - world: MessengerWorld - region_rules: Dict[str, Callable[[CollectionState], bool]] - location_rules: Dict[str, Callable[[CollectionState], bool]] + world: "MessengerWorld" + region_rules: Dict[str, CollectionRule] + location_rules: Dict[str, CollectionRule] + maximum_price: int + required_seals: int - def __init__(self, world: MessengerWorld) -> None: + def __init__(self, world: "MessengerWorld") -> None: self.player = world.player self.world = world + # these locations are at the top of the shop tree, and the entire shop tree needs to be purchased + maximum_price = (world.multiworld.get_location("The Shop - Demon's Bane", self.player).cost + + world.multiworld.get_location("The Shop - Focused Power Sense", self.player).cost) + self.maximum_price = min(maximum_price, world.total_shards) + self.required_seals = max(1, world.required_seals) + self.region_rules = { "Ninja Village": self.has_wingsuit, "Autumn Hills": self.has_wingsuit, @@ -36,9 +42,9 @@ def __init__(self, world: MessengerWorld) -> None: "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("Magic Firefly", self.player) and self.has_wingsuit(state), - "Music Box": lambda state: (state.has_all(set(NOTES), self.player) - or state.has("Power Seal", self.player, max(1, self.world.required_seals))) - and self.has_dart(state), + "Music Box": lambda state: (state.has_all(NOTES, self.player) + or self.has_enough_seals(state)) and self.has_dart(state), + "The Craftsman's Corner": lambda state: state.has("Money Wrench", self.player) and self.can_shop(state), } self.location_rules = { @@ -110,7 +116,7 @@ def has_vertical(self, state: CollectionState) -> bool: return self.has_wingsuit(state) or self.has_dart(state) 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) + return state.has("Power Seal", self.player, self.required_seals) def can_destroy_projectiles(self, state: CollectionState) -> bool: return state.has("Strike of the Ninja", self.player) @@ -127,9 +133,7 @@ def true(self, state: CollectionState) -> bool: 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) + return state.has("Shards", self.player, self.maximum_price) def set_messenger_rules(self) -> None: multiworld = self.world.multiworld @@ -141,9 +145,6 @@ 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 region.locations: - loc.access_rule = loc.can_afford multiworld.completion_condition[self.player] = lambda state: state.has("Rescue Phantom", self.player) if multiworld.accessibility[self.player]: # not locations accessibility @@ -151,9 +152,9 @@ def set_messenger_rules(self) -> None: class MessengerHardRules(MessengerRules): - extra_rules: Dict[str, Callable[[CollectionState], bool]] + extra_rules: Dict[str, CollectionRule] - def __init__(self, world: MessengerWorld) -> None: + def __init__(self, world: "MessengerWorld") -> None: super().__init__(world) self.region_rules.update({ @@ -162,7 +163,7 @@ def __init__(self, world: MessengerWorld) -> None: "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), + "Forlorn Temple": lambda state: self.has_vertical(state) and state.has_all(PHOBEKINS, self.player), "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) @@ -215,14 +216,15 @@ def set_messenger_rules(self) -> None: class MessengerOOBRules(MessengerRules): - def __init__(self, world: MessengerWorld) -> None: + def __init__(self, world: "MessengerWorld") -> None: self.world = world self.player = world.player + self.required_seals = max(1, world.required_seals) self.region_rules = { "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) + "Music Box": lambda state: state.has_all(set(NOTES), self.player) or self.has_enough_seals(state), } self.location_rules = { @@ -238,16 +240,14 @@ def __init__(self, world: MessengerWorld) -> None: "Underworld Seal - Fireball Wave": lambda state: state.has_any({"Wingsuit", "Windmill Shuriken"}, self.player), "Tower of Time Seal - Time Waster": self.has_dart, - "Shop Chest": self.has_enough_seals } def set_messenger_rules(self) -> None: super().set_messenger_rules() - self.world.multiworld.completion_condition[self.player] = lambda state: True self.world.options.accessibility.value = MessengerAccessibility.option_minimal -def set_self_locking_items(world: MessengerWorld, player: int) -> None: +def set_self_locking_items(world: "MessengerWorld", player: int) -> None: multiworld = world.multiworld # do the ones for seal shuffle on and off first diff --git a/worlds/messenger/subclasses.py b/worlds/messenger/subclasses.py index 0c04bc015c35..b6a0b80b21a6 100644 --- a/worlds/messenger/subclasses.py +++ b/worlds/messenger/subclasses.py @@ -3,7 +3,6 @@ from BaseClasses import CollectionState, Item, ItemClassification, Location, Region from .constants import NOTES, PHOBEKINS, PROG_ITEMS, USEFUL_ITEMS -from .options import Goal from .regions import MEGA_SHARDS, REGIONS, SEALS from .shop import FIGURINES, PROG_SHOP_ITEMS, SHOP_ITEMS, USEFUL_SHOP_ITEMS @@ -19,8 +18,10 @@ def __init__(self, name: str, world: "MessengerWorld") -> None: if self.name == "The Shop": shop_locations = {f"The Shop - {shop_loc}": world.location_name_to_id[f"The Shop - {shop_loc}"] for shop_loc in SHOP_ITEMS} - shop_locations.update(**{figurine: world.location_name_to_id[figurine] for figurine in FIGURINES}) self.add_locations(shop_locations, MessengerShopLocation) + elif self.name == "The Craftsman's Corner": + self.add_locations({figurine: world.location_name_to_id[figurine] for figurine in FIGURINES}, + MessengerLocation) elif self.name == "Tower HQ": locations.append("Money Wrench") if world.options.shuffle_seals and self.name in SEALS: @@ -46,10 +47,6 @@ class MessengerShopLocation(MessengerLocation): def cost(self) -> int: name = self.name.replace("The Shop - ", "") # TODO use `remove_prefix` when 3.8 finally gets dropped world = cast("MessengerWorld", self.parent_region.multiworld.worlds[self.player]) - # short circuit figurines which all require demon's bane be purchased, but nothing else - if "Figurine" in name: - return world.figurine_prices[name] +\ - cast(MessengerShopLocation, world.multiworld.get_location("The Shop - Demon's Bane", self.player)).cost shop_data = SHOP_ITEMS[name] if shop_data.prerequisite: prereq_cost = 0 @@ -65,12 +62,9 @@ def cost(self) -> int: return world.shop_prices[name] + prereq_cost return world.shop_prices[name] - def can_afford(self, state: CollectionState) -> bool: + def access_rule(self, state: CollectionState) -> bool: world = cast("MessengerWorld", state.multiworld.worlds[self.player]) can_afford = state.has("Shards", self.player, min(self.cost, world.total_shards)) - if "Figurine" in self.name: - can_afford = state.has("Money Wrench", self.player) and can_afford\ - and state.can_reach("Money Wrench", "Location", self.player) return can_afford diff --git a/worlds/messenger/test/test_logic.py b/worlds/messenger/test/test_logic.py index 53ea92992212..15df89b92097 100644 --- a/worlds/messenger/test/test_logic.py +++ b/worlds/messenger/test/test_logic.py @@ -111,4 +111,3 @@ def test_access(self) -> None: for loc in all_locations: with self.subTest("Default unreachables", location=loc): self.assertFalse(self.can_reach_location(loc)) - self.assertBeatable(True) diff --git a/worlds/messenger/test/test_shop.py b/worlds/messenger/test/test_shop.py index bfd3b417a875..afb1b32b88e3 100644 --- a/worlds/messenger/test/test_shop.py +++ b/worlds/messenger/test/test_shop.py @@ -106,6 +106,5 @@ def test_costs(self) -> None: elif loc == "Demon Hive Figurine": self.assertIn(price, self.options["shop_price_plan"]["Demon Hive Figurine"]) - self.assertLessEqual(price, self.multiworld.get_location(loc, self.player).cost) self.assertTrue(loc in FIGURINES) self.assertEqual(len(figures), len(FIGURINES))