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