Merged in DRUnstable v1.2.0.1

This commit is contained in:
codemann8
2022-12-16 18:18:07 -06:00
22 changed files with 277 additions and 90 deletions

View File

@@ -110,7 +110,8 @@ class World(object):
set_player_attr('can_access_trock_front', None) set_player_attr('can_access_trock_front', None)
set_player_attr('can_access_trock_big_chest', None) set_player_attr('can_access_trock_big_chest', None)
set_player_attr('can_access_trock_middle', None) set_player_attr('can_access_trock_middle', None)
set_player_attr('fix_fake_world', logic[player] not in ['owglitches', 'nologic'] or shuffle[player] in ['crossed', 'insanity']) set_player_attr('fix_fake_world', logic[player] not in ['owglitches', 'nologic']
or shuffle[player] in ['lean', 'crossed', 'insanity'])
set_player_attr('mapshuffle', False) set_player_attr('mapshuffle', False)
set_player_attr('compassshuffle', False) set_player_attr('compassshuffle', False)
set_player_attr('keyshuffle', 'none') set_player_attr('keyshuffle', 'none')
@@ -155,7 +156,9 @@ class World(object):
def finish_init(self): def finish_init(self):
for player in range(1, self.players + 1): for player in range(1, self.players + 1):
if self.mode[player] == 'retro': if self.mode[player] == 'retro':
self.mode[player] == 'open' self.mode[player] = 'open'
if self.goal[player] == 'completionist':
self.accessibility[player] = 'locations'
def get_name_string_for_object(self, obj): def get_name_string_for_object(self, obj):
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})' return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})'
@@ -471,7 +474,10 @@ class World(object):
if self.has_beaten_game(state): if self.has_beaten_game(state):
return True return True
prog_locations = [location for location in self.get_locations() if location.item is not None and (location.item.advancement or location.event) and location not in state.locations_checked] prog_locations = [location for location in self.get_locations() if location.item is not None
and (location.item.advancement or location.event
or self.goal[location.player] == 'completionist')
and location not in state.locations_checked]
while prog_locations: while prog_locations:
sphere = [] sphere = []
@@ -1038,6 +1044,12 @@ class CollectionState(object):
def item_count(self, item, player): def item_count(self, item, player):
return self.prog_items[item, player] return self.prog_items[item, player]
def everything(self, player):
all_locations = self.world.get_filled_locations(player)
all_locations.remove(self.world.get_location('Ganon', player))
return (len([x for x in self.locations_checked if x.player == player])
>= len(all_locations))
def has_crystals(self, count, player): def has_crystals(self, count, player):
crystals = ['Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'] crystals = ['Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7']
return len([crystal for crystal in crystals if self.has(crystal, player)]) >= count return len([crystal for crystal in crystals if self.has(crystal, player)]) >= count
@@ -1155,6 +1167,8 @@ class CollectionState(object):
return self.has('Fire Rod', player) or self.has('Lamp', player) return self.has('Fire Rod', player) or self.has('Lamp', player)
def can_flute(self, player): def can_flute(self, player):
if self.world.mode[player] == 'standard' and not self.has('Zelda Delivered', player):
return False # can't flute in rain state
if any(map(lambda i: i.name in ['Ocarina', 'Ocarina (Activated)'], self.world.precollected_items)): if any(map(lambda i: i.name in ['Ocarina', 'Ocarina (Activated)'], self.world.precollected_items)):
return True return True
lw = self.world.get_region('Light World', player) lw = self.world.get_region('Light World', player)
@@ -2590,7 +2604,7 @@ class Spoiler(object):
outfile.write('Mode: %s\n' % self.metadata['mode'][player]) outfile.write('Mode: %s\n' % self.metadata['mode'][player])
outfile.write('Swords: %s\n' % self.metadata['weapons'][player]) outfile.write('Swords: %s\n' % self.metadata['weapons'][player])
outfile.write('Goal: %s\n' % self.metadata['goal'][player]) outfile.write('Goal: %s\n' % self.metadata['goal'][player])
if self.metadata['goal'][player] in ['triforcehunt', 'trinity']: if self.metadata['goal'][player] in ['triforcehunt', 'trinity', 'ganonhunt']:
outfile.write('Triforce Pieces Required: %s\n' % self.metadata['triforcegoal'][player]) outfile.write('Triforce Pieces Required: %s\n' % self.metadata['triforcegoal'][player])
outfile.write('Triforce Pieces Total: %s\n' % self.metadata['triforcepool'][player]) outfile.write('Triforce Pieces Total: %s\n' % self.metadata['triforcepool'][player])
outfile.write('Crystals required for GT: %s\n' % (str(self.world.crystals_gt_orig[player]))) outfile.write('Crystals required for GT: %s\n' % (str(self.world.crystals_gt_orig[player])))
@@ -2611,7 +2625,6 @@ class Spoiler(object):
outfile.write(f"GT/Ganon Shuffled: {yn(self.metadata['shuffleganon'])}\n") outfile.write(f"GT/Ganon Shuffled: {yn(self.metadata['shuffleganon'])}\n")
outfile.write(f"Overworld Map: {self.metadata['overworld_map'][player]}\n") outfile.write(f"Overworld Map: {self.metadata['overworld_map'][player]}\n")
outfile.write(f"Take Any Caves: {self.metadata['take_any'][player]}\n") outfile.write(f"Take Any Caves: {self.metadata['take_any'][player]}\n")
outfile.write(f"Overworld Map: {self.metadata['overworld_map'][player]}\n")
if self.metadata['goal'][player] != 'trinity': if self.metadata['goal'][player] != 'trinity':
outfile.write('Pyramid hole pre-opened: %s\n' % ('Yes' if self.metadata['open_pyramid'][player] else 'No')) outfile.write('Pyramid hole pre-opened: %s\n' % ('Yes' if self.metadata['open_pyramid'][player] else 'No'))
outfile.write('Door Shuffle: %s\n' % self.metadata['door_shuffle'][player]) outfile.write('Door Shuffle: %s\n' % self.metadata['door_shuffle'][player])
@@ -2868,7 +2881,8 @@ world_mode = {"open": 0, "standard": 1, "inverted": 2}
sword_mode = {"random": 0, "assured": 1, "swordless": 2, "vanilla": 3} sword_mode = {"random": 0, "assured": 1, "swordless": 2, "vanilla": 3}
# byte 2: GGGD DFFH (goal, diff, item_func, hints) # byte 2: GGGD DFFH (goal, diff, item_func, hints)
goal_mode = {'ganon': 0, 'pedestal': 1, 'dungeons': 2, 'triforcehunt': 3, 'crystals': 4, 'trinity': 5} goal_mode = {'ganon': 0, 'pedestal': 1, 'dungeons': 2, 'triforcehunt': 3, 'crystals': 4, 'trinity': 5,
'ganonhunt': 6, 'completionist': 7}
diff_mode = {"normal": 0, "hard": 1, "expert": 2} diff_mode = {"normal": 0, "hard": 1, "expert": 2}
func_mode = {"normal": 0, "hard": 1, "expert": 2} func_mode = {"normal": 0, "hard": 1, "expert": 2}
@@ -2909,7 +2923,7 @@ boss_mode = {"none": 0, "simple": 1, "full": 2, "chaos": 3, 'random': 3, 'unique
flute_mode = {'normal': 0, 'active': 1} flute_mode = {'normal': 0, 'active': 1}
keyshuffle_mode = {'none': 0, 'wild': 1, 'universal': 2} # reserved 8 modes? keyshuffle_mode = {'none': 0, 'wild': 1, 'universal': 2} # reserved 8 modes?
take_any_mode = {'none': 0, 'random': 1, 'fixed': 2} take_any_mode = {'none': 0, 'random': 1, 'fixed': 2}
bow_mode = {'progressive': 0, 'silvers': 1, 'retro': 2, 'retro_silver': 3} bow_mode = {'progressive': 0, 'silvers': 1, 'retro': 2, 'retro_silvers': 3}
# additions # additions
# psuedoboots does not effect code # psuedoboots does not effect code

View File

@@ -6,7 +6,7 @@ from enum import unique, Flag
from typing import DefaultDict, Dict, List from typing import DefaultDict, Dict, List
from itertools import chain from itertools import chain
from BaseClasses import RegionType, Region, Door, DoorType, Direction, Sector, CrystalBarrier, DungeonInfo, dungeon_keys from BaseClasses import RegionType, Region, Door, DoorType, Sector, CrystalBarrier, DungeonInfo, dungeon_keys
from BaseClasses import PotFlags, LocationType, Direction from BaseClasses import PotFlags, LocationType, Direction
from Doors import reset_portals from Doors import reset_portals
from Dungeons import dungeon_regions, region_starts, standard_starts, split_region_starts from Dungeons import dungeon_regions, region_starts, standard_starts, split_region_starts
@@ -15,13 +15,12 @@ from Items import ItemFactory
from RoomData import DoorKind, PairedDoor, reset_rooms from RoomData import DoorKind, PairedDoor, reset_rooms
from source.dungeon.DungeonStitcher import GenerationException, generate_dungeon from source.dungeon.DungeonStitcher import GenerationException, generate_dungeon
from source.dungeon.DungeonStitcher import ExplorationState as ExplorationState2 from source.dungeon.DungeonStitcher import ExplorationState as ExplorationState2
# from DungeonGenerator import generate_dungeon from DungeonGenerator import ExplorationState, convert_regions, determine_required_paths, drop_entrances
from DungeonGenerator import ExplorationState, convert_regions, pre_validate, determine_required_paths, drop_entrances
from DungeonGenerator import create_dungeon_builders, split_dungeon_builder, simple_dungeon_builder, default_dungeon_entrances from DungeonGenerator import create_dungeon_builders, split_dungeon_builder, simple_dungeon_builder, default_dungeon_entrances
from DungeonGenerator import dungeon_portals, dungeon_drops, connect_doors, count_reserved_locations from DungeonGenerator import dungeon_portals, dungeon_drops, connect_doors, count_reserved_locations
from DungeonGenerator import valid_region_to_explore from DungeonGenerator import valid_region_to_explore
from KeyDoorShuffle import analyze_dungeon, build_key_layout, validate_key_layout, determine_prize_lock from KeyDoorShuffle import analyze_dungeon, build_key_layout, validate_key_layout, determine_prize_lock
from KeyDoorShuffle import validate_bk_layout, check_bk_special from KeyDoorShuffle import validate_bk_layout
from Utils import ncr, kth_combination from Utils import ncr, kth_combination
@@ -1893,6 +1892,7 @@ def shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world,
def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, world, player): def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, world, player):
max_computation = 11 # this is around 6 billion worse case factorial don't want to exceed this much
for pool, door_type_pool in door_type_pools: for pool, door_type_pool in door_type_pools:
ttl = 0 ttl = 0
suggestion_map, small_map, flex_map = {}, {}, {} suggestion_map, small_map, flex_map = {}, {}, {}
@@ -1919,27 +1919,28 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, worl
calculated = int(round(builder.key_doors_num*total_keys/ttl)) calculated = int(round(builder.key_doors_num*total_keys/ttl))
max_keys = max(0, builder.location_cnt - calc_used_dungeon_items(builder, world, player)) max_keys = max(0, builder.location_cnt - calc_used_dungeon_items(builder, world, player))
cand_len = max(0, len(builder.candidates.small) - builder.key_drop_cnt) cand_len = max(0, len(builder.candidates.small) - builder.key_drop_cnt)
limit = min(max_keys, cand_len) limit = min(max_keys, cand_len, max_computation)
suggested = min(calculated, limit) suggested = min(calculated, limit)
combo_size = ncr(len(builder.candidates.small), suggested + builder.key_drop_cnt) key_door_num = min(suggested + builder.key_drop_cnt, max_computation)
combo_size = ncr(len(builder.candidates.small), key_door_num)
while combo_size > 500000 and suggested > 0: while combo_size > 500000 and suggested > 0:
suggested -= 1 suggested -= 1
combo_size = ncr(len(builder.candidates.small), suggested + builder.key_drop_cnt) combo_size = ncr(len(builder.candidates.small), key_door_num)
suggestion_map[dungeon] = builder.key_doors_num = suggested + builder.key_drop_cnt suggestion_map[dungeon] = builder.key_doors_num = key_door_num
remaining -= suggested + builder.key_drop_cnt remaining -= suggested + builder.key_drop_cnt
builder.combo_size = combo_size builder.combo_size = combo_size
flex_map[dungeon] = (limit - suggested) if suggested < limit else 0 flex_map[dungeon] = (limit - suggested) if suggested < limit else 0
for dungeon in pool: for dungeon in pool:
builder = world.dungeon_layouts[player][dungeon] builder = world.dungeon_layouts[player][dungeon]
if total_adjustable: if total_adjustable:
builder.total_keys = suggestion_map[dungeon] builder.total_keys = max(suggestion_map[dungeon], builder.key_drop_cnt)
valid_doors, small_number = find_valid_combination(builder, suggestion_map[dungeon], valid_doors, small_number = find_valid_combination(builder, suggestion_map[dungeon],
start_regions_map[dungeon], world, player) start_regions_map[dungeon], world, player)
small_map[dungeon] = valid_doors small_map[dungeon] = valid_doors
actual_chest_keys = small_number - builder.key_drop_cnt actual_chest_keys = small_number - builder.key_drop_cnt
if actual_chest_keys < suggestion_map[dungeon]: if actual_chest_keys < suggestion_map[dungeon]:
if total_adjustable: if total_adjustable:
builder.total_keys = actual_chest_keys builder.total_keys = actual_chest_keys + builder.key_drop_cnt
flex_map[dungeon] = 0 flex_map[dungeon] = 0
remaining += suggestion_map[dungeon] - actual_chest_keys remaining += suggestion_map[dungeon] - actual_chest_keys
suggestion_map[dungeon] = small_number suggestion_map[dungeon] = small_number
@@ -1950,6 +1951,8 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, worl
builder = queue.popleft() builder = queue.popleft()
dungeon = builder.name dungeon = builder.name
increased = suggestion_map[dungeon] + 1 increased = suggestion_map[dungeon] + 1
if increased > max_computation:
continue
builder.key_doors_num = increased builder.key_doors_num = increased
valid_doors, small_number = find_valid_combination(builder, increased, start_regions_map[dungeon], valid_doors, small_number = find_valid_combination(builder, increased, start_regions_map[dungeon],
world, player) world, player)
@@ -1959,7 +1962,7 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, worl
suggestion_map[dungeon] = increased suggestion_map[dungeon] = increased
flex_map[dungeon] -= 1 flex_map[dungeon] -= 1
if total_adjustable: if total_adjustable:
builder.total_keys = actual_chest_keys builder.total_keys = max(increased, builder.key_drop_cnt)
if flex_map[dungeon] > 0: if flex_map[dungeon] > 0:
builder.combo_size = ncr(len(builder.candidates.small), builder.key_doors_num) builder.combo_size = ncr(len(builder.candidates.small), builder.key_doors_num)
queue.append(builder) queue.append(builder)
@@ -2689,8 +2692,12 @@ def find_valid_bd_combination(builder, suggested, world, player):
test = random.choice([True, False]) test = random.choice([True, False])
if test: if test:
bomb_doors_needed -= 1 bomb_doors_needed -= 1
if bomb_doors_needed < 0:
bomb_doors_needed = 0
else: else:
dash_doors_needed -= 1 dash_doors_needed -= 1
if dash_doors_needed < 0:
dash_doors_needed = 0
bomb_proposal = random.sample(bd_door_pool, k=bomb_doors_needed) bomb_proposal = random.sample(bd_door_pool, k=bomb_doors_needed)
bomb_proposal.extend(custom_bomb_doors) bomb_proposal.extend(custom_bomb_doors)
dash_pool = [x for x in bd_door_pool if x not in bomb_proposal] dash_pool = [x for x in bd_door_pool if x not in bomb_proposal]

View File

@@ -12,7 +12,7 @@ from typing import List
from BaseClasses import DoorType, Direction, CrystalBarrier, RegionType, Polarity, PolSlot, flooded_keys, Sector from BaseClasses import DoorType, Direction, CrystalBarrier, RegionType, Polarity, PolSlot, flooded_keys, Sector
from BaseClasses import Hook, hook_from_door, Door from BaseClasses import Hook, hook_from_door, Door
from Regions import dungeon_events, flooded_keys_reverse from Regions import dungeon_events, flooded_keys_reverse
from Dungeons import dungeon_regions, split_region_starts from Dungeons import split_region_starts
from RoomData import DoorKind from RoomData import DoorKind
from source.dungeon.DungeonStitcher import generate_dungeon_find_proposal from source.dungeon.DungeonStitcher import generate_dungeon_find_proposal
@@ -860,7 +860,7 @@ class ExplorationState(object):
self.crystal = exp_door.crystal self.crystal = exp_door.crystal
return exp_door return exp_door
def visit_region(self, region, key_region=None, key_checks=False, bk_Flag=False): def visit_region(self, region, key_region=None, key_checks=False, bk_flag=False):
if region.type != RegionType.Dungeon: if region.type != RegionType.Dungeon:
self.crystal = CrystalBarrier.Orange self.crystal = CrystalBarrier.Orange
if self.crystal == CrystalBarrier.Either: if self.crystal == CrystalBarrier.Either:
@@ -881,7 +881,7 @@ class ExplorationState(object):
self.ttl_locations += 1 self.ttl_locations += 1
if location not in self.found_locations: if location not in self.found_locations:
self.found_locations.append(location) self.found_locations.append(location)
if not bk_Flag and (not location.forced_item or 'Big Key' in location.item.name): if not bk_flag and (not location.forced_item or 'Big Key' in location.item.name):
self.bk_found.add(location) self.bk_found.add(location)
if location.name in dungeon_events and location.name not in self.events: if location.name in dungeon_events and location.name not in self.events:
if self.flooded_key_check(location): if self.flooded_key_check(location):
@@ -1335,8 +1335,9 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, dunge
sector = find_sector(r_name, all_sectors) sector = find_sector(r_name, all_sectors)
reverse_d_map[sector] = key reverse_d_map[sector] = key
if world.mode[player] == 'standard': if world.mode[player] == 'standard':
current_dungeon = dungeon_map['Hyrule Castle'] if 'Hyrule Castle' in dungeon_map:
standard_stair_check(dungeon_map, current_dungeon, candidate_sectors, global_pole) current_dungeon = dungeon_map['Hyrule Castle']
standard_stair_check(dungeon_map, current_dungeon, candidate_sectors, global_pole)
complete_dungeons = {x: y for x, y in dungeon_map.items() if sum(len(sector.outstanding_doors) for sector in y.sectors) <= 0} complete_dungeons = {x: y for x, y in dungeon_map.items() if sum(len(sector.outstanding_doors) for sector in y.sectors) <= 0}
[dungeon_map.pop(key) for key in complete_dungeons.keys()] [dungeon_map.pop(key) for key in complete_dungeons.keys()]

View File

@@ -400,7 +400,7 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
else: else:
max_trash = gt_count max_trash = gt_count
scaled_trash = math.floor(max_trash * scale_factor) scaled_trash = math.floor(max_trash * scale_factor)
if world.goal[player] in ['triforcehunt', 'trinity']: if world.goal[player] in ['triforcehunt', 'trinity', 'ganonhunt']:
gftower_trash_count = random.randint(scaled_trash, max_trash) gftower_trash_count = random.randint(scaled_trash, max_trash)
else: else:
gftower_trash_count = random.randint(0, scaled_trash) gftower_trash_count = random.randint(0, scaled_trash)

View File

@@ -181,8 +181,12 @@ def get_custom_array_key(item):
def generate_itempool(world, player): def generate_itempool(world, player):
if (world.difficulty[player] not in ['normal', 'hard', 'expert'] or world.goal[player] not in ['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'trinity', 'crystals'] if (world.difficulty[player] not in ['normal', 'hard', 'expert']
or world.mode[player] not in ['open', 'standard', 'inverted'] or world.timer not in ['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'] or world.progressive not in ['on', 'off', 'random']): or world.goal[player] not in ['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'trinity', 'crystals',
'ganonhunt', 'completionist']
or world.mode[player] not in ['open', 'standard', 'inverted']
or world.timer not in ['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown']
or world.progressive not in ['on', 'off', 'random']):
raise NotImplementedError('Not supported yet') raise NotImplementedError('Not supported yet')
if world.timer in ['ohko', 'timed-ohko']: if world.timer in ['ohko', 'timed-ohko']:
@@ -344,7 +348,7 @@ def generate_itempool(world, player):
world.clock_mode = clock_mode world.clock_mode = clock_mode
goal = world.goal[player] goal = world.goal[player]
if goal in ['triforcehunt', 'trinity']: if goal in ['triforcehunt', 'trinity', 'ganonhunt']:
g, t = set_default_triforce(goal, world.treasure_hunt_count[player], world.treasure_hunt_total[player]) g, t = set_default_triforce(goal, world.treasure_hunt_count[player], world.treasure_hunt_total[player])
world.treasure_hunt_count[player], world.treasure_hunt_total[player] = g, t world.treasure_hunt_count[player], world.treasure_hunt_total[player] = g, t
world.treasure_hunt_icon[player] = 'Triforce Piece' world.treasure_hunt_icon[player] = 'Triforce Piece'
@@ -817,15 +821,15 @@ def add_pot_contents(world, player):
world.itempool.append(ItemFactory(item, player)) world.itempool.append(ItemFactory(item, player))
def get_pool_core(world, player, progressive, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords, bombbag, def get_pool_core(world, player, progressive, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords,
door_shuffle, logic, flute_activated): bombbag, door_shuffle, logic, flute_activated):
pool = [] pool = []
placed_items = {} placed_items = {}
precollected_items = [] precollected_items = []
clock_mode = None clock_mode = None
if goal in ['triforcehunt', 'trinity']: if goal in ['triforcehunt', 'trinity', 'ganonhunt']:
if treasure_hunt_total == 0: if treasure_hunt_total == 0:
treasure_hunt_total = 30 if goal == 'triforcehunt' else 10 treasure_hunt_total = 30 if goal in ['triforcehunt', 'ganonhunt'] else 10
# triforce pieces max out # triforce pieces max out
triforcepool = ['Triforce Piece'] * min(treasure_hunt_total, max_goal) triforcepool = ['Triforce Piece'] * min(treasure_hunt_total, max_goal)
@@ -928,7 +932,7 @@ def get_pool_core(world, player, progressive, shuffle, difficulty, treasure_hunt
elif timer == 'timed-ohko': elif timer == 'timed-ohko':
pool.extend(diff.timedohko) pool.extend(diff.timedohko)
clock_mode = 'countdown-ohko' clock_mode = 'countdown-ohko'
if goal in ['triforcehunt', 'trinity']: if goal in ['triforcehunt', 'trinity', 'ganonhunt']:
pool.extend(triforcepool) pool.extend(triforcepool)
for extra in diff.extras: for extra in diff.extras:
@@ -987,7 +991,7 @@ def make_custom_item_pool(world, player, progressive, shuffle, difficulty, timer
customitemarray["triforce"] = total_items_to_place customitemarray["triforce"] = total_items_to_place
# Triforce Pieces # Triforce Pieces
if goal in ['triforcehunt', 'trinity']: if goal in ['triforcehunt', 'trinity', 'ganonhunt']:
g, t = set_default_triforce(goal, customitemarray["triforcepiecesgoal"], customitemarray["triforcepieces"]) g, t = set_default_triforce(goal, customitemarray["triforcepiecesgoal"], customitemarray["triforcepieces"])
customitemarray["triforcepiecesgoal"], customitemarray["triforcepieces"] = g, t customitemarray["triforcepiecesgoal"], customitemarray["triforcepieces"] = g, t
@@ -1025,8 +1029,8 @@ def make_custom_item_pool(world, player, progressive, shuffle, difficulty, timer
treasure_hunt_count = max(min(customitemarray["triforcepiecesgoal"], max_goal), 1) treasure_hunt_count = max(min(customitemarray["triforcepiecesgoal"], max_goal), 1)
treasure_hunt_icon = 'Triforce Piece' treasure_hunt_icon = 'Triforce Piece'
# Ensure game is always possible to complete here, force sufficient pieces if the player is unwilling. # Ensure game is always possible to complete here, force sufficient pieces if the player is unwilling.
if ((customitemarray["triforcepieces"] < treasure_hunt_count) and (goal in ['triforcehunt', 'trinity']) if ((customitemarray["triforcepieces"] < treasure_hunt_count)
and (customitemarray["triforce"] == 0)): and (goal in ['triforcehunt', 'trinity', 'ganonhunt']) and (customitemarray["triforce"] == 0)):
extrapieces = treasure_hunt_count - customitemarray["triforcepieces"] extrapieces = treasure_hunt_count - customitemarray["triforcepieces"]
pool.extend(['Triforce Piece'] * extrapieces) pool.extend(['Triforce Piece'] * extrapieces)
itemtotal = itemtotal + extrapieces itemtotal = itemtotal + extrapieces
@@ -1214,7 +1218,7 @@ def get_player_dungeon_item_pool(world, player):
# location pool doesn't support larger values at this time # location pool doesn't support larger values at this time
def set_default_triforce(goal, custom_goal, custom_total): def set_default_triforce(goal, custom_goal, custom_total):
triforce_goal, triforce_total = 0, 0 triforce_goal, triforce_total = 0, 0
if goal == 'triforcehunt': if goal in ['triforcehunt', 'ganonhunt']:
triforce_goal, triforce_total = 20, 30 triforce_goal, triforce_total = 20, 30
elif goal == 'trinity': elif goal == 'trinity':
triforce_goal, triforce_total = 8, 10 triforce_goal, triforce_total = 8, 10

View File

@@ -1445,7 +1445,7 @@ def validate_bk_layout(proposal, builder, start_regions, world, player):
if loc.forced_big_key(): if loc.forced_big_key():
return True return True
else: else:
return len(state.bk_found) > 0 return state.count_locations_exclude_specials(world, player) > 0
return False return False

View File

@@ -33,7 +33,7 @@ from source.overworld.EntranceShuffle2 import link_entrances_new
from source.tools.BPS import create_bps_from_data from source.tools.BPS import create_bps_from_data
from source.classes.CustomSettings import CustomSettings from source.classes.CustomSettings import CustomSettings
__version__ = '1.2.0.0-x' __version__ = '1.2.0.1-u'
from source.classes.BabelFish import BabelFish from source.classes.BabelFish import BabelFish
@@ -605,7 +605,8 @@ def create_playthrough(world):
world = copy_world(world) world = copy_world(world)
# get locations containing progress items # get locations containing progress items
prog_locations = [location for location in world.get_filled_locations() if location.item.advancement] prog_locations = [location for location in world.get_filled_locations() if location.item.advancement
or world.goal[location.player] == 'completionist']
optional_locations = ['Trench 1 Switch', 'Trench 2 Switch', 'Ice Block Drop', 'Skull Star Tile'] optional_locations = ['Trench 1 Switch', 'Trench 2 Switch', 'Ice Block Drop', 'Skull Star Tile']
state_cache = [None] state_cache = [None]
collection_spheres = [] collection_spheres = []
@@ -642,6 +643,8 @@ def create_playthrough(world):
for num, sphere in reversed(list(enumerate(collection_spheres))): for num, sphere in reversed(list(enumerate(collection_spheres))):
to_delete = set() to_delete = set()
for location in sphere: for location in sphere:
if world.goal[location.player] == 'completionist':
continue # every location for that player is required
# we remove the item at location and check if game is still beatable # we remove the item at location and check if game is still beatable
logging.getLogger('').debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, location.item.player) logging.getLogger('').debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, location.item.player)
old_item = location.item old_item = location.item

132
README.md
View File

@@ -9,13 +9,16 @@ See https://alttpr.com/ for more details on the normal randomizer.
2. [Commonly Missed Things](#commonly-missed-things) (** **Read This If New** **) 2. [Commonly Missed Things](#commonly-missed-things) (** **Read This If New** **)
3. [Settings](#settings) 3. [Settings](#settings)
1. [Dungeon Randomization](#dungeon-settings) 1. [Dungeon Randomization](#dungeon-settings)
1. [Dungeon Door Shuffle](#door-shuffle---doorshuffle) 1. [Dungeon Door Shuffle](#door-shuffle)
2. [Intensity Level](#intensity---intensity-number) 2. [Intensity Level](#intensity---intensity-number)
3. [Key Drop Shuffle (Legacy)](#key-drop-shuffle-legacy---keydropshuffle) 3. [Key Drop Shuffle (Legacy)](#key-drop-shuffle-legacy---keydropshuffle)
4. [Pottery](#pottery) 4. [Door Type Shuffle](#door-type_shuffle)
5. [Shuffle Enemy Key Drops](#shuffle-enemy-key-drops---dropshuffle) 5. [Decouple Doors](#decouple-doors)
6. [Experimental Features](#experimental-features) 6. [Pottery](#pottery)
7. [Crossed Dungeon Specific Settings](#crossed-dungeon-specific-settings) 7. [Small Key Shuffle](#small-key-shuffle)
8. [Shuffle Enemy Key Drops](#shuffle-enemy-key-drops)
9. [Experimental Features](#experimental-features)
10. [Crossed Dungeon Specific Settings](#crossed-dungeon-specific-settings)
2. [Item Randomization Changes](#item-randomization) 2. [Item Randomization Changes](#item-randomization)
1. [New "Items"](#new-items) 1. [New "Items"](#new-items)
2. [Shopsanity](#shopsanity) 2. [Shopsanity](#shopsanity)
@@ -23,12 +26,15 @@ See https://alttpr.com/ for more details on the normal randomizer.
4. [Goal](#goal) 4. [Goal](#goal)
5. [Item Sorting](#item-sorting) 5. [Item Sorting](#item-sorting)
6. [Forbidden Boss Items](#forbidden-boss-items) 6. [Forbidden Boss Items](#forbidden-boss-items)
3. [Entrance Randomization](#entrance-randomization) 3. [Customizer](#customizer)
4. [Entrance Randomization](#entrance-randomization)
1. [Shuffle Links House](#shuffle-links-house) 1. [Shuffle Links House](#shuffle-links-house)
2. [Overworld Map](#overworld-map) 2. [Overworld Map](#overworld-map)
4. [Enemizer](#enemizer) 5. [Enemizer](#enemizer)
5. [Game Options](#game-options) 6. [Retro Changes](#retro-changes)
6. [Generation Setup & Miscellaneous](#generation-setup--miscellaneous) 7. [Standard Changes](#standard-changes)
8. [Game Options](#game-options)
9. [Generation Setup & Miscellaneous](#generation-setup--miscellaneous)
## Setup and Installation ## Setup and Installation
@@ -105,12 +111,15 @@ You start with a “Mirror Scroll”, a dumbed-down mirror that only works in du
Only extra settings are found here. All entrance randomizer settings are supported. See their [readme](https://github.com/KevinCathcart/ALttPEntranceRandomizer/blob/master/README.md) Only extra settings are found here. All entrance randomizer settings are supported. See their [readme](https://github.com/KevinCathcart/ALttPEntranceRandomizer/blob/master/README.md)
### Door Shuffle (--doorShuffle) ### Door Shuffle
* Vanilla - Doors are not shuffled * Vanilla - Doors are not shuffled
* Basic - Doors are shuffled only within a single dungeon. * Basic - Doors are shuffled only within a single dungeon.
* Paritioned - Dungeons are shuffled in 3 pools: Light World, Early Dark World, Late Dark World. (Late Dark are the four dungeons that require Mitts in vanilla, including Ganons Tower)
* Crossed - Doors are shuffled between dungeons as well. * Crossed - Doors are shuffled between dungeons as well.
CLI: `--doorShuffle [vanilla|basic|partitioned|crossed]`
### Intensity (--intensity number) ### Intensity (--intensity number)
* Level 1 - Normal door and spiral staircases are shuffled * Level 1 - Normal door and spiral staircases are shuffled
@@ -122,7 +131,24 @@ Only extra settings are found here. All entrance randomizer settings are support
Adds 33 new locations to the randomization pool. The 32 small keys found under pots and dropped by enemies and the Big Adds 33 new locations to the randomization pool. The 32 small keys found under pots and dropped by enemies and the Big
Key drop location are added to the pool. The keys normally found there are added to the item pool. Retro adds Key drop location are added to the pool. The keys normally found there are added to the item pool. Retro adds
32 generic keys to the pool instead. This has been can be controlled more granularly with the [Pottery](#pottery) and 32 generic keys to the pool instead. This has been can be controlled more granularly with the [Pottery](#pottery) and
[Shuffle Enemy Key Drops](#shuffle-enemy-key-drops---dropshuffle) [Shuffle Enemy Key Drops](#shuffle-enemy-key-drops)
### Door Type Shuffle
Four options here, and all of them only take effect if Dungeon Door Shuffle is not vanilla:
* Small Key Doors, Bomb Doors, Dash Doors: This is what was normally shuffled previously
* Adds Big Keys Doors: Big key doors are now shuffled in addition to those above, and Big Key doors are enabled to be on in both vertical directions thanks to a graphic that ended up on the cutting room floor. This does change
* Adds Trap Doors: All trap doors that are permanently shut in vanilla are shuffled.
* Increases all Door Types: This is a chaos mode where each door type per dungeon is randomized between 1 less and 4 more.
CLI: `--door_type_mode [original|big|all|chaos]`
### Decouple Doors
This is similar to insanity mode in ER where door entrances and exits are not paired anymore. Tends to remove more logic from dungeons as many rooms will not be required to traverse to explore. Hope you like transitions.
CLI `--decoupledoors`
### Pottery ### Pottery
@@ -155,11 +181,23 @@ CLI `--colorizepots`
This continues to works the same by shuffling all pots on a supertile. It works with the lottery option as well to move the switches to any valid pot on the supertile regardless of the pots chosen in the pottery mode. This may increase the number of pot locations slightly depending on the mode. This continues to works the same by shuffling all pots on a supertile. It works with the lottery option as well to move the switches to any valid pot on the supertile regardless of the pots chosen in the pottery mode. This may increase the number of pot locations slightly depending on the mode.
### Shuffle Enemy Key Drops (--dropshuffle) ### Small Key Shuffle
There are three options now available:
* In Dungeon: The small key will be in their own dungeon
* Randomized: Small keys can be shuffled outside their own dungeon
* Universal: Retro keys without the other options
CLI: `--keyshuffle [none|wild|universal]`
### Shuffle Enemy Key Drops
Enemies that drop keys can have their drop shuffled into the pool. This is the one part of the keydropshuffle option. Enemies that drop keys can have their drop shuffled into the pool. This is the one part of the keydropshuffle option.
See the pottery option for more options involving pots. See the pottery option for more options involving pots.
CLI: `--dropshuffle`
### Experimental Features ### Experimental Features
You will start as a bunny if your spawn point is in the dark world. CLI: `--experimental` You will start as a bunny if your spawn point is in the dark world. CLI: `--experimental`
@@ -197,14 +235,35 @@ Rooms adjacent to sanctuary get their coloring to match the Sanctuary's original
### New "Items" ### New "Items"
#### Bombbag (--bombbag) #### Bombbag
Two bomb bags are added to the item pool (They look like +10 Capacity upgrades). Bombs are unable to be used until one is found. Bomb capacity upgrades are otherwise unavailable. Two bomb bags are added to the item pool (They look like +10 Capacity upgrades). Bombs are unable to be used until one is found. Bomb capacity upgrades are otherwise unavailable.
#### Pseudo Boots (--pseudoboots) CLI `--bombbag`
#### Pseudo Boots
Dashing is allowed without the boots item however doors and certain rocks remain un-openable until boots are found. Items that require boots are still unattainable. Specific sequence breaks like hovering and water-walking are not allowed until boots are found. Bonk distance is shortened to prevent certain pits from being crossed. Finding boots restores all normal behavior. Dashing is allowed without the boots item however doors and certain rocks remain un-openable until boots are found. Items that require boots are still unattainable. Specific sequence breaks like hovering and water-walking are not allowed until boots are found. Bonk distance is shortened to prevent certain pits from being crossed. Finding boots restores all normal behavior.
CLI `--pseudoboots`
#### Flute Mode
Normal mode for flute means you need to activate it at the village statue after finding it like usual. Activated flute mode mean you can use it immediately upon finding it. The flute SFX plays to let you know this is the case.
CLI:`--flute_mode`
#### Bow Mode
Four options here :
* Progressive. Standard progressive bows.
* Silvers separate. One bow in the pool and silvers are a separate item.
* Retro (progressive). Arrows cost rupees. You need to purchase the single arrow item at a shop and there are two progressive bows places.
* Retro + Silvers. Arrows cost rupees. You need to purchase the single arrow item or find the silvers, there is only one bow, and silvers are a separate item (but count for the quiver if found).
CLI: `--bow_mode [progressive|silvers|retro|retro_silvers]`
### Shopsanity ### Shopsanity
This adds 32 shop locations (9 more in retro) to the general location pool. This adds 32 shop locations (9 more in retro) to the general location pool.
@@ -316,7 +375,12 @@ CLI: `--logic owglitches`
### Goal ### Goal
Trinity goal is now supported. Find one of 3 triforces to win. One is at pedestal. One is with Ganon. One is with Murahdahla who wants you to find 8 of 10 triforce pieces to complete. New supported goals:
* Trinity: Find one of 3 triforces to win. One is at pedestal. One is with Ganon. One is with Murahdahla who wants you to find 8 of 10 triforce pieces to complete.
* Triforce Hunt + Ganon: Collect the requisite triforce pieces, then defeat Ganon. (Aga2 not required). Use `ganonhunt` on CLI
* Completionist: All dungeons not enough for you? You have to obtain every item in the game too. This option turns on the collection rate counter and forces accessibility to be 100% locations. Finish by defeating Ganon.
### Item Sorting ### Item Sorting
@@ -394,12 +458,24 @@ CLI: ```--restrict_boss_items <option>```
* mapcompass: ~~The map and compass are logically required to defeat a boss. This prevents both of those from appearing on the dungeon boss. Note that this does affect item placement logic and the placement algorithm as maps and compasses are considered as required items to beat a boss.~~ Currently bugged, not recommended for use. * mapcompass: ~~The map and compass are logically required to defeat a boss. This prevents both of those from appearing on the dungeon boss. Note that this does affect item placement logic and the placement algorithm as maps and compasses are considered as required items to beat a boss.~~ Currently bugged, not recommended for use.
* dungeon: Same as above but both small keys and bigs keys of the dungeon are not allowed on a boss. (Note: this does not affect universal keys as they are not dungeon-specific) * dungeon: Same as above but both small keys and bigs keys of the dungeon are not allowed on a boss. (Note: this does not affect universal keys as they are not dungeon-specific)
## Customizer
Please see [Customizer documentation](docs/Customizer.md) on how to create custom seeds.
## Entrance Randomization ## Entrance Randomization
### New Modes
Lite and Lean ER is now available when Experimental Features are turned on. (todo: bring over documenation of these modes)
### Shuffle Links House ### Shuffle Links House
In certain ER shuffles, (not dungeonssimple or dungeonsfulls), you can now control whether Links House is shuffled or remains vanilla. Previously, inverted seeds had this behavior and would shuffle links house, but now if will only do so if this is specified. Now, also works for open modes, but links house is never shuffled in standard mode. In certain ER shuffles, (not dungeonssimple or dungeonsfulls), you can now control whether Links House is shuffled or remains vanilla. Previously, inverted seeds had this behavior and would shuffle links house, but now if will only do so if this is specified. Now, also works for open modes, but links house is never shuffled in standard mode.
### Shuffle Back of Tavern
You may shuffle the back of tavern entrance in ER modes when Experimental Features are turned on.
### Overworld Map ### Overworld Map
Option to move indicators on overworld map to reference dungeon location. The non-default options include indicators for Hyrule Castle, Agahnim's Tower, and Ganon's Tower. Option to move indicators on overworld map to reference dungeon location. The non-default options include indicators for Hyrule Castle, Agahnim's Tower, and Ganon's Tower.
@@ -411,7 +487,9 @@ Option to move indicators on overworld map to reference dungeon location. The no
If you do not shuffle the compass or map outside of the dungeon, the non-shuffled items are not needed to display the information. If a dungeon does not have a map or compass, it is not needed for the information. Talking to the bomb shop or Sahasrahla furnishes you with complete information as well as map information. If you do not shuffle the compass or map outside of the dungeon, the non-shuffled items are not needed to display the information. If a dungeon does not have a map or compass, it is not needed for the information. Talking to the bomb shop or Sahasrahla furnishes you with complete information as well as map information.
CLI ```--overworld_map [default, compass, map]``` CLI ```--overworld_map [default|compass|map]```
## Enemizer ## Enemizer
@@ -423,6 +501,26 @@ At least one boss each of the prize bosses will be present guarding the prizes.
If bosses are shuffled and Blind is chosen to be the boss of Thieves Town, then bombing the attic and delivering the maiden is still required. If bosses are shuffled and Blind is chosen to be the boss of Thieves Town, then bombing the attic and delivering the maiden is still required.
## Standard Changes
When dungeon door shuffle is on, the Sanctuary is guaranteed to be behind the Throne Room and the Mirror Scroll works like death warping instead of the mirror during the escape sequence.
## Retro Changes
Retro can be partially enabled: see Small Key Shuffle and Bow Mode. Retro checkbox or Retro mode still enable all 3 options.
New supported option:
### Take Any Caves
These are now independent of retro mode and have three options: None, Random, and Fixed. None disables the caves. Random works as take-any caves did before. Fixed means that the take any caves replace specific fairy caves in the pool and will be at those entrances unless ER is turned on (then they can be shuffled wherever). The fixed entrances are:
* Desert Healer Fairy
* Swamp Healer Fairy (aka Light Hype Cave)
* Dark Death Mountain Healer Fairy
* Dark Lake Hylia Ledge Healer Fairy (aka Shopping Mall Bomb)
* Bonk Fairy (Dark)
## Game Options ## Game Options
### MSU Resume ### MSU Resume

View File

@@ -53,7 +53,16 @@ This is similar to insanity mode in ER where door entrances and exits are not pa
## Customizer ## Customizer
Please see [Customizer documentation](docs/Customizer.md) on how to create custom seeds. Please see [Customizer documentation](docs/Customizer.md) on how to create custom seeds.
## New Goals
### Triforce Hunt + Ganon
Collect the requisite triforce pieces, then defeat Ganon. (Aga2 not required). Use `ganonhunt` on CLI
### Completionist
All dungeons not enough for you? You have to obtain every item in the game too. This option turns on the collection rate counter and forces accessibility to be 100% locations. Finish by defeating Ganon.
## Standard Generation Change ## Standard Generation Change
@@ -100,9 +109,15 @@ These are now independent of retro mode and have three options: None, Random, an
# Bug Fixes and Notes # Bug Fixes and Notes
None yet * 1.2.0.1-u
* Added new ganonhunt and completionist goals
* Fixed the issue when defeating Agahnim and standing in the doorway can cause door state to linger.
* Fix for Inverted Lean/Lite ER
* Fix for vanilla Doors + Standard + ER
* Added a limit per dungeon on small key doors to ensure reasonable generation
* Fixed many small bugs
# Known Issues # Known Issues
* Standing in the doorway when defeating Aga 1 and being teleported to the Dark World will not clear door state. It may cause issues requiring a Save & Quit to fix. * Decoupled doors can lead to situations where you aren't logically supposed to go back through a door without a big key or small key, but you can if you press the correct direction back through the door first. There are some transitions where you may get stuck without a bomb. These problems are planned to be fixed.
* Decoupled doors can lead to situations where you aren't logically supposed to go back through a door without a big key or small key, but you can if you press the correct direction back through the door first. There are some transitions where you may get stuck without a bomb. These problems are planned to be fixed. * Logic getting to Skull X room may be wrong if a trap door, big key door, or bombable wall is shuffled there. A bomb jump to get to those pot may be required if you don't have boots to bonk across.

17
Rom.py
View File

@@ -15,7 +15,7 @@ try:
except ImportError: except ImportError:
raise Exception('Could not load BPS module') raise Exception('Could not load BPS module')
from BaseClasses import ShopType, Region, Location, Door, DoorType, RegionType, LocationType, Item from BaseClasses import ShopType, Region, Location, Door, DoorType, RegionType, LocationType
from DoorShuffle import compass_data, DROptions, boss_indicator, dungeon_portals from DoorShuffle import compass_data, DROptions, boss_indicator, dungeon_portals
from Dungeons import dungeon_music_addresses, dungeon_table from Dungeons import dungeon_music_addresses, dungeon_table
from Regions import location_table, shop_to_location_table, retro_shops from Regions import location_table, shop_to_location_table, retro_shops
@@ -37,7 +37,7 @@ from source.dungeon.RoomList import Room0127
JAP10HASH = '03a63945398191337e896e5771f77173' JAP10HASH = '03a63945398191337e896e5771f77173'
RANDOMIZERBASEHASH = 'b6fcbc0d61faffa178135545f18fadbd' RANDOMIZERBASEHASH = 'f204143853a58e55a5fbc4c5bc87045e'
class JsonRom(object): class JsonRom(object):
@@ -730,7 +730,8 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
dr_flags = DROptions.Eternal_Mini_Bosses if world.doorShuffle[player] == 'vanilla' else DROptions.Town_Portal dr_flags = DROptions.Eternal_Mini_Bosses if world.doorShuffle[player] == 'vanilla' else DROptions.Town_Portal
if world.doorShuffle[player] not in ['vanilla', 'basic']: if world.doorShuffle[player] not in ['vanilla', 'basic']:
dr_flags |= DROptions.Map_Info dr_flags |= DROptions.Map_Info
if world.collection_rate[player] and world.goal[player] not in ['triforcehunt', 'trinity']: if ((world.collection_rate[player] or world.goal[player] == 'completionist')
and world.goal[player] not in ['triforcehunt', 'trinity', 'ganonhunt']):
dr_flags |= DROptions.Debug dr_flags |= DROptions.Debug
if world.doorShuffle[player] not in ['vanilla', 'basic'] and world.logic[player] != 'nologic'\ if world.doorShuffle[player] not in ['vanilla', 'basic'] and world.logic[player] != 'nologic'\
and world.mixed_travel[player] == 'prevent': and world.mixed_travel[player] == 'prevent':
@@ -1275,7 +1276,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
# set up goals for treasure hunt # set up goals for treasure hunt
rom.write_bytes(0x180165, [0x0E, 0x28] if world.treasure_hunt_icon[player] == 'Triforce Piece' else [0x0D, 0x28]) rom.write_bytes(0x180165, [0x0E, 0x28] if world.treasure_hunt_icon[player] == 'Triforce Piece' else [0x0D, 0x28])
if world.goal[player] in ['triforcehunt', 'trinity']: if world.goal[player] in ['triforcehunt', 'trinity', 'ganonhunt']:
rom.write_bytes(0x180167, int16_as_bytes(world.treasure_hunt_count[player])) rom.write_bytes(0x180167, int16_as_bytes(world.treasure_hunt_count[player]))
rom.write_byte(0x180194, 1) # Must turn in triforced pieces (instant win not enabled) rom.write_byte(0x180194, 1) # Must turn in triforced pieces (instant win not enabled)
@@ -1340,6 +1341,10 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
rom.write_byte(0x18003E, 0x02) # make ganon invincible until all dungeons are beat rom.write_byte(0x18003E, 0x02) # make ganon invincible until all dungeons are beat
elif world.goal[player] in ['crystals', 'trinity']: elif world.goal[player] in ['crystals', 'trinity']:
rom.write_byte(0x18003E, 0x04) # make ganon invincible until all crystals rom.write_byte(0x18003E, 0x04) # make ganon invincible until all crystals
elif world.goal[player] in ['ganonhunt']:
rom.write_byte(0x18003E, 0x05) # make ganon invincible until all triforce pieces collected
elif world.goal[player] in ['completionist']:
rom.write_byte(0x18003E, 0x0a) # make ganon invincible until everything is collected
else: else:
rom.write_byte(0x18003E, 0x03) # make ganon invincible until all crystals and aga 2 are collected rom.write_byte(0x18003E, 0x03) # make ganon invincible until all crystals and aga 2 are collected
@@ -2359,6 +2364,10 @@ def write_strings(rom, world, player, team):
trinity_crystal_text = ('%d crystal to beat Ganon.' if world.crystals_needed_for_ganon[player] == 1 else '%d crystals to beat Ganon.') % world.crystals_needed_for_ganon[player] trinity_crystal_text = ('%d crystal to beat Ganon.' if world.crystals_needed_for_ganon[player] == 1 else '%d crystals to beat Ganon.') % world.crystals_needed_for_ganon[player]
tt['sign_ganon'] = 'Three ways to victory! %s Get to it!' % trinity_crystal_text tt['sign_ganon'] = 'Three ways to victory! %s Get to it!' % trinity_crystal_text
tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\ninvisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\nhidden in a hollow tree. If you bring\n%d triforce pieces, I can reassemble it." % int(world.treasure_hunt_count[player]) tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\ninvisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\nhidden in a hollow tree. If you bring\n%d triforce pieces, I can reassemble it." % int(world.treasure_hunt_count[player])
elif world.goal[player] == 'ganonhunt':
tt['sign_ganon'] = 'Go find the Triforce pieces to beat Ganon'
elif world.goal[player] == 'completionist':
tt['sign_ganon'] = 'Ganon only respects those who have done everything'
tt['ganon_fall_in'] = Ganon1_texts[random.randint(0, len(Ganon1_texts) - 1)] tt['ganon_fall_in'] = Ganon1_texts[random.randint(0, len(Ganon1_texts) - 1)]
tt['ganon_fall_in_alt'] = 'You cannot defeat me until you finish your goal!' tt['ganon_fall_in_alt'] = 'You cannot defeat me until you finish your goal!'
tt['ganon_phase_3_alt'] = 'Got wax in\nyour ears?\nI can not die!' tt['ganon_phase_3_alt'] = 'Got wax in\nyour ears?\nI can not die!'

View File

@@ -59,6 +59,10 @@ def set_rules(world, player):
add_rule(world.get_location('Ganon', player), lambda state: state.has('Beat Agahnim 2', player)) add_rule(world.get_location('Ganon', player), lambda state: state.has('Beat Agahnim 2', player))
elif world.goal[player] in ['triforcehunt', 'trinity']: elif world.goal[player] in ['triforcehunt', 'trinity']:
add_rule(world.get_location('Murahdahla', player), lambda state: state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= int(state.world.treasure_hunt_count[player])) add_rule(world.get_location('Murahdahla', player), lambda state: state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= int(state.world.treasure_hunt_count[player]))
elif world.goal[player] == 'ganonhunt':
add_rule(world.get_location('Ganon', player), lambda state: state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= int(state.world.treasure_hunt_count[player]))
elif world.goal[player] == 'completionist':
add_rule(world.get_location('Ganon', player), lambda state: state.everything(player))
if world.mode[player] != 'inverted': if world.mode[player] != 'inverted':
set_big_bomb_rules(world, player) set_big_bomb_rules(world, player)
@@ -2042,7 +2046,8 @@ def add_key_logic_rules(world, player):
for chest in d_logic.bk_chests: for chest in d_logic.bk_chests:
big_chest = world.get_location(chest.name, player) big_chest = world.get_location(chest.name, player)
add_rule(big_chest, create_rule(d_logic.bk_name, player)) add_rule(big_chest, create_rule(d_logic.bk_name, player))
if len(d_logic.bk_doors) == 0 and len(d_logic.bk_chests) <= 1: if (len(d_logic.bk_doors) == 0 and len(d_logic.bk_chests) <= 1
and world.accessibility[player] != 'locations'):
set_always_allow(big_chest, allow_big_key_in_big_chest(d_logic.bk_name, player)) set_always_allow(big_chest, allow_big_key_in_big_chest(d_logic.bk_name, player))
if world.keyshuffle[player] == 'universal': if world.keyshuffle[player] == 'universal':
for d_name, layout in world.key_layout[player].items(): for d_name, layout in world.key_layout[player].items():

Binary file not shown.

View File

@@ -82,6 +82,8 @@
pedestal: 2 pedestal: 2
triforce-hunt: 2 triforce-hunt: 2
trinity: 2 trinity: 2
ganonhunt: 2
completionist: 1
triforce_goal_min: 10 triforce_goal_min: 10
triforce_goal_max: 30 triforce_goal_max: 30
triforce_pool_min: 20 triforce_pool_min: 20
@@ -95,7 +97,7 @@
mcu: 1 # map, compass, universal smalls mcu: 1 # map, compass, universal smalls
# for use when you aren't using the dungeon_items above # for use when you aren't using the dungeon_items above
# smallkey_shuffle: # smallkey_shuffle:
# standard: 5 # none: 5
# wild: 1 # wild: 1
# universal: 1 # universal: 1
dungeon_counters: dungeon_counters:

View File

@@ -67,7 +67,9 @@
"dungeons", "dungeons",
"triforcehunt", "triforcehunt",
"trinity", "trinity",
"crystals" "crystals",
"ganonhunt",
"completionist"
] ]
}, },
"difficulty": { "difficulty": {

View File

@@ -107,7 +107,10 @@
"Triforce Hunt: Places 30 Triforce Pieces in the world, collect", "Triforce Hunt: Places 30 Triforce Pieces in the world, collect",
" 20 of them to beat the game.", " 20 of them to beat the game.",
"Trinity: Can beat the game by defeating Ganon, pulling", "Trinity: Can beat the game by defeating Ganon, pulling",
" Pedestal, or delivering Triforce Pieces." " Pedestal, or delivering Triforce Pieces.",
"Ganon Hunt: Places 30 Triforce Pieces in the world, collect",
" 20 of them then defeat Ganon.",
"Completionist: Find everything then defeat Ganon."
], ],
"difficulty": [ "difficulty": [
"Select game difficulty. Affects available itempool. (default: %(default)s)", "Select game difficulty. Affects available itempool. (default: %(default)s)",

View File

@@ -248,6 +248,8 @@
"randomizer.item.goal.triforcehunt": "Triforce Hunt", "randomizer.item.goal.triforcehunt": "Triforce Hunt",
"randomizer.item.goal.trinity": "Trinity", "randomizer.item.goal.trinity": "Trinity",
"randomizer.item.goal.crystals": "Crystals", "randomizer.item.goal.crystals": "Crystals",
"randomizer.item.goal.ganonhunt": "Triforce Hunt + Ganon",
"randomizer.item.goal.completionist": "Completionist",
"randomizer.item.crystals_gt": "Crystals to open GT", "randomizer.item.crystals_gt": "Crystals to open GT",
"randomizer.item.crystals_gt.0": "0", "randomizer.item.crystals_gt.0": "0",

View File

@@ -36,7 +36,9 @@
"dungeons", "dungeons",
"triforcehunt", "triforcehunt",
"trinity", "trinity",
"crystals" "crystals",
"ganonhunt",
"completionist"
] ]
}, },
"crystals_gt": { "crystals_gt": {

View File

@@ -218,7 +218,6 @@ def modify_proposal(proposed_map, explored_state, doors_to_connect, hash_code_se
else: else:
logger.debug(f' Re-Linking {attempt.name} -> {new_door.name}') logger.debug(f' Re-Linking {attempt.name} -> {new_door.name}')
logger.debug(f' Re-Linking {old_attempt.name} -> {old_target.name}') logger.debug(f' Re-Linking {old_attempt.name} -> {old_target.name}')
hash_code_set.add(hash_code)
return proposed_map, hash_code return proposed_map, hash_code

View File

@@ -189,26 +189,27 @@ def district_item_pool_config(world):
if district.dungeon: if district.dungeon:
adjustment = len([i for i in world.get_dungeon(name, p).all_items adjustment = len([i for i in world.get_dungeon(name, p).all_items
if i.is_inside_dungeon_item(world)]) if i.is_inside_dungeon_item(world)])
dist_len = len(district.locations) - adjustment dist_adj = adjustment
if name not in district_choices: if name not in district_choices:
district_choices[name] = (district.sphere_one, dist_len) district_choices[name] = (district.sphere_one, dist_adj)
else: else:
so, amt = district_choices[name] so, amt = district_choices[name]
district_choices[name] = (so or district.sphere_one, amt + dist_len) district_choices[name] = (so or district.sphere_one, amt + dist_adj)
chosen_locations = defaultdict(set) chosen_locations = defaultdict(set)
location_cnt = 0 adjustment_cnt = 0
# choose a sphere one district # choose a sphere one district
sphere_one_choices = [d for d, info in district_choices.items() if info[0]] sphere_one_choices = [d for d, info in district_choices.items() if info[0]]
sphere_one = random.choice(sphere_one_choices) sphere_one = random.choice(sphere_one_choices)
so, amt = district_choices[sphere_one] so, adj = district_choices[sphere_one]
location_cnt += amt
for player in range(1, world.players + 1): for player in range(1, world.players + 1):
for location in world.districts[player][sphere_one].locations: for location in world.districts[player][sphere_one].locations:
chosen_locations[location].add(player) chosen_locations[location].add(player)
del district_choices[sphere_one] del district_choices[sphere_one]
config.recorded_choices.append(sphere_one) config.recorded_choices.append(sphere_one)
adjustment_cnt += adj
location_cnt = len(chosen_locations) - adjustment_cnt
scale_factors = defaultdict(int) scale_factors = defaultdict(int)
scale_total = 0 scale_total = 0
@@ -217,8 +218,9 @@ def district_item_pool_config(world):
dungeon = world.get_entrance(ent, p).connected_region.dungeon dungeon = world.get_entrance(ent, p).connected_region.dungeon
if dungeon: if dungeon:
scale = world.crystals_needed_for_gt[p] scale = world.crystals_needed_for_gt[p]
scale_total += scale if scale > 0:
scale_factors[dungeon.name] += scale scale_total += scale
scale_factors[dungeon.name] += scale
scale_total = max(1, scale_total) scale_total = max(1, scale_total)
scale_divisors = defaultdict(lambda: 1) scale_divisors = defaultdict(lambda: 1)
scale_divisors.update(scale_factors) scale_divisors.update(scale_factors)
@@ -226,13 +228,15 @@ def district_item_pool_config(world):
while location_cnt < item_cnt: while location_cnt < item_cnt:
weights = [scale_total / scale_divisors[d] for d in district_choices.keys()] weights = [scale_total / scale_divisors[d] for d in district_choices.keys()]
choice = random.choices(list(district_choices.keys()), weights=weights, k=1)[0] choice = random.choices(list(district_choices.keys()), weights=weights, k=1)[0]
so, amt = district_choices[choice] so, adj = district_choices[choice]
location_cnt += amt
for player in range(1, world.players + 1): for player in range(1, world.players + 1):
for location in world.districts[player][choice].locations: for location in world.districts[player][choice].locations:
chosen_locations[location].add(player) chosen_locations[location].add(player)
del district_choices[choice] del district_choices[choice]
config.recorded_choices.append(choice) config.recorded_choices.append(choice)
adjustment_cnt += adj
location_cnt = len(chosen_locations) - adjustment_cnt
config.placeholders = location_cnt - item_cnt config.placeholders = location_cnt - item_cnt
config.location_groups[0].locations = chosen_locations config.location_groups[0].locations = chosen_locations
@@ -383,7 +387,10 @@ def vanilla_fallback(item_to_place, locations, world):
def filter_locations(item_to_place, locations, world, vanilla_skip=False, potion=False): def filter_locations(item_to_place, locations, world, vanilla_skip=False, potion=False):
config = world.item_pool_config config = world.item_pool_config
item_name = 'Bottle' if item_to_place.name.startswith('Bottle') else item_to_place.name if not isinstance(item_to_place, str):
item_name = 'Bottle' if item_to_place.name.startswith('Bottle') else item_to_place.name
else:
item_name = item_to_place
if world.algorithm == 'vanilla_fill': if world.algorithm == 'vanilla_fill':
filtered = [] filtered = []
item_name = 'Bottle' if item_to_place.name.startswith('Bottle') else item_to_place.name item_name = 'Bottle' if item_to_place.name.startswith('Bottle') else item_to_place.name
@@ -443,7 +450,6 @@ def filter_pot_locations(locations, world):
return locations return locations
vanilla_mapping = { vanilla_mapping = {
'Green Pendant': ['Eastern Palace - Prize'], 'Green Pendant': ['Eastern Palace - Prize'],
'Red Pendant': ['Desert Palace - Prize', 'Tower of Hera - Prize'], 'Red Pendant': ['Desert Palace - Prize', 'Tower of Hera - Prize'],

View File

@@ -776,12 +776,19 @@ def do_vanilla_connect(pool_def, avail):
if avail.world.pottery[avail.player] not in ['none', 'keys', 'dungeon']: if avail.world.pottery[avail.player] not in ['none', 'keys', 'dungeon']:
return return
defaults = {**default_connections, **(inverted_default_connections if avail.inverted else open_default_connections)} defaults = {**default_connections, **(inverted_default_connections if avail.inverted else open_default_connections)}
if avail.inverted:
if 'Dark Death Mountain Fairy' in pool_def['entrances']:
pool_def['entrances'].remove('Dark Death Mountain Fairy')
pool_def['entrances'].append('Bumper Cave (top)')
for entrance in pool_def['entrances']: for entrance in pool_def['entrances']:
if entrance in avail.entrances: if entrance in avail.entrances:
target = defaults[entrance] target = defaults[entrance]
connect_simple(avail.world, entrance, target, avail.player) if entrance in avail.default_map:
avail.entrances.remove(entrance) connect_vanilla_two_way(entrance, avail.default_map[entrance], avail)
avail.exits.remove(target) else:
connect_simple(avail.world, entrance, target, avail.player)
avail.entrances.remove(entrance)
avail.exits.remove(target)
def do_mandatory_connections(avail, entrances, cave_options, must_exit): def do_mandatory_connections(avail, entrances, cave_options, must_exit):
@@ -1223,7 +1230,8 @@ modes = {
'Lumberjack House', 'Snitch Lady (West)', 'Snitch Lady (East)', 'Tavern (Front)', 'Lumberjack House', 'Snitch Lady (West)', 'Snitch Lady (East)', 'Tavern (Front)',
'Light World Bomb Hut', '20 Rupee Cave', '50 Rupee Cave', 'Hookshot Fairy', 'Light World Bomb Hut', '20 Rupee Cave', '50 Rupee Cave', 'Hookshot Fairy',
'Palace of Darkness Hint', 'Dark Lake Hylia Ledge Spike Cave', 'Palace of Darkness Hint', 'Dark Lake Hylia Ledge Spike Cave',
'Dark Desert Hint', 'Links House', 'Tavern North'] 'Dark Desert Hint',
'Links House', 'Tavern North']
}, },
'old_man_cave': { # have to do old man cave first so lw dungeon don't use up everything 'old_man_cave': { # have to do old man cave first so lw dungeon don't use up everything
'special': 'old_man_cave_east', 'special': 'old_man_cave_east',
@@ -1298,7 +1306,8 @@ modes = {
'Lumberjack House', 'Snitch Lady (West)', 'Snitch Lady (East)', 'Tavern (Front)', 'Lumberjack House', 'Snitch Lady (West)', 'Snitch Lady (East)', 'Tavern (Front)',
'Light World Bomb Hut', '20 Rupee Cave', '50 Rupee Cave', 'Hookshot Fairy', 'Light World Bomb Hut', '20 Rupee Cave', '50 Rupee Cave', 'Hookshot Fairy',
'Palace of Darkness Hint', 'Dark Lake Hylia Ledge Spike Cave', 'Palace of Darkness Hint', 'Dark Lake Hylia Ledge Spike Cave',
'Dark Desert Hint', 'Links House', 'Tavern North'] 'Dark Desert Hint',
'Links House', 'Tavern North'] # inverted links house gets substituted
} }
} }
}, },
@@ -1562,7 +1571,8 @@ entrance_map = {
'Spectacle Rock Cave': 'Spectacle Rock Cave Exit (Top)', 'Spectacle Rock Cave': 'Spectacle Rock Cave Exit (Top)',
'Paradox Cave (Bottom)': 'Paradox Cave Exit (Bottom)', 'Paradox Cave (Bottom)': 'Paradox Cave Exit (Bottom)',
'Paradox Cave (Middle)': 'Paradox Cave Exit (Middle)', 'Paradox Cave (Middle)': 'Paradox Cave Exit (Middle)',
'Paradox Cave (Top)': 'Paradox Cave Exit (Top)' 'Paradox Cave (Top)': 'Paradox Cave Exit (Top)',
'Inverted Dark Sanctuary': 'Inverted Dark Sanctuary Exit',
} }

View File

@@ -113,7 +113,9 @@ def roll_settings(weights):
'dungeons': 'dungeons', 'dungeons': 'dungeons',
'pedestal': 'pedestal', 'pedestal': 'pedestal',
'triforce-hunt': 'triforcehunt', 'triforce-hunt': 'triforcehunt',
'trinity': 'trinity' 'trinity': 'trinity',
'ganonhunt': 'ganonhunt',
'completionist': 'completionist'
}[goal] }[goal]
ret.openpyramid = goal in ['fast_ganon', 'trinity'] if ret.shuffle in ['vanilla', 'dungeonsfull', 'dungeonssimple'] else False ret.openpyramid = goal in ['fast_ganon', 'trinity'] if ret.shuffle in ['vanilla', 'dungeonsfull', 'dungeonssimple'] else False
@@ -201,5 +203,6 @@ def roll_settings(weights):
ret.ow_palettes = get_choice('ow_palettes', romweights) ret.ow_palettes = get_choice('ow_palettes', romweights)
ret.uw_palettes = get_choice('uw_palettes', romweights) ret.uw_palettes = get_choice('uw_palettes', romweights)
ret.shuffle_sfx = get_choice('shuffle_sfx', romweights) == 'on' ret.shuffle_sfx = get_choice('shuffle_sfx', romweights) == 'on'
ret.msu_resume = get_choice('msu_resume', romweights) == 'on'
return ret return ret

View File

@@ -1,5 +1,7 @@
import os
import sys
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..'))
import RaceRandom as random import RaceRandom as random
import logging
import time import time
from collections import Counter, defaultdict from collections import Counter, defaultdict