diff --git a/BaseClasses.py b/BaseClasses.py index 01825043..4a484834 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1080,6 +1080,9 @@ class CollectionState(object): return True return False + def can_collect_bonkdrops(self, player): + return self.has_Boots(player) or (self.has_sword(player) and self.has('Quake', player)) + def can_farm_rupees(self, player): tree_pulls = ['Lost Woods East Area', 'Snitch Lady (East)', @@ -1192,7 +1195,7 @@ class CollectionState(object): if can_reach_non_bunny(region): return True - if self.has_Boots(player): + if not self.world.shuffle_bonk_drops[player] and self.can_collect_bonkdrops(player): for region in bonk_bombs: if can_reach_non_bunny(region): return True @@ -2696,6 +2699,7 @@ class LocationType(FastEnum): Shop = 3 Pot = 4 Drop = 5 + Bonk = 6 class Item(object): @@ -2911,6 +2915,7 @@ class Spoiler(object): 'ow_mixed': self.world.owMixed, 'ow_whirlpool': self.world.owWhirlpoolShuffle, 'ow_fluteshuffle': self.world.owFluteShuffle, + 'bonk_drops': self.world.shuffle_bonk_drops, 'shuffle': self.world.shuffle, 'shuffleganon': self.world.shuffle_ganon, 'shufflelinks': self.world.shufflelinks, @@ -3123,6 +3128,7 @@ class Spoiler(object): outfile.write('Swapped OW (Mixed):'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_mixed'][player])) outfile.write('Whirlpool Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_whirlpool'][player])) outfile.write('Flute Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_fluteshuffle'][player]) + outfile.write('Bonk Drops:'.ljust(line_width) + '%s\n' % yn(self.metadata['bonk_drops'][player])) outfile.write('Entrance Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['shuffle'][player]) if self.metadata['shuffle'][player] != 'vanilla': outfile.write('Shuffle GT/Ganon:'.ljust(line_width) + '%s\n' % yn(self.metadata['shuffleganon'][player])) diff --git a/Fill.py b/Fill.py index 9bf7f98f..fe9cbcf9 100644 --- a/Fill.py +++ b/Fill.py @@ -355,18 +355,23 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None # handle pot shuffle pots_used = False pot_item_pool = collections.defaultdict(list) - for item in world.itempool: - if item.name in ['Chicken', 'Big Magic']: # can only fill these in that players world - pot_item_pool[item.player].append(item) - for player, pot_pool in pot_item_pool.items(): - if pot_pool: - for pot_item in pot_pool: - world.itempool.remove(pot_item) - pot_locations = [location for location in fill_locations - if location.type == LocationType.Pot and location.player == player] - pot_locations = filter_pot_locations(pot_locations, world) - fast_fill_helper(world, pot_pool, pot_locations) - pots_used = True + + # guarantee one big magic in a bonk location + for player in range(1, world.players + 1): + if world.shuffle_bonk_drops[player]: + for item in world.itempool: + if item.name in ['Big Magic']: + pot_item_pool[player].append(item) + break + + for player, magic_pool in pot_item_pool.items(): + world.itempool.remove(magic_pool[0]) + pot_locations = [location for location in fill_locations + if location.type == LocationType.Bonk and location.player == player] + pot_locations = filter_pot_locations(pot_locations, world) + fast_fill_helper(world, magic_pool, pot_locations) + pots_used = True + if pots_used: fill_locations = world.get_unfilled_locations() random.shuffle(fill_locations) diff --git a/ItemList.py b/ItemList.py index 18472fb0..7350ff9a 100644 --- a/ItemList.py +++ b/ItemList.py @@ -3,11 +3,12 @@ import logging import math import RaceRandom as random -from BaseClasses import Region, RegionType, Shop, ShopType, Location, CollectionState, PotItem +from BaseClasses import LocationType, Region, RegionType, Shop, ShopType, Location, CollectionState, PotItem from EntranceShuffle import connect_entrance from Regions import shop_to_location_table, retro_shops, shop_table_by_location, valid_pot_location from Fill import FillError, fill_restrictive, fast_fill, get_dungeon_item_pool from PotShuffle import vanilla_pots +from Tables import bonk_prize_lookup from Items import ItemFactory from source.item.FillUtil import trash_items, pot_items @@ -411,6 +412,10 @@ def generate_itempool(world, player): if world.pottery[player] not in ['none', 'keys']: add_pot_contents(world, player) + if world.shuffle_bonk_drops[player]: + create_dynamic_bonkdrop_locations(world, player) + add_bonkdrop_contents(world, player) + take_any_locations = [ 'Snitch Lady (East)', 'Snitch Lady (West)', 'Bush Covered House', 'Light World Bomb Hut', @@ -500,6 +505,21 @@ def create_dynamic_shop_locations(world, player): loc.locked = True +def create_dynamic_bonkdrop_locations(world, player): + from Regions import bonk_prize_table + for bonk_location, (_, _, _, region_name, hint_text) in bonk_prize_table.items(): + region = world.get_region(region_name, player) + loc = Location(player, bonk_location, 0, region, hint_text) + loc.type = LocationType.Bonk + loc.parent_region = region + loc.address = 0x2abb00 + (bonk_prize_table[loc.name][0] * 6) + 3 + + region.locations.append(loc) + world.dynamic_locations.append(loc) + + world.clear_location_cache() + + def fill_prizes(world, attempts=15): all_state = world.get_all_state(keys=True) for player in range(1, world.players + 1): @@ -779,6 +799,17 @@ def add_pot_contents(world, player): world.itempool.append(ItemFactory(item, player)) +def add_bonkdrop_contents(world, player): + from Items import item_table + for item_name, (_, count, alt_item) in bonk_prize_lookup.items(): + if item_name not in item_table: + item_name = alt_item + while (count > 0): + item = ItemFactory(item_name, player) + world.itempool.append(item) + count -= 1 + + def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords, retro, bombbag, door_shuffle, logic, flute_activated): pool = [] placed_items = {} diff --git a/Items.py b/Items.py index ea81c136..10d93159 100644 --- a/Items.py +++ b/Items.py @@ -81,10 +81,10 @@ item_table = {'Bow': (True, False, None, 0x0B, 200, 'You have\nchosen the\narche 'Arrow Upgrade (+10)': (False, False, None, 0x54, 100, 'Increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'), 'Arrow Upgrade (+5)': (False, False, None, 0x53, 100, 'Increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'), 'Single Bomb': (False, False, None, 0x27, 5, 'I make things\ngo BOOM! But\njust once.', 'and the explosion', 'the bomb-holding kid', 'firecracker for sale', 'blend fungus into bomb', '\'splosion boy explodes again', 'a bomb'), - 'Arrows (5)': (False, False, None, 0x5A, 15, 'This will give\nyou five shots\nwith your bow!', 'and the arrow pack', 'stick-collecting kid', 'sewing kit for sale', 'fungus for arrows', 'archer boy sews again', 'five arrows'), + 'Arrows (5)': (False, False, None, 0xB5, 15, 'This will give\nyou five shots\nwith your bow!', 'and the arrow pack', 'stick-collecting kid', 'sewing kit for sale', 'fungus for arrows', 'archer boy sews again', 'five arrows'), 'Small Magic': (False, False, None, 0x45, 5, 'A bit of magic', 'and the bit of magic', 'bit-o-magic kid', 'magic bit for sale', 'fungus for magic', 'magic boy conjures again', 'a bit of magic'), - 'Big Magic': (False, False, None, 0x5A, 40, 'A lot of magic', 'and lots of magic', 'lot-o-magic kid', 'magic refill for sale', 'fungus for magic', 'magic boy conjures again', 'a magic refill'), - 'Chicken': (False, False, None, 0x5A, 999, 'Cucco of Legend', 'and the legendary cucco', 'chicken kid', 'fried chicken for sale', 'fungus for chicken', 'cucco boy clucks again', 'a cucco'), + 'Big Magic': (False, False, None, 0xB4, 40, 'A lot of magic', 'and lots of magic', 'lot-o-magic kid', 'magic refill for sale', 'fungus for magic', 'magic boy conjures again', 'a magic refill'), + 'Chicken': (False, False, None, 0xB3, 999, 'Cucco of Legend', 'and the legendary cucco', 'chicken kid', 'fried chicken for sale', 'fungus for chicken', 'cucco boy clucks again', 'a cucco'), 'Bombs (3)': (False, False, None, 0x28, 15, 'I make things\ngo triple\nBOOM!!!', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'three bombs'), 'Bombs (10)': (False, False, None, 0x31, 50, 'I make things\ngo BOOM! Ten\ntimes!', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'ten bombs'), 'Bomb Upgrade (+10)': (False, False, None, 0x52, 100, 'Increase bomb\nstorage, low\nlow price', 'and the bomb bag', 'boom-enlarging kid', 'bomb boost for sale', 'the shroom goes boom', 'upgrade boy explodes more again', 'bomb capacity'), @@ -177,6 +177,8 @@ item_table = {'Bow': (True, False, None, 0x0B, 200, 'You have\nchosen the\narche 'Blue Potion': (False, False, None, 0x30, 160, 'Delicious blue goop!', 'and the blue goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has blue goo again', 'a blue potion'), 'Bee': (False, False, None, 0x0E, 10, 'I will sting your foes a few times', 'and the sting buddy', 'the beekeeper kid', 'insect for sale', 'shroom pollenation', 'bottle boy has mad bee again', 'a bee'), 'Small Heart': (False, False, None, 0x42, 10, 'Just a little\npiece of love!', 'and the heart', 'the life-giving kid', 'little love for sale', 'fungus for life', 'life boy feels some love again', 'a heart'), + 'Apples': (False, False, None, 0xB1, 30, 'Just a few pieces of fruit!', 'and the juicy fruit', 'the fruity kid', 'the fruit stand', 'expired fruit', 'bottle boy has fruit again', 'an apple hoard'), + 'Fairy': (False, False, None, 0xB2, 50, 'Just a pixie!', 'and the pixie', 'the pixie kid', 'pixie for sale', 'pixie fungus', 'bottle boy has pixie again', 'a pixie'), 'Beat Agahnim 1': (True, False, 'Event', 999, None, None, None, None, None, None, None, None), 'Beat Agahnim 2': (True, False, 'Event', 999, None, None, None, None, None, None, None, None), 'Get Frog': (True, False, 'Event', 999, None, None, None, None, None, None, None, None), diff --git a/Regions.py b/Regions.py index 18c14c9a..af3c13ef 100644 --- a/Regions.py +++ b/Regions.py @@ -1266,7 +1266,54 @@ def pot_address(pot_index, super_tile): return 0x7f6018 + super_tile * 2 + (pot_index << 24) -# (type, room_id, shopkeeper, custom, locked, [items]) +# bonk location: record id, aga required, default item, region, hint text +bonk_prize_table = { + 'Lost Woods Hideout Tree': (0x00, False, '', 'Lost Woods East Area', 'in a tree'), + 'Death Mountain Bonk Rocks': (0x01, False, '', 'East Death Mountain (Top East)', 'encased in stone'), + 'Mountain Entry Pull Tree': (0x02, False, '', 'Mountain Entry Area', 'in a tree'), + 'Mountain Entry Southeast Tree': (0x03, False, '', 'Mountain Entry Area', 'in a tree'), + 'Lost Woods Pass West Tree': (0x04, False, '', 'Lost Woods Pass West Area', 'in a tree'), + 'Kakariko Portal Tree': (0x05, False, '', 'Lost Woods Pass East Top Area', 'in a tree'), + 'Fortune Bonk Rocks': (0x06, False, '', 'Kakariko Fortune Area', 'in a tree'), + 'Kakariko Pond Tree': (0x07, True, '', 'Kakariko Pond Area', 'in a tree'), + 'Bonk Rocks Tree': (0x08, True, '', 'Bonk Rock Ledge', 'in a tree'), + 'Sanctuary Tree': (0x09, False, '', 'Sanctuary Area', 'in a tree'), + 'River Bend West Tree': (0x0a, True, '', 'River Bend Area', 'in a tree'), + 'River Bend East Tree': (0x0b, False, '', 'River Bend East Bank', 'in a tree'), + 'Blinds Hideout Tree': (0x0c, False, '', 'Kakariko Area', 'in a tree'), + 'Kakariko Welcome Tree': (0x0d, False, '', 'Kakariko Area', 'in a tree'), + 'Forgotten Forest Southwest Tree': (0x0e, False, '', 'Forgotten Forest Area', 'in a tree'), + 'Forgotten Forest Central Tree': (0x0f, False, '', 'Forgotten Forest Area', 'in a tree'), + #'Forgotten Forest Southeast Tree': (0x??, False, '', 'Forgotten Forest Area', 'in a tree'), + 'Hyrule Castle Tree': (0x10, False, '', 'Hyrule Castle Courtyard', 'in a tree'), + 'Wooden Bridge Tree': (0x11, False, '', 'Wooden Bridge Area', 'in a tree'), + 'Eastern Palace Tree': (0x12, True, '', 'Eastern Palace Area', 'in a tree'), + 'Flute Boy South Tree': (0x13, True, '', 'Flute Boy Area', 'in a tree'), + 'Flute Boy East Tree': (0x14, True, '', 'Flute Boy Area', 'in a tree'), + 'Central Bonk Rocks Tree': (0x15, False, '', 'Central Bonk Rocks Area', 'in a tree'), + 'Tree Line Tree 2': (0x16, True, '', 'Tree Line Area', 'in a tree'), + 'Tree Line Tree 4': (0x17, True, '', 'Tree Line Area', 'in a tree'), + 'Flute Boy Approach South Tree': (0x18, False, '', 'Flute Boy Approach Area', 'in a tree'), + 'Flute Boy Approach North Tree': (0x19, False, '', 'Flute Boy Approach Area', 'in a tree'), + 'Dark Lumberjack Tree': (0x1a, False, '', 'Dark Lumberjack Area', 'in a tree'), + 'Dark Fortune Bonk Rocks (Drop 1)': (0x1b, False, '', 'Dark Fortune Area', 'encased in stone'), + 'Dark Fortune Bonk Rocks (Drop 2)': (0x1c, False, '', 'Dark Fortune Area', 'encased in stone'), + 'Dark Graveyard West Bonk Rocks': (0x1d, False, '', 'Dark Graveyard Area', 'encased in stone'), + 'Dark Graveyard North Bonk Rocks': (0x1e, False, '', 'Dark Graveyard North', 'encased in stone'), + 'Dark Graveyard Tomb Bonk Rocks': (0x1f, False, '', 'Dark Graveyard North', 'encased in stone'), + 'Qirn Jump West Tree': (0x20, False, '', 'Qirn Jump Area', 'in a tree'), + 'Qirn Jump East Tree': (0x21, False, '', 'Qirn Jump East Bank', 'in a tree'), + 'Dark Witch Tree': (0x22, False, '', 'Dark Witch Area', 'in a tree'), + 'Pyramid Tree': (0x23, False, '', 'Pyramid Area', 'in a tree'), + 'Palace of Darkness Tree': (0x24, False, '', 'Palace of Darkness Area', 'in a tree'), + 'Dark Tree Line Tree 2': (0x25, False, '', 'Dark Tree Line Area', 'in a tree'), + 'Dark Tree Line Tree 3': (0x26, False, '', 'Dark Tree Line Area', 'in a tree'), + 'Dark Tree Line Tree 4': (0x27, False, '', 'Dark Tree Line Area', 'in a tree'), + 'Hype Cave Statue': (0x28, False, '', 'Hype Cave Area', 'encased in stone') +} + + +# (room_id, type, shopkeeper, custom, locked, [items]) # item = (item, price, max=0, replacement=None, replacement_price=0) _basic_shop_defaults = [('Red Potion', 150), ('Small Heart', 10), ('Bombs (10)', 50)] _dark_world_shop_defaults = [('Red Potion', 150), ('Blue Shield', 50), ('Bombs (10)', 50)] diff --git a/Rom.py b/Rom.py index b614e730..7c43ea9f 100644 --- a/Rom.py +++ b/Rom.py @@ -38,7 +38,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '210e4631353e3d094f01bf91562844a5' +RANDOMIZERBASEHASH = 'f76555dcc8cbd0f185fb37eafa3779c3' class JsonRom(object): @@ -630,6 +630,14 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_byte(sprite_pointer+1, 0) rom.write_byte(sprite_pointer+2, code) continue + elif location.type == LocationType.Bonk: + address = snes_to_pc(location.address) + rom.write_byte(address, handle_native_dungeon(location, itemid)) + if location.item.player != player: + rom.write_byte(address+1, location.item.player) + else: + rom.write_byte(address+1, 0) + continue if location.address is None or (type(location.address) is int and location.address >= 0x400000): continue @@ -771,18 +779,42 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): # set world flag world_flag = 0x00 if b >= 0x40 and b < 0x80 else 0x40 - rom.write_byte(0x153A00 + b, world_flag) + rom.write_byte(0x1539B0 + b, world_flag) if b & 0xBF in megatiles: - rom.write_byte(0x153A00 + b + 1, world_flag) - rom.write_byte(0x153A00 + b + 8, world_flag) - rom.write_byte(0x153A00 + b + 9, world_flag) + rom.write_byte(0x1539B0 + b + 1, world_flag) + rom.write_byte(0x1539B0 + b + 8, world_flag) + rom.write_byte(0x1539B0 + b + 9, world_flag) for edge in world.owedges: if edge.dest is not None and isinstance(edge.dest, OWEdge) and edge.player == player: write_int16(rom, edge.getAddress() + 0x0a, edge.vramLoc) if not edge.specialExit: - rom.write_byte(0x1539e0 + (edge.specialID - 0x80) * 2 if edge.specialEntrance else edge.getAddress() + 0x0e, edge.getTarget()) + rom.write_byte(0x1539A0 + (edge.specialID - 0x80) * 2 if edge.specialEntrance else edge.getAddress() + 0x0e, edge.getTarget()) + # patch bonk prizes + if world.shuffle_bonk_drops: + bonk_prizes = [0x79, 0xE3, 0x79, 0xAC, 0xAC, 0xE0, 0xDC, 0xAC, 0xE3, 0xE3, 0xDA, 0xE3, 0xDA, 0xD8, 0xAC, 0xAC, 0xE3, 0xD8, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xDC, 0xDB, 0xE3, 0xDA, 0x79, 0x79, 0xE3, 0xE3, + 0xDA, 0x79, 0xAC, 0xAC, 0x79, 0xE3, 0x79, 0xAC, 0xAC, 0xE0, 0xDC, 0xE3, 0x79, 0xDE, 0xE3, 0xAC, 0xDB, 0x79, 0xE3, 0xD8, 0xAC, 0x79, 0xE3, 0xDB, 0xDB, 0xE3, 0xE3, 0x79, 0xD8, 0xDD] + bonk_addresses = [0x4CF6C, 0x4CFBA, 0x4CFE0, 0x4CFFB, 0x4D018, 0x4D01B, 0x4D028, 0x4D03C, 0x4D059, 0x4D07A, 0x4D09E, 0x4D0A8, 0x4D0AB, 0x4D0AE, 0x4D0BE, 0x4D0DD, + 0x4D16A, 0x4D1E5, 0x4D1EE, 0x4D20B, 0x4CBBF, 0x4CBBF, 0x4CC17, 0x4CC1A, 0x4CC4A, 0x4CC4D, 0x4CC53, 0x4CC69, 0x4CC6F, 0x4CC7C, 0x4CCEF, 0x4CD51, + 0x4CDC0, 0x4CDC3, 0x4CDC6, 0x4CE37, 0x4D2DE, 0x4D32F, 0x4D355, 0x4D367, 0x4D384, 0x4D387, 0x4D397, 0x4D39E, 0x4D3AB, 0x4D3AE, 0x4D3D1, 0x4D3D7, + 0x4D3F8, 0x4D416, 0x4D420, 0x4D423, 0x4D42D, 0x4D449, 0x4D48C, 0x4D4D9, 0x4D4DC, 0x4D4E3, 0x4D504, 0x4D507, 0x4D55E, 0x4D56A] + + # # legacy bonk prize shuffle, shuffles bonk prizes amongst themselves + # random.shuffle(bonk_prizes) + # for prize, address in zip(bonk_prizes, bonk_addresses): + # rom.write_byte(address, prize) + + owFlags |= 0x200 + + # setting spriteID to D8, a placeholder sprite we use to inform ROM to spawn a dynamic item + #for address in bonk_addresses: + for address in [b for b in bonk_addresses if b != 0x4D0AE]: # temp fix for screen 1A murahdahla sprite replacement + rom.write_byte(address, 0xD8) + # temporary fix for screen 1A + rom.write_byte(snes_to_pc(0x09AE32), 0xD8) + rom.write_byte(snes_to_pc(0x09AE35), 0xD8) + write_int16(rom, 0x150002, owMode) write_int16(rom, 0x150004, owFlags) @@ -875,6 +907,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): valid_locations = [l for l in my_locations if ((l.type == LocationType.Pot and not l.forced_item) or (l.type == LocationType.Drop and not l.forced_item) or (l.type == LocationType.Normal and not l.forced_item) + or (l.type == LocationType.Bonk and not l.forced_item) or (l.type == LocationType.Shop and world.shopsanity[player]))] valid_loc_by_dungeon = valid_dungeon_locations(valid_locations) @@ -1251,18 +1284,6 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): # fill enemy prize packs rom.write_bytes(0x37A78, pack_prizes) - # set bonk prizes - bonk_prizes = [0x79, 0xE3, 0x79, 0xAC, 0xAC, 0xE0, 0xDC, 0xAC, 0xE3, 0xE3, 0xDA, 0xE3, 0xDA, 0xD8, 0xAC, 0xAC, 0xE3, 0xD8, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xDC, 0xDB, 0xE3, 0xDA, 0x79, 0x79, 0xE3, 0xE3, - 0xDA, 0x79, 0xAC, 0xAC, 0x79, 0xE3, 0x79, 0xAC, 0xAC, 0xE0, 0xDC, 0xE3, 0x79, 0xDE, 0xE3, 0xAC, 0xDB, 0x79, 0xE3, 0xD8, 0xAC, 0x79, 0xE3, 0xDB, 0xDB, 0xE3, 0xE3, 0x79, 0xD8, 0xDD] - bonk_addresses = [0x4CF6C, 0x4CFBA, 0x4CFE0, 0x4CFFB, 0x4D018, 0x4D01B, 0x4D028, 0x4D03C, 0x4D059, 0x4D07A, 0x4D09E, 0x4D0A8, 0x4D0AB, 0x4D0AE, 0x4D0BE, 0x4D0DD, - 0x4D16A, 0x4D1E5, 0x4D1EE, 0x4D20B, 0x4CBBF, 0x4CBBF, 0x4CC17, 0x4CC1A, 0x4CC4A, 0x4CC4D, 0x4CC53, 0x4CC69, 0x4CC6F, 0x4CC7C, 0x4CCEF, 0x4CD51, - 0x4CDC0, 0x4CDC3, 0x4CDC6, 0x4CE37, 0x4D2DE, 0x4D32F, 0x4D355, 0x4D367, 0x4D384, 0x4D387, 0x4D397, 0x4D39E, 0x4D3AB, 0x4D3AE, 0x4D3D1, 0x4D3D7, - 0x4D3F8, 0x4D416, 0x4D420, 0x4D423, 0x4D42D, 0x4D449, 0x4D48C, 0x4D4D9, 0x4D4DC, 0x4D4E3, 0x4D504, 0x4D507, 0x4D55E, 0x4D56A] - if world.shuffle_bonk_prizes: - random.shuffle(bonk_prizes) - for prize, address in zip(bonk_prizes, bonk_addresses): - rom.write_byte(address, prize) - # Fill in item substitutions table rom.write_bytes(0x184000, [ # original_item, limit, replacement_item, filler @@ -1644,6 +1665,12 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): # temporarally we are just nopping out this check we will conver this to a rom fix soon. rom.write_bytes(0x02F539, [0xEA, 0xEA, 0xEA, 0xEA, 0xEA] if world.powder_patch_required[player] else [0xAD, 0xBF, 0x0A, 0xF0, 0x4F]) + # sprite patches + rom.write_byte(snes_to_pc(0x0db7d1), 0x03) # patch apple sprites to not permadeatch like enemies + if world.shuffle_bonk_drops[player]: + # warning, this temporary patch might cause fairies to respawn differently?, limiting this to bonk drop mode only + rom.write_byte(snes_to_pc(0x0db808), 0x03) # patch fairies sprites to not permadeatch like enemies + # allow smith into multi-entrance caves in appropriate shuffles if world.shuffle[player] in ['restricted', 'full', 'lite', 'lean', 'crossed', 'insanity'] or (world.shuffle[player] == 'simple' and world.mode[player] == 'inverted'): rom.write_byte(0x18004C, 0x01) @@ -2754,7 +2781,7 @@ def set_inverted_mode(world, player, rom, inverted_buffer): # apply inverted map changes for b in range(0x00, len(inverted_buffer)): - rom.write_byte(0x153B00 + b, inverted_buffer[b]) + rom.write_byte(0x153A70 + b, inverted_buffer[b]) def patch_shuffled_dark_sanc(world, rom, player): dark_sanc = world.get_region('Dark Sanctuary Hint', player) diff --git a/Rules.py b/Rules.py index 45aa6ba6..9619c897 100644 --- a/Rules.py +++ b/Rules.py @@ -823,6 +823,15 @@ def default_rules(world, player): set_rule(world.get_location('Ether Tablet', player), lambda state: state.has('Book of Mudora', player) and state.has_beam_sword(player)) set_rule(world.get_location('Bombos Tablet', player), lambda state: state.has('Book of Mudora', player) and state.has_beam_sword(player)) + # Bonk Item Access + if world.shuffle_bonk_drops[player]: + if world.get_region('Big Bomb Shop', player).entrances: # just some location that is placed late in the ER algorithm, prevent standard rules from applying when trying to search reachability in the overworld + from Regions import bonk_prize_table + for location_name, (_, aga_required, _, _, _) in bonk_prize_table.items(): + loc = world.get_location(location_name, player) + set_rule(loc, lambda state: (state.can_collect_bonkdrops(player)) and (not aga_required or state.has_beaten_aga(player))) + add_bunny_rule(loc, player) + # Entrance Access set_rule(world.get_entrance('Lumberjack Tree Tree', player), lambda state: state.has_Boots(player) and state.has_beaten_aga(player)) set_rule(world.get_entrance('Bonk Rock Cave', player), lambda state: state.has_Boots(player)) @@ -1731,6 +1740,11 @@ def standard_rules(world, player): add_rule(world.get_entrance('Hyrule Castle Ledge Drop', player), lambda state: state.has('Zelda Delivered', player)) add_rule(world.get_entrance('Bonk Fairy (Light)', player), lambda state: state.has('Zelda Delivered', player)) + if world.shuffle_bonk_drops[player]: + if world.get_region('Big Bomb Shop', player).entrances: # just some location that is placed late in the ER algorithm, prevent standard rules from applying when trying to search reachability in the overworld + add_rule(world.get_location('Hyrule Castle Tree', player), lambda state: state.has('Zelda Delivered', player)) + add_rule(world.get_location('Central Bonk Rocks Tree', player), lambda state: state.has('Zelda Delivered', player)) + # don't allow bombs to get past here before zelda is rescued set_rule(world.get_entrance('GT Hookshot South Entry to Ranged Crystal', player), lambda state: (state.can_use_bombs(player) and state.has('Zelda Delivered', player)) or state.has('Blue Boomerang', player) or state.has('Red Boomerang', player)) # or state.has('Cane of Somaria', player)) diff --git a/Tables.py b/Tables.py index 64464b9f..a30f24f0 100644 --- a/Tables.py +++ b/Tables.py @@ -125,17 +125,23 @@ divisor_lookup = { # 0xf8: 0xbac, 0xf9: 0xbba, 0xfa: 0xbc1, 0xfb: 0xbcc, 0xfc: 0xbd7, 0xfd: 0xbd7, 0xfe: 0xbba, 0xff: 0xbe3 # } -prize_lookup = { - 0xd8: 'Small Magic Refill', - 0xd9: 'Rupee (1)', - 0xda: 'Rupees (5)', - 0xdb: 'Rupees (20)', - 0xdc: 'Bomb (1)', - 0xdd: 'Bombs (4)', - 0xde: 'Bombs (8)', - 0xdf: 'Heart', - 0xe0: 'Fairy', - 0xe1: 'Arrows (5)', - 0xe2: 'Arrows (10)', - 0xe3: 'Full Magic Refill' +# item name: (spriteID, pool count, replacement item) +bonk_prize_lookup = { + 'Chicken': (0x0b, 0, None), + 'Bee Trap': (0x79, 6, None), + 'Apples': (0xac, 8, None), + 'Small Heart': (0xd8, 2, None), + 'Rupee (1)': (0xd9, 0, None), + 'Rupees (5)': (0xda, 3, None), # TODO: add in murahdahla tree rupee + 'Rupees (20)': (0xdb, 3, None), + 'Single Bomb': (0xdc, 2, None), + 'Bombs (3)': (None, 0, 'Bombs (4)'), + 'Bombs (4)': (0xdd, 0, 'Bombs (3)'), + 'Bombs (8)': (0xde, 1, 'Bombs (10)'), + 'Bombs (10)': (None, 0, 'Bombs (8)'), + 'Small Magic': (0xdf, 0, None), + 'Big Magic': (0xe0, 1, None), + 'Arrows (5)': (0xe1, 0, None), + 'Arrows (10)': (0xe2, 0, None), + 'Fairy': (0xe3, 15, None) } diff --git a/asm/owrando.asm b/asm/owrando.asm index 2d0c1ebd..7793ce2d 100644 --- a/asm/owrando.asm +++ b/asm/owrando.asm @@ -143,6 +143,9 @@ jsl.l OWWorldCheck16 : nop org $02b16e ; AND #$3F : ORA 7EF3CA and #$7f : eor #$40 : nop #2 +org $06AD4C +jsl.l OWBonkDrops : nop #4 + ;Code org $aa8800 OWTransitionDirection: @@ -368,6 +371,136 @@ LoadMapDarkOrMixed: dw $0400+$0210 ; bottom right } +; Y = sprite slot index of bonk sprite +OWBonkDrops: +{ + CMP.b #$D8 : BEQ + + RTL + + LDA.l OWFlags+1 : AND.b #$02 : BNE + + JSL.l Sprite_TransmuteToBomb : RTL + + + + ; loop thru rando bonk table to find match + PHB : PHK : PLB + LDA.b $8A + LDX.b #(40*6) ; 40 bonk items, 6 bytes each + - CMP.w OWBonkPrizeData,X : BNE + + INX + LDA.w $0D10,Y : LSR A : LSR A : LSR A : LSR A + EOR.w $0D00,Y : CMP.w OWBonkPrizeData,X : BNE ++ ; X = row + 1 + BRA .found_match + ++ DEX : LDA.b $8A + + CPX.b #$00 : BNE + + PLB : RTL + + DEX : DEX : DEX : DEX : DEX : DEX : BRA - + + .found_match + INX : LDA.w OWBonkPrizeData,X : PHX : PHA ; S = FlagBitmask, X (row + 2) + LDX.b $8A : LDA.l OverworldEventDataWRAM,X : AND 1,S : PHA : BNE + ; S = Collected, FlagBitmask, X (row + 2) + LDA.b #$1B : STA $12F ; JSL Sound_SetSfx3PanLong ; seems that when you bonk, there is a pending bonk sfx, so we clear that out and replace with reveal secret sfx + + + LDA 3,S : TAX : INX : LDA.w OWBonkPrizeData,X + PHA : INX : LDA.w OWBonkPrizeData,X : BEQ + + ; multiworld item + DEX : PLA ; X = row + 3 + JMP .spawn_item + + DEX : PLA ; X = row + 3 + + .determine_type ; A = item id ; S = Collected, FlagBitmask, X (row + 2) + CMP.b #$B0 : BNE + + LDA.b #$79 : JMP .sprite_transform ; transform to bees + + CMP.b #$42 : BNE + + JSL.l Sprite_TransmuteToBomb ; transform a heart to bomb, vanilla behavior + JMP .mark_collected + + CMP.b #$34 : BNE + + LDA.b #$D9 : CLC : JMP .sprite_transform ; transform to single rupee + + CMP.b #$35 : BNE + + LDA.b #$DA : CLC : BRA .sprite_transform ; transform to blue rupee + + CMP.b #$36 : BNE + + LDA.b #$DB : CLC : BRA .sprite_transform ; transform to red rupee + + CMP.b #$27 : BNE + + LDA.b #$DC : CLC : BRA .sprite_transform ; transform to 1 bomb + + CMP.b #$28 : BNE + + LDA.b #$DD : CLC : BRA .sprite_transform ; transform to 4 bombs + + CMP.b #$31 : BNE + + LDA.b #$DE : CLC : BRA .sprite_transform ; transform to 8 bombs + + CMP.b #$45 : BNE + + LDA.b #$DF : CLC : BRA .sprite_transform ; transform to small magic + + CMP.b #$B4 : BNE + + LDA.b #$E0 : CLC : BRA .sprite_transform ; transform to big magic + + CMP.b #$B5 : BNE + + LDA.b #$E1 : CLC : BRA .sprite_transform ; transform to 5 arrows + + CMP.b #$44 : BNE + + LDA.b #$E2 : CLC : BRA .sprite_transform ; transform to 10 arrows + + CMP.b #$B1 : BNE + + LDA.b #$AC : BRA .sprite_transform ; transform to apples + + CMP.b #$B2 : BNE + + LDA.b #$E3 : BRA .sprite_transform ; transform to fairy + + CMP.b #$B3 : BNE .spawn_item + INX : INX : LDA.w OWBonkPrizeData,X ; X = row + 5 + CLC : ADC.b #$08 : PHA + LDA.w $0D00,Y : SEC : SBC.b 1,S : STA.w $0D00,Y + LDA.w $0D20,Y : SBC.b #$00 : STA.w $0D20,Y : PLX + LDA.b #$0B : SEC ; BRA .sprite_transform ; transform to chicken + + .sprite_transform + STA.w $0E20,Y + TYX : JSL.l Sprite_LoadProperties + BEQ + + ; these are sprite properties that make it fall out of the tree to the east + LDA #$30 : STA $0F80,Y ; amount of force (related to speed) + LDA #$10 : STA $0D50,Y ; eastward rate of speed + LDA #$FF : STA $0B58,Y ; expiration timer + + + + .mark_collected ; S = Collected, FlagBitmask, X (row + 2) + PLA : BNE + ; S = FlagBitmask, X (row + 2) + LDX.b $8A : LDA.l OverworldEventDataWRAM,X : ORA 1,S : STA.l OverworldEventDataWRAM,X + + REP #$20 + LDA.l TotalItemCounter : INC : STA.l TotalItemCounter + SEP #$20 + + JMP .return + + ; spawn itemget item + .spawn_item ; A = item id ; Y = tree sprite slot ; S = Collected, FlagBitmask, X (row + 2) + PLX : BEQ + : LDA.b #$00 : STA.w $0DD0,Y : JMP .return ; S = FlagBitmask, X (row + 2) + + LDA 2,S : TAX : INX : INX + LDA.w OWBonkPrizeData,X : STA.l !MULTIWORLD_SPRITEITEM_PLAYER_ID + DEX + + LDA.b #$01 : STA !REDRAW + + LDA.b #$EB + STA.l $7FFE00 + JSL Sprite_SpawnDynamically+15 ; +15 to skip finding a new slot, use existing sprite + + ; affects the rate the item moves in the Y/X direction + LDA.b #$00 : STA.w $0D40,Y + LDA.b #$0A : STA.w $0D50,Y + + LDA.b #$20 : STA.w $0F80,Y ; amount of force (gives height to the arch) + LDA.b #$FF : STA.w $0B58,Y ; stun timer + LDA.b #$30 : STA.w $0F10,Y ; aux delay timer 4 ?? dunno what that means + + LDA.b #$00 : STA.w $0F20,Y ; layer the sprite is on + + ; sets OW event bitmask flag, uses free RAM + PLA : STA.w $0ED0,Y ; S = X (row + 2) + + ; determines the initial spawn point of item + PLX : INX : INX : INX + LDA.w $0D00,Y : SEC : SBC.w OWBonkPrizeData,X : STA.w $0D00,Y + LDA.w $0D20,Y : SBC #$00 : STA.w $0D20,Y + + LDA.b #$01 : STA !REDRAW : STA !FORCE_HEART_SPAWN + + PLB : RTL + + .return + PLA : PLA : PLB : RTL +} + org $aa9000 OWDetectEdgeTransition: { @@ -1130,11 +1263,11 @@ dw $0f20, $0f40, $0020, $0f30, $757e, $0000, $0000, $0049 dw $0f70, $0fb8, $0048, $0f94, $757e, $0000, $0000, $004a dw $0058, $00c0, $0068, $008c, $8080, $0000, $0000, $0017 ;Hobo (unused) -org $aab9e0 ;PC 1539e0 +org $aab9a0 ;PC 1539a0 OWSpecialDestIndex: dw $0080, $0081, $0082 -org $aaba00 ;PC 153a00 +org $aab9b0 ;PC 1539b0 OWTileWorldAssoc: db 0, 0, 0, 0, 0, 0, 0, 0 db 0, 0, 0, 0, 0, 0, 0, 0 @@ -1154,7 +1287,7 @@ db $40, $40, $40, $40, $40, $40, $40, $40 db $40, $40, $40, $40, $40, $40, $40, $40 db $00, $00 -org $aabb00 ;PC 153b00 +org $aaba70 ;PC 153a70 OWTileMapAlt: db 0, 0, 0, 0, 0, 0, 0, 0 db 0, 0, 0, 0, 0, 0, 0, 0 @@ -1175,3 +1308,63 @@ db 0, 0, 0, 0, 0, 0, 0, 0 db 0, 0, 0, 0, 0, 0, 0, 0 db 0, 0 + +org $aabb00 ;PC 153b00 +OWBonkPrizeData: +; OWID YX Flag Item MW Offset +db $00, $59, $10, $b0, $00, $20 +db $05, $04, $10, $b2, $00, $00 +db $0a, $4e, $10, $b0, $00, $20 +db $0a, $a9, $08, $b1, $00, $20 +db $10, $c7, $10, $b1, $00, $20 +db $10, $f7, $08, $b4, $00, $20 +db $11, $08, $10, $27, $00, $00 +db $12, $a4, $10, $b2, $00, $20 +db $13, $c7, $10, $31, $00, $20 +db $13, $98, $08, $b1, $00, $20 +db $15, $a4, $10, $b1, $00, $20 +db $15, $fb, $08, $b2, $00, $20 +db $18, $a8, $10, $b2, $00, $20 +db $18, $36, $08, $35, $00, $20 +db $1a, $8a, $10, $42, $00, $20 +db $1a, $1d, $08, $b2, $00, $20 +;db $1a, $77, $04, $35, $00, $20 ; pre aga ONLY ; hijacked murahdahla bonk tree +db $1b, $46, $10, $b1, $00, $10 +db $1d, $6b, $10, $b1, $00, $20 +db $1e, $72, $10, $b2, $00, $20 +db $2a, $8f, $10, $36, $00, $20 +db $2a, $45, $08, $36, $00, $20 +db $2b, $d6, $10, $b2, $00, $20 +db $2e, $9c, $10, $b2, $00, $20 +db $2e, $b4, $08, $b0, $00, $20 +db $32, $29, $10, $42, $00, $20 +db $32, $9a, $08, $b2, $00, $20 +db $42, $66, $10, $b2, $00, $20 +db $51, $08, $10, $b2, $00, $00 +db $51, $09, $08, $b2, $00, $00 +db $54, $b5, $10, $27, $00, $00 +db $54, $ef, $08, $b2, $00, $08 +db $54, $b9, $04, $36, $00, $00 +db $55, $aa, $10, $b0, $00, $20 +db $55, $fb, $08, $35, $00, $20 +db $56, $e4, $10, $b0, $00, $20 +db $5b, $a7, $10, $b2, $00, $20 +db $5e, $00, $10, $b2, $00, $20 +db $6e, $8c, $10, $35, $00, $10 +db $6e, $90, $08, $b0, $00, $10 +db $6e, $a4, $04, $b1, $00, $10 +db $74, $4e, $10, $b1, $00, $1c + +; temporary fix - murahdahla replaces one of the bonk tree prizes +; so we copy the sprite table here and update the pointer +; longterm solution should be to spawn in murahdahla separately +org $09AE2A +Overworld_Sprites_Screen1A_2: +db $08, $0F, $41 ; yx:{ 0x080, 0x0F0 } +db $0E, $0C, $41 ; yx:{ 0x0E0, 0x0C0 } +db $11, $0D, $E3 ; yx:{ 0x110, 0x0D0 } +db $18, $0A, $D8 ; yx:{ 0x180, 0x0A0 } +db $18, $0F, $45 ; yx:{ 0x180, 0x0F0 } +db $FF ; END +org $09CA55 +dw Overworld_Sprites_Screen1A_2&$FFFF \ No newline at end of file diff --git a/data/base2current.bps b/data/base2current.bps index 52e4e07c..9e8c66e6 100644 Binary files a/data/base2current.bps and b/data/base2current.bps differ diff --git a/mystery_example.yml b/mystery_example.yml index 41a46ca6..ab7ffe63 100644 --- a/mystery_example.yml +++ b/mystery_example.yml @@ -22,6 +22,9 @@ vanilla: 0 balanced: 1 random: 1 + bonk_drops: + on: 1 + off: 1 door_shuffle: vanilla: 0 basic: 2 diff --git a/source/item/FillUtil.py b/source/item/FillUtil.py index 9acdb45b..bf48c755 100644 --- a/source/item/FillUtil.py +++ b/source/item/FillUtil.py @@ -792,8 +792,8 @@ trash_items = { 'Bee Trap': 0, 'Rupee (1)': 1, 'Rupees (5)': 1, 'Small Heart': 1, 'Bee': 1, 'Arrows (5)': 1, 'Chicken': 1, 'Single Bomb': 1, 'Rupees (20)': 2, 'Small Magic': 2, - 'Bombs (3)': 3, 'Arrows (10)': 3, 'Bombs (10)': 3, - 'Big Magic': 4, 'Red Potion': 4, 'Blue Shield': 4, 'Rupees (50)': 4, 'Rupees (100)': 4, + 'Bombs (3)': 3, 'Arrows (10)': 3, 'Bombs (10)': 3, 'Apples': 3, + 'Fairy': 4, 'Big Magic': 4, 'Red Potion': 4, 'Blue Shield': 4, 'Rupees (50)': 4, 'Rupees (100)': 4, 'Rupees (300)': 5, 'Piece of Heart': 17 }