From b0f4fa8cec3fdd61c5efb29a4b5b3e4176e620de Mon Sep 17 00:00:00 2001 From: Kevin Cathcart Date: Sun, 4 Aug 2019 12:32:35 -0400 Subject: [PATCH] Partial implementation of many V31 features Partial support for Progressive bow - Still needs to be added to item pool - Silver hint handling remains TBD even for VT Added weapons selection. - Vanilla needs to be implemented - Assured needs to be implemented - Inverted swordless is almost certainly messed up. - Swordless standard mode will likely softlock - Random weapon standard mode is currently treated as uncle assured Deleted removed difficulties - Remaining difficulties still need to be adjusted Added locked property to locations: - This is used for preplaced items etc so that multiworld balancing knows they cannot be moved. Made a few of the difficulty changes from V31, but not all. Added required text changes to handle crystals requirements - More changes will likely me made in future - Currently there is is no way to tell ganon requirement in Inverted mode --- .gitignore | 1 + BaseClasses.py | 39 ++++++++++++- Bosses.py | 12 ++-- Dungeons.py | 21 ++++--- EntranceRandomizer.py | 26 +++++---- Fill.py | 5 +- Gui.py | 4 +- ItemList.py | 130 ++++++++++-------------------------------- Items.py | 1 + Main.py | 6 +- README.md | 13 +---- Rom.py | 75 +++++++----------------- Rules.py | 16 ++---- Text.py | 2 + _vendor/__init__.py | 0 15 files changed, 143 insertions(+), 208 deletions(-) create mode 100644 _vendor/__init__.py diff --git a/.gitignore b/.gitignore index 65c35fa8..c4ce894b 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ README.html *multidata *multisave EnemizerCLI/ +.mypy_cache/ \ No newline at end of file diff --git a/BaseClasses.py b/BaseClasses.py index 09fdba1d..7bca192d 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -8,11 +8,12 @@ from Utils import int16_as_bytes class World(object): - def __init__(self, players, shuffle, logic, mode, difficulty, timer, progressive, goal, algorithm, place_dungeon_items, check_beatable_only, shuffle_ganon, quickswap, fastmenu, disable_music, keysanity, retro, custom, customitemarray, boss_shuffle, hints): + def __init__(self, players, shuffle, logic, mode, swords, difficulty, timer, progressive, goal, algorithm, place_dungeon_items, check_beatable_only, shuffle_ganon, quickswap, fastmenu, disable_music, keysanity, retro, custom, customitemarray, boss_shuffle, hints): self.players = players self.shuffle = shuffle self.logic = logic self.mode = mode + self.swords = swords self.difficulty = difficulty self.timer = timer self.progressive = progressive @@ -161,6 +162,13 @@ class World(object): ret.prog_items.add(('Red Shield', item.player)) elif self.difficulty_requirements.progressive_shield_limit >= 1: ret.prog_items.add(('Blue Shield', item.player)) + elif 'Bow' in item.name: + if ret.has('Silver Arrows', item.player): + pass + elif ret.has('Bow', item.player): + ret.prog_items.add(('Silver Arrows', item.player)) + else: + ret.prog_items.add(('Bow', item.player)) elif item.name.startswith('Bottle'): if ret.bottle_count(item.player) < self.difficulty_requirements.progressive_bottle_limit: ret.prog_items.add((item.name, item.player)) @@ -409,8 +417,6 @@ class CollectionState(object): basemagic = basemagic + int(basemagic * 0.5 * self.bottle_count(player)) elif self.world.difficulty == 'expert' and not fullrefill: basemagic = basemagic + int(basemagic * 0.25 * self.bottle_count(player)) - elif self.world.difficulty == 'insane' and not fullrefill: - basemagic = basemagic else: basemagic = basemagic + basemagic * self.bottle_count(player) return basemagic >= smallmagic @@ -524,6 +530,15 @@ class CollectionState(object): elif self.world.difficulty_requirements.progressive_shield_limit >= 1: self.prog_items.add(('Blue Shield', item.player)) changed = True + elif 'Bow' in item.name: + if self.has('Silver Arrows', item.player): + pass + elif self.has('Bow', item.player): + self.prog_items.add(('Silver Arrows', item.player)) + changed = True + else: + self.prog_items.add(('Bow', item.player)) + changed = True elif item.name.startswith('Bottle'): if self.bottle_count(item.player) < self.world.difficulty_requirements.progressive_bottle_limit: self.prog_items.add((item.name, item.player)) @@ -560,6 +575,22 @@ class CollectionState(object): to_remove = 'Power Glove' else: to_remove = None + elif 'Shield' in item.name: + if self.has('Mirror Shield', item.player): + to_remove = 'Mirror Shield' + elif self.has('Red Shield', item.player): + to_remove = 'Red Shield' + elif self.has('Blue Shield', item.player): + to_remove = 'Blue Shield' + else: + to_remove = 'None' + elif 'Bow' in item.name: + if self.has('Silver Arrows', item.player): + to_remove = 'Silver Arrows' + elif self.has('Bow', item.player): + to_remove = 'Bow' + else: + to_remove = None if to_remove is not None: try: @@ -742,6 +773,7 @@ class Location(object): self.recursion_count = 0 self.staleness_count = 0 self.event = False + self.locked = True self.always_allow = lambda item, state: False self.access_rule = lambda state: True self.item_rule = lambda item: True @@ -975,6 +1007,7 @@ class Spoiler(object): 'seed': self.world.seed, 'logic': self.world.logic, 'mode': self.world.mode, + 'swords': self.world.swords, 'goal': self.world.goal, 'shuffle': self.world.shuffle, 'algorithm': self.world.algorithm, diff --git a/Bosses.py b/Bosses.py index 43072966..8cdaee5a 100644 --- a/Bosses.py +++ b/Bosses.py @@ -72,18 +72,18 @@ def KholdstareDefeatRule(state, player): state.has('Fire Rod', player) or ( state.has('Bombos', player) and - # FIXME: the following only actually works for the vanilla location for swordless mode - (state.has_sword(player) or state.world.mode == 'swordless') + # FIXME: the following only actually works for the vanilla location for swordless + (state.has_sword(player) or state.world.swords == 'swordless') ) ) and ( state.has_blunt_weapon(player) or (state.has('Fire Rod', player) and state.can_extend_magic(player, 20)) or - # FIXME: this actually only works for the vanilla location for swordless mode + # FIXME: this actually only works for the vanilla location for swordless ( state.has('Fire Rod', player) and state.has('Bombos', player) and - state.world.mode == 'swordless' and + state.world.swords == 'swordless' and state.can_extend_magic(player, 16) ) ) @@ -116,7 +116,7 @@ boss_table = { } def can_place_boss(world, boss, dungeon_name, level=None): - if world.mode in ['swordless'] and boss == 'Kholdstare' and dungeon_name != 'Ice Palace': + if world.swords in ['swordless'] and boss == 'Kholdstare' and dungeon_name != 'Ice Palace': return False if dungeon_name == 'Ganons Tower' and level == 'top': @@ -161,7 +161,7 @@ def place_bosses(world, player): if world.boss_shuffle in ["basic", "normal"]: # temporary hack for swordless kholdstare: - if world.mode == 'swordless': + if world.swords == 'swordless': world.get_dungeon('Ice Palace', player).boss = BossFactory('Kholdstare', player) logging.getLogger('').debug('Placing boss Kholdstare at Ice Palace') boss_locations.remove(['Ice Palace', None]) diff --git a/Dungeons.py b/Dungeons.py index 12592af7..3e412f70 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -46,11 +46,13 @@ def fill_dungeons(world): all_state_base = world.get_all_state() for player in range(1, world.players + 1): + pinball_room = world.get_location('Skull Woods - Pinball Room', player) if world.retro: - world.push_item(world.get_location('Skull Woods - Pinball Room', player), ItemFactory('Small Key (Universal)', player), False) + world.push_item(pinball_room, ItemFactory('Small Key (Universal)', player), False) else: - world.push_item(world.get_location('Skull Woods - Pinball Room', player), ItemFactory('Small Key (Skull Woods)', player), False) - world.get_location('Skull Woods - Pinball Room', player).event = True + world.push_item(pinball_room, ItemFactory('Small Key (Skull Woods)', player), False) + pinball_room.event = True + pinball_room.locked = True dungeons = [(list(dungeon.regions), dungeon.big_key, list(dungeon.small_keys), list(dungeon.dungeon_items)) for dungeon in world.dungeons] @@ -77,6 +79,7 @@ def fill_dungeons(world): world.push_item(bk_location, big_key, False) bk_location.event = True + bk_location.locked = True dungeon_locations.remove(bk_location) big_key = None @@ -102,6 +105,7 @@ def fill_dungeons(world): world.push_item(sk_location, small_key, False) sk_location.event = True + sk_location.locked = True dungeon_locations.remove(sk_location) if small_keys: @@ -122,13 +126,14 @@ def fill_dungeons_restrictive(world, shuffled_locations): all_state_base = world.get_all_state() for player in range(1, world.players + 1): - skull_woods_big_chest = world.get_location('Skull Woods - Pinball Room', player) + pinball_room = world.get_location('Skull Woods - Pinball Room', player) if world.retro: - world.push_item(skull_woods_big_chest, ItemFactory('Small Key (Universal)', player), False) + world.push_item(pinball_room, ItemFactory('Small Key (Universal)', player), False) else: - world.push_item(skull_woods_big_chest, ItemFactory('Small Key (Skull Woods)', player), False) - skull_woods_big_chest.event = True - shuffled_locations.remove(skull_woods_big_chest) + world.push_item(pinball_room, ItemFactory('Small Key (Skull Woods)', player), False) + pinball_room.event = True + pinball_room.locked = True + shuffled_locations.remove(pinball_room) if world.keysanity: #in keysanity dungeon items are distributed as part of the normal item pool diff --git a/EntranceRandomizer.py b/EntranceRandomizer.py index 48127426..47efc450 100755 --- a/EntranceRandomizer.py +++ b/EntranceRandomizer.py @@ -28,7 +28,7 @@ def start(): No Logic: Distribute items without regard for item requirements. ''') - parser.add_argument('--mode', default='open', const='open', nargs='?', choices=['standard', 'open', 'swordless', 'inverted'], + parser.add_argument('--mode', default='open', const='open', nargs='?', choices=['standard', 'open', 'inverted'], help='''\ Select game mode. (default: %(default)s) Open: World starts with Zelda rescued. @@ -36,17 +36,25 @@ def start(): but may lead to weird rain state issues if you exit through the Hyrule Castle side exits before rescuing Zelda in a full shuffle. - Swordless: Like Open, but with no swords. Curtains in - Skull Woods and Agahnims Tower are removed, - Agahnim\'s Tower barrier can be destroyed with - hammer. Misery Mire and Turtle Rock can be opened - without a sword. Hammer damages Ganon. Ether and - Bombos Tablet can be activated with Hammer (and Book). Inverted: Starting locations are Dark Sanctuary in West Dark World or at Link's House, which is shuffled freely. Requires the moon pearl to be Link in the Light World instead of a bunny. ''') + parser.add_argument('--swords', default='random', const='random', nargs='?', choices= ['random', 'assured', 'swordless', 'vanilla'], + help='''\ + Select sword placement. (default: %(default)s) + Random: All swords placed randomly. + Assured: Start game with a sword already. + Swordless: No swords. Curtains in Skull Woods and Agahnim\'s + Tower are removed, Agahnim\'s Tower barrier can be + destroyed with hammer. Misery Mire and Turtle Rock + can be opened without a sword. Hammer damages Ganon. + Ether and Bombos Tablet can be activated with Hammer + (and Book). Bombos pads have been added in Ice + Palace, to allow for an alternative to firerod. + Vanilla: Swords are in vanilla locations. + ''') parser.add_argument('--goal', default='ganon', const='ganon', nargs='?', choices=['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'crystals'], help='''\ Select completion goal. (default: %(default)s) @@ -59,14 +67,12 @@ def start(): Triforce Hunt: Places 30 Triforce Pieces in the world, collect 20 of them to beat the game. ''') - parser.add_argument('--difficulty', default='normal', const='normal', nargs='?', choices=['easy', 'normal', 'hard', 'expert', 'insane'], + parser.add_argument('--difficulty', default='normal', const='normal', nargs='?', choices=['normal', 'hard', 'expert'], help='''\ Select game difficulty. Affects available itempool. (default: %(default)s) - Easy: An easy setting with extra equipment. Normal: Normal difficulty. Hard: A harder setting with less equipment and reduced health. Expert: A harder yet setting with minimum equipment and health. - Insane: A setting with the absolute minimum in equipment and no extra health. ''') parser.add_argument('--timer', default='none', const='normal', nargs='?', choices=['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'], help='''\ diff --git a/Fill.py b/Fill.py index 86e0a5a7..0e1a48cc 100644 --- a/Fill.py +++ b/Fill.py @@ -374,6 +374,9 @@ def balance_multiworld_progression(world): reducing_state.sweep_for_events(locations=locations_to_test) + if testing.locked: + continue + if world.has_beaten_game(balancing_state): if not world.has_beaten_game(reducing_state): items_to_replace.append(testing) @@ -383,7 +386,7 @@ def balance_multiworld_progression(world): items_to_replace.append(testing) replaced_items = False - locations_for_replacing = [l for l in checked_locations if not l.event] + locations_for_replacing = [l for l in checked_locations if not l.event and not l.locked] while locations_for_replacing and items_to_replace: new_location = locations_for_replacing.pop() old_location = items_to_replace.pop() diff --git a/Gui.py b/Gui.py index 078a3ee1..bcaa486e 100755 --- a/Gui.py +++ b/Gui.py @@ -145,7 +145,7 @@ def guiMain(args=None): modeFrame = Frame(drowDownFrame) modeVar = StringVar() modeVar.set('open') - modeOptionMenu = OptionMenu(modeFrame, modeVar, 'standard', 'open', 'swordless', 'inverted') + modeOptionMenu = OptionMenu(modeFrame, modeVar, 'standard', 'open', 'inverted') modeOptionMenu.pack(side=RIGHT) modeLabel = Label(modeFrame, text='Game Mode') modeLabel.pack(side=LEFT) @@ -169,7 +169,7 @@ def guiMain(args=None): difficultyFrame = Frame(drowDownFrame) difficultyVar = StringVar() difficultyVar.set('normal') - difficultyOptionMenu = OptionMenu(difficultyFrame, difficultyVar, 'easy', 'normal', 'hard', 'expert', 'insane') + difficultyOptionMenu = OptionMenu(difficultyFrame, difficultyVar, 'normal', 'hard', 'expert') difficultyOptionMenu.pack(side=RIGHT) difficultyLabel = Label(difficultyFrame, text='Game difficulty') difficultyLabel.pack(side=LEFT) diff --git a/ItemList.py b/ItemList.py index 629f2931..8b0ea681 100644 --- a/ItemList.py +++ b/ItemList.py @@ -29,17 +29,6 @@ normalthird10extra = ['Rupees (50)'] * 4 + ['Rupees (20)'] * 3 + ['Arrows (10)', normalfourth5extra = ['Arrows (10)'] * 2 + ['Rupees (20)'] * 2 + ['Rupees (5)'] normalfinal25extra = ['Rupees (20)'] * 23 + ['Rupees (5)'] * 2 - -easybaseitems = (['Sanctuary Heart Container'] + ['Rupees (300)'] * 4 + ['Magic Upgrade (1/2)'] * 2 + ['Lamp'] * 2 + ['Silver Arrows'] * 2 + - ['Boss Heart Container'] * 10 + ['Piece of Heart'] * 12) -easyextra = ['Piece of Heart'] * 12 + ['Rupees (300)'] -easylimitedextra = ['Boss Heart Container'] * 3 # collapsing down the 12 pieces of heart -easyfirst15extra = ['Rupees (100)'] + ['Arrows (10)'] * 7 + ['Bombs (3)'] * 7 -easysecond10extra = ['Bombs (3)'] * 7 + ['Rupee (1)', 'Rupees (50)', 'Bombs (10)'] -easythird5extra = ['Rupees (50)'] * 2 + ['Bombs (3)'] * 2 + ['Arrows (10)'] -easyfinal25extra = ['Rupees (50)'] * 4 + ['Rupees (20)'] * 14 + ['Rupee (1)'] + ['Arrows (10)'] * 4 + ['Rupees (5)'] * 2 -easytimedotherextra = ['Red Clock'] * 5 - hardbaseitems = ['Silver Arrows', 'Single Arrow', 'Bombs (10)'] + ['Rupees (300)'] * 4 + ['Boss Heart Container'] * 6 + ['Piece of Heart'] * 20 + ['Rupees (5)'] * 7 + ['Bombs (3)'] * 4 hardfirst20extra = ['Rupees (100)', 'Rupees (300)', 'Rupees (50)'] + ['Bombs (3)'] * 5 + ['Rupees (5)'] * 10 + ['Arrows (10)', 'Rupee (1)'] hardsecond10extra = ['Rupees (5)'] * 5 + ['Rupees (50)'] * 2 + ['Arrows (10)'] * 2 + ['Rupee (1)'] @@ -55,13 +44,6 @@ expertthird10extra = ['Rupees (50)'] * 4 + ['Rupees (5)'] * 2 + ['Arrows (10)'] expertfourth5extra = ['Rupees (5)'] * 5 expertfinal25extra = ['Rupees (20)'] * 23 + ['Rupees (5)'] * 2 -insanebaseitems = ['Rupees (300)'] * 4 + ['Single Arrow', 'Bombs (10)', 'Rupee (1)'] + ['Rupees (5)'] * 24 + ['Bombs (3)'] * 9 + ['Rupees (50)'] * 2 + ['Arrows (10)'] * 2 + ['Rupees (20)'] * 5 -insanefirst15extra = ['Rupees (100)', 'Rupees (300)', 'Rupees (50)'] + ['Rupees (5)'] * 12 -insanesecond15extra = ['Rupees (5)'] * 10 + ['Rupees (20)'] * 5 -insanethird10extra = ['Rupees (50)'] * 4 + ['Rupees (5)'] * 2 + ['Arrows (10)'] * 3 + ['Rupee (1)'] -insanefourth5extra = ['Rupees (5)'] * 5 -insanefinal25extra = ['Rupees (20)'] * 23 + ['Rupees (5)'] * 2 - Difficulty = namedtuple('Difficulty', ['baseitems', 'bottles', 'bottle_count', 'same_bottle', 'progressiveshield', 'basicshield', 'progressivearmor', 'basicarmor', 'swordless', @@ -72,14 +54,6 @@ Difficulty = namedtuple('Difficulty', total_items_to_place = 153 -def easy_conditional_extras(timer, _goal, _mode, pool, placed_items): - extraitems = total_items_to_place - len(pool) - len(placed_items) - if extraitems < len(easyextra): - return easylimitedextra - if timer in ['timed', 'timed-countdown']: - return easytimedotherextra - return [] - def no_conditional_extras(*_args): return [] @@ -109,30 +83,6 @@ difficulties = { progressive_armor_limit = 2, progressive_bottle_limit = 4, ), - 'easy': Difficulty( - baseitems = easybaseitems, - bottles = normalbottles, - bottle_count = 8, - same_bottle = False, - progressiveshield = ['Progressive Shield'] * 6, - basicshield = ['Blue Shield', 'Red Shield', 'Mirror Shield'] * 2, - progressivearmor = ['Progressive Armor'] * 4, - basicarmor = ['Blue Mail', 'Red Mail'] * 2, - swordless = ['Rupees (20)'] * 8, - progressivesword = ['Progressive Sword'] * 7, - basicsword = ['Master Sword', 'Tempered Sword', 'Golden Sword'] *2 + ['Fighter Sword'], - timedohko = ['Green Clock'] * 25, - timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 5, # +5 more Red Clocks if there is room - triforcehunt = ['Triforce Piece'] * 30, - triforce_pieces_required = 20, - retro = ['Small Key (Universal)'] * 27, - conditional_extras = easy_conditional_extras, - extras = [easyextra, easyfirst15extra, easysecond10extra, easythird5extra, easyfinal25extra], - progressive_sword_limit = 4, - progressive_shield_limit = 3, - progressive_armor_limit = 2, - progressive_bottle_limit = 4, - ), 'hard': Difficulty( baseitems = hardbaseitems, bottles = hardbottles, @@ -181,34 +131,10 @@ difficulties = { progressive_armor_limit = 0, progressive_bottle_limit = 4, ), - 'insane': Difficulty( - baseitems = insanebaseitems, - bottles = hardbottles, - bottle_count = 4, - same_bottle = False, - progressiveshield = [], - basicshield = [], - progressivearmor = [], - basicarmor = [], - swordless = ['Rupees (20)'] * 3 + ['Silver Arrows'], - progressivesword = ['Progressive Sword'] * 3, - basicsword = ['Fighter Sword', 'Master Sword', 'Master Sword'], - timedohko = ['Green Clock'] * 20 + ['Red Clock'] * 5, - timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10, - triforcehunt = ['Triforce Piece'] * 30, - triforce_pieces_required = 20, - retro = ['Small Key (Universal)'] * 12 + ['Rupees (5)'] * 15, - conditional_extras = no_conditional_extras, - extras = [insanefirst15extra, insanesecond15extra, insanethird10extra, insanefourth5extra, insanefinal25extra], - progressive_sword_limit = 2, - progressive_shield_limit = 0, - progressive_armor_limit = 0, - progressive_bottle_limit = 4, - ), } def generate_itempool(world, player): - if (world.difficulty not in ['easy', 'normal', 'hard', 'expert', 'insane'] or world.goal not in ['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'crystals'] + if (world.difficulty not in ['normal', 'hard', 'expert'] or world.goal not in ['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'crystals'] or world.mode not in ['open', 'standard', 'swordless', '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') @@ -217,29 +143,37 @@ def generate_itempool(world, player): world.push_item(world.get_location('Ganon', player), ItemFactory('Triforce', player), False) world.get_location('Ganon', player).event = True + world.get_location('Ganon', player).locked = True world.push_item(world.get_location('Agahnim 1', player), ItemFactory('Beat Agahnim 1', player), False) world.get_location('Agahnim 1', player).event = True + world.get_location('Agahnim 1', player).locked = True world.push_item(world.get_location('Agahnim 2', player), ItemFactory('Beat Agahnim 2', player), False) world.get_location('Agahnim 2', player).event = True + world.get_location('Agahnim 2', player).locked = True world.push_item(world.get_location('Dark Blacksmith Ruins', player), ItemFactory('Pick Up Purple Chest', player), False) world.get_location('Dark Blacksmith Ruins', player).event = True + world.get_location('Dark Blacksmith Ruins', player).locked = True world.push_item(world.get_location('Frog', player), ItemFactory('Get Frog', player), False) world.get_location('Frog', player).event = True + world.get_location('Frog', player).locked = True world.push_item(world.get_location('Missing Smith', player), ItemFactory('Return Smith', player), False) world.get_location('Missing Smith', player).event = True + world.get_location('Missing Smith', player).locked = True world.push_item(world.get_location('Floodgate', player), ItemFactory('Open Floodgate', player), False) world.get_location('Floodgate', player).event = True + world.get_location('Floodgate', player).locked = True # set up item pool if world.custom: - (pool, placed_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = make_custom_item_pool(world.progressive, world.shuffle, world.difficulty, world.timer, world.goal, world.mode, world.retro, world.customitemarray) + (pool, placed_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = make_custom_item_pool(world.progressive, world.shuffle, world.difficulty, world.timer, world.goal, world.mode, world.swords, world.retro, world.customitemarray) world.rupoor_cost = min(world.customitemarray[67], 9999) else: - (pool, placed_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = get_pool_core(world.progressive, world.shuffle, world.difficulty, world.timer, world.goal, world.mode, world.retro) + (pool, placed_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = get_pool_core(world.progressive, world.shuffle, world.difficulty, world.timer, world.goal, world.mode, world.swords, world.retro) world.itempool += ItemFactory(pool, player) for (location, item) in placed_items: world.push_item(world.get_location(location, player), ItemFactory(item, player), False) world.get_location(location, player).event = True + world.get_location(location, player).locked = True world.lamps_needed_for_dark_rooms = lamps_needed_for_dark_rooms if clock_mode is not None: world.clock_mode = clock_mode @@ -254,7 +188,7 @@ def generate_itempool(world, player): # logic has some branches where having 4 hearts is one possible requirement (of several alternatives) # rather than making all hearts/heart pieces progression items (which slows down generation considerably) # We mark one random heart container as an advancement item (or 4 heart pieces in expert mode) - if world.difficulty in ['easy', 'normal', 'hard'] and not (world.custom and world.customitemarray[30] == 0): + if world.difficulty in ['normal', 'hard'] and not (world.custom and world.customitemarray[30] == 0): [item for item in world.itempool if item.name == 'Boss Heart Container' and item.player == player][0].advancement = True elif world.difficulty in ['expert'] and not (world.custom and world.customitemarray[29] < 4): adv_heart_pieces = [item for item in world.itempool if item.name == 'Piece of Heart' and item.player == player][0:4] @@ -341,6 +275,7 @@ def create_dynamic_shop_locations(world, player): world.push_item(loc, ItemFactory(item['item'], player), False) loc.event = True + loc.locked = True def fill_prizes(world, attempts=15): @@ -393,7 +328,7 @@ def set_up_shops(world, player): #special shop types -def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, retro): +def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, retro): pool = [] placed_items = [] clock_mode = None @@ -411,8 +346,6 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, retro): pool.extend(basicgloves) lamps_needed_for_dark_rooms = 1 - if difficulty == 'easy': - lamps_needed_for_dark_rooms = 3 # insanity shuffle doesn't have fake LW/DW logic so for now guaranteed Mirror and Moon Pearl at the start if shuffle == 'insanity_legacy': @@ -448,7 +381,7 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, retro): else: pool.extend(diff.basicarmor) - if mode == 'swordless': + if swords == 'swordless': pool.extend(diff.swordless) elif mode == 'standard': if want_progressives(): @@ -505,7 +438,7 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, retro): pool.extend(['Small Key (Universal)']) return (pool, placed_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) -def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, retro, customitemarray): +def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, swords, retro, customitemarray): pool = [] placed_items = [] clock_mode = None @@ -589,8 +522,6 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, r diff = difficulties[difficulty] lamps_needed_for_dark_rooms = 1 - if difficulty == 'easy': - lamps_needed_for_dark_rooms = customitemarray[12] # expert+ difficulties produce the same contents for # all bottles, since only one bottle is available @@ -659,24 +590,25 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, r # A quick test to ensure all combinations generate the correct amount of items. def test(): - for difficulty in ['easy', 'normal', 'hard', 'expert', 'insane']: + for difficulty in ['normal', 'hard', 'expert']: for goal in ['ganon', 'triforcehunt', 'pedestal']: for timer in ['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown']: - for mode in ['open', 'standard', 'swordless', 'inverted']: - for progressive in ['on', 'off']: - for shuffle in ['full', 'insane']: - for retro in [True, False]: - out = get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, retro) - count = len(out[0]) + len(out[1]) + for mode in ['open', 'standard', 'inverted']: + for swords in ['random', 'assured', 'swordless', 'vanilla']: + for progressive in ['on', 'off']: + for shuffle in ['full', 'insanity_legacy']: + for retro in [True, False]: + out = get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, retro) + count = len(out[0]) + len(out[1]) - correct_count = total_items_to_place - if goal in ['pedestal']: - # pedestal goals generate one extra item - correct_count += 1 - if retro: - correct_count += 28 + correct_count = total_items_to_place + if goal in ['pedestal']: + # pedestal goals generate one extra item + correct_count += 1 + if retro: + correct_count += 28 - assert count == correct_count, "expected {0} items but found {1} items for {2}".format(correct_count, count, (progressive, shuffle, difficulty, timer, goal, mode, retro)) + assert count == correct_count, "expected {0} items but found {1} items for {2}".format(correct_count, count, (progressive, shuffle, difficulty, timer, goal, mode, retro)) if __name__ == '__main__': test() diff --git a/Items.py b/Items.py index 1bc94cc9..f9a7d884 100644 --- a/Items.py +++ b/Items.py @@ -24,6 +24,7 @@ def ItemFactory(items, player): # Format: Name: (Advancement, Priority, Type, ItemCode, Pedestal Hint Text, Pedestal Credit Text, Sick Kid Credit Text, Zora Credit Text, Witch Credit Text, Flute Boy Credit Text, Hint Text) item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'the Bow'), + 'Progressive Bow': (True, False, None, 0x64, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'), 'Book of Mudora': (True, False, None, 0x1D, 'This is a\nparadox?!', 'and the story book', 'the scholarly kid', 'moon runes for sale', 'drugs for literacy', 'book-worm boy can read again', 'the Book'), 'Hammer': (True, False, None, 0x09, 'stop\nhammer time!', 'and m c hammer', 'hammer-smashing kid', 'm c hammer for sale', 'stop... hammer time', 'stop, hammer time', 'the hammer'), 'Hookshot': (True, False, None, 0x0A, 'BOING!!!\nBOING!!!\nBOING!!!', 'and the tickle beam', 'tickle-monster kid', 'tickle beam for sale', 'witch and tickle boy', 'beam boy tickles again', 'the Hookshot'), diff --git a/Main.py b/Main.py index 33afc1d1..7d6536f6 100644 --- a/Main.py +++ b/Main.py @@ -24,7 +24,7 @@ def main(args, seed=None): start = time.clock() # initialize the world - world = World(args.multi, args.shuffle, args.logic, args.mode, args.difficulty, args.timer, args.progressive, args.goal, args.algorithm, not args.nodungeonitems, args.beatableonly, args.shuffleganon, args.quickswap, args.fastmenu, args.disablemusic, args.keysanity, args.retro, args.custom, args.customitemarray, args.shufflebosses, args.hints) + world = World(args.multi, args.shuffle, args.logic, args.mode, args.swords, args.difficulty, args.timer, args.progressive, args.goal, args.algorithm, not args.nodungeonitems, args.beatableonly, args.shuffleganon, args.quickswap, args.fastmenu, args.disablemusic, args.keysanity, args.retro, args.custom, args.customitemarray, args.shufflebosses, args.hints) logger = logging.getLogger('') if seed is None: random.seed(None) @@ -180,7 +180,7 @@ def gt_filler(world): def copy_world(world): # ToDo: Not good yet - ret = World(world.players, world.shuffle, world.logic, world.mode, world.difficulty, world.timer, world.progressive, world.goal, world.algorithm, world.place_dungeon_items, world.check_beatable_only, world.shuffle_ganon, world.quickswap, world.fastmenu, world.disable_music, world.keysanity, world.retro, world.custom, world.customitemarray, world.boss_shuffle, world.hints) + ret = World(world.players, world.shuffle, world.logic, world.mode, world.swords, world.difficulty, world.timer, world.progressive, world.goal, world.algorithm, world.place_dungeon_items, world.check_beatable_only, world.shuffle_ganon, world.quickswap, world.fastmenu, world.disable_music, world.keysanity, world.retro, world.custom, world.customitemarray, world.boss_shuffle, world.hints) ret.required_medallions = world.required_medallions.copy() ret.swamp_patch_required = world.swamp_patch_required.copy() ret.ganon_at_pyramid = world.ganon_at_pyramid.copy() @@ -238,6 +238,8 @@ def copy_world(world): item.location = ret.get_location(location.name, location.player) if location.event: ret.get_location(location.name, location.player).event = True + if location.locked: + ret.get_location(location.name, location.player).locked = True # copy remaining itempool. No item in itempool should have an assigned location for item in world.itempool: diff --git a/README.md b/README.md index 4c07f5ec..25bdbd9c 100644 --- a/README.md +++ b/README.md @@ -95,12 +95,6 @@ This is only noticeably different if the the Ganon shuffle option is enabled. ## Game Difficulty -### Easy - -This setting doubles the number of swords, shields, armors, bottles, and silver arrows in the item pool. -This setting will also triple the number of Lamps available, and all will be obtainable before dark rooms. -Within dungeons, the number of items found will be displayed on screen if there is no timer. - ### Normal This is the default setting that has an item pool most similar to the original @@ -118,11 +112,6 @@ the player from having fairies in bottles. This setting is a more extreme version of the Hard setting. Potions are further nerfed, the item pool is less helpful, and the player can find no armor, only a Master Sword, and only a single bottle. -### Insane - -This setting is a modest step up from Expert. The main difference is that the player will never find any -additional health. - ## Timer Setting ### None @@ -358,7 +347,7 @@ Select the game mode. (default: open) Select the game completion goal. (default: ganon) ``` ---difficulty [{easy,normal,hard,expert,insane}] +--difficulty [{normal,hard,expert}] ``` Select the game difficulty. Affects available itempool. (default: normal) diff --git a/Rom.py b/Rom.py index f4354731..da1f64fd 100644 --- a/Rom.py +++ b/Rom.py @@ -549,7 +549,7 @@ def patch_rom(world, player, rom): rom.write_byte(0x51DE, 0x00) # set open mode: - if world.mode in ['open', 'swordless', 'inverted']: + if world.mode in ['open', 'inverted']: rom.write_byte(0x180032, 0x01) # open mode # disable sword sprite from uncle @@ -587,7 +587,7 @@ def patch_rom(world, player, rom): # potion magic restore amount rom.write_byte(0x180085, 0x40) # Half Magic #Cape magic cost - rom.write_bytes(0x3ADA7, [0x02, 0x02, 0x02]) + rom.write_bytes(0x3ADA7, [0x02, 0x04, 0x08]) # Byrna Invulnerability: off rom.write_byte(0x18004F, 0x00) #Disable catching fairies @@ -603,31 +603,11 @@ def patch_rom(world, player, rom): # Powdered Fairies Prize rom.write_byte(0x36DD0, 0xD8) # One Heart # potion heal amount - rom.write_byte(0x180084, 0x08) # One Heart + rom.write_byte(0x180084, 0x20) # 4 Hearts # potion magic restore amount rom.write_byte(0x180085, 0x20) # Quarter Magic #Cape magic cost - rom.write_bytes(0x3ADA7, [0x01, 0x01, 0x01]) - # Byrna Invulnerability: off - rom.write_byte(0x18004F, 0x00) - #Disable catching fairies - rom.write_byte(0x34FD6, 0x80) - overflow_replacement = GREEN_TWENTY_RUPEES - # Rupoor negative value - rom.write_int16(0x180036, world.rupoor_cost) - # Set stun items - rom.write_byte(0x180180, 0x00) # Nothing - # Make silver arrows only usable against Ganon - rom.write_byte(0x180181, 0x01) - elif world.difficulty == 'insane': - # Powdered Fairies Prize - rom.write_byte(0x36DD0, 0x79) # Bees - # potion heal amount - rom.write_byte(0x180084, 0x00) # No healing - # potion magic restore amount - rom.write_byte(0x180085, 0x00) # No healing - #Cape magic cost - rom.write_bytes(0x3ADA7, [0x01, 0x01, 0x01]) + rom.write_bytes(0x3ADA7, [0x02, 0x04, 0x08]) # Byrna Invulnerability: off rom.write_byte(0x18004F, 0x00) #Disable catching fairies @@ -664,12 +644,7 @@ def patch_rom(world, player, rom): else: overflow_replacement = GREEN_TWENTY_RUPEES - if world.difficulty in ['easy']: - rom.write_byte(0x180182, 0x03) # auto equip silvers on pickup and at ganon - elif world.retro and world.difficulty in ['hard', 'expert', 'insane']: #FIXME: this is temporary for v29 baserom (perhaps no so temporary?) - rom.write_byte(0x180182, 0x03) # auto equip silvers on pickup and at ganon - else: - rom.write_byte(0x180182, 0x01) # auto equip silvers on pickup + rom.write_byte(0x180182, 0x01) # auto equip silvers on pickup #Byrna residual magic cost rom.write_bytes(0x45C42, [0x04, 0x02, 0x01]) @@ -709,7 +684,7 @@ def patch_rom(world, player, rom): random.shuffle(packs) prizes[:56] = [drop for pack in packs for drop in pack] - if world.difficulty in ['hard', 'expert', 'insane']: + if world.difficulty in ['hard', 'expert']: prize_replacements = {0xE0: 0xDF, # Fairy -> heart 0xE3: 0xD8} # Big magic -> small magic prizes = [prize_replacements.get(prize, prize) for prize in prizes] @@ -753,26 +728,16 @@ def patch_rom(world, player, rom): rom.write_byte(address, prize) # Fill in item substitutions table - if world.difficulty in ['easy']: - rom.write_bytes(0x184000, [ - # original_item, limit, replacement_item, filler - 0x12, 0x01, 0x35, 0xFF, # lamp -> 5 rupees - 0x58, 0x01, 0x43, 0xFF, # silver arrows -> 1 arrow - 0x51, 0x06, 0x52, 0xFF, # 6 +5 bomb upgrades -> +10 bomb upgrade - 0x53, 0x06, 0x54, 0xFF, # 6 +5 arrow upgrades -> +10 arrow upgrade - 0xFF, 0xFF, 0xFF, 0xFF, # end of table sentinel - ]) - else: - rom.write_bytes(0x184000, [ - # original_item, limit, replacement_item, filler - 0x12, 0x01, 0x35, 0xFF, # lamp -> 5 rupees - 0x51, 0x06, 0x52, 0xFF, # 6 +5 bomb upgrades -> +10 bomb upgrade - 0x53, 0x06, 0x54, 0xFF, # 6 +5 arrow upgrades -> +10 arrow upgrade - 0xFF, 0xFF, 0xFF, 0xFF, # end of table sentinel - ]) + rom.write_bytes(0x184000, [ + # original_item, limit, replacement_item, filler + 0x12, 0x01, 0x35, 0xFF, # lamp -> 5 rupees + 0x51, 0x06, 0x52, 0xFF, # 6 +5 bomb upgrades -> +10 bomb upgrade + 0x53, 0x06, 0x54, 0xFF, # 6 +5 arrow upgrades -> +10 arrow upgrade + 0xFF, 0xFF, 0xFF, 0xFF, # end of table sentinel + ]) # set Fountain bottle exchange items - if world.difficulty in ['hard', 'expert', 'insane']: + if world.difficulty in ['hard', 'expert']: rom.write_byte(0x348FF, [0x16, 0x2B, 0x2C, 0x2D, 0x3C, 0x48][random.randint(0, 5)]) rom.write_byte(0x3493B, [0x16, 0x2B, 0x2C, 0x2D, 0x3C, 0x48][random.randint(0, 5)]) else: @@ -837,9 +802,7 @@ def patch_rom(world, player, rom): rom.write_int32(0x180200, -100 * 60 * 60 * 60) # red clock adjustment time (in frames, sint32) rom.write_int32(0x180204, 2 * 60 * 60) # blue clock adjustment time (in frames, sint32) rom.write_int32(0x180208, 4 * 60 * 60) # green clock adjustment time (in frames, sint32) - if world.difficulty == 'easy': - rom.write_int32(0x18020C, (20 + ERtimeincrease) * 60 * 60) # starting time (in frames, sint32) - elif world.difficulty == 'normal': + if world.difficulty == 'normal': rom.write_int32(0x18020C, (10 + ERtimeincrease) * 60 * 60) # starting time (in frames, sint32) else: rom.write_int32(0x18020C, int((5 + ERtimeincrease / 2) * 60 * 60)) # starting time (in frames, sint32) @@ -866,7 +829,7 @@ def patch_rom(world, player, rom): rom.write_byte(0x180211, 0x06) #Game type, we set the Entrance and item randomization flags # assorted fixes - rom.write_byte(0x1800A2, 0x01) # remain in real dark world when dying in dark word dungion before killing aga1 + rom.write_byte(0x1800A2, 0x01) # remain in real dark world when dying in dark world dungeon before killing aga1 rom.write_byte(0x180169, 0x01 if world.lock_aga_door_in_escape else 0x00) # Lock or unlock aga tower door during escape sequence. if world.mode == 'inverted': rom.write_byte(0x180169, 0x02) # lock aga/ganon tower door with crystals in inverted @@ -878,6 +841,7 @@ def patch_rom(world, player, rom): rom.write_bytes(0x50563, [0x3F, 0x14]) # disable below ganon chest rom.write_byte(0x50599, 0x00) # disable below ganon chest rom.write_bytes(0xE9A5, [0x7E, 0x00, 0x24]) # disable below ganon chest + rom.write_byte(0x18008B, 0x00) # Pyramid Hole not pre-opened rom.write_byte(0xF5D73, 0xF0) # bees are catchable rom.write_byte(0xF5F10, 0xF0) # bees are catchable rom.write_byte(0x180086, 0x00 if world.aga_randomness else 0x01) # set blue ball and ganon warp randomness @@ -928,8 +892,6 @@ def patch_rom(world, player, rom): # compasses showing dungeon count if world.clock_mode != 'off': rom.write_byte(0x18003C, 0x00) # Currently must be off if timer is on, because they use same HUD location - elif world.difficulty == 'easy': - rom.write_byte(0x18003C, 0x02) # always on elif world.keysanity: rom.write_byte(0x18003C, 0x01) # show on pickup else: @@ -1311,6 +1273,9 @@ def write_strings(rom, world, player): greenpendant = world.find_items('Green Pendant', player)[0] tt['sahasrahla_bring_courage'] = 'I lost my family heirloom in %s' % greenpendant.hint_text + tt['sign_ganons_tower'] = ('You need %d crystal to enter.' if world.crystals_needed_for_gt == 1 else 'You need %d crystals to enter.') % world.crystals_needed_for_gt + tt['sign_ganon'] = ('You need %d crystal to beat Ganon.' if world.crystals_needed_for_ganon == 1 else 'You need %d crystals to beat Ganon.') % world.crystals_needed_for_ganon + tt['uncle_leaving_text'] = Uncle_texts[random.randint(0, len(Uncle_texts) - 1)] tt['end_triforce'] = "{NOBORDER}\n" + Triforce_texts[random.randint(0, len(Triforce_texts) - 1)] tt['bomb_shop_big_bomb'] = BombShop2_texts[random.randint(0, len(BombShop2_texts) - 1)] diff --git a/Rules.py b/Rules.py index 6d2d7767..1db807b8 100644 --- a/Rules.py +++ b/Rules.py @@ -26,14 +26,16 @@ def set_rules(world, player): open_rules(world, player) elif world.mode == 'standard': standard_rules(world, player) - elif world.mode == 'swordless': - swordless_rules(world, player) elif world.mode == 'inverted': open_rules(world, player) inverted_rules(world, player) else: raise NotImplementedError('Not implemented yet') + if world.swords == 'swordless': + # FIXME: !!! Does not handle inverted properly + swordless_rules(world, player) + if world.logic == 'noglitches': no_glitches_rules(world, player) elif world.logic == 'minorglitches': @@ -792,8 +794,7 @@ def inverted_rules(world, player): 'Ganons Tower - Pre-Moldorm Chest', 'Ganons Tower - Validation Chest']: forbid_item(world.get_location(location, player), 'Big Key (Ganons Tower)', player) - set_rule(world.get_location('Ganon', player), lambda state: state.has_beam_sword(player) and state.has_fire_source(player) and state.has('Crystal 1', player) and state.has('Crystal 2', player) - and state.has('Crystal 3', player) and state.has('Crystal 4', player) and state.has('Crystal 5', player) and state.has('Crystal 6', player) and state.has('Crystal 7', player) + set_rule(world.get_location('Ganon', player), lambda state: state.has_beam_sword(player) and state.has_fire_source(player) and state.has_crystals(world.crystals_needed_for_ganon, player) and (state.has('Tempered Sword', player) or state.has('Golden Sword', player) or (state.has('Silver Arrows', player) and state.can_shoot_arrows(player)) or state.has('Lamp', player) or state.can_extend_magic(player, 12))) # need to light torch a sufficient amount of times set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has_beam_sword(player)) # need to damage ganon to get tiles to drop @@ -801,7 +802,7 @@ def inverted_rules(world, player): set_trock_key_rules(world, player) - set_rule(world.get_entrance('Inverted Ganons Tower', player), lambda state: state.has('Crystal 1', player) and state.has('Crystal 2', player) and state.has('Crystal 3', player) and state.has('Crystal 4', player) and state.has('Crystal 5', player) and state.has('Crystal 6', player) and state.has('Crystal 7', player)) + set_rule(world.get_entrance('Inverted Ganons Tower', player), lambda state: state.has_crystals(world.crystals_needed_for_gt, player)) def no_glitches_rules(world, player): if world.mode != 'inverted': @@ -887,11 +888,6 @@ def open_rules(world, player): def swordless_rules(world, player): - # for the time being swordless mode just inherits all fixes from open mode. - # should there ever be fixes that apply to open mode but not swordless, this - # can be revisited. - open_rules(world, player) - set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has('Hammer', player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle set_rule(world.get_entrance('Agahnim 1', player), lambda state: (state.has('Hammer', player) or state.has('Fire Rod', player) or state.can_shoot_arrows(player) or state.has('Cane of Somaria', player)) and state.has_key('Small Key (Agahnims Tower)', player, 2)) set_rule(world.get_location('Ether Tablet', player), lambda state: state.has('Book of Mudora', player) and state.has('Hammer', player)) diff --git a/Text.py b/Text.py index b8baba83..7cafee7d 100644 --- a/Text.py +++ b/Text.py @@ -1883,5 +1883,7 @@ class TextTable(object): # 190 text['sign_east_death_mountain_bridge'] = CompressedTextMapper.convert("How did you get up here?") text['fish_money'] = CompressedTextMapper.convert("It's a secret to everyone.") + text['sign_ganons_tower'] = CompressedTextMapper.convert("You need all 7 crystals to enter.") + text['sign_ganon'] = CompressedTextMapper.convert("You need all 7 crystals to beat Ganon.") text['end_pad_data'] = bytearray([0xfb]) text['terminator'] = bytearray([0xFF, 0xFF]) diff --git a/_vendor/__init__.py b/_vendor/__init__.py new file mode 100644 index 00000000..e69de29b