diff --git a/BaseClasses.py b/BaseClasses.py index ce2e419e..fdf6bf54 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -128,7 +128,8 @@ class World(object): set_player_attr('can_access_trock_front', None) set_player_attr('can_access_trock_big_chest', 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 ['lean', '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('compassshuffle', False) set_player_attr('keyshuffle', 'none') @@ -174,7 +175,9 @@ class World(object): def finish_init(self): for player in range(1, self.players + 1): 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): return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})' @@ -547,7 +550,10 @@ class World(object): if self.has_beaten_game(state): 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: sphere = [] @@ -1129,6 +1135,12 @@ class CollectionState(object): def item_count(self, 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): 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 @@ -1260,7 +1272,7 @@ class CollectionState(object): def can_flute(self, player): if self.world.mode[player] == 'standard' and not self.has('Zelda Delivered', player): - return False + return False # can't flute in rain state if any(map(lambda i: i.name in ['Ocarina', 'Ocarina (Activated)'], self.world.precollected_items)): return True lw = self.world.get_region('Kakariko Area', player) @@ -3014,7 +3026,7 @@ class Spoiler(object): outfile.write('Mode:'.ljust(line_width) + '%s\n' % self.metadata['mode'][player]) outfile.write('Swords:'.ljust(line_width) + '%s\n' % self.metadata['weapons'][player]) outfile.write('Goal:'.ljust(line_width) + '%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:'.ljust(line_width) + '%s\n' % self.metadata['triforcegoal'][player]) outfile.write('Triforce Pieces Total:'.ljust(line_width) + '%s\n' % self.metadata['triforcepool'][player]) outfile.write('Crystals Required for GT:'.ljust(line_width) + '%s\n' % str(self.world.crystals_gt_orig[player])) @@ -3343,7 +3355,8 @@ world_mode = {"open": 0, "standard": 1, "inverted": 2} sword_mode = {"random": 0, "assured": 1, "swordless": 2, "vanilla": 3} # 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} func_mode = {"normal": 0, "hard": 1, "expert": 2} @@ -3391,7 +3404,7 @@ flutespot_mode = {"vanilla": 0, "balanced": 1, "random": 2} flute_mode = {'normal': 0, 'active': 1} keyshuffle_mode = {'none': 0, 'wild': 1, 'universal': 2} # reserved 8 modes? 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 # psuedoboots does not effect code diff --git a/DoorShuffle.py b/DoorShuffle.py index c047e9b4..1724dd0e 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -6,7 +6,7 @@ from enum import unique, Flag from typing import DefaultDict, Dict, List 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 Doors import reset_portals 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 source.dungeon.DungeonStitcher import GenerationException, generate_dungeon from source.dungeon.DungeonStitcher import ExplorationState as ExplorationState2 -# from DungeonGenerator import generate_dungeon -from DungeonGenerator import ExplorationState, convert_regions, pre_validate, determine_required_paths, drop_entrances +from DungeonGenerator import ExplorationState, convert_regions, determine_required_paths, drop_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 valid_region_to_explore 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 @@ -1895,6 +1894,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): + 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: ttl = 0 suggestion_map, small_map, flex_map = {}, {}, {} @@ -1921,27 +1921,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)) 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) - limit = min(max_keys, cand_len) + limit = min(max_keys, cand_len, max_computation) 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: suggested -= 1 - combo_size = ncr(len(builder.candidates.small), suggested + builder.key_drop_cnt) - suggestion_map[dungeon] = builder.key_doors_num = suggested + builder.key_drop_cnt + combo_size = ncr(len(builder.candidates.small), key_door_num) + suggestion_map[dungeon] = builder.key_doors_num = key_door_num remaining -= suggested + builder.key_drop_cnt builder.combo_size = combo_size flex_map[dungeon] = (limit - suggested) if suggested < limit else 0 for dungeon in pool: builder = world.dungeon_layouts[player][dungeon] 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], start_regions_map[dungeon], world, player) small_map[dungeon] = valid_doors actual_chest_keys = small_number - builder.key_drop_cnt if actual_chest_keys < suggestion_map[dungeon]: if total_adjustable: - builder.total_keys = actual_chest_keys + builder.total_keys = actual_chest_keys + builder.key_drop_cnt flex_map[dungeon] = 0 remaining += suggestion_map[dungeon] - actual_chest_keys suggestion_map[dungeon] = small_number @@ -1952,6 +1953,8 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, worl builder = queue.popleft() dungeon = builder.name increased = suggestion_map[dungeon] + 1 + if increased > max_computation: + continue builder.key_doors_num = increased valid_doors, small_number = find_valid_combination(builder, increased, start_regions_map[dungeon], world, player) @@ -1961,7 +1964,7 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, worl suggestion_map[dungeon] = increased flex_map[dungeon] -= 1 if total_adjustable: - builder.total_keys = actual_chest_keys + builder.total_keys = max(increased, builder.key_drop_cnt) if flex_map[dungeon] > 0: builder.combo_size = ncr(len(builder.candidates.small), builder.key_doors_num) queue.append(builder) @@ -2691,8 +2694,12 @@ def find_valid_bd_combination(builder, suggested, world, player): test = random.choice([True, False]) if test: bomb_doors_needed -= 1 + if bomb_doors_needed < 0: + bomb_doors_needed = 0 else: 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.extend(custom_bomb_doors) dash_pool = [x for x in bd_door_pool if x not in bomb_proposal] diff --git a/DungeonGenerator.py b/DungeonGenerator.py index b1ce5456..2140520e 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -12,7 +12,7 @@ from typing import List from BaseClasses import DoorType, Direction, CrystalBarrier, RegionType, Polarity, PolSlot, flooded_keys, Sector from BaseClasses import Hook, hook_from_door, Door 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 source.dungeon.DungeonStitcher import generate_dungeon_find_proposal @@ -860,7 +860,7 @@ class ExplorationState(object): self.crystal = exp_door.crystal 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: self.crystal = CrystalBarrier.Orange if self.crystal == CrystalBarrier.Either: @@ -881,7 +881,7 @@ class ExplorationState(object): self.ttl_locations += 1 if location not in self.found_locations: 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) if location.name in dungeon_events and location.name not in self.events: 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) reverse_d_map[sector] = key if world.mode[player] == 'standard': - current_dungeon = dungeon_map['Hyrule Castle'] - standard_stair_check(dungeon_map, current_dungeon, candidate_sectors, global_pole) + if 'Hyrule Castle' in dungeon_map: + 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} [dungeon_map.pop(key) for key in complete_dungeons.keys()] diff --git a/Fill.py b/Fill.py index 021888e8..7446f783 100644 --- a/Fill.py +++ b/Fill.py @@ -406,7 +406,7 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None else: max_trash = gt_count 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) else: gftower_trash_count = random.randint(0, scaled_trash) diff --git a/ItemList.py b/ItemList.py index b1f195af..dd15a1ad 100644 --- a/ItemList.py +++ b/ItemList.py @@ -182,8 +182,12 @@ def get_custom_array_key(item): 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'] - 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']): + if (world.difficulty[player] not in ['normal', 'hard', 'expert'] + 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') if world.timer in ['ohko', 'timed-ohko']: @@ -364,7 +368,7 @@ def generate_itempool(world, player): world.clock_mode = clock_mode 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]) world.treasure_hunt_count[player], world.treasure_hunt_total[player] = g, t world.treasure_hunt_icon[player] = 'Triforce Piece' @@ -978,14 +982,15 @@ def add_bonkdrop_contents(world, player): count -= 1 -def get_pool_core(world, player, progressive, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords, bombbag, - door_shuffle, logic, flute_activated): +def get_pool_core(world, player, progressive, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords, + bombbag, door_shuffle, logic, flute_activated): pool = [] placed_items = {} precollected_items = [] clock_mode = None - if treasure_hunt_total == 0 and goal in ['triforcehunt', 'trinity']: - treasure_hunt_total = 30 if goal == 'triforcehunt' else 10 + if goal in ['triforcehunt', 'trinity', 'ganonhunt']: + if treasure_hunt_total == 0: + treasure_hunt_total = 30 if goal in ['triforcehunt', 'ganonhunt'] else 10 # triforce pieces max out triforcepool = ['Triforce Piece'] * min(treasure_hunt_total, max_goal) @@ -1088,7 +1093,7 @@ def get_pool_core(world, player, progressive, shuffle, difficulty, treasure_hunt elif timer == 'timed-ohko': pool.extend(diff.timedohko) clock_mode = 'countdown-ohko' - if goal in ['triforcehunt', 'trinity']: + if goal in ['triforcehunt', 'trinity', 'ganonhunt']: pool.extend(triforcepool) for extra in diff.extras: @@ -1146,7 +1151,7 @@ def make_custom_item_pool(world, player, progressive, shuffle, difficulty, timer customitemarray["triforce"] = total_items_to_place # Triforce Pieces - if goal in ['triforcehunt', 'trinity']: + if goal in ['triforcehunt', 'trinity', 'ganonhunt']: g, t = set_default_triforce(goal, customitemarray["triforcepiecesgoal"], customitemarray["triforcepieces"]) customitemarray["triforcepiecesgoal"], customitemarray["triforcepieces"] = g, t @@ -1184,8 +1189,8 @@ def make_custom_item_pool(world, player, progressive, shuffle, difficulty, timer treasure_hunt_count = max(min(customitemarray["triforcepiecesgoal"], max_goal), 1) treasure_hunt_icon = 'Triforce Piece' # 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']) - and (customitemarray["triforce"] == 0)): + if ((customitemarray["triforcepieces"] < treasure_hunt_count) + and (goal in ['triforcehunt', 'trinity', 'ganonhunt']) and (customitemarray["triforce"] == 0)): extrapieces = treasure_hunt_count - customitemarray["triforcepieces"] pool.extend(['Triforce Piece'] * extrapieces) itemtotal = itemtotal + extrapieces @@ -1385,7 +1390,7 @@ def get_player_dungeon_item_pool(world, player): # location pool doesn't support larger values at this time def set_default_triforce(goal, custom_goal, custom_total): triforce_goal, triforce_total = 0, 0 - if goal == 'triforcehunt': + if goal in ['triforcehunt', 'ganonhunt']: triforce_goal, triforce_total = 20, 30 elif goal == 'trinity': triforce_goal, triforce_total = 8, 10 diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 01c76593..bb1eaaa6 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -1445,7 +1445,7 @@ def validate_bk_layout(proposal, builder, start_regions, world, player): if loc.forced_big_key(): return True else: - return len(state.bk_found) > 0 + return state.count_locations_exclude_specials(world, player) > 0 return False diff --git a/Main.py b/Main.py index ca8f6856..9f4ef4b6 100644 --- a/Main.py +++ b/Main.py @@ -35,7 +35,7 @@ from source.overworld.EntranceShuffle2 import link_entrances_new from source.tools.BPS import create_bps_from_data from source.classes.CustomSettings import CustomSettings -__version__ = '1.2.0.0-u' +__version__ = '1.2.0.1-u' from source.classes.BabelFish import BabelFish @@ -786,7 +786,8 @@ def create_playthrough(world): world = copy_world(world) # 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.extend(['Hyrule Castle Courtyard Tree Pull', 'Mountain Entry Area Tree Pull']) # adding pre-aga tree pulls optional_locations.extend(['Lumberjack Area Crab Drop', 'South Pass Area Crab Drop']) # adding pre-aga bush crabs @@ -825,6 +826,8 @@ def create_playthrough(world): for num, sphere in reversed(list(enumerate(collection_spheres))): to_delete = set() 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 logging.getLogger('').debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, location.item.player) old_item = location.item diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1905b64f..865e3c41 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -53,7 +53,16 @@ This is similar to insanity mode in ER where door entrances and exits are not pa ## 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 @@ -100,10 +109,15 @@ These are now independent of retro mode and have three options: None, Random, an # 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 -* 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. -* Logic getting to Skull X room may be wrong if a trap door, big key door, or bombable wall. A bomb jump to get to those pot may be required if you don't have boots to bonk across. \ No newline at end of file +* 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. \ No newline at end of file diff --git a/Rom.py b/Rom.py index 67c70be8..fdb2f0c1 100644 --- a/Rom.py +++ b/Rom.py @@ -15,7 +15,7 @@ try: except ImportError: raise Exception('Could not load BPS module') -from BaseClasses import ShopType, Region, Location, OWEdge, Door, DoorType, RegionType, LocationType, Item +from BaseClasses import ShopType, Region, Location, OWEdge, Door, DoorType, RegionType, LocationType from DoorShuffle import compass_data, DROptions, boss_indicator, dungeon_portals from Dungeons import dungeon_music_addresses, dungeon_table from Regions import location_table, shop_to_location_table, retro_shops @@ -38,7 +38,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '7a6619830c3521f2a09c4c64cd8d7d75' +RANDOMIZERBASEHASH = '1cdb34399467efa346b713db54d6cb02' class JsonRom(object): @@ -869,7 +869,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 if world.doorShuffle[player] not in ['vanilla', 'basic']: 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 if world.doorShuffle[player] not in ['vanilla', 'basic'] and world.logic[player] != 'nologic'\ and world.mixed_travel[player] == 'prevent': @@ -1339,7 +1340,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): # set up goals for treasure hunt 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_byte(0x180194, 1) # Must turn in triforced pieces (instant win not enabled) @@ -1411,6 +1412,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 elif world.goal[player] in ['crystals', 'trinity']: 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: rom.write_byte(0x18003E, 0x03) # make ganon invincible until all crystals and aga 2 are collected @@ -2479,6 +2484,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] 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]) + 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_alt'] = 'You cannot defeat me until you finish your goal!' tt['ganon_phase_3_alt'] = 'Got wax in\nyour ears?\nI can not die!' diff --git a/Rules.py b/Rules.py index 3321fa2d..7eb1353d 100644 --- a/Rules.py +++ b/Rules.py @@ -65,6 +65,10 @@ def set_rules(world, player): for location in world.get_region('Hyrule Castle Courtyard', player).locations: if location.name == 'Murahdahla': add_rule(location, 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 swamp and dam have not been moved we require mirror for swamp palace if not world.swamp_patch_required[player]: @@ -2090,7 +2094,8 @@ def add_key_logic_rules(world, player): for chest in d_logic.bk_chests: big_chest = world.get_location(chest.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)) if world.keyshuffle[player] == 'universal': for d_name, layout in world.key_layout[player].items(): diff --git a/data/base2current.bps b/data/base2current.bps index 26d183ab..ee0b630f 100644 Binary files a/data/base2current.bps and b/data/base2current.bps differ diff --git a/mystery_example.yml b/mystery_example.yml index 109cda02..fdb292bf 100644 --- a/mystery_example.yml +++ b/mystery_example.yml @@ -113,6 +113,8 @@ pedestal: 2 triforce-hunt: 2 trinity: 2 + ganonhunt: 2 + completionist: 1 triforce_goal_min: 10 triforce_goal_max: 30 triforce_pool_min: 20 @@ -136,7 +138,7 @@ mcu: 1 # map, compass, universal smalls # for use when you aren't using the dungeon_items above # smallkey_shuffle: -# standard: 5 +# none: 5 # wild: 1 # universal: 1 dungeon_counters: diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index f19b4b5f..97915be7 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -67,7 +67,9 @@ "dungeons", "triforcehunt", "trinity", - "crystals" + "crystals", + "ganonhunt", + "completionist" ] }, "difficulty": { diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index aad0f8eb..73e35b83 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -108,7 +108,10 @@ "Triforce Hunt: Places 30 Triforce Pieces in the world, collect", " 20 of them to beat the game.", "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": [ "Select game difficulty. Affects available itempool. (default: %(default)s)", diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index a0267825..939adf8c 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -279,6 +279,8 @@ "randomizer.item.goal.triforcehunt": "Triforce Hunt", "randomizer.item.goal.trinity": "Trinity", "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.0": "0", diff --git a/resources/app/gui/randomize/item/widgets.json b/resources/app/gui/randomize/item/widgets.json index 64546923..a2cde0ea 100644 --- a/resources/app/gui/randomize/item/widgets.json +++ b/resources/app/gui/randomize/item/widgets.json @@ -36,7 +36,9 @@ "dungeons", "triforcehunt", "trinity", - "crystals" + "crystals", + "ganonhunt", + "completionist" ] }, "crystals_gt": { diff --git a/source/dungeon/DungeonStitcher.py b/source/dungeon/DungeonStitcher.py index 0a0d3ad4..205941b5 100644 --- a/source/dungeon/DungeonStitcher.py +++ b/source/dungeon/DungeonStitcher.py @@ -218,7 +218,6 @@ def modify_proposal(proposed_map, explored_state, doors_to_connect, hash_code_se else: logger.debug(f' Re-Linking {attempt.name} -> {new_door.name}') logger.debug(f' Re-Linking {old_attempt.name} -> {old_target.name}') - hash_code_set.add(hash_code) return proposed_map, hash_code diff --git a/source/item/FillUtil.py b/source/item/FillUtil.py index 81eb7f93..dba7cb57 100644 --- a/source/item/FillUtil.py +++ b/source/item/FillUtil.py @@ -189,26 +189,27 @@ def district_item_pool_config(world): if district.dungeon: adjustment = len([i for i in world.get_dungeon(name, p).all_items if i.is_inside_dungeon_item(world)]) - dist_len = len(district.locations) - adjustment + dist_adj = adjustment if name not in district_choices: - district_choices[name] = (district.sphere_one, dist_len) + district_choices[name] = (district.sphere_one, dist_adj) else: 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) - location_cnt = 0 + adjustment_cnt = 0 # choose a sphere one district sphere_one_choices = [d for d, info in district_choices.items() if info[0]] sphere_one = random.choice(sphere_one_choices) - so, amt = district_choices[sphere_one] - location_cnt += amt + so, adj = district_choices[sphere_one] for player in range(1, world.players + 1): for location in world.districts[player][sphere_one].locations: chosen_locations[location].add(player) del district_choices[sphere_one] config.recorded_choices.append(sphere_one) + adjustment_cnt += adj + location_cnt = len(chosen_locations) - adjustment_cnt scale_factors = defaultdict(int) scale_total = 0 @@ -217,8 +218,9 @@ def district_item_pool_config(world): dungeon = world.get_entrance(ent, p).connected_region.dungeon if dungeon: scale = world.crystals_needed_for_gt[p] - scale_total += scale - scale_factors[dungeon.name] += scale + if scale > 0: + scale_total += scale + scale_factors[dungeon.name] += scale scale_total = max(1, scale_total) scale_divisors = defaultdict(lambda: 1) scale_divisors.update(scale_factors) @@ -226,13 +228,15 @@ def district_item_pool_config(world): while location_cnt < item_cnt: weights = [scale_total / scale_divisors[d] for d in district_choices.keys()] choice = random.choices(list(district_choices.keys()), weights=weights, k=1)[0] - so, amt = district_choices[choice] - location_cnt += amt + so, adj = district_choices[choice] + for player in range(1, world.players + 1): for location in world.districts[player][choice].locations: chosen_locations[location].add(player) del district_choices[choice] config.recorded_choices.append(choice) + adjustment_cnt += adj + location_cnt = len(chosen_locations) - adjustment_cnt config.placeholders = location_cnt - item_cnt 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): 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': filtered = [] 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 - vanilla_mapping = { 'Green Pendant': ['Eastern Palace - Prize'], 'Red Pendant': ['Desert Palace - Prize', 'Tower of Hera - Prize'], diff --git a/source/overworld/EntranceShuffle2.py b/source/overworld/EntranceShuffle2.py index 6760b480..b9bea9cd 100644 --- a/source/overworld/EntranceShuffle2.py +++ b/source/overworld/EntranceShuffle2.py @@ -774,12 +774,19 @@ def do_vanilla_connect(pool_def, avail): if avail.world.pottery[avail.player] not in ['none', 'keys', 'dungeon']: return defaults = inverted_default_connections if avail.inverted else 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']: if entrance in avail.entrances: target = defaults[entrance] - connect_simple(avail.world, entrance, target, avail.player) - avail.entrances.remove(entrance) - avail.exits.remove(target) + if entrance in avail.default_map: + connect_vanilla_two_way(entrance, avail.default_map[entrance], avail) + 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): @@ -1228,7 +1235,7 @@ modes = { 'Light World Bomb Hut', '20 Rupee Cave', '50 Rupee Cave', 'Hookshot Fairy', 'Palace of Darkness Hint', 'Dark Lake Hylia Ledge Spike Cave', 'Dark Desert Hint', - 'Links House', 'Inverted Links House', 'Tavern North'] + 'Links House', 'Tavern North'] }, 'old_man_cave': { # have to do old man cave first so lw dungeon don't use up everything 'special': 'old_man_cave_east', @@ -1304,7 +1311,7 @@ modes = { 'Light World Bomb Hut', '20 Rupee Cave', '50 Rupee Cave', 'Hookshot Fairy', 'Palace of Darkness Hint', 'Dark Lake Hylia Ledge Spike Cave', 'Dark Desert Hint', - 'Links House', 'Inverted Links House', 'Tavern North'] + 'Links House', 'Tavern North'] # inverted links house gets substituted } } }, @@ -1572,6 +1579,7 @@ entrance_map = { 'Paradox Cave (Bottom)': 'Paradox Cave Exit (Bottom)', 'Paradox Cave (Middle)': 'Paradox Cave Exit (Middle)', 'Paradox Cave (Top)': 'Paradox Cave Exit (Top)', + 'Inverted Dark Sanctuary': 'Inverted Dark Sanctuary Exit', } @@ -1607,7 +1615,7 @@ single_entrance_map = { 'Lake Hylia Fortune Teller': 'Lake Hylia Fortune Teller', 'Lake Hylia Fairy': 'Lake Hylia Healer Fairy', 'Bonk Fairy (Light)': 'Bonk Fairy (Light)', 'Lumberjack House': 'Lumberjack House', 'Dam': 'Dam', 'Blinds Hideout': 'Blinds Hideout', 'Waterfall of Wishing': 'Waterfall of Wishing', - 'Inverted Bomb Shop': 'Inverted Bomb Shop', 'Inverted Dark Sanctuary': 'Inverted Dark Sanctuary', + 'Inverted Bomb Shop': 'Inverted Bomb Shop' } default_dw = { diff --git a/source/tools/MysteryUtils.py b/source/tools/MysteryUtils.py index 046a98c9..1ebc467d 100644 --- a/source/tools/MysteryUtils.py +++ b/source/tools/MysteryUtils.py @@ -122,7 +122,9 @@ def roll_settings(weights): 'dungeons': 'dungeons', 'pedestal': 'pedestal', 'triforce-hunt': 'triforcehunt', - 'trinity': 'trinity' + 'trinity': 'trinity', + 'ganonhunt': 'ganonhunt', + 'completionist': 'completionist' }[goal] ret.openpyramid = get_choice('open_pyramid') if 'open_pyramid' in weights else 'auto' @@ -215,5 +217,6 @@ def roll_settings(weights): ret.ow_palettes = get_choice('ow_palettes', romweights) ret.uw_palettes = get_choice('uw_palettes', romweights) ret.shuffle_sfx = get_choice('shuffle_sfx', romweights) == 'on' + ret.msu_resume = get_choice('msu_resume', romweights) == 'on' return ret diff --git a/test/stats/EntranceShuffleStats.py b/test/stats/EntranceShuffleStats.py index 0ba45b97..130cdb8b 100644 --- a/test/stats/EntranceShuffleStats.py +++ b/test/stats/EntranceShuffleStats.py @@ -1,5 +1,7 @@ +import os +import sys +sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) import RaceRandom as random -import logging import time from collections import Counter, defaultdict @@ -99,7 +101,7 @@ def test_loop(tests, entrance_set, exit_set, ctr, shuffle_mode, main_mode, links # seed = 635441530 random.seed(seed) world = World(1, {1: shuffle_mode}, {1: 'vanilla'}, {1: 'noglitches'}, {1: main_mode}, {}, {}, {}, - {}, {}, {}, {}, {}, True, {}, {}, [], {}) + {}, {}, {}, {}, {}, True, {}, [], {}) world.customizer = False world.shufflelinks = {1: links} create_regions(world, 1)