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:
Aaron Wagener 2023-06-27 18:39:52 -05:00 committed by GitHub
parent 8c2584f872
commit 332eab9569
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 712 additions and 393 deletions

View File

@ -1,5 +1,6 @@
# items # items
# listing individual groups first for easy lookup # listing individual groups first for easy lookup
from .Shop import SHOP_ITEMS, FIGURINES
NOTES = [ NOTES = [
"Key of Hope", "Key of Hope",
@ -13,15 +14,16 @@ NOTES = [
PROG_ITEMS = [ PROG_ITEMS = [
"Wingsuit", "Wingsuit",
"Rope Dart", "Rope Dart",
"Ninja Tabi", "Lightfoot Tabi",
"Power Thistle", "Power Thistle",
"Demon King Crown", "Demon King Crown",
"Ruxxtin's Amulet", "Ruxxtin's Amulet",
"Fairy Bottle", "Magic Firefly",
"Sun Crest", "Sun Crest",
"Moon Crest", "Moon Crest",
# "Astral Seed", # "Astral Seed",
# "Astral Tea Leaves", # "Astral Tea Leaves",
"Money Wrench",
] ]
PHOBEKINS = [ PHOBEKINS = [
@ -35,13 +37,22 @@ USEFUL_ITEMS = [
"Windmill Shuriken", "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 # item_name_to_id needs to be deterministic and match upstream
ALL_ITEMS = [ ALL_ITEMS = [
*NOTES, *NOTES,
"Windmill Shuriken", "Windmill Shuriken",
"Wingsuit", "Wingsuit",
"Rope Dart", "Rope Dart",
"Ninja Tabi", "Lightfoot Tabi",
# "Astral Seed", # "Astral Seed",
# "Astral Tea Leaves", # "Astral Tea Leaves",
"Candle", "Candle",
@ -49,12 +60,15 @@ ALL_ITEMS = [
"Power Thistle", "Power Thistle",
"Demon King Crown", "Demon King Crown",
"Ruxxtin's Amulet", "Ruxxtin's Amulet",
"Fairy Bottle", "Magic Firefly",
"Sun Crest", "Sun Crest",
"Moon Crest", "Moon Crest",
*PHOBEKINS, *PHOBEKINS,
"Power Seal", "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 # locations
@ -62,100 +76,38 @@ ALL_ITEMS = [
# order must be exactly the same as upstream # order must be exactly the same as upstream
ALWAYS_LOCATIONS = [ ALWAYS_LOCATIONS = [
# notes # notes
"Key of Love", "Sunken Shrine - Key of Love",
"Key of Courage", "Corrupted Future - Key of Courage",
"Key of Chaos", "Underworld - Key of Chaos",
"Key of Symbiosis", "Elemental Skylands - Key of Symbiosis",
"Key of Strength", "Searing Crags - Key of Strength",
"Key of Hope", "Autumn Hills - Key of Hope",
# upgrades # upgrades
"Wingsuit", "Howling Grotto - Wingsuit",
"Rope Dart", "Searing Crags - Rope Dart",
"Ninja Tabi", "Sunken Shrine - Lightfoot Tabi",
"Climbing Claws", "Autumn Hills - Climbing Claws",
# quest items # quest items
"Astral Seed", "Ninja Village - Astral Seed",
"Astral Tea Leaves", "Searing Crags - Astral Tea Leaves",
"Candle", "Ninja Village - Candle",
"Seashell", "Quillshroom Marsh - Seashell",
"Power Thistle", "Searing Crags - Power Thistle",
"Demon King Crown", "Forlorn Temple - Demon King",
"Ruxxtin's Amulet", "Catacombs - Ruxxtin's Amulet",
"Fairy Bottle", "Riviere Turquoise - Butterfly Matriarch",
"Sun Crest", "Sunken Shrine - Sun Crest",
"Moon Crest", "Sunken Shrine - Moon Crest",
# phobekins # phobekins
"Necro", "Catacombs - Necro",
"Pyro", "Searing Crags - Pyro",
"Claustro", "Bamboo Creek - Claustro",
"Acro", "Cloud Ruins - 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",
] ]
BOSS_LOCATIONS = [ BOSS_LOCATIONS = [
"Leaf Golem", "Autumn Hills - Leaf Golem",
"Ruxxtin", "Catacombs - Ruxxtin",
"Emerald Golem", "Howling Grotto - Emerald Golem",
"Queen of Quills", "Quillshroom Marsh - Queen of Quills",
] ]

View File

@ -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): class MessengerAccessibility(Accessibility):
@ -10,16 +13,16 @@ class MessengerAccessibility(Accessibility):
class Logic(Choice): class Logic(Choice):
""" """
The level of logic to use when determining what locations in your world are accessible. 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. Normal: can require damage boosts, but otherwise approachable for someone who has beaten the game.
Challenging contains more medium and hard difficulty speedrunning tricks. 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. OoB: places everything with the minimum amount of rules possible. Expect to do OoB. Not guaranteed completable.
""" """
display_name = "Logic Level" display_name = "Logic Level"
option_normal = 0 option_normal = 0
option_hard = 1 option_hard = 1
option_challenging = 2 option_oob = 2
option_oob = 3 alias_challenging = 1
class PowerSeals(DefaultOnToggle): class PowerSeals(DefaultOnToggle):
@ -68,6 +71,64 @@ class RequiredSeals(Range):
default = range_end 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 = { messenger_options = {
"accessibility": MessengerAccessibility, "accessibility": MessengerAccessibility,
"start_inventory": StartInventoryPool, "start_inventory": StartInventoryPool,
@ -79,5 +140,7 @@ messenger_options = {
"notes_needed": NotesNeeded, "notes_needed": NotesNeeded,
"total_seals": AmountSeals, "total_seals": AmountSeals,
"percent_seals_required": RequiredSeals, "percent_seals_required": RequiredSeals,
"shop_price": ShopPrices,
"shop_price_plan": PlannedShopPrices,
"death_link": DeathLink, "death_link": DeathLink,
} }

View File

@ -5,27 +5,60 @@ REGIONS: Dict[str, List[str]] = {
"Tower HQ": [], "Tower HQ": [],
"The Shop": [], "The Shop": [],
"Tower of Time": [], "Tower of Time": [],
"Ninja Village": ["Candle", "Astral Seed"], "Ninja Village": ["Ninja Village - Candle", "Ninja Village - Astral Seed"],
"Autumn Hills": ["Climbing Claws", "Key of Hope", "Leaf Golem"], "Autumn Hills": ["Autumn Hills - Climbing Claws", "Autumn Hills - Key of Hope", "Autumn Hills - Leaf Golem"],
"Forlorn Temple": ["Demon King Crown"], "Forlorn Temple": ["Forlorn Temple - Demon King"],
"Catacombs": ["Necro", "Ruxxtin's Amulet", "Ruxxtin"], "Catacombs": ["Catacombs - Necro", "Catacombs - Ruxxtin's Amulet", "Catacombs - Ruxxtin"],
"Bamboo Creek": ["Claustro"], "Bamboo Creek": ["Bamboo Creek - Claustro"],
"Howling Grotto": ["Wingsuit", "Emerald Golem"], "Howling Grotto": ["Howling Grotto - Wingsuit", "Howling Grotto - Emerald Golem"],
"Quillshroom Marsh": ["Seashell", "Queen of Quills"], "Quillshroom Marsh": ["Quillshroom Marsh - Seashell", "Quillshroom Marsh - Queen of Quills"],
"Searing Crags": ["Rope Dart"], "Searing Crags": ["Searing Crags - Rope Dart"],
"Searing Crags Upper": ["Power Thistle", "Key of Strength", "Astral Tea Leaves"], "Searing Crags Upper": ["Searing Crags - Power Thistle", "Searing Crags - Key of Strength",
"Searing Crags - Astral Tea Leaves"],
"Glacial Peak": [], "Glacial Peak": [],
"Cloud Ruins": [], "Cloud Ruins": [],
"Cloud Ruins Right": ["Acro"], "Cloud Ruins Right": ["Cloud Ruins - Acro"],
"Underworld": ["Pyro", "Key of Chaos"], "Underworld": ["Searing Crags - Pyro", "Underworld - Key of Chaos"],
"Dark Cave": [], "Dark Cave": [],
"Riviere Turquoise": ["Fairy Bottle"], "Riviere Turquoise Entrance": [],
"Sunken Shrine": ["Ninja Tabi", "Sun Crest", "Moon Crest", "Key of Love"], "Riviere Turquoise": ["Riviere Turquoise - Butterfly Matriarch"],
"Elemental Skylands": ["Key of Symbiosis"], "Sunken Shrine": ["Sunken Shrine - Lightfoot Tabi", "Sunken Shrine - Sun Crest", "Sunken Shrine - Moon Crest",
"Corrupted Future": ["Key of Courage"], "Sunken Shrine - Key of Love"],
"Elemental Skylands": ["Elemental Skylands - Key of Symbiosis"],
"Corrupted Future": ["Corrupted Future - Key of Courage"],
"Music Box": ["Rescue Phantom"], "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]] = { MEGA_SHARDS: Dict[str, List[str]] = {
"Autumn Hills": ["Autumn Hills Mega Shard", "Hidden Entrance Mega Shard"], "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"], "Underworld": ["Under Entrance Mega Shard", "Hot Tub Mega Shard", "Projectile Pit Mega Shard"],
"Forlorn Temple": ["Sunny Day Mega Shard", "Down Under 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"], "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"], "Elemental Skylands": ["Earth Mega Shard", "Water Mega Shard"],
} }
REGION_CONNECTIONS: Dict[str, Set[str]] = { REGION_CONNECTIONS: Dict[str, Set[str]] = {
"Menu": {"Tower HQ"}, "Menu": {"Tower HQ"},
"Tower HQ": {"Autumn Hills", "Howling Grotto", "Searing Crags", "Glacial Peak", "Tower of Time", "Riviere Turquoise", "Tower HQ": {"Autumn Hills", "Howling Grotto", "Searing Crags", "Glacial Peak", "Tower of Time",
"Sunken Shrine", "Corrupted Future", "The Shop", "Music Box"}, "Riviere Turquoise Entrance", "Sunken Shrine", "Corrupted Future", "The Shop", "Music Box"},
"Tower of Time": set(), "Tower of Time": set(),
"Ninja Village": set(), "Ninja Village": set(),
"Autumn Hills": {"Ninja Village", "Forlorn Temple", "Catacombs"}, "Autumn Hills": {"Ninja Village", "Forlorn Temple", "Catacombs"},
@ -64,7 +98,8 @@ REGION_CONNECTIONS: Dict[str, Set[str]] = {
"Cloud Ruins": {"Cloud Ruins Right"}, "Cloud Ruins": {"Cloud Ruins Right"},
"Cloud Ruins Right": {"Underworld"}, "Cloud Ruins Right": {"Underworld"},
"Underworld": set(), "Underworld": set(),
"Dark Cave": {"Catacombs", "Riviere Turquoise"}, "Dark Cave": {"Catacombs", "Riviere Turquoise Entrance"},
"Riviere Turquoise Entrance": {"Riviere Turquoise"},
"Riviere Turquoise": set(), "Riviere Turquoise": set(),
"Sunken Shrine": {"Howling Grotto"}, "Sunken Shrine": {"Howling Grotto"},
"Elemental Skylands": set(), "Elemental Skylands": set(),

View File

@ -4,6 +4,7 @@ from BaseClasses import CollectionState, MultiWorld
from worlds.generic.Rules import set_rule, allow_self_locking_items, add_rule from worlds.generic.Rules import set_rule, allow_self_locking_items, add_rule
from .Options import MessengerAccessibility, Goal from .Options import MessengerAccessibility, Goal
from .Constants import NOTES, PHOBEKINS from .Constants import NOTES, PHOBEKINS
from .SubClasses import MessengerShopLocation
if TYPE_CHECKING: if TYPE_CHECKING:
from . import MessengerWorld from . import MessengerWorld
@ -28,62 +29,73 @@ class MessengerRules:
"Bamboo Creek": self.has_wingsuit, "Bamboo Creek": self.has_wingsuit,
"Searing Crags Upper": self.has_vertical, "Searing Crags Upper": self.has_vertical,
"Cloud Ruins": lambda state: self.has_vertical(state) and state.has("Ruxxtin's Amulet", self.player), "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, "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, "Glacial Peak": self.has_vertical,
"Elemental Skylands": lambda state: state.has("Fairy Bottle", self.player), "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_vertical(state), "Music Box": lambda state: state.has_all(set(NOTES), self.player) and self.has_dart(state),
} }
self.location_rules = { self.location_rules = {
# ninja village # ninja village
"Ninja Village Seal - Tree House": self.has_dart, "Ninja Village Seal - Tree House": self.has_dart,
# autumn hills # 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
"Howling Grotto Seal - Windy Saws and Balls": self.has_wingsuit, "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), "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 # searing crags
"Astral Tea Leaves": lambda state: state.can_reach("Astral Seed", "Location", self.player), "Searing Crags Seal - Triple Ball Spinner": self.has_vertical,
"Key of Strength": lambda state: state.has("Power Thistle", self.player), "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
"Glacial Peak Seal - Ice Climbers": self.has_dart, "Glacial Peak Seal - Ice Climbers": self.has_dart,
"Glacial Peak Seal - Projectile Spike Pit": self.has_vertical, "Glacial Peak Seal - Projectile Spike Pit": self.can_destroy_projectiles,
"Glacial Peak Seal - Glacial Air Swag": self.has_vertical, # cloud ruins
"Cloud Ruins Seal - Ghost Pit": self.has_dart,
# tower of time # tower of time
"Tower of Time Seal - Time Waster Seal": self.has_dart, "Tower of Time Seal - Time Waster": self.has_dart,
"Tower of Time Seal - Lantern Climb": self.has_wingsuit, "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), "Tower of Time Seal - Arcane Orbs": lambda state: self.has_wingsuit(state) and self.has_dart(state),
# underworld # underworld
"Underworld Seal - Sharp and Windy Climb": self.has_wingsuit, "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, "Underworld Seal - Rising Fanta": self.has_dart,
# sunken shrine # sunken shrine
"Sun Crest": self.has_tabi, "Sunken Shrine - Sun Crest": self.has_tabi,
"Moon Crest": self.has_tabi, "Sunken Shrine - Moon Crest": self.has_tabi,
"Key of Love": lambda state: state.has_all({"Sun Crest", "Moon Crest"}, self.player), "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 - Waterfall Paradise": self.has_tabi,
"Sunken Shrine Seal - Tabi Gauntlet": self.has_tabi, "Sunken Shrine Seal - Tabi Gauntlet": self.has_tabi,
"Mega Shard of the Moon": self.has_tabi, "Mega Shard of the Moon": self.has_tabi,
"Mega Shard of the Sun": self.has_tabi, "Mega Shard of the Sun": self.has_tabi,
# riviere turquoise # riviere turquoise
"Fairy Bottle": self.has_vertical, "Riviere Turquoise Seal - Bounces and Balls": self.can_dboost,
"Riviere Turquoise Seal - Flower Power": self.has_vertical, "Riviere Turquoise Seal - Launch of Faith": lambda state: self.can_dboost(state) or self.has_dart(state),
"Quick Restock Mega Shard 1": self.has_vertical,
"Quick Restock Mega Shard 2": self.has_vertical,
# elemental skylands # 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 - Air": self.has_wingsuit,
"Elemental Skylands Seal - Water": self.has_dart, "Elemental Skylands Seal - Water": lambda state: self.has_dart(state) and
"Elemental Skylands Seal - Fire": self.has_dart, 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, "Earth Mega Shard": self.has_dart,
"Water Mega Shard": self.has_dart, "Water Mega Shard": self.has_dart,
# corrupted future # 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 # the shop
"Shop Chest": self.has_enough_seals, "Shop Chest": self.has_enough_seals,
# tower hq
"Money Wrench": self.can_shop,
} }
def has_wingsuit(self, state: CollectionState) -> bool: def has_wingsuit(self, state: CollectionState) -> bool:
@ -93,7 +105,7 @@ class MessengerRules:
return state.has("Rope Dart", self.player) return state.has("Rope Dart", self.player)
def has_tabi(self, state: CollectionState) -> bool: 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: def has_vertical(self, state: CollectionState) -> bool:
return self.has_wingsuit(state) or self.has_dart(state) return self.has_wingsuit(state) or self.has_dart(state)
@ -101,10 +113,25 @@ class MessengerRules:
def has_enough_seals(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) 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: def true(self, state: CollectionState) -> bool:
"""I know this is stupid, but it's easier to read in the dicts.""" """I know this is stupid, but it's easier to read in the dicts."""
return True 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: def set_messenger_rules(self) -> None:
multiworld = self.world.multiworld multiworld = self.world.multiworld
@ -115,6 +142,9 @@ class MessengerRules:
for loc in region.locations: for loc in region.locations:
if loc.name in self.location_rules: if loc.name in self.location_rules:
loc.access_rule = self.location_rules[loc.name] 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: if multiworld.goal[self.player] == Goal.option_power_seal_hunt:
set_rule(multiworld.get_entrance("Tower HQ -> Music Box", self.player), set_rule(multiworld.get_entrance("Tower HQ -> Music Box", self.player),
lambda state: state.has("Shop Chest", self.player)) lambda state: state.has("Shop Chest", self.player))
@ -135,29 +165,45 @@ class MessengerHardRules(MessengerRules):
"Autumn Hills": self.has_vertical, "Autumn Hills": self.has_vertical,
"Catacombs": self.has_vertical, "Catacombs": self.has_vertical,
"Bamboo Creek": 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(set(PHOBEKINS), self.player),
"Searing Crags Upper": self.true, "Searing Crags Upper": lambda state: self.can_destroy_projectiles(state) or self.has_windmill(state)
"Glacial Peak": self.true, or self.has_vertical(state),
"Elemental Skylands": lambda state: state.has("Fairy Bottle", self.player) or self.has_windmill(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({ self.location_rules.update({
"Howling Grotto Seal - Windy Saws and Balls": self.true, "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, "Glacial Peak Seal - Projectile Spike Pit": self.true,
"Claustro": self.has_wingsuit, "Cloud Ruins Seal - Ghost Pit": self.true,
"Elemental Skylands Seal - Water": self.true, "Bamboo Creek - Claustro": self.has_wingsuit,
"Elemental Skylands Seal - Fire": self.true, "Tower of Time Seal - Lantern Climb": self.has_wingsuit,
"Earth Mega Shard": self.true, "Elemental Skylands Seal - Water": lambda state: self.has_dart(state) or self.can_dboost(state)
"Water Mega Shard": self.true, 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 = { self.extra_rules = {
"Key of Strength": lambda state: self.has_dart(state) or self.has_windmill(state), "Searing Crags - Key of Strength": lambda state: self.has_dart(state) or self.has_windmill(state),
"Key of Symbiosis": self.has_windmill, "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)) "Autumn Hills Seal - Spike Ball Darts": lambda state: (self.has_dart(state) and self.has_windmill(state))
or self.has_wingsuit(state), or self.has_wingsuit(state),
"Glacial Peak Seal - Glacial Air Swag": self.has_windmill, "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), self.player),
} }
@ -174,53 +220,31 @@ class MessengerHardRules(MessengerRules):
add_rule(self.world.multiworld.get_location(loc, self.player), rule, "or") 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): class MessengerOOBRules(MessengerRules):
def __init__(self, world: MessengerWorld) -> None: def __init__(self, world: MessengerWorld) -> None:
self.world = world self.world = world
self.player = world.player self.player = world.player
self.region_rules = { self.region_rules = {
"Elemental Skylands": lambda state: state.has_any({"Wingsuit", "Rope Dart", "Fairy Bottle"}, self.player), "Elemental Skylands":
"Music Box": lambda state: state.has_all(set(NOTES), self.player), 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 = { self.location_rules = {
"Claustro": self.has_wingsuit, "Bamboo Creek - Claustro": self.has_wingsuit,
"Key of Strength": lambda state: self.has_vertical(state) or state.has("Power Thistle", self.player), "Searing Crags - Key of Strength": self.has_wingsuit,
"Key of Love": lambda state: state.has_all({"Sun Crest", "Moon Crest"}, self.player), "Sunken Shrine - Key of Love": lambda state: state.has_all({"Sun Crest", "Moon Crest"}, self.player),
"Pyro": self.has_tabi, "Searing Crags - Pyro": self.has_tabi,
"Key of Chaos": self.has_tabi, "Underworld - Key of Chaos": self.has_tabi,
"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),
"Autumn Hills Seal - Spike Ball Darts": self.has_dart, "Autumn Hills Seal - Spike Ball Darts": self.has_dart,
"Ninja Village Seal - Tree House": self.has_dart, "Ninja Village Seal - Tree House": self.has_dart,
"Underworld Seal - Fireball Wave": lambda state: state.has_any({"Wingsuit", "Windmill Shuriken"}, "Underworld Seal - Fireball Wave": lambda state: state.has_any({"Wingsuit", "Windmill Shuriken"},
self.player), self.player),
"Tower of Time Seal - Time Waster Seal": self.has_dart, "Tower of Time Seal - Time Waster": self.has_dart,
"Shop Chest": self.has_enough_seals, "Shop Chest": self.has_enough_seals
} }
def set_messenger_rules(self) -> None: def set_messenger_rules(self) -> None:
@ -231,11 +255,14 @@ class MessengerOOBRules(MessengerRules):
def set_self_locking_items(multiworld: MultiWorld, player: int) -> None: def set_self_locking_items(multiworld: MultiWorld, player: int) -> None:
# do the ones for seal shuffle on and off first # 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("Searing Crags - 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("Sunken Shrine - 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("Corrupted Future - Key of Courage", player), "Demon King Crown")
# add these locations when seals aren't shuffled # add these locations when seals are shuffled
if not multiworld.shuffle_seals[player] and not multiworld.shuffle_shards[player]: 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("Cloud Ruins Right", player), "Ruxxtin's Amulet")
allow_self_locking_items(multiworld.get_region("Forlorn Temple", player), *PHOBEKINS) allow_self_locking_items(multiworld.get_region("Forlorn Temple", player), *PHOBEKINS)

100
worlds/messenger/Shop.py Normal file
View File

@ -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

View File

@ -1,9 +1,10 @@
from typing import Set, TYPE_CHECKING, Optional, Dict from typing import Set, TYPE_CHECKING, Optional, Dict
from BaseClasses import Region, Location, Item, ItemClassification, Entrance from BaseClasses import Region, Location, Item, ItemClassification, Entrance, CollectionState
from .Constants import SEALS, NOTES, PROG_ITEMS, PHOBEKINS, USEFUL_ITEMS from .Constants import NOTES, PROG_ITEMS, PHOBEKINS, USEFUL_ITEMS
from .Options import Goal 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: if TYPE_CHECKING:
from . import MessengerWorld from . import MessengerWorld
@ -20,14 +21,21 @@ class MessengerRegion(Region):
def add_locations(self, name_to_id: Dict[str, int]) -> None: def add_locations(self, name_to_id: Dict[str, int]) -> None:
for loc in REGIONS[self.name]: for loc in REGIONS[self.name]:
self.locations.append(MessengerLocation(loc, self, name_to_id.get(loc, None))) 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: if self.name == "The Shop":
self.locations.append(MessengerLocation("Shop Chest", self, name_to_id.get("Shop Chest", None))) if self.multiworld.goal[self.player] > Goal.option_open_music_box:
# putting some dumb special case for searing crags and ToT so i can split them into 2 regions self.locations.append(MessengerLocation("Shop Chest", self, None))
if self.multiworld.shuffle_seals[self.player] and self.name not in {"Searing Crags", "Tower HQ", "Cloud Ruins"}: self.locations += [MessengerShopLocation(f"The Shop - {shop_loc}", self,
self.locations += [MessengerLocation(seal_loc, self, name_to_id.get(seal_loc, None)) name_to_id[f"The Shop - {shop_loc}"])
for seal_loc in SEALS if seal_loc.startswith(self.name.split(" ")[0])] 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: 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]] for shard in MEGA_SHARDS[self.name]]
def add_exits(self, exits: Set[str]) -> None: def add_exits(self, exits: Set[str]) -> None:
@ -46,13 +54,33 @@ class MessengerLocation(Location):
self.place_locked_item(MessengerItem(name, parent.player, None)) 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): class MessengerItem(Item):
game = "The Messenger" game = "The Messenger"
def __init__(self, name: str, player: int, item_id: Optional[int] = None, override_progression: bool = False) -> None: def __init__(self, name: str, player: int, item_id: Optional[int] = None, override_progression: bool = False,
if name in {*NOTES, *PROG_ITEMS, *PHOBEKINS} or item_id is None or override_progression: 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 item_class = ItemClassification.progression
elif name in USEFUL_ITEMS: elif name in {*USEFUL_ITEMS, *USEFUL_SHOP_ITEMS}:
item_class = ItemClassification.useful item_class = ItemClassification.useful
else: else:
item_class = ItemClassification.filler item_class = ItemClassification.filler

View File

@ -1,11 +1,12 @@
import logging 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 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 .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 .SubClasses import MessengerRegion, MessengerItem
from . import Rules from . import Rules
@ -41,7 +42,6 @@ class MessengerWorld(World):
"Crest": {"Sun Crest", "Moon Crest"}, "Crest": {"Sun Crest", "Moon Crest"},
"Phobe": set(PHOBEKINS), "Phobe": set(PHOBEKINS),
"Phobekin": set(PHOBEKINS), "Phobekin": set(PHOBEKINS),
"Shuriken": {"Windmill Shuriken"},
} }
option_definitions = messenger_options option_definitions = messenger_options
@ -49,30 +49,35 @@ class MessengerWorld(World):
base_offset = 0xADD_000 base_offset = 0xADD_000
item_name_to_id = {item: item_id item_name_to_id = {item: item_id
for item_id, item in enumerate(ALL_ITEMS, base_offset)} 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 location_name_to_id = {location: location_id
for location_id, location in for location_id, location in
enumerate([ enumerate([
*ALWAYS_LOCATIONS, *ALWAYS_LOCATIONS,
*SEALS, *seal_locs,
*mega_shard_locs, *mega_shard_locs,
*BOSS_LOCATIONS, *BOSS_LOCATIONS,
*shop_locs,
*FIGURINES,
"Money Wrench",
], base_offset)} ], base_offset)}
data_version = 2 data_version = 3
required_client_version = (0, 3, 9) required_client_version = (0, 4, 0)
web = MessengerWeb() web = MessengerWeb()
total_seals: int = 0 total_seals: int = 0
required_seals: int = 0 required_seals: int = 0
total_shards: int
@classmethod shop_prices: Dict[str, int]
def stage_assert_generate(cls, multiworld: MultiWorld) -> None: figurine_prices: Dict[str, int]
for player in multiworld.get_game_players(cls.game):
player_name = multiworld.player_name[player] = multiworld.get_player_name(player).replace("_", " ") def __init__(self, multiworld: MultiWorld, player: int):
if not all(c.isalnum() or c in "- " for c in player_name): super().__init__(multiworld, player)
raise ValueError(f"Player name {player_name} is not alpha-numeric.") self.total_shards = 0
def generate_early(self) -> None: def generate_early(self) -> None:
if self.multiworld.goal[self.player] == Goal.option_power_seal_hunt: 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]) region.add_exits(REGION_CONNECTIONS[region.name])
def create_items(self) -> None: 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: if self.multiworld.goal[self.player] == Goal.option_open_music_box:
notes = self.multiworld.random.sample(NOTES, k=len(NOTES)) # make a list of all notes except those in the player's defined starting inventory, and adjust the
precollected_notes_amount = NotesNeeded.range_end - self.multiworld.notes_needed[self.player] # 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: if precollected_notes_amount:
for note in notes[:precollected_notes_amount]: for note in notes[:precollected_notes_amount]:
self.multiworld.push_precollected(self.create_item(note)) self.multiworld.push_precollected(self.create_item(note))
itempool += [self.create_item(note) for note in notes[precollected_notes_amount:]] notes = notes[precollected_notes_amount:]
itempool += [self.create_item(note) for note in notes]
itempool += [self.create_item(item) elif self.multiworld.goal[self.player] == Goal.option_power_seal_hunt:
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.
}]
if self.multiworld.goal[self.player] == Goal.option_power_seal_hunt:
total_seals = min(len(self.multiworld.get_unfilled_locations(self.player)) - len(itempool), total_seals = min(len(self.multiworld.get_unfilled_locations(self.player)) - len(itempool),
self.multiworld.total_seals[self.player].value) self.multiworld.total_seals[self.player].value)
if total_seals < self.total_seals: if total_seals < self.total_seals:
@ -118,39 +128,41 @@ class MessengerWorld(World):
seals[i].classification = ItemClassification.progression_skip_balancing seals[i].classification = ItemClassification.progression_skip_balancing
itempool += seals itempool += seals
itempool += [self.create_filler() itempool += [self.create_item(filler_item)
for _ in range(len(self.multiworld.get_unfilled_locations(self.player)) - len(itempool))] 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 self.multiworld.itempool += itempool
def set_rules(self) -> None: def set_rules(self) -> None:
self.shop_prices, self.figurine_prices = shuffle_shop_prices(self)
logic = self.multiworld.logic_level[self.player] logic = self.multiworld.logic_level[self.player]
if logic == Logic.option_normal: if logic == Logic.option_normal:
Rules.MessengerRules(self).set_messenger_rules() Rules.MessengerRules(self).set_messenger_rules()
elif logic == Logic.option_hard: elif logic == Logic.option_hard:
Rules.MessengerHardRules(self).set_messenger_rules() Rules.MessengerHardRules(self).set_messenger_rules()
elif logic == Logic.option_challenging:
Rules.MessengerChallengeRules(self).set_messenger_rules()
else: else:
Rules.MessengerOOBRules(self).set_messenger_rules() Rules.MessengerOOBRules(self).set_messenger_rules()
def fill_slot_data(self) -> Dict[str, Any]: def fill_slot_data(self) -> Dict[str, Any]:
locations: Dict[int, List[str]] = {} shop_prices = {SHOP_ITEMS[item].internal_name: price for item, price in self.shop_prices.items()}
for loc in self.multiworld.get_filled_locations(self.player): figure_prices = {FIGURINES[item].internal_name: price for item, price in self.figurine_prices.items()}
if loc.item.code:
locations[loc.address] = [loc.item.name, self.multiworld.player_name[loc.item.player]]
return { return {
"deathlink": self.multiworld.death_link[self.player].value, "deathlink": self.multiworld.death_link[self.player].value,
"goal": self.multiworld.goal[self.player].current_key, "goal": self.multiworld.goal[self.player].current_key,
"music_box": self.multiworld.music_box[self.player].value, "music_box": self.multiworld.music_box[self.player].value,
"required_seals": self.required_seals, "required_seals": self.required_seals,
"locations": locations, "mega_shards": self.multiworld.shuffle_shards[self.player].value,
"settings": {
"Difficulty": "Basic" if not self.multiworld.shuffle_seals[self.player] else "Advanced",
"Mega Shards": self.multiworld.shuffle_shards[self.player].value
},
"logic": self.multiworld.logic_level[self.player].current_key, "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: def get_filler_item_name(self) -> str:
@ -158,6 +170,21 @@ class MessengerWorld(World):
def create_item(self, name: str) -> MessengerItem: def create_item(self, name: str) -> MessengerItem:
item_id: Optional[int] = self.item_name_to_id.get(name, None) 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 \ override_prog = getattr(self, "multiworld") is not None and \
and self.multiworld.logic_level[self.player] > Logic.option_normal name in {"Windmill Shuriken"} and \
return MessengerItem(name, self.player, item_id, override_prog) 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)

View File

@ -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 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 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 obtained. You'll be forced to do sections of the game in different ways with your current abilities.
assumes you already have all shop upgrades.
## What items can appear in other players' worlds? ## What items can appear in other players' worlds?
@ -23,6 +22,7 @@ assumes you already have all shop upgrades.
* Music Box notes * Music Box notes
* The Phobekins * The Phobekins
* Time shards * Time shards
* Shop Upgrades
* Power Seals * Power Seals
## Where can I find items? ## 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 * Music Box notes
* Phobekins * Phobekins
* Bosses * Bosses
* Shop Upgrades, Money Wrench, and Figurine Purchases
* Power seals * Power seals
* Mega Time Shards * Mega Time Shards
@ -46,7 +47,6 @@ for it. The groups you can use for The Messenger are:
* Crest - The Sun and Moon Crests * Crest - The Sun and Moon Crests
* Phobekin - Any of the Phobekins * Phobekin - Any of the Phobekins
* Phobe - An alternative name for the Phobekins * Phobe - An alternative name for the Phobekins
* Shuriken - The windmill shuriken
## Other changes ## 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 * 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 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. 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 ## Known issues
* Necro cutscene will sometimes not play correctly, but will still reward the item
* Ruxxtin Coffin cutscene will sometimes not play correctly, but will still reward the item * 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. 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 * 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. 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? ## 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) and send a bug report either on GitHub or the [Archipelago Discord Server](http://archipelago.gg/discord)

View File

@ -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 * 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. time, but must do this step again to start new runs afterward.
4. Enter connection info using the relevant option buttons 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 ` `. * **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**
* This defaults to `archipelago.gg` and does not need to be manually changed if connecting to a game hosted on the * This defaults to `archipelago.gg` and does not need to be manually changed if connecting to a game hosted on the
website. 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 5. Select the `Connect to Archipelago` button
6. Navigate to save file selection 6. Navigate to save file selection
7. Select a new valid randomizer save 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 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 main menu and connect to the server as in [Joining a Multiworld Game](#joining-a-multiworld-game), then load the correct
save file. 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.

View File

@ -8,115 +8,131 @@ class AccessTest(MessengerTestBase):
} }
def testTabi(self) -> None: def testTabi(self) -> None:
"""locations that hard require the Ninja Tabi""" """locations that hard require the Lightfoot Tabi"""
locations = ["Pyro", "Key of Chaos", "Underworld Seal - Sharp and Windy Climb", "Underworld Seal - Spike Wall", locations = [
"Underworld Seal - Fireball Wave", "Underworld Seal - Rising Fanta", "Sun Crest", "Moon Crest", "Searing Crags - Pyro", "Underworld - Key of Chaos", "Underworld Seal - Sharp and Windy Climb",
"Sunken Shrine Seal - Waterfall Paradise", "Sunken Shrine Seal - Tabi Gauntlet", "Underworld Seal - Spike Wall", "Underworld Seal - Fireball Wave", "Underworld Seal - Rising Fanta",
"Mega Shard of the Moon", "Mega Shard of the Sun", "Under Entrance Mega Shard", "Sunken Shrine - Sun Crest", "Sunken Shrine - Moon Crest", "Sunken Shrine Seal - Waterfall Paradise",
"Hot Tub Mega Shard", "Projectile Pit Mega Shard"] "Sunken Shrine Seal - Tabi Gauntlet", "Mega Shard of the Moon", "Mega Shard of the Sun",
items = [["Ninja Tabi"]] "Under Entrance Mega Shard", "Hot Tub Mega Shard", "Projectile Pit Mega Shard"
]
items = [["Lightfoot Tabi"]]
self.assertAccessDependency(locations, items) self.assertAccessDependency(locations, items)
def testDart(self) -> None: def testDart(self) -> None:
"""locations that hard require the Rope Dart""" """locations that hard require the Rope Dart"""
locations = ["Ninja Village Seal - Tree House", "Key of Hope", "Howling Grotto Seal - Crushing Pits", locations = [
"Glacial Peak Seal - Ice Climbers", "Tower of Time Seal - Time Waster Seal", "Ninja Village Seal - Tree House", "Autumn Hills - Key of Hope", "Howling Grotto Seal - Crushing Pits",
"Tower of Time Seal - Arcane Orbs", "Underworld Seal - Rising Fanta", "Key of Symbiosis", "Glacial Peak Seal - Ice Climbers", "Tower of Time Seal - Time Waster", "Tower of Time Seal - Lantern Climb",
"Elemental Skylands Seal - Water", "Elemental Skylands Seal - Fire", "Earth Mega Shard", "Tower of Time Seal - Arcane Orbs", "Cloud Ruins Seal - Ghost Pit", "Underworld Seal - Rising Fanta",
"Water Mega Shard"] "Elemental Skylands - Key of Symbiosis", "Elemental Skylands Seal - Water",
"Elemental Skylands Seal - Fire", "Earth Mega Shard", "Water Mega Shard", "Rescue Phantom",
]
items = [["Rope Dart"]] items = [["Rope Dart"]]
self.assertAccessDependency(locations, items) self.assertAccessDependency(locations, items)
def testWingsuit(self) -> None: def testWingsuit(self) -> None:
"""locations that hard require the Wingsuit""" """locations that hard require the Wingsuit"""
locations = ["Candle", "Ninja Village Seal - Tree House", "Climbing Claws", "Key of Hope", locations = [
"Autumn Hills Seal - Trip Saws", "Autumn Hills Seal - Double Swing Saws", "Ninja Village - Candle", "Ninja Village Seal - Tree House", "Autumn Hills - Climbing Claws",
"Autumn Hills Seal - Spike Ball Swing", "Autumn Hills Seal - Spike Ball Darts", "Necro", "Autumn Hills - Key of Hope", "Autumn Hills Seal - Trip Saws", "Autumn Hills Seal - Double Swing Saws",
"Ruxxtin's Amulet", "Catacombs Seal - Triple Spike Crushers", "Catacombs Seal - Crusher Gauntlet", "Autumn Hills Seal - Spike Ball Swing", "Autumn Hills Seal - Spike Ball Darts", "Catacombs - Necro",
"Catacombs Seal - Dirty Pond", "Claustro", "Acro", "Bamboo Creek Seal - Spike Crushers and Doors", "Catacombs - Ruxxtin's Amulet", "Catacombs Seal - Triple Spike Crushers",
"Bamboo Creek Seal - Spike Ball Pits", "Bamboo Creek Seal - Spike Crushers and Doors v2", "Catacombs Seal - Crusher Gauntlet", "Catacombs Seal - Dirty Pond", "Bamboo Creek - Claustro",
"Howling Grotto Seal - Crushing Pits", "Howling Grotto Seal - Windy Saws and Balls", "Cloud Ruins - Acro", "Bamboo Creek Seal - Spike Crushers and Doors", "Bamboo Creek Seal - Spike Ball Pits",
"Tower of Time Seal - Lantern Climb", "Demon King Crown", "Cloud Ruins Seal - Ghost Pit", "Bamboo Creek Seal - Spike Crushers and Doors v2", "Howling Grotto Seal - Crushing Pits",
"Cloud Ruins Seal - Toothbrush Alley", "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room", "Howling Grotto Seal - Windy Saws and Balls", "Tower of Time Seal - Lantern Climb",
"Tower of Time Seal - Lantern Climb", "Tower of Time Seal - Arcane Orbs", "Forlorn Temple - Demon King", "Cloud Ruins Seal - Ghost Pit", "Cloud Ruins Seal - Toothbrush Alley",
"Underworld Seal - Sharp and Windy Climb", "Underworld Seal - Fireball Wave", "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room", "Tower of Time Seal - Lantern Climb",
"Elemental Skylands Seal - Air", "Forlorn Temple Seal - Rocket Maze", "Tower of Time Seal - Arcane Orbs", "Underworld Seal - Sharp and Windy Climb",
"Forlorn Temple Seal - Rocket Sunset", "Astral Seed", "Astral Tea Leaves", "Underworld Seal - Fireball Wave", "Elemental Skylands Seal - Air", "Elemental Skylands Seal - Water",
"Autumn Hills Mega Shard", "Hidden Entrance Mega Shard", "Sunny Day Mega Shard", "Elemental Skylands Seal - Fire", "Elemental Skylands - Key of Symbiosis",
"Down Under Mega Shard", "Catacombs Mega Shard", "Above Entrance Mega Shard", "Forlorn Temple Seal - Rocket Maze", "Forlorn Temple Seal - Rocket Sunset", "Ninja Village - Astral Seed",
"Abandoned Mega Shard", "Time Loop Mega Shard", "Money Farm Room Mega Shard 1", "Searing Crags - Astral Tea Leaves", "Autumn Hills Mega Shard", "Hidden Entrance Mega Shard",
"Money Farm Room Mega Shard 2", "Leaf Golem", "Ruxxtin", "Emerald Golem"] "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"]] items = [["Wingsuit"]]
self.assertAccessDependency(locations, items) self.assertAccessDependency(locations, items)
def testVertical(self) -> None: def testVertical(self) -> None:
"""locations that require either the Rope Dart or the Wingsuit""" """locations that require either the Rope Dart or the Wingsuit"""
locations = ["Ninja Village Seal - Tree House", "Key of Hope", "Howling Grotto Seal - Crushing Pits", locations = [
"Glacial Peak Seal - Ice Climbers", "Tower of Time Seal - Time Waster Seal", "Ninja Village Seal - Tree House", "Howling Grotto Seal - Crushing Pits",
"Underworld Seal - Rising Fanta", "Key of Symbiosis", "Glacial Peak Seal - Ice Climbers", "Tower of Time Seal - Time Waster",
"Elemental Skylands Seal - Water", "Elemental Skylands Seal - Fire", "Candle", "Underworld Seal - Rising Fanta", "Elemental Skylands - Key of Symbiosis",
"Climbing Claws", "Key of Hope", "Autumn Hills Seal - Trip Saws", "Elemental Skylands Seal - Water", "Elemental Skylands Seal - Fire", "Ninja Village - Candle",
"Autumn Hills Seal - Double Swing Saws", "Autumn Hills Seal - Spike Ball Swing", "Autumn Hills - Climbing Claws", "Autumn Hills - Key of Hope", "Autumn Hills Seal - Trip Saws",
"Autumn Hills Seal - Spike Ball Darts", "Necro", "Ruxxtin's Amulet", "Autumn Hills Seal - Double Swing Saws", "Autumn Hills Seal - Spike Ball Swing",
"Catacombs Seal - Triple Spike Crushers", "Catacombs Seal - Crusher Gauntlet", "Autumn Hills Seal - Spike Ball Darts", "Catacombs - Necro", "Catacombs - Ruxxtin's Amulet",
"Catacombs Seal - Dirty Pond", "Claustro", "Acro", "Bamboo Creek Seal - Spike Crushers and Doors", "Catacombs Seal - Triple Spike Crushers", "Catacombs Seal - Crusher Gauntlet",
"Bamboo Creek Seal - Spike Ball Pits", "Bamboo Creek Seal - Spike Crushers and Doors v2", "Catacombs Seal - Dirty Pond", "Bamboo Creek - Claustro", "Cloud Ruins - Acro",
"Howling Grotto Seal - Crushing Pits", "Howling Grotto Seal - Windy Saws and Balls", "Bamboo Creek Seal - Spike Crushers and Doors", "Bamboo Creek Seal - Spike Ball Pits",
"Demon King Crown", "Cloud Ruins Seal - Ghost Pit", "Cloud Ruins Seal - Toothbrush Alley", "Bamboo Creek Seal - Spike Crushers and Doors v2", "Howling Grotto Seal - Crushing Pits",
"Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room", "Howling Grotto Seal - Windy Saws and Balls", "Forlorn Temple - Demon King", "Cloud Ruins Seal - Ghost Pit",
"Tower of Time Seal - Lantern Climb", "Tower of Time Seal - Arcane Orbs", "Cloud Ruins Seal - Toothbrush Alley", "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room",
"Underworld Seal - Sharp and Windy Climb", "Underworld Seal - Fireball Wave", "Tower of Time Seal - Lantern Climb", "Tower of Time Seal - Arcane Orbs",
"Elemental Skylands Seal - Air", "Forlorn Temple Seal - Rocket Maze", "Underworld Seal - Sharp and Windy Climb", "Underworld Seal - Fireball Wave",
"Forlorn Temple Seal - Rocket Sunset", "Power Thistle", "Key of Strength", "Elemental Skylands Seal - Air", "Forlorn Temple Seal - Rocket Maze", "Forlorn Temple Seal - Rocket Sunset",
"Glacial Peak Seal - Projectile Spike Pit", "Glacial Peak Seal - Glacial Air Swag", "Searing Crags - Power Thistle", "Searing Crags - Key of Strength",
"Fairy Bottle", "Riviere Turquoise Seal - Flower Power", "Searing Crags Seal - Triple Ball Spinner", "Glacial Peak Seal - Projectile Spike Pit", "Glacial Peak Seal - Glacial Air Swag",
"Searing Crags Seal - Raining Rocks", "Searing Crags Seal - Rhythm Rocks", "Astral Seed", "Riviere Turquoise - Butterfly Matriarch", "Riviere Turquoise Seal - Flower Power",
"Astral Tea Leaves", "Rescue Phantom", "Autumn Hills Mega Shard", "Hidden Entrance Mega Shard", "Riviere Turquoise Seal - Launch of Faith",
"Sunny Day Mega Shard", "Down Under Mega Shard", "Catacombs Mega Shard", "Searing Crags Seal - Triple Ball Spinner", "Searing Crags Seal - Raining Rocks",
"Above Entrance Mega Shard", "Abandoned Mega Shard", "Time Loop Mega Shard", "Searing Crags Seal - Rhythm Rocks", "Ninja Village - Astral Seed", "Searing Crags - Astral Tea Leaves",
"Searing Crags Mega Shard", "Glacial Peak Mega Shard", "Cloud Entrance Mega Shard", "Rescue Phantom", "Autumn Hills Mega Shard", "Hidden Entrance Mega Shard", "Sunny Day Mega Shard",
"Time Warp Mega Shard", "Money Farm Room Mega Shard 1", "Money Farm Room Mega Shard 2", "Down Under Mega Shard", "Catacombs Mega Shard", "Above Entrance Mega Shard", "Abandoned Mega Shard",
"Quick Restock Mega Shard 1", "Quick Restock Mega Shard 2", "Earth Mega Shard", "Water Mega Shard", "Time Loop Mega Shard", "Searing Crags Mega Shard", "Glacial Peak Mega Shard", "Cloud Entrance Mega Shard",
"Leaf Golem", "Ruxxtin", "Emerald Golem"] "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"]] items = [["Wingsuit", "Rope Dart"]]
self.assertAccessDependency(locations, items) self.assertAccessDependency(locations, items)
def testAmulet(self) -> None: def testAmulet(self) -> None:
"""Locations that require Ruxxtin's Amulet""" """Locations that require Ruxxtin's Amulet"""
locations = ["Acro", "Cloud Ruins Seal - Ghost Pit", "Cloud Ruins Seal - Toothbrush Alley", locations = [
"Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room", "Cloud Entrance Mega Shard", "Cloud Ruins - Acro", "Cloud Ruins Seal - Ghost Pit", "Cloud Ruins Seal - Toothbrush Alley",
"Time Warp Mega Shard", "Money Farm Room Mega Shard 1", "Money Farm Room Mega Shard 2"] "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 # Cloud Ruins requires Ruxxtin's Amulet
items = [["Ruxxtin's Amulet"]] items = [["Ruxxtin's Amulet"]]
self.assertAccessDependency(locations, items) self.assertAccessDependency(locations, items)
def testBottle(self) -> None: def testFirefly(self) -> None:
"""Elemental Skylands and Corrupted Future require the Fairy Bottle""" """Elemental Skylands and Corrupted Future require the Magic Firefly"""
locations = ["Key of Symbiosis", "Elemental Skylands Seal - Air", "Elemental Skylands Seal - Fire", locations = [
"Elemental Skylands Seal - Water", "Key of Courage", "Earth Mega Shard", "Water Mega Shard"] "Elemental Skylands - Key of Symbiosis", "Elemental Skylands Seal - Air", "Elemental Skylands Seal - Fire",
items = [["Fairy Bottle"]] "Elemental Skylands Seal - Water", "Corrupted Future - Key of Courage", "Earth Mega Shard",
"Water Mega Shard"
]
items = [["Magic Firefly"]]
self.assertAccessDependency(locations, items) self.assertAccessDependency(locations, items)
def testCrests(self) -> None: def testCrests(self) -> None:
"""Test Key of Love nonsense""" """Test Key of Love nonsense"""
locations = ["Key of Love"] locations = ["Sunken Shrine - Key of Love"]
items = [["Sun Crest", "Moon Crest"]] items = [["Sun Crest", "Moon Crest"]]
self.assertAccessDependency(locations, items) self.assertAccessDependency(locations, items)
self.collect_all_but("Sun Crest") 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.remove(self.get_item_by_name("Moon Crest"))
self.collect_by_name("Sun 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: def testThistle(self) -> None:
"""I'm a chuckster!""" """I'm a chuckster!"""
locations = ["Key of Strength"] locations = ["Searing Crags - Key of Strength"]
items = [["Power Thistle"]] items = [["Power Thistle"]]
self.assertAccessDependency(locations, items) self.assertAccessDependency(locations, items)
def testCrown(self) -> None: def testCrown(self) -> None:
"""Crocomire but not""" """Crocomire but not"""
locations = ["Key of Courage"] locations = ["Corrupted Future - Key of Courage"]
items = [["Demon King Crown"]] items = [["Demon King Crown"]]
self.assertAccessDependency(locations, items) self.assertAccessDependency(locations, items)
@ -140,11 +156,11 @@ class ItemsAccessTest(MessengerTestBase):
def testSelfLockingItems(self) -> None: def testSelfLockingItems(self) -> None:
"""Force items that can be self locked to ensure it's valid placement.""" """Force items that can be self locked to ensure it's valid placement."""
location_lock_pairs = { location_lock_pairs = {
"Key of Strength": ["Power Thistle"], "Searing Crags - Key of Strength": ["Power Thistle"],
"Key of Love": ["Sun Crest", "Moon Crest"], "Sunken Shrine - Key of Love": ["Sun Crest", "Moon Crest"],
"Key of Courage": ["Demon King Crown"], "Corrupted Future - Key of Courage": ["Demon King Crown"],
"Acro": ["Ruxxtin's Amulet"], "Cloud Ruins - Acro": ["Ruxxtin's Amulet"],
"Demon King Crown": PHOBEKINS "Forlorn Temple - Demon King": PHOBEKINS
} }
for loc in location_lock_pairs: for loc in location_lock_pairs:
@ -152,4 +168,3 @@ class ItemsAccessTest(MessengerTestBase):
item = self.get_item_by_name(item_name) item = self.get_item_by_name(item_name)
with self.subTest("Fulfills Accessibility", location=loc, item=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)) self.assertTrue(self.multiworld.get_location(loc, self.player).can_fill(self.multiworld.state, item, True))

View File

@ -11,35 +11,33 @@ class HardLogicTest(MessengerTestBase):
"""Test the locations that still require wingsuit or rope dart.""" """Test the locations that still require wingsuit or rope dart."""
locations = [ locations = [
# tower of time # 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", "Tower of Time Seal - Arcane Orbs",
# ninja village # 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 # 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 - Trip Saws", "Autumn Hills Seal - Double Swing Saws",
"Autumn Hills Seal - Spike Ball Swing", "Autumn Hills Seal - Spike Ball Darts", "Autumn Hills Seal - Spike Ball Swing", "Autumn Hills Seal - Spike Ball Darts",
# forlorn temple # forlorn temple
"Demon King Crown", "Forlorn Temple - Demon King",
"Forlorn Temple Seal - Rocket Maze", "Forlorn Temple Seal - Rocket Sunset", "Forlorn Temple Seal - Rocket Maze", "Forlorn Temple Seal - Rocket Sunset",
# catacombs # 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", "Catacombs Seal - Triple Spike Crushers", "Catacombs Seal - Crusher Gauntlet", "Catacombs Seal - Dirty Pond",
# bamboo creek # 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", "Bamboo Creek Seal - Spike Ball Pits",
"Bamboo Creek Seal - Spike Crushers and Doors v2", "Bamboo Creek Seal - Spike Crushers and Doors v2",
# howling grotto # howling grotto
"Emerald Golem", "Howling Grotto Seal - Crushing Pits", "Howling Grotto Seal - Crushing Pits", "Howling Grotto - Emerald Golem", "Howling Grotto Seal - Crushing Pits", "Howling Grotto Seal - Crushing Pits",
# glacial peak # searing crags
"Glacial Peak Seal - Ice Climbers", "Searing Crags - Astral Tea Leaves",
# cloud ruins # 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", "Cloud Ruins Seal - Toothbrush Alley", "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room",
# underworld # underworld
"Underworld Seal - Rising Fanta", "Underworld Seal - Sharp and Windy Climb", "Underworld Seal - Rising Fanta", "Underworld Seal - Sharp and Windy Climb",
# riviere turquoise
"Fairy Bottle", "Riviere Turquoise Seal - Flower Power",
# elemental skylands # elemental skylands
"Elemental Skylands Seal - Air", "Elemental Skylands Seal - Air",
# phantom # phantom
@ -52,15 +50,15 @@ class HardLogicTest(MessengerTestBase):
"""Windmill Shuriken isn't progression on normal difficulty, so test it's marked correctly and required.""" """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) self.assertEqual(ItemClassification.progression, self.get_item_by_name("Windmill Shuriken").classification)
windmill_locs = [ windmill_locs = [
"Key of Strength", "Searing Crags - Key of Strength",
"Key of Symbiosis", "Elemental Skylands - Key of Symbiosis",
"Underworld Seal - Fireball Wave", "Underworld Seal - Fireball Wave",
] ]
for loc in windmill_locs: for loc in windmill_locs:
with self.subTest("can't reach location with nothing", location=loc): with self.subTest("can't reach location with nothing", location=loc):
self.assertFalse(self.can_reach_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) self.collect(items)
for loc in windmill_locs: for loc in windmill_locs:
with self.subTest("can reach with Windmill", location=loc): with self.subTest("can reach with Windmill", location=loc):
@ -77,13 +75,6 @@ class HardLogicTest(MessengerTestBase):
self.assertTrue(self.can_reach_location(special_loc)) self.assertTrue(self.can_reach_location(special_loc))
class ChallengingLogicTest(MessengerTestBase):
options = {
"shuffle_seals": "false",
"logic_level": "challenging",
}
class NoLogicTest(MessengerTestBase): class NoLogicTest(MessengerTestBase):
options = { options = {
"logic_level": "oob", "logic_level": "oob",
@ -92,17 +83,14 @@ class NoLogicTest(MessengerTestBase):
def testAccess(self) -> None: def testAccess(self) -> None:
"""Test the locations with rules still require things.""" """Test the locations with rules still require things."""
all_locations = [ all_locations = [
"Claustro", "Key of Strength", "Key of Symbiosis", "Key of Love", "Pyro", "Key of Chaos", "Key of Courage", "Bamboo Creek - Claustro", "Searing Crags - Key of Strength", "Elemental Skylands - Key of Symbiosis",
"Autumn Hills Seal - Spike Ball Darts", "Ninja Village Seal - Tree House", "Underworld Seal - Fireball Wave", "Sunken Shrine - Key of Love", "Searing Crags - Pyro", "Underworld - Key of Chaos",
"Tower of Time Seal - Time Waster Seal", "Rescue Phantom", "Elemental Skylands Seal - Air", "Corrupted Future - Key of Courage", "Autumn Hills Seal - Spike Ball Darts",
"Elemental Skylands Seal - Water", "Elemental Skylands Seal - Fire", "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: for loc in all_locations:
with self.subTest("Default unreachables", location=loc): with self.subTest("Default unreachables", location=loc):
self.assertFalse(self.can_reach_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) self.assertBeatable(True)

View File

@ -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))

View File

@ -2,18 +2,6 @@ from BaseClasses import ItemClassification, CollectionState
from . import MessengerTestBase 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): class AllSealsRequired(MessengerTestBase):
options = { options = {
"shuffle_seals": "false", "shuffle_seals": "false",