parent
2e99db5403
commit
9bd9bb4f93
|
@ -3,7 +3,7 @@ from enum import Enum, unique
|
|||
import logging
|
||||
import json
|
||||
from collections import OrderedDict
|
||||
|
||||
from Utils import int16_as_bytes
|
||||
|
||||
class World(object):
|
||||
|
||||
|
@ -18,6 +18,7 @@ class World(object):
|
|||
self.algorithm = algorithm
|
||||
self.dungeons = []
|
||||
self.regions = []
|
||||
self.shops = []
|
||||
self.itempool = []
|
||||
self.seed = None
|
||||
self.state = CollectionState(self)
|
||||
|
@ -379,11 +380,17 @@ class CollectionState(object):
|
|||
|
||||
def has_key(self, item, count=1):
|
||||
if self.world.retro:
|
||||
return True #FIXME: This needs to check for shop access to a small key shop
|
||||
return self.can_buy_unlimited('Small Key (Universal)')
|
||||
if count == 1:
|
||||
return item in self.prog_items
|
||||
return self.item_count(item) >= count
|
||||
|
||||
def can_buy_unlimited(self, item):
|
||||
for shop in self.world.shops:
|
||||
if shop.has_unlimited(item) and shop.region.can_reach(self):
|
||||
return True
|
||||
return False
|
||||
|
||||
def item_count(self, item):
|
||||
return len([pritem for pritem in self.prog_items if pritem == item])
|
||||
|
||||
|
@ -424,18 +431,25 @@ class CollectionState(object):
|
|||
basemagic = basemagic + int(basemagic * 0.25 * self.bottle_count())
|
||||
elif self.world.difficulty == 'insane' and not fullrefill:
|
||||
basemagic = basemagic
|
||||
else:
|
||||
elif self.can_buy_unlimited('Green Potion') or self.can_buy_unlimited('Red Potion'):
|
||||
basemagic = basemagic + basemagic * self.bottle_count()
|
||||
return basemagic >= smallmagic # FIXME bottle should really also have a requirement that we can reach some shop that sells green or blue potions
|
||||
return basemagic >= smallmagic
|
||||
|
||||
def can_kill_most_things(self, enemies=5):
|
||||
return (self.has_blunt_weapon()
|
||||
or self.has('Cane of Somaria')
|
||||
or (self.has('Cane of Byrna') and (enemies < 6 or self.can_extend_Magic()))
|
||||
or self.has('Bow')
|
||||
or self.can_shoot_arrows()
|
||||
or self.has('Fire Rod')
|
||||
)
|
||||
|
||||
def can_shoot_arrows(self):
|
||||
if self.world.retro:
|
||||
#TODO: need to decide how we want to handle wooden arrows longer-term (a can-buy-a check, or via dynamic shop location)
|
||||
#FIXME: Should do something about hard+ ganon only silvers. For the moment, i believe they effective grant wooden, so we are safe
|
||||
return self.has('Bow') and (self.has('Silver Arrows') or self.can_buy_unlimited('Single Arrow'))
|
||||
return self.has('Bow')
|
||||
|
||||
def has_sword(self):
|
||||
return self.has('Fighter Sword') or self.has('Master Sword') or self.has('Tempered Sword') or self.has('Golden Sword')
|
||||
|
||||
|
@ -574,7 +588,6 @@ class RegionType(Enum):
|
|||
return self in (RegionType.Cave, RegionType.Dungeon)
|
||||
|
||||
|
||||
|
||||
class Region(object):
|
||||
|
||||
def __init__(self, name, type):
|
||||
|
@ -584,6 +597,7 @@ class Region(object):
|
|||
self.exits = []
|
||||
self.locations = []
|
||||
self.dungeon = None
|
||||
self.shop = None
|
||||
self.world = None
|
||||
self.is_light_world = False # will be set aftermaking connections.
|
||||
self.is_dark_world = False
|
||||
|
@ -751,6 +765,64 @@ class Item(object):
|
|||
class Crystal(Item):
|
||||
pass
|
||||
|
||||
@unique
|
||||
class ShopType(Enum):
|
||||
Shop = 0
|
||||
TakeAny = 1
|
||||
|
||||
class Shop(object):
|
||||
def __init__(self, region, room_id, type, shopkeeper_config, replaceable):
|
||||
self.region = region
|
||||
self.room_id = room_id
|
||||
self.type = type
|
||||
self.inventory = [None, None, None]
|
||||
self.shopkeeper_config = shopkeeper_config
|
||||
self.replaceable = replaceable
|
||||
self.active = False
|
||||
|
||||
@property
|
||||
def item_count(self):
|
||||
return (3 if self.inventory[2] else
|
||||
2 if self.inventory[1] else
|
||||
1 if self.inventory[0] else
|
||||
0)
|
||||
|
||||
def get_bytes(self):
|
||||
# [id][roomID-low][roomID-high][doorID][zero][shop_config][shopkeeper_config][sram_index]
|
||||
entrances = self.region.entrances
|
||||
config = self.item_count
|
||||
if len(entrances) == 1 and entrances[0].addresses:
|
||||
door_id = entrances[0].addresses+1
|
||||
else:
|
||||
door_id = 0
|
||||
config |= 0x40 # ignore door id
|
||||
if self.type == ShopType.TakeAny:
|
||||
config |= 0x80
|
||||
return [0x00]+int16_as_bytes(self.room_id)+[door_id, 0x00, config, self.shopkeeper_config, 0x00]
|
||||
|
||||
def has_unlimited(self, item):
|
||||
for inv in self.inventory:
|
||||
if inv is None:
|
||||
continue
|
||||
if inv['max'] != 0 and inv['replacement'] is not None and inv['replacement'] == item:
|
||||
return True
|
||||
elif inv['item'] is not None and inv['item'] == item:
|
||||
return True
|
||||
return False
|
||||
|
||||
def clear_inventory(self):
|
||||
self.inventory = [None, None, None]
|
||||
|
||||
def add_inventory(self, slot, item, price, max=0, replacement=None, replacement_price=0, create_location=False):
|
||||
self.inventory[slot] = {
|
||||
'item': item,
|
||||
'price': price,
|
||||
'max': max,
|
||||
'replacement': replacement,
|
||||
'replacement_price': replacement_price,
|
||||
'create_location': create_location
|
||||
}
|
||||
|
||||
|
||||
class Spoiler(object):
|
||||
|
||||
|
|
|
@ -1026,7 +1026,7 @@ def link_entrances(world):
|
|||
if world.get_entrance('Dam').connected_region.name != 'Dam' or world.get_entrance('Swamp Palace').connected_region.name != 'Swamp Palace (Entrance)':
|
||||
world.swamp_patch_required = True
|
||||
|
||||
# check for
|
||||
# check for potion shop location
|
||||
if world.get_entrance('Potion Shop').connected_region.name != 'Potion Shop':
|
||||
world.powder_patch_required = True
|
||||
|
||||
|
@ -1744,6 +1744,7 @@ mandatory_connections = [('Lake Hylia Central Island Pier', 'Lake Hylia Central
|
|||
('Paradox Cave Push Block', 'Paradox Cave Front'),
|
||||
('Paradox Cave Bomb Jump', 'Paradox Cave'),
|
||||
('Paradox Cave Drop', 'Paradox Cave Chest Area'),
|
||||
('Light World Death Mountain Shop', 'Light World Death Mountain Shop'),
|
||||
('Fairy Ascension Rocks', 'Fairy Ascension Plateau'),
|
||||
('Fairy Ascension Mirror Spot', 'Fairy Ascension Plateau'),
|
||||
('Fairy Ascension Drop', 'East Death Mountain (Bottom)'),
|
||||
|
|
37
ItemList.py
37
ItemList.py
|
@ -262,6 +262,8 @@ def generate_itempool(world):
|
|||
tr_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)]
|
||||
world.required_medallions = (mm_medallion, tr_medallion)
|
||||
|
||||
set_up_shops(world)
|
||||
|
||||
# distribute crystals
|
||||
fill_prizes(world)
|
||||
|
||||
|
@ -292,7 +294,23 @@ def fill_prizes(world, attempts=15):
|
|||
raise FillError('Unable to place dungeon prizes')
|
||||
|
||||
|
||||
def set_up_shops(world):
|
||||
# Changes to basic Shops
|
||||
# TODO: move hard+ mode changes for sheilds here, utilizing the new shops
|
||||
|
||||
if world.retro:
|
||||
rss = world.get_region('Red Shield Shop').shop
|
||||
rss.active = True
|
||||
rss.add_inventory(2, 'Single Arrow', 80)
|
||||
|
||||
# Randomized changes to Shops
|
||||
if world.retro:
|
||||
for shop in random.sample([s for s in world.shops if s.replaceable], 5):
|
||||
shop.active = True
|
||||
shop.add_inventory(0, 'Single Arrow', 80)
|
||||
shop.add_inventory(1, 'Small Key (Universal)', 100)
|
||||
|
||||
#special shop types
|
||||
|
||||
def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, retro):
|
||||
pool = []
|
||||
|
@ -566,17 +584,18 @@ def test():
|
|||
for mode in ['open', 'standard', 'swordless']:
|
||||
for progressive in ['on', 'off']:
|
||||
for shuffle in ['full', 'insane']:
|
||||
out = get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, retro)
|
||||
count = len(out[0]) + len(out[1])
|
||||
for retro in [True, False]:
|
||||
out = get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, 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))
|
||||
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()
|
||||
|
|
5
Items.py
5
Items.py
|
@ -160,6 +160,11 @@ item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher cla
|
|||
'Map (Ganons Tower)': (False, True, 'Map', 0x72, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again'),
|
||||
'Small Key (Universal)': (False, True, None, 0xAF, 'A small key for any door', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again'),
|
||||
'Nothing': (False, False, None, 0x5A, 'Some Hot Air', 'and the Nothing', 'the zen kid', 'outright theft', 'shroom theft', 'empty boy is bored again'),
|
||||
'Red Potion': (False, False, None, 0x2E, None, None, None, None, None, None),
|
||||
'Green Potion': (False, False, None, 0x2F, None, None, None, None, None, None),
|
||||
'Blue Potion': (False, False, None, 0x30, None, None, None, None, None, None),
|
||||
'Bee': (False, False, None, 0x0E, None, None, None, None, None, None),
|
||||
'Small Heart': (False, False, None, 0x42, None, None, None, None, None, None),
|
||||
'Beat Agahnim 1': (True, False, 'Event', None, None, None, None, None, None, None),
|
||||
'Beat Agahnim 2': (True, False, 'Event', None, None, None, None, None, None, None),
|
||||
'Get Frog': (True, False, 'Event', None, None, None, None, None, None, None),
|
||||
|
|
10
Main.py
10
Main.py
|
@ -1,4 +1,5 @@
|
|||
from collections import OrderedDict
|
||||
import copy
|
||||
from itertools import zip_longest
|
||||
import json
|
||||
import logging
|
||||
|
@ -157,6 +158,15 @@ def copy_world(world):
|
|||
create_regions(ret)
|
||||
create_dungeons(ret)
|
||||
|
||||
#TODO: copy_dynamic_regions_and_locations() # also adds some new shops
|
||||
|
||||
for shop in world.shops:
|
||||
copied_shop = ret.get_region(shop.region.name).shop
|
||||
copied_shop.active = shop.active
|
||||
copied_shop.inventory = copy.copy(shop.inventory)
|
||||
|
||||
|
||||
|
||||
# connect copied world
|
||||
for region in world.regions:
|
||||
copied_region = ret.get_region(region.name)
|
||||
|
|
49
Regions.py
49
Regions.py
|
@ -1,5 +1,5 @@
|
|||
import collections
|
||||
from BaseClasses import Region, Location, Entrance, RegionType
|
||||
from BaseClasses import Region, Location, Entrance, RegionType, Shop, ShopType
|
||||
|
||||
|
||||
def create_regions(world):
|
||||
|
@ -25,7 +25,7 @@ def create_regions(world):
|
|||
create_cave_region('Hyrule Castle Secret Entrance', ['Link\'s Uncle', 'Secret Passage'], ['Hyrule Castle Secret Entrance Exit']),
|
||||
create_lw_region('Zoras River', ['King Zora', 'Zora\'s Ledge']),
|
||||
create_cave_region('Waterfall of Wishing', ['Waterfall Fairy - Left', 'Waterfall Fairy - Right']),
|
||||
create_lw_region('Kings Grave Area', None, ['Kings Grave', 'Kings Grave Inner Rocks']),
|
||||
create_lw_region('Kings Grave Area', None, ['Kings Grave', 'Kings Grave Inner Rocks']),
|
||||
create_cave_region('Kings Grave', ['King\'s Tomb']),
|
||||
create_cave_region('North Fairy Cave', None, ['North Fairy Cave Exit']),
|
||||
create_cave_region('Dam', ['Floodgate', 'Floodgate Chest']),
|
||||
|
@ -95,7 +95,7 @@ def create_regions(world):
|
|||
create_lw_region('Desert Palace Lone Stairs', None, ['Desert Palace Stairs Drop', 'Desert Palace Entrance (East)']),
|
||||
create_lw_region('Desert Palace Entrance (North) Spot', None, ['Desert Palace Entrance (North)', 'Desert Ledge Return Rocks']),
|
||||
create_dungeon_region('Desert Palace Main (Outer)', ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'],
|
||||
['Desert Palace Pots (Outer)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)', 'Desert Palace East Wing']),
|
||||
['Desert Palace Pots (Outer)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)', 'Desert Palace East Wing']),
|
||||
create_dungeon_region('Desert Palace Main (Inner)', None, ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']),
|
||||
create_dungeon_region('Desert Palace East', ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']),
|
||||
create_dungeon_region('Desert Palace North', ['Desert Palace - Lanmolas', 'Desert Palace - Prize'], ['Desert Palace Exit (North)']),
|
||||
|
@ -125,7 +125,7 @@ def create_regions(world):
|
|||
create_cave_region('Spectacle Rock Cave (Peak)', None, ['Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave Exit (Peak)']),
|
||||
create_lw_region('East Death Mountain (Bottom)', None, ['Broken Bridge (East)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'East Death Mountain Teleporter', 'Hookshot Fairy', 'Fairy Ascension Rocks', 'Spiral Cave (Bottom)']),
|
||||
create_cave_region('Hookshot Fairy'),
|
||||
create_cave_region('Paradox Cave Front', None, ['Paradox Cave Push Block Reverse', 'Paradox Cave Exit (Bottom)']),
|
||||
create_cave_region('Paradox Cave Front', None, ['Paradox Cave Push Block Reverse', 'Paradox Cave Exit (Bottom)', 'Light World Death Mountain Shop']),
|
||||
create_cave_region('Paradox Cave Chest Area', ['Paradox Cave Lower - Far Left',
|
||||
'Paradox Cave Lower - Left',
|
||||
'Paradox Cave Lower - Right',
|
||||
|
@ -135,6 +135,7 @@ def create_regions(world):
|
|||
'Paradox Cave Upper - Right'],
|
||||
['Paradox Cave Push Block', 'Paradox Cave Bomb Jump']),
|
||||
create_cave_region('Paradox Cave', None, ['Paradox Cave Exit (Middle)', 'Paradox Cave Exit (Top)', 'Paradox Cave Drop']),
|
||||
create_cave_region('Light World Death Mountain Shop'),
|
||||
create_lw_region('East Death Mountain (Top)', None, ['Paradox Cave (Top)', 'Death Mountain (Top)', 'Spiral Cave Ledge Access', 'East Death Mountain Drop', 'Turtle Rock Teleporter', 'Fairy Ascension Ledge']),
|
||||
create_lw_region('Spiral Cave Ledge', None, ['Spiral Cave', 'Spiral Cave Ledge Drop']),
|
||||
create_cave_region('Spiral Cave (Top)', ['Spiral Cave'], ['Spiral Cave (top to bottom)', 'Spiral Cave Exit (Top)']),
|
||||
|
@ -290,6 +291,14 @@ def create_regions(world):
|
|||
create_dw_region('Pyramid Ledge', None, ['Pyramid Entrance', 'Pyramid Drop'])
|
||||
]
|
||||
|
||||
for region_name, (room_id, shopkeeper, replaceable) in shop_table.items():
|
||||
region = world.get_region(region_name)
|
||||
shop = Shop(region, room_id, ShopType.Shop, shopkeeper, replaceable)
|
||||
region.shop = shop
|
||||
world.shops.append(shop)
|
||||
for index, (item, price) in enumerate(default_shop_contents[region_name]):
|
||||
shop.add_inventory(index, item, price)
|
||||
|
||||
world.intialize_regions()
|
||||
|
||||
def create_lw_region(name, locations=None, exits=None):
|
||||
|
@ -347,6 +356,38 @@ def mark_light_world_regions(world):
|
|||
seen.add(exit.connected_region)
|
||||
queue.append(exit.connected_region)
|
||||
|
||||
# (room_id, shopkeeper, replaceable)
|
||||
shop_table = {
|
||||
'Cave Shop (Dark Death Mountain)': (0x0112, 0x51, True),
|
||||
'Red Shield Shop': (0x0110, 0x51, True),
|
||||
'Dark Lake Hylia Shop': (0x010F, 0x51, True),
|
||||
'Dark World Lumberjack Shop': (0x010F, 0x51, True),
|
||||
'Village of Outcasts Shop': (0x010F, 0x51, True),
|
||||
'Dark World Potion Shop': (0x010F, 0x51, True),
|
||||
'Light World Death Mountain Shop': (0x00FF, 0x51, True),
|
||||
'Kakariko Shop': (0x011F, 0x51, True),
|
||||
'Cave Shop (Lake Hylia)': (0x0112, 0x51, True),
|
||||
'Potion Shop': (0x0109, 0xFF, False),
|
||||
# Bomb Shop not currently modeled as a shop, due to special nature of items
|
||||
}
|
||||
# region, [item]
|
||||
# slot, item, price, max=0, replacement=None, replacement_price=0
|
||||
# item = (item, price)
|
||||
|
||||
_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)]
|
||||
default_shop_contents = {
|
||||
'Cave Shop (Dark Death Mountain)': _basic_shop_defaults,
|
||||
'Red Shield Shop': [('Red Shield', 500), ('Bee', 10), ('Arrows (10)', 30)],
|
||||
'Dark Lake Hylia Shop': _dark_world_shop_defaults,
|
||||
'Dark World Lumberjack Shop': _dark_world_shop_defaults,
|
||||
'Village of Outcasts Shop': _dark_world_shop_defaults,
|
||||
'Dark World Potion Shop': _dark_world_shop_defaults,
|
||||
'Light World Death Mountain Shop': _basic_shop_defaults,
|
||||
'Kakariko Shop': _basic_shop_defaults,
|
||||
'Cave Shop (Lake Hylia)': _basic_shop_defaults,
|
||||
'Potion Shop': [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)],
|
||||
}
|
||||
|
||||
location_table = {'Mushroom': (0x180013, False, 'in the woods'),
|
||||
'Bottle Merchant': (0x2EB18, False, 'with a merchant'),
|
||||
|
|
65
Rom.py
65
Rom.py
|
@ -6,16 +6,17 @@ import os
|
|||
import struct
|
||||
import random
|
||||
|
||||
from BaseClasses import ShopType
|
||||
from Dungeons import dungeon_music_addresses
|
||||
from Text import MultiByteTextMapper, text_addresses, Credits
|
||||
from Text import Uncle_texts, Ganon1_texts, PyramidFairy_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, Blind_texts, BombShop2_texts
|
||||
from Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, DeathMountain_texts, LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names
|
||||
from Utils import local_path
|
||||
from Utils import local_path, int16_as_bytes, int32_as_bytes
|
||||
from Items import ItemFactory
|
||||
|
||||
|
||||
JAP10HASH = '03a63945398191337e896e5771f77173'
|
||||
RANDOMIZERBASEHASH = 'dc5840f0d1ef7b51009c5625a054b3dd'
|
||||
RANDOMIZERBASEHASH = '354bad0d01b6284c89f05c9b414d48c1'
|
||||
|
||||
|
||||
class JsonRom(object):
|
||||
|
@ -259,15 +260,6 @@ class Sprite(object):
|
|||
# split into palettes of 15 colors
|
||||
return array_chunk(palette_as_colors, 15)
|
||||
|
||||
|
||||
def int16_as_bytes(value):
|
||||
value = value & 0xFFFF
|
||||
return [value & 0xFF, (value >> 8) & 0xFF]
|
||||
|
||||
def int32_as_bytes(value):
|
||||
value = value & 0xFFFFFFFF
|
||||
return [value & 0xFF, (value >> 8) & 0xFF, (value >> 16) & 0xFF, (value >> 24) & 0xFF]
|
||||
|
||||
def patch_rom(world, rom, hashtable, beep='normal', color='red', sprite=None):
|
||||
# patch items
|
||||
for location in world.get_locations():
|
||||
|
@ -355,6 +347,7 @@ def patch_rom(world, rom, hashtable, beep='normal', color='red', sprite=None):
|
|||
# patch door table
|
||||
rom.write_byte(0xDBB73 + exit.addresses, exit.target)
|
||||
|
||||
write_custom_shops(rom, world)
|
||||
|
||||
# patch medallion requirements
|
||||
if world.required_medallions[0] == 'Bombos':
|
||||
|
@ -567,6 +560,19 @@ def patch_rom(world, rom, hashtable, beep='normal', color='red', sprite=None):
|
|||
prizes = [0xD8, 0xD8, 0xD8, 0xD8, 0xD9, 0xD8, 0xD8, 0xD9, 0xDA, 0xD9, 0xDA, 0xDB, 0xDA, 0xD9, 0xDA, 0xDA, 0xE0, 0xDF, 0xDF, 0xDA, 0xE0, 0xDF, 0xD8, 0xDF,
|
||||
0xDC, 0xDC, 0xDC, 0xDD, 0xDC, 0xDC, 0xDE, 0xDC, 0xE1, 0xD8, 0xE1, 0xE2, 0xE1, 0xD8, 0xE1, 0xE2, 0xDF, 0xD9, 0xD8, 0xE1, 0xDF, 0xDC, 0xD9, 0xD8,
|
||||
0xD8, 0xE3, 0xE0, 0xDB, 0xDE, 0xD8, 0xDB, 0xE2, 0xD9, 0xDA, 0xDB, 0xD9, 0xDB, 0xD9, 0xDB]
|
||||
dig_prizes = [0xB2, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8,
|
||||
0xD9, 0xD9, 0xD9, 0xD9, 0xD9, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA,
|
||||
0xDB, 0xDB, 0xDB, 0xDB, 0xDB, 0xDC, 0xDC, 0xDC, 0xDC, 0xDC,
|
||||
0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDE, 0xDE, 0xDE, 0xDE, 0xDE,
|
||||
0xDF, 0xDF, 0xDF, 0xDF, 0xDF, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0,
|
||||
0xE1, 0xE1, 0xE1, 0xE1, 0xE1, 0xE2, 0xE2, 0xE2, 0xE2, 0xE2,
|
||||
0xE3, 0xE3, 0xE3, 0xE3, 0xE3]
|
||||
if world.retro:
|
||||
prize_replacements = {0xE1: 0xDA, #5 Arrows -> Blue Rupee
|
||||
0xE2: 0xDB} #10 Arrows -> Red Rupee
|
||||
prizes = [prize_replacements.get(prize,prize) for prize in prizes]
|
||||
dig_prizes = [prize_replacements.get(prize,prize) for prize in dig_prizes]
|
||||
rom.write_bytes(0x180100, dig_prizes)
|
||||
random.shuffle(prizes)
|
||||
|
||||
# write tree pull prizes
|
||||
|
@ -808,15 +814,12 @@ def patch_rom(world, rom, hashtable, beep='normal', color='red', sprite=None):
|
|||
rom.write_bytes(0x6D2FB, [0x00, 0x00, 0xf7, 0xff, 0x02, 0x0E])
|
||||
rom.write_bytes(0x6D313, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E])
|
||||
|
||||
# Shop table
|
||||
rom.write_bytes(0x184800, [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])
|
||||
|
||||
# patch swamp: Need to enable permanent drain of water as dam or swamp were moved
|
||||
rom.write_byte(0x18003D, 0x01 if world.swamp_patch_required else 0x00)
|
||||
|
||||
# powder patch: remove the need to leave the scrren after powder, since it causes problems for potion shop at race game
|
||||
# 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 else [0xAD, 0xBF, 0x0A, 0xF0, 0x4F])
|
||||
rom.write_bytes(0x02F539, [0xEA, 0xEA, 0xEA, 0xEA, 0xEA] if world.powder_patch_required else [0xAD, 0xBF, 0x0A, 0xF0, 0x4F])
|
||||
|
||||
# allow smith into multi-entrance caves in appropriate shuffles
|
||||
if world.shuffle in ['restricted', 'full', 'crossed', 'insanity']:
|
||||
|
@ -851,6 +854,38 @@ def patch_rom(world, rom, hashtable, beep='normal', color='red', sprite=None):
|
|||
|
||||
return rom
|
||||
|
||||
def write_custom_shops(rom, world):
|
||||
shops = [shop for shop in world.shops if shop.replaceable and shop.active]
|
||||
|
||||
shop_data = bytearray()
|
||||
items_data = bytearray()
|
||||
sram_offset = 0
|
||||
|
||||
for shop_id, shop in enumerate(shops):
|
||||
if shop_id == len(shops) - 1:
|
||||
shop_id = 0xFF
|
||||
bytes = shop.get_bytes()
|
||||
bytes[0] = shop_id
|
||||
bytes[-1] = sram_offset
|
||||
if shop.type == ShopType.TakeAny:
|
||||
sram_offset += 1
|
||||
else:
|
||||
sram_offset += shop.item_count
|
||||
shop_data.extend(bytes)
|
||||
# [id][item][price-low][price-high][max][repl_id][repl_price-low][repl_price-high]
|
||||
for item in shop.inventory:
|
||||
if item is None:
|
||||
break
|
||||
item_data = [shop_id, ItemFactory(item['item']).code] + int16_as_bytes(item['price']) + [item['max'], ItemFactory(item['replacement']).code if item['replacement'] else 0xFF] + int16_as_bytes(item['replacement_price'])
|
||||
items_data.extend(item_data)
|
||||
|
||||
rom.write_bytes(0x184800, shop_data)
|
||||
|
||||
items_data.extend([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])
|
||||
rom.write_bytes(0x184900, items_data)
|
||||
|
||||
|
||||
|
||||
def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite):
|
||||
|
||||
# enable instant item menu
|
||||
|
|
36
Rules.py
36
Rules.py
|
@ -219,8 +219,8 @@ def global_rules(world):
|
|||
set_rule(world.get_entrance('Sewers Back Door'), lambda state: state.has_key('Small Key (Escape)'))
|
||||
|
||||
set_rule(world.get_location('Eastern Palace - Big Chest'), lambda state: state.has('Big Key (Eastern Palace)'))
|
||||
set_rule(world.get_location('Eastern Palace - Armos Knights'), lambda state: state.has('Bow') and state.has('Big Key (Eastern Palace)'))
|
||||
set_rule(world.get_location('Eastern Palace - Prize'), lambda state: state.has('Bow') and state.has('Big Key (Eastern Palace)'))
|
||||
set_rule(world.get_location('Eastern Palace - Armos Knights'), lambda state: state.can_shoot_arrows() and state.has('Big Key (Eastern Palace)'))
|
||||
set_rule(world.get_location('Eastern Palace - Prize'), lambda state: state.can_shoot_arrows() and state.has('Big Key (Eastern Palace)'))
|
||||
for location in ['Eastern Palace - Armos Knights', 'Eastern Palace - Big Chest']:
|
||||
forbid_item(world.get_location(location), 'Big Key (Eastern Palace)')
|
||||
|
||||
|
@ -228,9 +228,9 @@ def global_rules(world):
|
|||
set_rule(world.get_location('Desert Palace - Torch'), lambda state: state.has_Boots())
|
||||
set_rule(world.get_entrance('Desert Palace East Wing'), lambda state: state.has_key('Small Key (Desert Palace)'))
|
||||
set_rule(world.get_location('Desert Palace - Prize'), lambda state: state.has_key('Small Key (Desert Palace)') and state.has('Big Key (Desert Palace)') and state.has_fire_source() and
|
||||
(state.has_blunt_weapon() or state.has('Fire Rod') or state.has('Ice Rod') or state.has('Bow')))
|
||||
(state.has_blunt_weapon() or state.has('Fire Rod') or state.has('Ice Rod') or state.can_shoot_arrows()))
|
||||
set_rule(world.get_location('Desert Palace - Lanmolas'), lambda state: state.has_key('Small Key (Desert Palace)') and state.has('Big Key (Desert Palace)') and state.has_fire_source() and
|
||||
(state.has_blunt_weapon() or state.has('Fire Rod') or state.has('Ice Rod') or state.has('Bow')))
|
||||
(state.has_blunt_weapon() or state.has('Fire Rod') or state.has('Ice Rod') or state.can_shoot_arrows()))
|
||||
for location in ['Desert Palace - Lanmolas', 'Desert Palace - Big Chest']:
|
||||
forbid_item(world.get_location(location), 'Big Key (Desert Palace)')
|
||||
|
||||
|
@ -290,7 +290,7 @@ def global_rules(world):
|
|||
for location in ['Ice Palace - Big Chest', 'Ice Palace - Kholdstare']:
|
||||
forbid_item(world.get_location(location), 'Big Key (Ice Palace)')
|
||||
|
||||
set_rule(world.get_entrance('Misery Mire Entrance Gap'), lambda state: (state.has_Boots() or state.has('Hookshot')) and (state.has_sword() or state.has('Fire Rod') or state.has('Ice Rod') or state.has('Hammer') or state.has('Cane of Somaria') or state.has('Bow'))) # need to defeat wizzrobes, bombs don't work ...
|
||||
set_rule(world.get_entrance('Misery Mire Entrance Gap'), lambda state: (state.has_Boots() or state.has('Hookshot')) and (state.has_sword() or state.has('Fire Rod') or state.has('Ice Rod') or state.has('Hammer') or state.has('Cane of Somaria') or state.can_shoot_arrows())) # need to defeat wizzrobes, bombs don't work ...
|
||||
set_rule(world.get_location('Misery Mire - Big Chest'), lambda state: state.has('Big Key (Misery Mire)'))
|
||||
set_rule(world.get_location('Misery Mire - Spike Chest'), lambda state: (state.world.can_take_damage and state.has_hearts(4)) or state.has('Cane of Byrna') or state.has('Cape'))
|
||||
set_rule(world.get_entrance('Misery Mire Big Key Door'), lambda state: state.has('Big Key (Misery Mire)'))
|
||||
|
@ -304,7 +304,7 @@ def global_rules(world):
|
|||
(item_name(state, 'Misery Mire - Big Key Chest') in ['Big Key (Misery Mire)'])) else state.has_key('Small Key (Misery Mire)', 3))
|
||||
set_rule(world.get_location('Misery Mire - Compass Chest'), lambda state: state.has_fire_source())
|
||||
set_rule(world.get_location('Misery Mire - Big Key Chest'), lambda state: state.has_fire_source())
|
||||
set_rule(world.get_entrance('Misery Mire (Vitreous)'), lambda state: state.has('Cane of Somaria') and (state.has('Bow') or state.has_blunt_weapon()))
|
||||
set_rule(world.get_entrance('Misery Mire (Vitreous)'), lambda state: state.has('Cane of Somaria') and (state.can_shoot_arrows() or state.has_blunt_weapon()))
|
||||
for location in ['Misery Mire - Big Chest', 'Misery Mire - Vitreous']:
|
||||
forbid_item(world.get_location(location), 'Big Key (Misery Mire)')
|
||||
|
||||
|
@ -328,10 +328,10 @@ def global_rules(world):
|
|||
|
||||
set_trock_key_rules(world)
|
||||
|
||||
set_rule(world.get_entrance('Palace of Darkness Bonk Wall'), lambda state: state.has('Bow'))
|
||||
set_rule(world.get_entrance('Palace of Darkness Bonk Wall'), lambda state: state.can_shoot_arrows())
|
||||
set_rule(world.get_entrance('Palace of Darkness Hammer Peg Drop'), lambda state: state.has('Hammer'))
|
||||
set_rule(world.get_entrance('Palace of Darkness Bridge Room'), lambda state: state.has_key('Small Key (Palace of Darkness)', 1)) # If we can reach any other small key door, we already have back door access to this area
|
||||
set_rule(world.get_entrance('Palace of Darkness Big Key Door'), lambda state: state.has_key('Small Key (Palace of Darkness)', 6) and state.has('Big Key (Palace of Darkness)') and state.has('Bow') and state.has('Hammer'))
|
||||
set_rule(world.get_entrance('Palace of Darkness Big Key Door'), lambda state: state.has_key('Small Key (Palace of Darkness)', 6) and state.has('Big Key (Palace of Darkness)') and state.can_shoot_arrows() and state.has('Hammer'))
|
||||
set_rule(world.get_entrance('Palace of Darkness (North)'), lambda state: state.has_key('Small Key (Palace of Darkness)', 4))
|
||||
set_rule(world.get_location('Palace of Darkness - Big Chest'), lambda state: state.has('Big Key (Palace of Darkness)'))
|
||||
|
||||
|
@ -371,10 +371,10 @@ def global_rules(world):
|
|||
set_rule(world.get_location(location), lambda state: state.has('Fire Rod') and (state.has_key('Small Key (Ganons Tower)', 4) or (item_in_locations(state, 'Big Key (Ganons Tower)', compass_room_chests) and state.has_key('Small Key (Ganons Tower)', 3))))
|
||||
|
||||
set_rule(world.get_location('Ganons Tower - Big Chest'), lambda state: state.has('Big Key (Ganons Tower)'))
|
||||
set_rule(world.get_location('Ganons Tower - Big Key Room - Left'), lambda state: state.has('Bow') or state.has_blunt_weapon())
|
||||
set_rule(world.get_location('Ganons Tower - Big Key Chest'), lambda state: state.has('Bow') or state.has_blunt_weapon())
|
||||
set_rule(world.get_location('Ganons Tower - Big Key Room - Right'), lambda state: state.has('Bow') or state.has_blunt_weapon())
|
||||
set_rule(world.get_entrance('Ganons Tower Big Key Door'), lambda state: state.has('Big Key (Ganons Tower)') and state.has('Bow'))
|
||||
set_rule(world.get_location('Ganons Tower - Big Key Room - Left'), lambda state: state.can_shoot_arrows() or state.has_blunt_weapon())
|
||||
set_rule(world.get_location('Ganons Tower - Big Key Chest'), lambda state: state.can_shoot_arrows() or state.has_blunt_weapon())
|
||||
set_rule(world.get_location('Ganons Tower - Big Key Room - Right'), lambda state: state.can_shoot_arrows() or state.has_blunt_weapon())
|
||||
set_rule(world.get_entrance('Ganons Tower Big Key Door'), lambda state: state.has('Big Key (Ganons Tower)') and state.can_shoot_arrows())
|
||||
set_rule(world.get_entrance('Ganons Tower Torch Rooms'), lambda state: state.has_fire_source())
|
||||
set_rule(world.get_location('Ganons Tower - Pre-Moldorm Chest'), lambda state: state.has_key('Small Key (Ganons Tower)', 3))
|
||||
set_rule(world.get_entrance('Ganons Tower Moldorm Door'), lambda state: state.has_key('Small Key (Ganons Tower)', 4))
|
||||
|
@ -387,7 +387,7 @@ def global_rules(world):
|
|||
|
||||
set_rule(world.get_location('Ganon'), lambda state: state.has_beam_sword() and state.has_fire_source() and state.has('Crystal 1') and state.has('Crystal 2')
|
||||
and state.has('Crystal 3') and state.has('Crystal 4') and state.has('Crystal 5') and state.has('Crystal 6') and state.has('Crystal 7')
|
||||
and (state.has('Tempered Sword') or state.has('Golden Sword') or (state.has('Silver Arrows') and state.has('Bow')) or state.has('Lamp') or state.can_extend_magic(12))) # need to light torch a sufficient amount of times
|
||||
and (state.has('Tempered Sword') or state.has('Golden Sword') or (state.has('Silver Arrows') and state.can_shoot_arrows()) or state.has('Lamp') or state.can_extend_magic(12))) # need to light torch a sufficient amount of times
|
||||
set_rule(world.get_entrance('Ganon Drop'), lambda state: state.has_beam_sword()) # need to damage ganon to get tiles to drop
|
||||
|
||||
|
||||
|
@ -464,7 +464,7 @@ def swordless_rules(world):
|
|||
open_rules(world)
|
||||
|
||||
set_rule(world.get_entrance('Agahnims Tower'), lambda state: state.has('Cape') or state.has('Hammer') or state.has('Beat Agahnim 1')) # barrier gets removed after killing agahnim, relevant for entrance shuffle
|
||||
set_rule(world.get_entrance('Agahnim 1'), lambda state: (state.has('Hammer') or (state.has('Bug Catching Net') and (state.has('Fire Rod') or state.has('Bow') or state.has('Cane of Somaria')))) and state.has_key('Small Key (Agahnims Tower)', 2))
|
||||
set_rule(world.get_entrance('Agahnim 1'), lambda state: (state.has('Hammer') or (state.has('Bug Catching Net') and (state.has('Fire Rod') or state.can_shoot_arrows() or state.has('Cane of Somaria')))) and state.has_key('Small Key (Agahnims Tower)', 2))
|
||||
set_rule(world.get_location('Ether Tablet'), lambda state: state.has('Book of Mudora') and state.has('Hammer'))
|
||||
set_rule(world.get_location('Bombos Tablet'), lambda state: state.has('Book of Mudora') and state.has('Hammer') and state.has_Mirror())
|
||||
set_rule(world.get_entrance('Misery Mire'), lambda state: state.has_Pearl() and state.has_misery_mire_medallion()) # sword not required to use medallion for opening in swordless (!)
|
||||
|
@ -472,7 +472,7 @@ def swordless_rules(world):
|
|||
set_rule(world.get_entrance('Skull Woods Torch Room'), lambda state: state.has_key('Small Key (Skull Woods)', 3) and state.has('Fire Rod')) # no curtain
|
||||
set_rule(world.get_entrance('Ice Palace Entrance Room'), lambda state: state.has('Fire Rod') or state.has('Bombos')) #in swordless mode bombos pads are present in the relevant parts of ice palace
|
||||
set_rule(world.get_location('Agahnim 2'), lambda state: state.has('Hammer') or state.has('Bug Catching Net'))
|
||||
set_rule(world.get_location('Ganon'), lambda state: state.has('Hammer') and state.has_fire_source() and state.has('Silver Arrows') and state.has('Bow') and state.has('Crystal 1') and state.has('Crystal 2')
|
||||
set_rule(world.get_location('Ganon'), lambda state: state.has('Hammer') and state.has_fire_source() and state.has('Silver Arrows') and state.can_shoot_arrows() and state.has('Crystal 1') and state.has('Crystal 2')
|
||||
and state.has('Crystal 3') and state.has('Crystal 4') and state.has('Crystal 5') and state.has('Crystal 6') and state.has('Crystal 7'))
|
||||
set_rule(world.get_entrance('Ganon Drop'), lambda state: state.has('Hammer')) # need to damage ganon to get tiles to drop
|
||||
|
||||
|
@ -805,7 +805,7 @@ def set_bunny_rules(world):
|
|||
|
||||
# We will search entrances recursively until we find
|
||||
# one that leads to an exclusively light world region
|
||||
# for each such entrance a new option ios aded that consist of:
|
||||
# for each such entrance a new option is added that consist of:
|
||||
# a) being able to reach it, and
|
||||
# b) being able to access all entrances from there to `region`
|
||||
seen = set([region])
|
||||
|
@ -836,6 +836,10 @@ def set_bunny_rules(world):
|
|||
for exit in region.exits:
|
||||
add_rule(exit, rule)
|
||||
|
||||
paradox_shop = world.get_region('Light World Death Mountain Shop')
|
||||
if paradox_shop.is_dark_world:
|
||||
add_rule(paradox_shop.entrances[0], get_rule_to_add(paradox_shop))
|
||||
|
||||
# Add requirements for all locations that are actually in the dark world, except those available to the bunny
|
||||
for location in world.get_locations():
|
||||
if location.parent_region.is_dark_world:
|
||||
|
|
8
Utils.py
8
Utils.py
|
@ -2,6 +2,14 @@ import os
|
|||
import subprocess
|
||||
import sys
|
||||
|
||||
def int16_as_bytes(value):
|
||||
value = value & 0xFFFF
|
||||
return [value & 0xFF, (value >> 8) & 0xFF]
|
||||
|
||||
def int32_as_bytes(value):
|
||||
value = value & 0xFFFFFFFF
|
||||
return [value & 0xFF, (value >> 8) & 0xFF, (value >> 16) & 0xFF, (value >> 24) & 0xFF]
|
||||
|
||||
def is_bundled():
|
||||
return getattr(sys, 'frozen', False)
|
||||
|
||||
|
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue