 332dde154f
			
		
	
	332dde154f
	
	
	
		
			
			* add freetext and freetextchoice options * fix textchoice. create plando_bosses bool so worlds can check if boss plando is enabled * remove strange unneccessary \ escapes * lttp: rip boss plando out of core * fix broken text methods so they read the data correctly * revert `None` key in boss_shuffle_options. fix failing tests * lttp: rewrite boss plando * lttp: rewrite boss shuffle * add generic verification step and allow options to set a plando module * add default typing to plando_options set * use PlandoSettings intflag for lttp boss plando * fix plandosettings boss flag check * minor lttp init cleanup * make suggested changes. account for "random" existing within plando boss options * override eq operator * Please document me! * Forgot to mention it supports plando * remove auto_display_name * Throw warning alerting user to which shuffle is being used if plando is off. Set the remaining boss shuffle in init and boss placement cleanup * move the convoluted string matching to `from_text` * remove unneccessary text lowering and actually turn off plando option when it's disabled * typing * strong typing for verify method and reorder * typing is your friend * log warning correctly * 3.8 support :( * also list apparently * rip out old boss shuffle spoiler code * verification step for plando bosses and locations * update plando guide to reference new supported behavior * empty string is not `None`. remove unneccessary error throw * Fix bad ordering * validate boss_shuffle only contains a normal boss option at the end * get random choice from a list dummy * >:( Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * minor textchoice cleanup Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
		
			
				
	
	
		
			527 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			527 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| import typing
 | |
| 
 | |
| from BaseClasses import MultiWorld
 | |
| from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, TextChoice
 | |
| 
 | |
| 
 | |
| class Logic(Choice):
 | |
|     option_no_glitches = 0
 | |
|     option_minor_glitches = 1
 | |
|     option_overworld_glitches = 2
 | |
|     option_hybrid_major_glitches = 3
 | |
|     option_no_logic = 4
 | |
|     alias_owg = 2
 | |
|     alias_hmg = 3
 | |
| 
 | |
| 
 | |
| class Objective(Choice):
 | |
|     option_crystals = 0
 | |
|     # option_pendants = 1
 | |
|     option_triforce_pieces = 2
 | |
|     option_pedestal = 3
 | |
|     option_bingo = 4
 | |
| 
 | |
| 
 | |
| class Goal(Choice):
 | |
|     option_kill_ganon = 0
 | |
|     option_kill_ganon_and_gt_agahnim = 1
 | |
|     option_hand_in = 2
 | |
| 
 | |
| 
 | |
| class OpenPyramid(Choice):
 | |
|     """Determines whether the hole at the top of pyramid is open.
 | |
|     Goal will open the pyramid if the goal requires you to kill Ganon, without needing to kill Agahnim 2.
 | |
|     Auto is the same as goal except if Ganon's dropdown is in another location, the hole will be closed."""
 | |
|     display_name = "Open Pyramid Hole"
 | |
|     option_closed = 0
 | |
|     option_open = 1
 | |
|     option_goal = 2
 | |
|     option_auto = 3
 | |
|     default = option_goal
 | |
| 
 | |
|     alias_yes = option_open
 | |
|     alias_no = option_closed
 | |
| 
 | |
|     def to_bool(self, world: MultiWorld, player: int) -> bool:
 | |
|         if self.value == self.option_goal:
 | |
|             return world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'}
 | |
|         elif self.value == self.option_auto:
 | |
|             return world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'} \
 | |
|             and (world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed'} or not
 | |
|                  world.shuffle_ganon)
 | |
|         elif self.value == self.option_open:
 | |
|             return True
 | |
|         else:
 | |
|             return False
 | |
| 
 | |
| 
 | |
| class DungeonItem(Choice):
 | |
|     value: int
 | |
|     option_original_dungeon = 0
 | |
|     option_own_dungeons = 1
 | |
|     option_own_world = 2
 | |
|     option_any_world = 3
 | |
|     option_different_world = 4
 | |
|     option_start_with = 6
 | |
|     alias_true = 3
 | |
|     alias_false = 0
 | |
| 
 | |
|     @property
 | |
|     def in_dungeon(self):
 | |
|         return self.value in {0, 1}
 | |
| 
 | |
|     @property
 | |
|     def hints_useful(self):
 | |
|         """Indicates if hints for this Item are useful in any way."""
 | |
|         return self.value in {1, 2, 3, 4}
 | |
| 
 | |
| 
 | |
| class bigkey_shuffle(DungeonItem):
 | |
|     """Big Key Placement"""
 | |
|     item_name_group = "Big Keys"
 | |
|     display_name = "Big Key Shuffle"
 | |
| 
 | |
| 
 | |
| class smallkey_shuffle(DungeonItem):
 | |
|     """Small Key Placement"""
 | |
|     option_universal = 5
 | |
|     item_name_group = "Small Keys"
 | |
|     display_name = "Small Key Shuffle"
 | |
| 
 | |
| 
 | |
| class compass_shuffle(DungeonItem):
 | |
|     """Compass Placement"""
 | |
|     item_name_group = "Compasses"
 | |
|     display_name = "Compass Shuffle"
 | |
| 
 | |
| 
 | |
| class map_shuffle(DungeonItem):
 | |
|     """Map Placement"""
 | |
|     item_name_group = "Maps"
 | |
|     display_name = "Map Shuffle"
 | |
| 
 | |
| 
 | |
| class Crystals(Range):
 | |
|     range_start = 0
 | |
|     range_end = 7
 | |
| 
 | |
| 
 | |
| class CrystalsTower(Crystals):
 | |
|     default = 7
 | |
| 
 | |
| 
 | |
| class CrystalsGanon(Crystals):
 | |
|     default = 7
 | |
| 
 | |
| 
 | |
| class TriforcePieces(Range):
 | |
|     default = 30
 | |
|     range_start = 1
 | |
|     range_end = 90
 | |
| 
 | |
| 
 | |
| class ShopItemSlots(Range):
 | |
|     range_start = 0
 | |
|     range_end = 30
 | |
| 
 | |
| 
 | |
| class ShopPriceModifier(Range):
 | |
|     """Percentage modifier for shuffled item prices in shops"""
 | |
|     range_start = 0
 | |
|     default = 100
 | |
|     range_end = 400
 | |
| 
 | |
| 
 | |
| class WorldState(Choice):
 | |
|     option_standard = 1
 | |
|     option_open = 0
 | |
|     option_inverted = 2
 | |
| 
 | |
| 
 | |
| class Bosses(TextChoice):
 | |
|     """Shuffles bosses around to different locations.
 | |
|     Basic will shuffle all bosses except Ganon and Agahnim anywhere they can be placed.
 | |
|     Full chooses 3 bosses at random to be placed twice instead of Lanmolas, Moldorm, and Helmasaur.
 | |
|     Chaos allows any boss to appear any number of times.
 | |
|     Singularity places a single boss in as many places as possible, and a second boss in any remaining locations.
 | |
|     Supports plando placement. Formatting here: https://archipelago.gg/tutorial/A%20Link%20to%20the%20Past/plando/en"""
 | |
|     display_name = "Boss Shuffle"
 | |
|     option_none = 0
 | |
|     option_basic = 1
 | |
|     option_full = 2
 | |
|     option_chaos = 3
 | |
|     option_singularity = 4
 | |
| 
 | |
|     bosses: set = {
 | |
|         "Armos Knights",
 | |
|         "Lanmolas",
 | |
|         "Moldorm",
 | |
|         "Helmasaur King",
 | |
|         "Arrghus",
 | |
|         "Mothula",
 | |
|         "Blind",
 | |
|         "Kholdstare",
 | |
|         "Vitreous",
 | |
|         "Trinexx",
 | |
|     }
 | |
| 
 | |
|     locations: set = {
 | |
|         "Ganons Tower Top",
 | |
|         "Tower of Hera",
 | |
|         "Skull Woods",
 | |
|         "Ganons Tower Middle",
 | |
|         "Eastern Palace",
 | |
|         "Desert Palace",
 | |
|         "Palace of Darkness",
 | |
|         "Swamp Palace",
 | |
|         "Thieves Town",
 | |
|         "Ice Palace",
 | |
|         "Misery Mire",
 | |
|         "Turtle Rock",
 | |
|         "Ganons Tower Bottom"
 | |
|     }
 | |
| 
 | |
|     def __init__(self, value: typing.Union[str, int]):
 | |
|         assert isinstance(value, str) or isinstance(value, int), \
 | |
|             f"{value} is not a valid option for {self.__class__.__name__}"
 | |
|         self.value = value
 | |
| 
 | |
|     @classmethod
 | |
|     def from_text(cls, text: str):
 | |
|         import random
 | |
|         # set all of our text to lower case for name checking
 | |
|         text = text.lower()
 | |
|         cls.bosses = {boss_name.lower() for boss_name in cls.bosses}
 | |
|         cls.locations = {boss_location.lower() for boss_location in cls.locations}
 | |
|         if text == "random":
 | |
|             return cls(random.choice(list(cls.options.values())))
 | |
|         for option_name, value in cls.options.items():
 | |
|             if option_name == text:
 | |
|                 return cls(value)
 | |
|         options = text.split(";")
 | |
| 
 | |
|         # since plando exists in the option verify the plando values given are valid
 | |
|         cls.validate_plando_bosses(options)
 | |
| 
 | |
|         # find out what type of boss shuffle we should use for placing bosses after plando
 | |
|         # and add as a string to look nice in the spoiler
 | |
|         if "random" in options:
 | |
|             shuffle = random.choice(list(cls.options))
 | |
|             options.remove("random")
 | |
|             options = ";".join(options) + ";" + shuffle
 | |
|             boss_class = cls(options)
 | |
|         else:
 | |
|             for option in options:
 | |
|                 if option in cls.options:
 | |
|                     boss_class = cls(";".join(options))
 | |
|                     break
 | |
|             else:
 | |
|                 if len(options) == 1:
 | |
|                     if cls.valid_boss_name(options[0]):
 | |
|                         options = options[0] + ";singularity"
 | |
|                         boss_class = cls(options)
 | |
|                     else:
 | |
|                         options = options[0] + ";none"
 | |
|                         boss_class = cls(options)
 | |
|                 else:
 | |
|                     options = ";".join(options) + ";none"
 | |
|                     boss_class = cls(options)
 | |
|         return boss_class
 | |
| 
 | |
|     @classmethod
 | |
|     def validate_plando_bosses(cls, options: typing.List[str]) -> None:
 | |
|         from .Bosses import can_place_boss, format_boss_location
 | |
|         for option in options:
 | |
|             if option == "random" or option in cls.options:
 | |
|                 if option != options[-1]:
 | |
|                     raise ValueError(f"{option} option must be at the end of the boss_shuffle options!")
 | |
|                 continue
 | |
|             if "-" in option:
 | |
|                 location, boss = option.split("-")
 | |
|                 level = ''
 | |
|                 if not cls.valid_boss_name(boss):
 | |
|                     raise ValueError(f"{boss} is not a valid boss name for location {location}.")
 | |
|                 if not cls.valid_location_name(location):
 | |
|                     raise ValueError(f"{location} is not a valid boss location name.")
 | |
|                 if location.split(" ")[-1] in ("top", "middle", "bottom"):
 | |
|                     location = location.split(" ")
 | |
|                     level = location[-1]
 | |
|                     location = " ".join(location[:-1])
 | |
|                 location = location.title().replace("Of", "of")
 | |
|                 if not can_place_boss(boss.title(), location, level):
 | |
|                     raise ValueError(f"{format_boss_location(location, level)} "
 | |
|                                      f"is not a valid location for {boss.title()}.")
 | |
|             else:
 | |
|                 if not cls.valid_boss_name(option):
 | |
|                     raise ValueError(f"{option} is not a valid boss name.")
 | |
| 
 | |
|     @classmethod
 | |
|     def valid_boss_name(cls, value: str) -> bool:
 | |
|         return value.lower() in cls.bosses
 | |
| 
 | |
|     @classmethod
 | |
|     def valid_location_name(cls, value: str) -> bool:
 | |
|         return value in cls.locations
 | |
| 
 | |
|     def verify(self, world, player_name: str, plando_options) -> None:
 | |
|         if isinstance(self.value, int):
 | |
|             return
 | |
|         from Generate import PlandoSettings
 | |
|         if not(PlandoSettings.bosses & plando_options):
 | |
|             import logging
 | |
|             # plando is disabled but plando options were given so pull the option and change it to an int
 | |
|             option = self.value.split(";")[-1]
 | |
|             self.value = self.options[option]
 | |
|             logging.warning(f"The plando bosses module is turned off, so {self.name_lookup[self.value].title()} "
 | |
|                     f"boss shuffle will be used for player {player_name}.")
 | |
| 
 | |
| 
 | |
| class Enemies(Choice):
 | |
|     option_vanilla = 0
 | |
|     option_shuffled = 1
 | |
|     option_chaos = 2
 | |
| 
 | |
| 
 | |
| class Progressive(Choice):
 | |
|     display_name = "Progressive Items"
 | |
|     option_off = 0
 | |
|     option_grouped_random = 1
 | |
|     option_on = 2
 | |
|     default = 2
 | |
| 
 | |
|     def want_progressives(self, random):
 | |
|         return random.choice([True, False]) if self.value == self.option_grouped_random else bool(self.value)
 | |
| 
 | |
| 
 | |
| class Swordless(Toggle):
 | |
|     """No swords. Curtains in Skull Woods and Agahnim's
 | |
|     Tower are removed, Agahnim's Tower barrier can be
 | |
|     destroyed with hammer. Misery Mire and Turtle Rock
 | |
|     can be opened without a sword. Hammer damages Ganon.
 | |
|     Ether and Bombos Tablet can be activated with Hammer
 | |
|     (and Book)."""
 | |
|     display_name = "Swordless"
 | |
| 
 | |
| 
 | |
| # Might be a decent idea to split "Bow" into its own option with choices of
 | |
| # Defer to Progressive Option (default), Progressive, Non-Progressive, Bow + Silvers, Retro
 | |
| class RetroBow(Toggle):
 | |
|     """Zelda-1 like mode. You have to purchase a quiver to shoot arrows using rupees."""
 | |
|     display_name = "Retro Bow"
 | |
| 
 | |
| 
 | |
| class RetroCaves(Toggle):
 | |
|     """Zelda-1 like mode. There are randomly placed take-any caves that contain one Sword and
 | |
|     choices of Heart Container/Blue Potion."""
 | |
|     display_name = "Retro Caves"
 | |
| 
 | |
| 
 | |
| class RestrictBossItem(Toggle):
 | |
|     """Don't place dungeon-native items on the dungeon's boss."""
 | |
|     display_name = "Prevent Dungeon Item on Boss"
 | |
| 
 | |
| 
 | |
| class Hints(Choice):
 | |
|     """On/Full: Put item and entrance placement hints on telepathic tiles and some NPCs, Full removes joke hints."""
 | |
|     display_name = "Hints"
 | |
|     option_off = 0
 | |
|     option_on = 2
 | |
|     option_full = 3
 | |
|     default = 2
 | |
| 
 | |
| 
 | |
| class Scams(Choice):
 | |
|     """If on, these Merchants will no longer tell you what they're selling."""
 | |
|     display_name = "Scams"
 | |
|     option_off = 0
 | |
|     option_king_zora = 1
 | |
|     option_bottle_merchant = 2
 | |
|     option_all = 3
 | |
| 
 | |
|     @property
 | |
|     def gives_king_zora_hint(self):
 | |
|         return self.value in {0, 2}
 | |
| 
 | |
|     @property
 | |
|     def gives_bottle_merchant_hint(self):
 | |
|         return self.value in {0, 1}
 | |
| 
 | |
| 
 | |
| class EnemyShuffle(Toggle):
 | |
|     """Randomize every enemy spawn.
 | |
|     If mode is Standard, Hyrule Castle is left out (may result in visually wrong enemy sprites in that area.)"""
 | |
|     display_name = "Enemy Shuffle"
 | |
| 
 | |
| 
 | |
| class KillableThieves(Toggle):
 | |
|     """Makes Thieves killable."""
 | |
|     display_name = "Killable Thieves"
 | |
| 
 | |
| 
 | |
| class BushShuffle(Toggle):
 | |
|     """Randomize chance that a bush contains an enemy as well as which enemy may spawn."""
 | |
|     display_name = "Bush Shuffle"
 | |
| 
 | |
| 
 | |
| class TileShuffle(Toggle):
 | |
|     """Randomize flying tiles floor patterns."""
 | |
|     display_name = "Tile Shuffle"
 | |
| 
 | |
| 
 | |
| class PotShuffle(Toggle):
 | |
|     """Shuffle contents of pots within "supertiles" (item will still be nearby original placement)."""
 | |
|     display_name = "Pot Shuffle"
 | |
| 
 | |
| 
 | |
| class Palette(Choice):
 | |
|     option_default = 0
 | |
|     option_good = 1
 | |
|     option_blackout = 2
 | |
|     option_puke = 3
 | |
|     option_classic = 4
 | |
|     option_grayscale = 5
 | |
|     option_negative = 6
 | |
|     option_dizzy = 7
 | |
|     option_sick = 8
 | |
| 
 | |
| 
 | |
| class OWPalette(Palette):
 | |
|     display_name = "Overworld Palette"
 | |
| 
 | |
| 
 | |
| class UWPalette(Palette):
 | |
|     display_name = "Underworld Palette"
 | |
| 
 | |
| 
 | |
| class HUDPalette(Palette):
 | |
|     display_name = "Menu Palette"
 | |
| 
 | |
| 
 | |
| class SwordPalette(Palette):
 | |
|     display_name = "Sword Palette"
 | |
| 
 | |
| 
 | |
| class ShieldPalette(Palette):
 | |
|     display_name = "Shield Palette"
 | |
| 
 | |
| 
 | |
| # class LinkPalette(Palette):
 | |
| #     display_name = "Link Palette"
 | |
| 
 | |
| 
 | |
| class HeartBeep(Choice):
 | |
|     display_name = "Heart Beep Rate"
 | |
|     option_normal = 0
 | |
|     option_double = 1
 | |
|     option_half = 2
 | |
|     option_quarter = 3
 | |
|     option_off = 4
 | |
| 
 | |
| 
 | |
| class HeartColor(Choice):
 | |
|     display_name = "Heart Color"
 | |
|     option_red = 0
 | |
|     option_blue = 1
 | |
|     option_green = 2
 | |
|     option_yellow = 3
 | |
| 
 | |
| 
 | |
| class QuickSwap(DefaultOnToggle):
 | |
|     display_name = "L/R Quickswapping"
 | |
| 
 | |
| 
 | |
| class MenuSpeed(Choice):
 | |
|     display_name = "Menu Speed"
 | |
|     option_normal = 0
 | |
|     option_instant = 1,
 | |
|     option_double = 2
 | |
|     option_triple = 3
 | |
|     option_quadruple = 4
 | |
|     option_half = 5
 | |
| 
 | |
| 
 | |
| class Music(DefaultOnToggle):
 | |
|     display_name = "Play music"
 | |
| 
 | |
| 
 | |
| class ReduceFlashing(DefaultOnToggle):
 | |
|     display_name = "Reduce Screen Flashes"
 | |
| 
 | |
| 
 | |
| class TriforceHud(Choice):
 | |
|     display_name = "Display Method for Triforce Hunt"
 | |
|     option_normal = 0
 | |
|     option_hide_goal = 1
 | |
|     option_hide_required = 2
 | |
|     option_hide_both = 3
 | |
| 
 | |
| 
 | |
| class BeemizerRange(Range):
 | |
|     value: int
 | |
|     range_start = 0
 | |
|     range_end = 100
 | |
| 
 | |
| 
 | |
| class BeemizerTotalChance(BeemizerRange):
 | |
|     """Percentage chance for each junk-fill item (rupees, bombs, arrows) to be
 | |
|     replaced with either a bee swarm trap or a single bottle-filling bee."""
 | |
|     default = 0
 | |
|     display_name = "Beemizer Total Chance"
 | |
| 
 | |
| 
 | |
| class BeemizerTrapChance(BeemizerRange):
 | |
|     """Percentage chance for each replaced junk-fill item to be a bee swarm
 | |
|     trap; all other replaced items are single bottle-filling bees."""
 | |
|     default = 60
 | |
|     display_name = "Beemizer Trap Chance"
 | |
| 
 | |
| 
 | |
| class AllowCollect(Toggle):
 | |
|     """Allows for !collect / co-op to auto-open chests containing items for other players.
 | |
|     Off by default, because it currently crashes on real hardware."""
 | |
|     display_name = "Allow Collection of checks for other players"
 | |
| 
 | |
| 
 | |
| alttp_options: typing.Dict[str, type(Option)] = {
 | |
|     "crystals_needed_for_gt": CrystalsTower,
 | |
|     "crystals_needed_for_ganon": CrystalsGanon,
 | |
|     "open_pyramid": OpenPyramid,
 | |
|     "bigkey_shuffle": bigkey_shuffle,
 | |
|     "smallkey_shuffle": smallkey_shuffle,
 | |
|     "compass_shuffle": compass_shuffle,
 | |
|     "map_shuffle": map_shuffle,
 | |
|     "progressive": Progressive,
 | |
|     "swordless": Swordless,
 | |
|     "retro_bow": RetroBow,
 | |
|     "retro_caves": RetroCaves,
 | |
|     "hints": Hints,
 | |
|     "scams": Scams,
 | |
|     "restrict_dungeon_item_on_boss": RestrictBossItem,
 | |
|     "boss_shuffle": Bosses,
 | |
|     "pot_shuffle": PotShuffle,
 | |
|     "enemy_shuffle": EnemyShuffle,
 | |
|     "killable_thieves": KillableThieves,
 | |
|     "bush_shuffle": BushShuffle,
 | |
|     "shop_item_slots": ShopItemSlots,
 | |
|     "shop_price_modifier": ShopPriceModifier,
 | |
|     "tile_shuffle": TileShuffle,
 | |
|     "ow_palettes": OWPalette,
 | |
|     "uw_palettes": UWPalette,
 | |
|     "hud_palettes": HUDPalette,
 | |
|     "sword_palettes": SwordPalette,
 | |
|     "shield_palettes": ShieldPalette,
 | |
|     # "link_palettes": LinkPalette,
 | |
|     "heartbeep": HeartBeep,
 | |
|     "heartcolor": HeartColor,
 | |
|     "quickswap": QuickSwap,
 | |
|     "menuspeed": MenuSpeed,
 | |
|     "music": Music,
 | |
|     "reduceflashing": ReduceFlashing,
 | |
|     "triforcehud": TriforceHud,
 | |
|     "glitch_boots": DefaultOnToggle,
 | |
|     "beemizer_total_chance": BeemizerTotalChance,
 | |
|     "beemizer_trap_chance": BeemizerTrapChance,
 | |
|     "death_link": DeathLink,
 | |
|     "allow_collect": AllowCollect
 | |
| }
 |