The Messenger: implement new game (#1494)
* initial commit of messenger integration * setup no_logic and needed slot_data * fix some typos and determinism * make all of it deterministic * add documentation * swapped to non local items so change the fed data * ~~deathlink~~ * satisfy the docs test * update doc test to show expected name * split custom classes into a separate file and fix an errant rule * make access dependency test give more useful errors * implement tests * remove some unneccessary back entrances and make names clearer * fix some big dumbs * successful unit tests are good also some slight reorganizing * add astral tea quest line, and potentially power seals as items * if TYPE_CHECKING... aahhhhhh * oop forgot to remove legacy code * having the seed and leaves as actual items doesn't seem to do anything so remove them. locations still work though * update setup guide with some changes * Tower HQ was creating duplicate locations * allow self locking items * cleanup * move self_locking_items function to core * docstring * implement choice of notes needed for music box * test the default value * don't create any starting inventory items * make item creation faster * change default accessibility and power seals options * improve documentation * precollected_items is a dict of Items... * implement shop chest goal * tests * always assign total and required seals * add new goals and set music box as requiring shop chest on shop chest goals instead of just setting it as the completion * fix dumb test quirk * implement music box skip as an option * world rewrite/cleanup * default to apworld and add game to readme * revert bleeding commits from other PRs * more bleeds * fix some errors in options docstrings * ??? * make my set rules method not have an awful name * test cleanup * add a test for item accessibility * fix issues with tests * make the self locking item behavior work correctly * misc cleanup * more general cleanup to be a good example * quick rules rewrite * more general cleanup and typing * more speed, more clean * bump data version * make sure the locked item belongs to current player * fix bad name and indent. call MessengerItem directly for events * add poptracker pack to docs * doc cleanup and "known issues" section that I probably won't be able to fix any time soon. * missed some spots * add another bug i forgot about * be consistently wrong
This commit is contained in:
parent
39563cc347
commit
070a92e76c
|
@ -38,6 +38,7 @@ Currently, the following games are supported:
|
|||
* Wargroove
|
||||
* Stardew Valley
|
||||
* The Legend of Zelda
|
||||
* The Messenger
|
||||
|
||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
|
||||
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||
|
|
1
setup.py
1
setup.py
|
@ -54,6 +54,7 @@ apworlds: set = {
|
|||
"Stardew Valley",
|
||||
"Timespinner",
|
||||
"Minecraft",
|
||||
"The Messenger",
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
# items
|
||||
# listing individual groups first for easy lookup
|
||||
NOTES = [
|
||||
"Key of Hope",
|
||||
"Key of Chaos",
|
||||
"Key of Courage",
|
||||
"Key of Love",
|
||||
"Key of Strength",
|
||||
"Key of Symbiosis"
|
||||
]
|
||||
|
||||
PROG_ITEMS = [
|
||||
"Wingsuit",
|
||||
"Rope Dart",
|
||||
"Ninja Tabi",
|
||||
"Power Thistle",
|
||||
"Demon King Crown",
|
||||
"Ruxxtin's Amulet",
|
||||
"Fairy Bottle",
|
||||
"Sun Crest",
|
||||
"Moon Crest",
|
||||
# "Astral Seed",
|
||||
# "Astral Tea Leaves"
|
||||
]
|
||||
|
||||
PHOBEKINS = [
|
||||
"Necro",
|
||||
"Pyro",
|
||||
"Claustro",
|
||||
"Acro"
|
||||
]
|
||||
|
||||
USEFUL_ITEMS = [
|
||||
"Windmill Shuriken"
|
||||
]
|
||||
|
||||
# item_name_to_id needs to be deterministic and match upstream
|
||||
ALL_ITEMS = [
|
||||
*NOTES,
|
||||
"Windmill Shuriken",
|
||||
"Wingsuit",
|
||||
"Rope Dart",
|
||||
"Ninja Tabi",
|
||||
# "Astral Seed",
|
||||
# "Astral Tea Leaves",
|
||||
"Candle",
|
||||
"Seashell",
|
||||
"Power Thistle",
|
||||
"Demon King Crown",
|
||||
"Ruxxtin's Amulet",
|
||||
"Fairy Bottle",
|
||||
"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?
|
||||
]
|
||||
|
||||
# locations
|
||||
# the names of these don't actually matter, but using the upstream's names for now
|
||||
# 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",
|
||||
# upgrades
|
||||
"Wingsuit",
|
||||
"Rope Dart",
|
||||
"Ninja Tabi",
|
||||
"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",
|
||||
# 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"
|
||||
]
|
|
@ -0,0 +1,66 @@
|
|||
from Options import DefaultOnToggle, DeathLink, Range, Accessibility, Choice
|
||||
|
||||
|
||||
class MessengerAccessibility(Accessibility):
|
||||
default = Accessibility.option_locations
|
||||
# defaulting to locations accessibility since items makes certain items self-locking
|
||||
__doc__ = Accessibility.__doc__.replace(f"default {Accessibility.default}", f"default {default}")
|
||||
|
||||
|
||||
class Logic(DefaultOnToggle):
|
||||
"""Whether the seed should be guaranteed completable."""
|
||||
display_name = "Use Logic"
|
||||
|
||||
|
||||
class PowerSeals(DefaultOnToggle):
|
||||
"""Whether power seal locations should be randomized."""
|
||||
display_name = "Shuffle Seals"
|
||||
|
||||
|
||||
class Goal(Choice):
|
||||
"""Requirement to finish the game. Power Seal Hunt will force power seal locations to be shuffled."""
|
||||
display_name = "Goal"
|
||||
option_open_music_box = 0
|
||||
option_power_seal_hunt = 1
|
||||
|
||||
|
||||
class MusicBox(DefaultOnToggle):
|
||||
"""Whether the music box gauntlet needs to be done."""
|
||||
display_name = "Music Box Gauntlet"
|
||||
|
||||
|
||||
class NotesNeeded(Range):
|
||||
"""How many notes are needed to access the Music Box."""
|
||||
display_name = "Notes Needed"
|
||||
range_start = 1
|
||||
range_end = 6
|
||||
default = range_end
|
||||
|
||||
|
||||
class AmountSeals(Range):
|
||||
"""Number of power seals that exist in the item pool when power seal hunt is the goal."""
|
||||
display_name = "Total Power Seals"
|
||||
range_start = 1
|
||||
range_end = 45
|
||||
default = range_end
|
||||
|
||||
|
||||
class RequiredSeals(Range):
|
||||
"""Percentage of total seals required to open the shop chest."""
|
||||
display_name = "Percent Seals Required"
|
||||
range_start = 10
|
||||
range_end = 100
|
||||
default = range_end
|
||||
|
||||
|
||||
messenger_options = {
|
||||
"accessibility": MessengerAccessibility,
|
||||
"enable_logic": Logic,
|
||||
"shuffle_seals": PowerSeals,
|
||||
"goal": Goal,
|
||||
"music_box": MusicBox,
|
||||
"notes_needed": NotesNeeded,
|
||||
"total_seals": AmountSeals,
|
||||
"percent_seals_required": RequiredSeals,
|
||||
"death_link": DeathLink,
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
from typing import Dict, Set, List
|
||||
|
||||
REGIONS: Dict[str, List[str]] = {
|
||||
"Menu": [],
|
||||
"Tower HQ": [],
|
||||
"The Shop": [],
|
||||
"Tower of Time": [],
|
||||
"Ninja Village": ["Candle", "Astral Seed"],
|
||||
"Autumn Hills": ["Climbing Claws", "Key of Hope"],
|
||||
"Forlorn Temple": ["Demon King Crown"],
|
||||
"Catacombs": ["Necro", "Ruxxtin's Amulet"],
|
||||
"Bamboo Creek": ["Claustro"],
|
||||
"Howling Grotto": ["Wingsuit"],
|
||||
"Quillshroom Marsh": ["Seashell"],
|
||||
"Searing Crags": ["Rope Dart"],
|
||||
"Searing Crags Upper": ["Power Thistle", "Key of Strength", "Astral Tea Leaves"],
|
||||
"Glacial Peak": [],
|
||||
"Cloud Ruins": ["Acro"],
|
||||
"Underworld": ["Pyro", "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"],
|
||||
"Music Box": ["Rescue Phantom"]
|
||||
}
|
||||
"""seal locations have the region in their name and may not need to be created so skip them here"""
|
||||
|
||||
|
||||
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 of Time": set(),
|
||||
"Ninja Village": set(),
|
||||
"Autumn Hills": {"Ninja Village", "Forlorn Temple", "Catacombs"},
|
||||
"Forlorn Temple": {"Catacombs", "Bamboo Creek"},
|
||||
"Catacombs": {"Autumn Hills", "Bamboo Creek", "Dark Cave"},
|
||||
"Bamboo Creek": {"Catacombs", "Howling Grotto"},
|
||||
"Howling Grotto": {"Bamboo Creek", "Quillshroom Marsh", "Sunken Shrine"},
|
||||
"Quillshroom Marsh": {"Howling Grotto", "Searing Crags"},
|
||||
"Searing Crags": {"Searing Crags Upper", "Quillshroom Marsh", "Underworld"},
|
||||
"Searing Crags Upper": {"Searing Crags", "Glacial Peak"},
|
||||
"Glacial Peak": {"Searing Crags Upper", "Tower HQ", "Cloud Ruins", "Elemental Skylands"},
|
||||
"Cloud Ruins": {"Underworld"},
|
||||
"Underworld": set(),
|
||||
"Dark Cave": {"Catacombs", "Riviere Turquoise"},
|
||||
"Riviere Turquoise": set(),
|
||||
"Sunken Shrine": {"Howling Grotto"},
|
||||
"Elemental Skylands": set()
|
||||
}
|
||||
"""Vanilla layout mapping with all Tower HQ portals open. from -> to"""
|
|
@ -0,0 +1,158 @@
|
|||
from typing import Dict, Callable, Optional, Tuple, Union, TYPE_CHECKING, List, Iterable
|
||||
|
||||
from BaseClasses import CollectionState, MultiWorld, Location, Region, Entrance, Item
|
||||
from .Options import MessengerAccessibility, Goal
|
||||
from .Constants import NOTES, PHOBEKINS
|
||||
from ..generic.Rules import add_rule, set_rule
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import MessengerWorld
|
||||
else:
|
||||
MessengerWorld = object
|
||||
|
||||
|
||||
class MessengerRules:
|
||||
player: int
|
||||
world: MessengerWorld
|
||||
|
||||
def __init__(self, world: MessengerWorld):
|
||||
self.player = world.player
|
||||
self.world = world
|
||||
|
||||
self.region_rules: Dict[str, Callable[[CollectionState], bool]] = {
|
||||
"Ninja Village": self.has_wingsuit,
|
||||
"Autumn Hills": self.has_wingsuit,
|
||||
"Catacombs": self.has_wingsuit,
|
||||
"Bamboo Creek": self.has_wingsuit,
|
||||
"Searing Crags Upper": self.has_vertical,
|
||||
"Cloud Ruins": lambda state: self.has_wingsuit(state) and state.has("Ruxxtin's Amulet", self.player),
|
||||
"Underworld": self.has_tabi,
|
||||
"Forlorn Temple": lambda state: state.has_all(PHOBEKINS, self.player) and self.has_wingsuit(state),
|
||||
"Glacial Peak": self.has_vertical,
|
||||
"Elemental Skylands": lambda state: state.has("Fairy Bottle", self.player),
|
||||
"Music Box": lambda state: state.has_all(NOTES, self.player)
|
||||
}
|
||||
|
||||
self.location_rules: Dict[str, Callable[[CollectionState], bool]] = {
|
||||
# ninja village
|
||||
"Ninja Village Seal - Tree House": self.has_dart,
|
||||
# autumn hills
|
||||
"Key of Hope": self.has_dart,
|
||||
# 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),
|
||||
# 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,
|
||||
# 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 - 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 - 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 Seal - Waterfall Paradise": self.has_tabi,
|
||||
"Sunken Shrine Seal - Tabi Gauntlet": self.has_tabi,
|
||||
# riviere turquoise
|
||||
"Fairy Bottle": self.has_vertical,
|
||||
"Riviere Turquoise Seal - Flower Power": self.has_vertical,
|
||||
# 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,
|
||||
# corrupted future
|
||||
"Key of Courage": lambda state: state.has_all({"Demon King Crown", "Fairy Bottle"}, self.player),
|
||||
# the shop
|
||||
"Shop Chest": self.has_enough_seals
|
||||
}
|
||||
|
||||
def has_wingsuit(self, state: CollectionState) -> bool:
|
||||
return state.has("Wingsuit", self.player)
|
||||
|
||||
def has_dart(self, state: CollectionState) -> bool:
|
||||
return state.has("Rope Dart", self.player)
|
||||
|
||||
def has_tabi(self, state: CollectionState) -> bool:
|
||||
return state.has("Ninja Tabi", self.player)
|
||||
|
||||
def has_vertical(self, state: CollectionState) -> bool:
|
||||
return self.has_wingsuit(state) or self.has_dart(state)
|
||||
|
||||
def has_enough_seals(self, state: CollectionState) -> bool:
|
||||
required_seals = state.multiworld.worlds[self.player].required_seals
|
||||
return state.has("Power Seal", self.player, required_seals)
|
||||
|
||||
def set_messenger_rules(self) -> None:
|
||||
multiworld = self.world.multiworld
|
||||
|
||||
for region in multiworld.get_regions(self.player):
|
||||
if region.name in self.region_rules:
|
||||
for entrance in region.entrances:
|
||||
entrance.access_rule = self.region_rules[region.name]
|
||||
for loc in region.locations:
|
||||
if loc.name in self.location_rules:
|
||||
loc.access_rule = self.location_rules[loc.name]
|
||||
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))
|
||||
|
||||
if multiworld.enable_logic[self.player]:
|
||||
multiworld.completion_condition[self.player] = lambda state: state.has("Rescue Phantom", self.player)
|
||||
else:
|
||||
multiworld.accessibility[self.player].value = MessengerAccessibility.option_minimal
|
||||
if multiworld.accessibility[self.player] > MessengerAccessibility.option_locations:
|
||||
set_self_locking_items(multiworld, self.player)
|
||||
|
||||
|
||||
def location_item_name(state: CollectionState, location_name: str, player: int) -> Optional[Tuple[str, int]]:
|
||||
location = state.multiworld.get_location(location_name, player)
|
||||
if location.item is None:
|
||||
return None
|
||||
return location.item.name, location.item.player
|
||||
|
||||
|
||||
def allow_self_locking_items(spot: Union[Location, Region], *item_names: str) -> None:
|
||||
"""
|
||||
Sets rules on the supplied spot, such that the supplied item_name(s) can possibly be placed there.
|
||||
:param spot: Location or Region that the item(s) are allowed to be placed in
|
||||
:param item_names: item name or names that are allowed to be placed in the Location or Region
|
||||
"""
|
||||
player = spot.player
|
||||
|
||||
def set_always_allow(location: Location, rule: Callable[[CollectionState, Item], bool]) -> None:
|
||||
location.always_allow = rule
|
||||
|
||||
def add_allowed_rules(area: Union[Location, Entrance], location: Location) -> None:
|
||||
for item_name in item_names:
|
||||
add_rule(area, lambda state, item_name=item_name:
|
||||
location_item_name(state, location.name, player) == (item_name, player), "or")
|
||||
set_always_allow(location, lambda state, item:
|
||||
item.player == player and item.name in [item_name for item_name in item_names])
|
||||
|
||||
if isinstance(spot, Region):
|
||||
for entrance in spot.entrances:
|
||||
for location in spot.locations:
|
||||
add_allowed_rules(entrance, location)
|
||||
else:
|
||||
add_allowed_rules(spot, spot)
|
||||
|
||||
|
||||
def set_self_locking_items(multiworld: MultiWorld, player: int) -> None:
|
||||
# do the ones for seal shuffle on and off first
|
||||
allow_self_locking_items(multiworld.get_location("Key of Strength", player), "Power Thistle")
|
||||
allow_self_locking_items(multiworld.get_location("Key of Love", player), "Sun Crest", "Moon Crest")
|
||||
allow_self_locking_items(multiworld.get_location("Key of Courage", player), "Demon King Crown")
|
||||
|
||||
# add these locations when seals aren't shuffled
|
||||
if not multiworld.shuffle_seals[player]:
|
||||
allow_self_locking_items(multiworld.get_region("Cloud Ruins", player), "Ruxxtin's Amulet")
|
||||
allow_self_locking_items(multiworld.get_region("Forlorn Temple", player), *PHOBEKINS)
|
|
@ -0,0 +1,58 @@
|
|||
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 .Options import Goal
|
||||
from .Regions import REGIONS
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import MessengerWorld
|
||||
else:
|
||||
MessengerWorld = object
|
||||
|
||||
|
||||
class MessengerRegion(Region):
|
||||
def __init__(self, name: str, world: MessengerWorld):
|
||||
super().__init__(name, world.player, world.multiworld)
|
||||
self.add_locations(self.multiworld.worlds[self.player].location_name_to_id)
|
||||
world.multiworld.regions.append(self)
|
||||
|
||||
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"}:
|
||||
for seal_loc in SEALS:
|
||||
if seal_loc.startswith(self.name.split(" ")[0]):
|
||||
self.locations.append(MessengerLocation(seal_loc, self, name_to_id.get(seal_loc, None)))
|
||||
|
||||
def add_exits(self, exits: Set[str]) -> None:
|
||||
for exit in exits:
|
||||
ret = Entrance(self.player, f"{self.name} -> {exit}", self)
|
||||
self.exits.append(ret)
|
||||
ret.connect(self.multiworld.get_region(exit, self.player))
|
||||
|
||||
|
||||
class MessengerLocation(Location):
|
||||
game = "The Messenger"
|
||||
|
||||
def __init__(self, name: str, parent: MessengerRegion, loc_id: Optional[int]):
|
||||
super().__init__(parent.player, name, loc_id, parent)
|
||||
if loc_id is None:
|
||||
self.place_locked_item(MessengerItem(name, parent.player, None))
|
||||
|
||||
|
||||
class MessengerItem(Item):
|
||||
game = "The Messenger"
|
||||
|
||||
def __init__(self, name: str, player: int, item_id: Optional[int] = None):
|
||||
if name in {*NOTES, *PROG_ITEMS, *PHOBEKINS} or item_id is None:
|
||||
item_class = ItemClassification.progression
|
||||
elif name in USEFUL_ITEMS:
|
||||
item_class = ItemClassification.useful
|
||||
else:
|
||||
item_class = ItemClassification.filler
|
||||
super().__init__(name, item_class, item_id, player)
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
from typing import Dict, Any, List, Optional
|
||||
|
||||
from BaseClasses import Tutorial, ItemClassification
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
from .Constants import NOTES, PROG_ITEMS, PHOBEKINS, USEFUL_ITEMS, ALWAYS_LOCATIONS, SEALS, ALL_ITEMS
|
||||
from .Options import messenger_options, NotesNeeded, Goal, PowerSeals
|
||||
from .Regions import REGIONS, REGION_CONNECTIONS
|
||||
from .Rules import MessengerRules
|
||||
from .SubClasses import MessengerRegion, MessengerItem
|
||||
|
||||
|
||||
class MessengerWeb(WebWorld):
|
||||
theme = "ocean"
|
||||
|
||||
bug_report_page = "https://github.com/minous27/TheMessengerRandomizerMod/issues"
|
||||
|
||||
tut_en = Tutorial(
|
||||
"Multiworld Setup Tutorial",
|
||||
"A guide to setting up The Messenger randomizer on your computer.",
|
||||
"English",
|
||||
"setup_en.md",
|
||||
"setup/en",
|
||||
["alwaysintreble"]
|
||||
)
|
||||
|
||||
tutorials = [tut_en]
|
||||
|
||||
|
||||
class MessengerWorld(World):
|
||||
"""
|
||||
As a demon army besieges his village, a young ninja ventures through a cursed world, to deliver a scroll paramount
|
||||
to his clan’s survival. What begins as a classic action platformer soon unravels into an expansive time-traveling
|
||||
adventure full of thrills, surprises, and humor.
|
||||
"""
|
||||
game = "The Messenger"
|
||||
|
||||
item_name_groups = {
|
||||
"Notes": set(NOTES),
|
||||
"Keys": set(NOTES),
|
||||
"Crest": {"Sun Crest", "Moon Crest"},
|
||||
"Phobe": set(PHOBEKINS),
|
||||
"Phobekin": set(PHOBEKINS),
|
||||
"Shuriken": {"Windmill Shuriken"},
|
||||
}
|
||||
|
||||
option_definitions = messenger_options
|
||||
|
||||
base_offset = 0xADD_000
|
||||
item_name_to_id = {item: item_id
|
||||
for item_id, item in enumerate(ALL_ITEMS, base_offset)}
|
||||
location_name_to_id = {location: location_id
|
||||
for location_id, location in enumerate([*ALWAYS_LOCATIONS, *SEALS], base_offset)}
|
||||
|
||||
data_version = 1
|
||||
|
||||
web = MessengerWeb()
|
||||
|
||||
total_seals: Optional[int] = None
|
||||
required_seals: Optional[int] = None
|
||||
|
||||
def generate_early(self) -> None:
|
||||
if self.multiworld.goal[self.player] == Goal.option_power_seal_hunt:
|
||||
self.multiworld.shuffle_seals[self.player].value = PowerSeals.option_true
|
||||
self.total_seals = self.multiworld.total_seals[self.player].value
|
||||
self.required_seals = int(self.multiworld.percent_seals_required[self.player].value / 100 * self.total_seals)
|
||||
|
||||
def create_regions(self) -> None:
|
||||
for region in [MessengerRegion(reg_name, self) for reg_name in REGIONS]:
|
||||
if region.name in REGION_CONNECTIONS:
|
||||
region.add_exits(REGION_CONNECTIONS[region.name])
|
||||
|
||||
def create_items(self) -> None:
|
||||
itempool: List[MessengerItem] = []
|
||||
if self.multiworld.goal[self.player] == Goal.option_power_seal_hunt:
|
||||
seals = [self.create_item("Power Seal") for _ in range(self.total_seals)]
|
||||
for i in range(self.required_seals):
|
||||
seals[i].classification = ItemClassification.progression_skip_balancing
|
||||
itempool += seals
|
||||
else:
|
||||
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)
|
||||
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.
|
||||
}]
|
||||
itempool += [self.create_filler()
|
||||
for _ in range(len(self.multiworld.get_unfilled_locations(self.player)) - len(itempool))]
|
||||
|
||||
self.multiworld.itempool += itempool
|
||||
|
||||
def set_rules(self) -> None:
|
||||
MessengerRules(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]]
|
||||
|
||||
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"}
|
||||
}
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return "Time Shard"
|
||||
|
||||
def create_item(self, name: str) -> MessengerItem:
|
||||
item_id: Optional[int] = self.item_name_to_id.get(name, None)
|
||||
return MessengerItem(name, self.player, item_id)
|
|
@ -0,0 +1,75 @@
|
|||
# The Messenger
|
||||
|
||||
## Quick Links
|
||||
- [Setup](../../../../games/The%20Messenger/setup/en)
|
||||
- [Settings Page](../../../../games/The%20Messenger/player-settings)
|
||||
- [Courier Github](https://github.com/Brokemia/Courier)
|
||||
- [The Messenger Randomizer Github](https://github.com/minous27/TheMessengerRandomizerMod)
|
||||
- [Jacksonbird8237's Item Tracker](https://github.com/Jacksonbird8237/TheMessengerItemTracker)
|
||||
- [PopTracker Pack](https://github.com/alwaysintreble/TheMessengerTrackPack)
|
||||
|
||||
## What does randomization do in this game?
|
||||
|
||||
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.
|
||||
|
||||
## What items can appear in other players' worlds?
|
||||
|
||||
* The player's movement items
|
||||
* Quest and pedestal items
|
||||
* Music Box notes
|
||||
* The Phobekins
|
||||
* Time shards
|
||||
* Power Seals
|
||||
|
||||
## Where can I find items?
|
||||
|
||||
You can find items wherever items can be picked up in the original game. This includes:
|
||||
* Shopkeeper dialog where the player originally gains movement items
|
||||
* Quest Item pickups
|
||||
* Music Box notes
|
||||
* Phobekins
|
||||
* Power seals
|
||||
|
||||
## What are the item name groups?
|
||||
|
||||
When you attempt to hint for items in Archipelago you can use either the name for the specific item, or the name of a
|
||||
group of items. Hinting for a group will choose a random item from the group that you do not currently have and hint
|
||||
for it. The groups you can use for The Messenger are:
|
||||
* Notes - This covers the music notes
|
||||
* Keys - An alternative name for the music notes
|
||||
* Crest - The Sun and Moon Crests
|
||||
* Phobekin - Any of the Phobekins
|
||||
* Phobe - An alternative name for the Phobekins
|
||||
* Shuriken - The windmill shuriken
|
||||
|
||||
## Other changes
|
||||
|
||||
* The player can return to the Tower of Time HQ at any point by selecting the button from the options menu
|
||||
* This can cause issues if used at specific times. Current known:
|
||||
* During Boss fights
|
||||
* After Courage Note collection (Corrupted Future chase)
|
||||
* This is currently an expected action in logic. If you do need to teleport during this chase sequence, it
|
||||
is recommended to quit to title and reload the save
|
||||
* After reaching ninja village a teleport option is added to the menu to reach it quickly
|
||||
* Toggle Windmill Shuriken button is added to option menu once the item is received
|
||||
|
||||
## Currently known issues
|
||||
* Necro cutscene will sometimes not play correctly, but will still reward the item
|
||||
* Ruxxtin Coffin cutscene will sometimes not play correctly, but will still reward the item
|
||||
* If you receive the Fairy Bottle while in Quillshroom Marsh, The Decurse Queen cutscene will not play
|
||||
* If you defeat Barma'thazël, the cutscene afterward will not play correctly since that is what normally transitions
|
||||
you to 2nd quest. The game will not kill you if you fall here, so you can teleport to HQ at any point after defeating him.
|
||||
* Sometimes upon teleporting back to HQ, Ninja will run left and enter a different portal than the one entered by the
|
||||
player.
|
||||
* If playing the game in non-english, sometimes the text entry menus will say "What is your name?" in local language
|
||||
instead of the correct text. This can be fixed by going into the game options and selecting your language in the menu.
|
||||
It does not need to be changed to something else and back.
|
||||
* Text entry menus don't accept controller input
|
||||
|
||||
## 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
|
||||
and send a bug report either on github or the [Archipelago Discord Server](http://archipelago.gg/discord)
|
|
@ -0,0 +1,52 @@
|
|||
# The Messenger Randomizer Setup Guide
|
||||
|
||||
## Quick Links
|
||||
- [Main Page](../../../../games/The%20Messenger/info/en)
|
||||
- [Settings Page](../../../../games/The%20Messenger/player-settings)
|
||||
- [Courier Github](https://github.com/Brokemia/Courier)
|
||||
- [The Messenger Randomizer Github](https://github.com/minous27/TheMessengerRandomizerMod)
|
||||
- [Jacksonbird8237's Item Tracker](https://github.com/Jacksonbird8237/TheMessengerItemTracker)
|
||||
- [PopTracker Pack](https://github.com/alwaysintreble/TheMessengerTrackPack)
|
||||
|
||||
## Required Software
|
||||
|
||||
- [The Messenger](https://store.steampowered.com/app/764790/The_Messenger/)
|
||||
- Only Steam version is currently supported.
|
||||
- [Courier Mod Loader](https://github.com/Brokemia/Courier/releases)
|
||||
- [The Messenger Randomizer Mod](https://github.com/minous27/TheMessengerRandomizerMod/releases)
|
||||
|
||||
## Installation
|
||||
|
||||
1. Download and install Courier Mod Loader using the instructions on the release page
|
||||
2. Download and install the randomizer mod
|
||||
* Download the latest `TheMessengerRandomizer.zip`
|
||||
* Extract the zip file to `TheMessenger/Mods/` of your game's install location
|
||||
* Optionally, Backup your save game
|
||||
1. Press `Windows Key + R` to open run
|
||||
2. Type `%appdata%` to access AppData
|
||||
3. Navigate to `AppData/locallow/SabotageStudios/The Messenger`
|
||||
4. Rename `SaveGame.txt` to any name of your choice
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
1. Launch the game
|
||||
2. Navigate to `Options > Third Party Mod Options`
|
||||
3. Select `Reset Randomizer File Slots`
|
||||
* 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 afterwards.
|
||||
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 ` ` and
|
||||
ensure that your player name when generating a settings file follows these constrictions**
|
||||
* This defaults to `archipelago.gg` and does not need to be manually changed if connecting to a game hosted on the
|
||||
website.
|
||||
5. Select the `Connect to Archipelago` button
|
||||
6. Navigate to save file selection
|
||||
7. Select a new valid randomizer save
|
||||
|
||||
## 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 `TheMessengerRandomizer` from the `Mods` folder.
|
||||
2. Launch The Messenger
|
||||
3. Delete any save slot
|
||||
4. Reinstall the randomizer mod following step 2 of the installation.
|
|
@ -0,0 +1,149 @@
|
|||
from . import MessengerTestBase
|
||||
from ..Constants import NOTES, PHOBEKINS
|
||||
from ..Options import MessengerAccessibility
|
||||
|
||||
|
||||
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"]
|
||||
items = [["Ninja 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"]
|
||||
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"]
|
||||
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",
|
||||
"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",
|
||||
"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"]
|
||||
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 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"]
|
||||
items = [["Fairy Bottle"]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
def testCrests(self) -> None:
|
||||
"""Test Key of Love nonsense"""
|
||||
locations = ["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.remove(self.get_item_by_name("Moon Crest"))
|
||||
self.collect_by_name("Sun Crest")
|
||||
self.assertEqual(self.can_reach_location("Key of Love"), False)
|
||||
|
||||
def testThistle(self) -> None:
|
||||
"""I'm a chuckster!"""
|
||||
locations = ["Key of Strength"]
|
||||
items = [["Power Thistle"]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
def testCrown(self) -> None:
|
||||
"""Crocomire but not"""
|
||||
locations = ["Key of Courage"]
|
||||
items = [["Demon King Crown"]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
def testGoal(self) -> None:
|
||||
"""Test some different states to verify goal requires the correct items"""
|
||||
self.collect_all_but([*NOTES, "Rescue Phantom"])
|
||||
self.assertEqual(self.can_reach_location("Rescue Phantom"), False)
|
||||
self.collect_all_but(["Key of Love", "Rescue Phantom"])
|
||||
self.assertBeatable(False)
|
||||
self.collect_by_name(["Key of Love"])
|
||||
self.assertEqual(self.can_reach_location("Rescue Phantom"), True)
|
||||
self.assertBeatable(True)
|
||||
|
||||
|
||||
class ItemsAccessTest(MessengerTestBase):
|
||||
options = {
|
||||
"shuffle_seals": False,
|
||||
"accessibility": MessengerAccessibility.option_items
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
for loc in location_lock_pairs:
|
||||
for item_name in location_lock_pairs[loc]:
|
||||
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))
|
||||
|
||||
|
||||
class NoLogicTest(MessengerTestBase):
|
||||
options = {
|
||||
"enable_logic": "false"
|
||||
}
|
||||
|
||||
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,30 @@
|
|||
from . import MessengerTestBase
|
||||
from ..Constants import NOTES
|
||||
|
||||
|
||||
class TwoNoteGoalTest(MessengerTestBase):
|
||||
options = {
|
||||
"notes_needed": 2,
|
||||
}
|
||||
|
||||
def testPrecollectedNotes(self) -> None:
|
||||
self.assertEqual(self.multiworld.state.count_group("Notes", self.player), 4)
|
||||
|
||||
|
||||
class FourNoteGoalTest(MessengerTestBase):
|
||||
options = {
|
||||
"notes_needed": 4,
|
||||
}
|
||||
|
||||
def testPrecollectedNotes(self) -> None:
|
||||
self.assertEqual(self.multiworld.state.count_group("Notes", self.player), 2)
|
||||
|
||||
|
||||
class DefaultGoalTest(MessengerTestBase):
|
||||
def testPrecollectedNotes(self) -> None:
|
||||
self.assertEqual(self.multiworld.state.count_group("Notes", self.player), 0)
|
||||
|
||||
def testGoal(self) -> None:
|
||||
self.assertBeatable(False)
|
||||
self.collect_by_name(NOTES)
|
||||
self.assertBeatable(True)
|
|
@ -0,0 +1,79 @@
|
|||
from BaseClasses import ItemClassification, CollectionState
|
||||
from . import MessengerTestBase
|
||||
|
||||
|
||||
class NoLogicTest(MessengerTestBase):
|
||||
options = {
|
||||
"enable_logic": "false",
|
||||
"goal": "power_seal_hunt",
|
||||
}
|
||||
|
||||
def testChestAccess(self):
|
||||
"""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",
|
||||
"goal": "power_seal_hunt",
|
||||
}
|
||||
|
||||
def testSealsShuffled(self) -> None:
|
||||
"""Shuffle seals should be forced on when shop chest is the goal so test it."""
|
||||
self.assertTrue(self.multiworld.shuffle_seals[self.player])
|
||||
|
||||
def testChestAccess(self) -> None:
|
||||
"""Defaults to a total of 45 power seals in the pool and required."""
|
||||
with self.subTest("Access Dependency"):
|
||||
self.assertEqual(len([seal for seal in self.multiworld.itempool if seal.name == "Power Seal"]),
|
||||
self.multiworld.total_seals[self.player])
|
||||
locations = ["Shop Chest"]
|
||||
items = [["Power Seal"]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
self.multiworld.state = CollectionState(self.multiworld)
|
||||
|
||||
self.assertEqual(self.can_reach_location("Shop Chest"), False)
|
||||
self.assertBeatable(False)
|
||||
self.collect_all_but(["Power Seal", "Shop Chest", "Rescue Phantom"])
|
||||
self.assertEqual(self.can_reach_location("Shop Chest"), False)
|
||||
self.assertBeatable(False)
|
||||
self.collect_by_name("Power Seal")
|
||||
self.assertEqual(self.can_reach_location("Shop Chest"), True)
|
||||
self.assertBeatable(True)
|
||||
|
||||
|
||||
class HalfSealsRequired(MessengerTestBase):
|
||||
options = {
|
||||
"goal": "power_seal_hunt",
|
||||
"percent_seals_required": 50,
|
||||
}
|
||||
|
||||
def testSealsAmount(self) -> None:
|
||||
"""Should have 45 power seals in the item pool and half that required"""
|
||||
self.assertEqual(self.multiworld.total_seals[self.player], 45)
|
||||
self.assertEqual(self.multiworld.worlds[self.player].total_seals, 45)
|
||||
self.assertEqual(self.multiworld.worlds[self.player].required_seals, 22)
|
||||
total_seals = [seal for seal in self.multiworld.itempool if seal.name == "Power Seal"]
|
||||
required_seals = [seal for seal in total_seals if seal.classification == ItemClassification.progression_skip_balancing]
|
||||
self.assertEqual(len(total_seals), 45)
|
||||
self.assertEqual(len(required_seals), 22)
|
||||
|
||||
|
||||
class ThirtyThirtySeals(MessengerTestBase):
|
||||
options = {
|
||||
"goal": "power_seal_hunt",
|
||||
"total_seals": 30,
|
||||
"percent_seals_required": 34,
|
||||
}
|
||||
|
||||
def testSealsAmount(self) -> None:
|
||||
"""Should have 30 power seals in the pool and 33 percent of that required."""
|
||||
self.assertEqual(self.multiworld.total_seals[self.player], 30)
|
||||
self.assertEqual(self.multiworld.worlds[self.player].total_seals, 30)
|
||||
self.assertEqual(self.multiworld.worlds[self.player].required_seals, 10)
|
||||
total_seals = [seal for seal in self.multiworld.itempool if seal.name == "Power Seal"]
|
||||
required_seals = [seal for seal in total_seals if seal.classification == ItemClassification.progression_skip_balancing]
|
||||
self.assertEqual(len(total_seals), 30)
|
||||
self.assertEqual(len(required_seals), 10)
|
|
@ -0,0 +1,6 @@
|
|||
from test.TestBase import WorldTestBase
|
||||
|
||||
|
||||
class MessengerTestBase(WorldTestBase):
|
||||
game = "The Messenger"
|
||||
player: int = 1
|
Loading…
Reference in New Issue