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