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
# listing individual groups first for easy lookup
from .Shop import SHOP_ITEMS, FIGURINES
NOTES = [
"Key of Hope",
@ -13,15 +14,16 @@ NOTES = [
PROG_ITEMS = [
"Wingsuit",
"Rope Dart",
"Ninja Tabi",
"Lightfoot Tabi",
"Power Thistle",
"Demon King Crown",
"Ruxxtin's Amulet",
"Fairy Bottle",
"Magic Firefly",
"Sun Crest",
"Moon Crest",
# "Astral Seed",
# "Astral Tea Leaves",
"Money Wrench",
]
PHOBEKINS = [
@ -35,13 +37,22 @@ USEFUL_ITEMS = [
"Windmill Shuriken",
]
FILLER = {
"Time Shard": 5,
"Time Shard (10)": 10,
"Time Shard (50)": 20,
"Time Shard (100)": 20,
"Time Shard (300)": 10,
"Time Shard (500)": 5,
}
# item_name_to_id needs to be deterministic and match upstream
ALL_ITEMS = [
*NOTES,
"Windmill Shuriken",
"Wingsuit",
"Rope Dart",
"Ninja Tabi",
"Lightfoot Tabi",
# "Astral Seed",
# "Astral Tea Leaves",
"Candle",
@ -49,12 +60,15 @@ ALL_ITEMS = [
"Power Thistle",
"Demon King Crown",
"Ruxxtin's Amulet",
"Fairy Bottle",
"Magic Firefly",
"Sun Crest",
"Moon Crest",
*PHOBEKINS,
"Power Seal",
"Time Shard", # there's 45 separate instances of this in the client lookup, but hopefully we don't care?
*FILLER,
*SHOP_ITEMS,
*FIGURINES,
"Money Wrench",
]
# locations
@ -62,100 +76,38 @@ ALL_ITEMS = [
# order must be exactly the same as upstream
ALWAYS_LOCATIONS = [
# notes
"Key of Love",
"Key of Courage",
"Key of Chaos",
"Key of Symbiosis",
"Key of Strength",
"Key of Hope",
"Sunken Shrine - Key of Love",
"Corrupted Future - Key of Courage",
"Underworld - Key of Chaos",
"Elemental Skylands - Key of Symbiosis",
"Searing Crags - Key of Strength",
"Autumn Hills - Key of Hope",
# upgrades
"Wingsuit",
"Rope Dart",
"Ninja Tabi",
"Climbing Claws",
"Howling Grotto - Wingsuit",
"Searing Crags - Rope Dart",
"Sunken Shrine - Lightfoot Tabi",
"Autumn Hills - Climbing Claws",
# quest items
"Astral Seed",
"Astral Tea Leaves",
"Candle",
"Seashell",
"Power Thistle",
"Demon King Crown",
"Ruxxtin's Amulet",
"Fairy Bottle",
"Sun Crest",
"Moon Crest",
"Ninja Village - Astral Seed",
"Searing Crags - Astral Tea Leaves",
"Ninja Village - Candle",
"Quillshroom Marsh - Seashell",
"Searing Crags - Power Thistle",
"Forlorn Temple - Demon King",
"Catacombs - Ruxxtin's Amulet",
"Riviere Turquoise - Butterfly Matriarch",
"Sunken Shrine - Sun Crest",
"Sunken Shrine - Moon Crest",
# phobekins
"Necro",
"Pyro",
"Claustro",
"Acro",
]
SEALS = [
"Ninja Village Seal - Tree House",
"Autumn Hills Seal - Trip Saws",
"Autumn Hills Seal - Double Swing Saws",
"Autumn Hills Seal - Spike Ball Swing",
"Autumn Hills Seal - Spike Ball Darts",
"Catacombs Seal - Triple Spike Crushers",
"Catacombs Seal - Crusher Gauntlet",
"Catacombs Seal - Dirty Pond",
"Bamboo Creek Seal - Spike Crushers and Doors",
"Bamboo Creek Seal - Spike Ball Pits",
"Bamboo Creek Seal - Spike Crushers and Doors v2",
"Howling Grotto Seal - Windy Saws and Balls",
"Howling Grotto Seal - Crushing Pits",
"Howling Grotto Seal - Breezy Crushers",
"Quillshroom Marsh Seal - Spikey Window",
"Quillshroom Marsh Seal - Sand Trap",
"Quillshroom Marsh Seal - Do the Spike Wave",
"Searing Crags Seal - Triple Ball Spinner",
"Searing Crags Seal - Raining Rocks",
"Searing Crags Seal - Rhythm Rocks",
"Glacial Peak Seal - Ice Climbers",
"Glacial Peak Seal - Projectile Spike Pit",
"Glacial Peak Seal - Glacial Air Swag",
"Tower of Time Seal - Time Waster Seal",
"Tower of Time Seal - Lantern Climb",
"Tower of Time Seal - Arcane Orbs",
"Cloud Ruins Seal - Ghost Pit",
"Cloud Ruins Seal - Toothbrush Alley",
"Cloud Ruins Seal - Saw Pit",
"Cloud Ruins Seal - Money Farm Room",
"Underworld Seal - Sharp and Windy Climb",
"Underworld Seal - Spike Wall",
"Underworld Seal - Fireball Wave",
"Underworld Seal - Rising Fanta",
"Forlorn Temple Seal - Rocket Maze",
"Forlorn Temple Seal - Rocket Sunset",
"Sunken Shrine Seal - Ultra Lifeguard",
"Sunken Shrine Seal - Waterfall Paradise",
"Sunken Shrine Seal - Tabi Gauntlet",
"Riviere Turquoise Seal - Bounces and Balls",
"Riviere Turquoise Seal - Launch of Faith",
"Riviere Turquoise Seal - Flower Power",
"Elemental Skylands Seal - Air",
"Elemental Skylands Seal - Water",
"Elemental Skylands Seal - Fire",
"Catacombs - Necro",
"Searing Crags - Pyro",
"Bamboo Creek - Claustro",
"Cloud Ruins - Acro",
]
BOSS_LOCATIONS = [
"Leaf Golem",
"Ruxxtin",
"Emerald Golem",
"Queen of Quills",
"Autumn Hills - Leaf Golem",
"Catacombs - Ruxxtin",
"Howling Grotto - Emerald Golem",
"Quillshroom Marsh - Queen of Quills",
]

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):
@ -10,16 +13,16 @@ class MessengerAccessibility(Accessibility):
class Logic(Choice):
"""
The level of logic to use when determining what locations in your world are accessible.
Normal can require damage boosts, but otherwise approachable for someone who has beaten the game.
Hard has some easier speedrunning tricks in logic. May need to leash.
Challenging contains more medium and hard difficulty speedrunning tricks.
OoB places everything with the minimum amount of rules possible. Expect to do OoB. Not guaranteed completable.
Normal: can require damage boosts, but otherwise approachable for someone who has beaten the game.
Hard: has leashing, normal clips, time warps and turtle boosting in logic.
OoB: places everything with the minimum amount of rules possible. Expect to do OoB. Not guaranteed completable.
"""
display_name = "Logic Level"
option_normal = 0
option_hard = 1
option_challenging = 2
option_oob = 3
option_oob = 2
alias_challenging = 1
class PowerSeals(DefaultOnToggle):
@ -68,6 +71,64 @@ class RequiredSeals(Range):
default = range_end
class ShopPrices(Range):
"""Percentage modifier for shuffled item prices in shops"""
display_name = "Shop Prices Modifier"
range_start = 25
range_end = 400
default = 100
def planned_price(location: str) -> Dict[Optional, Or]:
return {
Optional(location): Or(
And(int, lambda n: n >= 0),
{
Optional(And(int, lambda n: n >= 0)): And(int, lambda n: n >= 0)
}
)
}
class PlannedShopPrices(OptionDict):
"""Plan specific prices on shop slots. Supports weighting"""
display_name = "Shop Price Plando"
schema = Schema({
**planned_price("Karuta Plates"),
**planned_price("Serendipitous Bodies"),
**planned_price("Path of Resilience"),
**planned_price("Kusari Jacket"),
**planned_price("Energy Shuriken"),
**planned_price("Serendipitous Minds"),
**planned_price("Prepared Mind"),
**planned_price("Meditation"),
**planned_price("Rejuvenative Spirit"),
**planned_price("Centered Mind"),
**planned_price("Strike of the Ninja"),
**planned_price("Second Wind"),
**planned_price("Currents Master"),
**planned_price("Aerobatics Warrior"),
**planned_price("Demon's Bane"),
**planned_price("Devil's Due"),
**planned_price("Time Sense"),
**planned_price("Power Sense"),
**planned_price("Focused Power Sense"),
**planned_price("Green Kappa Figurine"),
**planned_price("Blue Kappa Figurine"),
**planned_price("Ountarde Figurine"),
**planned_price("Red Kappa Figurine"),
**planned_price("Demon King Figurine"),
**planned_price("Quillshroom Figurine"),
**planned_price("Jumping Quillshroom Figurine"),
**planned_price("Scurubu Figurine"),
**planned_price("Jumping Scurubu Figurine"),
**planned_price("Wallaxer Figurine"),
**planned_price("Barmath'azel Figurine"),
**planned_price("Queen of Quills Figurine"),
**planned_price("Demon Hive Figurine"),
})
messenger_options = {
"accessibility": MessengerAccessibility,
"start_inventory": StartInventoryPool,
@ -79,5 +140,7 @@ messenger_options = {
"notes_needed": NotesNeeded,
"total_seals": AmountSeals,
"percent_seals_required": RequiredSeals,
"shop_price": ShopPrices,
"shop_price_plan": PlannedShopPrices,
"death_link": DeathLink,
}

View File

@ -5,27 +5,60 @@ REGIONS: Dict[str, List[str]] = {
"Tower HQ": [],
"The Shop": [],
"Tower of Time": [],
"Ninja Village": ["Candle", "Astral Seed"],
"Autumn Hills": ["Climbing Claws", "Key of Hope", "Leaf Golem"],
"Forlorn Temple": ["Demon King Crown"],
"Catacombs": ["Necro", "Ruxxtin's Amulet", "Ruxxtin"],
"Bamboo Creek": ["Claustro"],
"Howling Grotto": ["Wingsuit", "Emerald Golem"],
"Quillshroom Marsh": ["Seashell", "Queen of Quills"],
"Searing Crags": ["Rope Dart"],
"Searing Crags Upper": ["Power Thistle", "Key of Strength", "Astral Tea Leaves"],
"Ninja Village": ["Ninja Village - Candle", "Ninja Village - Astral Seed"],
"Autumn Hills": ["Autumn Hills - Climbing Claws", "Autumn Hills - Key of Hope", "Autumn Hills - Leaf Golem"],
"Forlorn Temple": ["Forlorn Temple - Demon King"],
"Catacombs": ["Catacombs - Necro", "Catacombs - Ruxxtin's Amulet", "Catacombs - Ruxxtin"],
"Bamboo Creek": ["Bamboo Creek - Claustro"],
"Howling Grotto": ["Howling Grotto - Wingsuit", "Howling Grotto - Emerald Golem"],
"Quillshroom Marsh": ["Quillshroom Marsh - Seashell", "Quillshroom Marsh - Queen of Quills"],
"Searing Crags": ["Searing Crags - Rope Dart"],
"Searing Crags Upper": ["Searing Crags - Power Thistle", "Searing Crags - Key of Strength",
"Searing Crags - Astral Tea Leaves"],
"Glacial Peak": [],
"Cloud Ruins": [],
"Cloud Ruins Right": ["Acro"],
"Underworld": ["Pyro", "Key of Chaos"],
"Cloud Ruins Right": ["Cloud Ruins - Acro"],
"Underworld": ["Searing Crags - Pyro", "Underworld - Key of Chaos"],
"Dark Cave": [],
"Riviere Turquoise": ["Fairy Bottle"],
"Sunken Shrine": ["Ninja Tabi", "Sun Crest", "Moon Crest", "Key of Love"],
"Elemental Skylands": ["Key of Symbiosis"],
"Corrupted Future": ["Key of Courage"],
"Riviere Turquoise Entrance": [],
"Riviere Turquoise": ["Riviere Turquoise - Butterfly Matriarch"],
"Sunken Shrine": ["Sunken Shrine - Lightfoot Tabi", "Sunken Shrine - Sun Crest", "Sunken Shrine - Moon Crest",
"Sunken Shrine - Key of Love"],
"Elemental Skylands": ["Elemental Skylands - Key of Symbiosis"],
"Corrupted Future": ["Corrupted Future - Key of Courage"],
"Music Box": ["Rescue Phantom"],
}
"""seal locations have the region in their name and may not need to be created so skip them here"""
SEALS: Dict[str, List[str]] = {
"Ninja Village": ["Ninja Village Seal - Tree House"],
"Autumn Hills": ["Autumn Hills Seal - Trip Saws", "Autumn Hills Seal - Double Swing Saws",
"Autumn Hills Seal - Spike Ball Swing", "Autumn Hills Seal - Spike Ball Darts"],
"Catacombs": ["Catacombs Seal - Triple Spike Crushers", "Catacombs Seal - Crusher Gauntlet",
"Catacombs Seal - Dirty Pond"],
"Bamboo Creek": ["Bamboo Creek Seal - Spike Crushers and Doors", "Bamboo Creek Seal - Spike Ball Pits",
"Bamboo Creek Seal - Spike Crushers and Doors v2"],
"Howling Grotto": ["Howling Grotto Seal - Windy Saws and Balls", "Howling Grotto Seal - Crushing Pits",
"Howling Grotto Seal - Breezy Crushers"],
"Quillshroom Marsh": ["Quillshroom Marsh Seal - Spikey Window", "Quillshroom Marsh Seal - Sand Trap",
"Quillshroom Marsh Seal - Do the Spike Wave"],
"Searing Crags": ["Searing Crags Seal - Triple Ball Spinner"],
"Searing Crags Upper": ["Searing Crags Seal - Raining Rocks", "Searing Crags Seal - Rhythm Rocks"],
"Glacial Peak": ["Glacial Peak Seal - Ice Climbers", "Glacial Peak Seal - Projectile Spike Pit",
"Glacial Peak Seal - Glacial Air Swag"],
"Tower of Time": ["Tower of Time Seal - Time Waster", "Tower of Time Seal - Lantern Climb",
"Tower of Time Seal - Arcane Orbs"],
"Cloud Ruins Right": ["Cloud Ruins Seal - Ghost Pit", "Cloud Ruins Seal - Toothbrush Alley",
"Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room"],
"Underworld": ["Underworld Seal - Sharp and Windy Climb", "Underworld Seal - Spike Wall",
"Underworld Seal - Fireball Wave", "Underworld Seal - Rising Fanta"],
"Forlorn Temple": ["Forlorn Temple Seal - Rocket Maze", "Forlorn Temple Seal - Rocket Sunset"],
"Sunken Shrine": ["Sunken Shrine Seal - Ultra Lifeguard", "Sunken Shrine Seal - Waterfall Paradise",
"Sunken Shrine Seal - Tabi Gauntlet"],
"Riviere Turquoise Entrance": ["Riviere Turquoise Seal - Bounces and Balls"],
"Riviere Turquoise": ["Riviere Turquoise Seal - Launch of Faith", "Riviere Turquoise Seal - Flower Power"],
"Elemental Skylands": ["Elemental Skylands Seal - Air", "Elemental Skylands Seal - Water",
"Elemental Skylands Seal - Fire"]
}
MEGA_SHARDS: Dict[str, List[str]] = {
"Autumn Hills": ["Autumn Hills Mega Shard", "Hidden Entrance Mega Shard"],
@ -41,15 +74,16 @@ MEGA_SHARDS: Dict[str, List[str]] = {
"Underworld": ["Under Entrance Mega Shard", "Hot Tub Mega Shard", "Projectile Pit Mega Shard"],
"Forlorn Temple": ["Sunny Day Mega Shard", "Down Under Mega Shard"],
"Sunken Shrine": ["Mega Shard of the Moon", "Beginner's Mega Shard", "Mega Shard of the Stars", "Mega Shard of the Sun"],
"Riviere Turquoise": ["Waterfall Mega Shard", "Quick Restock Mega Shard 1", "Quick Restock Mega Shard 2"],
"RIviere Turquoise Entrance": ["Waterfall Mega Shard"],
"Riviere Turquoise": ["Quick Restock Mega Shard 1", "Quick Restock Mega Shard 2"],
"Elemental Skylands": ["Earth Mega Shard", "Water Mega Shard"],
}
REGION_CONNECTIONS: Dict[str, Set[str]] = {
"Menu": {"Tower HQ"},
"Tower HQ": {"Autumn Hills", "Howling Grotto", "Searing Crags", "Glacial Peak", "Tower of Time", "Riviere Turquoise",
"Sunken Shrine", "Corrupted Future", "The Shop", "Music Box"},
"Tower HQ": {"Autumn Hills", "Howling Grotto", "Searing Crags", "Glacial Peak", "Tower of Time",
"Riviere Turquoise Entrance", "Sunken Shrine", "Corrupted Future", "The Shop", "Music Box"},
"Tower of Time": set(),
"Ninja Village": set(),
"Autumn Hills": {"Ninja Village", "Forlorn Temple", "Catacombs"},
@ -64,7 +98,8 @@ REGION_CONNECTIONS: Dict[str, Set[str]] = {
"Cloud Ruins": {"Cloud Ruins Right"},
"Cloud Ruins Right": {"Underworld"},
"Underworld": set(),
"Dark Cave": {"Catacombs", "Riviere Turquoise"},
"Dark Cave": {"Catacombs", "Riviere Turquoise Entrance"},
"Riviere Turquoise Entrance": {"Riviere Turquoise"},
"Riviere Turquoise": set(),
"Sunken Shrine": {"Howling Grotto"},
"Elemental Skylands": set(),

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

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 BaseClasses import Region, Location, Item, ItemClassification, Entrance
from .Constants import SEALS, NOTES, PROG_ITEMS, PHOBEKINS, USEFUL_ITEMS
from BaseClasses import Region, Location, Item, ItemClassification, Entrance, CollectionState
from .Constants import NOTES, PROG_ITEMS, PHOBEKINS, USEFUL_ITEMS
from .Options import Goal
from .Regions import REGIONS, MEGA_SHARDS
from .Regions import REGIONS, SEALS, MEGA_SHARDS
from .Shop import SHOP_ITEMS, PROG_SHOP_ITEMS, USEFUL_SHOP_ITEMS, FIGURINES
if TYPE_CHECKING:
from . import MessengerWorld
@ -20,14 +21,21 @@ class MessengerRegion(Region):
def add_locations(self, name_to_id: Dict[str, int]) -> None:
for loc in REGIONS[self.name]:
self.locations.append(MessengerLocation(loc, self, name_to_id.get(loc, None)))
if self.name == "The Shop" and self.multiworld.goal[self.player] > Goal.option_open_music_box:
self.locations.append(MessengerLocation("Shop Chest", self, name_to_id.get("Shop Chest", None)))
# putting some dumb special case for searing crags and ToT so i can split them into 2 regions
if self.multiworld.shuffle_seals[self.player] and self.name not in {"Searing Crags", "Tower HQ", "Cloud Ruins"}:
self.locations += [MessengerLocation(seal_loc, self, name_to_id.get(seal_loc, None))
for seal_loc in SEALS if seal_loc.startswith(self.name.split(" ")[0])]
if self.name == "The Shop":
if self.multiworld.goal[self.player] > Goal.option_open_music_box:
self.locations.append(MessengerLocation("Shop Chest", self, None))
self.locations += [MessengerShopLocation(f"The Shop - {shop_loc}", self,
name_to_id[f"The Shop - {shop_loc}"])
for shop_loc in SHOP_ITEMS]
self.locations += [MessengerShopLocation(figurine, self, name_to_id[figurine])
for figurine in FIGURINES]
elif self.name == "Tower HQ":
self.locations.append(MessengerLocation("Money Wrench", self, name_to_id["Money Wrench"]))
if self.multiworld.shuffle_seals[self.player] and self.name in SEALS:
self.locations += [MessengerLocation(seal_loc, self, name_to_id[seal_loc])
for seal_loc in SEALS[self.name]]
if self.multiworld.shuffle_shards[self.player] and self.name in MEGA_SHARDS:
self.locations += [MessengerLocation(shard, self, name_to_id.get(shard, None))
self.locations += [MessengerLocation(shard, self, name_to_id[shard])
for shard in MEGA_SHARDS[self.name]]
def add_exits(self, exits: Set[str]) -> None:
@ -46,13 +54,33 @@ class MessengerLocation(Location):
self.place_locked_item(MessengerItem(name, parent.player, None))
class MessengerShopLocation(MessengerLocation):
def cost(self) -> int:
name = self.name.replace("The Shop - ", "") # TODO use `remove_prefix` when 3.8 finally gets dropped
world: MessengerWorld = self.parent_region.multiworld.worlds[self.player]
return world.shop_prices.get(name, world.figurine_prices.get(name))
def can_afford(self, state: CollectionState) -> bool:
world: MessengerWorld = state.multiworld.worlds[self.player]
cost = self.cost() * 2
if cost >= 1000:
cost *= 2
can_afford = state.has("Shards", self.player, min(cost, world.total_shards))
if "Figurine" in self.name:
return state.has("Money Wrench", self.player) and can_afford
return can_afford
class MessengerItem(Item):
game = "The Messenger"
def __init__(self, name: str, player: int, item_id: Optional[int] = None, override_progression: bool = False) -> None:
if name in {*NOTES, *PROG_ITEMS, *PHOBEKINS} or item_id is None or override_progression:
def __init__(self, name: str, player: int, item_id: Optional[int] = None, override_progression: bool = False,
count: int = 0) -> None:
if count:
item_class = ItemClassification.progression_skip_balancing
elif item_id is None or override_progression or name in {*NOTES, *PROG_ITEMS, *PHOBEKINS, *PROG_SHOP_ITEMS}:
item_class = ItemClassification.progression
elif name in USEFUL_ITEMS:
elif name in {*USEFUL_ITEMS, *USEFUL_SHOP_ITEMS}:
item_class = ItemClassification.useful
else:
item_class = ItemClassification.filler

View File

@ -1,11 +1,12 @@
import logging
from typing import Dict, Any, Optional, List
from typing import Dict, Any, Optional
from BaseClasses import Tutorial, ItemClassification, MultiWorld
from BaseClasses import Tutorial, ItemClassification, CollectionState, Item, MultiWorld
from worlds.AutoWorld import World, WebWorld
from .Constants import NOTES, PHOBEKINS, ALL_ITEMS, ALWAYS_LOCATIONS, SEALS, BOSS_LOCATIONS
from .Constants import NOTES, PHOBEKINS, ALL_ITEMS, ALWAYS_LOCATIONS, BOSS_LOCATIONS, FILLER
from .Options import messenger_options, NotesNeeded, Goal, PowerSeals, Logic
from .Regions import REGIONS, REGION_CONNECTIONS, MEGA_SHARDS
from .Regions import REGIONS, REGION_CONNECTIONS, SEALS, MEGA_SHARDS
from .Shop import SHOP_ITEMS, shuffle_shop_prices, FIGURINES
from .SubClasses import MessengerRegion, MessengerItem
from . import Rules
@ -41,7 +42,6 @@ class MessengerWorld(World):
"Crest": {"Sun Crest", "Moon Crest"},
"Phobe": set(PHOBEKINS),
"Phobekin": set(PHOBEKINS),
"Shuriken": {"Windmill Shuriken"},
}
option_definitions = messenger_options
@ -49,30 +49,35 @@ class MessengerWorld(World):
base_offset = 0xADD_000
item_name_to_id = {item: item_id
for item_id, item in enumerate(ALL_ITEMS, base_offset)}
mega_shard_locs = [shard for region in MEGA_SHARDS for shard in MEGA_SHARDS[region]]
seal_locs = [seal for seals in SEALS.values() for seal in seals]
mega_shard_locs = [shard for shards in MEGA_SHARDS.values() for shard in shards]
shop_locs = [f"The Shop - {shop_loc}" for shop_loc in SHOP_ITEMS]
location_name_to_id = {location: location_id
for location_id, location in
enumerate([
*ALWAYS_LOCATIONS,
*SEALS,
*seal_locs,
*mega_shard_locs,
*BOSS_LOCATIONS,
*shop_locs,
*FIGURINES,
"Money Wrench",
], base_offset)}
data_version = 2
required_client_version = (0, 3, 9)
data_version = 3
required_client_version = (0, 4, 0)
web = MessengerWeb()
total_seals: int = 0
required_seals: int = 0
total_shards: int
shop_prices: Dict[str, int]
figurine_prices: Dict[str, int]
@classmethod
def stage_assert_generate(cls, multiworld: MultiWorld) -> None:
for player in multiworld.get_game_players(cls.game):
player_name = multiworld.player_name[player] = multiworld.get_player_name(player).replace("_", " ")
if not all(c.isalnum() or c in "- " for c in player_name):
raise ValueError(f"Player name {player_name} is not alpha-numeric.")
def __init__(self, multiworld: MultiWorld, player: int):
super().__init__(multiworld, player)
self.total_shards = 0
def generate_early(self) -> None:
if self.multiworld.goal[self.player] == Goal.option_power_seal_hunt:
@ -85,27 +90,32 @@ class MessengerWorld(World):
region.add_exits(REGION_CONNECTIONS[region.name])
def create_items(self) -> None:
itempool: List[MessengerItem] = []
if self.multiworld.goal[self.player] == Goal.option_open_music_box:
notes = self.multiworld.random.sample(NOTES, k=len(NOTES))
precollected_notes_amount = NotesNeeded.range_end - self.multiworld.notes_needed[self.player]
if precollected_notes_amount:
for note in notes[:precollected_notes_amount]:
self.multiworld.push_precollected(self.create_item(note))
itempool += [self.create_item(note) for note in notes[precollected_notes_amount:]]
itempool += [self.create_item(item)
# create items that are always in the item pool
itempool = [
self.create_item(item)
for item in self.item_name_to_id
if item not in
{
"Power Seal", "Time Shard", *NOTES,
"Power Seal", *NOTES,
*{collected_item.name for collected_item in self.multiworld.precollected_items[self.player]},
# this is a set and currently won't create items for anything that appears in here at all
# if we get in a position where this can have duplicates of items that aren't Power Seals
# or Time shards, this will need to be redone.
}]
} and "Time Shard" not in item
]
if self.multiworld.goal[self.player] == Goal.option_power_seal_hunt:
if self.multiworld.goal[self.player] == Goal.option_open_music_box:
# make a list of all notes except those in the player's defined starting inventory, and adjust the
# amount we need to put in the itempool and precollect based on that
notes = [note for note in NOTES if note not in self.multiworld.precollected_items[self.player]]
self.multiworld.per_slot_randoms[self.player].shuffle(notes)
precollected_notes_amount = NotesNeeded.range_end - \
self.multiworld.notes_needed[self.player] - \
(len(NOTES) - len(notes))
if precollected_notes_amount:
for note in notes[:precollected_notes_amount]:
self.multiworld.push_precollected(self.create_item(note))
notes = notes[precollected_notes_amount:]
itempool += [self.create_item(note) for note in notes]
elif self.multiworld.goal[self.player] == Goal.option_power_seal_hunt:
total_seals = min(len(self.multiworld.get_unfilled_locations(self.player)) - len(itempool),
self.multiworld.total_seals[self.player].value)
if total_seals < self.total_seals:
@ -118,39 +128,41 @@ class MessengerWorld(World):
seals[i].classification = ItemClassification.progression_skip_balancing
itempool += seals
itempool += [self.create_filler()
for _ in range(len(self.multiworld.get_unfilled_locations(self.player)) - len(itempool))]
itempool += [self.create_item(filler_item)
for filler_item in
self.multiworld.random.choices(
list(FILLER),
weights=list(FILLER.values()),
k=len(self.multiworld.get_unfilled_locations(self.player)) - len(itempool)
)]
self.multiworld.itempool += itempool
def set_rules(self) -> None:
self.shop_prices, self.figurine_prices = shuffle_shop_prices(self)
logic = self.multiworld.logic_level[self.player]
if logic == Logic.option_normal:
Rules.MessengerRules(self).set_messenger_rules()
elif logic == Logic.option_hard:
Rules.MessengerHardRules(self).set_messenger_rules()
elif logic == Logic.option_challenging:
Rules.MessengerChallengeRules(self).set_messenger_rules()
else:
Rules.MessengerOOBRules(self).set_messenger_rules()
def fill_slot_data(self) -> Dict[str, Any]:
locations: Dict[int, List[str]] = {}
for loc in self.multiworld.get_filled_locations(self.player):
if loc.item.code:
locations[loc.address] = [loc.item.name, self.multiworld.player_name[loc.item.player]]
shop_prices = {SHOP_ITEMS[item].internal_name: price for item, price in self.shop_prices.items()}
figure_prices = {FIGURINES[item].internal_name: price for item, price in self.figurine_prices.items()}
return {
"deathlink": self.multiworld.death_link[self.player].value,
"goal": self.multiworld.goal[self.player].current_key,
"music_box": self.multiworld.music_box[self.player].value,
"required_seals": self.required_seals,
"locations": locations,
"settings": {
"Difficulty": "Basic" if not self.multiworld.shuffle_seals[self.player] else "Advanced",
"Mega Shards": self.multiworld.shuffle_shards[self.player].value
},
"mega_shards": self.multiworld.shuffle_shards[self.player].value,
"logic": self.multiworld.logic_level[self.player].current_key,
"shop": shop_prices,
"figures": figure_prices,
"max_price": self.total_shards,
}
def get_filler_item_name(self) -> str:
@ -158,6 +170,21 @@ class MessengerWorld(World):
def create_item(self, name: str) -> MessengerItem:
item_id: Optional[int] = self.item_name_to_id.get(name, None)
override_prog = name in {"Windmill Shuriken"} and getattr(self, "multiworld") is not None \
and self.multiworld.logic_level[self.player] > Logic.option_normal
return MessengerItem(name, self.player, item_id, override_prog)
override_prog = getattr(self, "multiworld") is not None and \
name in {"Windmill Shuriken"} and \
self.multiworld.logic_level[self.player] > Logic.option_normal
count = 0
if "Time Shard " in name:
count = int(name.strip("Time Shard ()"))
count = count if count >= 100 else 0
self.total_shards += count
return MessengerItem(name, self.player, item_id, override_prog, count)
def collect_item(self, state: "CollectionState", item: "Item", remove: bool = False) -> Optional[str]:
if item.advancement and "Time Shard" in item.name:
shard_count = int(item.name.strip("Time Shard ()"))
if remove:
shard_count = -shard_count
state.prog_items["Shards", self.player] += shard_count
return super().collect_item(state, item, remove)

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
Time HQ with the past section finished, all area portals open, and with the cloud step, and climbing claws already
obtained. You'll be forced to do sections of the game in different ways with your current abilities. Currently, logic
assumes you already have all shop upgrades.
obtained. You'll be forced to do sections of the game in different ways with your current abilities.
## What items can appear in other players' worlds?
@ -23,6 +22,7 @@ assumes you already have all shop upgrades.
* Music Box notes
* The Phobekins
* Time shards
* Shop Upgrades
* Power Seals
## Where can I find items?
@ -33,6 +33,7 @@ You can find items wherever items can be picked up in the original game. This in
* Music Box notes
* Phobekins
* Bosses
* Shop Upgrades, Money Wrench, and Figurine Purchases
* Power seals
* Mega Time Shards
@ -46,7 +47,6 @@ for it. The groups you can use for The Messenger are:
* Crest - The Sun and Moon Crests
* Phobekin - Any of the Phobekins
* Phobe - An alternative name for the Phobekins
* Shuriken - The windmill shuriken
## Other changes
@ -60,11 +60,13 @@ for it. The groups you can use for The Messenger are:
* Toggle Windmill Shuriken button is added to option menu once the item is received
* The mod option menu will also have a hint item button, as well as a release and collect button that are all placed when
the player fulfills the necessary conditions.
* After running the game with the mod, a config file (APConfig.toml) will be generated in your game folder that can be
used to modify certain settings such as text size and color. This can also be used to specify a player name that can't
be entered in game.
## Currently known issues
* Necro cutscene will sometimes not play correctly, but will still reward the item
## Known issues
* Ruxxtin Coffin cutscene will sometimes not play correctly, but will still reward the item
* If you receive the Fairy Bottle while in Quillshroom Marsh, The De-curse Queen cutscene will not play. You can exit
* If you receive the Magic Firefly while in Quillshroom Marsh, The De-curse Queen cutscene will not play. You can exit
to Searing Crags and re-enter to get it to play correctly.
* Sometimes upon teleporting back to HQ, Ninja will run left and enter a different portal than the one entered by the
player. This may also cause a softlock.

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
time, but must do this step again to start new runs afterward.
4. Enter connection info using the relevant option buttons
* **The game is limited to alphanumerical characters and `-` so when entering the host name replace `.` with ` `.
Ensure that your player name when generating a settings file follows these constrictions**
* **The game is limited to alphanumerical characters and `-` so when entering the host name replace `.` with ` `.**
* This defaults to `archipelago.gg` and does not need to be manually changed if connecting to a game hosted on the
website.
* If using a name that cannot be entered in the in game menus, there is a config file (APConfig.toml) in the game
directory. When using this, all connection information must be entered in the file.
5. Select the `Connect to Archipelago` button
6. Navigate to save file selection
7. Select a new valid randomizer save
@ -55,11 +56,3 @@ MultiWorld.
If the reconnection fails, the message on screen will state you are disconnected. If this happens, you can return to the
main menu and connect to the server as in [Joining a Multiworld Game](#joining-a-multiworld-game), then load the correct
save file.
## Troubleshooting
If you launch the game, and it hangs on the splash screen for more than 30 seconds try these steps:
1. Close the game and remove `TheMessengerRandomizerAP` from the `Mods` folder.
2. Launch The Messenger
3. Delete any save slot
4. Reinstall the randomizer mod following step 2 of the installation.

View File

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

View File

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

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
class NoLogicTest(MessengerTestBase):
options = {
"logic_level": "oob",
"goal": "power_seal_hunt",
}
def testChestAccess(self) -> None:
"""Test to make sure we can win even though we can't reach the chest."""
self.assertEqual(self.can_reach_location("Shop Chest"), False)
self.assertBeatable(True)
class AllSealsRequired(MessengerTestBase):
options = {
"shuffle_seals": "false",