Merge remote-tracking branch 'Main/main' into rework_accessibility

# Conflicts:
#	BaseClasses.py
#	worlds/alttp/Rules.py
This commit is contained in:
alwaysintreble
2023-04-06 02:54:24 -05:00
519 changed files with 52417 additions and 5503 deletions

View File

@@ -2,13 +2,12 @@ from __future__ import annotations
import copy
import functools
import json
import logging
import random
import secrets
import typing # this can go away when Python 3.8 support is dropped
from argparse import Namespace
from collections import OrderedDict, Counter, deque
from collections import OrderedDict, Counter, deque, ChainMap
from enum import IntEnum, IntFlag
from typing import List, Dict, Optional, Set, Iterable, Union, Any, Tuple, TypedDict, Callable, NamedTuple
@@ -72,6 +71,11 @@ class MultiWorld():
completion_condition: Dict[int, Callable[[CollectionState], bool]]
indirect_connections: Dict[Region, Set[Entrance]]
exclude_locations: Dict[int, Options.ExcludeLocations]
priority_locations: Dict[int, Options.PriorityLocations]
start_inventory: Dict[int, Options.StartInventory]
start_hints: Dict[int, Options.StartHints]
start_location_hints: Dict[int, Options.StartLocationHints]
item_links: Dict[int, Options.ItemLinks]
game: Dict[int, str]
@@ -332,7 +336,7 @@ class MultiWorld():
return self.player_name[player]
def get_file_safe_player_name(self, player: int) -> str:
return ''.join(c for c in self.get_player_name(player) if c not in '<>:"/\\|?*')
return Utils.get_file_safe_name(self.get_player_name(player))
def get_out_file_name_base(self, player: int) -> str:
""" the base name (without file extension) for each player's output file for a seed """
@@ -761,169 +765,9 @@ class CollectionState():
found += self.prog_items[item_name, player]
return found
def can_buy_unlimited(self, item: str, player: int) -> bool:
return any(shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(self) for
shop in self.multiworld.shops)
def can_buy(self, item: str, player: int) -> bool:
return any(shop.region.player == player and shop.has(item) and shop.region.can_reach(self) for
shop in self.multiworld.shops)
def item_count(self, item: str, player: int) -> int:
return self.prog_items[item, player]
def has_triforce_pieces(self, count: int, player: int) -> bool:
return self.item_count('Triforce Piece', player) + self.item_count('Power Star', player) >= count
def has_crystals(self, count: int, player: int) -> bool:
found: int = 0
for crystalnumber in range(1, 8):
found += self.prog_items[f"Crystal {crystalnumber}", player]
if found >= count:
return True
return False
def can_lift_rocks(self, player: int):
return self.has('Power Glove', player) or self.has('Titans Mitts', player)
def bottle_count(self, player: int) -> int:
return min(self.multiworld.difficulty_requirements[player].progressive_bottle_limit,
self.count_group("Bottles", player))
def has_hearts(self, player: int, count: int) -> int:
# Warning: This only considers items that are marked as advancement items
return self.heart_count(player) >= count
def heart_count(self, player: int) -> int:
# Warning: This only considers items that are marked as advancement items
diff = self.multiworld.difficulty_requirements[player]
return min(self.item_count('Boss Heart Container', player), diff.boss_heart_container_limit) \
+ self.item_count('Sanctuary Heart Container', player) \
+ min(self.item_count('Piece of Heart', player), diff.heart_piece_limit) // 4 \
+ 3 # starting hearts
def can_lift_heavy_rocks(self, player: int) -> bool:
return self.has('Titans Mitts', player)
def can_extend_magic(self, player: int, smallmagic: int = 16,
fullrefill: bool = False): # This reflects the total magic Link has, not the total extra he has.
basemagic = 8
if self.has('Magic Upgrade (1/4)', player):
basemagic = 32
elif self.has('Magic Upgrade (1/2)', player):
basemagic = 16
if self.can_buy_unlimited('Green Potion', player) or self.can_buy_unlimited('Blue Potion', player):
if self.multiworld.item_functionality[player] == 'hard' and not fullrefill:
basemagic = basemagic + int(basemagic * 0.5 * self.bottle_count(player))
elif self.multiworld.item_functionality[player] == 'expert' and not fullrefill:
basemagic = basemagic + int(basemagic * 0.25 * self.bottle_count(player))
else:
basemagic = basemagic + basemagic * self.bottle_count(player)
return basemagic >= smallmagic
def can_kill_most_things(self, player: int, enemies: int = 5) -> bool:
return (self.has_melee_weapon(player)
or self.has('Cane of Somaria', player)
or (self.has('Cane of Byrna', player) and (enemies < 6 or self.can_extend_magic(player)))
or self.can_shoot_arrows(player)
or self.has('Fire Rod', player)
or (self.has('Bombs (10)', player) and enemies < 6))
def can_shoot_arrows(self, player: int) -> bool:
if self.multiworld.retro_bow[player]:
return (self.has('Bow', player) or self.has('Silver Bow', player)) and self.can_buy('Single Arrow', player)
return self.has('Bow', player) or self.has('Silver Bow', player)
def can_get_good_bee(self, player: int) -> bool:
cave = self.multiworld.get_region('Good Bee Cave', player)
return (
self.has_group("Bottles", player) and
self.has('Bug Catching Net', player) and
(self.has('Pegasus Boots', player) or (self.has_sword(player) and self.has('Quake', player))) and
cave.can_reach(self) and
self.is_not_bunny(cave, player)
)
def can_retrieve_tablet(self, player: int) -> bool:
return self.has('Book of Mudora', player) and (self.has_beam_sword(player) or
(self.multiworld.swordless[player] and
self.has("Hammer", player)))
def has_sword(self, player: int) -> bool:
return self.has('Fighter Sword', player) \
or self.has('Master Sword', player) \
or self.has('Tempered Sword', player) \
or self.has('Golden Sword', player)
def has_beam_sword(self, player: int) -> bool:
return self.has('Master Sword', player) or self.has('Tempered Sword', player) or self.has('Golden Sword',
player)
def has_melee_weapon(self, player: int) -> bool:
return self.has_sword(player) or self.has('Hammer', player)
def has_fire_source(self, player: int) -> bool:
return self.has('Fire Rod', player) or self.has('Lamp', player)
def can_melt_things(self, player: int) -> bool:
return self.has('Fire Rod', player) or \
(self.has('Bombos', player) and
(self.multiworld.swordless[player] or
self.has_sword(player)))
def can_avoid_lasers(self, player: int) -> bool:
return self.has('Mirror Shield', player) or self.has('Cane of Byrna', player) or self.has('Cape', player)
def is_not_bunny(self, region: Region, player: int) -> bool:
if self.has('Moon Pearl', player):
return True
return region.is_light_world if self.multiworld.mode[player] != 'inverted' else region.is_dark_world
def can_reach_light_world(self, player: int) -> bool:
if True in [i.is_light_world for i in self.reachable_regions[player]]:
return True
return False
def can_reach_dark_world(self, player: int) -> bool:
if True in [i.is_dark_world for i in self.reachable_regions[player]]:
return True
return False
def has_misery_mire_medallion(self, player: int) -> bool:
return self.has(self.multiworld.required_medallions[player][0], player)
def has_turtle_rock_medallion(self, player: int) -> bool:
return self.has(self.multiworld.required_medallions[player][1], player)
def can_boots_clip_lw(self, player: int) -> bool:
if self.multiworld.mode[player] == 'inverted':
return self.has('Pegasus Boots', player) and self.has('Moon Pearl', player)
return self.has('Pegasus Boots', player)
def can_boots_clip_dw(self, player: int) -> bool:
if self.multiworld.mode[player] != 'inverted':
return self.has('Pegasus Boots', player) and self.has('Moon Pearl', player)
return self.has('Pegasus Boots', player)
def can_get_glitched_speed_lw(self, player: int) -> bool:
rules = [self.has('Pegasus Boots', player), any([self.has('Hookshot', player), self.has_sword(player)])]
if self.multiworld.mode[player] == 'inverted':
rules.append(self.has('Moon Pearl', player))
return all(rules)
def can_superbunny_mirror_with_sword(self, player: int) -> bool:
return self.has('Magic Mirror', player) and self.has_sword(player)
def can_get_glitched_speed_dw(self, player: int) -> bool:
rules = [self.has('Pegasus Boots', player), any([self.has('Hookshot', player), self.has_sword(player)])]
if self.multiworld.mode[player] != 'inverted':
rules.append(self.has('Moon Pearl', player))
return all(rules)
def can_bomb_clip(self, region: Region, player: int) -> bool:
return self.is_not_bunny(region, player) and self.has('Pegasus Boots', player)
def collect(self, item: Item, event: bool = False, location: Optional[Location] = None) -> bool:
if location:
self.locations_checked.add(location)
@@ -959,12 +803,6 @@ class Region:
exits: List[Entrance]
locations: List[Location]
dungeon: Optional[Dungeon] = None
shop: Optional = None
# LttP specific. TODO: move to a LttPRegion
# will be set after making connections.
is_light_world: bool = False
is_dark_world: bool = False
def __init__(self, name: str, player: int, multiworld: MultiWorld, hint: Optional[str] = None):
self.name = name
@@ -1129,7 +967,7 @@ class Location:
self.parent_region = parent
def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
return (self.always_allow(state, item)
return ((self.always_allow(state, item) and item.name not in state.multiworld.non_local_items[item.player])
or ((self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful))
and self.item_rule(item)
and (not check_access or self.can_reach(state))))
@@ -1371,7 +1209,7 @@ class Spoiler():
raise RuntimeError(f'Not all required items reachable. Unreachable locations: {required_locations}')
# we can finally output our playthrough
self.playthrough = {"0": sorted([str(item) for item in
self.playthrough = {"0": sorted([self.multiworld.get_name_string_for_object(item) for item in
chain.from_iterable(multiworld.precollected_items.values())
if item.advancement])}
@@ -1429,7 +1267,7 @@ class Spoiler():
res = getattr(self.multiworld, option_key)[player]
display_name = getattr(option_obj, "display_name", option_key)
try:
outfile.write(f'{display_name + ":":33}{res.get_current_option_name()}\n')
outfile.write(f'{display_name + ":":33}{res.current_option_name}\n')
except:
raise Exception
@@ -1446,12 +1284,11 @@ class Spoiler():
if self.multiworld.players > 1:
outfile.write('\nPlayer %d: %s\n' % (player, self.multiworld.get_player_name(player)))
outfile.write('Game: %s\n' % self.multiworld.game[player])
for f_option, option in Options.per_game_common_options.items():
options = ChainMap(Options.per_game_common_options, self.multiworld.worlds[player].option_definitions)
for f_option, option in options.items():
write_option(f_option, option)
options = self.multiworld.worlds[player].option_definitions
if options:
for f_option, option in options.items():
write_option(f_option, option)
AutoWorld.call_single(self.multiworld, "write_spoiler_header", player, outfile)
if self.entrances:
@@ -1465,7 +1302,7 @@ class Spoiler():
AutoWorld.call_all(self.multiworld, "write_spoiler", outfile)
locations = [(str(location), str(location.item) if location.item is not None else "Nothing")
for location in self.multiworld.get_locations()]
for location in self.multiworld.get_locations() if location.show_in_spoiler]
outfile.write('\n\nLocations:\n\n')
outfile.write('\n'.join(
['%s: %s' % (location, item) for location, item in locations]))