Skip to content

Commit

Permalink
LttP: Bombless Start and Options/Shops overhaul (#2357)
Browse files Browse the repository at this point in the history
## What is this fixing or adding?
Adds Bombless Start option, along with proper bomb logic. This involves updating `can_kill_most_things` to include checking how many bombs can be held. Many places where the ability to kill enemies was assumed, now have logic. This fixes some possible existing logic issues, for example: Mini Moldorm cave checks currently are always in logic despite the fact that on expert enemy health it would require 12 bombs to kill each mini moldorm.

Overhauls options, pulling them out of core and in particular making large changes to how the shop options work.


Co-authored-by: espeon65536 <[email protected]>
Co-authored-by: black-sliver <[email protected]>
Co-authored-by: Bondo <[email protected]>
Co-authored-by: espeon65536 <[email protected]>
Co-authored-by: Fabian Dill <[email protected]>
  • Loading branch information
6 people authored Feb 20, 2024
1 parent 933e5ba commit 7a86285
Show file tree
Hide file tree
Showing 60 changed files with 1,929 additions and 2,029 deletions.
6 changes: 3 additions & 3 deletions BaseClasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,11 +159,11 @@ def __init__(self, players: int):
self.fix_trock_doors = self.AttributeProxy(
lambda player: self.shuffle[player] != 'vanilla' or self.mode[player] == 'inverted')
self.fix_skullwoods_exit = self.AttributeProxy(
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeons_simple'])
self.fix_palaceofdarkness_exit = self.AttributeProxy(
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeons_simple'])
self.fix_trock_exit = self.AttributeProxy(
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeons_simple'])

for player in range(1, players + 1):
def set_player_attr(attr, val):
Expand Down
118 changes: 0 additions & 118 deletions Generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,20 +315,6 @@ def prefer_int(input_data: str) -> Union[str, int]:
return input_data


goals = {
'ganon': 'ganon',
'crystals': 'crystals',
'bosses': 'bosses',
'pedestal': 'pedestal',
'ganon_pedestal': 'ganonpedestal',
'triforce_hunt': 'triforcehunt',
'local_triforce_hunt': 'localtriforcehunt',
'ganon_triforce_hunt': 'ganontriforcehunt',
'local_ganon_triforce_hunt': 'localganontriforcehunt',
'ice_rod_hunt': 'icerodhunt',
}


def roll_percentage(percentage: Union[int, float]) -> bool:
"""Roll a percentage chance.
percentage is expected to be in range [0, 100]"""
Expand Down Expand Up @@ -357,15 +343,6 @@ def roll_meta_option(option_key, game: str, category_dict: Dict) -> Any:
if options[option_key].supports_weighting:
return get_choice(option_key, category_dict)
return category_dict[option_key]
if game == "A Link to the Past": # TODO wow i hate this
if option_key in {"glitches_required", "dark_room_logic", "entrance_shuffle", "goals", "triforce_pieces_mode",
"triforce_pieces_percentage", "triforce_pieces_available", "triforce_pieces_extra",
"triforce_pieces_required", "shop_shuffle", "mode", "item_pool", "item_functionality",
"boss_shuffle", "enemy_damage", "enemy_health", "timer", "countdown_start_time",
"red_clock_time", "blue_clock_time", "green_clock_time", "dungeon_counters", "shuffle_prizes",
"misery_mire_medallion", "turtle_rock_medallion", "sprite_pool", "sprite",
"random_sprite_on_event"}:
return get_choice(option_key, category_dict)
raise Exception(f"Error generating meta option {option_key} for {game}.")


Expand Down Expand Up @@ -504,101 +481,6 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b


def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
if "dungeon_items" in weights and get_choice_legacy('dungeon_items', weights, "none") != "none":
raise Exception(f"dungeon_items key in A Link to the Past was removed, but is present in these weights as {get_choice_legacy('dungeon_items', weights, False)}.")
glitches_required = get_choice_legacy('glitches_required', weights)
if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'hybrid_major_glitches', 'minor_glitches']:
logging.warning("Only NMG, OWG, HMG and No Logic supported")
glitches_required = 'none'
ret.logic = {None: 'noglitches', 'none': 'noglitches', 'no_logic': 'nologic', 'overworld_glitches': 'owglitches',
'minor_glitches': 'minorglitches', 'hybrid_major_glitches': 'hybridglitches'}[
glitches_required]

ret.dark_room_logic = get_choice_legacy("dark_room_logic", weights, "lamp")
if not ret.dark_room_logic: # None/False
ret.dark_room_logic = "none"
if ret.dark_room_logic == "sconces":
ret.dark_room_logic = "torches"
if ret.dark_room_logic not in {"lamp", "torches", "none"}:
raise ValueError(f"Unknown Dark Room Logic: \"{ret.dark_room_logic}\"")

entrance_shuffle = get_choice_legacy('entrance_shuffle', weights, 'vanilla')
if entrance_shuffle.startswith('none-'):
ret.shuffle = 'vanilla'
else:
ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla'

goal = get_choice_legacy('goals', weights, 'ganon')

ret.goal = goals[goal]


extra_pieces = get_choice_legacy('triforce_pieces_mode', weights, 'available')

ret.triforce_pieces_required = LttPOptions.TriforcePieces.from_any(get_choice_legacy('triforce_pieces_required', weights, 20))

# sum a percentage to required
if extra_pieces == 'percentage':
percentage = max(100, float(get_choice_legacy('triforce_pieces_percentage', weights, 150))) / 100
ret.triforce_pieces_available = int(round(ret.triforce_pieces_required * percentage, 0))
# vanilla mode (specify how many pieces are)
elif extra_pieces == 'available':
ret.triforce_pieces_available = LttPOptions.TriforcePieces.from_any(
get_choice_legacy('triforce_pieces_available', weights, 30))
# required pieces + fixed extra
elif extra_pieces == 'extra':
extra_pieces = max(0, int(get_choice_legacy('triforce_pieces_extra', weights, 10)))
ret.triforce_pieces_available = ret.triforce_pieces_required + extra_pieces

# change minimum to required pieces to avoid problems
ret.triforce_pieces_available = min(max(ret.triforce_pieces_required, int(ret.triforce_pieces_available)), 90)

ret.shop_shuffle = get_choice_legacy('shop_shuffle', weights, '')
if not ret.shop_shuffle:
ret.shop_shuffle = ''

ret.mode = get_choice_legacy("mode", weights)

ret.difficulty = get_choice_legacy('item_pool', weights)

ret.item_functionality = get_choice_legacy('item_functionality', weights)


ret.enemy_damage = {None: 'default',
'default': 'default',
'shuffled': 'shuffled',
'random': 'chaos', # to be removed
'chaos': 'chaos',
}[get_choice_legacy('enemy_damage', weights)]

ret.enemy_health = get_choice_legacy('enemy_health', weights)

ret.timer = {'none': False,
None: False,
False: False,
'timed': 'timed',
'timed_ohko': 'timed-ohko',
'ohko': 'ohko',
'timed_countdown': 'timed-countdown',
'display': 'display'}[get_choice_legacy('timer', weights, False)]

ret.countdown_start_time = int(get_choice_legacy('countdown_start_time', weights, 10))
ret.red_clock_time = int(get_choice_legacy('red_clock_time', weights, -2))
ret.blue_clock_time = int(get_choice_legacy('blue_clock_time', weights, 2))
ret.green_clock_time = int(get_choice_legacy('green_clock_time', weights, 4))

ret.dungeon_counters = get_choice_legacy('dungeon_counters', weights, 'default')

ret.shuffle_prizes = get_choice_legacy('shuffle_prizes', weights, "g")

ret.required_medallions = [get_choice_legacy("misery_mire_medallion", weights, "random"),
get_choice_legacy("turtle_rock_medallion", weights, "random")]

for index, medallion in enumerate(ret.required_medallions):
ret.required_medallions[index] = {"ether": "Ether", "quake": "Quake", "bombos": "Bombos", "random": "random"} \
.get(medallion.lower(), None)
if not ret.required_medallions[index]:
raise Exception(f"unknown Medallion {medallion} for {'misery mire' if index == 0 else 'turtle rock'}")

ret.plando_texts = {}
if PlandoOptions.texts in plando_options:
Expand Down
Binary file modified data/basepatch.bsdiff4
Binary file not shown.
2 changes: 0 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,8 +396,6 @@ def run(self):
folders_to_remove.append(file_name)
shutil.rmtree(world_directory)
shutil.copyfile("meta.yaml", self.buildfolder / "Players" / "Templates" / "meta.yaml")
# TODO: fix LttP options one day
shutil.copyfile("playerSettings.yaml", self.buildfolder / "Players" / "Templates" / "A Link to the Past.yaml")
try:
from maseya import z3pr
except ImportError:
Expand Down
7 changes: 4 additions & 3 deletions worlds/alttp/Bosses.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from Fill import FillError
from .Options import LTTPBosses as Bosses
from .StateHelpers import can_shoot_arrows, can_extend_magic, can_get_good_bee, has_sword, has_beam_sword, \
has_melee_weapon, has_fire_source
has_melee_weapon, has_fire_source, can_use_bombs

if TYPE_CHECKING:
from . import ALTTPWorld
Expand Down Expand Up @@ -62,7 +62,8 @@ def MoldormDefeatRule(state, player: int) -> bool:

def HelmasaurKingDefeatRule(state, player: int) -> bool:
# TODO: technically possible with the hammer
return has_sword(state, player) or can_shoot_arrows(state, player)
return (can_use_bombs(state, player, 5) or state.has("Hammer", player)) and (has_sword(state, player)
or can_shoot_arrows(state, player))


def ArrghusDefeatRule(state, player: int) -> bool:
Expand Down Expand Up @@ -143,7 +144,7 @@ def GanonDefeatRule(state, player: int) -> bool:
can_hurt = has_beam_sword(state, player)
common = can_hurt and has_fire_source(state, player)
# silverless ganon may be needed in anything higher than no glitches
if state.multiworld.logic[player] != 'noglitches':
if state.multiworld.glitches_required[player] != 'no_glitches':
# need to light torch a sufficient amount of times
return common and (state.has('Tempered Sword', player) or state.has('Golden Sword', player) or (
state.has('Silver Bow', player) and can_shoot_arrows(state, player)) or
Expand Down
4 changes: 2 additions & 2 deletions worlds/alttp/Dungeons.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from .Bosses import BossFactory, Boss
from .Items import ItemFactory
from .Regions import lookup_boss_drops, key_drop_data
from .Options import smallkey_shuffle
from .Options import small_key_shuffle

if typing.TYPE_CHECKING:
from .SubClasses import ALttPLocation, ALttPItem
Expand Down Expand Up @@ -66,7 +66,7 @@ def create_dungeons(world: "ALTTPWorld"):

def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dungeon_items):
dungeon = Dungeon(name, dungeon_regions, big_key,
[] if multiworld.smallkey_shuffle[player] == smallkey_shuffle.option_universal else small_keys,
[] if multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal else small_keys,
dungeon_items, player)
for item in dungeon.all_items:
item.dungeon = dungeon
Expand Down
12 changes: 6 additions & 6 deletions worlds/alttp/EntranceRandomizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def defval(value):
multiargs, _ = parser.parse_known_args(argv)

parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument('--logic', default=defval('noglitches'), const='noglitches', nargs='?', choices=['noglitches', 'minorglitches', 'owglitches', 'hybridglitches', 'nologic'],
parser.add_argument('--logic', default=defval('no_glitches'), const='no_glitches', nargs='?', choices=['no_glitches', 'minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'],
help='''\
Select Enforcement of Item Requirements. (default: %(default)s)
No Glitches:
Expand All @@ -49,7 +49,7 @@ def defval(value):
instead of a bunny.
''')
parser.add_argument('--goal', default=defval('ganon'), const='ganon', nargs='?',
choices=['ganon', 'pedestal', 'bosses', 'triforcehunt', 'localtriforcehunt', 'ganontriforcehunt', 'localganontriforcehunt', 'crystals', 'ganonpedestal'],
choices=['ganon', 'pedestal', 'bosses', 'triforce_hunt', 'local_triforce_hunt', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'crystals', 'ganon_pedestal'],
help='''\
Select completion goal. (default: %(default)s)
Ganon: Collect all crystals, beat Agahnim 2 then
Expand Down Expand Up @@ -92,7 +92,7 @@ def defval(value):
Hard: Reduced functionality.
Expert: Greatly reduced functionality.
''')
parser.add_argument('--timer', default=defval('none'), const='normal', nargs='?', choices=['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'],
parser.add_argument('--timer', default=defval('none'), const='normal', nargs='?', choices=['none', 'display', 'timed', 'timed_ohko', 'ohko', 'timed_countdown'],
help='''\
Select game timer setting. Affects available itempool. (default: %(default)s)
None: No timer.
Expand Down Expand Up @@ -151,7 +151,7 @@ def defval(value):
slightly biased to placing progression items with
less restrictions.
''')
parser.add_argument('--shuffle', default=defval('vanilla'), const='vanilla', nargs='?', choices=['vanilla', 'simple', 'restricted', 'full', 'crossed', 'insanity', 'restricted_legacy', 'full_legacy', 'madness_legacy', 'insanity_legacy', 'dungeonsfull', 'dungeonssimple', 'dungeonscrossed'],
parser.add_argument('--shuffle', default=defval('vanilla'), const='vanilla', nargs='?', choices=['vanilla', 'simple', 'restricted', 'full', 'crossed', 'insanity', 'restricted_legacy', 'full_legacy', 'madness_legacy', 'insanity_legacy', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed'],
help='''\
Select Entrance Shuffling Algorithm. (default: %(default)s)
Full: Mix cave and dungeon entrances freely while limiting
Expand All @@ -178,9 +178,9 @@ def defval(value):
parser.add_argument('--open_pyramid', default=defval('auto'), help='''\
Pre-opens the pyramid hole, this removes the Agahnim 2 requirement for it.
Depending on goal, you might still need to beat Agahnim 2 in order to beat ganon.
fast ganon goals are crystals, ganontriforcehunt, localganontriforcehunt, pedestalganon
fast ganon goals are crystals, ganon_triforce_hunt, local_ganon_triforce_hunt, pedestalganon
auto - Only opens pyramid hole if the goal specifies a fast ganon, and entrance shuffle
is vanilla, dungeonssimple or dungeonsfull.
is vanilla, dungeons_simple or dungeons_full.
goal - Opens pyramid hole if the goal specifies a fast ganon.
yes - Always opens the pyramid hole.
no - Never opens the pyramid hole.
Expand Down
Loading

0 comments on commit 7a86285

Please sign in to comment.