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.glitch_triforce = False
self.algorithm = 'balanced'
self.dungeons: Dict[Tuple[str, int], Dungeon] = {}
self.groups = {}
self.regions = []
self.shops = []
@ -386,12 +385,6 @@ class MultiWorld():
self._recache()
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:
cached = getattr(self, "_all_state", None)
if use_cache and cached:
@ -801,7 +794,6 @@ class Region:
entrances: List[Entrance]
exits: List[Entrance]
locations: List[Location]
dungeon: Optional[Dungeon] = None
def __init__(self, name: str, player: int, multiworld: MultiWorld, hint: Optional[str] = None):
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})'
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):
DEFAULT = 1
PRIORITY = 2

View File

@ -1,10 +1,29 @@
import logging
from typing import Optional, Union, List, Tuple, Callable, Dict
from __future__ import annotations
import logging
from typing import Optional, Union, List, Tuple, Callable, Dict, TYPE_CHECKING
from BaseClasses import Boss
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
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]:
@ -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
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]))
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]
loc = " ".join(loc[:-1])
loc = loc.title().replace("Of", "of")
place_boss(world, player, boss, loc, level)
place_boss(world, boss, loc, level)
already_placed_bosses.append(boss)
boss_locations.remove((loc, level))
else: # boss chosen with no specified locations
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
@ -224,20 +243,23 @@ for location in boss_location_table:
for boss in boss_table if not boss.startswith("Agahnim"))
def place_boss(world, player: int, boss: str, location: str, level: Optional[str]) -> None:
if location == 'Ganons Tower' and world.mode[player] == 'inverted':
def place_boss(world: "ALTTPWorld", boss: str, location: str, level: Optional[str]) -> None:
player = world.player
if location == 'Ganons Tower' and world.multiworld.mode[player] == 'inverted':
location = 'Inverted Ganons Tower'
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:
return location + (' (' + level + ')' if level else '')
def format_boss_location(location_name: str, level: str) -> str:
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
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] = []
remaining_locations: List[Tuple[str, str]] = []
# handle plando
@ -246,14 +268,14 @@ def place_bosses(world, player: int) -> None:
options = boss_shuffle.split(";")
boss_shuffle = Bosses.options[options.pop()]
# 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
return
# Most to least restrictive order
if not remaining_locations and not already_placed_bosses:
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]))
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
bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm']
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
while already_placed_bosses:
@ -275,7 +297,7 @@ def place_bosses(world, player: int) -> None:
logging.debug('Bosses chosen %s', bosses)
world.random.shuffle(bosses)
multiworld.random.shuffle(bosses)
for loc, level in remaining_locations:
for _ in range(len(bosses)):
boss = bosses.pop()
@ -288,39 +310,39 @@ def place_bosses(world, player: int) -> None:
else:
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
for loc, level in remaining_locations:
try:
boss = world.random.choice(
boss = multiworld.random.choice(
[b for b in placeable_bosses if can_place_boss(b, loc, level)])
except IndexError:
raise FillError(f'Could not place boss for location {format_boss_location(loc, level)}')
else:
place_boss(world, player, boss, loc, level)
place_boss(world, boss, loc, level)
elif boss_shuffle == Bosses.option_singularity:
primary_boss = world.random.choice(placeable_bosses)
remaining_boss_locations, _ = place_where_possible(world, player, primary_boss, remaining_locations)
primary_boss = multiworld.random.choice(placeable_bosses)
remaining_boss_locations, _ = place_where_possible(world, primary_boss, remaining_locations)
if remaining_boss_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)])
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:
raise Exception("Unfilled boss locations!")
else:
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]] = []
placed_bosses: List[str] = []
for loc, level in boss_locations:
# place that boss where it can go
if can_place_boss(boss, loc, level):
place_boss(world, player, boss, loc, level)
place_boss(world, boss, loc, level)
placed_bosses.append(boss)
else:
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 .Bosses import BossFactory
from .Bosses import BossFactory, Boss
from .Items import ItemFactory
from .Regions import lookup_boss_drops
from .Options import smallkey_shuffle
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):
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)
for item in dungeon.all_items:
item.dungeon = dungeon
dungeon.boss = BossFactory(default_boss, player) if default_boss else None
for region in dungeon.regions:
world.get_region(region, player).dungeon = dungeon
dungeon.multiworld = world
regions = []
for region_name in dungeon.regions:
region = multiworld.get_region(region_name, player)
region.dungeon = dungeon
regions.append(region)
dungeon.multiworld = multiworld
dungeon.regions = regions
return dungeon
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(['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,
ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), [])
GT = make_dungeon('Ganons Tower', 'Agahnim2',
@ -111,26 +166,34 @@ def create_dungeons(world, player):
GT.bosses['top'] = BossFactory('Moldorm', player)
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:
return [item for dungeon in world.dungeons.values() for item in dungeon.all_items]
def get_dungeon_item_pool(multiworld: MultiWorld) -> typing.List[ALttPItem]:
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:
return [item for dungeon in world.dungeons.values() if dungeon.player == player for item in dungeon.all_items]
def get_dungeon_item_pool_player(world) -> typing.List[ALttPItem]:
return [item
for dungeon in world.dungeons.values()
for item in dungeon.all_items]
def get_unfilled_dungeon_locations(multiworld) -> typing.List:
return [location for location in multiworld.get_locations() if not location.item and location.parent_region.dungeon]
def get_unfilled_dungeon_locations(multiworld: MultiWorld) -> typing.List[ALttPLocation]:
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."""
localized: 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
localized |= {(player, item_name) for item_name in
subworld.dungeon_local_item_names}
@ -138,12 +201,12 @@ def fill_dungeons_restrictive(world):
subworld.dungeon_specific_item_names}
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:
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}
locations: typing.List["ALttPLocation"] = [
location for location in get_unfilled_dungeon_locations(world)
location for location in get_unfilled_dungeon_locations(multiworld)
# filter boss
if not (location.player in restricted_players and location.name in lookup_boss_drops)]
if dungeon_specific:
@ -153,7 +216,7 @@ def fill_dungeons_restrictive(world):
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)
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
# 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) +
(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}
all_state_base = CollectionState(world)
for item in world.itempool:
world.worlds[item.player].collect(all_state_base, item)
all_state_base = CollectionState(multiworld)
for item in multiworld.itempool:
multiworld.worlds[item.player].collect(all_state_base, item)
pre_fill_items = []
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:
try:
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
pass
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()
# Remove completion condition so that minimal-accessibility worlds place keys properly
for player in {item.player for item in in_dungeon_items}:
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],
@ -200,3 +263,4 @@ dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A],
'Ice Palace - Prize': [0x155BF],
'Misery Mire - Prize': [0x155B9],
'Turtle Rock - Prize': [0x155C7, 0x155A7, 0x155AA, 0x155AB]}

View File

@ -226,40 +226,40 @@ for diff in {'easy', 'normal', 'hard', 'expert'}:
def generate_itempool(world):
player = world.player
world = world.multiworld
multiworld = world.multiworld
if world.difficulty[player] not in difficulties:
raise NotImplementedError(f"Diffulty {world.difficulty[player]}")
if world.goal[player] not in {'ganon', 'pedestal', 'bosses', 'triforcehunt', 'localtriforcehunt', 'icerodhunt',
if multiworld.difficulty[player] not in difficulties:
raise NotImplementedError(f"Diffulty {multiworld.difficulty[player]}")
if multiworld.goal[player] not in {'ganon', 'pedestal', 'bosses', 'triforcehunt', 'localtriforcehunt', 'icerodhunt',
'ganontriforcehunt', 'localganontriforcehunt', 'crystals', 'ganonpedestal'}:
raise NotImplementedError(f"Goal {world.goal[player]} for player {player}")
if world.mode[player] not in {'open', 'standard', 'inverted'}:
raise NotImplementedError(f"Mode {world.mode[player]} for player {player}")
if world.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"Goal {multiworld.goal[player]} for player {player}")
if multiworld.mode[player] not in {'open', 'standard', 'inverted'}:
raise NotImplementedError(f"Mode {multiworld.mode[player]} for player {player}")
if multiworld.timer[player] not in {False, 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'}:
raise NotImplementedError(f"Timer {multiworld.mode[player]} for player {player}")
if world.timer[player] in ['ohko', 'timed-ohko']:
world.can_take_damage[player] = False
if world.goal[player] in ['pedestal', 'triforcehunt', 'localtriforcehunt', 'icerodhunt']:
world.push_item(world.get_location('Ganon', player), ItemFactory('Nothing', player), False)
if multiworld.timer[player] in ['ohko', 'timed-ohko']:
multiworld.can_take_damage[player] = False
if multiworld.goal[player] in ['pedestal', 'triforcehunt', 'localtriforcehunt', 'icerodhunt']:
multiworld.push_item(multiworld.get_location('Ganon', player), ItemFactory('Nothing', player), False)
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':
world.progression_balancing[player].value = 0
loc = world.get_location('Turtle Rock - Boss', player)
world.push_item(loc, ItemFactory('Triforce Piece', player), False)
world.treasure_hunt_count[player] = 1
if world.boss_shuffle[player] != 'none':
if isinstance(world.boss_shuffle[player].value, str) and 'turtle rock-' not in world.boss_shuffle[player].value:
world.boss_shuffle[player] = LTTPBosses.from_text(f'Turtle Rock-Trinexx;{world.boss_shuffle[player].current_key}')
elif isinstance(world.boss_shuffle[player].value, int):
world.boss_shuffle[player] = LTTPBosses.from_text(f'Turtle Rock-Trinexx;{world.boss_shuffle[player].current_key}')
if multiworld.goal[player] == 'icerodhunt':
multiworld.progression_balancing[player].value = 0
loc = multiworld.get_location('Turtle Rock - Boss', player)
multiworld.push_item(loc, ItemFactory('Triforce Piece', player), False)
multiworld.treasure_hunt_count[player] = 1
if multiworld.boss_shuffle[player] != 'none':
if isinstance(multiworld.boss_shuffle[player].value, str) and 'turtle rock-' not in multiworld.boss_shuffle[player].value:
multiworld.boss_shuffle[player] = LTTPBosses.from_text(f'Turtle Rock-Trinexx;{multiworld.boss_shuffle[player].current_key}')
elif isinstance(multiworld.boss_shuffle[player].value, int):
multiworld.boss_shuffle[player] = LTTPBosses.from_text(f'Turtle Rock-Trinexx;{multiworld.boss_shuffle[player].current_key}')
else:
logging.warning(f'Cannot guarantee that Trinexx is the boss of Turtle Rock for player {player}')
loc.event = True
loc.locked = True
itemdiff = difficulties[world.difficulty[player]]
itemdiff = difficulties[multiworld.difficulty[player]]
itempool = []
itempool.extend(itemdiff.alwaysitems)
itempool.remove('Ice Rod')
@ -270,7 +270,7 @@ def generate_itempool(world):
itempool.extend(itemdiff.bottles)
itempool.extend(itemdiff.basicbow)
itempool.extend(itemdiff.basicarmor)
if not world.swordless[player]:
if not multiworld.swordless[player]:
itempool.extend(itemdiff.basicsword)
itempool.extend(itemdiff.basicmagic)
itempool.extend(itemdiff.basicglove)
@ -279,28 +279,28 @@ def generate_itempool(world):
itempool.extend(['Rupees (300)'] * 34)
itempool.extend(['Bombs (10)'] * 5)
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.append('Small Key (Universal)')
for item in itempool:
world.push_precollected(ItemFactory(item, player))
multiworld.push_precollected(ItemFactory(item, player))
if world.goal[player] in ['triforcehunt', 'localtriforcehunt', 'icerodhunt']:
region = world.get_region('Light World', player)
if multiworld.goal[player] in ['triforcehunt', 'localtriforcehunt', 'icerodhunt']:
region = multiworld.get_region('Light World', player)
loc = ALttPLocation(player, "Murahdahla", parent=region)
loc.access_rule = lambda state: has_triforce_pieces(state, player)
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.locked = True
world.get_location('Ganon', player).event = True
world.get_location('Ganon', player).locked = True
multiworld.get_location('Ganon', player).event = True
multiworld.get_location('Ganon', player).locked = True
event_pairs = [
('Agahnim 1', 'Beat Agahnim 1'),
('Agahnim 2', 'Beat Agahnim 2'),
@ -312,26 +312,26 @@ def generate_itempool(world):
('Flute Activation Spot', 'Activated Flute')
]
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)
world.push_item(location, event, False)
multiworld.push_item(location, event, False)
location.event = location.locked = True
# set up item pool
additional_triforce_pieces = 0
if world.custom:
if multiworld.custom:
(pool, placed_items, precollected_items, clock_mode, treasure_hunt_count,
treasure_hunt_icon) = make_custom_item_pool(world, player)
world.rupoor_cost = min(world.customitemarray[67], 9999)
treasure_hunt_icon) = make_custom_item_pool(multiworld, player)
multiworld.rupoor_cost = min(multiworld.customitemarray[67], 9999)
else:
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:
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:
found_sword = 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 not in possible_weapons:
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
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']:
world.escape_assist[player].append('bombs')
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']:
multiworld.escape_assist[player].append('bombs')
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)
# 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:
if item.code == 0x64: # Progressive Bow
item.code = 0x65 # Progressive Bow (Alt)
break
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:
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:
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)
if item.name not in world.worlds[player].dungeon_local_item_names]
dungeon_item_replacements = difficulties[world.difficulty[player]].extras[0]\
+ difficulties[world.difficulty[player]].extras[1]\
+ difficulties[world.difficulty[player]].extras[2]\
+ difficulties[world.difficulty[player]].extras[3]\
+ difficulties[world.difficulty[player]].extras[4]
world.random.shuffle(dungeon_item_replacements)
if world.goal[player] == 'icerodhunt':
dungeon_items = [item for item in get_dungeon_item_pool_player(world)
if item.name not in multiworld.worlds[player].dungeon_local_item_names]
dungeon_item_replacements = difficulties[multiworld.difficulty[player]].extras[0]\
+ difficulties[multiworld.difficulty[player]].extras[1]\
+ difficulties[multiworld.difficulty[player]].extras[2]\
+ difficulties[multiworld.difficulty[player]].extras[3]\
+ difficulties[multiworld.difficulty[player]].extras[4]
multiworld.random.shuffle(dungeon_item_replacements)
if multiworld.goal[player] == 'icerodhunt':
for item in dungeon_items:
world.itempool.append(ItemFactory(GetBeemizerItem(world, player, 'Nothing'), player))
world.push_precollected(item)
multiworld.itempool.append(ItemFactory(GetBeemizerItem(multiworld, player, 'Nothing'), player))
multiworld.push_precollected(item)
else:
for x in range(len(dungeon_items)-1, -1, -1):
item = dungeon_items[x]
if ((world.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 (world.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')):
if ((multiworld.smallkey_shuffle[player] == smallkey_shuffle.option_start_with and item.type == 'SmallKey')
or (multiworld.bigkey_shuffle[player] == bigkey_shuffle.option_start_with and item.type == 'BigKey')
or (multiworld.compass_shuffle[player] == compass_shuffle.option_start_with and item.type == 'Compass')
or (multiworld.map_shuffle[player] == map_shuffle.option_start_with and item.type == 'Map')):
dungeon_items.remove(item)
world.push_precollected(item)
world.itempool.append(ItemFactory(dungeon_item_replacements.pop(), player))
world.itempool.extend([item for item in dungeon_items])
multiworld.push_precollected(item)
multiworld.itempool.append(ItemFactory(dungeon_item_replacements.pop(), player))
multiworld.itempool.extend([item for item in dungeon_items])
# 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)
# 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
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')
for i in range(4):
next(adv_heart_pieces).classification = ItemClassification.progression
@ -412,41 +412,41 @@ def generate_itempool(world):
if item.advancement or item.type:
progressionitems.append(item)
else:
nonprogressionitems.append(GetBeemizerItem(world, item.player, item))
world.random.shuffle(nonprogressionitems)
nonprogressionitems.append(GetBeemizerItem(multiworld, item.player, item))
multiworld.random.shuffle(nonprogressionitems)
if additional_triforce_pieces:
if additional_triforce_pieces > len(nonprogressionitems):
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)]
nonprogressionitems.sort(key=lambda item: int("Heart" in item.name)) # try to keep hearts in the pool
nonprogressionitems = nonprogressionitems[additional_triforce_pieces:]
world.random.shuffle(nonprogressionitems)
multiworld.random.shuffle(nonprogressionitems)
# shuffle medallions
if world.required_medallions[player][0] == "random":
mm_medallion = world.random.choice(['Ether', 'Quake', 'Bombos'])
if multiworld.required_medallions[player][0] == "random":
mm_medallion = multiworld.random.choice(['Ether', 'Quake', 'Bombos'])
else:
mm_medallion = world.required_medallions[player][0]
if world.required_medallions[player][1] == "random":
tr_medallion = world.random.choice(['Ether', 'Quake', 'Bombos'])
mm_medallion = multiworld.required_medallions[player][0]
if multiworld.required_medallions[player][1] == "random":
tr_medallion = multiworld.random.choice(['Ether', 'Quake', 'Bombos'])
else:
tr_medallion = world.required_medallions[player][1]
world.required_medallions[player] = (mm_medallion, tr_medallion)
tr_medallion = multiworld.required_medallions[player][1]
multiworld.required_medallions[player] = (mm_medallion, tr_medallion)
place_bosses(world, player)
set_up_shops(world, player)
place_bosses(world)
set_up_shops(multiworld, player)
if world.shop_shuffle[player]:
shuffle_shops(world, nonprogressionitems, player)
if multiworld.shop_shuffle[player]:
shuffle_shops(multiworld, nonprogressionitems, player)
world.itempool += progressionitems + nonprogressionitems
multiworld.itempool += progressionitems + nonprogressionitems
if world.retro_caves[player]:
set_up_take_anys(world, player) # depends on world.itempool to be set
if multiworld.retro_caves[player]:
set_up_take_anys(multiworld, player) # depends on world.itempool to be set
# set_up_take_anys needs to run first
create_dynamic_shop_locations(world, player)
create_dynamic_shop_locations(multiworld, player)
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)
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)
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'))
@ -287,18 +289,18 @@ def patch_enemizer(world, player: int, rom: LocalRom, enemizercli, output_direct
# write options file for enemizer
options = {
'RandomizeEnemies': world.enemy_shuffle[player].value,
'RandomizeEnemies': multiworld.enemy_shuffle[player].value,
'RandomizeEnemiesType': 3,
'RandomizeBushEnemyChance': world.bush_shuffle[player].value,
'RandomizeEnemyHealthRange': world.enemy_health[player] != 'default',
'RandomizeBushEnemyChance': multiworld.bush_shuffle[player].value,
'RandomizeEnemyHealthRange': multiworld.enemy_health[player] != 'default',
'RandomizeEnemyHealthType': {'default': 0, 'easy': 0, 'normal': 1, 'hard': 2, 'expert': 3}[
world.enemy_health[player]],
multiworld.enemy_health[player]],
'OHKO': False,
'RandomizeEnemyDamage': world.enemy_damage[player] != 'default',
'RandomizeEnemyDamage': multiworld.enemy_damage[player] != 'default',
'AllowEnemyZeroDamage': True,
'ShuffleEnemyDamageGroups': world.enemy_damage[player] != 'default',
'EnemyDamageChaosMode': world.enemy_damage[player] == 'chaos',
'EasyModeEscape': world.mode[player] == "standard",
'ShuffleEnemyDamageGroups': multiworld.enemy_damage[player] != 'default',
'EnemyDamageChaosMode': multiworld.enemy_damage[player] == 'chaos',
'EasyModeEscape': multiworld.mode[player] == "standard",
'EnemiesAbsorbable': False,
'AbsorbableSpawnRate': 10,
'AbsorbableTypes': {
@ -327,7 +329,7 @@ def patch_enemizer(world, player: int, rom: LocalRom, enemizercli, output_direct
'GrayscaleMode': False,
'GenerateSpoilers': False,
'RandomizeLinkSpritePalette': False,
'RandomizePots': world.pot_shuffle[player].value,
'RandomizePots': multiworld.pot_shuffle[player].value,
'ShuffleMusic': False,
'BootlegMagic': True,
'CustomBosses': False,
@ -340,7 +342,7 @@ def patch_enemizer(world, player: int, rom: LocalRom, enemizercli, output_direct
'BeesLevel': 0,
'RandomizeTileTrapPattern': False,
'RandomizeTileTrapFloorTile': False,
'AllowKillableThief': world.killable_thieves[player].value,
'AllowKillableThief': multiworld.killable_thieves[player].value,
'RandomizeSpriteOnHit': False,
'DebugMode': False,
'DebugForceEnemy': False,
@ -352,26 +354,26 @@ def patch_enemizer(world, player: int, rom: LocalRom, enemizercli, output_direct
'DebugShowRoomIdInRupeeCounter': False,
'UseManualBosses': True,
'ManualBosses': {
'EasternPalace': world.get_dungeon("Eastern Palace", player).boss.enemizer_name,
'DesertPalace': world.get_dungeon("Desert Palace", player).boss.enemizer_name,
'TowerOfHera': world.get_dungeon("Tower of Hera", player).boss.enemizer_name,
'EasternPalace': world.dungeons["Eastern Palace"].boss.enemizer_name,
'DesertPalace': world.dungeons["Desert Palace"].boss.enemizer_name,
'TowerOfHera': world.dungeons["Tower of Hera"].boss.enemizer_name,
'AgahnimsTower': 'Agahnim',
'PalaceOfDarkness': world.get_dungeon("Palace of Darkness", player).boss.enemizer_name,
'SwampPalace': world.get_dungeon("Swamp Palace", player).boss.enemizer_name,
'SkullWoods': world.get_dungeon("Skull Woods", player).boss.enemizer_name,
'ThievesTown': world.get_dungeon("Thieves Town", player).boss.enemizer_name,
'IcePalace': world.get_dungeon("Ice Palace", player).boss.enemizer_name,
'MiseryMire': world.get_dungeon("Misery Mire", player).boss.enemizer_name,
'TurtleRock': world.get_dungeon("Turtle Rock", player).boss.enemizer_name,
'PalaceOfDarkness': world.dungeons["Palace of Darkness"].boss.enemizer_name,
'SwampPalace': world.dungeons["Swamp Palace"].boss.enemizer_name,
'SkullWoods': world.dungeons["Skull Woods"].boss.enemizer_name,
'ThievesTown': world.dungeons["Thieves Town"].boss.enemizer_name,
'IcePalace': world.dungeons["Ice Palace"].boss.enemizer_name,
'MiseryMire': world.dungeons["Misery Mire"].boss.enemizer_name,
'TurtleRock': world.dungeons["Turtle Rock"].boss.enemizer_name,
'GanonsTower1':
world.get_dungeon('Ganons Tower' if world.mode[player] != 'inverted' else 'Inverted Ganons Tower',
player).bosses['bottom'].enemizer_name,
world.dungeons["Ganons Tower" if multiworld.mode[player] != 'inverted' else
"Inverted Ganons Tower"].bosses['bottom'].enemizer_name,
'GanonsTower2':
world.get_dungeon('Ganons Tower' if world.mode[player] != 'inverted' else 'Inverted Ganons Tower',
player).bosses['middle'].enemizer_name,
world.dungeons["Ganons Tower" if multiworld.mode[player] != 'inverted' else
"Inverted Ganons Tower"].bosses['middle'].enemizer_name,
'GanonsTower3':
world.get_dungeon('Ganons Tower' if world.mode[player] != 'inverted' else 'Inverted Ganons Tower',
player).bosses['top'].enemizer_name,
world.dungeons["Ganons Tower" if multiworld.mode[player] != 'inverted' else
"Inverted Ganons Tower"].bosses['top'].enemizer_name,
'GanonsTower4': 'Agahnim2',
'Ganon': 'Ganon',
}
@ -384,7 +386,7 @@ def patch_enemizer(world, player: int, rom: LocalRom, enemizercli, output_direct
max_enemizer_tries = 5
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),
'--rom', randopatch_path,
'--seed', enemizer_seed,
@ -414,7 +416,7 @@ def patch_enemizer(world, player: int, rom: LocalRom, enemizercli, output_direct
continue
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.
# This allows for future enemizer bug fixes to NOT affect the rest of the seed's randomness
break
@ -422,7 +424,7 @@ def patch_enemizer(world, player: int, rom: LocalRom, enemizercli, output_direct
rom.read_from_file(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(0x1B0101, 0) # Do not close boss room door on entry.

View File

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

View File

@ -6,7 +6,7 @@ import typing
import Utils
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, \
indirect_connections, indirect_connections_inverted, indirect_connections_not_inverted
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"]) \
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):
self.dungeon_local_item_names = set()
self.dungeon_specific_item_names = set()
self.rom_name_available_event = threading.Event()
self.has_progressive_bows = False
self.dungeons = {}
super(ALTTPWorld, self).__init__(*args, **kwargs)
@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['Crystals']
create_dungeons = create_dungeons
def create_regions(self):
player = self.player
world = self.multiworld
@ -302,7 +312,7 @@ class ALTTPWorld(World):
else:
create_inverted_regions(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 \
{"vanilla", "dungeonssimple", "dungeonsfull", "simple", "restricted", "full"}:
@ -468,50 +478,50 @@ class ALTTPWorld(World):
or world.killable_thieves[player])
def generate_output(self, output_directory: str):
world = self.multiworld
multiworld = self.multiworld
player = self.player
try:
use_enemizer = self.use_enemizer
rom = LocalRom(get_base_rom_path())
patch_rom(world, rom, player, use_enemizer)
patch_rom(multiworld, rom, player, 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:
patch_race_rom(rom, world, player)
if multiworld.is_race:
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 = {
'dungeon': world.uw_palettes[player],
'overworld': world.ow_palettes[player],
'hud': world.hud_palettes[player],
'sword': world.sword_palettes[player],
'shield': world.shield_palettes[player],
'dungeon': multiworld.uw_palettes[player],
'overworld': multiworld.ow_palettes[player],
'hud': multiworld.hud_palettes[player],
'sword': multiworld.sword_palettes[player],
'shield': multiworld.shield_palettes[player],
# 'link': world.link_palettes[player]
}
palettes_options = {key: option.current_key for key, option in palettes_options.items()}
apply_rom_settings(rom, world.heartbeep[player].current_key,
world.heartcolor[player].current_key,
world.quickswap[player],
world.menuspeed[player].current_key,
world.music[player],
world.sprite[player],
apply_rom_settings(rom, multiworld.heartbeep[player].current_key,
multiworld.heartcolor[player].current_key,
multiworld.quickswap[player],
multiworld.menuspeed[player].current_key,
multiworld.music[player],
multiworld.sprite[player],
None,
palettes_options, world, player, True,
reduceflashing=world.reduceflashing[player] or world.is_race,
triforcehud=world.triforcehud[player].current_key,
deathlink=world.death_link[player],
allowcollect=world.allow_collect[player])
palettes_options, multiworld, player, True,
reduceflashing=multiworld.reduceflashing[player] or multiworld.is_race,
triforcehud=multiworld.triforcehud[player].current_key,
deathlink=multiworld.death_link[player],
allowcollect=multiworld.allow_collect[player])
rompath = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.sfc")
rom.write_to_file(rompath)
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()
os.unlink(rompath)
self.rom_name = rom.name
@ -629,35 +639,34 @@ class ALTTPWorld(World):
if self.multiworld.boss_shuffle[self.player] != "none":
def create_boss_map() -> typing.Dict:
boss_map = {
"Eastern Palace": self.multiworld.get_dungeon("Eastern Palace", self.player).boss.name,
"Desert Palace": self.multiworld.get_dungeon("Desert Palace", self.player).boss.name,
"Tower Of Hera": self.multiworld.get_dungeon("Tower of Hera", self.player).boss.name,
"Eastern Palace": self.dungeons["Eastern Palace"].boss.name,
"Desert Palace": self.dungeons["Desert Palace"].boss.name,
"Tower Of Hera": self.dungeons["Tower of Hera"].boss.name,
"Hyrule Castle": "Agahnim",
"Palace Of Darkness": self.multiworld.get_dungeon("Palace of Darkness",
self.player).boss.name,
"Swamp Palace": self.multiworld.get_dungeon("Swamp Palace", self.player).boss.name,
"Skull Woods": self.multiworld.get_dungeon("Skull Woods", self.player).boss.name,
"Thieves Town": self.multiworld.get_dungeon("Thieves Town", self.player).boss.name,
"Ice Palace": self.multiworld.get_dungeon("Ice Palace", self.player).boss.name,
"Misery Mire": self.multiworld.get_dungeon("Misery Mire", self.player).boss.name,
"Turtle Rock": self.multiworld.get_dungeon("Turtle Rock", self.player).boss.name,
"Palace Of Darkness": self.dungeons["Palace of Darkness"].boss.name,
"Swamp Palace": self.dungeons["Swamp Palace"].boss.name,
"Skull Woods": self.dungeons["Skull Woods"].boss.name,
"Thieves Town": self.dungeons["Thieves Town"].boss.name,
"Ice Palace": self.dungeons["Ice Palace"].boss.name,
"Misery Mire": self.dungeons["Misery Mire"].boss.name,
"Turtle Rock": self.dungeons["Turtle Rock"].boss.name,
"Ganons Tower": "Agahnim 2",
"Ganon": "Ganon"
}
if self.multiworld.mode[self.player] != 'inverted':
boss_map.update({
"Ganons Tower Basement":
self.multiworld.get_dungeon("Ganons Tower", self.player).bosses["bottom"].name,
"Ganons Tower Middle": self.multiworld.get_dungeon("Ganons Tower", self.player).bosses[
self.dungeons["Ganons Tower"].bosses["bottom"].name,
"Ganons Tower Middle": self.dungeons["Ganons Tower"].bosses[
"middle"].name,
"Ganons Tower Top": self.multiworld.get_dungeon("Ganons Tower", self.player).bosses[
"Ganons Tower Top": self.dungeons["Ganons Tower"].bosses[
"top"].name
})
else:
boss_map.update({
"Ganons Tower Basement": self.multiworld.get_dungeon("Inverted Ganons Tower", self.player).bosses["bottom"].name,
"Ganons Tower Middle": self.multiworld.get_dungeon("Inverted Ganons Tower", self.player).bosses["middle"].name,
"Ganons Tower Top": self.multiworld.get_dungeon("Inverted Ganons Tower", self.player).bosses["top"].name
"Ganons Tower Basement": self.dungeons["Inverted Ganons Tower"].bosses["bottom"].name,
"Ganons Tower Middle": self.dungeons["Inverted Ganons Tower"].bosses["middle"].name,
"Ganons Tower Top": self.dungeons["Inverted Ganons Tower"].bosses["top"].name
})
return boss_map
@ -709,11 +718,10 @@ class ALTTPWorld(World):
def get_pre_fill_items(self):
res = []
if self.dungeon_local_item_names:
for (name, player), dungeon in self.multiworld.dungeons.items():
if player == self.player:
for item in dungeon.all_items:
if item.name in self.dungeon_local_item_names:
res.append(item)
for dungeon in self.dungeons.values():
for item in dungeon.all_items:
if item.name in self.dungeon_local_item_names:
res.append(item)
return res

View File

@ -2,13 +2,12 @@ import unittest
from argparse import Namespace
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.ItemPool import difficulties, generate_itempool
from worlds.alttp.ItemPool import difficulties
from worlds.alttp.Items import ItemFactory
from worlds.alttp.Regions import create_regions
from worlds.alttp.Shops import create_shops
from worlds.alttp.Rules import set_rules
from worlds import AutoWorld
@ -24,7 +23,7 @@ class TestDungeon(unittest.TestCase):
self.remove_exits = [] # Block dungeon exits
self.multiworld.difficulty_requirements[1] = difficulties['normal']
create_regions(self.multiworld, 1)
create_dungeons(self.multiworld, 1)
self.multiworld.worlds[1].create_dungeons()
create_shops(self.multiworld, 1)
for exitname, regionname in mandatory_connections:
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.mode[1] = "inverted"
create_inverted_regions(self.multiworld, 1)
create_dungeons(self.multiworld, 1)
self.multiworld.worlds[1].create_dungeons()
create_shops(self.multiworld, 1)
link_inverted_entrances(self.multiworld, 1)
self.multiworld.worlds[1].create_items()

View File

@ -23,7 +23,7 @@ class TestInvertedBombRules(unittest.TestCase):
self.multiworld.set_default_common_options()
self.multiworld.difficulty_requirements[1] = difficulties['normal']
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.
def testInvertedBombRulesAreComplete(self):

View File

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

View File

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

View File

@ -474,7 +474,8 @@ def get_woth_hint(world, checked):
locations = world.required_locations
locations = list(filter(lambda location:
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_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)
checked[location.player].add(location.name)
if location.parent_region.dungeon:
if getattr(location.parent_region, "dungeon", None):
world.woth_dungeon += 1
location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text
else:
@ -570,7 +571,7 @@ def get_good_item_hint(world, checked):
checked[location.player].add(location.name)
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
return (GossipText('#%s# hoards #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)),
['Green', 'Red']), location)
@ -613,8 +614,8 @@ def get_specific_item_hint(world, checked):
location = world.hint_rng.choice(locations)
checked[location.player].add(location.name)
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
if world.hint_dist_user.get('vague_named_items', False):
return (GossipText('#%s# may be on the hero\'s path.' % (location_text), ['Green']), location)