LttP: extract Dungeon and Boss from core (#1787)
This commit is contained in:
parent
a2ddd5c9e8
commit
c8453035da
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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]}
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue