LttP: extract Dungeon and Boss from core (#1787)

This commit is contained in:
Fabian Dill 2023-05-20 19:57:48 +02:00 committed by GitHub
parent a2ddd5c9e8
commit c8453035da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 342 additions and 305 deletions

View File

@ -96,7 +96,6 @@ class MultiWorld():
self.player_types = {player: NetUtils.SlotType.player for player in self.player_ids} self.player_types = {player: NetUtils.SlotType.player for player in self.player_ids}
self.glitch_triforce = False self.glitch_triforce = False
self.algorithm = 'balanced' self.algorithm = 'balanced'
self.dungeons: Dict[Tuple[str, int], Dungeon] = {}
self.groups = {} self.groups = {}
self.regions = [] self.regions = []
self.shops = [] self.shops = []
@ -386,12 +385,6 @@ class MultiWorld():
self._recache() self._recache()
return self._location_cache[location, player] return self._location_cache[location, player]
def get_dungeon(self, dungeonname: str, player: int) -> Dungeon:
try:
return self.dungeons[dungeonname, player]
except KeyError as e:
raise KeyError('No such dungeon %s for player %d' % (dungeonname, player)) from e
def get_all_state(self, use_cache: bool) -> CollectionState: def get_all_state(self, use_cache: bool) -> CollectionState:
cached = getattr(self, "_all_state", None) cached = getattr(self, "_all_state", None)
if use_cache and cached: if use_cache and cached:
@ -801,7 +794,6 @@ class Region:
entrances: List[Entrance] entrances: List[Entrance]
exits: List[Entrance] exits: List[Entrance]
locations: List[Location] locations: List[Location]
dungeon: Optional[Dungeon] = None
def __init__(self, name: str, player: int, multiworld: MultiWorld, hint: Optional[str] = None): def __init__(self, name: str, player: int, multiworld: MultiWorld, hint: Optional[str] = None):
self.name = name self.name = name
@ -904,63 +896,6 @@ class Entrance:
return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})' return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
class Dungeon(object):
def __init__(self, name: str, regions: List[Region], big_key: Item, small_keys: List[Item],
dungeon_items: List[Item], player: int):
self.name = name
self.regions = regions
self.big_key = big_key
self.small_keys = small_keys
self.dungeon_items = dungeon_items
self.bosses = dict()
self.player = player
self.multiworld = None
@property
def boss(self) -> Optional[Boss]:
return self.bosses.get(None, None)
@boss.setter
def boss(self, value: Optional[Boss]):
self.bosses[None] = value
@property
def keys(self) -> List[Item]:
return self.small_keys + ([self.big_key] if self.big_key else [])
@property
def all_items(self) -> List[Item]:
return self.dungeon_items + self.keys
def is_dungeon_item(self, item: Item) -> bool:
return item.player == self.player and item.name in (dungeon_item.name for dungeon_item in self.all_items)
def __eq__(self, other: Dungeon) -> bool:
if not other:
return False
return self.name == other.name and self.player == other.player
def __repr__(self):
return self.__str__()
def __str__(self):
return self.multiworld.get_name_string_for_object(self) if self.multiworld else f'{self.name} (Player {self.player})'
class Boss():
def __init__(self, name: str, enemizer_name: str, defeat_rule: Callable, player: int):
self.name = name
self.enemizer_name = enemizer_name
self.defeat_rule = defeat_rule
self.player = player
def can_defeat(self, state) -> bool:
return self.defeat_rule(state, self.player)
def __repr__(self):
return f"Boss({self.name})"
class LocationProgressType(IntEnum): class LocationProgressType(IntEnum):
DEFAULT = 1 DEFAULT = 1
PRIORITY = 2 PRIORITY = 2

View File

@ -1,10 +1,29 @@
import logging from __future__ import annotations
from typing import Optional, Union, List, Tuple, Callable, Dict
import logging
from typing import Optional, Union, List, Tuple, Callable, Dict, TYPE_CHECKING
from BaseClasses import Boss
from Fill import FillError from Fill import FillError
from .Options import LTTPBosses as Bosses 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 from .StateHelpers import can_shoot_arrows, can_extend_magic, can_get_good_bee, has_sword, has_beam_sword, \
has_melee_weapon, has_fire_source
if TYPE_CHECKING:
from . import ALTTPWorld
class Boss:
def __init__(self, name: str, enemizer_name: str, defeat_rule: Callable, player: int):
self.name = name
self.enemizer_name = enemizer_name
self.defeat_rule = defeat_rule
self.player = player
def can_defeat(self, state) -> bool:
return self.defeat_rule(state, self.player)
def __repr__(self):
return f"Boss({self.name})"
def BossFactory(boss: str, player: int) -> Optional[Boss]: def BossFactory(boss: str, player: int) -> Optional[Boss]:
@ -166,10 +185,10 @@ boss_location_table: List[Tuple[str, str]] = [
] ]
def place_plando_bosses(bosses: List[str], world, player: int) -> Tuple[List[str], List[Tuple[str, str]]]: def place_plando_bosses(world: "ALTTPWorld", bosses: List[str]) -> Tuple[List[str], List[Tuple[str, str]]]:
# Most to least restrictive order # Most to least restrictive order
boss_locations = boss_location_table.copy() boss_locations = boss_location_table.copy()
world.random.shuffle(boss_locations) world.multiworld.random.shuffle(boss_locations)
boss_locations.sort(key=lambda location: -int(restrictive_boss_locations[location])) boss_locations.sort(key=lambda location: -int(restrictive_boss_locations[location]))
already_placed_bosses: List[str] = [] already_placed_bosses: List[str] = []
@ -184,12 +203,12 @@ def place_plando_bosses(bosses: List[str], world, player: int) -> Tuple[List[str
level = loc[-1] level = loc[-1]
loc = " ".join(loc[:-1]) loc = " ".join(loc[:-1])
loc = loc.title().replace("Of", "of") loc = loc.title().replace("Of", "of")
place_boss(world, player, boss, loc, level) place_boss(world, boss, loc, level)
already_placed_bosses.append(boss) already_placed_bosses.append(boss)
boss_locations.remove((loc, level)) boss_locations.remove((loc, level))
else: # boss chosen with no specified locations else: # boss chosen with no specified locations
boss = boss.title() boss = boss.title()
boss_locations, already_placed_bosses = place_where_possible(world, player, boss, boss_locations) boss_locations, already_placed_bosses = place_where_possible(world, boss, boss_locations)
return already_placed_bosses, boss_locations return already_placed_bosses, boss_locations
@ -224,20 +243,23 @@ for location in boss_location_table:
for boss in boss_table if not boss.startswith("Agahnim")) for boss in boss_table if not boss.startswith("Agahnim"))
def place_boss(world, player: int, boss: str, location: str, level: Optional[str]) -> None: def place_boss(world: "ALTTPWorld", boss: str, location: str, level: Optional[str]) -> None:
if location == 'Ganons Tower' and world.mode[player] == 'inverted': player = world.player
if location == 'Ganons Tower' and world.multiworld.mode[player] == 'inverted':
location = 'Inverted Ganons Tower' location = 'Inverted Ganons Tower'
logging.debug('Placing boss %s at %s', boss, location + (' (' + level + ')' if level else '')) logging.debug('Placing boss %s at %s', boss, location + (' (' + level + ')' if level else ''))
world.get_dungeon(location, player).bosses[level] = BossFactory(boss, player) world.dungeons[location].bosses[level] = BossFactory(boss, player)
def format_boss_location(location: str, level: str) -> str: def format_boss_location(location_name: str, level: str) -> str:
return location + (' (' + level + ')' if level else '') return location_name + (' (' + level + ')' if level else '')
def place_bosses(world, player: int) -> None: def place_bosses(world: "ALTTPWorld") -> None:
multiworld = world.multiworld
player = world.player
# will either be an int or a lower case string with ';' between options # will either be an int or a lower case string with ';' between options
boss_shuffle: Union[str, int] = world.boss_shuffle[player].value boss_shuffle: Union[str, int] = multiworld.boss_shuffle[player].value
already_placed_bosses: List[str] = [] already_placed_bosses: List[str] = []
remaining_locations: List[Tuple[str, str]] = [] remaining_locations: List[Tuple[str, str]] = []
# handle plando # handle plando
@ -246,14 +268,14 @@ def place_bosses(world, player: int) -> None:
options = boss_shuffle.split(";") options = boss_shuffle.split(";")
boss_shuffle = Bosses.options[options.pop()] boss_shuffle = Bosses.options[options.pop()]
# place our plando bosses # place our plando bosses
already_placed_bosses, remaining_locations = place_plando_bosses(options, world, player) already_placed_bosses, remaining_locations = place_plando_bosses(world, options)
if boss_shuffle == Bosses.option_none: # vanilla boss locations if boss_shuffle == Bosses.option_none: # vanilla boss locations
return return
# Most to least restrictive order # Most to least restrictive order
if not remaining_locations and not already_placed_bosses: if not remaining_locations and not already_placed_bosses:
remaining_locations = boss_location_table.copy() remaining_locations = boss_location_table.copy()
world.random.shuffle(remaining_locations) multiworld.random.shuffle(remaining_locations)
remaining_locations.sort(key=lambda location: -int(restrictive_boss_locations[location])) remaining_locations.sort(key=lambda location: -int(restrictive_boss_locations[location]))
all_bosses = sorted(boss_table.keys()) # sorted to be deterministic on older pythons all_bosses = sorted(boss_table.keys()) # sorted to be deterministic on older pythons
@ -263,7 +285,7 @@ def place_bosses(world, player: int) -> None:
if boss_shuffle == Bosses.option_basic: # vanilla bosses shuffled if boss_shuffle == Bosses.option_basic: # vanilla bosses shuffled
bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm'] bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm']
else: # all bosses present, the three duplicates chosen at random else: # all bosses present, the three duplicates chosen at random
bosses = placeable_bosses + world.random.sample(placeable_bosses, 3) bosses = placeable_bosses + multiworld.random.sample(placeable_bosses, 3)
# there is probably a better way to do this # there is probably a better way to do this
while already_placed_bosses: while already_placed_bosses:
@ -275,7 +297,7 @@ def place_bosses(world, player: int) -> None:
logging.debug('Bosses chosen %s', bosses) logging.debug('Bosses chosen %s', bosses)
world.random.shuffle(bosses) multiworld.random.shuffle(bosses)
for loc, level in remaining_locations: for loc, level in remaining_locations:
for _ in range(len(bosses)): for _ in range(len(bosses)):
boss = bosses.pop() boss = bosses.pop()
@ -288,39 +310,39 @@ def place_bosses(world, player: int) -> None:
else: else:
raise FillError(f'Could not place boss for location {format_boss_location(loc, level)}') raise FillError(f'Could not place boss for location {format_boss_location(loc, level)}')
place_boss(world, player, boss, loc, level) place_boss(world, boss, loc, level)
elif boss_shuffle == Bosses.option_chaos: # all bosses chosen at random elif boss_shuffle == Bosses.option_chaos: # all bosses chosen at random
for loc, level in remaining_locations: for loc, level in remaining_locations:
try: try:
boss = world.random.choice( boss = multiworld.random.choice(
[b for b in placeable_bosses if can_place_boss(b, loc, level)]) [b for b in placeable_bosses if can_place_boss(b, loc, level)])
except IndexError: except IndexError:
raise FillError(f'Could not place boss for location {format_boss_location(loc, level)}') raise FillError(f'Could not place boss for location {format_boss_location(loc, level)}')
else: else:
place_boss(world, player, boss, loc, level) place_boss(world, boss, loc, level)
elif boss_shuffle == Bosses.option_singularity: elif boss_shuffle == Bosses.option_singularity:
primary_boss = world.random.choice(placeable_bosses) primary_boss = multiworld.random.choice(placeable_bosses)
remaining_boss_locations, _ = place_where_possible(world, player, primary_boss, remaining_locations) remaining_boss_locations, _ = place_where_possible(world, primary_boss, remaining_locations)
if remaining_boss_locations: if remaining_boss_locations:
# pick a boss to go into the remaining locations # pick a boss to go into the remaining locations
remaining_boss = world.random.choice([boss for boss in placeable_bosses if all( remaining_boss = multiworld.random.choice([boss for boss in placeable_bosses if all(
can_place_boss(boss, loc, level) for loc, level in remaining_boss_locations)]) can_place_boss(boss, loc, level) for loc, level in remaining_boss_locations)])
remaining_boss_locations, _ = place_where_possible(world, player, remaining_boss, remaining_boss_locations) remaining_boss_locations, _ = place_where_possible(world, remaining_boss, remaining_boss_locations)
if remaining_boss_locations: if remaining_boss_locations:
raise Exception("Unfilled boss locations!") raise Exception("Unfilled boss locations!")
else: else:
raise FillError(f"Could not find boss shuffle mode {boss_shuffle}") raise FillError(f"Could not find boss shuffle mode {boss_shuffle}")
def place_where_possible(world, player: int, boss: str, boss_locations) -> Tuple[List[Tuple[str, str]], List[str]]: def place_where_possible(world: "ALTTPWorld", boss: str, boss_locations) -> Tuple[List[Tuple[str, str]], List[str]]:
remainder: List[Tuple[str, str]] = [] remainder: List[Tuple[str, str]] = []
placed_bosses: List[str] = [] placed_bosses: List[str] = []
for loc, level in boss_locations: for loc, level in boss_locations:
# place that boss where it can go # place that boss where it can go
if can_place_boss(boss, loc, level): if can_place_boss(boss, loc, level):
place_boss(world, player, boss, loc, level) place_boss(world, boss, loc, level)
placed_bosses.append(boss) placed_bosses.append(boss)
else: else:
remainder.append((loc, level)) remainder.append((loc, level))

View File

@ -1,28 +1,83 @@
import typing from __future__ import annotations
from BaseClasses import CollectionState, Dungeon import typing
from typing import List, Optional
from BaseClasses import CollectionState, Region, MultiWorld
from Fill import fill_restrictive from Fill import fill_restrictive
from .Bosses import BossFactory from .Bosses import BossFactory, Boss
from .Items import ItemFactory from .Items import ItemFactory
from .Regions import lookup_boss_drops from .Regions import lookup_boss_drops
from .Options import smallkey_shuffle from .Options import smallkey_shuffle
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from .SubClasses import ALttPLocation from .SubClasses import ALttPLocation, ALttPItem
from . import ALTTPWorld
def create_dungeons(world, player): class Dungeon:
def __init__(self, name: str, regions: List[Region], big_key: ALttPItem, small_keys: List[ALttPItem],
dungeon_items: List[ALttPItem], player: int):
self.name = name
self.regions = regions
self.big_key = big_key
self.small_keys = small_keys
self.dungeon_items = dungeon_items
self.bosses = dict()
self.player = player
self.multiworld = None
@property
def boss(self) -> Optional[Boss]:
return self.bosses.get(None, None)
@boss.setter
def boss(self, value: Optional[Boss]):
self.bosses[None] = value
@property
def keys(self) -> List[ALttPItem]:
return self.small_keys + ([self.big_key] if self.big_key else [])
@property
def all_items(self) -> List[ALttPItem]:
return self.dungeon_items + self.keys
def is_dungeon_item(self, item: ALttPItem) -> bool:
return item.player == self.player and item.name in (dungeon_item.name for dungeon_item in self.all_items)
def __eq__(self, other: Dungeon) -> bool:
if not other:
return False
return self.name == other.name and self.player == other.player
def __repr__(self):
return self.__str__()
def __str__(self):
return self.multiworld.get_name_string_for_object(self) if self.multiworld \
else f'{self.name} (Player {self.player})'
def create_dungeons(world: "ALTTPWorld"):
multiworld = world.multiworld
player = world.player
def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dungeon_items): def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dungeon_items):
dungeon = Dungeon(name, dungeon_regions, big_key, dungeon = Dungeon(name, dungeon_regions, big_key,
[] if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal else small_keys, [] if multiworld.smallkey_shuffle[player] == smallkey_shuffle.option_universal else small_keys,
dungeon_items, player) dungeon_items, player)
for item in dungeon.all_items: for item in dungeon.all_items:
item.dungeon = dungeon item.dungeon = dungeon
dungeon.boss = BossFactory(default_boss, player) if default_boss else None dungeon.boss = BossFactory(default_boss, player) if default_boss else None
for region in dungeon.regions: regions = []
world.get_region(region, player).dungeon = dungeon for region_name in dungeon.regions:
dungeon.multiworld = world region = multiworld.get_region(region_name, player)
region.dungeon = dungeon
regions.append(region)
dungeon.multiworld = multiworld
dungeon.regions = regions
return dungeon return dungeon
ES = make_dungeon('Hyrule Castle', None, ['Hyrule Castle', 'Sewers', 'Sewer Drop', 'Sewers (Dark)', 'Sanctuary'], ES = make_dungeon('Hyrule Castle', None, ['Hyrule Castle', 'Sewers', 'Sewer Drop', 'Sewers (Dark)', 'Sanctuary'],
@ -83,7 +138,7 @@ def create_dungeons(world, player):
ItemFactory(['Small Key (Turtle Rock)'] * 4, player), ItemFactory(['Small Key (Turtle Rock)'] * 4, player),
ItemFactory(['Map (Turtle Rock)', 'Compass (Turtle Rock)'], player)) ItemFactory(['Map (Turtle Rock)', 'Compass (Turtle Rock)'], player))
if world.mode[player] != 'inverted': if multiworld.mode[player] != 'inverted':
AT = make_dungeon('Agahnims Tower', 'Agahnim', ['Agahnims Tower', 'Agahnim 1'], None, AT = make_dungeon('Agahnims Tower', 'Agahnim', ['Agahnims Tower', 'Agahnim 1'], None,
ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), []) ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), [])
GT = make_dungeon('Ganons Tower', 'Agahnim2', GT = make_dungeon('Ganons Tower', 'Agahnim2',
@ -111,26 +166,34 @@ def create_dungeons(world, player):
GT.bosses['top'] = BossFactory('Moldorm', player) GT.bosses['top'] = BossFactory('Moldorm', player)
for dungeon in [ES, EP, DP, ToH, AT, PoD, TT, SW, SP, IP, MM, TR, GT]: for dungeon in [ES, EP, DP, ToH, AT, PoD, TT, SW, SP, IP, MM, TR, GT]:
world.dungeons[dungeon.name, dungeon.player] = dungeon world.dungeons[dungeon.name] = dungeon
def get_dungeon_item_pool(world) -> typing.List: def get_dungeon_item_pool(multiworld: MultiWorld) -> typing.List[ALttPItem]:
return [item for dungeon in world.dungeons.values() for item in dungeon.all_items] return [item
for world in multiworld.get_game_worlds("A Link to the Past")
for item in get_dungeon_item_pool_player(world)]
def get_dungeon_item_pool_player(world, player) -> typing.List: def get_dungeon_item_pool_player(world) -> typing.List[ALttPItem]:
return [item for dungeon in world.dungeons.values() if dungeon.player == player for item in dungeon.all_items] return [item
for dungeon in world.dungeons.values()
for item in dungeon.all_items]
def get_unfilled_dungeon_locations(multiworld) -> typing.List: def get_unfilled_dungeon_locations(multiworld: MultiWorld) -> typing.List[ALttPLocation]:
return [location for location in multiworld.get_locations() if not location.item and location.parent_region.dungeon] return [location
for world in multiworld.get_game_worlds("A Link to the Past")
for dungeon in world.dungeons.values()
for region in dungeon.regions
for location in region.locations if not location.item]
def fill_dungeons_restrictive(world): def fill_dungeons_restrictive(multiworld: MultiWorld):
"""Places dungeon-native items into their dungeons, places nothing if everything is shuffled outside.""" """Places dungeon-native items into their dungeons, places nothing if everything is shuffled outside."""
localized: set = set() localized: set = set()
dungeon_specific: set = set() dungeon_specific: set = set()
for subworld in world.get_game_worlds("A Link to the Past"): for subworld in multiworld.get_game_worlds("A Link to the Past"):
player = subworld.player player = subworld.player
localized |= {(player, item_name) for item_name in localized |= {(player, item_name) for item_name in
subworld.dungeon_local_item_names} subworld.dungeon_local_item_names}
@ -138,12 +201,12 @@ def fill_dungeons_restrictive(world):
subworld.dungeon_specific_item_names} subworld.dungeon_specific_item_names}
if localized: if localized:
in_dungeon_items = [item for item in get_dungeon_item_pool(world) if (item.player, item.name) in localized] in_dungeon_items = [item for item in get_dungeon_item_pool(multiworld) if (item.player, item.name) in localized]
if in_dungeon_items: if in_dungeon_items:
restricted_players = {player for player, restricted in world.restrict_dungeon_item_on_boss.items() if restricted_players = {player for player, restricted in multiworld.restrict_dungeon_item_on_boss.items() if
restricted} restricted}
locations: typing.List["ALttPLocation"] = [ locations: typing.List["ALttPLocation"] = [
location for location in get_unfilled_dungeon_locations(world) location for location in get_unfilled_dungeon_locations(multiworld)
# filter boss # filter boss
if not (location.player in restricted_players and location.name in lookup_boss_drops)] if not (location.player in restricted_players and location.name in lookup_boss_drops)]
if dungeon_specific: if dungeon_specific:
@ -153,7 +216,7 @@ def fill_dungeons_restrictive(world):
location.item_rule = lambda item, dungeon=dungeon, orig_rule=orig_rule: \ location.item_rule = lambda item, dungeon=dungeon, orig_rule=orig_rule: \
(not (item.player, item.name) in dungeon_specific or item.dungeon is dungeon) and orig_rule(item) (not (item.player, item.name) in dungeon_specific or item.dungeon is dungeon) and orig_rule(item)
world.random.shuffle(locations) multiworld.random.shuffle(locations)
# Dungeon-locked items have to be placed first, to not run out of spaces for dungeon-locked items # Dungeon-locked items have to be placed first, to not run out of spaces for dungeon-locked items
# subsort in the order Big Key, Small Key, Other before placing dungeon items # subsort in the order Big Key, Small Key, Other before placing dungeon items
@ -162,14 +225,15 @@ def fill_dungeons_restrictive(world):
key=lambda item: sort_order.get(item.type, 1) + key=lambda item: sort_order.get(item.type, 1) +
(5 if (item.player, item.name) in dungeon_specific else 0)) (5 if (item.player, item.name) in dungeon_specific else 0))
# Construct a partial all_state which contains only the items from get_pre_fill_items which aren't in_dungeon # Construct a partial all_state which contains only the items from get_pre_fill_items,
# which aren't in_dungeon
in_dungeon_player_ids = {item.player for item in in_dungeon_items} in_dungeon_player_ids = {item.player for item in in_dungeon_items}
all_state_base = CollectionState(world) all_state_base = CollectionState(multiworld)
for item in world.itempool: for item in multiworld.itempool:
world.worlds[item.player].collect(all_state_base, item) multiworld.worlds[item.player].collect(all_state_base, item)
pre_fill_items = [] pre_fill_items = []
for player in in_dungeon_player_ids: for player in in_dungeon_player_ids:
pre_fill_items += world.worlds[player].get_pre_fill_items() pre_fill_items += multiworld.worlds[player].get_pre_fill_items()
for item in in_dungeon_items: for item in in_dungeon_items:
try: try:
pre_fill_items.remove(item) pre_fill_items.remove(item)
@ -177,16 +241,15 @@ def fill_dungeons_restrictive(world):
# pre_fill_items should be a subset of in_dungeon_items, but just in case # pre_fill_items should be a subset of in_dungeon_items, but just in case
pass pass
for item in pre_fill_items: for item in pre_fill_items:
world.worlds[item.player].collect(all_state_base, item) multiworld.worlds[item.player].collect(all_state_base, item)
all_state_base.sweep_for_events() all_state_base.sweep_for_events()
# Remove completion condition so that minimal-accessibility worlds place keys properly # Remove completion condition so that minimal-accessibility worlds place keys properly
for player in {item.player for item in in_dungeon_items}: for player in {item.player for item in in_dungeon_items}:
if all_state_base.has("Triforce", player): if all_state_base.has("Triforce", player):
all_state_base.remove(world.worlds[player].create_item("Triforce")) all_state_base.remove(multiworld.worlds[player].create_item("Triforce"))
fill_restrictive(world, all_state_base, locations, in_dungeon_items, True, True, allow_excluded=True) fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True, allow_excluded=True)
dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A], dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A],
@ -200,3 +263,4 @@ dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A],
'Ice Palace - Prize': [0x155BF], 'Ice Palace - Prize': [0x155BF],
'Misery Mire - Prize': [0x155B9], 'Misery Mire - Prize': [0x155B9],
'Turtle Rock - Prize': [0x155C7, 0x155A7, 0x155AA, 0x155AB]} 'Turtle Rock - Prize': [0x155C7, 0x155A7, 0x155AA, 0x155AB]}

View File

@ -226,40 +226,40 @@ for diff in {'easy', 'normal', 'hard', 'expert'}:
def generate_itempool(world): def generate_itempool(world):
player = world.player player = world.player
world = world.multiworld multiworld = world.multiworld
if world.difficulty[player] not in difficulties: if multiworld.difficulty[player] not in difficulties:
raise NotImplementedError(f"Diffulty {world.difficulty[player]}") raise NotImplementedError(f"Diffulty {multiworld.difficulty[player]}")
if world.goal[player] not in {'ganon', 'pedestal', 'bosses', 'triforcehunt', 'localtriforcehunt', 'icerodhunt', if multiworld.goal[player] not in {'ganon', 'pedestal', 'bosses', 'triforcehunt', 'localtriforcehunt', 'icerodhunt',
'ganontriforcehunt', 'localganontriforcehunt', 'crystals', 'ganonpedestal'}: 'ganontriforcehunt', 'localganontriforcehunt', 'crystals', 'ganonpedestal'}:
raise NotImplementedError(f"Goal {world.goal[player]} for player {player}") raise NotImplementedError(f"Goal {multiworld.goal[player]} for player {player}")
if world.mode[player] not in {'open', 'standard', 'inverted'}: if multiworld.mode[player] not in {'open', 'standard', 'inverted'}:
raise NotImplementedError(f"Mode {world.mode[player]} for player {player}") raise NotImplementedError(f"Mode {multiworld.mode[player]} for player {player}")
if world.timer[player] not in {False, 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'}: if multiworld.timer[player] not in {False, 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'}:
raise NotImplementedError(f"Timer {world.mode[player]} for player {player}") raise NotImplementedError(f"Timer {multiworld.mode[player]} for player {player}")
if world.timer[player] in ['ohko', 'timed-ohko']: if multiworld.timer[player] in ['ohko', 'timed-ohko']:
world.can_take_damage[player] = False multiworld.can_take_damage[player] = False
if world.goal[player] in ['pedestal', 'triforcehunt', 'localtriforcehunt', 'icerodhunt']: if multiworld.goal[player] in ['pedestal', 'triforcehunt', 'localtriforcehunt', 'icerodhunt']:
world.push_item(world.get_location('Ganon', player), ItemFactory('Nothing', player), False) multiworld.push_item(multiworld.get_location('Ganon', player), ItemFactory('Nothing', player), False)
else: else:
world.push_item(world.get_location('Ganon', player), ItemFactory('Triforce', player), False) multiworld.push_item(multiworld.get_location('Ganon', player), ItemFactory('Triforce', player), False)
if world.goal[player] == 'icerodhunt': if multiworld.goal[player] == 'icerodhunt':
world.progression_balancing[player].value = 0 multiworld.progression_balancing[player].value = 0
loc = world.get_location('Turtle Rock - Boss', player) loc = multiworld.get_location('Turtle Rock - Boss', player)
world.push_item(loc, ItemFactory('Triforce Piece', player), False) multiworld.push_item(loc, ItemFactory('Triforce Piece', player), False)
world.treasure_hunt_count[player] = 1 multiworld.treasure_hunt_count[player] = 1
if world.boss_shuffle[player] != 'none': if multiworld.boss_shuffle[player] != 'none':
if isinstance(world.boss_shuffle[player].value, str) and 'turtle rock-' not in world.boss_shuffle[player].value: if isinstance(multiworld.boss_shuffle[player].value, str) and 'turtle rock-' not in multiworld.boss_shuffle[player].value:
world.boss_shuffle[player] = LTTPBosses.from_text(f'Turtle Rock-Trinexx;{world.boss_shuffle[player].current_key}') multiworld.boss_shuffle[player] = LTTPBosses.from_text(f'Turtle Rock-Trinexx;{multiworld.boss_shuffle[player].current_key}')
elif isinstance(world.boss_shuffle[player].value, int): elif isinstance(multiworld.boss_shuffle[player].value, int):
world.boss_shuffle[player] = LTTPBosses.from_text(f'Turtle Rock-Trinexx;{world.boss_shuffle[player].current_key}') multiworld.boss_shuffle[player] = LTTPBosses.from_text(f'Turtle Rock-Trinexx;{multiworld.boss_shuffle[player].current_key}')
else: else:
logging.warning(f'Cannot guarantee that Trinexx is the boss of Turtle Rock for player {player}') logging.warning(f'Cannot guarantee that Trinexx is the boss of Turtle Rock for player {player}')
loc.event = True loc.event = True
loc.locked = True loc.locked = True
itemdiff = difficulties[world.difficulty[player]] itemdiff = difficulties[multiworld.difficulty[player]]
itempool = [] itempool = []
itempool.extend(itemdiff.alwaysitems) itempool.extend(itemdiff.alwaysitems)
itempool.remove('Ice Rod') itempool.remove('Ice Rod')
@ -270,7 +270,7 @@ def generate_itempool(world):
itempool.extend(itemdiff.bottles) itempool.extend(itemdiff.bottles)
itempool.extend(itemdiff.basicbow) itempool.extend(itemdiff.basicbow)
itempool.extend(itemdiff.basicarmor) itempool.extend(itemdiff.basicarmor)
if not world.swordless[player]: if not multiworld.swordless[player]:
itempool.extend(itemdiff.basicsword) itempool.extend(itemdiff.basicsword)
itempool.extend(itemdiff.basicmagic) itempool.extend(itemdiff.basicmagic)
itempool.extend(itemdiff.basicglove) itempool.extend(itemdiff.basicglove)
@ -279,28 +279,28 @@ def generate_itempool(world):
itempool.extend(['Rupees (300)'] * 34) itempool.extend(['Rupees (300)'] * 34)
itempool.extend(['Bombs (10)'] * 5) itempool.extend(['Bombs (10)'] * 5)
itempool.extend(['Arrows (10)'] * 7) itempool.extend(['Arrows (10)'] * 7)
if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal: if multiworld.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
itempool.extend(itemdiff.universal_keys) itempool.extend(itemdiff.universal_keys)
itempool.append('Small Key (Universal)') itempool.append('Small Key (Universal)')
for item in itempool: for item in itempool:
world.push_precollected(ItemFactory(item, player)) multiworld.push_precollected(ItemFactory(item, player))
if world.goal[player] in ['triforcehunt', 'localtriforcehunt', 'icerodhunt']: if multiworld.goal[player] in ['triforcehunt', 'localtriforcehunt', 'icerodhunt']:
region = world.get_region('Light World', player) region = multiworld.get_region('Light World', player)
loc = ALttPLocation(player, "Murahdahla", parent=region) loc = ALttPLocation(player, "Murahdahla", parent=region)
loc.access_rule = lambda state: has_triforce_pieces(state, player) loc.access_rule = lambda state: has_triforce_pieces(state, player)
region.locations.append(loc) region.locations.append(loc)
world.clear_location_cache() multiworld.clear_location_cache()
world.push_item(loc, ItemFactory('Triforce', player), False) multiworld.push_item(loc, ItemFactory('Triforce', player), False)
loc.event = True loc.event = True
loc.locked = True loc.locked = True
world.get_location('Ganon', player).event = True multiworld.get_location('Ganon', player).event = True
world.get_location('Ganon', player).locked = True multiworld.get_location('Ganon', player).locked = True
event_pairs = [ event_pairs = [
('Agahnim 1', 'Beat Agahnim 1'), ('Agahnim 1', 'Beat Agahnim 1'),
('Agahnim 2', 'Beat Agahnim 2'), ('Agahnim 2', 'Beat Agahnim 2'),
@ -312,26 +312,26 @@ def generate_itempool(world):
('Flute Activation Spot', 'Activated Flute') ('Flute Activation Spot', 'Activated Flute')
] ]
for location_name, event_name in event_pairs: for location_name, event_name in event_pairs:
location = world.get_location(location_name, player) location = multiworld.get_location(location_name, player)
event = ItemFactory(event_name, player) event = ItemFactory(event_name, player)
world.push_item(location, event, False) multiworld.push_item(location, event, False)
location.event = location.locked = True location.event = location.locked = True
# set up item pool # set up item pool
additional_triforce_pieces = 0 additional_triforce_pieces = 0
if world.custom: if multiworld.custom:
(pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count,
treasure_hunt_icon) = make_custom_item_pool(world, player) treasure_hunt_icon) = make_custom_item_pool(multiworld, player)
world.rupoor_cost = min(world.customitemarray[67], 9999) multiworld.rupoor_cost = min(multiworld.customitemarray[67], 9999)
else: else:
pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, \ pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, \
treasure_hunt_icon, additional_triforce_pieces = get_pool_core(world, player) treasure_hunt_icon, additional_triforce_pieces = get_pool_core(multiworld, player)
for item in precollected_items: for item in precollected_items:
world.push_precollected(ItemFactory(item, player)) multiworld.push_precollected(ItemFactory(item, player))
if world.mode[player] == 'standard' and not has_melee_weapon(world.state, player): if multiworld.mode[player] == 'standard' and not has_melee_weapon(multiworld.state, player):
if "Link's Uncle" not in placed_items: if "Link's Uncle" not in placed_items:
found_sword = False found_sword = False
found_bow = False found_bow = False
@ -347,60 +347,60 @@ def generate_itempool(world):
if item in ['Hammer', 'Bombs (10)', 'Fire Rod', 'Cane of Somaria', 'Cane of Byrna']: if item in ['Hammer', 'Bombs (10)', 'Fire Rod', 'Cane of Somaria', 'Cane of Byrna']:
if item not in possible_weapons: if item not in possible_weapons:
possible_weapons.append(item) possible_weapons.append(item)
starting_weapon = world.random.choice(possible_weapons) starting_weapon = multiworld.random.choice(possible_weapons)
placed_items["Link's Uncle"] = starting_weapon placed_items["Link's Uncle"] = starting_weapon
pool.remove(starting_weapon) pool.remove(starting_weapon)
if placed_items["Link's Uncle"] in ['Bow', 'Progressive Bow', 'Bombs (10)', 'Cane of Somaria', 'Cane of Byrna'] and world.enemy_health[player] not in ['default', 'easy']: if placed_items["Link's Uncle"] in ['Bow', 'Progressive Bow', 'Bombs (10)', 'Cane of Somaria', 'Cane of Byrna'] and multiworld.enemy_health[player] not in ['default', 'easy']:
world.escape_assist[player].append('bombs') multiworld.escape_assist[player].append('bombs')
for (location, item) in placed_items.items(): for (location, item) in placed_items.items():
world.get_location(location, player).place_locked_item(ItemFactory(item, player)) multiworld.get_location(location, player).place_locked_item(ItemFactory(item, player))
items = ItemFactory(pool, player) items = ItemFactory(pool, player)
# convert one Progressive Bow into Progressive Bow (Alt), in ID only, for ganon silvers hint text # convert one Progressive Bow into Progressive Bow (Alt), in ID only, for ganon silvers hint text
if world.worlds[player].has_progressive_bows: if multiworld.worlds[player].has_progressive_bows:
for item in items: for item in items:
if item.code == 0x64: # Progressive Bow if item.code == 0x64: # Progressive Bow
item.code = 0x65 # Progressive Bow (Alt) item.code = 0x65 # Progressive Bow (Alt)
break break
if clock_mode is not None: if clock_mode is not None:
world.clock_mode[player] = clock_mode multiworld.clock_mode[player] = clock_mode
if treasure_hunt_count is not None: if treasure_hunt_count is not None:
world.treasure_hunt_count[player] = treasure_hunt_count % 999 multiworld.treasure_hunt_count[player] = treasure_hunt_count % 999
if treasure_hunt_icon is not None: if treasure_hunt_icon is not None:
world.treasure_hunt_icon[player] = treasure_hunt_icon multiworld.treasure_hunt_icon[player] = treasure_hunt_icon
dungeon_items = [item for item in get_dungeon_item_pool_player(world, player) dungeon_items = [item for item in get_dungeon_item_pool_player(world)
if item.name not in world.worlds[player].dungeon_local_item_names] if item.name not in multiworld.worlds[player].dungeon_local_item_names]
dungeon_item_replacements = difficulties[world.difficulty[player]].extras[0]\ dungeon_item_replacements = difficulties[multiworld.difficulty[player]].extras[0]\
+ difficulties[world.difficulty[player]].extras[1]\ + difficulties[multiworld.difficulty[player]].extras[1]\
+ difficulties[world.difficulty[player]].extras[2]\ + difficulties[multiworld.difficulty[player]].extras[2]\
+ difficulties[world.difficulty[player]].extras[3]\ + difficulties[multiworld.difficulty[player]].extras[3]\
+ difficulties[world.difficulty[player]].extras[4] + difficulties[multiworld.difficulty[player]].extras[4]
world.random.shuffle(dungeon_item_replacements) multiworld.random.shuffle(dungeon_item_replacements)
if world.goal[player] == 'icerodhunt': if multiworld.goal[player] == 'icerodhunt':
for item in dungeon_items: for item in dungeon_items:
world.itempool.append(ItemFactory(GetBeemizerItem(world, player, 'Nothing'), player)) multiworld.itempool.append(ItemFactory(GetBeemizerItem(multiworld, player, 'Nothing'), player))
world.push_precollected(item) multiworld.push_precollected(item)
else: else:
for x in range(len(dungeon_items)-1, -1, -1): for x in range(len(dungeon_items)-1, -1, -1):
item = dungeon_items[x] item = dungeon_items[x]
if ((world.smallkey_shuffle[player] == smallkey_shuffle.option_start_with and item.type == 'SmallKey') if ((multiworld.smallkey_shuffle[player] == smallkey_shuffle.option_start_with and item.type == 'SmallKey')
or (world.bigkey_shuffle[player] == bigkey_shuffle.option_start_with and item.type == 'BigKey') or (multiworld.bigkey_shuffle[player] == bigkey_shuffle.option_start_with and item.type == 'BigKey')
or (world.compass_shuffle[player] == compass_shuffle.option_start_with and item.type == 'Compass') or (multiworld.compass_shuffle[player] == compass_shuffle.option_start_with and item.type == 'Compass')
or (world.map_shuffle[player] == map_shuffle.option_start_with and item.type == 'Map')): or (multiworld.map_shuffle[player] == map_shuffle.option_start_with and item.type == 'Map')):
dungeon_items.remove(item) dungeon_items.remove(item)
world.push_precollected(item) multiworld.push_precollected(item)
world.itempool.append(ItemFactory(dungeon_item_replacements.pop(), player)) multiworld.itempool.append(ItemFactory(dungeon_item_replacements.pop(), player))
world.itempool.extend([item for item in dungeon_items]) multiworld.itempool.extend([item for item in dungeon_items])
# logic has some branches where having 4 hearts is one possible requirement (of several alternatives) # logic has some branches where having 4 hearts is one possible requirement (of several alternatives)
# rather than making all hearts/heart pieces progression items (which slows down generation considerably) # rather than making all hearts/heart pieces progression items (which slows down generation considerably)
# We mark one random heart container as an advancement item (or 4 heart pieces in expert mode) # We mark one random heart container as an advancement item (or 4 heart pieces in expert mode)
if world.goal[player] != 'icerodhunt' and world.difficulty[player] in ['easy', 'normal', 'hard'] and not (world.custom and world.customitemarray[30] == 0): if multiworld.goal[player] != 'icerodhunt' and multiworld.difficulty[player] in ['easy', 'normal', 'hard'] and not (multiworld.custom and multiworld.customitemarray[30] == 0):
next(item for item in items if item.name == 'Boss Heart Container').classification = ItemClassification.progression next(item for item in items if item.name == 'Boss Heart Container').classification = ItemClassification.progression
elif world.goal[player] != 'icerodhunt' and world.difficulty[player] in ['expert'] and not (world.custom and world.customitemarray[29] < 4): elif multiworld.goal[player] != 'icerodhunt' and multiworld.difficulty[player] in ['expert'] and not (multiworld.custom and multiworld.customitemarray[29] < 4):
adv_heart_pieces = (item for item in items if item.name == 'Piece of Heart') adv_heart_pieces = (item for item in items if item.name == 'Piece of Heart')
for i in range(4): for i in range(4):
next(adv_heart_pieces).classification = ItemClassification.progression next(adv_heart_pieces).classification = ItemClassification.progression
@ -412,41 +412,41 @@ def generate_itempool(world):
if item.advancement or item.type: if item.advancement or item.type:
progressionitems.append(item) progressionitems.append(item)
else: else:
nonprogressionitems.append(GetBeemizerItem(world, item.player, item)) nonprogressionitems.append(GetBeemizerItem(multiworld, item.player, item))
world.random.shuffle(nonprogressionitems) multiworld.random.shuffle(nonprogressionitems)
if additional_triforce_pieces: if additional_triforce_pieces:
if additional_triforce_pieces > len(nonprogressionitems): if additional_triforce_pieces > len(nonprogressionitems):
raise FillError(f"Not enough non-progression items to replace with Triforce pieces found for player " raise FillError(f"Not enough non-progression items to replace with Triforce pieces found for player "
f"{world.get_player_name(player)}.") f"{multiworld.get_player_name(player)}.")
progressionitems += [ItemFactory("Triforce Piece", player) for _ in range(additional_triforce_pieces)] progressionitems += [ItemFactory("Triforce Piece", player) for _ in range(additional_triforce_pieces)]
nonprogressionitems.sort(key=lambda item: int("Heart" in item.name)) # try to keep hearts in the pool nonprogressionitems.sort(key=lambda item: int("Heart" in item.name)) # try to keep hearts in the pool
nonprogressionitems = nonprogressionitems[additional_triforce_pieces:] nonprogressionitems = nonprogressionitems[additional_triforce_pieces:]
world.random.shuffle(nonprogressionitems) multiworld.random.shuffle(nonprogressionitems)
# shuffle medallions # shuffle medallions
if world.required_medallions[player][0] == "random": if multiworld.required_medallions[player][0] == "random":
mm_medallion = world.random.choice(['Ether', 'Quake', 'Bombos']) mm_medallion = multiworld.random.choice(['Ether', 'Quake', 'Bombos'])
else: else:
mm_medallion = world.required_medallions[player][0] mm_medallion = multiworld.required_medallions[player][0]
if world.required_medallions[player][1] == "random": if multiworld.required_medallions[player][1] == "random":
tr_medallion = world.random.choice(['Ether', 'Quake', 'Bombos']) tr_medallion = multiworld.random.choice(['Ether', 'Quake', 'Bombos'])
else: else:
tr_medallion = world.required_medallions[player][1] tr_medallion = multiworld.required_medallions[player][1]
world.required_medallions[player] = (mm_medallion, tr_medallion) multiworld.required_medallions[player] = (mm_medallion, tr_medallion)
place_bosses(world, player) place_bosses(world)
set_up_shops(world, player) set_up_shops(multiworld, player)
if world.shop_shuffle[player]: if multiworld.shop_shuffle[player]:
shuffle_shops(world, nonprogressionitems, player) shuffle_shops(multiworld, nonprogressionitems, player)
world.itempool += progressionitems + nonprogressionitems multiworld.itempool += progressionitems + nonprogressionitems
if world.retro_caves[player]: if multiworld.retro_caves[player]:
set_up_take_anys(world, player) # depends on world.itempool to be set set_up_take_anys(multiworld, player) # depends on world.itempool to be set
# set_up_take_anys needs to run first # set_up_take_anys needs to run first
create_dynamic_shop_locations(world, player) create_dynamic_shop_locations(multiworld, player)
take_any_locations = { take_any_locations = {

View File

@ -279,7 +279,9 @@ def apply_random_sprite_on_event(rom: LocalRom, sprite, local_random, allow_rand
rom.write_bytes(0x307078 + (i * 0x8000), sprite.glove_palette) rom.write_bytes(0x307078 + (i * 0x8000), sprite.glove_palette)
def patch_enemizer(world, player: int, rom: LocalRom, enemizercli, output_directory): def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
player = world.player
multiworld = world.multiworld
check_enemizer(enemizercli) check_enemizer(enemizercli)
randopatch_path = os.path.abspath(os.path.join(output_directory, f'enemizer_randopatch_{player}.sfc')) randopatch_path = os.path.abspath(os.path.join(output_directory, f'enemizer_randopatch_{player}.sfc'))
options_path = os.path.abspath(os.path.join(output_directory, f'enemizer_options_{player}.json')) options_path = os.path.abspath(os.path.join(output_directory, f'enemizer_options_{player}.json'))
@ -287,18 +289,18 @@ def patch_enemizer(world, player: int, rom: LocalRom, enemizercli, output_direct
# write options file for enemizer # write options file for enemizer
options = { options = {
'RandomizeEnemies': world.enemy_shuffle[player].value, 'RandomizeEnemies': multiworld.enemy_shuffle[player].value,
'RandomizeEnemiesType': 3, 'RandomizeEnemiesType': 3,
'RandomizeBushEnemyChance': world.bush_shuffle[player].value, 'RandomizeBushEnemyChance': multiworld.bush_shuffle[player].value,
'RandomizeEnemyHealthRange': world.enemy_health[player] != 'default', 'RandomizeEnemyHealthRange': multiworld.enemy_health[player] != 'default',
'RandomizeEnemyHealthType': {'default': 0, 'easy': 0, 'normal': 1, 'hard': 2, 'expert': 3}[ 'RandomizeEnemyHealthType': {'default': 0, 'easy': 0, 'normal': 1, 'hard': 2, 'expert': 3}[
world.enemy_health[player]], multiworld.enemy_health[player]],
'OHKO': False, 'OHKO': False,
'RandomizeEnemyDamage': world.enemy_damage[player] != 'default', 'RandomizeEnemyDamage': multiworld.enemy_damage[player] != 'default',
'AllowEnemyZeroDamage': True, 'AllowEnemyZeroDamage': True,
'ShuffleEnemyDamageGroups': world.enemy_damage[player] != 'default', 'ShuffleEnemyDamageGroups': multiworld.enemy_damage[player] != 'default',
'EnemyDamageChaosMode': world.enemy_damage[player] == 'chaos', 'EnemyDamageChaosMode': multiworld.enemy_damage[player] == 'chaos',
'EasyModeEscape': world.mode[player] == "standard", 'EasyModeEscape': multiworld.mode[player] == "standard",
'EnemiesAbsorbable': False, 'EnemiesAbsorbable': False,
'AbsorbableSpawnRate': 10, 'AbsorbableSpawnRate': 10,
'AbsorbableTypes': { 'AbsorbableTypes': {
@ -327,7 +329,7 @@ def patch_enemizer(world, player: int, rom: LocalRom, enemizercli, output_direct
'GrayscaleMode': False, 'GrayscaleMode': False,
'GenerateSpoilers': False, 'GenerateSpoilers': False,
'RandomizeLinkSpritePalette': False, 'RandomizeLinkSpritePalette': False,
'RandomizePots': world.pot_shuffle[player].value, 'RandomizePots': multiworld.pot_shuffle[player].value,
'ShuffleMusic': False, 'ShuffleMusic': False,
'BootlegMagic': True, 'BootlegMagic': True,
'CustomBosses': False, 'CustomBosses': False,
@ -340,7 +342,7 @@ def patch_enemizer(world, player: int, rom: LocalRom, enemizercli, output_direct
'BeesLevel': 0, 'BeesLevel': 0,
'RandomizeTileTrapPattern': False, 'RandomizeTileTrapPattern': False,
'RandomizeTileTrapFloorTile': False, 'RandomizeTileTrapFloorTile': False,
'AllowKillableThief': world.killable_thieves[player].value, 'AllowKillableThief': multiworld.killable_thieves[player].value,
'RandomizeSpriteOnHit': False, 'RandomizeSpriteOnHit': False,
'DebugMode': False, 'DebugMode': False,
'DebugForceEnemy': False, 'DebugForceEnemy': False,
@ -352,26 +354,26 @@ def patch_enemizer(world, player: int, rom: LocalRom, enemizercli, output_direct
'DebugShowRoomIdInRupeeCounter': False, 'DebugShowRoomIdInRupeeCounter': False,
'UseManualBosses': True, 'UseManualBosses': True,
'ManualBosses': { 'ManualBosses': {
'EasternPalace': world.get_dungeon("Eastern Palace", player).boss.enemizer_name, 'EasternPalace': world.dungeons["Eastern Palace"].boss.enemizer_name,
'DesertPalace': world.get_dungeon("Desert Palace", player).boss.enemizer_name, 'DesertPalace': world.dungeons["Desert Palace"].boss.enemizer_name,
'TowerOfHera': world.get_dungeon("Tower of Hera", player).boss.enemizer_name, 'TowerOfHera': world.dungeons["Tower of Hera"].boss.enemizer_name,
'AgahnimsTower': 'Agahnim', 'AgahnimsTower': 'Agahnim',
'PalaceOfDarkness': world.get_dungeon("Palace of Darkness", player).boss.enemizer_name, 'PalaceOfDarkness': world.dungeons["Palace of Darkness"].boss.enemizer_name,
'SwampPalace': world.get_dungeon("Swamp Palace", player).boss.enemizer_name, 'SwampPalace': world.dungeons["Swamp Palace"].boss.enemizer_name,
'SkullWoods': world.get_dungeon("Skull Woods", player).boss.enemizer_name, 'SkullWoods': world.dungeons["Skull Woods"].boss.enemizer_name,
'ThievesTown': world.get_dungeon("Thieves Town", player).boss.enemizer_name, 'ThievesTown': world.dungeons["Thieves Town"].boss.enemizer_name,
'IcePalace': world.get_dungeon("Ice Palace", player).boss.enemizer_name, 'IcePalace': world.dungeons["Ice Palace"].boss.enemizer_name,
'MiseryMire': world.get_dungeon("Misery Mire", player).boss.enemizer_name, 'MiseryMire': world.dungeons["Misery Mire"].boss.enemizer_name,
'TurtleRock': world.get_dungeon("Turtle Rock", player).boss.enemizer_name, 'TurtleRock': world.dungeons["Turtle Rock"].boss.enemizer_name,
'GanonsTower1': 'GanonsTower1':
world.get_dungeon('Ganons Tower' if world.mode[player] != 'inverted' else 'Inverted Ganons Tower', world.dungeons["Ganons Tower" if multiworld.mode[player] != 'inverted' else
player).bosses['bottom'].enemizer_name, "Inverted Ganons Tower"].bosses['bottom'].enemizer_name,
'GanonsTower2': 'GanonsTower2':
world.get_dungeon('Ganons Tower' if world.mode[player] != 'inverted' else 'Inverted Ganons Tower', world.dungeons["Ganons Tower" if multiworld.mode[player] != 'inverted' else
player).bosses['middle'].enemizer_name, "Inverted Ganons Tower"].bosses['middle'].enemizer_name,
'GanonsTower3': 'GanonsTower3':
world.get_dungeon('Ganons Tower' if world.mode[player] != 'inverted' else 'Inverted Ganons Tower', world.dungeons["Ganons Tower" if multiworld.mode[player] != 'inverted' else
player).bosses['top'].enemizer_name, "Inverted Ganons Tower"].bosses['top'].enemizer_name,
'GanonsTower4': 'Agahnim2', 'GanonsTower4': 'Agahnim2',
'Ganon': 'Ganon', 'Ganon': 'Ganon',
} }
@ -384,7 +386,7 @@ def patch_enemizer(world, player: int, rom: LocalRom, enemizercli, output_direct
max_enemizer_tries = 5 max_enemizer_tries = 5
for i in range(max_enemizer_tries): for i in range(max_enemizer_tries):
enemizer_seed = str(world.per_slot_randoms[player].randint(0, 999999999)) enemizer_seed = str(multiworld.per_slot_randoms[player].randint(0, 999999999))
enemizer_command = [os.path.abspath(enemizercli), enemizer_command = [os.path.abspath(enemizercli),
'--rom', randopatch_path, '--rom', randopatch_path,
'--seed', enemizer_seed, '--seed', enemizer_seed,
@ -414,7 +416,7 @@ def patch_enemizer(world, player: int, rom: LocalRom, enemizercli, output_direct
continue continue
for j in range(i + 1, max_enemizer_tries): for j in range(i + 1, max_enemizer_tries):
world.per_slot_randoms[player].randint(0, 999999999) multiworld.per_slot_randoms[player].randint(0, 999999999)
# Sacrifice all remaining random numbers that would have been used for unused enemizer tries. # Sacrifice all remaining random numbers that would have been used for unused enemizer tries.
# This allows for future enemizer bug fixes to NOT affect the rest of the seed's randomness # This allows for future enemizer bug fixes to NOT affect the rest of the seed's randomness
break break
@ -422,7 +424,7 @@ def patch_enemizer(world, player: int, rom: LocalRom, enemizercli, output_direct
rom.read_from_file(enemizer_output_path) rom.read_from_file(enemizer_output_path)
os.remove(enemizer_output_path) os.remove(enemizer_output_path)
if world.get_dungeon("Thieves Town", player).boss.enemizer_name == "Blind": if world.dungeons["Thieves Town"].boss.enemizer_name == "Blind":
rom.write_byte(0x04DE81, 6) rom.write_byte(0x04DE81, 6)
rom.write_byte(0x1B0101, 0) # Do not close boss room door on entry. rom.write_byte(0x1B0101, 0) # Do not close boss room door on entry.

View File

@ -1,9 +1,13 @@
"""Module extending BaseClasses.py for aLttP""" """Module extending BaseClasses.py for aLttP"""
from typing import Optional from typing import Optional, TYPE_CHECKING
from enum import IntEnum from enum import IntEnum
from BaseClasses import Location, Item, ItemClassification, Region, MultiWorld from BaseClasses import Location, Item, ItemClassification, Region, MultiWorld
if TYPE_CHECKING:
from .Dungeons import Dungeon
from .Regions import LTTPRegion
class ALttPLocation(Location): class ALttPLocation(Location):
game: str = "A Link to the Past" game: str = "A Link to the Past"
@ -13,6 +17,7 @@ class ALttPLocation(Location):
shop_slot: Optional[int] = None shop_slot: Optional[int] = None
"""If given as integer, shop_slot is the shop's inventory index.""" """If given as integer, shop_slot is the shop's inventory index."""
shop_slot_disabled: bool = False shop_slot_disabled: bool = False
parent_region: "LTTPRegion"
def __init__(self, player: int, name: str, address: Optional[int] = None, crystal: bool = False, def __init__(self, player: int, name: str, address: Optional[int] = None, crystal: bool = False,
hint_text: Optional[str] = None, parent=None, player_address: Optional[int] = None): hint_text: Optional[str] = None, parent=None, player_address: Optional[int] = None):
@ -86,6 +91,7 @@ class LTTPRegion(Region):
is_dark_world: bool = False is_dark_world: bool = False
shop: Optional = None shop: Optional = None
dungeon: Optional["Dungeon"] = None
def __init__(self, name: str, type_: LTTPRegionType, hint: str, player: int, multiworld: MultiWorld): def __init__(self, name: str, type_: LTTPRegionType, hint: str, player: int, multiworld: MultiWorld):
super().__init__(name, player, multiworld, hint) super().__init__(name, player, multiworld, hint)

View File

@ -6,7 +6,7 @@ import typing
import Utils import Utils
from BaseClasses import Item, CollectionState, Tutorial, MultiWorld from BaseClasses import Item, CollectionState, Tutorial, MultiWorld
from .Dungeons import create_dungeons from .Dungeons import create_dungeons, Dungeon
from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect, \ from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect, \
indirect_connections, indirect_connections_inverted, indirect_connections_not_inverted indirect_connections, indirect_connections_inverted, indirect_connections_not_inverted
from .InvertedRegions import create_inverted_regions, mark_dark_world_regions from .InvertedRegions import create_inverted_regions, mark_dark_world_regions
@ -223,11 +223,19 @@ class ALTTPWorld(World):
if os.path.isabs(Utils.get_options()["generator"]["enemizer_path"]) \ if os.path.isabs(Utils.get_options()["generator"]["enemizer_path"]) \
else Utils.local_path(Utils.get_options()["generator"]["enemizer_path"]) else Utils.local_path(Utils.get_options()["generator"]["enemizer_path"])
# custom instance vars
dungeon_local_item_names: typing.Set[str]
dungeon_specific_item_names: typing.Set[str]
rom_name_available_event: threading.Event
has_progressive_bows: bool
dungeons: typing.Dict[str, Dungeon]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.dungeon_local_item_names = set() self.dungeon_local_item_names = set()
self.dungeon_specific_item_names = set() self.dungeon_specific_item_names = set()
self.rom_name_available_event = threading.Event() self.rom_name_available_event = threading.Event()
self.has_progressive_bows = False self.has_progressive_bows = False
self.dungeons = {}
super(ALTTPWorld, self).__init__(*args, **kwargs) super(ALTTPWorld, self).__init__(*args, **kwargs)
@classmethod @classmethod
@ -290,6 +298,8 @@ class ALTTPWorld(World):
world.non_local_items[player].value -= item_name_groups['Pendants'] world.non_local_items[player].value -= item_name_groups['Pendants']
world.non_local_items[player].value -= item_name_groups['Crystals'] world.non_local_items[player].value -= item_name_groups['Crystals']
create_dungeons = create_dungeons
def create_regions(self): def create_regions(self):
player = self.player player = self.player
world = self.multiworld world = self.multiworld
@ -302,7 +312,7 @@ class ALTTPWorld(World):
else: else:
create_inverted_regions(world, player) create_inverted_regions(world, player)
create_shops(world, player) create_shops(world, player)
create_dungeons(world, player) self.create_dungeons()
if world.logic[player] not in ["noglitches", "minorglitches"] and world.shuffle[player] in \ if world.logic[player] not in ["noglitches", "minorglitches"] and world.shuffle[player] in \
{"vanilla", "dungeonssimple", "dungeonsfull", "simple", "restricted", "full"}: {"vanilla", "dungeonssimple", "dungeonsfull", "simple", "restricted", "full"}:
@ -468,50 +478,50 @@ class ALTTPWorld(World):
or world.killable_thieves[player]) or world.killable_thieves[player])
def generate_output(self, output_directory: str): def generate_output(self, output_directory: str):
world = self.multiworld multiworld = self.multiworld
player = self.player player = self.player
try: try:
use_enemizer = self.use_enemizer use_enemizer = self.use_enemizer
rom = LocalRom(get_base_rom_path()) rom = LocalRom(get_base_rom_path())
patch_rom(world, rom, player, use_enemizer) patch_rom(multiworld, rom, player, use_enemizer)
if use_enemizer: if use_enemizer:
patch_enemizer(world, player, rom, self.enemizer_path, output_directory) patch_enemizer(self, rom, self.enemizer_path, output_directory)
if world.is_race: if multiworld.is_race:
patch_race_rom(rom, world, player) patch_race_rom(rom, multiworld, player)
world.spoiler.hashes[player] = get_hash_string(rom.hash) multiworld.spoiler.hashes[player] = get_hash_string(rom.hash)
palettes_options = { palettes_options = {
'dungeon': world.uw_palettes[player], 'dungeon': multiworld.uw_palettes[player],
'overworld': world.ow_palettes[player], 'overworld': multiworld.ow_palettes[player],
'hud': world.hud_palettes[player], 'hud': multiworld.hud_palettes[player],
'sword': world.sword_palettes[player], 'sword': multiworld.sword_palettes[player],
'shield': world.shield_palettes[player], 'shield': multiworld.shield_palettes[player],
# 'link': world.link_palettes[player] # 'link': world.link_palettes[player]
} }
palettes_options = {key: option.current_key for key, option in palettes_options.items()} palettes_options = {key: option.current_key for key, option in palettes_options.items()}
apply_rom_settings(rom, world.heartbeep[player].current_key, apply_rom_settings(rom, multiworld.heartbeep[player].current_key,
world.heartcolor[player].current_key, multiworld.heartcolor[player].current_key,
world.quickswap[player], multiworld.quickswap[player],
world.menuspeed[player].current_key, multiworld.menuspeed[player].current_key,
world.music[player], multiworld.music[player],
world.sprite[player], multiworld.sprite[player],
None, None,
palettes_options, world, player, True, palettes_options, multiworld, player, True,
reduceflashing=world.reduceflashing[player] or world.is_race, reduceflashing=multiworld.reduceflashing[player] or multiworld.is_race,
triforcehud=world.triforcehud[player].current_key, triforcehud=multiworld.triforcehud[player].current_key,
deathlink=world.death_link[player], deathlink=multiworld.death_link[player],
allowcollect=world.allow_collect[player]) allowcollect=multiworld.allow_collect[player])
rompath = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.sfc") rompath = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.sfc")
rom.write_to_file(rompath) rom.write_to_file(rompath)
patch = LttPDeltaPatch(os.path.splitext(rompath)[0]+LttPDeltaPatch.patch_file_ending, player=player, patch = LttPDeltaPatch(os.path.splitext(rompath)[0]+LttPDeltaPatch.patch_file_ending, player=player,
player_name=world.player_name[player], patched_path=rompath) player_name=multiworld.player_name[player], patched_path=rompath)
patch.write() patch.write()
os.unlink(rompath) os.unlink(rompath)
self.rom_name = rom.name self.rom_name = rom.name
@ -629,35 +639,34 @@ class ALTTPWorld(World):
if self.multiworld.boss_shuffle[self.player] != "none": if self.multiworld.boss_shuffle[self.player] != "none":
def create_boss_map() -> typing.Dict: def create_boss_map() -> typing.Dict:
boss_map = { boss_map = {
"Eastern Palace": self.multiworld.get_dungeon("Eastern Palace", self.player).boss.name, "Eastern Palace": self.dungeons["Eastern Palace"].boss.name,
"Desert Palace": self.multiworld.get_dungeon("Desert Palace", self.player).boss.name, "Desert Palace": self.dungeons["Desert Palace"].boss.name,
"Tower Of Hera": self.multiworld.get_dungeon("Tower of Hera", self.player).boss.name, "Tower Of Hera": self.dungeons["Tower of Hera"].boss.name,
"Hyrule Castle": "Agahnim", "Hyrule Castle": "Agahnim",
"Palace Of Darkness": self.multiworld.get_dungeon("Palace of Darkness", "Palace Of Darkness": self.dungeons["Palace of Darkness"].boss.name,
self.player).boss.name, "Swamp Palace": self.dungeons["Swamp Palace"].boss.name,
"Swamp Palace": self.multiworld.get_dungeon("Swamp Palace", self.player).boss.name, "Skull Woods": self.dungeons["Skull Woods"].boss.name,
"Skull Woods": self.multiworld.get_dungeon("Skull Woods", self.player).boss.name, "Thieves Town": self.dungeons["Thieves Town"].boss.name,
"Thieves Town": self.multiworld.get_dungeon("Thieves Town", self.player).boss.name, "Ice Palace": self.dungeons["Ice Palace"].boss.name,
"Ice Palace": self.multiworld.get_dungeon("Ice Palace", self.player).boss.name, "Misery Mire": self.dungeons["Misery Mire"].boss.name,
"Misery Mire": self.multiworld.get_dungeon("Misery Mire", self.player).boss.name, "Turtle Rock": self.dungeons["Turtle Rock"].boss.name,
"Turtle Rock": self.multiworld.get_dungeon("Turtle Rock", self.player).boss.name,
"Ganons Tower": "Agahnim 2", "Ganons Tower": "Agahnim 2",
"Ganon": "Ganon" "Ganon": "Ganon"
} }
if self.multiworld.mode[self.player] != 'inverted': if self.multiworld.mode[self.player] != 'inverted':
boss_map.update({ boss_map.update({
"Ganons Tower Basement": "Ganons Tower Basement":
self.multiworld.get_dungeon("Ganons Tower", self.player).bosses["bottom"].name, self.dungeons["Ganons Tower"].bosses["bottom"].name,
"Ganons Tower Middle": self.multiworld.get_dungeon("Ganons Tower", self.player).bosses[ "Ganons Tower Middle": self.dungeons["Ganons Tower"].bosses[
"middle"].name, "middle"].name,
"Ganons Tower Top": self.multiworld.get_dungeon("Ganons Tower", self.player).bosses[ "Ganons Tower Top": self.dungeons["Ganons Tower"].bosses[
"top"].name "top"].name
}) })
else: else:
boss_map.update({ boss_map.update({
"Ganons Tower Basement": self.multiworld.get_dungeon("Inverted Ganons Tower", self.player).bosses["bottom"].name, "Ganons Tower Basement": self.dungeons["Inverted Ganons Tower"].bosses["bottom"].name,
"Ganons Tower Middle": self.multiworld.get_dungeon("Inverted Ganons Tower", self.player).bosses["middle"].name, "Ganons Tower Middle": self.dungeons["Inverted Ganons Tower"].bosses["middle"].name,
"Ganons Tower Top": self.multiworld.get_dungeon("Inverted Ganons Tower", self.player).bosses["top"].name "Ganons Tower Top": self.dungeons["Inverted Ganons Tower"].bosses["top"].name
}) })
return boss_map return boss_map
@ -709,11 +718,10 @@ class ALTTPWorld(World):
def get_pre_fill_items(self): def get_pre_fill_items(self):
res = [] res = []
if self.dungeon_local_item_names: if self.dungeon_local_item_names:
for (name, player), dungeon in self.multiworld.dungeons.items(): for dungeon in self.dungeons.values():
if player == self.player: for item in dungeon.all_items:
for item in dungeon.all_items: if item.name in self.dungeon_local_item_names:
if item.name in self.dungeon_local_item_names: res.append(item)
res.append(item)
return res return res

View File

@ -2,13 +2,12 @@ import unittest
from argparse import Namespace from argparse import Namespace
from BaseClasses import MultiWorld, CollectionState, ItemClassification from BaseClasses import MultiWorld, CollectionState, ItemClassification
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool from worlds.alttp.Dungeons import get_dungeon_item_pool
from worlds.alttp.EntranceShuffle import mandatory_connections, connect_simple from worlds.alttp.EntranceShuffle import mandatory_connections, connect_simple
from worlds.alttp.ItemPool import difficulties, generate_itempool from worlds.alttp.ItemPool import difficulties
from worlds.alttp.Items import ItemFactory from worlds.alttp.Items import ItemFactory
from worlds.alttp.Regions import create_regions from worlds.alttp.Regions import create_regions
from worlds.alttp.Shops import create_shops from worlds.alttp.Shops import create_shops
from worlds.alttp.Rules import set_rules
from worlds import AutoWorld from worlds import AutoWorld
@ -24,7 +23,7 @@ class TestDungeon(unittest.TestCase):
self.remove_exits = [] # Block dungeon exits self.remove_exits = [] # Block dungeon exits
self.multiworld.difficulty_requirements[1] = difficulties['normal'] self.multiworld.difficulty_requirements[1] = difficulties['normal']
create_regions(self.multiworld, 1) create_regions(self.multiworld, 1)
create_dungeons(self.multiworld, 1) self.multiworld.worlds[1].create_dungeons()
create_shops(self.multiworld, 1) create_shops(self.multiworld, 1)
for exitname, regionname in mandatory_connections: for exitname, regionname in mandatory_connections:
connect_simple(self.multiworld, exitname, regionname, 1) connect_simple(self.multiworld, exitname, regionname, 1)

View File

@ -23,7 +23,7 @@ class TestInverted(TestBase):
self.multiworld.difficulty_requirements[1] = difficulties['normal'] self.multiworld.difficulty_requirements[1] = difficulties['normal']
self.multiworld.mode[1] = "inverted" self.multiworld.mode[1] = "inverted"
create_inverted_regions(self.multiworld, 1) create_inverted_regions(self.multiworld, 1)
create_dungeons(self.multiworld, 1) self.multiworld.worlds[1].create_dungeons()
create_shops(self.multiworld, 1) create_shops(self.multiworld, 1)
link_inverted_entrances(self.multiworld, 1) link_inverted_entrances(self.multiworld, 1)
self.multiworld.worlds[1].create_items() self.multiworld.worlds[1].create_items()

View File

@ -23,7 +23,7 @@ class TestInvertedBombRules(unittest.TestCase):
self.multiworld.set_default_common_options() self.multiworld.set_default_common_options()
self.multiworld.difficulty_requirements[1] = difficulties['normal'] self.multiworld.difficulty_requirements[1] = difficulties['normal']
create_inverted_regions(self.multiworld, 1) create_inverted_regions(self.multiworld, 1)
create_dungeons(self.multiworld, 1) self.multiworld.worlds[1].create_dungeons()
#TODO: Just making sure I haven't missed an entrance. It would be good to test the rules make sense as well. #TODO: Just making sure I haven't missed an entrance. It would be good to test the rules make sense as well.
def testInvertedBombRulesAreComplete(self): def testInvertedBombRulesAreComplete(self):

View File

@ -25,7 +25,7 @@ class TestInvertedMinor(TestBase):
self.multiworld.logic[1] = "minorglitches" self.multiworld.logic[1] = "minorglitches"
self.multiworld.difficulty_requirements[1] = difficulties['normal'] self.multiworld.difficulty_requirements[1] = difficulties['normal']
create_inverted_regions(self.multiworld, 1) create_inverted_regions(self.multiworld, 1)
create_dungeons(self.multiworld, 1) self.multiworld.worlds[1].create_dungeons()
create_shops(self.multiworld, 1) create_shops(self.multiworld, 1)
link_inverted_entrances(self.multiworld, 1) link_inverted_entrances(self.multiworld, 1)
self.multiworld.worlds[1].create_items() self.multiworld.worlds[1].create_items()

View File

@ -26,7 +26,7 @@ class TestInvertedOWG(TestBase):
self.multiworld.mode[1] = "inverted" self.multiworld.mode[1] = "inverted"
self.multiworld.difficulty_requirements[1] = difficulties['normal'] self.multiworld.difficulty_requirements[1] = difficulties['normal']
create_inverted_regions(self.multiworld, 1) create_inverted_regions(self.multiworld, 1)
create_dungeons(self.multiworld, 1) self.multiworld.worlds[1].create_dungeons()
create_shops(self.multiworld, 1) create_shops(self.multiworld, 1)
link_inverted_entrances(self.multiworld, 1) link_inverted_entrances(self.multiworld, 1)
self.multiworld.worlds[1].create_items() self.multiworld.worlds[1].create_items()

View File

@ -474,7 +474,8 @@ def get_woth_hint(world, checked):
locations = world.required_locations locations = world.required_locations
locations = list(filter(lambda location: locations = list(filter(lambda location:
location.name not in checked[location.player] location.name not in checked[location.player]
and not (world.woth_dungeon >= world.hint_dist_user['dungeons_woth_limit'] and location.parent_region.dungeon) and not (world.woth_dungeon >= world.hint_dist_user['dungeons_woth_limit']
and getattr(location.parent_region, "dungeon", None))
and location.name not in world.hint_exclusions and location.name not in world.hint_exclusions
and location.name not in world.hint_type_overrides['woth'] and location.name not in world.hint_type_overrides['woth']
and location.item.name not in world.item_hint_type_overrides['woth'], and location.item.name not in world.item_hint_type_overrides['woth'],
@ -486,7 +487,7 @@ def get_woth_hint(world, checked):
location = world.hint_rng.choice(locations) location = world.hint_rng.choice(locations)
checked[location.player].add(location.name) checked[location.player].add(location.name)
if location.parent_region.dungeon: if getattr(location.parent_region, "dungeon", None):
world.woth_dungeon += 1 world.woth_dungeon += 1
location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text
else: else:
@ -570,7 +571,7 @@ def get_good_item_hint(world, checked):
checked[location.player].add(location.name) checked[location.player].add(location.name)
item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text
if location.parent_region.dungeon: if getattr(location.parent_region, "dungeon", None):
location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text
return (GossipText('#%s# hoards #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)), return (GossipText('#%s# hoards #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)),
['Green', 'Red']), location) ['Green', 'Red']), location)
@ -614,7 +615,7 @@ def get_specific_item_hint(world, checked):
checked[location.player].add(location.name) checked[location.player].add(location.name)
item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text
if location.parent_region.dungeon: if getattr(location.parent_region, "dungeon", None):
location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text
if world.hint_dist_user.get('vague_named_items', False): if world.hint_dist_user.get('vague_named_items', False):
return (GossipText('#%s# may be on the hero\'s path.' % (location_text), ['Green']), location) return (GossipText('#%s# may be on the hero\'s path.' % (location_text), ['Green']), location)