Links Awakening: Implement New Game (#1334)
Adds Link's Awakening: DX. Fully imports and forks LADXR, with permission - https://github.com/daid/LADXR
This commit is contained in:
parent
67bf12369a
commit
81a239325d
|
@ -134,6 +134,8 @@ components: Iterable[Component] = (
|
|||
Component('SNI Client', 'SNIClient',
|
||||
file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3',
|
||||
'.apsmw', '.apl2ac')),
|
||||
Component('Links Awakening DX Client', 'LinksAwakeningClient',
|
||||
file_identifier=SuffixIdentifier('.apladx')),
|
||||
Component('LttP Adjuster', 'LttPAdjuster'),
|
||||
# Factorio
|
||||
Component('Factorio Client', 'FactorioClient'),
|
||||
|
|
|
@ -0,0 +1,609 @@
|
|||
import ModuleUpdate
|
||||
ModuleUpdate.update()
|
||||
|
||||
import Utils
|
||||
|
||||
if __name__ == "__main__":
|
||||
Utils.init_logging("LinksAwakeningContext", exception_logger="Client")
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import binascii
|
||||
import io
|
||||
import logging
|
||||
import select
|
||||
import socket
|
||||
import time
|
||||
import typing
|
||||
import urllib
|
||||
|
||||
import colorama
|
||||
|
||||
|
||||
from CommonClient import (CommonContext, get_base_parser, gui_enabled, logger,
|
||||
server_loop)
|
||||
from NetUtils import ClientStatus
|
||||
from worlds.ladx.Common import BASE_ID as LABaseID
|
||||
from worlds.ladx.GpsTracker import GpsTracker
|
||||
from worlds.ladx.ItemTracker import ItemTracker
|
||||
from worlds.ladx.LADXR.checkMetadata import checkMetadataTable
|
||||
from worlds.ladx.Locations import get_locations_to_id, meta_to_name
|
||||
from worlds.ladx.Tracker import LocationTracker, MagpieBridge
|
||||
|
||||
class GameboyException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class RetroArchDisconnectError(GameboyException):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidEmulatorStateError(GameboyException):
|
||||
pass
|
||||
|
||||
|
||||
class BadRetroArchResponse(GameboyException):
|
||||
pass
|
||||
|
||||
|
||||
def magpie_logo():
|
||||
from kivy.uix.image import CoreImage
|
||||
binary_data = """
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAAXN
|
||||
SR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA
|
||||
7DAcdvqGQAAADGSURBVDhPhVLBEcIwDHOYhjHCBuXHj2OTbAL8+
|
||||
MEGZIxOQ1CinOOk0Op0bmo7tlXXeR9FJMYDLOD9mwcLjQK7+hSZ
|
||||
wgcWMZJOAGeGKtChNHFL0j+FZD3jSCuo0w7l03wDrWdg00C4/aW
|
||||
eDEYNenuzPOfPspBnxf0kssE80vN0L8361j10P03DK4x6FHabuV
|
||||
ear8fHme+b17rwSjbAXeUMLb+EVTV2QHm46MWQanmnydA98KsVS
|
||||
XkV+qFpGQXrLhT/fqraQeQLuplpNH5g+WkAAAAASUVORK5CYII="""
|
||||
binary_data = base64.b64decode(binary_data)
|
||||
data = io.BytesIO(binary_data)
|
||||
return CoreImage(data, ext="png").texture
|
||||
|
||||
|
||||
class LAClientConstants:
|
||||
# Connector version
|
||||
VERSION = 0x01
|
||||
#
|
||||
# Memory locations of LADXR
|
||||
ROMGameID = 0x0051 # 4 bytes
|
||||
SlotName = 0x0134
|
||||
# Unused
|
||||
# ROMWorldID = 0x0055
|
||||
# ROMConnectorVersion = 0x0056
|
||||
# RO: We should only act if this is higher then 6, as it indicates that the game is running normally
|
||||
wGameplayType = 0xDB95
|
||||
# RO: Starts at 0, increases every time an item is received from the server and processed
|
||||
wLinkSyncSequenceNumber = 0xDDF6
|
||||
wLinkStatusBits = 0xDDF7 # RW:
|
||||
# Bit0: wLinkGive* contains valid data, set from script cleared from ROM.
|
||||
wLinkHealth = 0xDB5A
|
||||
wLinkGiveItem = 0xDDF8 # RW
|
||||
wLinkGiveItemFrom = 0xDDF9 # RW
|
||||
# All of these six bytes are unused, we can repurpose
|
||||
# wLinkSendItemRoomHigh = 0xDDFA # RO
|
||||
# wLinkSendItemRoomLow = 0xDDFB # RO
|
||||
# wLinkSendItemTarget = 0xDDFC # RO
|
||||
# wLinkSendItemItem = 0xDDFD # RO
|
||||
# wLinkSendShopItem = 0xDDFE # RO, which item to send (1 based, order of the shop items)
|
||||
# RO, which player to send to, but it's just the X position of the NPC used, so 0x18 is player 0
|
||||
# wLinkSendShopTarget = 0xDDFF
|
||||
|
||||
|
||||
wRecvIndex = 0xDDFE # 0xDB58
|
||||
wCheckAddress = 0xC0FF - 0x4
|
||||
WRamCheckSize = 0x4
|
||||
WRamSafetyValue = bytearray([0]*WRamCheckSize)
|
||||
|
||||
MinGameplayValue = 0x06
|
||||
MaxGameplayValue = 0x1A
|
||||
VictoryGameplayAndSub = 0x0102
|
||||
|
||||
|
||||
class RAGameboy():
|
||||
cache = []
|
||||
cache_start = 0
|
||||
cache_size = 0
|
||||
last_cache_read = None
|
||||
socket = None
|
||||
|
||||
def __init__(self, address, port) -> None:
|
||||
self.address = address
|
||||
self.port = port
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
assert (self.socket)
|
||||
self.socket.setblocking(False)
|
||||
|
||||
def get_retroarch_version(self):
|
||||
self.send(b'VERSION\n')
|
||||
select.select([self.socket], [], [])
|
||||
response_str, addr = self.socket.recvfrom(16)
|
||||
return response_str.rstrip()
|
||||
|
||||
def get_retroarch_status(self, timeout):
|
||||
self.send(b'GET_STATUS\n')
|
||||
select.select([self.socket], [], [], timeout)
|
||||
response_str, addr = self.socket.recvfrom(1000, )
|
||||
return response_str.rstrip()
|
||||
|
||||
def set_cache_limits(self, cache_start, cache_size):
|
||||
self.cache_start = cache_start
|
||||
self.cache_size = cache_size
|
||||
|
||||
def send(self, b):
|
||||
if type(b) is str:
|
||||
b = b.encode('ascii')
|
||||
self.socket.sendto(b, (self.address, self.port))
|
||||
|
||||
def recv(self):
|
||||
select.select([self.socket], [], [])
|
||||
response, _ = self.socket.recvfrom(4096)
|
||||
return response
|
||||
|
||||
async def async_recv(self):
|
||||
response = await asyncio.get_event_loop().sock_recv(self.socket, 4096)
|
||||
return response
|
||||
|
||||
async def check_safe_gameplay(self, throw=True):
|
||||
async def check_wram():
|
||||
check_values = await self.async_read_memory(LAClientConstants.wCheckAddress, LAClientConstants.WRamCheckSize)
|
||||
|
||||
if check_values != LAClientConstants.WRamSafetyValue:
|
||||
if throw:
|
||||
raise InvalidEmulatorStateError()
|
||||
return False
|
||||
return True
|
||||
|
||||
if not await check_wram():
|
||||
if throw:
|
||||
raise InvalidEmulatorStateError()
|
||||
return False
|
||||
|
||||
gameplay_value = await self.async_read_memory(LAClientConstants.wGameplayType)
|
||||
gameplay_value = gameplay_value[0]
|
||||
# In gameplay or credits
|
||||
if not (LAClientConstants.MinGameplayValue <= gameplay_value <= LAClientConstants.MaxGameplayValue) and gameplay_value != 0x1:
|
||||
if throw:
|
||||
logger.info("invalid emu state")
|
||||
raise InvalidEmulatorStateError()
|
||||
return False
|
||||
if not await check_wram():
|
||||
return False
|
||||
return True
|
||||
|
||||
# We're sadly unable to update the whole cache at once
|
||||
# as RetroArch only gives back some number of bytes at a time
|
||||
# So instead read as big as chunks at a time as we can manage
|
||||
async def update_cache(self):
|
||||
# First read the safety address - if it's invalid, bail
|
||||
self.cache = []
|
||||
|
||||
if not await self.check_safe_gameplay():
|
||||
return
|
||||
|
||||
cache = []
|
||||
remaining_size = self.cache_size
|
||||
while remaining_size:
|
||||
block = await self.async_read_memory(self.cache_start + len(cache), remaining_size)
|
||||
remaining_size -= len(block)
|
||||
cache += block
|
||||
|
||||
if not await self.check_safe_gameplay():
|
||||
return
|
||||
|
||||
self.cache = cache
|
||||
self.last_cache_read = time.time()
|
||||
|
||||
async def read_memory_cache(self, addresses):
|
||||
# TODO: can we just update once per frame?
|
||||
if not self.last_cache_read or self.last_cache_read + 0.1 < time.time():
|
||||
await self.update_cache()
|
||||
if not self.cache:
|
||||
return None
|
||||
assert (len(self.cache) == self.cache_size)
|
||||
for address in addresses:
|
||||
assert self.cache_start <= address <= self.cache_start + self.cache_size
|
||||
r = {address: self.cache[address - self.cache_start]
|
||||
for address in addresses}
|
||||
return r
|
||||
|
||||
async def async_read_memory_safe(self, address, size=1):
|
||||
# whenever we do a read for a check, we need to make sure that we aren't reading
|
||||
# garbage memory values - we also need to protect against reading a value, then the emulator resetting
|
||||
#
|
||||
# ...actually, we probably _only_ need the post check
|
||||
|
||||
# Check before read
|
||||
if not await self.check_safe_gameplay():
|
||||
return None
|
||||
|
||||
# Do read
|
||||
r = await self.async_read_memory(address, size)
|
||||
|
||||
# Check after read
|
||||
if not await self.check_safe_gameplay():
|
||||
return None
|
||||
|
||||
return r
|
||||
|
||||
def read_memory(self, address, size=1):
|
||||
command = "READ_CORE_MEMORY"
|
||||
|
||||
self.send(f'{command} {hex(address)} {size}\n')
|
||||
response = self.recv()
|
||||
|
||||
splits = response.decode().split(" ", 2)
|
||||
|
||||
assert (splits[0] == command)
|
||||
# Ignore the address for now
|
||||
|
||||
# TODO: transform to bytes
|
||||
if splits[2][:2] == "-1" or splits[0] != "READ_CORE_MEMORY":
|
||||
raise BadRetroArchResponse()
|
||||
return bytearray.fromhex(splits[2])
|
||||
|
||||
async def async_read_memory(self, address, size=1):
|
||||
command = "READ_CORE_MEMORY"
|
||||
|
||||
self.send(f'{command} {hex(address)} {size}\n')
|
||||
response = await self.async_recv()
|
||||
response = response[:-1]
|
||||
splits = response.decode().split(" ", 2)
|
||||
|
||||
assert (splits[0] == command)
|
||||
# Ignore the address for now
|
||||
|
||||
# TODO: transform to bytes
|
||||
return bytearray.fromhex(splits[2])
|
||||
|
||||
def write_memory(self, address, bytes):
|
||||
command = "WRITE_CORE_MEMORY"
|
||||
|
||||
self.send(f'{command} {hex(address)} {" ".join(hex(b) for b in bytes)}')
|
||||
select.select([self.socket], [], [])
|
||||
response, _ = self.socket.recvfrom(4096)
|
||||
|
||||
splits = response.decode().split(" ", 3)
|
||||
|
||||
assert (splits[0] == command)
|
||||
|
||||
if splits[2] == "-1":
|
||||
logger.info(splits[3])
|
||||
|
||||
|
||||
class LinksAwakeningClient():
|
||||
socket = None
|
||||
gameboy = None
|
||||
tracker = None
|
||||
auth = None
|
||||
game_crc = None
|
||||
pending_deathlink = False
|
||||
deathlink_debounce = True
|
||||
recvd_checks = {}
|
||||
|
||||
def msg(self, m):
|
||||
logger.info(m)
|
||||
s = f"SHOW_MSG {m}\n"
|
||||
self.gameboy.send(s)
|
||||
|
||||
def __init__(self, retroarch_address="127.0.0.1", retroarch_port=55355):
|
||||
self.gameboy = RAGameboy(retroarch_address, retroarch_port)
|
||||
|
||||
async def wait_for_retroarch_connection(self):
|
||||
logger.info("Waiting on connection to Retroarch...")
|
||||
while True:
|
||||
try:
|
||||
version = self.gameboy.get_retroarch_version()
|
||||
NO_CONTENT = b"GET_STATUS CONTENTLESS"
|
||||
status = NO_CONTENT
|
||||
core_type = None
|
||||
GAME_BOY = b"game_boy"
|
||||
while status == NO_CONTENT or core_type != GAME_BOY:
|
||||
try:
|
||||
status = self.gameboy.get_retroarch_status(0.1)
|
||||
if status.count(b" ") < 2:
|
||||
await asyncio.sleep(1.0)
|
||||
continue
|
||||
|
||||
GET_STATUS, PLAYING, info = status.split(b" ", 2)
|
||||
if status.count(b",") < 2:
|
||||
await asyncio.sleep(1.0)
|
||||
continue
|
||||
core_type, rom_name, self.game_crc = info.split(b",", 2)
|
||||
if core_type != GAME_BOY:
|
||||
logger.info(
|
||||
f"Core type should be '{GAME_BOY}', found {core_type} instead - wrong type of ROM?")
|
||||
await asyncio.sleep(1.0)
|
||||
continue
|
||||
except (BlockingIOError, TimeoutError) as e:
|
||||
await asyncio.sleep(0.1)
|
||||
pass
|
||||
logger.info(f"Connected to Retroarch {version} {info}")
|
||||
self.gameboy.read_memory(0x1000)
|
||||
return
|
||||
except ConnectionResetError:
|
||||
await asyncio.sleep(1.0)
|
||||
pass
|
||||
|
||||
def reset_auth(self):
|
||||
auth = binascii.hexlify(self.gameboy.read_memory(0x0134, 12)).decode()
|
||||
|
||||
if self.auth:
|
||||
assert (auth == self.auth)
|
||||
|
||||
self.auth = auth
|
||||
|
||||
async def wait_and_init_tracker(self):
|
||||
await self.wait_for_game_ready()
|
||||
self.tracker = LocationTracker(self.gameboy)
|
||||
self.item_tracker = ItemTracker(self.gameboy)
|
||||
self.gps_tracker = GpsTracker(self.gameboy)
|
||||
|
||||
async def recved_item_from_ap(self, item_id, from_player, next_index):
|
||||
# Don't allow getting an item until you've got your first check
|
||||
if not self.tracker.has_start_item():
|
||||
return
|
||||
|
||||
# Spin until we either:
|
||||
# get an exception from a bad read (emu shut down or reset)
|
||||
# beat the game
|
||||
# the client handles the last pending item
|
||||
status = (await self.gameboy.async_read_memory_safe(LAClientConstants.wLinkStatusBits))[0]
|
||||
while not (await self.is_victory()) and status & 1 == 1:
|
||||
time.sleep(0.1)
|
||||
status = (await self.gameboy.async_read_memory_safe(LAClientConstants.wLinkStatusBits))[0]
|
||||
|
||||
item_id -= LABaseID
|
||||
# The player name table only goes up to 100, so don't go past that
|
||||
# Even if it didn't, the remote player _index_ byte is just a byte, so 255 max
|
||||
if from_player > 100:
|
||||
from_player = 100
|
||||
|
||||
next_index += 1
|
||||
self.gameboy.write_memory(LAClientConstants.wLinkGiveItem, [
|
||||
item_id, from_player])
|
||||
status |= 1
|
||||
status = self.gameboy.write_memory(LAClientConstants.wLinkStatusBits, [status])
|
||||
self.gameboy.write_memory(LAClientConstants.wRecvIndex, [next_index])
|
||||
|
||||
async def wait_for_game_ready(self):
|
||||
logger.info("Waiting on game to be in valid state...")
|
||||
while not await self.gameboy.check_safe_gameplay(throw=False):
|
||||
pass
|
||||
logger.info("Ready!")
|
||||
last_index = 0
|
||||
|
||||
async def is_victory(self):
|
||||
return (await self.gameboy.read_memory_cache([LAClientConstants.wGameplayType]))[LAClientConstants.wGameplayType] == 1
|
||||
|
||||
async def main_tick(self, item_get_cb, win_cb, deathlink_cb):
|
||||
await self.tracker.readChecks(item_get_cb)
|
||||
await self.item_tracker.readItems()
|
||||
await self.gps_tracker.read_location()
|
||||
|
||||
next_index = self.gameboy.read_memory(LAClientConstants.wRecvIndex)[0]
|
||||
if next_index != self.last_index:
|
||||
self.last_index = next_index
|
||||
# logger.info(f"Got new index {next_index}")
|
||||
|
||||
current_health = (await self.gameboy.read_memory_cache([LAClientConstants.wLinkHealth]))[LAClientConstants.wLinkHealth]
|
||||
if self.deathlink_debounce and current_health != 0:
|
||||
self.deathlink_debounce = False
|
||||
elif not self.deathlink_debounce and current_health == 0:
|
||||
# logger.info("YOU DIED.")
|
||||
await deathlink_cb()
|
||||
self.deathlink_debounce = True
|
||||
|
||||
if self.pending_deathlink:
|
||||
logger.info("Got a deathlink")
|
||||
self.gameboy.write_memory(LAClientConstants.wLinkHealth, [0])
|
||||
self.pending_deathlink = False
|
||||
self.deathlink_debounce = True
|
||||
|
||||
if await self.is_victory():
|
||||
await win_cb()
|
||||
|
||||
recv_index = (await self.gameboy.async_read_memory_safe(LAClientConstants.wRecvIndex))[0]
|
||||
|
||||
# Play back one at a time
|
||||
if recv_index in self.recvd_checks:
|
||||
item = self.recvd_checks[recv_index]
|
||||
await self.recved_item_from_ap(item.item, item.player, recv_index)
|
||||
|
||||
|
||||
all_tasks = set()
|
||||
|
||||
def create_task_log_exception(awaitable) -> asyncio.Task:
|
||||
async def _log_exception(awaitable):
|
||||
try:
|
||||
return await awaitable
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
pass
|
||||
finally:
|
||||
all_tasks.remove(task)
|
||||
task = asyncio.create_task(_log_exception(awaitable))
|
||||
all_tasks.add(task)
|
||||
|
||||
|
||||
class LinksAwakeningContext(CommonContext):
|
||||
tags = {"AP"}
|
||||
game = "Links Awakening DX"
|
||||
items_handling = 0b101
|
||||
want_slot_data = True
|
||||
la_task = None
|
||||
client = None
|
||||
# TODO: does this need to re-read on reset?
|
||||
found_checks = []
|
||||
last_resend = time.time()
|
||||
|
||||
magpie = MagpieBridge()
|
||||
magpie_task = None
|
||||
won = False
|
||||
|
||||
def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str]) -> None:
|
||||
self.client = LinksAwakeningClient()
|
||||
super().__init__(server_address, password)
|
||||
|
||||
def run_gui(self) -> None:
|
||||
import webbrowser
|
||||
import kvui
|
||||
from kvui import Button, GameManager
|
||||
from kivy.uix.image import Image
|
||||
|
||||
class LADXManager(GameManager):
|
||||
logging_pairs = [
|
||||
("Client", "Archipelago"),
|
||||
("Tracker", "Tracker"),
|
||||
]
|
||||
base_title = "Archipelago Links Awakening DX Client"
|
||||
|
||||
def build(self):
|
||||
b = super().build()
|
||||
|
||||
button = Button(text="", size=(30, 30), size_hint_x=None,
|
||||
on_press=lambda _: webbrowser.open('https://magpietracker.us/?enable_autotracker=1'))
|
||||
image = Image(size=(16, 16), texture=magpie_logo())
|
||||
button.add_widget(image)
|
||||
|
||||
def set_center(_, center):
|
||||
image.center = center
|
||||
button.bind(center=set_center)
|
||||
|
||||
self.connect_layout.add_widget(button)
|
||||
return b
|
||||
|
||||
self.ui = LADXManager(self)
|
||||
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||
|
||||
async def send_checks(self):
|
||||
message = [{"cmd": 'LocationChecks', "locations": self.found_checks}]
|
||||
await self.send_msgs(message)
|
||||
|
||||
ENABLE_DEATHLINK = False
|
||||
async def send_deathlink(self):
|
||||
if self.ENABLE_DEATHLINK:
|
||||
message = [{"cmd": 'Deathlink',
|
||||
'time': time.time(),
|
||||
'cause': 'Had a nightmare',
|
||||
# 'source': self.slot_info[self.slot].name,
|
||||
}]
|
||||
await self.send_msgs(message)
|
||||
|
||||
async def send_victory(self):
|
||||
if not self.won:
|
||||
message = [{"cmd": "StatusUpdate",
|
||||
"status": ClientStatus.CLIENT_GOAL}]
|
||||
logger.info("victory!")
|
||||
await self.send_msgs(message)
|
||||
self.won = True
|
||||
|
||||
async def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None:
|
||||
if self.ENABLE_DEATHLINK:
|
||||
self.client.pending_deathlink = True
|
||||
|
||||
def new_checks(self, item_ids, ladxr_ids):
|
||||
self.found_checks += item_ids
|
||||
create_task_log_exception(self.send_checks())
|
||||
create_task_log_exception(self.magpie.send_new_checks(ladxr_ids))
|
||||
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
await super(LinksAwakeningContext, self).server_auth(password_requested)
|
||||
self.auth = self.client.auth
|
||||
await self.get_username()
|
||||
await self.send_connect()
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
if cmd == "Connected":
|
||||
self.game = self.slot_info[self.slot].game
|
||||
# TODO - use watcher_event
|
||||
if cmd == "ReceivedItems":
|
||||
for index, item in enumerate(args["items"], args["index"]):
|
||||
self.client.recvd_checks[index] = item
|
||||
|
||||
item_id_lookup = get_locations_to_id()
|
||||
|
||||
async def run_game_loop(self):
|
||||
def on_item_get(ladxr_checks):
|
||||
checks = [self.item_id_lookup[meta_to_name(
|
||||
checkMetadataTable[check.id])] for check in ladxr_checks]
|
||||
self.new_checks(checks, [check.id for check in ladxr_checks])
|
||||
|
||||
async def victory():
|
||||
await self.send_victory()
|
||||
|
||||
async def deathlink():
|
||||
await self.send_deathlink()
|
||||
|
||||
self.magpie_task = asyncio.create_task(self.magpie.serve())
|
||||
|
||||
# yield to allow UI to start
|
||||
await asyncio.sleep(0)
|
||||
|
||||
while True:
|
||||
try:
|
||||
# TODO: cancel all client tasks
|
||||
logger.info("(Re)Starting game loop")
|
||||
self.found_checks.clear()
|
||||
await self.client.wait_for_retroarch_connection()
|
||||
self.client.reset_auth()
|
||||
await self.client.wait_and_init_tracker()
|
||||
|
||||
while True:
|
||||
await self.client.main_tick(on_item_get, victory, deathlink)
|
||||
await asyncio.sleep(0.1)
|
||||
now = time.time()
|
||||
if self.last_resend + 5.0 < now:
|
||||
self.last_resend = now
|
||||
await self.send_checks()
|
||||
self.magpie.set_checks(self.client.tracker.all_checks)
|
||||
await self.magpie.set_item_tracker(self.client.item_tracker)
|
||||
await self.magpie.send_gps(self.client.gps_tracker)
|
||||
|
||||
except GameboyException:
|
||||
time.sleep(1.0)
|
||||
pass
|
||||
|
||||
|
||||
async def main():
|
||||
parser = get_base_parser(description="Link's Awakening Client.")
|
||||
parser.add_argument("--url", help="Archipelago connection url")
|
||||
|
||||
parser.add_argument('diff_file', default="", type=str, nargs="?",
|
||||
help='Path to a .apladx Archipelago Binary Patch file')
|
||||
args = parser.parse_args()
|
||||
logger.info(args)
|
||||
|
||||
if args.diff_file:
|
||||
import Patch
|
||||
logger.info("patch file was supplied - creating rom...")
|
||||
meta, rom_file = Patch.create_rom_file(args.diff_file)
|
||||
if "server" in meta:
|
||||
args.url = meta["server"]
|
||||
logger.info(f"wrote rom file to {rom_file}")
|
||||
|
||||
if args.url:
|
||||
url = urllib.parse.urlparse(args.url)
|
||||
args.connect = url.netloc
|
||||
if url.password:
|
||||
args.password = urllib.parse.unquote(url.password)
|
||||
|
||||
ctx = LinksAwakeningContext(args.connect, args.password)
|
||||
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
|
||||
|
||||
# TODO: nothing about the lambda about has to be in a lambda
|
||||
ctx.la_task = create_task_log_exception(ctx.run_game_loop())
|
||||
if gui_enabled:
|
||||
ctx.run_gui()
|
||||
ctx.run_cli()
|
||||
|
||||
await ctx.exit_event.wait()
|
||||
await ctx.shutdown()
|
||||
|
||||
if __name__ == '__main__':
|
||||
colorama.init()
|
||||
asyncio.run(main())
|
||||
colorama.deinit()
|
3
Utils.py
3
Utils.py
|
@ -260,6 +260,9 @@ def get_default_options() -> OptionsType:
|
|||
"lttp_options": {
|
||||
"rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc",
|
||||
},
|
||||
"ladx_options": {
|
||||
"rom_file": "Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc",
|
||||
},
|
||||
"server_options": {
|
||||
"host": None,
|
||||
"port": 38281,
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -93,6 +93,10 @@ sni_options:
|
|||
lttp_options:
|
||||
# File name of the v1.0 J rom
|
||||
rom_file: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"
|
||||
ladx_options:
|
||||
# File name of the Link's Awakening DX rom
|
||||
rom_file: "Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc"
|
||||
|
||||
lufia2ac_options:
|
||||
# File name of the US rom
|
||||
rom_file: "Lufia II - Rise of the Sinistrals (USA).sfc"
|
||||
|
@ -163,3 +167,4 @@ zillion_options:
|
|||
# RetroArch doesn't make it easy to launch a game from the command line.
|
||||
# You have to know the path to the emulator core library on the user's computer.
|
||||
rom_start: "retroarch"
|
||||
|
||||
|
|
|
@ -63,8 +63,10 @@ Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full
|
|||
Name: "generator/zl"; Description: "Zillion ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 150000; Flags: disablenouninstallwarning
|
||||
Name: "generator/pkmn_r"; Description: "Pokemon Red ROM Setup"; Types: full hosting
|
||||
Name: "generator/pkmn_b"; Description: "Pokemon Blue ROM Setup"; Types: full hosting
|
||||
Name: "generator/ladx"; Description: "Link's Awakening DX ROM Setup"; Types: full hosting
|
||||
Name: "server"; Description: "Server"; Types: full hosting
|
||||
Name: "client"; Description: "Clients"; Types: full playing
|
||||
Name: "client/la"; Description: "Links Awakening DX Client"; Types: full playing
|
||||
Name: "client/sni"; Description: "SNI Client"; Types: full playing
|
||||
Name: "client/sni/lttp"; Description: "SNI Client - A Link to the Past Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
|
||||
Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
|
||||
|
@ -78,6 +80,7 @@ Name: "client/ff1"; Description: "Final Fantasy 1"; Types: full playing
|
|||
Name: "client/pkmn"; Description: "Pokemon Client"
|
||||
Name: "client/pkmn/red"; Description: "Pokemon Client - Pokemon Red Setup"; Types: full playing; ExtraDiskSpaceRequired: 1048576
|
||||
Name: "client/pkmn/blue"; Description: "Pokemon Client - Pokemon Blue Setup"; Types: full playing; ExtraDiskSpaceRequired: 1048576
|
||||
Name: "client/ladx"; Description: "Link's Awakening Client"; Types: full playing; ExtraDiskSpaceRequired: 1048576
|
||||
Name: "client/cf"; Description: "ChecksFinder"; Types: full playing
|
||||
Name: "client/sc2"; Description: "Starcraft 2"; Types: full playing
|
||||
Name: "client/zl"; Description: "Zillion"; Types: full playing
|
||||
|
@ -97,6 +100,7 @@ Source: "{code:GetOoTROMPath}"; DestDir: "{app}"; DestName: "The Legend of Zelda
|
|||
Source: "{code:GetZlROMPath}"; DestDir: "{app}"; DestName: "Zillion (UE) [!].sms"; Flags: external; Components: client/zl or generator/zl
|
||||
Source: "{code:GetRedROMPath}"; DestDir: "{app}"; DestName: "Pokemon Red (UE) [S][!].gb"; Flags: external; Components: client/pkmn/red or generator/pkmn_r
|
||||
Source: "{code:GetBlueROMPath}"; DestDir: "{app}"; DestName: "Pokemon Blue (UE) [S][!].gb"; Flags: external; Components: client/pkmn/blue or generator/pkmn_b
|
||||
Source: "{code:GetLADXROMPath}"; DestDir: "{app}"; DestName: "Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc"; Flags: external; Components: client/ladx or generator/ladx
|
||||
Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, Archipelago*.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
Source: "{#source_path}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/sni
|
||||
Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp
|
||||
|
@ -106,6 +110,7 @@ Source: "{#source_path}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignorev
|
|||
Source: "{#source_path}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio
|
||||
Source: "{#source_path}\ArchipelagoTextClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/text
|
||||
Source: "{#source_path}\ArchipelagoSNIClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni
|
||||
Source: "{#source_path}\ArchipelagoLinksAwakeningClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/la
|
||||
Source: "{#source_path}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni/lttp or generator/lttp
|
||||
Source: "{#source_path}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft
|
||||
Source: "{#source_path}\ArchipelagoOoTClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot
|
||||
|
@ -219,6 +224,11 @@ Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch"; ValueData: "Ar
|
|||
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: ""; Components: client/pkmn
|
||||
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/pkmn
|
||||
|
||||
Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/ladx
|
||||
Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Archipelago Links Awakening DX Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/ladx
|
||||
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: ""; Components: client/ladx
|
||||
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\shell\open\command"; ValueData: """{app}\ArchipelagoLinksAwakeningClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/ladx
|
||||
|
||||
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: server
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: server
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: ""; Components: server
|
||||
|
@ -286,6 +296,9 @@ var RedROMFilePage: TInputFileWizardPage;
|
|||
var bluerom: string;
|
||||
var BlueROMFilePage: TInputFileWizardPage;
|
||||
|
||||
var ladxrom: string;
|
||||
var LADXROMFilePage: TInputFileWizardPage;
|
||||
|
||||
function GetSNESMD5OfFile(const rom: string): string;
|
||||
var data: AnsiString;
|
||||
begin
|
||||
|
@ -440,6 +453,12 @@ begin
|
|||
Result := not (OoTROMFilePage.Values[0] = '')
|
||||
else if (assigned(ZlROMFilePage)) and (CurPageID = ZlROMFilePage.ID) then
|
||||
Result := not (ZlROMFilePage.Values[0] = '')
|
||||
else if (assigned(RedROMFilePage)) and (CurPageID = RedROMFilePage.ID) then
|
||||
Result := not (RedROMFilePage.Values[0] = '')
|
||||
else if (assigned(BlueROMFilePage)) and (CurPageID = BlueROMFilePage.ID) then
|
||||
Result := not (BlueROMFilePage.Values[0] = '')
|
||||
else if (assigned(LADXROMFilePage)) and (CurPageID = LADXROMFilePage.ID) then
|
||||
Result := not (LADXROMFilePage.Values[0] = '')
|
||||
else
|
||||
Result := True;
|
||||
end;
|
||||
|
@ -576,7 +595,7 @@ function GetRedROMPath(Param: string): string;
|
|||
begin
|
||||
if Length(redrom) > 0 then
|
||||
Result := redrom
|
||||
else if Assigned(RedRomFilePage) then
|
||||
else if Assigned(RedROMFilePage) then
|
||||
begin
|
||||
R := CompareStr(GetMD5OfFile(RedROMFilePage.Values[0]), '3d45c1ee9abd5738df46d2bdda8b57dc')
|
||||
if R <> 0 then
|
||||
|
@ -592,7 +611,7 @@ function GetBlueROMPath(Param: string): string;
|
|||
begin
|
||||
if Length(bluerom) > 0 then
|
||||
Result := bluerom
|
||||
else if Assigned(BlueRomFilePage) then
|
||||
else if Assigned(BlueROMFilePage) then
|
||||
begin
|
||||
R := CompareStr(GetMD5OfFile(BlueROMFilePage.Values[0]), '50927e843568814f7ed45ec4f944bd8b')
|
||||
if R <> 0 then
|
||||
|
@ -604,6 +623,22 @@ begin
|
|||
Result := '';
|
||||
end;
|
||||
|
||||
function GetLADXROMPath(Param: string): string;
|
||||
begin
|
||||
if Length(ladxrom) > 0 then
|
||||
Result := ladxrom
|
||||
else if Assigned(LADXROMFilePage) then
|
||||
begin
|
||||
R := CompareStr(GetMD5OfFile(LADXROMFilePage.Values[0]), '07c211479386825042efb4ad31bb525f')
|
||||
if R <> 0 then
|
||||
MsgBox('Link''s Awakening DX ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||
|
||||
Result := LADXROMFilePage.Values[0]
|
||||
end
|
||||
else
|
||||
Result := '';
|
||||
end;
|
||||
|
||||
procedure InitializeWizard();
|
||||
begin
|
||||
AddOoTRomPage();
|
||||
|
@ -640,6 +675,10 @@ begin
|
|||
if Length(bluerom) = 0 then
|
||||
BlueROMFilePage:= AddGBRomPage('Pokemon Blue (UE) [S][!].gb');
|
||||
|
||||
ladxrom := CheckRom('Legend of Zelda, The - Link''s Awakening DX (USA, Europe) (SGB Enhanced).gbc','07c211479386825042efb4ad31bb525f');
|
||||
if Length(ladxrom) = 0 then
|
||||
LADXROMFilePage:= AddGBRomPage('Legend of Zelda, The - Link''s Awakening DX (USA, Europe) (SGB Enhanced).gbc');
|
||||
|
||||
l2acrom := CheckRom('Lufia II - Rise of the Sinistrals (USA).sfc', '6efc477d6203ed2b3b9133c1cd9e9c5d');
|
||||
if Length(l2acrom) = 0 then
|
||||
L2ACROMFilePage:= AddRomPage('Lufia II - Rise of the Sinistrals (USA).sfc');
|
||||
|
@ -669,4 +708,6 @@ begin
|
|||
Result := not (WizardIsComponentSelected('generator/pkmn_r') or WizardIsComponentSelected('client/pkmn/red'));
|
||||
if (assigned(BlueROMFilePage)) and (PageID = BlueROMFilePage.ID) then
|
||||
Result := not (WizardIsComponentSelected('generator/pkmn_b') or WizardIsComponentSelected('client/pkmn/blue'));
|
||||
if (assigned(LADXROMFilePage)) and (PageID = LADXROMFilePage.ID) then
|
||||
Result := not (WizardIsComponentSelected('generator/ladx') or WizardIsComponentSelected('client/ladx'));
|
||||
end;
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
LINKS_AWAKENING = "Links Awakening DX"
|
||||
BASE_ID = 10000000
|
|
@ -0,0 +1,92 @@
|
|||
import json
|
||||
roomAddress = 0xFFF6
|
||||
mapIdAddress = 0xFFF7
|
||||
indoorFlagAddress = 0xDBA5
|
||||
entranceRoomOffset = 0xD800
|
||||
screenCoordAddress = 0xFFFA
|
||||
|
||||
mapMap = {
|
||||
0x00: 0x01,
|
||||
0x01: 0x01,
|
||||
0x02: 0x01,
|
||||
0x03: 0x01,
|
||||
0x04: 0x01,
|
||||
0x05: 0x01,
|
||||
0x06: 0x02,
|
||||
0x07: 0x02,
|
||||
0x08: 0x02,
|
||||
0x09: 0x02,
|
||||
0x0A: 0x02,
|
||||
0x0B: 0x02,
|
||||
0x0C: 0x02,
|
||||
0x0D: 0x02,
|
||||
0x0E: 0x02,
|
||||
0x0F: 0x02,
|
||||
0x10: 0x02,
|
||||
0x11: 0x02,
|
||||
0x12: 0x02,
|
||||
0x13: 0x02,
|
||||
0x14: 0x02,
|
||||
0x15: 0x02,
|
||||
0x16: 0x02,
|
||||
0x17: 0x02,
|
||||
0x18: 0x02,
|
||||
0x19: 0x02,
|
||||
0x1D: 0x01,
|
||||
0x1E: 0x01,
|
||||
0x1F: 0x01,
|
||||
0xFF: 0x03,
|
||||
}
|
||||
|
||||
class GpsTracker:
|
||||
room = None
|
||||
location_changed = False
|
||||
screenX = 0
|
||||
screenY = 0
|
||||
indoors = None
|
||||
|
||||
def __init__(self, gameboy) -> None:
|
||||
self.gameboy = gameboy
|
||||
|
||||
async def read_byte(self, b):
|
||||
return (await self.gameboy.async_read_memory(b))[0]
|
||||
|
||||
async def read_location(self):
|
||||
indoors = await self.read_byte(indoorFlagAddress)
|
||||
|
||||
if indoors != self.indoors and self.indoors != None:
|
||||
self.indoorsChanged = True
|
||||
|
||||
self.indoors = indoors
|
||||
|
||||
mapId = await self.read_byte(mapIdAddress)
|
||||
if mapId not in mapMap:
|
||||
print(f'Unknown map ID {hex(mapId)}')
|
||||
return
|
||||
|
||||
mapDigit = mapMap[mapId] << 8 if indoors else 0
|
||||
last_room = self.room
|
||||
self.room = await self.read_byte(roomAddress) + mapDigit
|
||||
|
||||
coords = await self.read_byte(screenCoordAddress)
|
||||
self.screenX = coords & 0x0F
|
||||
self.screenY = (coords & 0xF0) >> 4
|
||||
|
||||
if (self.room != last_room):
|
||||
self.location_changed = True
|
||||
|
||||
last_message = {}
|
||||
async def send_location(self, socket, diff=False):
|
||||
if self.room is None:
|
||||
return
|
||||
message = {
|
||||
"type":"location",
|
||||
"refresh": True,
|
||||
"version":"1.0",
|
||||
"room": f'0x{self.room:02X}',
|
||||
"x": self.screenX,
|
||||
"y": self.screenY,
|
||||
}
|
||||
if message != self.last_message:
|
||||
self.last_message = message
|
||||
await socket.send(json.dumps(message))
|
|
@ -0,0 +1,283 @@
|
|||
import json
|
||||
gameStateAddress = 0xDB95
|
||||
validGameStates = {0x0B, 0x0C}
|
||||
gameStateResetThreshold = 0x06
|
||||
|
||||
inventorySlotCount = 16
|
||||
inventoryStartAddress = 0xDB00
|
||||
inventoryEndAddress = inventoryStartAddress + inventorySlotCount
|
||||
|
||||
inventoryItemIds = {
|
||||
0x02: 'BOMB',
|
||||
0x05: 'BOW',
|
||||
0x06: 'HOOKSHOT',
|
||||
0x07: 'MAGIC_ROD',
|
||||
0x08: 'PEGASUS_BOOTS',
|
||||
0x09: 'OCARINA',
|
||||
0x0A: 'FEATHER',
|
||||
0x0B: 'SHOVEL',
|
||||
0x0C: 'MAGIC_POWDER',
|
||||
0x0D: 'BOOMERANG',
|
||||
0x0E: 'TOADSTOOL',
|
||||
0x0F: 'ROOSTER',
|
||||
}
|
||||
|
||||
dungeonKeyDoors = [
|
||||
{ # D1
|
||||
0xD907: [0x04],
|
||||
0xD909: [0x40],
|
||||
0xD90F: [0x01],
|
||||
},
|
||||
{ # D2
|
||||
0xD921: [0x02],
|
||||
0xD925: [0x02],
|
||||
0xD931: [0x02],
|
||||
0xD932: [0x08],
|
||||
0xD935: [0x04],
|
||||
},
|
||||
{ # D3
|
||||
0xD945: [0x40],
|
||||
0xD946: [0x40],
|
||||
0xD949: [0x40],
|
||||
0xD94A: [0x40],
|
||||
0xD956: [0x01, 0x02, 0x04, 0x08],
|
||||
},
|
||||
{ # D4
|
||||
0xD969: [0x04],
|
||||
0xD96A: [0x40],
|
||||
0xD96E: [0x40],
|
||||
0xD978: [0x01],
|
||||
0xD979: [0x04],
|
||||
},
|
||||
{ # D5
|
||||
0xD98C: [0x40],
|
||||
0xD994: [0x40],
|
||||
0xD99F: [0x04],
|
||||
},
|
||||
{ # D6
|
||||
0xD9C3: [0x40],
|
||||
0xD9C6: [0x40],
|
||||
0xD9D0: [0x04],
|
||||
},
|
||||
{ # D7
|
||||
0xDA10: [0x04],
|
||||
0xDA1E: [0x40],
|
||||
0xDA21: [0x40],
|
||||
},
|
||||
{ # D8
|
||||
0xDA39: [0x02],
|
||||
0xDA3B: [0x01],
|
||||
0xDA42: [0x40],
|
||||
0xDA43: [0x40],
|
||||
0xDA44: [0x40],
|
||||
0xDA49: [0x40],
|
||||
0xDA4A: [0x01],
|
||||
},
|
||||
{ # D0(9)
|
||||
0xDDE5: [0x02],
|
||||
0xDDE9: [0x04],
|
||||
0xDDF0: [0x04],
|
||||
},
|
||||
]
|
||||
|
||||
dungeonItemAddresses = [
|
||||
0xDB16, # D1
|
||||
0xDB1B, # D2
|
||||
0xDB20, # D3
|
||||
0xDB25, # D4
|
||||
0xDB2A, # D5
|
||||
0xDB2F, # D6
|
||||
0xDB34, # D7
|
||||
0xDB39, # D8
|
||||
0xDDDA, # Color Dungeon
|
||||
]
|
||||
|
||||
dungeonItemOffsets = {
|
||||
'MAP{}': 0,
|
||||
'COMPASS{}': 1,
|
||||
'STONE_BEAK{}': 2,
|
||||
'NIGHTMARE_KEY{}': 3,
|
||||
'KEY{}': 4,
|
||||
}
|
||||
|
||||
class Item:
|
||||
def __init__(self, id, address, threshold=0, mask=None, increaseOnly=False, count=False, max=None):
|
||||
self.id = id
|
||||
self.address = address
|
||||
self.threshold = threshold
|
||||
self.mask = mask
|
||||
self.increaseOnly = increaseOnly
|
||||
self.count = count
|
||||
self.value = 0 if increaseOnly else None
|
||||
self.rawValue = 0
|
||||
self.diff = 0
|
||||
self.max = max
|
||||
|
||||
def set(self, byte, extra):
|
||||
oldValue = self.value
|
||||
|
||||
if self.mask:
|
||||
byte = byte & self.mask
|
||||
|
||||
if not self.count:
|
||||
byte = int(byte > self.threshold)
|
||||
else:
|
||||
# LADX seems to store one decimal digit per nibble
|
||||
byte = byte - (byte // 16 * 6)
|
||||
|
||||
byte += extra
|
||||
|
||||
if self.max and byte > self.max:
|
||||
byte = self.max
|
||||
|
||||
if self.increaseOnly:
|
||||
if byte > self.rawValue:
|
||||
self.value += byte - self.rawValue
|
||||
else:
|
||||
self.value = byte
|
||||
|
||||
self.rawValue = byte
|
||||
|
||||
if oldValue != self.value:
|
||||
self.diff += self.value - (oldValue or 0)
|
||||
|
||||
class ItemTracker:
|
||||
def __init__(self, gameboy) -> None:
|
||||
self.gameboy = gameboy
|
||||
self.loadItems()
|
||||
pass
|
||||
extraItems = {}
|
||||
|
||||
async def readRamByte(self, byte):
|
||||
return (await self.gameboy.read_memory_cache([byte]))[byte]
|
||||
|
||||
def loadItems(self):
|
||||
self.items = [
|
||||
Item('BOMB', None),
|
||||
Item('BOW', None),
|
||||
Item('HOOKSHOT', None),
|
||||
Item('MAGIC_ROD', None),
|
||||
Item('PEGASUS_BOOTS', None),
|
||||
Item('OCARINA', None),
|
||||
Item('FEATHER', None),
|
||||
Item('SHOVEL', None),
|
||||
Item('MAGIC_POWDER', None),
|
||||
Item('BOOMERANG', None),
|
||||
Item('TOADSTOOL', None),
|
||||
Item('ROOSTER', None),
|
||||
Item('SWORD', 0xDB4E, count=True),
|
||||
Item('POWER_BRACELET', 0xDB43, count=True),
|
||||
Item('SHIELD', 0xDB44, count=True),
|
||||
Item('BOWWOW', 0xDB56),
|
||||
Item('MAX_POWDER_UPGRADE', 0xDB76, threshold=0x20),
|
||||
Item('MAX_BOMBS_UPGRADE', 0xDB77, threshold=0x30),
|
||||
Item('MAX_ARROWS_UPGRADE', 0xDB78, threshold=0x30),
|
||||
Item('TAIL_KEY', 0xDB11),
|
||||
Item('SLIME_KEY', 0xDB15),
|
||||
Item('ANGLER_KEY', 0xDB12),
|
||||
Item('FACE_KEY', 0xDB13),
|
||||
Item('BIRD_KEY', 0xDB14),
|
||||
Item('FLIPPERS', 0xDB3E),
|
||||
Item('SEASHELL', 0xDB41, count=True),
|
||||
Item('GOLD_LEAF', 0xDB42, count=True, max=5),
|
||||
Item('INSTRUMENT1', 0xDB65, mask=1 << 1),
|
||||
Item('INSTRUMENT2', 0xDB66, mask=1 << 1),
|
||||
Item('INSTRUMENT3', 0xDB67, mask=1 << 1),
|
||||
Item('INSTRUMENT4', 0xDB68, mask=1 << 1),
|
||||
Item('INSTRUMENT5', 0xDB69, mask=1 << 1),
|
||||
Item('INSTRUMENT6', 0xDB6A, mask=1 << 1),
|
||||
Item('INSTRUMENT7', 0xDB6B, mask=1 << 1),
|
||||
Item('INSTRUMENT8', 0xDB6C, mask=1 << 1),
|
||||
Item('TRADING_ITEM_YOSHI_DOLL', 0xDB40, mask=1 << 0),
|
||||
Item('TRADING_ITEM_RIBBON', 0xDB40, mask=1 << 1),
|
||||
Item('TRADING_ITEM_DOG_FOOD', 0xDB40, mask=1 << 2),
|
||||
Item('TRADING_ITEM_BANANAS', 0xDB40, mask=1 << 3),
|
||||
Item('TRADING_ITEM_STICK', 0xDB40, mask=1 << 4),
|
||||
Item('TRADING_ITEM_HONEYCOMB', 0xDB40, mask=1 << 5),
|
||||
Item('TRADING_ITEM_PINEAPPLE', 0xDB40, mask=1 << 6),
|
||||
Item('TRADING_ITEM_HIBISCUS', 0xDB40, mask=1 << 7),
|
||||
Item('TRADING_ITEM_LETTER', 0xDB7F, mask=1 << 0),
|
||||
Item('TRADING_ITEM_BROOM', 0xDB7F, mask=1 << 1),
|
||||
Item('TRADING_ITEM_FISHING_HOOK', 0xDB7F, mask=1 << 2),
|
||||
Item('TRADING_ITEM_NECKLACE', 0xDB7F, mask=1 << 3),
|
||||
Item('TRADING_ITEM_SCALE', 0xDB7F, mask=1 << 4),
|
||||
Item('TRADING_ITEM_MAGNIFYING_GLASS', 0xDB7F, mask=1 << 5),
|
||||
Item('SONG1', 0xDB49, mask=1 << 2),
|
||||
Item('SONG2', 0xDB49, mask=1 << 1),
|
||||
Item('SONG3', 0xDB49, mask=1 << 0),
|
||||
Item('RED_TUNIC', 0xDB6D, mask=1 << 0),
|
||||
Item('BLUE_TUNIC', 0xDB6D, mask=1 << 1),
|
||||
Item('GREAT_FAIRY', 0xDDE1, mask=1 << 4),
|
||||
]
|
||||
|
||||
for i in range(len(dungeonItemAddresses)):
|
||||
for item, offset in dungeonItemOffsets.items():
|
||||
if item.startswith('KEY'):
|
||||
self.items.append(Item(item.format(i + 1), dungeonItemAddresses[i] + offset, count=True))
|
||||
else:
|
||||
self.items.append(Item(item.format(i + 1), dungeonItemAddresses[i] + offset))
|
||||
|
||||
self.itemDict = {item.id: item for item in self.items}
|
||||
|
||||
async def readItems(state):
|
||||
extraItems = state.extraItems
|
||||
missingItems = {x for x in state.items if x.address == None}
|
||||
|
||||
# Add keys for opened key doors
|
||||
for i in range(len(dungeonKeyDoors)):
|
||||
item = f'KEY{i + 1}'
|
||||
extraItems[item] = 0
|
||||
|
||||
for address, masks in dungeonKeyDoors[i].items():
|
||||
for mask in masks:
|
||||
value = await state.readRamByte(address) & mask
|
||||
if value > 0:
|
||||
extraItems[item] += 1
|
||||
|
||||
# Main inventory items
|
||||
for i in range(inventoryStartAddress, inventoryEndAddress):
|
||||
value = await state.readRamByte(i)
|
||||
|
||||
if value in inventoryItemIds:
|
||||
item = state.itemDict[inventoryItemIds[value]]
|
||||
extra = extraItems[item.id] if item.id in extraItems else 0
|
||||
item.set(1, extra)
|
||||
missingItems.remove(item)
|
||||
|
||||
for item in missingItems:
|
||||
extra = extraItems[item.id] if item.id in extraItems else 0
|
||||
item.set(0, extra)
|
||||
|
||||
# All other items
|
||||
for item in [x for x in state.items if x.address]:
|
||||
extra = extraItems[item.id] if item.id in extraItems else 0
|
||||
item.set(await state.readRamByte(item.address), extra)
|
||||
|
||||
async def sendItems(self, socket, diff=False):
|
||||
if not self.items:
|
||||
return
|
||||
message = {
|
||||
"type":"item",
|
||||
"refresh": True,
|
||||
"version":"1.0",
|
||||
"diff": diff,
|
||||
"items": [],
|
||||
}
|
||||
items = self.items
|
||||
if diff:
|
||||
items = [item for item in items if item.diff != 0]
|
||||
if not items:
|
||||
return
|
||||
for item in items:
|
||||
value = item.diff if diff else item.value
|
||||
|
||||
message["items"].append(
|
||||
{
|
||||
'id': item.id,
|
||||
'qty': value,
|
||||
}
|
||||
)
|
||||
|
||||
item.diff = 0
|
||||
|
||||
await socket.send(json.dumps(message))
|
|
@ -0,0 +1,304 @@
|
|||
from BaseClasses import Item, ItemClassification
|
||||
from . import Common
|
||||
import typing
|
||||
from enum import IntEnum
|
||||
from .LADXR.locations.constants import CHEST_ITEMS
|
||||
|
||||
class ItemData(typing.NamedTuple):
|
||||
item_name: str
|
||||
ladxr_id: str
|
||||
classification: ItemClassification
|
||||
mark_only_first_progression: bool = False
|
||||
created_for_players = set()
|
||||
@property
|
||||
def item_id(self):
|
||||
return CHEST_ITEMS[self.ladxr_id]
|
||||
|
||||
|
||||
class DungeonItemType(IntEnum):
|
||||
INSTRUMENT = 0
|
||||
NIGHTMARE_KEY = 1
|
||||
KEY = 2
|
||||
STONE_BEAK = 3
|
||||
MAP = 4
|
||||
COMPASS = 5
|
||||
|
||||
class DungeonItemData(ItemData):
|
||||
@property
|
||||
def dungeon_index(self):
|
||||
return int(self.ladxr_id[-1])
|
||||
|
||||
@property
|
||||
def dungeon_item_type(self):
|
||||
s = self.ladxr_id[:-1]
|
||||
return DungeonItemType.__dict__[s]
|
||||
|
||||
class LinksAwakeningItem(Item):
|
||||
game: str = Common.LINKS_AWAKENING
|
||||
|
||||
def __init__(self, item_data, world, player):
|
||||
classification = item_data.classification
|
||||
if callable(classification):
|
||||
classification = classification(world, player)
|
||||
# this doesn't work lol
|
||||
MARK_FIRST_ITEM = False
|
||||
if MARK_FIRST_ITEM:
|
||||
if item_data.mark_only_first_progression:
|
||||
if player in item_data.created_for_players:
|
||||
classification = ItemClassification.filler
|
||||
else:
|
||||
item_data.created_for_players.add(player)
|
||||
super().__init__(item_data.item_name, classification, Common.BASE_ID + item_data.item_id, player)
|
||||
self.item_data = item_data
|
||||
|
||||
# TODO: use _NAMES instead?
|
||||
class ItemName:
|
||||
POWER_BRACELET = "Progressive Power Bracelet"
|
||||
SHIELD = "Progressive Shield"
|
||||
BOW = "Bow"
|
||||
HOOKSHOT = "Hookshot"
|
||||
MAGIC_ROD = "Magic Rod"
|
||||
PEGASUS_BOOTS = "Pegasus Boots"
|
||||
OCARINA = "Ocarina"
|
||||
FEATHER = "Feather"
|
||||
SHOVEL = "Shovel"
|
||||
MAGIC_POWDER = "Magic Powder"
|
||||
BOMB = "Bomb"
|
||||
SWORD = "Progressive Sword"
|
||||
FLIPPERS = "Flippers"
|
||||
MAGNIFYING_LENS = "Magnifying Lens"
|
||||
MEDICINE = "Medicine"
|
||||
TAIL_KEY = "Tail Key"
|
||||
ANGLER_KEY = "Angler Key"
|
||||
FACE_KEY = "Face Key"
|
||||
BIRD_KEY = "Bird Key"
|
||||
SLIME_KEY = "Slime Key"
|
||||
GOLD_LEAF = "Gold Leaf"
|
||||
RUPEES_20 = "20 Rupees"
|
||||
RUPEES_50 = "50 Rupees"
|
||||
RUPEES_100 = "100 Rupees"
|
||||
RUPEES_200 = "200 Rupees"
|
||||
RUPEES_500 = "500 Rupees"
|
||||
SEASHELL = "Seashell"
|
||||
MESSAGE = "Master Stalfos' Message"
|
||||
GEL = "Gel"
|
||||
BOOMERANG = "Boomerang"
|
||||
HEART_PIECE = "Heart Piece"
|
||||
BOWWOW = "BowWow"
|
||||
ARROWS_10 = "10 Arrows"
|
||||
SINGLE_ARROW = "Single Arrow"
|
||||
ROOSTER = "Rooster"
|
||||
MAX_POWDER_UPGRADE = "Max Powder Upgrade"
|
||||
MAX_BOMBS_UPGRADE = "Max Bombs Upgrade"
|
||||
MAX_ARROWS_UPGRADE = "Max Arrows Upgrade"
|
||||
RED_TUNIC = "Red Tunic"
|
||||
BLUE_TUNIC = "Blue Tunic"
|
||||
HEART_CONTAINER = "Heart Container"
|
||||
BAD_HEART_CONTAINER = "Bad Heart Container"
|
||||
TOADSTOOL = "Toadstool"
|
||||
KEY = "Key"
|
||||
KEY1 = "Small Key (Tail Cave)"
|
||||
KEY2 = "Small Key (Bottle Grotto)"
|
||||
KEY3 = "Small Key (Key Cavern)"
|
||||
KEY4 = "Small Key (Angler's Tunnel)"
|
||||
KEY5 = "Small Key (Catfish's Maw)"
|
||||
KEY6 = "Small Key (Face Shrine)"
|
||||
KEY7 = "Small Key (Eagle's Tower)"
|
||||
KEY8 = "Small Key (Turtle Rock)"
|
||||
KEY9 = "Small Key (Color Dungeon)"
|
||||
NIGHTMARE_KEY = "Nightmare Key"
|
||||
NIGHTMARE_KEY1 = "Nightmare Key (Tail Cave)"
|
||||
NIGHTMARE_KEY2 = "Nightmare Key (Bottle Grotto)"
|
||||
NIGHTMARE_KEY3 = "Nightmare Key (Key Cavern)"
|
||||
NIGHTMARE_KEY4 = "Nightmare Key (Angler's Tunnel)"
|
||||
NIGHTMARE_KEY5 = "Nightmare Key (Catfish's Maw)"
|
||||
NIGHTMARE_KEY6 = "Nightmare Key (Face Shrine)"
|
||||
NIGHTMARE_KEY7 = "Nightmare Key (Eagle's Tower)"
|
||||
NIGHTMARE_KEY8 = "Nightmare Key (Turtle Rock)"
|
||||
NIGHTMARE_KEY9 = "Nightmare Key (Color Dungeon)"
|
||||
MAP = "Map"
|
||||
MAP1 = "Dungeon Map (Tail Cave)"
|
||||
MAP2 = "Dungeon Map (Bottle Grotto)"
|
||||
MAP3 = "Dungeon Map (Key Cavern)"
|
||||
MAP4 = "Dungeon Map (Angler's Tunnel)"
|
||||
MAP5 = "Dungeon Map (Catfish's Maw)"
|
||||
MAP6 = "Dungeon Map (Face Shrine)"
|
||||
MAP7 = "Dungeon Map (Eagle's Tower)"
|
||||
MAP8 = "Dungeon Map (Turtle Rock)"
|
||||
MAP9 = "Dungeon Map (Color Dungeon)"
|
||||
COMPASS = "Compass"
|
||||
COMPASS1 = "Compass (Tail Cave)"
|
||||
COMPASS2 = "Compass (Bottle Grotto)"
|
||||
COMPASS3 = "Compass (Key Cavern)"
|
||||
COMPASS4 = "Compass (Angler's Tunnel)"
|
||||
COMPASS5 = "Compass (Catfish's Maw)"
|
||||
COMPASS6 = "Compass (Face Shrine)"
|
||||
COMPASS7 = "Compass (Eagle's Tower)"
|
||||
COMPASS8 = "Compass (Turtle Rock)"
|
||||
COMPASS9 = "Compass (Color Dungeon)"
|
||||
STONE_BEAK = "Stone Beak"
|
||||
STONE_BEAK1 = "Stone Beak (Tail Cave)"
|
||||
STONE_BEAK2 = "Stone Beak (Bottle Grotto)"
|
||||
STONE_BEAK3 = "Stone Beak (Key Cavern)"
|
||||
STONE_BEAK4 = "Stone Beak (Angler's Tunnel)"
|
||||
STONE_BEAK5 = "Stone Beak (Catfish's Maw)"
|
||||
STONE_BEAK6 = "Stone Beak (Face Shrine)"
|
||||
STONE_BEAK7 = "Stone Beak (Eagle's Tower)"
|
||||
STONE_BEAK8 = "Stone Beak (Turtle Rock)"
|
||||
STONE_BEAK9 = "Stone Beak (Color Dungeon)"
|
||||
SONG1 = "Ballad of the Wind Fish"
|
||||
SONG2 = "Manbo's Mambo"
|
||||
SONG3 = "Frog's Song of Soul"
|
||||
INSTRUMENT1 = "Full Moon Cello"
|
||||
INSTRUMENT2 = "Conch Horn"
|
||||
INSTRUMENT3 = "Sea Lily's Bell"
|
||||
INSTRUMENT4 = "Surf Harp"
|
||||
INSTRUMENT5 = "Wind Marimba"
|
||||
INSTRUMENT6 = "Coral Triangle"
|
||||
INSTRUMENT7 = "Organ of Evening Calm"
|
||||
INSTRUMENT8 = "Thunder Drum"
|
||||
TRADING_ITEM_YOSHI_DOLL = "Yoshi Doll"
|
||||
TRADING_ITEM_RIBBON = "Ribbon"
|
||||
TRADING_ITEM_DOG_FOOD = "Dog Food"
|
||||
TRADING_ITEM_BANANAS = "Bananas"
|
||||
TRADING_ITEM_STICK = "Stick"
|
||||
TRADING_ITEM_HONEYCOMB = "Honeycomb"
|
||||
TRADING_ITEM_PINEAPPLE = "Pineapple"
|
||||
TRADING_ITEM_HIBISCUS = "Hibiscus"
|
||||
TRADING_ITEM_LETTER = "Letter"
|
||||
TRADING_ITEM_BROOM = "Broom"
|
||||
TRADING_ITEM_FISHING_HOOK = "Fishing Hook"
|
||||
TRADING_ITEM_NECKLACE = "Necklace"
|
||||
TRADING_ITEM_SCALE = "Scale"
|
||||
TRADING_ITEM_MAGNIFYING_GLASS = "Magnifying Glass"
|
||||
|
||||
trade_item_prog = ItemClassification.progression
|
||||
|
||||
links_awakening_items = [
|
||||
ItemData(ItemName.POWER_BRACELET, "POWER_BRACELET", ItemClassification.progression),
|
||||
ItemData(ItemName.SHIELD, "SHIELD", ItemClassification.progression),
|
||||
ItemData(ItemName.BOW, "BOW", ItemClassification.progression),
|
||||
ItemData(ItemName.HOOKSHOT, "HOOKSHOT", ItemClassification.progression),
|
||||
ItemData(ItemName.MAGIC_ROD, "MAGIC_ROD", ItemClassification.progression),
|
||||
ItemData(ItemName.PEGASUS_BOOTS, "PEGASUS_BOOTS", ItemClassification.progression),
|
||||
ItemData(ItemName.OCARINA, "OCARINA", ItemClassification.progression),
|
||||
ItemData(ItemName.FEATHER, "FEATHER", ItemClassification.progression),
|
||||
ItemData(ItemName.SHOVEL, "SHOVEL", ItemClassification.progression),
|
||||
ItemData(ItemName.MAGIC_POWDER, "MAGIC_POWDER", ItemClassification.progression, True),
|
||||
ItemData(ItemName.BOMB, "BOMB", ItemClassification.progression, True),
|
||||
ItemData(ItemName.SWORD, "SWORD", ItemClassification.progression),
|
||||
ItemData(ItemName.FLIPPERS, "FLIPPERS", ItemClassification.progression),
|
||||
ItemData(ItemName.MAGNIFYING_LENS, "MAGNIFYING_LENS", ItemClassification.progression),
|
||||
ItemData(ItemName.MEDICINE, "MEDICINE", ItemClassification.useful),
|
||||
ItemData(ItemName.TAIL_KEY, "TAIL_KEY", ItemClassification.progression),
|
||||
ItemData(ItemName.ANGLER_KEY, "ANGLER_KEY", ItemClassification.progression),
|
||||
ItemData(ItemName.FACE_KEY, "FACE_KEY", ItemClassification.progression),
|
||||
ItemData(ItemName.BIRD_KEY, "BIRD_KEY", ItemClassification.progression),
|
||||
ItemData(ItemName.SLIME_KEY, "SLIME_KEY", ItemClassification.progression),
|
||||
ItemData(ItemName.GOLD_LEAF, "GOLD_LEAF", ItemClassification.progression),
|
||||
ItemData(ItemName.RUPEES_20, "RUPEES_20", ItemClassification.filler),
|
||||
ItemData(ItemName.RUPEES_50, "RUPEES_50", ItemClassification.useful),
|
||||
ItemData(ItemName.RUPEES_100, "RUPEES_100", ItemClassification.progression_skip_balancing),
|
||||
ItemData(ItemName.RUPEES_200, "RUPEES_200", ItemClassification.progression_skip_balancing),
|
||||
ItemData(ItemName.RUPEES_500, "RUPEES_500", ItemClassification.progression_skip_balancing),
|
||||
ItemData(ItemName.SEASHELL, "SEASHELL", ItemClassification.progression_skip_balancing),
|
||||
ItemData(ItemName.MESSAGE, "MESSAGE", ItemClassification.progression),
|
||||
ItemData(ItemName.GEL, "GEL", ItemClassification.trap),
|
||||
ItemData(ItemName.BOOMERANG, "BOOMERANG", ItemClassification.progression),
|
||||
ItemData(ItemName.HEART_PIECE, "HEART_PIECE", ItemClassification.filler),
|
||||
ItemData(ItemName.BOWWOW, "BOWWOW", ItemClassification.progression),
|
||||
ItemData(ItemName.ARROWS_10, "ARROWS_10", ItemClassification.filler),
|
||||
ItemData(ItemName.SINGLE_ARROW, "SINGLE_ARROW", ItemClassification.filler),
|
||||
ItemData(ItemName.ROOSTER, "ROOSTER", ItemClassification.progression),
|
||||
ItemData(ItemName.MAX_POWDER_UPGRADE, "MAX_POWDER_UPGRADE", ItemClassification.filler),
|
||||
ItemData(ItemName.MAX_BOMBS_UPGRADE, "MAX_BOMBS_UPGRADE", ItemClassification.filler),
|
||||
ItemData(ItemName.MAX_ARROWS_UPGRADE, "MAX_ARROWS_UPGRADE", ItemClassification.filler),
|
||||
ItemData(ItemName.RED_TUNIC, "RED_TUNIC", ItemClassification.useful),
|
||||
ItemData(ItemName.BLUE_TUNIC, "BLUE_TUNIC", ItemClassification.useful),
|
||||
ItemData(ItemName.HEART_CONTAINER, "HEART_CONTAINER", ItemClassification.useful),
|
||||
#ItemData(ItemName.BAD_HEART_CONTAINER, "BAD_HEART_CONTAINER", ItemClassification.trap),
|
||||
ItemData(ItemName.TOADSTOOL, "TOADSTOOL", ItemClassification.progression),
|
||||
DungeonItemData(ItemName.KEY, "KEY", ItemClassification.progression),
|
||||
DungeonItemData(ItemName.KEY1, "KEY1", ItemClassification.progression),
|
||||
DungeonItemData(ItemName.KEY2, "KEY2", ItemClassification.progression),
|
||||
DungeonItemData(ItemName.KEY3, "KEY3", ItemClassification.progression),
|
||||
DungeonItemData(ItemName.KEY4, "KEY4", ItemClassification.progression),
|
||||
DungeonItemData(ItemName.KEY5, "KEY5", ItemClassification.progression),
|
||||
DungeonItemData(ItemName.KEY6, "KEY6", ItemClassification.progression),
|
||||
DungeonItemData(ItemName.KEY7, "KEY7", ItemClassification.progression),
|
||||
DungeonItemData(ItemName.KEY8, "KEY8", ItemClassification.progression),
|
||||
DungeonItemData(ItemName.KEY9, "KEY9", ItemClassification.progression),
|
||||
DungeonItemData(ItemName.NIGHTMARE_KEY, "NIGHTMARE_KEY", ItemClassification.progression),
|
||||
DungeonItemData(ItemName.NIGHTMARE_KEY1, "NIGHTMARE_KEY1", ItemClassification.progression),
|
||||
DungeonItemData(ItemName.NIGHTMARE_KEY2, "NIGHTMARE_KEY2", ItemClassification.progression),
|
||||
DungeonItemData(ItemName.NIGHTMARE_KEY3, "NIGHTMARE_KEY3", ItemClassification.progression),
|
||||
DungeonItemData(ItemName.NIGHTMARE_KEY4, "NIGHTMARE_KEY4", ItemClassification.progression),
|
||||
DungeonItemData(ItemName.NIGHTMARE_KEY5, "NIGHTMARE_KEY5", ItemClassification.progression),
|
||||
DungeonItemData(ItemName.NIGHTMARE_KEY6, "NIGHTMARE_KEY6", ItemClassification.progression),
|
||||
DungeonItemData(ItemName.NIGHTMARE_KEY7, "NIGHTMARE_KEY7", ItemClassification.progression),
|
||||
DungeonItemData(ItemName.NIGHTMARE_KEY8, "NIGHTMARE_KEY8", ItemClassification.progression),
|
||||
DungeonItemData(ItemName.NIGHTMARE_KEY9, "NIGHTMARE_KEY9", ItemClassification.progression),
|
||||
DungeonItemData(ItemName.MAP, "MAP", ItemClassification.filler),
|
||||
DungeonItemData(ItemName.MAP1, "MAP1", ItemClassification.filler),
|
||||
DungeonItemData(ItemName.MAP2, "MAP2", ItemClassification.filler),
|
||||
DungeonItemData(ItemName.MAP3, "MAP3", ItemClassification.filler),
|
||||
DungeonItemData(ItemName.MAP4, "MAP4", ItemClassification.filler),
|
||||
DungeonItemData(ItemName.MAP5, "MAP5", ItemClassification.filler),
|
||||
DungeonItemData(ItemName.MAP6, "MAP6", ItemClassification.filler),
|
||||
DungeonItemData(ItemName.MAP7, "MAP7", ItemClassification.filler),
|
||||
DungeonItemData(ItemName.MAP8, "MAP8", ItemClassification.filler),
|
||||
DungeonItemData(ItemName.MAP9, "MAP9", ItemClassification.filler),
|
||||
DungeonItemData(ItemName.COMPASS, "COMPASS", ItemClassification.filler),
|
||||
DungeonItemData(ItemName.COMPASS1, "COMPASS1", ItemClassification.filler),
|
||||
DungeonItemData(ItemName.COMPASS2, "COMPASS2", ItemClassification.filler),
|
||||
DungeonItemData(ItemName.COMPASS3, "COMPASS3", ItemClassification.filler),
|
||||
DungeonItemData(ItemName.COMPASS4, "COMPASS4", ItemClassification.filler),
|
||||
DungeonItemData(ItemName.COMPASS5, "COMPASS5", ItemClassification.filler),
|
||||
DungeonItemData(ItemName.COMPASS6, "COMPASS6", ItemClassification.filler),
|
||||
DungeonItemData(ItemName.COMPASS7, "COMPASS7", ItemClassification.filler),
|
||||
DungeonItemData(ItemName.COMPASS8, "COMPASS8", ItemClassification.filler),
|
||||
DungeonItemData(ItemName.COMPASS9, "COMPASS9", ItemClassification.filler),
|
||||
DungeonItemData(ItemName.STONE_BEAK, "STONE_BEAK", ItemClassification.filler),
|
||||
DungeonItemData(ItemName.STONE_BEAK1, "STONE_BEAK1", ItemClassification.filler),
|
||||
DungeonItemData(ItemName.STONE_BEAK2, "STONE_BEAK2", ItemClassification.filler),
|
||||
DungeonItemData(ItemName.STONE_BEAK3, "STONE_BEAK3", ItemClassification.filler),
|
||||
DungeonItemData(ItemName.STONE_BEAK4, "STONE_BEAK4", ItemClassification.filler),
|
||||
DungeonItemData(ItemName.STONE_BEAK5, "STONE_BEAK5", ItemClassification.filler),
|
||||
DungeonItemData(ItemName.STONE_BEAK6, "STONE_BEAK6", ItemClassification.filler),
|
||||
DungeonItemData(ItemName.STONE_BEAK7, "STONE_BEAK7", ItemClassification.filler),
|
||||
DungeonItemData(ItemName.STONE_BEAK8, "STONE_BEAK8", ItemClassification.filler),
|
||||
DungeonItemData(ItemName.STONE_BEAK9, "STONE_BEAK9", ItemClassification.filler),
|
||||
ItemData(ItemName.SONG1, "SONG1", ItemClassification.progression),
|
||||
ItemData(ItemName.SONG2, "SONG2", ItemClassification.useful),
|
||||
ItemData(ItemName.SONG3, "SONG3", ItemClassification.progression),
|
||||
DungeonItemData(ItemName.INSTRUMENT1, "INSTRUMENT1", ItemClassification.progression),
|
||||
DungeonItemData(ItemName.INSTRUMENT2, "INSTRUMENT2", ItemClassification.progression),
|
||||
DungeonItemData(ItemName.INSTRUMENT3, "INSTRUMENT3", ItemClassification.progression),
|
||||
DungeonItemData(ItemName.INSTRUMENT4, "INSTRUMENT4", ItemClassification.progression),
|
||||
DungeonItemData(ItemName.INSTRUMENT5, "INSTRUMENT5", ItemClassification.progression),
|
||||
DungeonItemData(ItemName.INSTRUMENT6, "INSTRUMENT6", ItemClassification.progression),
|
||||
DungeonItemData(ItemName.INSTRUMENT7, "INSTRUMENT7", ItemClassification.progression),
|
||||
DungeonItemData(ItemName.INSTRUMENT8, "INSTRUMENT8", ItemClassification.progression),
|
||||
ItemData(ItemName.TRADING_ITEM_YOSHI_DOLL, "TRADING_ITEM_YOSHI_DOLL", trade_item_prog),
|
||||
ItemData(ItemName.TRADING_ITEM_RIBBON, "TRADING_ITEM_RIBBON", trade_item_prog),
|
||||
ItemData(ItemName.TRADING_ITEM_DOG_FOOD, "TRADING_ITEM_DOG_FOOD", trade_item_prog),
|
||||
ItemData(ItemName.TRADING_ITEM_BANANAS, "TRADING_ITEM_BANANAS", trade_item_prog),
|
||||
ItemData(ItemName.TRADING_ITEM_STICK, "TRADING_ITEM_STICK", trade_item_prog),
|
||||
ItemData(ItemName.TRADING_ITEM_HONEYCOMB, "TRADING_ITEM_HONEYCOMB", trade_item_prog),
|
||||
ItemData(ItemName.TRADING_ITEM_PINEAPPLE, "TRADING_ITEM_PINEAPPLE", trade_item_prog),
|
||||
ItemData(ItemName.TRADING_ITEM_HIBISCUS, "TRADING_ITEM_HIBISCUS", trade_item_prog),
|
||||
ItemData(ItemName.TRADING_ITEM_LETTER, "TRADING_ITEM_LETTER", trade_item_prog),
|
||||
ItemData(ItemName.TRADING_ITEM_BROOM, "TRADING_ITEM_BROOM", trade_item_prog),
|
||||
ItemData(ItemName.TRADING_ITEM_FISHING_HOOK, "TRADING_ITEM_FISHING_HOOK", trade_item_prog),
|
||||
ItemData(ItemName.TRADING_ITEM_NECKLACE, "TRADING_ITEM_NECKLACE", trade_item_prog),
|
||||
ItemData(ItemName.TRADING_ITEM_SCALE, "TRADING_ITEM_SCALE", trade_item_prog),
|
||||
ItemData(ItemName.TRADING_ITEM_MAGNIFYING_GLASS, "TRADING_ITEM_MAGNIFYING_GLASS", trade_item_prog)
|
||||
]
|
||||
|
||||
ladxr_item_to_la_item_name = {
|
||||
item.ladxr_id: item.item_name for item in links_awakening_items
|
||||
}
|
||||
|
||||
links_awakening_items_by_name = {
|
||||
item.item_name : item for item in links_awakening_items
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
[tinyci]
|
||||
enabled = True
|
||||
|
||||
[build-test]
|
||||
directory = _test
|
||||
commands =
|
||||
python3 ../main.py ../input.gbc --timeout 120 --output /dev/null
|
||||
python3 ../main.py ../input.gbc --timeout 120 -s seashells=0 -s heartpiece=0 -s dungeon_items=keysanity --output /dev/null
|
||||
python3 ../main.py ../input.gbc --timeout 120 -s logic=glitched -s dungeon_items=keysanity -s heartpiece=0 -s seashells=0 -s heartcontainers=0 -s instruments=1 -s owlstatues=both -s dungeonshuffle=1 -s witch=0 -s boomerang=gift -s steal=never -s goal=random --output /dev/null
|
||||
python3 ../main.py ../input.gbc --timeout 120 -s logic=casual -s dungeon_items=keysy -s itempool=casual --output /dev/null
|
||||
python3 ../main.py ../input.gbc --timeout 120 -s textmode=none --output /dev/null
|
||||
python3 ../main.py ../input.gbc --timeout 120 -s overworld=dungeondive --output /dev/null
|
||||
ignore =
|
||||
python3 ../main.py ../input.gbc --timeout 120 --seashells --heartpiece --entranceshuffle simple --output /dev/null
|
||||
python3 ../main.py ../input.gbc --timeout 120 --seashells --heartpiece --entranceshuffle advanced --output /dev/null
|
||||
python3 ../main.py ../input.gbc --timeout 120 --seashells --heartpiece --entranceshuffle insanity --output /dev/null
|
||||
python3 ../main.py ../input.gbc --timeout 120 --seashells --heartpiece --spoilerformat text --spoilerfilename /dev/null --output /dev/null
|
||||
python3 ../main.py ../input.gbc --timeout 120 --seashells --heartpiece --spoilerformat json --spoilerfilename /dev/null --output /dev/null
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2020 Daid
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,25 @@
|
|||
# Legend Of Zelda: Link's Awakening DX: Randomizer
|
||||
Or, LADXR for short.
|
||||
|
||||
## What is this?
|
||||
|
||||
See https://daid.github.io/LADXR/
|
||||
|
||||
## Usage
|
||||
|
||||
The only requirements are: to use python3, and the English v1.0 ROM for Links Awakening DX.
|
||||
|
||||
The proper SHA-1 for the rom is `d90ac17e9bf17b6c61624ad9f05447bdb5efc01a`.
|
||||
|
||||
Basic usage:
|
||||
`python3 main.py zelda.gbc`
|
||||
|
||||
The script will generate a new rom with item locations shuffled. There are many options, see `-h` on the script for details.
|
||||
|
||||
## Development
|
||||
|
||||
This is still in the early stage of development. Important bits are:
|
||||
* `randomizer.py`: Contains the actual logic to randomize the rom, and checks to make sure it can be solved.
|
||||
* `logic/*.py`: Contains the logic definitions of what connects to what in the world and what it requires to access that part.
|
||||
* `locations/*.py`: Contains definitions of location types, and what items can be there. As well as the code on how to place an item there. For example the Chest class has a list of all items that can be in a chest. And the needed rom patch to put that an item in a specific chest.
|
||||
* `patches/*.py`: Various patches on the code that are not directly related to a specific location. But more general fixes
|
|
@ -0,0 +1,845 @@
|
|||
import binascii
|
||||
from typing import Optional, Dict, ItemsView, List, Union, Tuple
|
||||
import unicodedata
|
||||
|
||||
from . import utils
|
||||
import re
|
||||
|
||||
|
||||
REGS8 = {"A": 7, "B": 0, "C": 1, "D": 2, "E": 3, "H": 4, "L": 5, "[HL]": 6}
|
||||
REGS16A = {"BC": 0, "DE": 1, "HL": 2, "SP": 3}
|
||||
REGS16B = {"BC": 0, "DE": 1, "HL": 2, "AF": 3}
|
||||
FLAGS = {"NZ": 0x00, "Z": 0x08, "NC": 0x10, "C": 0x18}
|
||||
|
||||
CONST_MAP: Dict[str, int] = {}
|
||||
|
||||
|
||||
class ExprBase:
|
||||
def asReg8(self) -> Optional[int]:
|
||||
return None
|
||||
|
||||
def isA(self, kind: str, value: Optional[str] = None) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
class Token(ExprBase):
|
||||
def __init__(self, kind: str, value: Union[str, int], line_nr: int) -> None:
|
||||
self.kind = kind
|
||||
self.value = value
|
||||
self.line_nr = line_nr
|
||||
|
||||
def isA(self, kind: str, value: Optional[str] = None) -> bool:
|
||||
return self.kind == kind and (value is None or value == self.value)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "[%s:%s:%d]" % (self.kind, self.value, self.line_nr)
|
||||
|
||||
def asReg8(self) -> Optional[int]:
|
||||
if self.kind == 'ID':
|
||||
return REGS8.get(str(self.value), None)
|
||||
return None
|
||||
|
||||
|
||||
class REF(ExprBase):
|
||||
def __init__(self, expr: ExprBase) -> None:
|
||||
self.expr = expr
|
||||
|
||||
def asReg8(self) -> Optional[int]:
|
||||
if self.expr.isA('ID', 'HL'):
|
||||
return REGS8['[HL]']
|
||||
return None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "[%s]" % (self.expr)
|
||||
|
||||
|
||||
class OP(ExprBase):
|
||||
def __init__(self, op: str, left: ExprBase, right: Optional[ExprBase] = None):
|
||||
self.op = op
|
||||
self.left = left
|
||||
self.right = right
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "%s %s %s" % (self.left, self.op, self.right)
|
||||
|
||||
@staticmethod
|
||||
def make(op: str, left: ExprBase, right: Optional[ExprBase] = None) -> ExprBase:
|
||||
if left.isA('NUMBER') and right is not None and right.isA('NUMBER'):
|
||||
assert isinstance(right, Token) and isinstance(right.value, int)
|
||||
assert isinstance(left, Token) and isinstance(left.value, int)
|
||||
if op == '+':
|
||||
left.value += right.value
|
||||
return left
|
||||
if op == '-':
|
||||
left.value -= right.value
|
||||
return left
|
||||
if op == '*':
|
||||
left.value *= right.value
|
||||
return left
|
||||
if op == '/':
|
||||
left.value //= right.value
|
||||
return left
|
||||
if left.isA('NUMBER') and right is None:
|
||||
assert isinstance(left, Token) and isinstance(left.value, int)
|
||||
if op == '+':
|
||||
return left
|
||||
if op == '-':
|
||||
left.value = -left.value
|
||||
return left
|
||||
return OP(op, left, right)
|
||||
|
||||
|
||||
class Tokenizer:
|
||||
TOKEN_REGEX = re.compile('|'.join('(?P<%s>%s)' % pair for pair in [
|
||||
('NUMBER', r'\d+(\.\d*)?'),
|
||||
('HEX', r'\$[0-9A-Fa-f]+'),
|
||||
('ASSIGN', r':='),
|
||||
('COMMENT', r';[^\n]+'),
|
||||
('LABEL', r':'),
|
||||
('DIRECTIVE', r'#[A-Za-z_]+'),
|
||||
('STRING', '[a-zA-Z]?"[^"]*"'),
|
||||
('ID', r'\.?[A-Za-z_][A-Za-z0-9_\.]*'),
|
||||
('OP', r'[+\-*/,\(\)]'),
|
||||
('REFOPEN', r'\['),
|
||||
('REFCLOSE', r'\]'),
|
||||
('NEWLINE', r'\n'),
|
||||
('SKIP', r'[ \t]+'),
|
||||
('MISMATCH', r'.'),
|
||||
]))
|
||||
|
||||
def __init__(self, code: str) -> None:
|
||||
self.__tokens: List[Token] = []
|
||||
line_num = 1
|
||||
for mo in self.TOKEN_REGEX.finditer(code):
|
||||
kind = mo.lastgroup
|
||||
assert kind is not None
|
||||
value: Union[str, int] = mo.group()
|
||||
if kind == 'MISMATCH':
|
||||
print(code.split("\n")[line_num-1])
|
||||
raise RuntimeError("Syntax error on line: %d: %s\n%s", line_num, value)
|
||||
elif kind == 'SKIP':
|
||||
pass
|
||||
elif kind == 'COMMENT':
|
||||
pass
|
||||
else:
|
||||
if kind == 'NUMBER':
|
||||
value = int(value)
|
||||
elif kind == 'HEX':
|
||||
value = int(str(value)[1:], 16)
|
||||
kind = 'NUMBER'
|
||||
elif kind == 'ID':
|
||||
value = str(value).upper()
|
||||
self.__tokens.append(Token(kind, value, line_num))
|
||||
if kind == 'NEWLINE':
|
||||
line_num += 1
|
||||
self.__tokens.append(Token('NEWLINE', '\n', line_num))
|
||||
|
||||
def peek(self) -> Token:
|
||||
return self.__tokens[0]
|
||||
|
||||
def pop(self) -> Token:
|
||||
return self.__tokens.pop(0)
|
||||
|
||||
def expect(self, kind: str, value: Optional[str] = None) -> None:
|
||||
pop = self.pop()
|
||||
if not pop.isA(kind, value):
|
||||
if value is not None:
|
||||
raise SyntaxError("%s != %s:%s" % (pop, kind, value))
|
||||
raise SyntaxError("%s != %s" % (pop, kind))
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return bool(self.__tokens)
|
||||
|
||||
|
||||
class Assembler:
|
||||
SIMPLE_INSTR = {
|
||||
'NOP': 0x00,
|
||||
'RLCA': 0x07,
|
||||
'RRCA': 0x0F,
|
||||
'STOP': 0x010,
|
||||
'RLA': 0x17,
|
||||
'RRA': 0x1F,
|
||||
'DAA': 0x27,
|
||||
'CPL': 0x2F,
|
||||
'SCF': 0x37,
|
||||
'CCF': 0x3F,
|
||||
'HALT': 0x76,
|
||||
'RETI': 0xD9,
|
||||
'DI': 0xF3,
|
||||
'EI': 0xFB,
|
||||
}
|
||||
|
||||
LINK_REL8 = 0
|
||||
LINK_ABS8 = 1
|
||||
LINK_ABS16 = 2
|
||||
|
||||
def __init__(self, base_address: Optional[int] = None) -> None:
|
||||
self.__base_address = base_address or -1
|
||||
self.__result = bytearray()
|
||||
self.__label: Dict[str, int] = {}
|
||||
self.__constant: Dict[str, int] = {}
|
||||
self.__link: Dict[int, Tuple[int, ExprBase]] = {}
|
||||
self.__scope: Optional[str] = None
|
||||
|
||||
self.__tok = Tokenizer("")
|
||||
|
||||
def process(self, code: str) -> None:
|
||||
conditional_stack = [True]
|
||||
self.__tok = Tokenizer(code)
|
||||
try:
|
||||
while self.__tok:
|
||||
start = self.__tok.pop()
|
||||
if start.kind == 'NEWLINE':
|
||||
pass # Empty newline
|
||||
elif start.kind == 'DIRECTIVE':
|
||||
if start.value == '#IF':
|
||||
t = self.parseExpression()
|
||||
assert isinstance(t, Token)
|
||||
conditional_stack.append(conditional_stack[-1] and t.value != 0)
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif start.value == '#ELSE':
|
||||
conditional_stack[-1] = not conditional_stack[-1] and conditional_stack[-2]
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif start.value == '#ENDIF':
|
||||
conditional_stack.pop()
|
||||
assert conditional_stack
|
||||
self.__tok.expect('NEWLINE')
|
||||
else:
|
||||
raise SyntaxError(start)
|
||||
elif not conditional_stack[-1]:
|
||||
while not self.__tok.pop().isA('NEWLINE'):
|
||||
pass
|
||||
elif start.kind == 'ID':
|
||||
if start.value == 'DB':
|
||||
self.instrDB()
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif start.value == 'DW':
|
||||
self.instrDW()
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif start.value == 'LD':
|
||||
self.instrLD()
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif start.value == 'LDH':
|
||||
self.instrLDH()
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif start.value == 'LDI':
|
||||
self.instrLDI()
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif start.value == 'LDD':
|
||||
self.instrLDD()
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif start.value == 'INC':
|
||||
self.instrINC()
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif start.value == 'DEC':
|
||||
self.instrDEC()
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif start.value == 'ADD':
|
||||
self.instrADD()
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif start.value == 'ADC':
|
||||
self.instrALU(0x88)
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif start.value == 'SUB':
|
||||
self.instrALU(0x90)
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif start.value == 'SBC':
|
||||
self.instrALU(0x98)
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif start.value == 'AND':
|
||||
self.instrALU(0xA0)
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif start.value == 'XOR':
|
||||
self.instrALU(0xA8)
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif start.value == 'OR':
|
||||
self.instrALU(0xB0)
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif start.value == 'CP':
|
||||
self.instrALU(0xB8)
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif start.value == 'BIT':
|
||||
self.instrBIT(0x40)
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif start.value == 'RES':
|
||||
self.instrBIT(0x80)
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif start.value == 'SET':
|
||||
self.instrBIT(0xC0)
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif start.value == 'RET':
|
||||
self.instrRET()
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif start.value == 'CALL':
|
||||
self.instrCALL()
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif start.value == 'RLC':
|
||||
self.instrCB(0x00)
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif start.value == 'RRC':
|
||||
self.instrCB(0x08)
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif start.value == 'RL':
|
||||
self.instrCB(0x10)
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif start.value == 'RR':
|
||||
self.instrCB(0x18)
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif start.value == 'SLA':
|
||||
self.instrCB(0x20)
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif start.value == 'SRA':
|
||||
self.instrCB(0x28)
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif start.value == 'SWAP':
|
||||
self.instrCB(0x30)
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif start.value == 'SRL':
|
||||
self.instrCB(0x38)
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif start.value == 'RST':
|
||||
self.instrRST()
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif start.value == 'JP':
|
||||
self.instrJP()
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif start.value == 'JR':
|
||||
self.instrJR()
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif start.value == 'PUSH':
|
||||
self.instrPUSHPOP(0xC5)
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif start.value == 'POP':
|
||||
self.instrPUSHPOP(0xC1)
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif start.value in self.SIMPLE_INSTR:
|
||||
self.__result.append(self.SIMPLE_INSTR[str(start.value)])
|
||||
self.__tok.expect('NEWLINE')
|
||||
elif self.__tok.peek().kind == 'LABEL':
|
||||
self.__tok.pop()
|
||||
self.addLabel(str(start.value))
|
||||
elif self.__tok.peek().kind == 'ASSIGN':
|
||||
self.__tok.pop()
|
||||
value = self.__tok.pop()
|
||||
if value.kind != 'NUMBER':
|
||||
raise SyntaxError(start)
|
||||
self.addConstant(str(start.value), int(value.value))
|
||||
else:
|
||||
raise SyntaxError(start)
|
||||
else:
|
||||
raise SyntaxError(start)
|
||||
except SyntaxError:
|
||||
print("Syntax error on line: %s" % code.split("\n")[self.__tok.peek().line_nr-1])
|
||||
raise
|
||||
|
||||
def insert8(self, expr: ExprBase) -> None:
|
||||
if expr.isA('NUMBER'):
|
||||
assert isinstance(expr, Token)
|
||||
value = int(expr.value)
|
||||
else:
|
||||
self.__link[len(self.__result)] = (Assembler.LINK_ABS8, expr)
|
||||
value = 0
|
||||
assert 0 <= value < 256
|
||||
self.__result.append(value)
|
||||
|
||||
def insertRel8(self, expr: ExprBase) -> None:
|
||||
if expr.isA('NUMBER'):
|
||||
assert isinstance(expr, Token)
|
||||
self.__result.append(int(expr.value))
|
||||
else:
|
||||
self.__link[len(self.__result)] = (Assembler.LINK_REL8, expr)
|
||||
self.__result.append(0x00)
|
||||
|
||||
def insert16(self, expr: ExprBase) -> None:
|
||||
if expr.isA('NUMBER'):
|
||||
assert isinstance(expr, Token)
|
||||
value = int(expr.value)
|
||||
else:
|
||||
self.__link[len(self.__result)] = (Assembler.LINK_ABS16, expr)
|
||||
value = 0
|
||||
assert 0 <= value <= 0xFFFF
|
||||
self.__result.append(value & 0xFF)
|
||||
self.__result.append(value >> 8)
|
||||
|
||||
def insertString(self, string: str) -> None:
|
||||
if string.startswith('"') and string.endswith('"'):
|
||||
string = string[1:-1]
|
||||
string = unicodedata.normalize('NFKD', string)
|
||||
self.__result += string.encode("latin1", "ignore")
|
||||
elif string.startswith("m\"") and string.endswith("\""):
|
||||
self.__result += utils.formatText(string[2:-1].replace("|", "\n"))
|
||||
else:
|
||||
raise SyntaxError
|
||||
|
||||
def instrLD(self) -> None:
|
||||
left_param = self.parseParam()
|
||||
self.__tok.expect('OP', ',')
|
||||
right_param = self.parseParam()
|
||||
lr8 = left_param.asReg8()
|
||||
rr8 = right_param.asReg8()
|
||||
if lr8 is not None and rr8 is not None:
|
||||
self.__result.append(0x40 | (lr8 << 3) | rr8)
|
||||
elif left_param.isA('ID', 'A') and isinstance(right_param, REF):
|
||||
if right_param.expr.isA('ID', 'BC'):
|
||||
self.__result.append(0x0A)
|
||||
elif right_param.expr.isA('ID', 'DE'):
|
||||
self.__result.append(0x1A)
|
||||
elif right_param.expr.isA('ID', 'HL+'): # TODO
|
||||
self.__result.append(0x2A)
|
||||
elif right_param.expr.isA('ID', 'HL-'): # TODO
|
||||
self.__result.append(0x3A)
|
||||
elif right_param.expr.isA('ID', 'C'):
|
||||
self.__result.append(0xF2)
|
||||
else:
|
||||
self.__result.append(0xFA)
|
||||
self.insert16(right_param.expr)
|
||||
elif right_param.isA('ID', 'A') and isinstance(left_param, REF):
|
||||
if left_param.expr.isA('ID', 'BC'):
|
||||
self.__result.append(0x02)
|
||||
elif left_param.expr.isA('ID', 'DE'):
|
||||
self.__result.append(0x12)
|
||||
elif left_param.expr.isA('ID', 'HL+'): # TODO
|
||||
self.__result.append(0x22)
|
||||
elif left_param.expr.isA('ID', 'HL-'): # TODO
|
||||
self.__result.append(0x32)
|
||||
elif left_param.expr.isA('ID', 'C'):
|
||||
self.__result.append(0xE2)
|
||||
else:
|
||||
self.__result.append(0xEA)
|
||||
self.insert16(left_param.expr)
|
||||
elif left_param.isA('ID', 'BC'):
|
||||
self.__result.append(0x01)
|
||||
self.insert16(right_param)
|
||||
elif left_param.isA('ID', 'DE'):
|
||||
self.__result.append(0x11)
|
||||
self.insert16(right_param)
|
||||
elif left_param.isA('ID', 'HL'):
|
||||
self.__result.append(0x21)
|
||||
self.insert16(right_param)
|
||||
elif left_param.isA('ID', 'SP'):
|
||||
if right_param.isA('ID', 'HL'):
|
||||
self.__result.append(0xF9)
|
||||
else:
|
||||
self.__result.append(0x31)
|
||||
self.insert16(right_param)
|
||||
elif right_param.isA('ID', 'SP') and isinstance(left_param, REF):
|
||||
self.__result.append(0x08)
|
||||
self.insert16(left_param.expr)
|
||||
elif lr8 is not None:
|
||||
self.__result.append(0x06 | (lr8 << 3))
|
||||
self.insert8(right_param)
|
||||
else:
|
||||
raise SyntaxError
|
||||
|
||||
def instrLDH(self) -> None:
|
||||
left_param = self.parseParam()
|
||||
self.__tok.expect('OP', ',')
|
||||
right_param = self.parseParam()
|
||||
if left_param.isA('ID', 'A') and isinstance(right_param, REF):
|
||||
if right_param.expr.isA('ID', 'C'):
|
||||
self.__result.append(0xF2)
|
||||
else:
|
||||
self.__result.append(0xF0)
|
||||
self.insert8(right_param.expr)
|
||||
elif right_param.isA('ID', 'A') and isinstance(left_param, REF):
|
||||
if left_param.expr.isA('ID', 'C'):
|
||||
self.__result.append(0xE2)
|
||||
else:
|
||||
self.__result.append(0xE0)
|
||||
self.insert8(left_param.expr)
|
||||
else:
|
||||
raise SyntaxError
|
||||
|
||||
def instrLDI(self) -> None:
|
||||
left_param = self.parseParam()
|
||||
self.__tok.expect('OP', ',')
|
||||
right_param = self.parseParam()
|
||||
if left_param.isA('ID', 'A') and isinstance(right_param, REF) and right_param.expr.isA('ID', 'HL'):
|
||||
self.__result.append(0x2A)
|
||||
elif right_param.isA('ID', 'A') and isinstance(left_param, REF) and left_param.expr.isA('ID', 'HL'):
|
||||
self.__result.append(0x22)
|
||||
else:
|
||||
raise SyntaxError
|
||||
|
||||
def instrLDD(self) -> None:
|
||||
left_param = self.parseParam()
|
||||
self.__tok.expect('OP', ',')
|
||||
right_param = self.parseParam()
|
||||
if left_param.isA('ID', 'A') and isinstance(right_param, REF) and right_param.expr.isA('ID', 'HL'):
|
||||
self.__result.append(0x3A)
|
||||
elif right_param.isA('ID', 'A') and isinstance(left_param, REF) and left_param.expr.isA('ID', 'HL'):
|
||||
self.__result.append(0x32)
|
||||
else:
|
||||
raise SyntaxError
|
||||
|
||||
def instrINC(self) -> None:
|
||||
param = self.parseParam()
|
||||
r8 = param.asReg8()
|
||||
if r8 is not None:
|
||||
self.__result.append(0x04 | (r8 << 3))
|
||||
elif param.isA('ID', 'BC'):
|
||||
self.__result.append(0x03)
|
||||
elif param.isA('ID', 'DE'):
|
||||
self.__result.append(0x13)
|
||||
elif param.isA('ID', 'HL'):
|
||||
self.__result.append(0x23)
|
||||
elif param.isA('ID', 'SP'):
|
||||
self.__result.append(0x33)
|
||||
else:
|
||||
raise SyntaxError
|
||||
|
||||
def instrDEC(self) -> None:
|
||||
param = self.parseParam()
|
||||
r8 = param.asReg8()
|
||||
if r8 is not None:
|
||||
self.__result.append(0x05 | (r8 << 3))
|
||||
elif param.isA('ID', 'BC'):
|
||||
self.__result.append(0x0B)
|
||||
elif param.isA('ID', 'DE'):
|
||||
self.__result.append(0x1B)
|
||||
elif param.isA('ID', 'HL'):
|
||||
self.__result.append(0x2B)
|
||||
elif param.isA('ID', 'SP'):
|
||||
self.__result.append(0x3B)
|
||||
else:
|
||||
raise SyntaxError
|
||||
|
||||
def instrADD(self) -> None:
|
||||
left_param = self.parseParam()
|
||||
self.__tok.expect('OP', ',')
|
||||
right_param = self.parseParam()
|
||||
|
||||
if left_param.isA('ID', 'A'):
|
||||
rr8 = right_param.asReg8()
|
||||
if rr8 is not None:
|
||||
self.__result.append(0x80 | rr8)
|
||||
else:
|
||||
self.__result.append(0xC6)
|
||||
self.insert8(right_param)
|
||||
elif left_param.isA('ID', 'HL') and right_param.isA('ID') and isinstance(right_param, Token) and right_param.value in REGS16A:
|
||||
self.__result.append(0x09 | REGS16A[str(right_param.value)] << 4)
|
||||
elif left_param.isA('ID', 'SP'):
|
||||
self.__result.append(0xE8)
|
||||
self.insert8(right_param)
|
||||
else:
|
||||
raise SyntaxError
|
||||
|
||||
def instrALU(self, code_value: int) -> None:
|
||||
param = self.parseParam()
|
||||
if param.isA('ID', 'A') and self.__tok.peek().isA('OP', ','):
|
||||
self.__tok.pop()
|
||||
param = self.parseParam()
|
||||
r8 = param.asReg8()
|
||||
if r8 is not None:
|
||||
self.__result.append(code_value | r8)
|
||||
else:
|
||||
self.__result.append(code_value | 0x46)
|
||||
self.insert8(param)
|
||||
|
||||
def instrRST(self) -> None:
|
||||
param = self.parseParam()
|
||||
if param.isA('NUMBER') and isinstance(param, Token) and (int(param.value) & ~0x38) == 0:
|
||||
self.__result.append(0xC7 | int(param.value))
|
||||
else:
|
||||
raise SyntaxError
|
||||
|
||||
def instrPUSHPOP(self, code_value: int) -> None:
|
||||
param = self.parseParam()
|
||||
if param.isA('ID') and isinstance(param, Token) and str(param.value) in REGS16B:
|
||||
self.__result.append(code_value | (REGS16B[str(param.value)] << 4))
|
||||
else:
|
||||
raise SyntaxError
|
||||
|
||||
def instrJR(self) -> None:
|
||||
param = self.parseParam()
|
||||
if self.__tok.peek().isA('OP', ','):
|
||||
self.__tok.pop()
|
||||
condition = param
|
||||
param = self.parseParam()
|
||||
if condition.isA('ID') and isinstance(condition, Token) and str(condition.value) in FLAGS:
|
||||
self.__result.append(0x20 | FLAGS[str(condition.value)])
|
||||
else:
|
||||
raise SyntaxError
|
||||
else:
|
||||
self.__result.append(0x18)
|
||||
self.insertRel8(param)
|
||||
|
||||
def instrCB(self, code_value: int) -> None:
|
||||
param = self.parseParam()
|
||||
r8 = param.asReg8()
|
||||
if r8 is not None:
|
||||
self.__result.append(0xCB)
|
||||
self.__result.append(code_value | r8)
|
||||
else:
|
||||
raise SyntaxError
|
||||
|
||||
def instrBIT(self, code_value: int) -> None:
|
||||
left_param = self.parseParam()
|
||||
self.__tok.expect('OP', ',')
|
||||
right_param = self.parseParam()
|
||||
rr8 = right_param.asReg8()
|
||||
if left_param.isA('NUMBER') and isinstance(left_param, Token) and rr8 is not None:
|
||||
self.__result.append(0xCB)
|
||||
self.__result.append(code_value | (int(left_param.value) << 3) | rr8)
|
||||
else:
|
||||
raise SyntaxError
|
||||
|
||||
def instrRET(self) -> None:
|
||||
if self.__tok.peek().isA('ID'):
|
||||
condition = self.__tok.pop()
|
||||
if condition.isA('ID') and condition.value in FLAGS:
|
||||
self.__result.append(0xC0 | FLAGS[str(condition.value)])
|
||||
else:
|
||||
raise SyntaxError
|
||||
else:
|
||||
self.__result.append(0xC9)
|
||||
|
||||
def instrCALL(self) -> None:
|
||||
param = self.parseParam()
|
||||
if self.__tok.peek().isA('OP', ','):
|
||||
self.__tok.pop()
|
||||
condition = param
|
||||
param = self.parseParam()
|
||||
if condition.isA('ID') and isinstance(condition, Token) and condition.value in FLAGS:
|
||||
self.__result.append(0xC4 | FLAGS[str(condition.value)])
|
||||
else:
|
||||
raise SyntaxError
|
||||
else:
|
||||
self.__result.append(0xCD)
|
||||
self.insert16(param)
|
||||
|
||||
def instrJP(self) -> None:
|
||||
param = self.parseParam()
|
||||
if self.__tok.peek().isA('OP', ','):
|
||||
self.__tok.pop()
|
||||
condition = param
|
||||
param = self.parseParam()
|
||||
if condition.isA('ID') and isinstance(condition, Token) and condition.value in FLAGS:
|
||||
self.__result.append(0xC2 | FLAGS[str(condition.value)])
|
||||
else:
|
||||
raise SyntaxError
|
||||
elif param.isA('ID', 'HL'):
|
||||
self.__result.append(0xE9)
|
||||
return
|
||||
else:
|
||||
self.__result.append(0xC3)
|
||||
self.insert16(param)
|
||||
|
||||
def instrDW(self) -> None:
|
||||
param = self.parseExpression()
|
||||
self.insert16(param)
|
||||
while self.__tok.peek().isA('OP', ','):
|
||||
self.__tok.pop()
|
||||
param = self.parseExpression()
|
||||
self.insert16(param)
|
||||
|
||||
def instrDB(self) -> None:
|
||||
param = self.parseExpression()
|
||||
if param.isA('STRING'):
|
||||
assert isinstance(param, Token)
|
||||
self.insertString(str(param.value))
|
||||
else:
|
||||
self.insert8(param)
|
||||
while self.__tok.peek().isA('OP', ','):
|
||||
self.__tok.pop()
|
||||
param = self.parseExpression()
|
||||
if param.isA('STRING'):
|
||||
assert isinstance(param, Token)
|
||||
self.insertString(str(param.value))
|
||||
else:
|
||||
self.insert8(param)
|
||||
|
||||
def addLabel(self, label: str) -> None:
|
||||
if label.startswith("."):
|
||||
assert self.__scope is not None
|
||||
label = self.__scope + label
|
||||
else:
|
||||
assert "." not in label, label
|
||||
self.__scope = label
|
||||
assert label not in self.__label, "Duplicate label: %s" % (label)
|
||||
assert label not in self.__constant, "Duplicate label: %s" % (label)
|
||||
self.__label[label] = len(self.__result)
|
||||
|
||||
def addConstant(self, name: str, value: int) -> None:
|
||||
assert name not in self.__constant, "Duplicate constant: %s" % (name)
|
||||
assert name not in self.__label, "Duplicate constant: %s" % (name)
|
||||
self.__constant[name] = value
|
||||
|
||||
def parseParam(self) -> ExprBase:
|
||||
t = self.__tok.peek()
|
||||
if t.kind == 'REFOPEN':
|
||||
self.__tok.pop()
|
||||
expr = self.parseExpression()
|
||||
self.__tok.expect('REFCLOSE')
|
||||
return REF(expr)
|
||||
return self.parseExpression()
|
||||
|
||||
def parseExpression(self) -> ExprBase:
|
||||
t = self.parseAddSub()
|
||||
return t
|
||||
|
||||
def parseAddSub(self) -> ExprBase:
|
||||
t = self.parseFactor()
|
||||
p = self.__tok.peek()
|
||||
if p.isA('OP', '+') or p.isA('OP', '-'):
|
||||
self.__tok.pop()
|
||||
return OP.make(str(p.value), t, self.parseAddSub())
|
||||
return t
|
||||
|
||||
def parseFactor(self) -> ExprBase:
|
||||
t = self.parseUnary()
|
||||
p = self.__tok.peek()
|
||||
if p.isA('OP', '*') or p.isA('OP', '/'):
|
||||
self.__tok.pop()
|
||||
return OP.make(str(p.value), t, self.parseFactor())
|
||||
return t
|
||||
|
||||
def parseUnary(self) -> ExprBase:
|
||||
t = self.__tok.pop()
|
||||
if t.isA('OP', '-') or t.isA('OP', '+'):
|
||||
return OP.make(str(t.value), self.parseUnary())
|
||||
elif t.isA('OP', '('):
|
||||
result = self.parseExpression()
|
||||
self.__tok.expect('OP', ')')
|
||||
return result
|
||||
if t.kind not in ('ID', 'NUMBER', 'STRING'):
|
||||
raise SyntaxError
|
||||
if t.isA('ID') and t.value in CONST_MAP:
|
||||
t.kind = 'NUMBER'
|
||||
t.value = CONST_MAP[str(t.value)]
|
||||
elif t.isA('ID') and t.value in self.__constant:
|
||||
t.kind = 'NUMBER'
|
||||
t.value = self.__constant[str(t.value)]
|
||||
elif t.isA('ID') and str(t.value).startswith("."):
|
||||
assert self.__scope is not None
|
||||
t.value = self.__scope + str(t.value)
|
||||
return t
|
||||
|
||||
def link(self) -> None:
|
||||
for offset, (link_type, link_expr) in self.__link.items():
|
||||
expr = self.resolveExpr(link_expr)
|
||||
assert expr is not None
|
||||
assert expr.isA('NUMBER'), expr
|
||||
assert isinstance(expr, Token)
|
||||
value = int(expr.value)
|
||||
if link_type == Assembler.LINK_REL8:
|
||||
byte = (value - self.__base_address) - offset - 1
|
||||
assert -128 <= byte <= 127, expr
|
||||
self.__result[offset] = byte & 0xFF
|
||||
elif link_type == Assembler.LINK_ABS8:
|
||||
assert 0 <= value <= 0xFF
|
||||
self.__result[offset] = value & 0xFF
|
||||
elif link_type == Assembler.LINK_ABS16:
|
||||
assert self.__base_address >= 0, "Cannot place absolute values in a relocatable code piece"
|
||||
assert 0 <= value <= 0xFFFF
|
||||
self.__result[offset] = value & 0xFF
|
||||
self.__result[offset + 1] = value >> 8
|
||||
else:
|
||||
raise RuntimeError
|
||||
|
||||
def resolveExpr(self, expr: Optional[ExprBase]) -> Optional[ExprBase]:
|
||||
if expr is None:
|
||||
return None
|
||||
elif isinstance(expr, OP):
|
||||
left = self.resolveExpr(expr.left)
|
||||
assert left is not None
|
||||
return OP.make(expr.op, left, self.resolveExpr(expr.right))
|
||||
elif isinstance(expr, Token) and expr.isA('ID') and isinstance(expr, Token) and expr.value in self.__label:
|
||||
return Token('NUMBER', self.__label[str(expr.value)] + self.__base_address, expr.line_nr)
|
||||
return expr
|
||||
|
||||
def getResult(self) -> bytearray:
|
||||
return self.__result
|
||||
|
||||
def getLabels(self) -> ItemsView[str, int]:
|
||||
return self.__label.items()
|
||||
|
||||
|
||||
def const(name: str, value: int) -> None:
|
||||
name = name.upper()
|
||||
assert name not in CONST_MAP
|
||||
CONST_MAP[name] = value
|
||||
|
||||
|
||||
def resetConsts() -> None:
|
||||
CONST_MAP.clear()
|
||||
|
||||
|
||||
def ASM(code: str, base_address: Optional[int] = None, labels_result: Optional[Dict[str, int]] = None) -> bytes:
|
||||
asm = Assembler(base_address)
|
||||
asm.process(code)
|
||||
asm.link()
|
||||
if labels_result is not None:
|
||||
assert base_address is not None
|
||||
for label, offset in asm.getLabels():
|
||||
labels_result[label] = base_address + offset
|
||||
return binascii.hexlify(asm.getResult())
|
||||
|
||||
|
||||
def allOpcodesTest() -> None:
|
||||
import json
|
||||
opcodes = json.load(open("Opcodes.json", "rt"))
|
||||
for label in (False, True):
|
||||
for prefix, codes in opcodes.items():
|
||||
for num, op in codes.items():
|
||||
if op['mnemonic'].startswith('ILLEGAL_') or op['mnemonic'] == 'PREFIX':
|
||||
continue
|
||||
params = []
|
||||
postfix = ''
|
||||
for o in op['operands']:
|
||||
name = o['name']
|
||||
if name == 'd16' or name == 'a16':
|
||||
if label:
|
||||
name = 'LABEL'
|
||||
else:
|
||||
name = '$0000'
|
||||
if name == 'd8' or name == 'a8':
|
||||
name = '$00'
|
||||
if name == 'r8':
|
||||
if label and num != '0xE8':
|
||||
name = 'LABEL'
|
||||
else:
|
||||
name = '$00'
|
||||
if name[-1] == 'H' and name[0].isnumeric():
|
||||
name = '$' + name[:-1]
|
||||
if o['immediate']:
|
||||
params.append(name)
|
||||
else:
|
||||
params.append("[%s]" % (name))
|
||||
if 'increment' in o and o['increment']:
|
||||
postfix = 'I'
|
||||
if 'decrement' in o and o['decrement']:
|
||||
postfix = 'D'
|
||||
code = op["mnemonic"] + postfix + " " + ", ".join(params)
|
||||
code = code.strip()
|
||||
try:
|
||||
data = ASM("LABEL:\n%s" % (code), 0x0000)
|
||||
if prefix == 'cbprefixed':
|
||||
assert data[0:2] == b'cb'
|
||||
data = data[2:]
|
||||
assert data[0:2] == num[2:].encode('ascii').lower(), data[0:2] + b"!=" + num[2:].encode('ascii').lower()
|
||||
except Exception as e:
|
||||
print("%s\t\t|%r|\t%s" % (code, e, num))
|
||||
print(op)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
#allOpcodesTest()
|
||||
const("CONST1", 1)
|
||||
const("CONST2", 2)
|
||||
ASM("""
|
||||
ld a, (123)
|
||||
ld hl, $1234 + 456
|
||||
ld hl, $1234 + CONST1
|
||||
ld hl, label
|
||||
ld hl, label.end - label
|
||||
ld c, label.end - label
|
||||
label:
|
||||
nop
|
||||
.end:
|
||||
""", 0)
|
||||
ASM("""
|
||||
jr label
|
||||
label:
|
||||
""")
|
||||
assert ASM("db 1 + 2 * 3") == b'07'
|
|
@ -0,0 +1,69 @@
|
|||
|
||||
class BackgroundEditor:
|
||||
def __init__(self, rom, index, *, attributes=False):
|
||||
self.__index = index
|
||||
self.__is_attributes = attributes
|
||||
|
||||
self.tiles = {}
|
||||
if attributes:
|
||||
data = rom.background_attributes[index]
|
||||
else:
|
||||
data = rom.background_tiles[index]
|
||||
idx = 0
|
||||
while data[idx] != 0x00:
|
||||
addr = data[idx] << 8 | data[idx + 1]
|
||||
amount = (data[idx + 2] & 0x3F) + 1
|
||||
repeat = (data[idx + 2] & 0x40) == 0x40
|
||||
vertical = (data[idx + 2] & 0x80) == 0x80
|
||||
idx += 3
|
||||
for n in range(amount):
|
||||
self.tiles[addr] = data[idx]
|
||||
if not repeat:
|
||||
idx += 1
|
||||
addr += 0x20 if vertical else 0x01
|
||||
if repeat:
|
||||
idx += 1
|
||||
|
||||
def dump(self):
|
||||
if not self.tiles:
|
||||
return
|
||||
low = min(self.tiles.keys()) & 0xFFE0
|
||||
high = (max(self.tiles.keys()) | 0x001F) + 1
|
||||
print("0x%02x " % (self.__index) + "".join(map(lambda n: "%2X" % (n), range(0x20))))
|
||||
for addr in range(low, high, 0x20):
|
||||
print("%04x " % (addr) + "".join(map(lambda n: ("%02X" % (self.tiles[addr + n])) if addr + n in self.tiles else " ", range(0x20))))
|
||||
|
||||
def store(self, rom):
|
||||
# NOTE: This is not a very good encoder, but the background back has so much free space that we really don't care.
|
||||
# Improvements can be done to find long sequences of bytes and store those as repeated.
|
||||
result = bytearray()
|
||||
low = min(self.tiles.keys())
|
||||
high = max(self.tiles.keys()) + 1
|
||||
while low < high:
|
||||
if low not in self.tiles:
|
||||
low += 1
|
||||
continue
|
||||
different_count = 1
|
||||
while low + different_count in self.tiles and different_count < 0x40:
|
||||
different_count += 1
|
||||
same_count = 1
|
||||
while low + same_count in self.tiles and self.tiles[low] == self.tiles[low + same_count] and same_count < 0x40:
|
||||
same_count += 1
|
||||
if same_count > different_count - 4 and same_count > 2:
|
||||
result.append(low >> 8)
|
||||
result.append(low & 0xFF)
|
||||
result.append((same_count - 1) | 0x40)
|
||||
result.append(self.tiles[low])
|
||||
low += same_count
|
||||
else:
|
||||
result.append(low >> 8)
|
||||
result.append(low & 0xFF)
|
||||
result.append(different_count - 1)
|
||||
for n in range(different_count):
|
||||
result.append(self.tiles[low + n])
|
||||
low += different_count
|
||||
result.append(0x00)
|
||||
if self.__is_attributes:
|
||||
rom.background_attributes[self.__index] = result
|
||||
else:
|
||||
rom.background_tiles[self.__index] = result
|
|
@ -0,0 +1,270 @@
|
|||
class CheckMetadata:
|
||||
__slots__ = "name", "area"
|
||||
def __init__(self, name, area):
|
||||
self.name = name
|
||||
self.area = area
|
||||
|
||||
def __repr__(self):
|
||||
result = "%s - %s" % (self.area, self.name)
|
||||
return result
|
||||
|
||||
|
||||
checkMetadataTable = {
|
||||
"None": CheckMetadata("Unset Room", "None"),
|
||||
"0x1F5": CheckMetadata("Boomerang Guy Item", "Toronbo Shores"), #http://artemis251.fobby.net/zelda/maps/underworld1/01F5.GIF
|
||||
"0x2A3": CheckMetadata("Tarin's Gift", "Mabe Village"), #http://artemis251.fobby.net/zelda/maps/underworld2/02A3.GIF
|
||||
"0x301-0": CheckMetadata("Tunic Fairy Item 1", "Color Dungeon"), #http://artemis251.fobby.net/zelda/maps/underworld3/0301.GIF
|
||||
"0x301-1": CheckMetadata("Tunic Fairy Item 2", "Color Dungeon"), #http://artemis251.fobby.net/zelda/maps/underworld3/0301.GIF
|
||||
"0x2A2": CheckMetadata("Witch Item", "Koholint Prairie"), #http://artemis251.fobby.net/zelda/maps/underworld2/02A2.GIF
|
||||
"0x2A1": CheckMetadata("Shop 200 Item", "Mabe Village"), #http://artemis251.fobby.net/zelda/maps/underworld2/02A1.GIF
|
||||
"0x2A7": CheckMetadata("Shop 980 Item", "Mabe Village"), #http://artemis251.fobby.net/zelda/maps/underworld2/02A1.GIF
|
||||
"0x2A1-2": CheckMetadata("Shop 10 Item", "Mabe Village"), #http://artemis251.fobby.net/zelda/maps/underworld2/02A1.GIF
|
||||
"0x113": CheckMetadata("Pit Button Chest", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/0113.GIF
|
||||
"0x115": CheckMetadata("Four Zol Chest", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/0115.GIF
|
||||
"0x10E": CheckMetadata("Spark, Mini-Moldorm Chest", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/010E.GIF
|
||||
"0x116": CheckMetadata("Hardhat Beetles Key", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/0116.GIF
|
||||
"0x10D": CheckMetadata("Mini-Moldorm Spawn Chest", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/010D.GIF
|
||||
"0x114": CheckMetadata("Two Stalfos, Two Keese Chest", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/0114.GIF
|
||||
"0x10C": CheckMetadata("Bombable Wall Seashell Chest", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/010C.GIF
|
||||
"0x103-Owl": CheckMetadata("Spiked Beetle Owl", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/0103.GIF
|
||||
"0x104-Owl": CheckMetadata("Movable Block Owl", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/0104.GIF
|
||||
"0x11D": CheckMetadata("Feather Chest", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/011D.GIF
|
||||
"0x108": CheckMetadata("Nightmare Key Chest", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/0108.GIF
|
||||
"0x10A": CheckMetadata("Three of a Kind Chest", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/010A.GIF
|
||||
"0x10A-Owl": CheckMetadata("Three of a Kind Owl", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/010A.GIF
|
||||
"0x106": CheckMetadata("Moldorm Heart Container", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/0106.GIF
|
||||
"0x102": CheckMetadata("Full Moon Cello", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/0102.GIF
|
||||
"0x136": CheckMetadata("Entrance Chest", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0136.GIF
|
||||
"0x12E": CheckMetadata("Hardhat Beetle Pit Chest", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/012E.GIF
|
||||
"0x132": CheckMetadata("Two Stalfos Key", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0132.GIF
|
||||
"0x137": CheckMetadata("Mask-Mimic Chest", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0137.GIF
|
||||
"0x133-Owl": CheckMetadata("Switch Owl", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0133.GIF
|
||||
"0x138": CheckMetadata("First Switch Locked Chest", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0138.GIF
|
||||
"0x139": CheckMetadata("Button Spawn Chest", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0139.GIF
|
||||
"0x134": CheckMetadata("Mask-Mimic Key", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0134.GIF
|
||||
"0x126": CheckMetadata("Vacuum Mouth Chest", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0126.GIF
|
||||
"0x121": CheckMetadata("Outside Boo Buddies Room Chest", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0121.GIF
|
||||
"0x129-Owl": CheckMetadata("After Hinox Owl", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0129.GIF
|
||||
"0x12F-Owl": CheckMetadata("Before First Staircase Owl", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/012F.GIF
|
||||
"0x120": CheckMetadata("Boo Buddies Room Chest", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0120.GIF
|
||||
"0x122": CheckMetadata("Second Switch Locked Chest", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0122.GIF
|
||||
"0x127": CheckMetadata("Enemy Order Room Chest", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0127.GIF
|
||||
"0x12B": CheckMetadata("Genie Heart Container", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/012B.GIF
|
||||
"0x12A": CheckMetadata("Conch Horn", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/012A.GIF
|
||||
"0x153": CheckMetadata("Vacuum Mouth Chest", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0153.GIF
|
||||
"0x151": CheckMetadata("Two Bombite, Sword Stalfos, Zol Chest", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0151.GIF
|
||||
"0x14F": CheckMetadata("Four Zol Chest", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/014F.GIF
|
||||
"0x14E": CheckMetadata("Two Stalfos, Zol Chest", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/014E.GIF
|
||||
"0x154": CheckMetadata("North Key Room Key", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0154.GIF
|
||||
"0x154-Owl": CheckMetadata("North Key Room Owl", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0154.GIF
|
||||
"0x150": CheckMetadata("Sword Stalfos, Keese Switch Chest", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0150.GIF
|
||||
"0x14C": CheckMetadata("Zol Switch Chest", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/014C.GIF
|
||||
"0x155": CheckMetadata("West Key Room Key", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0155.GIF
|
||||
"0x158": CheckMetadata("South Key Room Key", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0158.GIF
|
||||
"0x14D": CheckMetadata("After Stairs Key", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/014D.GIF
|
||||
"0x147-Owl": CheckMetadata("Tile Arrow Owl", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0147.GIF
|
||||
"0x147": CheckMetadata("Tile Arrow Ledge Chest", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0147.GIF
|
||||
"0x146": CheckMetadata("Boots Chest", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0146.GIF
|
||||
"0x142": CheckMetadata("Three Zol, Stalfos Chest", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0142.GIF
|
||||
"0x141": CheckMetadata("Three Bombite Key", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0141.GIF
|
||||
"0x148": CheckMetadata("Two Zol, Two Pairodd Key", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0148.GIF
|
||||
"0x144": CheckMetadata("Two Zol, Stalfos Ledge Chest", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0144.GIF
|
||||
"0x140-Owl": CheckMetadata("Flying Bomb Owl", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0140.GIF
|
||||
"0x15B": CheckMetadata("Nightmare Door Key", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/015B.GIF
|
||||
"0x15A": CheckMetadata("Slime Eye Heart Container", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/015A.GIF
|
||||
"0x159": CheckMetadata("Sea Lily's Bell", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0159.GIF
|
||||
"0x179": CheckMetadata("Watery Statue Chest", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/0179.GIF
|
||||
"0x16A": CheckMetadata("NW of Boots Pit Ledge Chest", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/016A.GIF
|
||||
"0x178": CheckMetadata("Two Spiked Beetle, Zol Chest", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/0178.GIF
|
||||
"0x17B": CheckMetadata("Crystal Chest", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/017B.GIF
|
||||
"0x171": CheckMetadata("Lower Bomb Locked Watery Chest", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/0171.GIF
|
||||
"0x165": CheckMetadata("Upper Bomb Locked Watery Chest", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/0165.GIF
|
||||
"0x175": CheckMetadata("Flipper Locked Before Boots Pit Chest", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/0175.GIF
|
||||
"0x16F-Owl": CheckMetadata("Spiked Beetle Owl", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/016F.GIF
|
||||
"0x169": CheckMetadata("Pit Key", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/0169.GIF
|
||||
"0x16E": CheckMetadata("Flipper Locked After Boots Pit Chest", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/016E.GIF
|
||||
"0x16D": CheckMetadata("Blob Chest", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/016D.GIF
|
||||
"0x168": CheckMetadata("Spark Chest", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/0168.GIF
|
||||
"0x160": CheckMetadata("Flippers Chest", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/0160.GIF
|
||||
"0x176": CheckMetadata("Nightmare Key Ledge Chest", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/0176.GIF
|
||||
"0x166": CheckMetadata("Angler Fish Heart Container", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/01FF.GIF
|
||||
"0x162": CheckMetadata("Surf Harp", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/0162.GIF
|
||||
"0x1A0": CheckMetadata("Entrance Hookshottable Chest", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/01A0.GIF
|
||||
"0x19E": CheckMetadata("Spark, Two Iron Mask Chest", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/019E.GIF
|
||||
"0x181": CheckMetadata("Crystal Key", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/0181.GIF
|
||||
"0x19A-Owl": CheckMetadata("Crystal Owl", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/019A.GIF
|
||||
"0x19B": CheckMetadata("Flying Bomb Chest South", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/019B.GIF
|
||||
"0x197": CheckMetadata("Three Iron Mask Chest", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/0197.GIF
|
||||
"0x196": CheckMetadata("Hookshot Note Chest", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/0196.GIF
|
||||
"0x18A-Owl": CheckMetadata("Star Owl", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/018A.GIF
|
||||
"0x18E": CheckMetadata("Two Stalfos, Star Pit Chest", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/018E.GIF
|
||||
"0x188": CheckMetadata("Swort Stalfos, Star, Bridge Chest", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/0188.GIF
|
||||
"0x18F": CheckMetadata("Flying Bomb Chest East", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/018F.GIF
|
||||
"0x180": CheckMetadata("Master Stalfos Item", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/0180.GIF
|
||||
"0x183": CheckMetadata("Three Stalfos Chest", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/0183.GIF
|
||||
"0x186": CheckMetadata("Nightmare Key/Torch Cross Chest", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/0186.GIF
|
||||
"0x185": CheckMetadata("Slime Eel Heart Container", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/0185.GIF
|
||||
"0x182": CheckMetadata("Wind Marimba", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/0182.GIF
|
||||
"0x1CF": CheckMetadata("Mini-Moldorm, Spark Chest", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01CF.GIF
|
||||
"0x1C9": CheckMetadata("Flying Heart, Statue Chest", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01C9.GIF
|
||||
"0x1BB-Owl": CheckMetadata("Corridor Owl", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01BB.GIF
|
||||
"0x1CE": CheckMetadata("L2 Bracelet Chest", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01CE.GIF
|
||||
"0x1C0": CheckMetadata("Three Wizzrobe, Switch Chest", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01C0.GIF
|
||||
"0x1B9": CheckMetadata("Stairs Across Statues Chest", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01B9.GIF
|
||||
"0x1B3": CheckMetadata("Switch, Star Above Statues Chest", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01B3.GIF
|
||||
"0x1B4": CheckMetadata("Two Wizzrobe Key", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01B4.GIF
|
||||
"0x1B0": CheckMetadata("Top Left Horse Heads Chest", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01B0.GIF
|
||||
"0x06C": CheckMetadata("Raft Chest", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/overworld/006C.GIF
|
||||
"0x1BE": CheckMetadata("Water Tektite Chest", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01BE.GIF
|
||||
"0x1D1": CheckMetadata("Four Wizzrobe Ledge Chest", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01D1.GIF
|
||||
"0x1D7-Owl": CheckMetadata("Blade Trap Owl", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01D7.GIF
|
||||
"0x1C3": CheckMetadata("Tile Room Key", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01C3.GIF
|
||||
"0x1B1": CheckMetadata("Top Right Horse Heads Chest", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01B1.GIF
|
||||
"0x1B6-Owl": CheckMetadata("Pot Owl", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01B6.GIF
|
||||
"0x1B6": CheckMetadata("Pot Locked Chest", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01B6.GIF
|
||||
"0x1BC": CheckMetadata("Facade Heart Container", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01BC.GIF
|
||||
"0x1B5": CheckMetadata("Coral Triangle", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01B5.GIF
|
||||
"0x210": CheckMetadata("Entrance Key", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/0210.GIF
|
||||
"0x216-Owl": CheckMetadata("Ball Owl", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/0216.GIF
|
||||
"0x212": CheckMetadata("Horse Head, Bubble Chest", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/0212.GIF
|
||||
"0x204-Owl": CheckMetadata("Beamos Owl", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/0204.GIF
|
||||
"0x204": CheckMetadata("Beamos Ledge Chest", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/0204.GIF
|
||||
"0x209": CheckMetadata("Switch Wrapped Chest", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/0209.GIF
|
||||
"0x211": CheckMetadata("Three of a Kind, No Pit Chest", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/0211.GIF
|
||||
"0x21B": CheckMetadata("Hinox Key", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/021B.GIF
|
||||
"0x201": CheckMetadata("Kirby Ledge Chest", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/0201.GIF
|
||||
"0x21C-Owl": CheckMetadata("Three of a Kind, Pit Owl", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/021C.GIF
|
||||
"0x21C": CheckMetadata("Three of a Kind, Pit Chest", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/021C.GIF
|
||||
"0x224": CheckMetadata("Nightmare Key/After Grim Creeper Chest", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/0224.GIF
|
||||
"0x21A": CheckMetadata("Mirror Shield Chest", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/021A.GIF
|
||||
"0x220": CheckMetadata("Conveyor Beamos Chest", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/0220.GIF
|
||||
"0x223": CheckMetadata("Evil Eagle Heart Container", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/02E8.GIF
|
||||
"0x22C": CheckMetadata("Organ of Evening Calm", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/022C.GIF
|
||||
"0x24F": CheckMetadata("Push Block Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/024F.GIF
|
||||
"0x24D": CheckMetadata("Left of Hinox Zamboni Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/024D.GIF
|
||||
"0x25C": CheckMetadata("Vacuum Mouth Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/025C.GIF
|
||||
"0x24C": CheckMetadata("Left Vire Key", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/024C.GIF
|
||||
"0x255": CheckMetadata("Spark, Pit Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0255.GIF
|
||||
"0x246": CheckMetadata("Two Torches Room Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0246.GIF
|
||||
"0x253-Owl": CheckMetadata("Beamos Owl", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0253.GIF
|
||||
"0x259": CheckMetadata("Right Lava Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0259.GIF
|
||||
"0x25A": CheckMetadata("Zamboni, Two Zol Key", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/025A.GIF
|
||||
"0x25F": CheckMetadata("Four Ropes Pot Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/025F.GIF
|
||||
"0x245-Owl": CheckMetadata("Bombable Blocks Owl", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0245.GIF
|
||||
"0x23E": CheckMetadata("Gibdos on Cracked Floor Key", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/023E.GIF
|
||||
"0x235": CheckMetadata("Lava Ledge Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0235.GIF
|
||||
"0x237": CheckMetadata("Magic Rod Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0237.GIF
|
||||
"0x240": CheckMetadata("Beamos Blocked Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0240.GIF
|
||||
"0x23D": CheckMetadata("Dodongo Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/023D.GIF
|
||||
"0x000": CheckMetadata("Outside Heart Piece", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/overworld/0000.GIF
|
||||
"0x241": CheckMetadata("Lava Arrow Statue Key", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0241.GIF
|
||||
"0x241-Owl": CheckMetadata("Lava Arrow Statue Owl", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0241.GIF
|
||||
"0x23A": CheckMetadata("West of Boss Door Ledge Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/023A.GIF
|
||||
"0x232": CheckMetadata("Nightmare Key/Big Zamboni Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0232.GIF
|
||||
"0x234": CheckMetadata("Hot Head Heart Container", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0234.GIF
|
||||
"0x230": CheckMetadata("Thunder Drum", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0230.GIF
|
||||
"0x314": CheckMetadata("Lower Small Key", "Color Dungeon"), #http://artemis251.fobby.net/zelda/maps/underworld3/0314.GIF
|
||||
"0x308-Owl": CheckMetadata("Upper Key Owl", "Color Dungeon"), #http://artemis251.fobby.net/zelda/maps/underworld3/0308.GIF
|
||||
"0x308": CheckMetadata("Upper Small Key", "Color Dungeon"), #http://artemis251.fobby.net/zelda/maps/underworld3/0308.GIF
|
||||
"0x30F-Owl": CheckMetadata("Entrance Owl", "Color Dungeon"), #http://artemis251.fobby.net/zelda/maps/underworld3/030F.GIF
|
||||
"0x30F": CheckMetadata("Entrance Chest", "Color Dungeon"), #http://artemis251.fobby.net/zelda/maps/underworld3/030F.GIF
|
||||
"0x311": CheckMetadata("Two Socket Chest", "Color Dungeon"), #http://artemis251.fobby.net/zelda/maps/underworld3/0311.GIF
|
||||
"0x302": CheckMetadata("Nightmare Key Chest", "Color Dungeon"), #http://artemis251.fobby.net/zelda/maps/underworld3/0302.GIF
|
||||
"0x306": CheckMetadata("Zol Chest", "Color Dungeon"), #http://artemis251.fobby.net/zelda/maps/underworld3/0306.GIF
|
||||
"0x307": CheckMetadata("Bullshit Room", "Color Dungeon"), #http://artemis251.fobby.net/zelda/maps/underworld3/0307.GIF
|
||||
"0x30A-Owl": CheckMetadata("Puzzowl", "Color Dungeon"), #http://artemis251.fobby.net/zelda/maps/underworld3/030A.GIF
|
||||
"0x2BF": CheckMetadata("Dream Hut East", "Mabe Village"), #http://artemis251.fobby.net/zelda/maps/underworld2/02BF.GIF
|
||||
"0x2BE": CheckMetadata("Dream Hut West", "Mabe Village"), #http://artemis251.fobby.net/zelda/maps/underworld2/02BE.GIF
|
||||
"0x2A4": CheckMetadata("Well Heart Piece", "Mabe Village"), #http://artemis251.fobby.net/zelda/maps/underworld2/02A4.GIF
|
||||
"0x2B1": CheckMetadata("Fishing Game Heart Piece", "Mabe Village"), #http://artemis251.fobby.net/zelda/maps/underworld2/02B1.GIF
|
||||
"0x0A3": CheckMetadata("Bush Field", "Mabe Village"), #http://artemis251.fobby.net/zelda/maps/overworld/00A3.GIF
|
||||
"0x2B2": CheckMetadata("Dog House Dig", "Mabe Village"), #http://artemis251.fobby.net/zelda/maps/underworld2/02B2.GIF
|
||||
"0x0D2": CheckMetadata("Outside D1 Tree Bonk", "Toronbo Shores"), #http://artemis251.fobby.net/zelda/maps/overworld/00D2.GIF
|
||||
"0x0E5": CheckMetadata("West of Ghost House Chest", "Toronbo Shores"), #http://artemis251.fobby.net/zelda/maps/overworld/00E5.GIF
|
||||
"0x1E3": CheckMetadata("Ghost House Barrel", "Martha's Bay"), #http://artemis251.fobby.net/zelda/maps/underworld1/01E3.GIF
|
||||
"0x044": CheckMetadata("Heart Piece of Shame", "Koholint Prairie"), #http://artemis251.fobby.net/zelda/maps/overworld/0044.GIF
|
||||
"0x071": CheckMetadata("Two Zol, Moblin Chest", "Mysterious Woods"), #http://artemis251.fobby.net/zelda/maps/overworld/0071.GIF
|
||||
"0x1E1": CheckMetadata("Mad Batter", "Mysterious Woods"), #http://artemis251.fobby.net/zelda/maps/underworld1/01E1.GIF
|
||||
"0x034": CheckMetadata("Swampy Chest", "Goponga Swamp"), #http://artemis251.fobby.net/zelda/maps/overworld/0034.GIF
|
||||
"0x041": CheckMetadata("Tail Key Chest", "Mysterious Woods"), #http://artemis251.fobby.net/zelda/maps/overworld/0041.GIF
|
||||
"0x2BD": CheckMetadata("Cave Crystal Chest", "Mysterious Woods"), #http://artemis251.fobby.net/zelda/maps/underworld2/02BD.GIF
|
||||
"0x2AB": CheckMetadata("Cave Skull Heart Piece", "Mysterious Woods"), #http://artemis251.fobby.net/zelda/maps/underworld2/02AB.GIF
|
||||
"0x2B3": CheckMetadata("Hookshot Cave", "Mysterious Woods"), #http://artemis251.fobby.net/zelda/maps/underworld2/02B3.GIF
|
||||
"0x2AE": CheckMetadata("Write Cave West", "Goponga Swamp"), #http://artemis251.fobby.net/zelda/maps/underworld2/02AE.GIF
|
||||
"0x011-Owl": CheckMetadata("North of Write Owl", "Goponga Swamp"), #http://artemis251.fobby.net/zelda/maps/overworld/0011.GIF #might come out as "0x11
|
||||
"0x2AF": CheckMetadata("Write Cave East", "Goponga Swamp"), #http://artemis251.fobby.net/zelda/maps/underworld2/02AF.GIF
|
||||
"0x035-Owl": CheckMetadata("Moblin Cave Owl", "Tal Tal Heights"), #http://artemis251.fobby.net/zelda/maps/overworld/0035.GIF
|
||||
"0x2DF": CheckMetadata("Graveyard Connector", "Koholint Prairie"), #http://artemis251.fobby.net/zelda/maps/underworld2/02DF.GIF
|
||||
"0x074": CheckMetadata("Ghost Grave Dig", "Koholint Prairie"), #http://artemis251.fobby.net/zelda/maps/overworld/0074.GIF
|
||||
"0x2E2": CheckMetadata("Moblin Cave", "Tal Tal Heights"), #http://artemis251.fobby.net/zelda/maps/underworld2/02E2.GIF
|
||||
"0x2CD": CheckMetadata("Cave East of Mabe", "Ukuku Prairie"), #http://artemis251.fobby.net/zelda/maps/underworld2/02CD.GIF
|
||||
"0x2F4": CheckMetadata("Boots 'n' Bomb Cave Chest", "Ukuku Prairie"), #http://artemis251.fobby.net/zelda/maps/underworld2/02F4.GIF
|
||||
"0x2E5": CheckMetadata("Boots 'n' Bomb Cave Bombable Wall", "Ukuku Prairie"), #http://artemis251.fobby.net/zelda/maps/underworld2/02E5.GIF
|
||||
"0x0A5": CheckMetadata("Outside D3 Ledge Dig", "Ukuku Prairie"), #http://artemis251.fobby.net/zelda/maps/overworld/00A5.GIF
|
||||
"0x0A6": CheckMetadata("Outside D3 Island Bush", "Ukuku Prairie"), #http://artemis251.fobby.net/zelda/maps/overworld/00A6.GIF
|
||||
"0x08B": CheckMetadata("East of Seashell Mansion Bush", "Ukuku Prairie"), #http://artemis251.fobby.net/zelda/maps/overworld/008B.GIF
|
||||
"0x0A4": CheckMetadata("East of Mabe Tree Bonk", "Ukuku Prairie"), #http://artemis251.fobby.net/zelda/maps/overworld/00A4.GIF
|
||||
"0x2E9": CheckMetadata("Seashell Mansion", "Ukuku Prairie"),
|
||||
"0x1FD": CheckMetadata("Boots Pit", "Kanalet Castle"), #http://artemis251.fobby.net/zelda/maps/underworld1/01FD.GIF
|
||||
"0x0B9": CheckMetadata("Rock Seashell", "Donut Plains"), #http://artemis251.fobby.net/zelda/maps/overworld/00B9.GIF
|
||||
"0x0E9": CheckMetadata("Lone Bush", "Martha's Bay"), #http://artemis251.fobby.net/zelda/maps/overworld/00E9.GIF
|
||||
"0x0F8": CheckMetadata("Island Bush of Destiny", "Martha's Bay"), #http://artemis251.fobby.net/zelda/maps/overworld/00F8.GIF
|
||||
"0x0A8": CheckMetadata("Donut Plains Ledge Dig", "Donut Plains"), #http://artemis251.fobby.net/zelda/maps/overworld/00A8.GIF
|
||||
"0x0A8-Owl": CheckMetadata("Donut Plains Ledge Owl", "Donut Plains"), #http://artemis251.fobby.net/zelda/maps/overworld/00A8.GIF
|
||||
"0x1E0": CheckMetadata("Mad Batter", "Martha's Bay"), #http://artemis251.fobby.net/zelda/maps/underworld1/01E0.GIF
|
||||
"0x0C6-Owl": CheckMetadata("Slime Key Owl", "Pothole Field"), #http://artemis251.fobby.net/zelda/maps/overworld/00C6.GIF
|
||||
"0x0C6": CheckMetadata("Slime Key Dig", "Pothole Field"), #http://artemis251.fobby.net/zelda/maps/overworld/00C6.GIF
|
||||
"0x2C8": CheckMetadata("Under Richard's House", "Pothole Field"), #http://artemis251.fobby.net/zelda/maps/underworld2/02C8.GIF
|
||||
"0x078": CheckMetadata("In the Moat Heart Piece", "Kanalet Castle"), #http://artemis251.fobby.net/zelda/maps/overworld/0078.GIF
|
||||
"0x05A": CheckMetadata("Bomberman Meets Whack-a-mole Leaf", "Kanalet Castle"), #http://artemis251.fobby.net/zelda/maps/overworld/005A.GIF
|
||||
"0x058": CheckMetadata("Crow Rock Leaf", "Kanalet Castle"), #http://artemis251.fobby.net/zelda/maps/overworld/0058.GIF
|
||||
"0x2D2": CheckMetadata("Darknut, Zol, Bubble Leaf", "Kanalet Castle"), #http://artemis251.fobby.net/zelda/maps/underworld2/02D2.GIF
|
||||
"0x2C5": CheckMetadata("Bombable Darknut Leaf", "Kanalet Castle"), #http://artemis251.fobby.net/zelda/maps/underworld2/02C5.GIF
|
||||
"0x2C6": CheckMetadata("Ball and Chain Darknut Leaf", "Kanalet Castle"), #http://artemis251.fobby.net/zelda/maps/underworld2/02C6.GIF
|
||||
"0x0DA": CheckMetadata("Peninsula Dig", "Martha's Bay"), #http://artemis251.fobby.net/zelda/maps/overworld/00DA.GIF
|
||||
"0x0DA-Owl": CheckMetadata("Peninsula Owl", "Martha's Bay"), #http://artemis251.fobby.net/zelda/maps/overworld/00DA.GIF
|
||||
"0x0CF-Owl": CheckMetadata("Desert Owl", "Yarna Desert"), #http://artemis251.fobby.net/zelda/maps/overworld/00CF.GIF
|
||||
"0x2E6": CheckMetadata("Bomb Arrow Cave", "Yarna Desert"), #http://artemis251.fobby.net/zelda/maps/underworld2/02E6.GIF
|
||||
"0x1E8": CheckMetadata("Cave Under Lanmola", "Yarna Desert"), #http://artemis251.fobby.net/zelda/maps/underworld1/01E8.GIF
|
||||
"0x0FF": CheckMetadata("Rock Seashell", "Yarna Desert"), #http://artemis251.fobby.net/zelda/maps/overworld/00FF.GIF
|
||||
"0x018": CheckMetadata("Access Tunnel Exterior", "Tal Tal Mountains"), #http://artemis251.fobby.net/zelda/maps/overworld/0018.GIF
|
||||
"0x2BB": CheckMetadata("Access Tunnel Interior", "Tal Tal Mountains"), #http://artemis251.fobby.net/zelda/maps/underworld2/02BB.GIF
|
||||
"0x28A": CheckMetadata("Paphl Cave", "Tal Tal Mountains"), #http://artemis251.fobby.net/zelda/maps/underworld2/028A.GIF
|
||||
"0x1F2": CheckMetadata("Damp Cave Heart Piece", "Tal Tal Heights"), #http://artemis251.fobby.net/zelda/maps/underworld1/01F2.GIF
|
||||
"0x2FC": CheckMetadata("Under Armos Cave", "Southern Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld2/02FC.GIF
|
||||
"0x08F-Owl": CheckMetadata("Outside Owl", "Southern Face Shrine"), #http://artemis251.fobby.net/zelda/maps/overworld/008F.GIF
|
||||
"0x05C": CheckMetadata("West", "Rapids Ride"), #http://artemis251.fobby.net/zelda/maps/overworld/005C.GIF
|
||||
"0x05D": CheckMetadata("East", "Rapids Ride"), #http://artemis251.fobby.net/zelda/maps/overworld/005D.GIF
|
||||
"0x05D-Owl": CheckMetadata("Owl", "Rapids Ride"), #http://artemis251.fobby.net/zelda/maps/overworld/005D.GIF
|
||||
"0x01E-Owl": CheckMetadata("Outside D7 Owl", "Tal Tal Mountains"), #http://artemis251.fobby.net/zelda/maps/overworld/001E.GIF
|
||||
"0x00C": CheckMetadata("Bridge Rock", "Tal Tal Mountains"), #http://artemis251.fobby.net/zelda/maps/overworld/000C.GIF
|
||||
"0x2F2": CheckMetadata("Five Chest Game", "Tal Tal Mountains"), #http://artemis251.fobby.net/zelda/maps/underworld2/02F2.GIF
|
||||
"0x01D": CheckMetadata("Outside Five Chest Game", "Tal Tal Mountains"), #http://artemis251.fobby.net/zelda/maps/overworld/001D.GIF
|
||||
"0x004": CheckMetadata("Outside Mad Batter", "Tal Tal Mountains"), #http://artemis251.fobby.net/zelda/maps/overworld/0004.GIF
|
||||
"0x1E2": CheckMetadata("Mad Batter", "Tal Tal Mountains"), #http://artemis251.fobby.net/zelda/maps/underworld1/01E2.GIF
|
||||
"0x2BA": CheckMetadata("Access Tunnel Bombable Heart Piece", "Tal Tal Mountains"), #http://artemis251.fobby.net/zelda/maps/underworld2/02BA.GIF
|
||||
"0x0F2": CheckMetadata("Sword on the Beach", "Toronbo Shores"), #http://artemis251.fobby.net/zelda/maps/overworld/00F2.GIF
|
||||
"0x050": CheckMetadata("Toadstool", "Mysterious Woods"), #http://artemis251.fobby.net/zelda/maps/overworld/0050.GIF
|
||||
"0x0CE": CheckMetadata("Lanmola", "Yarna Desert"), #http://artemis251.fobby.net/zelda/maps/overworld/00CE.GIF
|
||||
"0x27F": CheckMetadata("Armos Knight", "Southern Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld2/027F.GIF
|
||||
"0x27A": CheckMetadata("Bird Key Cave", "Tal Tal Mountains"), #http://artemis251.fobby.net/zelda/maps/underworld2/027A.GIF
|
||||
"0x092": CheckMetadata("Ballad of the Wind Fish", "Mabe Village"),
|
||||
"0x2FD": CheckMetadata("Manbo's Mambo", "Tal Tal Heights"),
|
||||
"0x2FB": CheckMetadata("Mamu", "Ukuku Prairie"),
|
||||
"0x1E4": CheckMetadata("Rooster", "Mabe Village"),
|
||||
|
||||
"0x2A0-Trade": CheckMetadata("Trendy Game", "Mabe Village"),
|
||||
"0x2A6-Trade": CheckMetadata("Papahl's Wife", "Mabe Village"),
|
||||
"0x2B2-Trade": CheckMetadata("YipYip", "Mabe Village"),
|
||||
"0x2FE-Trade": CheckMetadata("Banana Sale", "Toronbo Shores"),
|
||||
"0x07B-Trade": CheckMetadata("Kiki", "Ukuku Prairie"),
|
||||
"0x087-Trade": CheckMetadata("Honeycomb", "Ukuku Prairie"),
|
||||
"0x2D7-Trade": CheckMetadata("Bear Cook", "Animal Village"),
|
||||
"0x019-Trade": CheckMetadata("Papahl", "Tal Tal Heights"),
|
||||
"0x2D9-Trade": CheckMetadata("Goat", "Animal Village"),
|
||||
"0x2A8-Trade": CheckMetadata("MrWrite", "Goponga Swamp"),
|
||||
"0x0CD-Trade": CheckMetadata("Grandma", "Animal Village"),
|
||||
"0x2F5-Trade": CheckMetadata("Fisher", "Martha's Bay"),
|
||||
"0x0C9-Trade": CheckMetadata("Mermaid", "Martha's Bay"),
|
||||
"0x297-Trade": CheckMetadata("Mermaid Statue", "Martha's Bay"),
|
||||
}
|
|
@ -0,0 +1,561 @@
|
|||
COUNT = 0xFB
|
||||
NAME = [
|
||||
"ARROW",
|
||||
"BOOMERANG",
|
||||
"BOMB",
|
||||
"HOOKSHOT_CHAIN",
|
||||
"HOOKSHOT_HIT",
|
||||
"LIFTABLE_ROCK",
|
||||
"PUSHED_BLOCK",
|
||||
"CHEST_WITH_ITEM",
|
||||
"MAGIC_POWDER_SPRINKLE",
|
||||
"OCTOROCK",
|
||||
"OCTOROCK_ROCK",
|
||||
"MOBLIN",
|
||||
"MOBLIN_ARROW",
|
||||
"TEKTITE",
|
||||
"LEEVER",
|
||||
"ARMOS_STATUE",
|
||||
"HIDING_GHINI",
|
||||
"GIANT_GHINI",
|
||||
"GHINI",
|
||||
"BROKEN_HEART_CONTAINER",
|
||||
"MOBLIN_SWORD",
|
||||
"ANTI_FAIRY",
|
||||
"SPARK_COUNTER_CLOCKWISE",
|
||||
"SPARK_CLOCKWISE",
|
||||
"POLS_VOICE",
|
||||
"KEESE",
|
||||
"STALFOS_AGGRESSIVE",
|
||||
"GEL",
|
||||
"MINI_GEL",
|
||||
"DISABLED",
|
||||
"STALFOS_EVASIVE",
|
||||
"GIBDO",
|
||||
"HARDHAT_BEETLE",
|
||||
"WIZROBE",
|
||||
"WIZROBE_PROJECTILE",
|
||||
"LIKE_LIKE",
|
||||
"IRON_MASK",
|
||||
"SMALL_EXPLOSION_ENEMY",
|
||||
"SMALL_EXPLOSION_ENEMY_2",
|
||||
"SPIKE_TRAP",
|
||||
"MIMIC",
|
||||
"MINI_MOLDORM",
|
||||
"LASER",
|
||||
"LASER_BEAM",
|
||||
"SPIKED_BEETLE",
|
||||
"DROPPABLE_HEART",
|
||||
"DROPPABLE_RUPEE",
|
||||
"DROPPABLE_FAIRY",
|
||||
"KEY_DROP_POINT",
|
||||
"SWORD",
|
||||
"32",
|
||||
"PIECE_OF_POWER",
|
||||
"GUARDIAN_ACORN",
|
||||
"HEART_PIECE",
|
||||
"HEART_CONTAINER",
|
||||
"DROPPABLE_ARROWS",
|
||||
"DROPPABLE_BOMBS",
|
||||
"INSTRUMENT_OF_THE_SIRENS",
|
||||
"SLEEPY_TOADSTOOL",
|
||||
"DROPPABLE_MAGIC_POWDER",
|
||||
"HIDING_SLIME_KEY",
|
||||
"DROPPABLE_SECRET_SEASHELL",
|
||||
"MARIN",
|
||||
"RACOON",
|
||||
"WITCH",
|
||||
"OWL_EVENT",
|
||||
"OWL_STATUE",
|
||||
"SEASHELL_MANSION_TREES",
|
||||
"YARNA_TALKING_BONES",
|
||||
"BOULDERS",
|
||||
"MOVING_BLOCK_LEFT_TOP",
|
||||
"MOVING_BLOCK_LEFT_BOTTOM",
|
||||
"MOVING_BLOCK_BOTTOM_LEFT",
|
||||
"MOVING_BLOCK_BOTTOM_RIGHT",
|
||||
"COLOR_DUNGEON_BOOK",
|
||||
"POT",
|
||||
"DISABLED",
|
||||
"SHOP_OWNER",
|
||||
"4D",
|
||||
"TRENDY_GAME_OWNER",
|
||||
"BOO_BUDDY",
|
||||
"KNIGHT",
|
||||
"TRACTOR_DEVICE",
|
||||
"TRACTOR_DEVICE_REVERSE",
|
||||
"FISHERMAN_FISHING_GAME",
|
||||
"BOUNCING_BOMBITE",
|
||||
"TIMER_BOMBITE",
|
||||
"PAIRODD",
|
||||
"PAIRODD_PROJECTILE",
|
||||
"MOLDORM",
|
||||
"FACADE",
|
||||
"SLIME_EYE",
|
||||
"GENIE",
|
||||
"SLIME_EEL",
|
||||
"GHOMA",
|
||||
"MASTER_STALFOS",
|
||||
"DODONGO_SNAKE",
|
||||
"WARP",
|
||||
"HOT_HEAD",
|
||||
"EVIL_EAGLE",
|
||||
"SOUTH_FACE_SHRINE_DOOR",
|
||||
"ANGLER_FISH",
|
||||
"CRYSTAL_SWITCH",
|
||||
"67",
|
||||
"68",
|
||||
"MOVING_BLOCK_MOVER",
|
||||
"RAFT_RAFT_OWNER",
|
||||
"TEXT_DEBUGGER",
|
||||
"CUCCO",
|
||||
"BOW_WOW",
|
||||
"BUTTERFLY",
|
||||
"DOG",
|
||||
"KID_70",
|
||||
"KID_71",
|
||||
"KID_72",
|
||||
"KID_73",
|
||||
"PAPAHLS_WIFE",
|
||||
"GRANDMA_ULRIRA",
|
||||
"MR_WRITE",
|
||||
"GRANDPA_ULRIRA",
|
||||
"YIP_YIP",
|
||||
"MADAM_MEOWMEOW",
|
||||
"CROW",
|
||||
"CRAZY_TRACY",
|
||||
"GIANT_GOPONGA_FLOWER",
|
||||
"GOPONGA_FLOWER_PROJECTILE",
|
||||
"GOPONGA_FLOWER",
|
||||
"TURTLE_ROCK_HEAD",
|
||||
"TELEPHONE",
|
||||
"ROLLING_BONES",
|
||||
"ROLLING_BONES_BAR",
|
||||
"DREAM_SHRINE_BED",
|
||||
"BIG_FAIRY",
|
||||
"MR_WRITES_BIRD",
|
||||
"FLOATING_ITEM",
|
||||
"DESERT_LANMOLA",
|
||||
"ARMOS_KNIGHT",
|
||||
"HINOX",
|
||||
"TILE_GLINT_SHOWN",
|
||||
"TILE_GLINT_HIDDEN",
|
||||
"8C",
|
||||
"8D",
|
||||
"CUE_BALL",
|
||||
"MASKED_MIMIC_GORIYA",
|
||||
"THREE_OF_A_KIND",
|
||||
"ANTI_KIRBY",
|
||||
"SMASHER",
|
||||
"MAD_BOMBER",
|
||||
"KANALET_BOMBABLE_WALL",
|
||||
"RICHARD",
|
||||
"RICHARD_FROG",
|
||||
"DIVE_SPOT",
|
||||
"HORSE_PIECE",
|
||||
"WATER_TEKTITE",
|
||||
"FLYING_TILES",
|
||||
"HIDING_GEL",
|
||||
"STAR",
|
||||
"LIFTABLE_STATUE",
|
||||
"FIREBALL_SHOOTER",
|
||||
"GOOMBA",
|
||||
"PEAHAT",
|
||||
"SNAKE",
|
||||
"PIRANHA_PLANT",
|
||||
"SIDE_VIEW_PLATFORM_HORIZONTAL",
|
||||
"SIDE_VIEW_PLATFORM_VERTICAL",
|
||||
"SIDE_VIEW_PLATFORM",
|
||||
"SIDE_VIEW_WEIGHTS",
|
||||
"SMASHABLE_PILLAR",
|
||||
"WRECKING_BALL",
|
||||
"BLOOPER",
|
||||
"CHEEP_CHEEP_HORIZONTAL",
|
||||
"CHEEP_CHEEP_VERTICAL",
|
||||
"CHEEP_CHEEP_JUMPING",
|
||||
"KIKI_THE_MONKEY",
|
||||
"WINGED_OCTOROK",
|
||||
"TRADING_ITEM",
|
||||
"PINCER",
|
||||
"HOLE_FILLER",
|
||||
"BEETLE_SPAWNER",
|
||||
"HONEYCOMB",
|
||||
"TARIN",
|
||||
"BEAR",
|
||||
"PAPAHL",
|
||||
"MERMAID",
|
||||
"FISHERMAN_UNDER_BRIDGE",
|
||||
"BUZZ_BLOB",
|
||||
"BOMBER",
|
||||
"BUSH_CRAWLER",
|
||||
"GRIM_CREEPER",
|
||||
"VIRE",
|
||||
"BLAINO",
|
||||
"ZOMBIE",
|
||||
"MAZE_SIGNPOST",
|
||||
"MARIN_AT_THE_SHORE",
|
||||
"MARIN_AT_TAL_TAL_HEIGHTS",
|
||||
"MAMU_AND_FROGS",
|
||||
"WALRUS",
|
||||
"URCHIN",
|
||||
"SAND_CRAB",
|
||||
"MANBO_AND_FISHES",
|
||||
"BUNNY_CALLING_MARIN",
|
||||
"MUSICAL_NOTE",
|
||||
"MAD_BATTER",
|
||||
"ZORA",
|
||||
"FISH",
|
||||
"BANANAS_SCHULE_SALE",
|
||||
"MERMAID_STATUE",
|
||||
"SEASHELL_MANSION",
|
||||
"ANIMAL_D0",
|
||||
"ANIMAL_D1",
|
||||
"ANIMAL_D2",
|
||||
"BUNNY_D3",
|
||||
"GHOST",
|
||||
"ROOSTER",
|
||||
"SIDE_VIEW_POT",
|
||||
"THWIMP",
|
||||
"THWOMP",
|
||||
"THWOMP_RAMMABLE",
|
||||
"PODOBOO",
|
||||
"GIANT_BUBBLE",
|
||||
"FLYING_ROOSTER_EVENTS",
|
||||
"BOOK",
|
||||
"EGG_SONG_EVENT",
|
||||
"SWORD_BEAM",
|
||||
"MONKEY",
|
||||
"WITCH_RAT",
|
||||
"FLAME_SHOOTER",
|
||||
"POKEY",
|
||||
"MOBLIN_KING",
|
||||
"FLOATING_ITEM_2",
|
||||
"FINAL_NIGHTMARE",
|
||||
"KANALET_CASTLE_GATE_SWITCH",
|
||||
"ENDING_OWL_STAIR_CLIMBING",
|
||||
"COLOR_SHELL_RED",
|
||||
"COLOR_SHELL_GREEN",
|
||||
"COLOR_SHELL_BLUE",
|
||||
"COLOR_GHOUL_RED",
|
||||
"COLOR_GHOUL_GREEN",
|
||||
"COLOR_GHOUL_BLUE",
|
||||
"ROTOSWITCH_RED",
|
||||
"ROTOSWITCH_YELLOW",
|
||||
"ROTOSWITCH_BLUE",
|
||||
"FLYING_HOPPER_BOMBS",
|
||||
"HOPPER",
|
||||
"AVALAUNCH",
|
||||
"BOUNCING_BOULDER",
|
||||
"COLOR_GUARDIAN_BLUE",
|
||||
"COLOR_GUARDIAN_RED",
|
||||
"GIANT_BUZZ_BLOB",
|
||||
"HARDHIT_BEETLE",
|
||||
"PHOTOGRAPHER",
|
||||
]
|
||||
|
||||
def _moblinSpriteData(room):
|
||||
if room.room in (0x002, 0x013): # Tal tal heights exception
|
||||
return (2, 0x9C) # Hooded stalfos
|
||||
if room.room < 0x100:
|
||||
x = room.room & 0x0F
|
||||
y = (room.room >> 4) & 0x0F
|
||||
if x < 0x04: # Left side is woods and mountain moblins
|
||||
return (2, 0x7C) # Moblin
|
||||
if 0x08 <= x <= 0x0B and 4 <= y <= 0x07: # Castle
|
||||
return (2, 0x92) # Knight
|
||||
# Everything else is pigs
|
||||
return (2, 0x83) # Pig
|
||||
elif room.room < 0x1DF: # Dungeons contain hooded stalfos
|
||||
return (2, 0x9C) # Hooded stalfos
|
||||
elif room.room < 0x200: # Caves contain moblins
|
||||
return (2, 0x7C) # Moblin
|
||||
elif room.room < 0x276: # Dungeons contain hooded stalfos
|
||||
return (2, 0x9C) # Hooded stalfos
|
||||
elif room.room < 0x300: # Caves contain moblins
|
||||
x = room.room & 0x0F
|
||||
y = (room.room >> 4) & 0x0F
|
||||
if 2 <= x <= 6 and 0x0C <= y <= 0x0D: # Castle indoors
|
||||
return (2, 0x92) # Knight
|
||||
return (2, 0x7C) # Moblin
|
||||
else: # Dungeon contains hooded stalfos
|
||||
return (2, 0x9C) # Hooded stalfos
|
||||
|
||||
_CAVES_B_ROOMS = {0x2B6, 0x2B7, 0x2B8, 0x2B9, 0x285, 0x286, 0x2FD, 0x2F3, 0x2ED, 0x2EE, 0x2EA, 0x2EB, 0x2EC, 0x287, 0x2F1, 0x2F2, 0x2FE, 0x2EF, 0x2BA, 0x2BB, 0x2BC, 0x28D, 0x2F9, 0x2FA, 0x280, 0x281, 0x282, 0x283, 0x284, 0x28C, 0x288, 0x28A, 0x290, 0x291, 0x292, 0x28E, 0x29A, 0x289, 0x28B, 0x297, 0x293, 0x294, 0x295, 0x296, 0x2AB, 0x2AC, 0x298, 0x27A, 0x27B, 0x2E6, 0x2E7, 0x2BD, 0x27C, 0x27D, 0x27E, 0x2F6, 0x2F7, 0x2DE, 0x2DF}
|
||||
|
||||
# For each entity, which sprite slot is used and which value should be used.
|
||||
SPRITE_DATA = {
|
||||
0x09: (2, 0xE3), # OCTOROCK
|
||||
0x0B: _moblinSpriteData, # MOBLIN
|
||||
0x0D: (1, 0x87), # TEKTITE
|
||||
0x0E: (1, 0x81), # LEEVER
|
||||
0x0F: (2, 0x78), # ARMOS_STATUE
|
||||
0x10: (1, 0x42), # HIDING_GHINI
|
||||
0x11: (2, 0x8A), # GIANT_GHINI
|
||||
0x12: (1, 0x42), # GHINI
|
||||
0x14: _moblinSpriteData, # MOBLIN_SWORD
|
||||
0x15: (1, 0x91), # ANTI_FAIRY
|
||||
0x16: (1, {0x91, 0x65}), # SPARK_COUNTER_CLOCKWISE
|
||||
0x17: (1, {0x91, 0x65}), # SPARK_CLOCKWISE
|
||||
0x18: (3, 0x93), # POLS_VOICE
|
||||
0x19: lambda room: (2, 0x90) if room.room in _CAVES_B_ROOMS else (0, 0x90), # KEESE
|
||||
0x1A: (0, {0x90, 0x77}), # STALFOS_AGGRESSIVE
|
||||
0x1B: None, # GEL
|
||||
0x1C: (1, 0x91), # MINI_GEL
|
||||
0x1E: (0, {0x90, 0x77}), # STALFOS_EVASIVE
|
||||
0x1F: lambda room: (0, 0x77) if 0x230 <= room.room <= 0x26B else (0, 0x90, 3, 0x93), # GIBDO
|
||||
0x20: lambda room: (2, 0x90) if room.room in _CAVES_B_ROOMS else (0, 0x90), # HARDHAT_BEETLE
|
||||
0x21: (2, 0x95), # WIZROBE
|
||||
0x23: (3, 0x93), # LIKE_LIKE
|
||||
0x24: (2, 0x94, 3, 0x9F), # IRON_MASK
|
||||
0x27: (1, 0x91), # SPIKE_TRAP
|
||||
0x28: (2, 0x96), # MIMIC
|
||||
0x29: (3, 0x98), # MINI_MOLDORM
|
||||
0x2A: (3, 0x99), # LASER
|
||||
0x2C: lambda room: (2, 0x9B) if 0x15E <= room.room <= 0x17F else (3, 0x9B), # SPIKED_BEETLE
|
||||
0x2D: None, # DROPPABLE_HEART
|
||||
0x2E: None, # DROPPABLE_RUPEE
|
||||
0x2F: None, # DROPPABLE_FAIRY
|
||||
0x30: None, # KEY_DROP_POINT
|
||||
0x31: None, # SWORD
|
||||
0x35: None, # HEART_PIECE
|
||||
0x37: None, # DROPPABLE_ARROWS
|
||||
0x38: None, # DROPPABLE_BOMBS
|
||||
0x39: (2, 0x4F), # INSTRUMENT_OF_THE_SIRENS
|
||||
0x3A: (1, 0x8E), # SLEEPY_TOADSTOOL
|
||||
0x3B: None, # DROPPABLE_MAGIC_POWDER
|
||||
0x3C: None, # HIDING_SLIME_KEY
|
||||
0x3D: None, # DROPPABLE_SECRET_SEASHELL
|
||||
0x3E: lambda room: (0, 0x8D, 2, 0x8F) if room.room == 0x2A3 else (2, 0xE6), # MARIN
|
||||
0x3F: lambda room: (1, 0x8E, 3, 0x6A) if room.room == 0x2A3 else (1, 0x6C, 3, 0xC8), # RACOON
|
||||
0x40: (2, 0xA3), # WITCH
|
||||
0x41: None, # OWL_EVENT
|
||||
0x42: lambda room: (1, 0xD5) if room.room == 0x26F else (1, 0x91), # OWL_STATUE
|
||||
0x43: None, # SEASHELL_MANSION_TREES
|
||||
0x44: None, # YARNA_TALKING_BONES
|
||||
0x45: (1, 0x44), # BOULDERS
|
||||
0x46: None, # MOVING_BLOCK_LEFT_TOP
|
||||
0x47: None, # MOVING_BLOCK_LEFT_BOTTOM
|
||||
0x48: None, # MOVING_BLOCK_BOTTOM_LEFT
|
||||
0x49: None, # MOVING_BLOCK_BOTTOM_RIGHT
|
||||
0x4A: (1, 0xd5), # COLOR_DUNGEON_BOOK
|
||||
0x4C: None, # Used by Bingo board, otherwise unused.
|
||||
0x4D: (2, 0x88, 3, 0xC7), # SHOP_OWNER
|
||||
0x4F: (2, 0x84, 3, 0x89), # TRENDY_GAME_OWNER
|
||||
0x50: (2, 0x97), # BOO_BUDDY
|
||||
0x51: (3, 0x9A), # KNIGHT
|
||||
0x52: lambda room: (3, {0x7b, 0xa6}) if 0x120 <= room.room <= 0x13F else (0, {0x7b, 0xa6}), # TRACTOR_DEVICE
|
||||
0x53: lambda room: (3, {0x7b, 0xa6}) if 0x120 <= room.room <= 0x13F else (0, {0x7b, 0xa6}), # TRACTOR_DEVICE_REVERSE
|
||||
0x54: lambda room: (0, 0xA0, 1, 0xA1) if room.room == 0x2B1 else (3, 0x4e), # FISHERMAN_FISHING_GAME
|
||||
0x55: (3, 0x9d), # BOUNCING_BOMBITE
|
||||
0x56: (3, 0x9d), # TIMER_BOMBITE
|
||||
0x57: (3, 0x9e), # PAIRODD
|
||||
0x59: (2, 0xb0, 3, 0xb1), # MOLDORM
|
||||
0x5A: (0, 0x66, 2, 0xb2, 3, 0xb3), # FACADE
|
||||
0x5B: (2, 0xb4, 3, 0xb5), # SLIME_EYE
|
||||
0x5C: (2, 0xb6, 3, 0xb7), # GENIE
|
||||
0x5D: (2, 0xb8, 3, 0xb9), # SLIME_EEL
|
||||
0x5E: (2, 0xa8), # GHOMA
|
||||
0x5F: (2, 0x62, 3, 0x63), # MASTER_STALFOS
|
||||
0x60: lambda room: (3, 0xaa) if 0x230 <= room.room <= 0x26B else (2, 0xaa), # DODONGO_SNAKE
|
||||
0x61: None, # WARP
|
||||
0x62: (2, 0xba, 3, 0xbb), # HOT_HEAD
|
||||
0x63: (0, 0xbc, 1, 0xbd, 2, 0xbe, 3, 0xbf), # EVIL_EAGLE
|
||||
0x65: (0, 0xac, 1, 0xad, 2, 0xae, 3, 0xaf), # ANGLER_FISH
|
||||
0x66: (1, 0x91), # CRYSTAL_SWITCH
|
||||
0x69: (0, 0x66), # MOVING_BLOCK_MOVER
|
||||
0x6A: lambda room: (1, 0x87, 2, 0x84) if room.room >= 0x100 else (1, 0x87), # RAFT_RAFT_OWNER
|
||||
0x6C: None, # CUCCU
|
||||
0x6D: (3, 0xA4), # BOW_WOW
|
||||
0x6E: (1, {0xE5, 0xC4}), # BUTTERFLY
|
||||
0x6F: (1, 0xE5), # DOG
|
||||
0x70: (3, 0xE7), # KID_70
|
||||
0x71: (3, 0xE7), # KID_71
|
||||
0x72: (3, 0xE7), # KID_72
|
||||
0x73: (3, 0xDC), # KID_73
|
||||
0x74: (2, 0x45), # PAPAHLS_WIFE
|
||||
0x75: (2, 0x43), # GRANDMA_ULRIRA
|
||||
0x76: lambda room: (3, 0x74) if room.room == 0x2D9 else (3, 0x4b), # MR_WRITE
|
||||
0x77: (3, 0x46), # GRANDPA_ULRIRA
|
||||
0x78: (3, 0x48), # YIP_YIP
|
||||
0x79: (2, 0x47), # MADAM_MEOWMEOW
|
||||
0x7A: lambda room: (1, 0xC6) if room.room < 0x040 else (1, 0x42), # CROW
|
||||
0x7B: (2, 0x49), # CRAZY_TRACY
|
||||
0x7C: (3, 0x40), # GIANT_GOPONGA_FLOWER
|
||||
0x7E: (1, 0x4A), # GOPONGA_FLOWER
|
||||
0x7F: (3, 0x41), # TURTLE_ROCK_HEAD
|
||||
0x80: (1, 0x4C), # TELEPHONE
|
||||
0x81: lambda room: (3, 0xAB) if 0x230 <= room.room <= 0x26B else (2, 0xAB), # ROLLING_BONES (sometimes in slot 3?)
|
||||
0x82: lambda room: (3, 0xAB) if 0x230 <= room.room <= 0x26B else (2, 0xAB), # ROLLING_BONES_BAR (sometimes in slot 3?)
|
||||
0x83: (1, 0x8D), # DREAM_SHRINE_BED
|
||||
0x84: (1, 0x4D), # BIG_FAIRY
|
||||
0x85: (2, 0x4C), # MR_WRITES_BIRD
|
||||
0x86: None, # FLOATING_ITEM
|
||||
0x87: (3, 0x52), # DESERT_LANMOLA
|
||||
0x88: (3, 0x53), # ARMOS_KNIGHT
|
||||
0x89: (2, 0x54), # HINOX
|
||||
0x8A: None, # TILE_GLINT_SHOWN
|
||||
0x8B: None, # TILE_GLINT_HIDDEN
|
||||
0x8E: (2, 0x56), # CUE_BALL
|
||||
0x8F: lambda room: (2, 0x86) if room.room == 0x1F5 else (2, 0x58), # MASKED_MIMIC_GORIYA
|
||||
0x90: (3, 0x59), # THREE_OF_A_KIND
|
||||
0x91: (2, 0x55), # ANTI_KIRBY
|
||||
0x92: (2, 0x57), # SMASHER
|
||||
0x93: (3, 0x5A), # MAD_BOMBER
|
||||
0x94: (2, 0x92), # KANALET_BOMBABLE_WALL
|
||||
0x95: (1, 0x5b), # RICHARD
|
||||
0x96: (2, 0x5c), # RICHARD_FROG
|
||||
0x97: None, # DIVE_SPOT
|
||||
0x98: (2, 0x5e), # HORSE_PIECE
|
||||
0x99: (3, 0x60), # WATER_TEKTITE
|
||||
0x9A: lambda room: (0, 0x66) if 0x200 <= room.room <= 0x22F else (0, 0xa6), # FLYING_TILES
|
||||
0x9B: None, # HIDING_GEL
|
||||
0x9C: (3, 0x60), # STAR
|
||||
0x9D: (0, 0xa6), # LIFTABLE_STATUE
|
||||
0x9E: None, # FIREBALL_SHOOTER
|
||||
0x9F: (0, 0x5f), # GOOMBA
|
||||
0xA0: (0, {0x5f, 0x68}), # PEAHAT
|
||||
0xA1: (0, {0x5f, 0x7b}), # SNAKE
|
||||
0xA2: (3, 0x64), # PIRANHA_PLANT
|
||||
0xA3: (1, 0x65), # SIDE_VIEW_PLATFORM_HORIZONTAL
|
||||
0xA4: (1, 0x65), # SIDE_VIEW_PLATFORM_VERTICAL
|
||||
0xA5: (1, 0x65), # SIDE_VIEW_PLATFORM
|
||||
0xA6: (1, 0x65), # SIDE_VIEW_WEIGHTS
|
||||
0xA7: (0, 0x66), # SMASHABLE_PILLAR
|
||||
0xA9: (2, 0x5d), # BLOOPER
|
||||
0xAA: (2, 0x5d), # CHEEP_CHEEP_HORIZONTAL
|
||||
0xAB: (2, 0x5d), # CHEEP_CHEEP_VERTICAL
|
||||
0xAC: (2, 0x5d), # CHEEP_CHEEP_JUMPING
|
||||
0xAD: (3, 0x67), # KIKI_THE_MONKEY
|
||||
0xAE: (1, 0xE3), # WINGED_OCTOROCK
|
||||
0xAF: None, # TRADING_ITEM
|
||||
0xB0: (2, 0x8B), # PINCER
|
||||
0xB1: (0, 0x7b), # HOLE_FILLER (or 0x77)
|
||||
0xB2: (3, 0x8C), # BEETLE_SPAWNER
|
||||
0xB3: (3, 0x6B), # HONEYCOMB
|
||||
0xB4: (1, 0x6C), # TARIN
|
||||
0xB5: (3, 0x69), # BEAR
|
||||
0xB6: (3, 0x6D), # PAPAHL
|
||||
0xB7: (3, 0x71), # MERMAID
|
||||
0xB8: (1, 0xa1, 2, 0x75, 3, 0x4e), # FISHERMAN_UNDER_BRIDGE
|
||||
0xB9: (2, 0x79), # BUZZ_BLOB
|
||||
0xBA: (3, 0x76), # BOMBER
|
||||
0xBB: (3, 0x76), # BUSH_CRAWLER
|
||||
0xBC: (2, 0xa9), # GRIM_CREEPER
|
||||
0xBD: (2, 0x7a), # VIRE
|
||||
0xBE: (2, 0xa7), # BLAINO
|
||||
0xBF: (2, 0x82), # ZOMBIE
|
||||
0xC0: None, # MAZE_SIGNPOST
|
||||
0xC1: (2, 0x8F), # MARIN_AT_THE_SHORE
|
||||
0xC2: (1, 0x6C, 2, 0x8F), # MARIN_AT_TAL_TAL_HEIGHTS
|
||||
0xC3: (1, 0x7d, 2, 0x7e, 3, 0x7F), # MAMU_AND_FROGS
|
||||
0xC4: (2, 0x6E, 3, 0x6F), # WALRUS
|
||||
0xC5: (1, 0x81), # URCHIN
|
||||
0xC6: (1, 0x81), # SAND_CRAB
|
||||
0xC7: (0, 0xC0, 1, 0xc1, 2, 0xc2, 3, 0xc3), # MANBO_AND_FISHES
|
||||
0xCA: (3, 0xc7), # MAD_BATTER
|
||||
0xCB: (1, 0x61), # ZORA
|
||||
0xCC: (1, 0x4A), # FISH
|
||||
0xCD: lambda room: (1, 0xCC, 2, 0xCD, 3, 0xCE) if room.room == 0x2DD else (1, 0xD1, 2, 0xD2, 3, 0x6A) if room.room == 0x2FE else (3, 0xD4), # BANANAS_SCHULE_SALE
|
||||
0xCE: (3, 0x73), # MERMAID_STATUE
|
||||
0xCF: (1, 0xC9, 2, 0xCA, 3, 0xCB), # SEASHELL_MANSION
|
||||
0xD0: (1, 0xC4), # ANIMAL_D0
|
||||
0xD1: (3, 0xCF), # ANIMAL_D1
|
||||
0xD2: (3, 0xCF), # ANIMAL_D2
|
||||
0xD3: (1, 0xC4), # BUNNY_D3
|
||||
0xD6: (1, 0x65), # SIDE_VIEW_POT
|
||||
0xD7: (1, 0x65), # THWIMP
|
||||
0xD8: (2, 0xDA, 3, 0xDB), # THWOMP
|
||||
0xD9: (1, 0xD9), # THWOMP_RAMMABLE
|
||||
0xDA: (3, 0x64), # PODOBOO
|
||||
0xDB: (2, 0xDA), # GIANT_BUBBLE
|
||||
0xDC: lambda room: (0, 0xDD, 2, 0xDE) if room.room == 0x1E4 else (2, 0xD3, 3, 0xDD) if room.room == 0x29F else (3, 0xDC), # FLYING_ROOSTER_EVENTS
|
||||
0xDD: (1, 0xD5), # BOOK
|
||||
0xDE: None, # EGG_SONG_EVENT
|
||||
0xE0: (3, 0xD4), # MONKEY
|
||||
0xE1: (1, 0xDF), # WITCH_RAT
|
||||
0xE2: (3, 0xF4), # FLAME_SHOOTER
|
||||
0xE3: (3, 0x8C), # POKEY
|
||||
0xE4: (1, 0x80, 3, 0xA5), # MOBLIN_KING
|
||||
0xE5: None, # FLOATING_ITEM_2
|
||||
0xE6: (0, 0xe8, 1, 0xe9, 2, 0xea, 3, 0xeb), # FINAL_NIGHTMARE
|
||||
0xE7: None, # KANALET_CASTLE_GATE_SWITCH
|
||||
0xE9: (0, 0x04, 1, 0x05), # COLOR_SHELL_RED
|
||||
0xEA: (0, 0x04, 1, 0x05), # COLOR_SHELL_GREEN
|
||||
0xEB: (0, 0x04, 1, 0x05), # COLOR_SHELL_BLUE
|
||||
0xEC: (2, 0x06), # COLOR_GHOUL_RED
|
||||
0xED: (2, 0x06), # COLOR_GHOUL_GREEN
|
||||
0xEE: (2, 0x06), # COLOR_GHOUL_BLUE
|
||||
0xEF: (3, 0x07), # ROTOSWITCH_RED
|
||||
0xF0: (3, 0x07), # ROTOSWITCH_YELLOW
|
||||
0xF1: (3, 0x07), # ROTOSWITCH_BLUE
|
||||
0xF2: (3, 0x07), # FLYING_HOPPER_BOMBS
|
||||
0xF3: (3, 0x07), # HOPPER
|
||||
0xF4: (0, 0x08, 1, 0x09, 2, 0x0A), # AVALAUNCH
|
||||
0xF6: (0, 0x0E), # COLOR_GUARDIAN_BLUE
|
||||
0xF7: (0, 0x0E), # COLOR_GUARDIAN_BLUE
|
||||
0xF8: (0, 0x0B, 1, 0x0C, 3, 0x0D), # GIANT_BUZZ_BLOB
|
||||
0xF9: (0, 0x11, 2, 0x10), # HARDHIT_BEETLE
|
||||
0xFA: lambda room: (0, 0x44) if room.room == 0x2F5 else None, # PHOTOGRAPHER
|
||||
}
|
||||
|
||||
assert len(NAME) == COUNT
|
||||
|
||||
|
||||
class Entity:
|
||||
def __init__(self, index):
|
||||
self.index = index
|
||||
self.group = None
|
||||
self.physics_flags = None
|
||||
self.bowwow_eat_flag = None
|
||||
|
||||
|
||||
class Group:
|
||||
def __init__(self, index):
|
||||
self.index = index
|
||||
self.health = None
|
||||
self.link_damage = None
|
||||
|
||||
|
||||
class EntityData:
|
||||
def __init__(self, rom):
|
||||
groups = rom.banks[0x03][0x01F6:0x01F6+COUNT]
|
||||
group_count = max(groups) + 1
|
||||
group_damage_type = rom.banks[0x03][0x03EC:0x03EC+group_count*16]
|
||||
damage_per_damage_type = rom.banks[0x03][0x073C:0x073C+8*16]
|
||||
|
||||
self.entities = []
|
||||
self.groups = []
|
||||
for n in range(group_count):
|
||||
g = Group(n)
|
||||
g.health = rom.banks[0x03][0x07BC+n]
|
||||
g.link_damage = rom.banks[0x03][0x07F1+n]
|
||||
self.groups.append(g)
|
||||
for n in range(COUNT):
|
||||
e = Entity(n)
|
||||
e.group = self.groups[groups[n]]
|
||||
e.physics_flags = rom.banks[0x03][0x0000 + n]
|
||||
e.bowwow_eat_flag = rom.banks[0x14][0x1218+n]
|
||||
self.entities.append(e)
|
||||
|
||||
#print(sum(bowwow_eatable))
|
||||
#for n in range(COUNT):
|
||||
# if bowwow_eatable[n]:
|
||||
# print(hex(n), NAME[n])
|
||||
for n in range(group_count):
|
||||
entities = list(map(lambda data: NAME[data[0]], filter(lambda data: data[1] == n, enumerate(groups))))
|
||||
#print(hex(n), damage_to_link[n], entities)
|
||||
dmg = bytearray()
|
||||
for m in range(16):
|
||||
dmg.append(damage_per_damage_type[m*8+group_damage_type[n*16+m]])
|
||||
import binascii
|
||||
#print(binascii.hexlify(group_damage_type[n*16:n*16+16]))
|
||||
#print(binascii.hexlify(dmg))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from rom import ROM
|
||||
import sys
|
||||
rom = ROM(sys.argv[1])
|
||||
ed = EntityData(rom)
|
||||
for e in ed.entities:
|
||||
print(NAME[e.index], e.bowwow_eat_flag)
|
|
@ -0,0 +1,136 @@
|
|||
|
||||
class EntranceInfo:
|
||||
def __init__(self, room, alt_room=None, *, type=None, dungeon=None, index=None, instrument_room=None, target=None):
|
||||
if type is None and dungeon is not None:
|
||||
type = "dungeon"
|
||||
assert type is not None, "Missing entrance type"
|
||||
self.type = type
|
||||
self.room = room
|
||||
self.alt_room = alt_room
|
||||
self.dungeon = dungeon
|
||||
self.index = index
|
||||
self.instrument_room = instrument_room
|
||||
self.target = target
|
||||
|
||||
|
||||
ENTRANCE_INFO = {
|
||||
# Row0-1
|
||||
"d8": EntranceInfo(0x10, target=0x25d, dungeon=8, instrument_room=0x230),
|
||||
"phone_d8": EntranceInfo(0x11, target=0x299, type="dummy"),
|
||||
"fire_cave_exit": EntranceInfo(0x03, target=0x1ee, type="connector"),
|
||||
"fire_cave_entrance": EntranceInfo(0x13, target=0x1fe, type="connector"),
|
||||
"madbatter_taltal": EntranceInfo(0x04, target=0x1e2, type="single"),
|
||||
"left_taltal_entrance": EntranceInfo(0x15, target=0x2ea, type="connector"),
|
||||
"obstacle_cave_entrance": EntranceInfo(0x17, target=0x2b6, type="connector"),
|
||||
"left_to_right_taltalentrance": EntranceInfo(0x07, target=0x2ee, type="connector"),
|
||||
"obstacle_cave_outside_chest": EntranceInfo(0x18, target=0x2bb, type="connector", index=0),
|
||||
"obstacle_cave_exit": EntranceInfo(0x18, target=0x2bc, type="connector", index=1),
|
||||
"papahl_entrance": EntranceInfo(0x19, target=0x289, type="connector"),
|
||||
"papahl_exit": EntranceInfo(0x0A, target=0x28b, type="connector", index=0),
|
||||
"rooster_house": EntranceInfo(0x0A, target=0x29f, type="dummy", index=2),
|
||||
"bird_cave": EntranceInfo(0x0A, target=0x27e, type="single", index=1),
|
||||
"multichest_left": EntranceInfo(0x1D, target=0x2f9, type="connector", index=0),
|
||||
"multichest_right": EntranceInfo(0x1D, target=0x2fa, type="connector", index=1),
|
||||
"multichest_top": EntranceInfo(0x0D, target=0x2f2, type="connector"),
|
||||
"right_taltal_connector1": EntranceInfo(0x1E, target=0x280, type="connector", index=0),
|
||||
"right_taltal_connector2": EntranceInfo(0x1F, target=0x282, type="connector", index=0),
|
||||
"right_taltal_connector3": EntranceInfo(0x1E, target=0x283, type="connector", index=1),
|
||||
"right_taltal_connector4": EntranceInfo(0x1F, target=0x287, type="connector", index=2),
|
||||
"right_taltal_connector5": EntranceInfo(0x1F, target=0x28c, type="connector", index=1),
|
||||
"right_taltal_connector6": EntranceInfo(0x0F, target=0x28e, type="connector"),
|
||||
"right_fairy": EntranceInfo(0x1F, target=0x1fb, type="dummy", index=3),
|
||||
"d7": EntranceInfo(0x0E, "Alt0E", target=0x20e, dungeon=7, instrument_room=0x22C),
|
||||
# Row 2-3
|
||||
"writes_cave_left": EntranceInfo(0x20, target=0x2ae, type="connector"),
|
||||
"writes_cave_right": EntranceInfo(0x21, target=0x2af, type="connector"),
|
||||
"writes_house": EntranceInfo(0x30, target=0x2a8, type="trade"),
|
||||
"writes_phone": EntranceInfo(0x31, target=0x29b, type="dummy"),
|
||||
"d2": EntranceInfo(0x24, target=0x136, dungeon=2, instrument_room=0x12A),
|
||||
"moblin_cave": EntranceInfo(0x35, target=0x2f0, type="single"),
|
||||
"photo_house": EntranceInfo(0x37, target=0x2b5, type="dummy"),
|
||||
"mambo": EntranceInfo(0x2A, target=0x2fd, type="single"),
|
||||
"d4": EntranceInfo(0x2B, "Alt2B", target=0x17a, dungeon=4, index=0, instrument_room=0x162),
|
||||
# TODO
|
||||
# "d4_connector": EntranceInfo(0x2B, "Alt2B", index=1),
|
||||
# "d4_connector_exit": EntranceInfo(0x2D),
|
||||
"heartpiece_swim_cave": EntranceInfo(0x2E, target=0x1f2, type="single"),
|
||||
"raft_return_exit": EntranceInfo(0x2F, target=0x1e7, type="connector"),
|
||||
"raft_house": EntranceInfo(0x3F, target=0x2b0, type="insanity"),
|
||||
"raft_return_enter": EntranceInfo(0x8F, target=0x1f7, type="connector"),
|
||||
# Forest and everything right of it
|
||||
"hookshot_cave": EntranceInfo(0x42, target=0x2b3, type="single"),
|
||||
"toadstool_exit": EntranceInfo(0x50, target=0x2ab, type="connector"),
|
||||
"forest_madbatter": EntranceInfo(0x52, target=0x1e1, type="single"),
|
||||
"toadstool_entrance": EntranceInfo(0x62, target=0x2bd, type="connector"),
|
||||
"crazy_tracy": EntranceInfo(0x45, target=0x2ad, type="dummy"),
|
||||
"witch": EntranceInfo(0x65, target=0x2a2, type="single"),
|
||||
"graveyard_cave_left": EntranceInfo(0x75, target=0x2de, type="connector"),
|
||||
"graveyard_cave_right": EntranceInfo(0x76, target=0x2df, type="connector"),
|
||||
"d0": EntranceInfo(0x77, target=0x312, dungeon=9, index="all", instrument_room=0x301),
|
||||
# Castle
|
||||
"castle_jump_cave": EntranceInfo(0x78, target=0x1fd, type="single"),
|
||||
"castle_main_entrance": EntranceInfo(0x69, target=0x2d3, type="connector"),
|
||||
"castle_upper_left": EntranceInfo(0x59, target=0x2d5, type="connector", index=0),
|
||||
"castle_upper_right": EntranceInfo(0x59, target=0x2d6, type="single", index=1),
|
||||
"castle_secret_exit": EntranceInfo(0x49, target=0x1eb, type="connector"),
|
||||
"castle_secret_entrance": EntranceInfo(0x4A, target=0x1ec, type="connector"),
|
||||
"castle_phone": EntranceInfo(0x4B, target=0x2cc, type="dummy"),
|
||||
# Mabe village
|
||||
"papahl_house_left": EntranceInfo(0x82, target=0x2a5, type="connector", index=0),
|
||||
"papahl_house_right": EntranceInfo(0x82, target=0x2a6, type="connector", index=1),
|
||||
"dream_hut": EntranceInfo(0x83, target=0x2aa, type="single"),
|
||||
"rooster_grave": EntranceInfo(0x92, target=0x1f4, type="single"),
|
||||
"shop": EntranceInfo(0x93, target=0x2a1, type="single"),
|
||||
"madambowwow": EntranceInfo(0xA1, target=0x2a7, type="dummy", index=1),
|
||||
"kennel": EntranceInfo(0xA1, target=0x2b2, type="single", index=0),
|
||||
"start_house": EntranceInfo(0xA2, target=0x2a3, type="start"),
|
||||
"library": EntranceInfo(0xB0, target=0x1fa, type="dummy"),
|
||||
"ulrira": EntranceInfo(0xB1, target=0x2a9, type="dummy"),
|
||||
"mabe_phone": EntranceInfo(0xB2, target=0x2cb, type="dummy"),
|
||||
"trendy_shop": EntranceInfo(0xB3, target=0x2a0, type="trade"),
|
||||
# Ukuku Prairie
|
||||
"prairie_left_phone": EntranceInfo(0xA4, target=0x2b4, type="dummy"),
|
||||
"prairie_left_cave1": EntranceInfo(0x84, target=0x2cd, type="single"),
|
||||
"prairie_left_cave2": EntranceInfo(0x86, target=0x2f4, type="single"),
|
||||
"prairie_left_fairy": EntranceInfo(0x87, target=0x1f3, type="dummy"),
|
||||
"mamu": EntranceInfo(0xD4, target=0x2fb, type="insanity"),
|
||||
"d3": EntranceInfo(0xB5, target=0x152, dungeon=3, instrument_room=0x159),
|
||||
"prairie_right_phone": EntranceInfo(0x88, target=0x29c, type="dummy"),
|
||||
"seashell_mansion": EntranceInfo(0x8A, target=0x2e9, type="single"),
|
||||
"prairie_right_cave_top": EntranceInfo(0xB8, target=0x292, type="connector", index=1),
|
||||
"prairie_right_cave_bottom": EntranceInfo(0xC8, target=0x293, type="connector"),
|
||||
"prairie_right_cave_high": EntranceInfo(0xB8, target=0x295, type="connector", index=0),
|
||||
"prairie_to_animal_connector": EntranceInfo(0xAA, target=0x2d0, type="connector"),
|
||||
"animal_to_prairie_connector": EntranceInfo(0xAB, target=0x2d1, type="connector"),
|
||||
|
||||
"d6": EntranceInfo(0x8C, "Alt8C", target=0x1d4, dungeon=6, instrument_room=0x1B5),
|
||||
"d6_connector_exit": EntranceInfo(0x9C, target=0x1f0, type="connector"),
|
||||
"d6_connector_entrance": EntranceInfo(0x9D, target=0x1f1, type="connector"),
|
||||
"armos_fairy": EntranceInfo(0x8D, target=0x1ac, type="dummy"),
|
||||
"armos_maze_cave": EntranceInfo(0xAE, target=0x2fc, type="single"),
|
||||
"armos_temple": EntranceInfo(0xAC, target=0x28f, type="single"),
|
||||
# Beach area
|
||||
"d1": EntranceInfo(0xD3, target=0x117, dungeon=1, instrument_room=0x102),
|
||||
"boomerang_cave": EntranceInfo(0xF4, target=0x1f5, type="single", instrument_room="Alt1F5"), # instrument_room is to configure the exit on the alt room layout
|
||||
"banana_seller": EntranceInfo(0xE3, target=0x2fe, type="trade"),
|
||||
"ghost_house": EntranceInfo(0xF6, target=0x1e3, type="single"),
|
||||
|
||||
# Lower prairie
|
||||
"richard_house": EntranceInfo(0xD6, target=0x2c7, type="connector"),
|
||||
"richard_maze": EntranceInfo(0xC6, target=0x2c9, type="connector"),
|
||||
"prairie_low_phone": EntranceInfo(0xE8, target=0x29d, type="dummy"),
|
||||
"prairie_madbatter_connector_entrance": EntranceInfo(0xF9, target=0x1f6, type="connector"),
|
||||
"prairie_madbatter_connector_exit": EntranceInfo(0xE7, target=0x1e5, type="connector"),
|
||||
"prairie_madbatter": EntranceInfo(0xE6, target=0x1e0, type="single"),
|
||||
|
||||
"d5": EntranceInfo(0xD9, target=0x1a1, dungeon=5, instrument_room=0x182),
|
||||
# Animal village
|
||||
"animal_phone": EntranceInfo(0xDB, target=0x2e3, type="dummy"),
|
||||
"animal_house1": EntranceInfo(0xCC, target=0x2db, type="dummy", index=0),
|
||||
"animal_house2": EntranceInfo(0xCC, target=0x2dd, type="dummy", index=1),
|
||||
"animal_house3": EntranceInfo(0xCD, target=0x2d9, type="trade", index=1),
|
||||
"animal_house4": EntranceInfo(0xCD, target=0x2da, type="dummy", index=2),
|
||||
"animal_house5": EntranceInfo(0xDD, target=0x2d7, type="trade"),
|
||||
"animal_cave": EntranceInfo(0xCD, target=0x2f7, type="single", index=0),
|
||||
"desert_cave": EntranceInfo(0xCF, target=0x1f9, type="single"),
|
||||
}
|
|
@ -0,0 +1,427 @@
|
|||
import binascii
|
||||
import importlib.util
|
||||
import importlib.machinery
|
||||
import os
|
||||
|
||||
from .romTables import ROMWithTables
|
||||
from . import assembler
|
||||
from . import mapgen
|
||||
from . import patches
|
||||
from .patches import overworld as _
|
||||
from .patches import dungeon as _
|
||||
from .patches import entrances as _
|
||||
from .patches import enemies as _
|
||||
from .patches import titleScreen as _
|
||||
from .patches import aesthetics as _
|
||||
from .patches import music as _
|
||||
from .patches import core as _
|
||||
from .patches import phone as _
|
||||
from .patches import photographer as _
|
||||
from .patches import owl as _
|
||||
from .patches import bank3e as _
|
||||
from .patches import bank3f as _
|
||||
from .patches import inventory as _
|
||||
from .patches import witch as _
|
||||
from .patches import tarin as _
|
||||
from .patches import fishingMinigame as _
|
||||
from .patches import softlock as _
|
||||
from .patches import maptweaks as _
|
||||
from .patches import chest as _
|
||||
from .patches import bomb as _
|
||||
from .patches import rooster as _
|
||||
from .patches import shop as _
|
||||
from .patches import trendy as _
|
||||
from .patches import goal as _
|
||||
from .patches import hardMode as _
|
||||
from .patches import weapons as _
|
||||
from .patches import health as _
|
||||
from .patches import heartPiece as _
|
||||
from .patches import droppedKey as _
|
||||
from .patches import goldenLeaf as _
|
||||
from .patches import songs as _
|
||||
from .patches import bowwow as _
|
||||
from .patches import desert as _
|
||||
from .patches import reduceRNG as _
|
||||
from .patches import madBatter as _
|
||||
from .patches import tunicFairy as _
|
||||
from .patches import seashell as _
|
||||
from .patches import instrument as _
|
||||
from .patches import endscreen as _
|
||||
from .patches import save as _
|
||||
from .patches import bingo as _
|
||||
from .patches import multiworld as _
|
||||
from .patches import tradeSequence as _
|
||||
from . import hints
|
||||
|
||||
from .locations.keyLocation import KeyLocation
|
||||
from .patches import bank34
|
||||
|
||||
from ..Options import TrendyGame, Palette
|
||||
|
||||
|
||||
# Function to generate a final rom, this patches the rom with all required patches
|
||||
def generateRom(args, settings, ap_settings, seed, logic, rnd=None, multiworld=None, player_name=None, player_names=[], player_id = 0):
|
||||
rom = ROMWithTables(args.input_filename)
|
||||
rom.player_names = player_names
|
||||
pymods = []
|
||||
if args.pymod:
|
||||
for pymod in args.pymod:
|
||||
spec = importlib.util.spec_from_loader(pymod, importlib.machinery.SourceFileLoader(pymod, pymod))
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
pymods.append(module)
|
||||
for pymod in pymods:
|
||||
pymod.prePatch(rom)
|
||||
|
||||
if settings.gfxmod:
|
||||
patches.aesthetics.gfxMod(rom, os.path.join("data", "sprites", "ladx", settings.gfxmod))
|
||||
|
||||
item_list = [item for item in logic.iteminfo_list if not isinstance(item, KeyLocation)]
|
||||
|
||||
assembler.resetConsts()
|
||||
assembler.const("INV_SIZE", 16)
|
||||
assembler.const("wHasFlippers", 0xDB3E)
|
||||
assembler.const("wHasMedicine", 0xDB3F)
|
||||
assembler.const("wTradeSequenceItem", 0xDB40) # we use it to store flags of which trade items we have
|
||||
assembler.const("wTradeSequenceItem2", 0xDB7F) # Normally used to store that we have exchanged the trade item, we use it to store flags of which trade items we have
|
||||
assembler.const("wSeashellsCount", 0xDB41)
|
||||
assembler.const("wGoldenLeaves", 0xDB42) # New memory location where to store the golden leaf counter
|
||||
assembler.const("wCollectedTunics", 0xDB6D) # Memory location where to store which tunic options are available
|
||||
assembler.const("wCustomMessage", 0xC0A0)
|
||||
|
||||
# We store the link info in unused color dungeon flags, so it gets preserved in the savegame.
|
||||
assembler.const("wLinkSyncSequenceNumber", 0xDDF6)
|
||||
assembler.const("wLinkStatusBits", 0xDDF7)
|
||||
assembler.const("wLinkGiveItem", 0xDDF8)
|
||||
assembler.const("wLinkGiveItemFrom", 0xDDF9)
|
||||
assembler.const("wLinkSendItemRoomHigh", 0xDDFA)
|
||||
assembler.const("wLinkSendItemRoomLow", 0xDDFB)
|
||||
assembler.const("wLinkSendItemTarget", 0xDDFC)
|
||||
assembler.const("wLinkSendItemItem", 0xDDFD)
|
||||
|
||||
assembler.const("wZolSpawnCount", 0xDE10)
|
||||
assembler.const("wCuccoSpawnCount", 0xDE11)
|
||||
assembler.const("wDropBombSpawnCount", 0xDE12)
|
||||
assembler.const("wLinkSpawnDelay", 0xDE13)
|
||||
|
||||
#assembler.const("HARDWARE_LINK", 1)
|
||||
assembler.const("HARD_MODE", 1 if settings.hardmode != "none" else 0)
|
||||
|
||||
patches.core.cleanup(rom)
|
||||
patches.save.singleSaveSlot(rom)
|
||||
patches.phone.patchPhone(rom)
|
||||
patches.photographer.fixPhotographer(rom)
|
||||
patches.core.bugfixWrittingWrongRoomStatus(rom)
|
||||
patches.core.bugfixBossroomTopPush(rom)
|
||||
patches.core.bugfixPowderBagSprite(rom)
|
||||
patches.core.fixEggDeathClearingItems(rom)
|
||||
patches.core.disablePhotoPrint(rom)
|
||||
patches.core.easyColorDungeonAccess(rom)
|
||||
patches.owl.removeOwlEvents(rom)
|
||||
patches.enemies.fixArmosKnightAsMiniboss(rom)
|
||||
patches.bank3e.addBank3E(rom, seed, player_id, player_names)
|
||||
patches.bank3f.addBank3F(rom)
|
||||
patches.bank34.addBank34(rom, item_list)
|
||||
patches.core.removeGhost(rom)
|
||||
patches.core.fixMarinFollower(rom)
|
||||
patches.core.fixWrongWarp(rom)
|
||||
patches.core.alwaysAllowSecretBook(rom)
|
||||
patches.core.injectMainLoop(rom)
|
||||
|
||||
from ..Options import ShuffleSmallKeys, ShuffleNightmareKeys
|
||||
|
||||
if ap_settings["shuffle_small_keys"] != ShuffleSmallKeys.option_original_dungeon or ap_settings["shuffle_nightmare_keys"] != ShuffleNightmareKeys.option_original_dungeon:
|
||||
patches.inventory.advancedInventorySubscreen(rom)
|
||||
patches.inventory.moreSlots(rom)
|
||||
if settings.witch:
|
||||
patches.witch.updateWitch(rom)
|
||||
patches.softlock.fixAll(rom)
|
||||
patches.maptweaks.tweakMap(rom)
|
||||
patches.chest.fixChests(rom)
|
||||
patches.shop.fixShop(rom)
|
||||
patches.rooster.patchRooster(rom)
|
||||
patches.trendy.fixTrendy(rom)
|
||||
patches.droppedKey.fixDroppedKey(rom)
|
||||
patches.madBatter.upgradeMadBatter(rom)
|
||||
patches.tunicFairy.upgradeTunicFairy(rom)
|
||||
patches.tarin.updateTarin(rom)
|
||||
patches.fishingMinigame.updateFinishingMinigame(rom)
|
||||
patches.health.upgradeHealthContainers(rom)
|
||||
if settings.owlstatues in ("dungeon", "both"):
|
||||
patches.owl.upgradeDungeonOwlStatues(rom)
|
||||
if settings.owlstatues in ("overworld", "both"):
|
||||
patches.owl.upgradeOverworldOwlStatues(rom)
|
||||
patches.goldenLeaf.fixGoldenLeaf(rom)
|
||||
patches.heartPiece.fixHeartPiece(rom)
|
||||
patches.seashell.fixSeashell(rom)
|
||||
patches.instrument.fixInstruments(rom)
|
||||
patches.seashell.upgradeMansion(rom)
|
||||
patches.songs.upgradeMarin(rom)
|
||||
patches.songs.upgradeManbo(rom)
|
||||
patches.songs.upgradeMamu(rom)
|
||||
if settings.tradequest:
|
||||
patches.tradeSequence.patchTradeSequence(rom, settings.boomerang)
|
||||
else:
|
||||
# Monkey bridge patch, always have the bridge there.
|
||||
rom.patch(0x00, 0x333D, assembler.ASM("bit 4, e\njr Z, $05"), b"", fill_nop=True)
|
||||
patches.bowwow.fixBowwow(rom, everywhere=settings.bowwow != 'normal')
|
||||
if settings.bowwow != 'normal':
|
||||
patches.bowwow.bowwowMapPatches(rom)
|
||||
patches.desert.desertAccess(rom)
|
||||
if settings.overworld == 'dungeondive':
|
||||
patches.overworld.patchOverworldTilesets(rom)
|
||||
patches.overworld.createDungeonOnlyOverworld(rom)
|
||||
elif settings.overworld == 'nodungeons':
|
||||
patches.dungeon.patchNoDungeons(rom)
|
||||
elif settings.overworld == 'random':
|
||||
patches.overworld.patchOverworldTilesets(rom)
|
||||
mapgen.store_map(rom, logic.world.map)
|
||||
#if settings.dungeon_items == 'keysy':
|
||||
# patches.dungeon.removeKeyDoors(rom)
|
||||
# patches.reduceRNG.slowdownThreeOfAKind(rom)
|
||||
patches.reduceRNG.fixHorseHeads(rom)
|
||||
patches.bomb.onlyDropBombsWhenHaveBombs(rom)
|
||||
# patches.aesthetics.noSwordMusic(rom)
|
||||
patches.aesthetics.reduceMessageLengths(rom, rnd)
|
||||
patches.aesthetics.allowColorDungeonSpritesEverywhere(rom)
|
||||
if settings.music == 'random':
|
||||
patches.music.randomizeMusic(rom, rnd)
|
||||
elif settings.music == 'off':
|
||||
patches.music.noMusic(rom)
|
||||
if settings.noflash:
|
||||
patches.aesthetics.removeFlashingLights(rom)
|
||||
if settings.hardmode == "oracle":
|
||||
patches.hardMode.oracleMode(rom)
|
||||
elif settings.hardmode == "hero":
|
||||
patches.hardMode.heroMode(rom)
|
||||
elif settings.hardmode == "ohko":
|
||||
patches.hardMode.oneHitKO(rom)
|
||||
if settings.superweapons:
|
||||
patches.weapons.patchSuperWeapons(rom)
|
||||
if settings.textmode == 'fast':
|
||||
patches.aesthetics.fastText(rom)
|
||||
if settings.textmode == 'none':
|
||||
patches.aesthetics.fastText(rom)
|
||||
patches.aesthetics.noText(rom)
|
||||
if not settings.nagmessages:
|
||||
patches.aesthetics.removeNagMessages(rom)
|
||||
if settings.lowhpbeep == 'slow':
|
||||
patches.aesthetics.slowLowHPBeep(rom)
|
||||
if settings.lowhpbeep == 'none':
|
||||
patches.aesthetics.removeLowHPBeep(rom)
|
||||
if 0 <= int(settings.linkspalette):
|
||||
patches.aesthetics.forceLinksPalette(rom, int(settings.linkspalette))
|
||||
if args.romdebugmode:
|
||||
# The default rom has this build in, just need to set a flag and we get this save.
|
||||
rom.patch(0, 0x0003, "00", "01")
|
||||
|
||||
# Patch the sword check on the shopkeeper turning around.
|
||||
if settings.steal == 'never':
|
||||
rom.patch(4, 0x36F9, "FA4EDB", "3E0000")
|
||||
elif settings.steal == 'always':
|
||||
rom.patch(4, 0x36F9, "FA4EDB", "3E0100")
|
||||
|
||||
if settings.hpmode == 'inverted':
|
||||
patches.health.setStartHealth(rom, 9)
|
||||
elif settings.hpmode == '1':
|
||||
patches.health.setStartHealth(rom, 1)
|
||||
|
||||
patches.inventory.songSelectAfterOcarinaSelect(rom)
|
||||
if settings.quickswap == 'a':
|
||||
patches.core.quickswap(rom, 1)
|
||||
elif settings.quickswap == 'b':
|
||||
patches.core.quickswap(rom, 0)
|
||||
|
||||
# TODO: hints bad
|
||||
|
||||
world_setup = logic.world_setup
|
||||
|
||||
|
||||
hints.addHints(rom, rnd, item_list)
|
||||
|
||||
if world_setup.goal == "raft":
|
||||
patches.goal.setRaftGoal(rom)
|
||||
elif world_setup.goal in ("bingo", "bingo-full"):
|
||||
patches.bingo.setBingoGoal(rom, world_setup.bingo_goals, world_setup.goal)
|
||||
elif world_setup.goal == "seashells":
|
||||
patches.goal.setSeashellGoal(rom, 20)
|
||||
else:
|
||||
patches.goal.setRequiredInstrumentCount(rom, world_setup.goal)
|
||||
|
||||
# Patch the generated logic into the rom
|
||||
patches.chest.setMultiChest(rom, world_setup.multichest)
|
||||
if settings.overworld not in {"dungeondive", "random"}:
|
||||
patches.entrances.changeEntrances(rom, world_setup.entrance_mapping)
|
||||
for spot in item_list:
|
||||
if spot.item and spot.item.startswith("*"):
|
||||
spot.item = spot.item[1:]
|
||||
mw = None
|
||||
if spot.item_owner != spot.location_owner:
|
||||
mw = spot.item_owner
|
||||
if mw > 255:
|
||||
# Don't torture the game with higher slot numbers
|
||||
mw = 255
|
||||
spot.patch(rom, spot.item, multiworld=mw)
|
||||
patches.enemies.changeBosses(rom, world_setup.boss_mapping)
|
||||
patches.enemies.changeMiniBosses(rom, world_setup.miniboss_mapping)
|
||||
|
||||
if not args.romdebugmode:
|
||||
patches.core.addFrameCounter(rom, len(item_list))
|
||||
|
||||
patches.core.warpHome(rom) # Needs to be done after setting the start location.
|
||||
patches.titleScreen.setRomInfo(rom, binascii.hexlify(seed).decode("ascii").upper(), settings, player_name, player_id)
|
||||
patches.endscreen.updateEndScreen(rom)
|
||||
patches.aesthetics.updateSpriteData(rom)
|
||||
if args.doubletrouble:
|
||||
patches.enemies.doubleTrouble(rom)
|
||||
|
||||
if ap_settings["trendy_game"] != TrendyGame.option_normal:
|
||||
|
||||
# TODO: if 0 or 4, 5, remove inaccurate conveyor tiles
|
||||
|
||||
from .roomEditor import RoomEditor, Object
|
||||
room_editor = RoomEditor(rom, 0x2A0)
|
||||
|
||||
if ap_settings["trendy_game"] == TrendyGame.option_easy:
|
||||
# Set physics flag on all objects
|
||||
for i in range(0, 6):
|
||||
rom.banks[0x4][0x6F1E + i -0x4000] = 0x4
|
||||
else:
|
||||
# All levels
|
||||
# Set physics flag on yoshi
|
||||
rom.banks[0x4][0x6F21-0x4000] = 0x3
|
||||
# Add new conveyor to "push" yoshi (it's only a visual)
|
||||
room_editor.objects.append(Object(5, 3, 0xD0))
|
||||
|
||||
if int(ap_settings["trendy_game"]) >= TrendyGame.option_harder:
|
||||
"""
|
||||
Data_004_76A0::
|
||||
db $FC, $00, $04, $00, $00
|
||||
|
||||
Data_004_76A5::
|
||||
db $00, $04, $00, $FC, $00
|
||||
"""
|
||||
speeds = {
|
||||
TrendyGame.option_harder: (3, 8),
|
||||
TrendyGame.option_hardest: (3, 8),
|
||||
TrendyGame.option_impossible: (3, 16),
|
||||
}
|
||||
def speed():
|
||||
return rnd.randint(*speeds[ap_settings["trendy_game"]])
|
||||
rom.banks[0x4][0x76A0-0x4000] = 0xFF - speed()
|
||||
rom.banks[0x4][0x76A2-0x4000] = speed()
|
||||
rom.banks[0x4][0x76A6-0x4000] = speed()
|
||||
rom.banks[0x4][0x76A8-0x4000] = 0xFF - speed()
|
||||
if int(ap_settings["trendy_game"]) >= TrendyGame.option_hardest:
|
||||
rom.banks[0x4][0x76A1-0x4000] = 0xFF - speed()
|
||||
rom.banks[0x4][0x76A3-0x4000] = speed()
|
||||
rom.banks[0x4][0x76A5-0x4000] = speed()
|
||||
rom.banks[0x4][0x76A7-0x4000] = 0xFF - speed()
|
||||
|
||||
room_editor.store(rom)
|
||||
# This doesn't work, you can set random conveyors, but they aren't used
|
||||
# for x in range(3, 9):
|
||||
# for y in range(1, 5):
|
||||
# room_editor.objects.append(Object(x, y, 0xCF + rnd.randint(0, 3)))
|
||||
|
||||
# Attempt at imitating gb palette, fails
|
||||
if False:
|
||||
gb_colors = [
|
||||
[0x0f, 0x38, 0x0f],
|
||||
[0x30, 0x62, 0x30],
|
||||
[0x8b, 0xac, 0x0f],
|
||||
[0x9b, 0xbc, 0x0f],
|
||||
]
|
||||
for color in gb_colors:
|
||||
for channel in range(3):
|
||||
color[channel] = color[channel] * 31 // 0xbc
|
||||
|
||||
|
||||
palette = ap_settings["palette"]
|
||||
if palette != Palette.option_normal:
|
||||
ranges = {
|
||||
# Object palettes
|
||||
# Overworld palettes
|
||||
# Dungeon palettes
|
||||
# Interior palettes
|
||||
"code/palettes.asm 1": (0x21, 0x1518, 0x34A0),
|
||||
# Intro/outro(?)
|
||||
# File select
|
||||
# S+Q
|
||||
# Map
|
||||
"code/palettes.asm 2": (0x21, 0x3536, 0x3FFE),
|
||||
# Used for transitioning in and out of forest
|
||||
"backgrounds/palettes.asm": (0x24, 0x3478, 0x3578),
|
||||
# Haven't yet found menu palette
|
||||
}
|
||||
|
||||
for name, (bank, start, end) in ranges.items():
|
||||
def clamp(x, min, max):
|
||||
if x < min:
|
||||
return min
|
||||
if x > max:
|
||||
return max
|
||||
return x
|
||||
def bin_to_rgb(word):
|
||||
red = word & 0b11111
|
||||
word >>= 5
|
||||
green = word & 0b11111
|
||||
word >>= 5
|
||||
blue = word & 0b11111
|
||||
return (red, green, blue)
|
||||
def rgb_to_bin(r, g, b):
|
||||
return (b << 10) | (g << 5) | r
|
||||
|
||||
for address in range(start, end, 2):
|
||||
packed = (rom.banks[bank][address + 1] << 8) | rom.banks[bank][address]
|
||||
r,g,b = bin_to_rgb(packed)
|
||||
|
||||
# 1 bit
|
||||
if palette == Palette.option_1bit:
|
||||
r &= 0b10000
|
||||
g &= 0b10000
|
||||
b &= 0b10000
|
||||
# 2 bit
|
||||
elif palette == Palette.option_1bit:
|
||||
r &= 0b11000
|
||||
g &= 0b11000
|
||||
b &= 0b11000
|
||||
# Invert
|
||||
elif palette == Palette.option_inverted:
|
||||
r = 31 - r
|
||||
g = 31 - g
|
||||
b = 31 - b
|
||||
# Pink
|
||||
elif palette == Palette.option_pink:
|
||||
r = r // 2
|
||||
r += 16
|
||||
r = int(r)
|
||||
r = clamp(r, 0, 0x1F)
|
||||
b = b // 2
|
||||
b += 16
|
||||
b = int(b)
|
||||
b = clamp(b, 0, 0x1F)
|
||||
elif palette == Palette.option_greyscale:
|
||||
# gray=int(0.299*r+0.587*g+0.114*b)
|
||||
gray = (r + g + b) // 3
|
||||
r = g = b = gray
|
||||
|
||||
packed = rgb_to_bin(r, g, b)
|
||||
rom.banks[bank][address] = packed & 0xFF
|
||||
rom.banks[bank][address + 1] = packed >> 8
|
||||
|
||||
SEED_LOCATION = 0x0134
|
||||
SEED_SIZE = 10
|
||||
|
||||
# TODO: pass this in
|
||||
# Patch over the title
|
||||
assert(len(seed) == SEED_SIZE)
|
||||
gameid = seed + player_id.to_bytes(2, 'big')
|
||||
rom.patch(0x00, SEED_LOCATION, None, binascii.hexlify(gameid))
|
||||
|
||||
|
||||
for pymod in pymods:
|
||||
pymod.postPatch(rom)
|
||||
|
||||
|
||||
return rom
|
|
@ -0,0 +1,41 @@
|
|||
import requests
|
||||
import PIL.Image
|
||||
import re
|
||||
|
||||
url = "https://raw.githubusercontent.com/CrystalSaver/Z4RandomizerBeta2/master/"
|
||||
|
||||
for k, v in requests.get(url + "asset-manifest.json").json()['files'].items():
|
||||
m = re.match("static/media/Graphics(.+)\\.bin", k)
|
||||
assert m is not None
|
||||
if not k.startswith("static/media/Graphics") or not k.endswith(".bin"):
|
||||
continue
|
||||
name = m.group(1)
|
||||
|
||||
data = requests.get(url + v).content
|
||||
|
||||
icon = PIL.Image.new("P", (16, 16))
|
||||
buffer = bytearray(b'\x00' * 16 * 8)
|
||||
for idx in range(0x0C0, 0x0C2):
|
||||
for y in range(16):
|
||||
a = data[idx * 32 + y * 2]
|
||||
b = data[idx * 32 + y * 2 + 1]
|
||||
for x in range(8):
|
||||
v = 0
|
||||
if a & (0x80 >> x):
|
||||
v |= 1
|
||||
if b & (0x80 >> x):
|
||||
v |= 2
|
||||
buffer[x+y*8] = v
|
||||
tile = PIL.Image.frombytes('P', (8, 16), bytes(buffer))
|
||||
x = (idx % 16) * 8
|
||||
icon.paste(tile, (x, 0))
|
||||
pal = icon.getpalette()
|
||||
assert pal is not None
|
||||
pal[0:3] = [150, 150, 255]
|
||||
pal[3:6] = [0, 0, 0]
|
||||
pal[6:9] = [59, 180, 112]
|
||||
pal[9:12] = [251, 221, 197]
|
||||
icon.putpalette(pal)
|
||||
icon = icon.resize((32, 32))
|
||||
icon.save("gfx/%s.bin.png" % (name))
|
||||
open("gfx/%s.bin" % (name), "wb").write(data)
|
|
@ -0,0 +1,66 @@
|
|||
from .locations.items import *
|
||||
from .utils import formatText
|
||||
|
||||
|
||||
hint_text_ids = [
|
||||
# Overworld owl statues
|
||||
0x1B6, 0x1B7, 0x1B8, 0x1B9, 0x1BA, 0x1BB, 0x1BC, 0x1BD, 0x1BE, 0x22D,
|
||||
|
||||
0x288, 0x280, # D1
|
||||
0x28A, 0x289, 0x281, # D2
|
||||
0x282, 0x28C, 0x28B, # D3
|
||||
0x283, # D4
|
||||
0x28D, 0x284, # D5
|
||||
0x285, 0x28F, 0x28E, # D6
|
||||
0x291, 0x290, 0x286, # D7
|
||||
0x293, 0x287, 0x292, # D8
|
||||
0x263, # D0
|
||||
|
||||
# Hint books
|
||||
0x267, # color dungeon
|
||||
0x201, # Pre open: 0x200
|
||||
0x203, # Pre open: 0x202
|
||||
0x205, # Pre open: 0x204
|
||||
0x207, # Pre open: 0x206
|
||||
0x209, # Pre open: 0x208
|
||||
0x20B, # Pre open: 0x20A
|
||||
]
|
||||
|
||||
hint_items = (POWER_BRACELET, SHIELD, BOW, HOOKSHOT, MAGIC_ROD, PEGASUS_BOOTS, OCARINA, FEATHER, SHOVEL,
|
||||
MAGIC_POWDER, SWORD, FLIPPERS, TAIL_KEY, ANGLER_KEY, FACE_KEY,
|
||||
BIRD_KEY, SLIME_KEY, GOLD_LEAF, BOOMERANG, BOWWOW)
|
||||
|
||||
hints = [
|
||||
"{0} is at {1}",
|
||||
"If you want {0} start looking in {1}",
|
||||
"{1} holds {0}",
|
||||
"They say that {0} is at {1}",
|
||||
"You might want to look in {1} for a secret",
|
||||
]
|
||||
useless_hint = [
|
||||
("Egg", "Mt. Tamaranch"),
|
||||
("Marin", "Mabe Village"),
|
||||
("Marin", "Mabe Village"),
|
||||
("Witch", "Koholint Prairie"),
|
||||
("Mermaid", "Martha's Bay"),
|
||||
("Nothing", "Tabahl Wasteland"),
|
||||
("Animals", "Animal Village"),
|
||||
("Sand", "Yarna Desert"),
|
||||
]
|
||||
|
||||
|
||||
def addHints(rom, rnd, spots):
|
||||
spots = list(sorted(filter(lambda spot: spot.item in hint_items, spots), key=lambda spot: spot.nameId))
|
||||
text_ids = hint_text_ids.copy()
|
||||
rnd.shuffle(text_ids)
|
||||
for text_id in text_ids:
|
||||
if len(spots) > 0:
|
||||
spot_index = rnd.randint(0, len(spots) - 1)
|
||||
spot = spots.pop(spot_index)
|
||||
hint = rnd.choice(hints).format("{%s}" % (spot.item), spot.metadata.area)
|
||||
else:
|
||||
hint = rnd.choice(hints).format(*rnd.choice(useless_hint))
|
||||
rom.texts[text_id] = formatText(hint)
|
||||
|
||||
for text_id in range(0x200, 0x20C, 2):
|
||||
rom.texts[text_id] = formatText("Read this book?", ask="YES NO")
|
|
@ -0,0 +1,278 @@
|
|||
from .locations.items import *
|
||||
|
||||
|
||||
DEFAULT_ITEM_POOL = {
|
||||
SWORD: 2,
|
||||
FEATHER: 1,
|
||||
HOOKSHOT: 1,
|
||||
BOW: 1,
|
||||
BOMB: 1,
|
||||
MAGIC_POWDER: 1,
|
||||
MAGIC_ROD: 1,
|
||||
OCARINA: 1,
|
||||
PEGASUS_BOOTS: 1,
|
||||
POWER_BRACELET: 2,
|
||||
SHIELD: 2,
|
||||
SHOVEL: 1,
|
||||
ROOSTER: 1,
|
||||
TOADSTOOL: 1,
|
||||
|
||||
TAIL_KEY: 1, SLIME_KEY: 1, ANGLER_KEY: 1, FACE_KEY: 1, BIRD_KEY: 1,
|
||||
GOLD_LEAF: 5,
|
||||
|
||||
FLIPPERS: 1,
|
||||
BOWWOW: 1,
|
||||
SONG1: 1, SONG2: 1, SONG3: 1,
|
||||
|
||||
BLUE_TUNIC: 1, RED_TUNIC: 1,
|
||||
MAX_ARROWS_UPGRADE: 1, MAX_BOMBS_UPGRADE: 1, MAX_POWDER_UPGRADE: 1,
|
||||
|
||||
HEART_CONTAINER: 8,
|
||||
HEART_PIECE: 12,
|
||||
|
||||
RUPEES_100: 3,
|
||||
RUPEES_20: 6,
|
||||
RUPEES_200: 3,
|
||||
RUPEES_50: 19,
|
||||
|
||||
SEASHELL: 24,
|
||||
MEDICINE: 3,
|
||||
GEL: 4,
|
||||
MESSAGE: 1,
|
||||
|
||||
COMPASS1: 1, COMPASS2: 1, COMPASS3: 1, COMPASS4: 1, COMPASS5: 1, COMPASS6: 1, COMPASS7: 1, COMPASS8: 1, COMPASS9: 1,
|
||||
KEY1: 3, KEY2: 5, KEY3: 9, KEY4: 5, KEY5: 3, KEY6: 3, KEY7: 3, KEY8: 7, KEY9: 3,
|
||||
MAP1: 1, MAP2: 1, MAP3: 1, MAP4: 1, MAP5: 1, MAP6: 1, MAP7: 1, MAP8: 1, MAP9: 1,
|
||||
NIGHTMARE_KEY1: 1, NIGHTMARE_KEY2: 1, NIGHTMARE_KEY3: 1, NIGHTMARE_KEY4: 1, NIGHTMARE_KEY5: 1, NIGHTMARE_KEY6: 1, NIGHTMARE_KEY7: 1, NIGHTMARE_KEY8: 1, NIGHTMARE_KEY9: 1,
|
||||
STONE_BEAK1: 1, STONE_BEAK2: 1, STONE_BEAK3: 1, STONE_BEAK4: 1, STONE_BEAK5: 1, STONE_BEAK6: 1, STONE_BEAK7: 1, STONE_BEAK8: 1, STONE_BEAK9: 1,
|
||||
|
||||
INSTRUMENT1: 1, INSTRUMENT2: 1, INSTRUMENT3: 1, INSTRUMENT4: 1, INSTRUMENT5: 1, INSTRUMENT6: 1, INSTRUMENT7: 1, INSTRUMENT8: 1,
|
||||
|
||||
TRADING_ITEM_YOSHI_DOLL: 1,
|
||||
TRADING_ITEM_RIBBON: 1,
|
||||
TRADING_ITEM_DOG_FOOD: 1,
|
||||
TRADING_ITEM_BANANAS: 1,
|
||||
TRADING_ITEM_STICK: 1,
|
||||
TRADING_ITEM_HONEYCOMB: 1,
|
||||
TRADING_ITEM_PINEAPPLE: 1,
|
||||
TRADING_ITEM_HIBISCUS: 1,
|
||||
TRADING_ITEM_LETTER: 1,
|
||||
TRADING_ITEM_BROOM: 1,
|
||||
TRADING_ITEM_FISHING_HOOK: 1,
|
||||
TRADING_ITEM_NECKLACE: 1,
|
||||
TRADING_ITEM_SCALE: 1,
|
||||
TRADING_ITEM_MAGNIFYING_GLASS: 1,
|
||||
|
||||
"MEDICINE2": 1, "RAFT": 1, "ANGLER_KEYHOLE": 1, "CASTLE_BUTTON": 1
|
||||
}
|
||||
|
||||
|
||||
class ItemPool:
|
||||
def __init__(self, logic, settings, rnd):
|
||||
self.__pool = {}
|
||||
self.__setup(logic, settings)
|
||||
self.__randomizeRupees(settings, rnd)
|
||||
|
||||
def add(self, item, count=1):
|
||||
self.__pool[item] = self.__pool.get(item, 0) + count
|
||||
|
||||
def remove(self, item, count=1):
|
||||
self.__pool[item] = self.__pool.get(item, 0) - count
|
||||
if self.__pool[item] == 0:
|
||||
del self.__pool[item]
|
||||
|
||||
def get(self, item):
|
||||
return self.__pool.get(item, 0)
|
||||
|
||||
def count(self):
|
||||
total = 0
|
||||
for count in self.__pool.values():
|
||||
total += count
|
||||
return total
|
||||
|
||||
def removeRupees(self, count):
|
||||
for n in range(count):
|
||||
self.removeRupee()
|
||||
|
||||
def removeRupee(self):
|
||||
for item in (RUPEES_20, RUPEES_50, RUPEES_200, RUPEES_500):
|
||||
if self.get(item) > 0:
|
||||
self.remove(item)
|
||||
return
|
||||
raise RuntimeError("Wanted to remove more rupees from the pool then we have")
|
||||
|
||||
def __setup(self, logic, settings):
|
||||
default_item_pool = DEFAULT_ITEM_POOL
|
||||
if settings.overworld == "random":
|
||||
default_item_pool = logic.world.map.get_item_pool()
|
||||
for item, count in default_item_pool.items():
|
||||
self.add(item, count)
|
||||
if settings.boomerang != 'default' and settings.overworld != "random":
|
||||
self.add(BOOMERANG)
|
||||
if settings.owlstatues == 'both':
|
||||
self.add(RUPEES_20, 9 + 24)
|
||||
elif settings.owlstatues == 'dungeon':
|
||||
self.add(RUPEES_20, 24)
|
||||
elif settings.owlstatues == 'overworld':
|
||||
self.add(RUPEES_20, 9)
|
||||
|
||||
if settings.bowwow == 'always':
|
||||
# Bowwow mode takes a sword from the pool to give as bowwow. So we need to fix that.
|
||||
self.add(SWORD)
|
||||
self.remove(BOWWOW)
|
||||
elif settings.bowwow == 'swordless':
|
||||
# Bowwow mode takes a sword from the pool to give as bowwow, we need to remove all swords and Bowwow except for 1
|
||||
self.add(RUPEES_20, self.get(BOWWOW) + self.get(SWORD) - 1)
|
||||
self.remove(SWORD, self.get(SWORD) - 1)
|
||||
self.remove(BOWWOW, self.get(BOWWOW))
|
||||
if settings.hpmode == 'inverted':
|
||||
self.add(BAD_HEART_CONTAINER, self.get(HEART_CONTAINER))
|
||||
self.remove(HEART_CONTAINER, self.get(HEART_CONTAINER))
|
||||
elif settings.hpmode == 'low':
|
||||
self.add(HEART_PIECE, self.get(HEART_CONTAINER))
|
||||
self.remove(HEART_CONTAINER, self.get(HEART_CONTAINER))
|
||||
elif settings.hpmode == 'extralow':
|
||||
self.add(RUPEES_20, self.get(HEART_CONTAINER))
|
||||
self.remove(HEART_CONTAINER, self.get(HEART_CONTAINER))
|
||||
|
||||
if settings.itempool == 'casual':
|
||||
self.add(FLIPPERS)
|
||||
self.add(FEATHER)
|
||||
self.add(HOOKSHOT)
|
||||
self.add(BOW)
|
||||
self.add(BOMB)
|
||||
self.add(MAGIC_POWDER)
|
||||
self.add(MAGIC_ROD)
|
||||
self.add(OCARINA)
|
||||
self.add(PEGASUS_BOOTS)
|
||||
self.add(POWER_BRACELET)
|
||||
self.add(SHOVEL)
|
||||
self.add(RUPEES_200, 2)
|
||||
self.removeRupees(13)
|
||||
|
||||
for n in range(9):
|
||||
self.remove("MAP%d" % (n + 1))
|
||||
self.remove("COMPASS%d" % (n + 1))
|
||||
self.add("KEY%d" % (n + 1))
|
||||
self.add("NIGHTMARE_KEY%d" % (n +1))
|
||||
elif settings.itempool == 'pain':
|
||||
self.add(BAD_HEART_CONTAINER, 12)
|
||||
self.remove(BLUE_TUNIC)
|
||||
self.remove(MEDICINE, 2)
|
||||
self.remove(HEART_PIECE, 4)
|
||||
self.removeRupees(5)
|
||||
elif settings.itempool == 'keyup':
|
||||
for n in range(9):
|
||||
self.remove("MAP%d" % (n + 1))
|
||||
self.remove("COMPASS%d" % (n + 1))
|
||||
self.add("KEY%d" % (n +1))
|
||||
self.add("NIGHTMARE_KEY%d" % (n +1))
|
||||
if settings.owlstatues in ("none", "overworld"):
|
||||
for n in range(9):
|
||||
self.remove("STONE_BEAK%d" % (n + 1))
|
||||
self.add("KEY%d" % (n +1))
|
||||
|
||||
# if settings.dungeon_items == 'keysy':
|
||||
# for n in range(9):
|
||||
# for amount, item_name in ((9, "KEY"), (1, "NIGHTMARE_KEY")):
|
||||
# item_name = "%s%d" % (item_name, n + 1)
|
||||
# if item_name in self.__pool:
|
||||
# self.add(RUPEES_20, self.__pool[item_name])
|
||||
# self.remove(item_name, self.__pool[item_name])
|
||||
# self.add(item_name, amount)
|
||||
|
||||
if settings.goal == "seashells":
|
||||
for n in range(8):
|
||||
self.remove("INSTRUMENT%d" % (n + 1))
|
||||
self.add(SEASHELL, 8)
|
||||
|
||||
if settings.overworld == "dungeondive":
|
||||
self.remove(SWORD)
|
||||
self.remove(MAX_ARROWS_UPGRADE)
|
||||
self.remove(MAX_BOMBS_UPGRADE)
|
||||
self.remove(MAX_POWDER_UPGRADE)
|
||||
self.remove(SEASHELL, 24)
|
||||
self.remove(TAIL_KEY)
|
||||
self.remove(SLIME_KEY)
|
||||
self.remove(ANGLER_KEY)
|
||||
self.remove(FACE_KEY)
|
||||
self.remove(BIRD_KEY)
|
||||
self.remove(GOLD_LEAF, 5)
|
||||
self.remove(SONG2)
|
||||
self.remove(SONG3)
|
||||
self.remove(HEART_PIECE, 8)
|
||||
self.remove(RUPEES_50, 9)
|
||||
self.remove(RUPEES_20, 2)
|
||||
self.remove(MEDICINE, 3)
|
||||
self.remove(MESSAGE)
|
||||
self.remove(BOWWOW)
|
||||
self.remove(ROOSTER)
|
||||
self.remove(GEL, 2)
|
||||
self.remove("MEDICINE2")
|
||||
self.remove("RAFT")
|
||||
self.remove("ANGLER_KEYHOLE")
|
||||
self.remove("CASTLE_BUTTON")
|
||||
self.remove(TRADING_ITEM_YOSHI_DOLL)
|
||||
self.remove(TRADING_ITEM_RIBBON)
|
||||
self.remove(TRADING_ITEM_DOG_FOOD)
|
||||
self.remove(TRADING_ITEM_BANANAS)
|
||||
self.remove(TRADING_ITEM_STICK)
|
||||
self.remove(TRADING_ITEM_HONEYCOMB)
|
||||
self.remove(TRADING_ITEM_PINEAPPLE)
|
||||
self.remove(TRADING_ITEM_HIBISCUS)
|
||||
self.remove(TRADING_ITEM_LETTER)
|
||||
self.remove(TRADING_ITEM_BROOM)
|
||||
self.remove(TRADING_ITEM_FISHING_HOOK)
|
||||
self.remove(TRADING_ITEM_NECKLACE)
|
||||
self.remove(TRADING_ITEM_SCALE)
|
||||
self.remove(TRADING_ITEM_MAGNIFYING_GLASS)
|
||||
elif not settings.rooster:
|
||||
self.remove(ROOSTER)
|
||||
self.add(RUPEES_50)
|
||||
|
||||
if settings.overworld == "nodungeons":
|
||||
for n in range(9):
|
||||
for item_name in {KEY, NIGHTMARE_KEY, MAP, COMPASS, STONE_BEAK}:
|
||||
self.remove(f"{item_name}{n+1}", self.get(f"{item_name}{n+1}"))
|
||||
self.remove(BLUE_TUNIC)
|
||||
self.remove(RED_TUNIC)
|
||||
self.remove(SEASHELL, 2)
|
||||
self.remove(RUPEES_20, 6)
|
||||
self.remove(RUPEES_50, 17)
|
||||
self.remove(MEDICINE, 3)
|
||||
self.remove(GEL, 4)
|
||||
self.remove(MESSAGE, 1)
|
||||
self.remove(BOMB, 1)
|
||||
self.remove(RUPEES_100, 3)
|
||||
self.add(RUPEES_500, 3)
|
||||
|
||||
# # In multiworld, put a bit more rupees in the seed, this helps with generation (2nd shop item)
|
||||
# # As we cheat and can place rupees for the wrong player.
|
||||
# if settings.multiworld:
|
||||
# rupees20 = self.__pool.get(RUPEES_20, 0)
|
||||
# self.add(RUPEES_50, rupees20 // 2)
|
||||
# self.remove(RUPEES_20, rupees20 // 2)
|
||||
# rupees50 = self.__pool.get(RUPEES_50, 0)
|
||||
# self.add(RUPEES_200, rupees50 // 5)
|
||||
# self.remove(RUPEES_50, rupees50 // 5)
|
||||
|
||||
def __randomizeRupees(self, options, rnd):
|
||||
# Remove rupees from the item pool and replace them with other items to create more variety
|
||||
rupee_item = []
|
||||
rupee_item_count = []
|
||||
for k, v in self.__pool.items():
|
||||
if k in {RUPEES_20, RUPEES_50} and v > 0:
|
||||
rupee_item.append(k)
|
||||
rupee_item_count.append(v)
|
||||
rupee_chests = sum(v for k, v in self.__pool.items() if k.startswith("RUPEES_"))
|
||||
for n in range(rupee_chests // 5):
|
||||
new_item = rnd.choices((BOMB, SINGLE_ARROW, ARROWS_10, MAGIC_POWDER, MEDICINE), (10, 5, 10, 10, 1))[0]
|
||||
while True:
|
||||
remove_item = rnd.choices(rupee_item, rupee_item_count)[0]
|
||||
if self.get(remove_item) > 0:
|
||||
break
|
||||
self.add(new_item)
|
||||
self.remove(remove_item)
|
||||
|
||||
def toDict(self):
|
||||
return self.__pool.copy()
|
|
@ -0,0 +1,26 @@
|
|||
from .beachSword import BeachSword
|
||||
from .chest import Chest, DungeonChest
|
||||
from .droppedKey import DroppedKey
|
||||
from .seashell import Seashell, SeashellMansion
|
||||
from .heartContainer import HeartContainer
|
||||
from .owlStatue import OwlStatue
|
||||
from .madBatter import MadBatter
|
||||
from .shop import ShopItem
|
||||
from .startItem import StartItem
|
||||
from .toadstool import Toadstool
|
||||
from .witch import Witch
|
||||
from .goldLeaf import GoldLeaf, SlimeKey
|
||||
from .boomerangGuy import BoomerangGuy
|
||||
from .anglerKey import AnglerKey
|
||||
from .hookshot import HookshotDrop
|
||||
from .faceKey import FaceKey
|
||||
from .birdKey import BirdKey
|
||||
from .heartPiece import HeartPiece
|
||||
from .tunicFairy import TunicFairy
|
||||
from .song import Song
|
||||
from .instrument import Instrument
|
||||
from .fishingMinigame import FishingMinigame
|
||||
from .keyLocation import KeyLocation
|
||||
from .tradeSequence import TradeSequenceItem
|
||||
|
||||
from .items import *
|
|
@ -0,0 +1,6 @@
|
|||
from .droppedKey import DroppedKey
|
||||
|
||||
|
||||
class AnglerKey(DroppedKey):
|
||||
def __init__(self):
|
||||
super().__init__(0x0CE)
|
|
@ -0,0 +1,32 @@
|
|||
from .droppedKey import DroppedKey
|
||||
from .items import *
|
||||
from ..roomEditor import RoomEditor
|
||||
from ..assembler import ASM
|
||||
from typing import Optional
|
||||
from ..rom import ROM
|
||||
|
||||
|
||||
class BeachSword(DroppedKey):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(0x0F2)
|
||||
|
||||
def patch(self, rom: ROM, option: str, *, multiworld: Optional[int] = None) -> None:
|
||||
if option != SWORD or multiworld is not None:
|
||||
# Set the heart piece data
|
||||
super().patch(rom, option, multiworld=multiworld)
|
||||
|
||||
# Patch the room to contain a heart piece instead of the sword on the beach
|
||||
re = RoomEditor(rom, 0x0F2)
|
||||
re.removeEntities(0x31) # remove sword
|
||||
re.addEntity(5, 5, 0x35) # add heart piece
|
||||
re.store(rom)
|
||||
|
||||
# Prevent shield drops from the like-like from turning into swords.
|
||||
rom.patch(0x03, 0x1B9C, ASM("ld a, [$DB4E]"), ASM("ld a, $01"), fill_nop=True)
|
||||
rom.patch(0x03, 0x244D, ASM("ld a, [$DB4E]"), ASM("ld a, $01"), fill_nop=True)
|
||||
|
||||
def read(self, rom: ROM) -> str:
|
||||
re = RoomEditor(rom, 0x0F2)
|
||||
if re.hasEntity(0x31):
|
||||
return SWORD
|
||||
return super().read(rom)
|
|
@ -0,0 +1,23 @@
|
|||
from .droppedKey import DroppedKey
|
||||
from ..roomEditor import RoomEditor
|
||||
from ..assembler import ASM
|
||||
|
||||
|
||||
class BirdKey(DroppedKey):
|
||||
def __init__(self):
|
||||
super().__init__(0x27A)
|
||||
|
||||
def patch(self, rom, option, *, multiworld=None):
|
||||
super().patch(rom, option, multiworld=multiworld)
|
||||
|
||||
re = RoomEditor(rom, self.room)
|
||||
|
||||
# Make the bird key accessible without the rooster
|
||||
re.removeObject(1, 6)
|
||||
re.removeObject(2, 6)
|
||||
re.removeObject(3, 5)
|
||||
re.removeObject(3, 6)
|
||||
re.moveObject(1, 5, 2, 6)
|
||||
re.moveObject(2, 5, 3, 6)
|
||||
re.addEntity(3, 5, 0x9D)
|
||||
re.store(rom)
|
|
@ -0,0 +1,94 @@
|
|||
from .itemInfo import ItemInfo
|
||||
from .constants import *
|
||||
from ..assembler import ASM
|
||||
from ..utils import formatText
|
||||
|
||||
|
||||
class BoomerangGuy(ItemInfo):
|
||||
OPTIONS = [BOOMERANG, HOOKSHOT, MAGIC_ROD, PEGASUS_BOOTS, FEATHER, SHOVEL]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(0x1F5)
|
||||
self.setting = 'trade'
|
||||
|
||||
def configure(self, options):
|
||||
self.MULTIWORLD = False
|
||||
|
||||
self.setting = options.boomerang
|
||||
if self.setting == 'gift':
|
||||
self.MULTIWORLD = True
|
||||
|
||||
# Cannot trade:
|
||||
# SWORD, BOMB, SHIELD, POWER_BRACELET, OCARINA, MAGIC_POWDER, BOW
|
||||
# Checks for these are at $46A2, and potentially we could remove those.
|
||||
# But SHIELD, BOMB and MAGIC_POWDER would most likely break things.
|
||||
# SWORD and POWER_BRACELET would most likely introduce the lv0 shield/bracelet issue
|
||||
def patch(self, rom, option, *, multiworld=None):
|
||||
# Always have the boomerang trade guy enabled (normally you need the magnifier)
|
||||
rom.patch(0x19, 0x05EC, ASM("ld a, [wTradeSequenceItem]\ncp $0E"), ASM("ld a, $0E\ncp $0E"), fill_nop=True) # show the guy
|
||||
rom.patch(0x00, 0x3199, ASM("ld a, [wTradeSequenceItem]\ncp $0E"), ASM("ld a, $0E\ncp $0E"), fill_nop=True) # load the proper room layout
|
||||
rom.patch(0x19, 0x05F4, ASM("ld a, [wTradeSequenceItem2]\nand a"), ASM("xor a"), fill_nop=True)
|
||||
|
||||
if self.setting == 'trade':
|
||||
inv = INVENTORY_MAP[option]
|
||||
# Patch the check if you traded back the boomerang (so traded twice)
|
||||
rom.patch(0x19, 0x063F, ASM("cp $0D"), ASM("cp $%s" % (inv)))
|
||||
# Item to give by "default" (aka, boomerang)
|
||||
rom.patch(0x19, 0x06C1, ASM("ld a, $0D"), ASM("ld a, $%s" % (inv)))
|
||||
# Check if inventory slot is boomerang to give back item in this slot
|
||||
rom.patch(0x19, 0x06FC, ASM("cp $0D"), ASM("cp $%s" % (inv)))
|
||||
# Put the boomerang ID in the inventory of the boomerang guy (aka, traded back)
|
||||
rom.patch(0x19, 0x0710, ASM("ld a, $0D"), ASM("ld a, $%s" % (inv)))
|
||||
|
||||
rom.texts[0x222] = formatText("Okay, let's do it!")
|
||||
rom.texts[0x224] = formatText("You got the {%s} in exchange for the item you had." % (option))
|
||||
rom.texts[0x225] = formatText("Give me back my {%s}, I beg you! I'll return the item you gave me" % (option), ask="Okay Not Now")
|
||||
rom.texts[0x226] = formatText("The item came back to you. You returned the other item.")
|
||||
else:
|
||||
# Patch the inventory trade to give an specific item instead
|
||||
rom.texts[0x221] = formatText("I found a good item washed up on the beach... Want to have it?", ask="Okay No")
|
||||
rom.patch(0x19, 0x069C, 0x06C6, ASM("""
|
||||
; Mark trade as done
|
||||
ld a, $06
|
||||
ld [$DB7D], a
|
||||
|
||||
ld a, [$472B]
|
||||
ldh [$F1], a
|
||||
ld a, $06
|
||||
rst 8
|
||||
|
||||
ld a, $0D
|
||||
"""), fill_nop=True)
|
||||
# Show the right item above link
|
||||
rom.patch(0x19, 0x0786, 0x0793, ASM("""
|
||||
ld a, [$472B]
|
||||
ldh [$F1], a
|
||||
ld a, $01
|
||||
rst 8
|
||||
"""), fill_nop=True)
|
||||
# Give the proper message for this item
|
||||
rom.patch(0x19, 0x075A, 0x076A, ASM("""
|
||||
ld a, [$472B]
|
||||
ldh [$F1], a
|
||||
ld a, $0A
|
||||
rst 8
|
||||
"""), fill_nop=True)
|
||||
rom.patch(0x19, 0x072B, "00", "%02X" % (CHEST_ITEMS[option]))
|
||||
|
||||
# Ignore the trade back.
|
||||
rom.texts[0x225] = formatText("It's a secret to everybody.")
|
||||
rom.patch(0x19, 0x0668, ASM("ld a, [$DB7D]"), ASM("ret"), fill_nop=True)
|
||||
|
||||
if multiworld is not None:
|
||||
rom.banks[0x3E][0x3300 + self.room] = multiworld
|
||||
|
||||
def read(self, rom):
|
||||
if rom.banks[0x19][0x06C5] == 0x00:
|
||||
for k, v in CHEST_ITEMS.items():
|
||||
if v == rom.banks[0x19][0x072B]:
|
||||
return k
|
||||
else:
|
||||
for k, v in INVENTORY_MAP.items():
|
||||
if int(v, 16) == rom.banks[0x19][0x0640]:
|
||||
return k
|
||||
raise ValueError()
|
|
@ -0,0 +1,50 @@
|
|||
from .itemInfo import ItemInfo
|
||||
from .constants import *
|
||||
from ..assembler import ASM
|
||||
|
||||
|
||||
class Chest(ItemInfo):
|
||||
def __init__(self, room):
|
||||
super().__init__(room)
|
||||
self.addr = room + 0x560
|
||||
|
||||
def patch(self, rom, option, *, multiworld=None):
|
||||
rom.banks[0x14][self.addr] = CHEST_ITEMS[option]
|
||||
|
||||
if self.room == 0x1B6:
|
||||
# Patch the code that gives the nightmare key when you throw the pot at the chest in dungeon 6
|
||||
# As this is hardcoded for a specific chest type
|
||||
rom.patch(3, 0x145D, ASM("ld a, $19"), ASM("ld a, $%02x" % (CHEST_ITEMS[option])))
|
||||
if multiworld is not None:
|
||||
rom.banks[0x3E][0x3300 + self.room] = multiworld
|
||||
|
||||
def read(self, rom):
|
||||
value = rom.banks[0x14][self.addr]
|
||||
for k, v in CHEST_ITEMS.items():
|
||||
if v == value:
|
||||
return k
|
||||
raise ValueError("Could not find chest contents in ROM (0x%02x)" % (value))
|
||||
|
||||
def __repr__(self):
|
||||
return "%s:%03x" % (self.__class__.__name__, self.room)
|
||||
|
||||
|
||||
class DungeonChest(Chest):
|
||||
def patch(self, rom, option, *, multiworld=None):
|
||||
if (option.startswith(MAP) and option != MAP) \
|
||||
or (option.startswith(COMPASS) and option != COMPASS) \
|
||||
or (option.startswith(STONE_BEAK) and option != STONE_BEAK) \
|
||||
or (option.startswith(NIGHTMARE_KEY) and option != NIGHTMARE_KEY) \
|
||||
or (option.startswith(KEY) and option != KEY):
|
||||
if self._location.dungeon == int(option[-1]) and multiworld is None:
|
||||
option = option[:-1]
|
||||
super().patch(rom, option, multiworld=multiworld)
|
||||
|
||||
def read(self, rom):
|
||||
result = super().read(rom)
|
||||
if result in [MAP, COMPASS, STONE_BEAK, NIGHTMARE_KEY, KEY]:
|
||||
return "%s%d" % (result, self._location.dungeon)
|
||||
return result
|
||||
|
||||
def __repr__(self):
|
||||
return "%s:%03x:%d" % (self.__class__.__name__, self.room, self._location.dungeon)
|
|
@ -0,0 +1,131 @@
|
|||
from .items import *
|
||||
|
||||
INVENTORY_MAP = {
|
||||
SWORD: "01",
|
||||
BOMB: "02",
|
||||
POWER_BRACELET: "03",
|
||||
SHIELD: "04",
|
||||
BOW: "05",
|
||||
HOOKSHOT: "06",
|
||||
MAGIC_ROD: "07",
|
||||
PEGASUS_BOOTS: "08",
|
||||
OCARINA: "09",
|
||||
FEATHER: "0A",
|
||||
SHOVEL: "0B",
|
||||
MAGIC_POWDER: "0C",
|
||||
BOOMERANG: "0D",
|
||||
TOADSTOOL: "0E",
|
||||
}
|
||||
CHEST_ITEMS = {
|
||||
POWER_BRACELET: 0x00,
|
||||
SHIELD: 0x01,
|
||||
BOW: 0x02,
|
||||
HOOKSHOT: 0x03,
|
||||
MAGIC_ROD: 0x04,
|
||||
PEGASUS_BOOTS: 0x05,
|
||||
OCARINA: 0x06,
|
||||
FEATHER: 0x07, SHOVEL: 0x08, MAGIC_POWDER: 0x09, BOMB: 0x0A, SWORD: 0x0B, FLIPPERS: 0x0C,
|
||||
MAGNIFYING_LENS: 0x0D, MEDICINE: 0x10,
|
||||
TAIL_KEY: 0x11, ANGLER_KEY: 0x12, FACE_KEY: 0x13, BIRD_KEY: 0x14, GOLD_LEAF: 0x15,
|
||||
RUPEES_50: 0x1B, RUPEES_20: 0x1C, RUPEES_100: 0x1D, RUPEES_200: 0x1E, RUPEES_500: 0x1F,
|
||||
SEASHELL: 0x20, MESSAGE: 0x21, GEL: 0x22,
|
||||
MAP: 0x16, COMPASS: 0x17, STONE_BEAK: 0x18, NIGHTMARE_KEY: 0x19, KEY: 0x1A,
|
||||
ROOSTER: 0x96,
|
||||
|
||||
BOOMERANG: 0x0E,
|
||||
SLIME_KEY: 0x0F,
|
||||
|
||||
KEY1: 0x23,
|
||||
KEY2: 0x24,
|
||||
KEY3: 0x25,
|
||||
KEY4: 0x26,
|
||||
KEY5: 0x27,
|
||||
KEY6: 0x28,
|
||||
KEY7: 0x29,
|
||||
KEY8: 0x2A,
|
||||
KEY9: 0x2B,
|
||||
|
||||
MAP1: 0x2C,
|
||||
MAP2: 0x2D,
|
||||
MAP3: 0x2E,
|
||||
MAP4: 0x2F,
|
||||
MAP5: 0x30,
|
||||
MAP6: 0x31,
|
||||
MAP7: 0x32,
|
||||
MAP8: 0x33,
|
||||
MAP9: 0x34,
|
||||
|
||||
COMPASS1: 0x35,
|
||||
COMPASS2: 0x36,
|
||||
COMPASS3: 0x37,
|
||||
COMPASS4: 0x38,
|
||||
COMPASS5: 0x39,
|
||||
COMPASS6: 0x3A,
|
||||
COMPASS7: 0x3B,
|
||||
COMPASS8: 0x3C,
|
||||
COMPASS9: 0x3D,
|
||||
|
||||
STONE_BEAK1: 0x3E,
|
||||
STONE_BEAK2: 0x3F,
|
||||
STONE_BEAK3: 0x40,
|
||||
STONE_BEAK4: 0x41,
|
||||
STONE_BEAK5: 0x42,
|
||||
STONE_BEAK6: 0x43,
|
||||
STONE_BEAK7: 0x44,
|
||||
STONE_BEAK8: 0x45,
|
||||
STONE_BEAK9: 0x46,
|
||||
|
||||
NIGHTMARE_KEY1: 0x47,
|
||||
NIGHTMARE_KEY2: 0x48,
|
||||
NIGHTMARE_KEY3: 0x49,
|
||||
NIGHTMARE_KEY4: 0x4A,
|
||||
NIGHTMARE_KEY5: 0x4B,
|
||||
NIGHTMARE_KEY6: 0x4C,
|
||||
NIGHTMARE_KEY7: 0x4D,
|
||||
NIGHTMARE_KEY8: 0x4E,
|
||||
NIGHTMARE_KEY9: 0x4F,
|
||||
|
||||
TOADSTOOL: 0x50,
|
||||
|
||||
HEART_PIECE: 0x80,
|
||||
BOWWOW: 0x81,
|
||||
ARROWS_10: 0x82,
|
||||
SINGLE_ARROW: 0x83,
|
||||
|
||||
MAX_POWDER_UPGRADE: 0x84,
|
||||
MAX_BOMBS_UPGRADE: 0x85,
|
||||
MAX_ARROWS_UPGRADE: 0x86,
|
||||
|
||||
RED_TUNIC: 0x87,
|
||||
BLUE_TUNIC: 0x88,
|
||||
HEART_CONTAINER: 0x89,
|
||||
BAD_HEART_CONTAINER: 0x8A,
|
||||
|
||||
SONG1: 0x8B,
|
||||
SONG2: 0x8C,
|
||||
SONG3: 0x8D,
|
||||
|
||||
INSTRUMENT1: 0x8E,
|
||||
INSTRUMENT2: 0x8F,
|
||||
INSTRUMENT3: 0x90,
|
||||
INSTRUMENT4: 0x91,
|
||||
INSTRUMENT5: 0x92,
|
||||
INSTRUMENT6: 0x93,
|
||||
INSTRUMENT7: 0x94,
|
||||
INSTRUMENT8: 0x95,
|
||||
|
||||
TRADING_ITEM_YOSHI_DOLL: 0x97,
|
||||
TRADING_ITEM_RIBBON: 0x98,
|
||||
TRADING_ITEM_DOG_FOOD: 0x99,
|
||||
TRADING_ITEM_BANANAS: 0x9A,
|
||||
TRADING_ITEM_STICK: 0x9B,
|
||||
TRADING_ITEM_HONEYCOMB: 0x9C,
|
||||
TRADING_ITEM_PINEAPPLE: 0x9D,
|
||||
TRADING_ITEM_HIBISCUS: 0x9E,
|
||||
TRADING_ITEM_LETTER: 0x9F,
|
||||
TRADING_ITEM_BROOM: 0xA0,
|
||||
TRADING_ITEM_FISHING_HOOK: 0xA1,
|
||||
TRADING_ITEM_NECKLACE: 0xA2,
|
||||
TRADING_ITEM_SCALE: 0xA3,
|
||||
TRADING_ITEM_MAGNIFYING_GLASS: 0xA4,
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
from .itemInfo import ItemInfo
|
||||
from .constants import *
|
||||
patched_already = {}
|
||||
|
||||
class DroppedKey(ItemInfo):
|
||||
default_item = None
|
||||
|
||||
def __init__(self, room=None):
|
||||
extra = None
|
||||
if room == 0x169: # Room in D4 where the key drops down the hole into the sidescroller
|
||||
extra = 0x017C
|
||||
elif room == 0x166: # D4 boss, also place the item in out real boss room.
|
||||
extra = 0x01ff
|
||||
elif room == 0x223: # D7 boss, also place the item in our real boss room.
|
||||
extra = 0x02E8
|
||||
elif room == 0x092: # Marins song
|
||||
extra = 0x00DC
|
||||
elif room == 0x0CE:
|
||||
extra = 0x01F8
|
||||
super().__init__(room, extra)
|
||||
def patch(self, rom, option, *, multiworld=None):
|
||||
if (option.startswith(MAP) and option != MAP) or (option.startswith(COMPASS) and option != COMPASS) or option.startswith(STONE_BEAK) or (option.startswith(NIGHTMARE_KEY) and option != NIGHTMARE_KEY )or (option.startswith(KEY) and option != KEY):
|
||||
if option[-1] == 'P':
|
||||
print(option)
|
||||
if self._location.dungeon == int(option[-1]) and multiworld is None and self.room not in {0x166, 0x223}:
|
||||
option = option[:-1]
|
||||
rom.banks[0x3E][self.room + 0x3800] = CHEST_ITEMS[option]
|
||||
#assert room not in patched_already, f"{self} {patched_already[room]}"
|
||||
#patched_already[room] = self
|
||||
|
||||
|
||||
if self.extra:
|
||||
assert(not self.default_item)
|
||||
rom.banks[0x3E][self.extra + 0x3800] = CHEST_ITEMS[option]
|
||||
|
||||
if multiworld is not None:
|
||||
rom.banks[0x3E][0x3300 + self.room] = multiworld
|
||||
|
||||
if self.extra:
|
||||
rom.banks[0x3E][0x3300 + self.extra] = multiworld
|
||||
|
||||
def read(self, rom):
|
||||
assert self._location is not None, hex(self.room)
|
||||
value = rom.banks[0x3E][self.room + 0x3800]
|
||||
for k, v in CHEST_ITEMS.items():
|
||||
if v == value:
|
||||
if k in [MAP, COMPASS, STONE_BEAK, NIGHTMARE_KEY, KEY]:
|
||||
assert self._location.dungeon is not None, "Dungeon item outside of dungeon? %r" % (self)
|
||||
return "%s%d" % (k, self._location.dungeon)
|
||||
return k
|
||||
raise ValueError("Could not find chest contents in ROM (0x%02x)" % (value))
|
||||
|
||||
def __repr__(self):
|
||||
if self._location and self._location.dungeon:
|
||||
return "%s:%03x:%d" % (self.__class__.__name__, self.room, self._location.dungeon)
|
||||
else:
|
||||
return "%s:%03x" % (self.__class__.__name__, self.room)
|
|
@ -0,0 +1,6 @@
|
|||
from .droppedKey import DroppedKey
|
||||
|
||||
|
||||
class FaceKey(DroppedKey):
|
||||
def __init__(self):
|
||||
super().__init__(0x27F)
|
|
@ -0,0 +1,13 @@
|
|||
from .droppedKey import DroppedKey
|
||||
from .constants import *
|
||||
|
||||
|
||||
class FishingMinigame(DroppedKey):
|
||||
def __init__(self):
|
||||
super().__init__(0x2B1)
|
||||
|
||||
def configure(self, options):
|
||||
if options.heartpiece:
|
||||
super().configure(options)
|
||||
else:
|
||||
self.OPTIONS = [HEART_PIECE]
|
|
@ -0,0 +1,12 @@
|
|||
from .droppedKey import DroppedKey
|
||||
|
||||
|
||||
class GoldLeaf(DroppedKey):
|
||||
pass # Golden leaves are patched to work exactly like dropped keys
|
||||
|
||||
|
||||
class SlimeKey(DroppedKey):
|
||||
# The slime key is secretly a golden leaf and just normally uses logic depended on the room number.
|
||||
# As we patched it to act like a dropped key, we can just be a dropped key in the right room
|
||||
def __init__(self):
|
||||
super().__init__(0x0C6)
|
|
@ -0,0 +1,15 @@
|
|||
from .droppedKey import DroppedKey
|
||||
from .items import *
|
||||
|
||||
|
||||
class HeartContainer(DroppedKey):
|
||||
# Due to the patches a heartContainers acts like a dropped key.
|
||||
def configure(self, options):
|
||||
if options.heartcontainers or options.hpmode == 'extralow':
|
||||
super().configure(options)
|
||||
elif options.hpmode == 'inverted':
|
||||
self.OPTIONS = [BAD_HEART_CONTAINER]
|
||||
elif options.hpmode == 'low':
|
||||
self.OPTIONS = [HEART_PIECE]
|
||||
else:
|
||||
self.OPTIONS = [HEART_CONTAINER]
|
|
@ -0,0 +1,12 @@
|
|||
from .droppedKey import DroppedKey
|
||||
from .items import *
|
||||
|
||||
|
||||
class HeartPiece(DroppedKey):
|
||||
# Due to the patches a heartPiece acts like a dropped key.
|
||||
|
||||
def configure(self, options):
|
||||
if options.heartpiece:
|
||||
super().configure(options)
|
||||
else:
|
||||
self.OPTIONS = [HEART_PIECE]
|
|
@ -0,0 +1,18 @@
|
|||
from .droppedKey import DroppedKey
|
||||
|
||||
|
||||
"""
|
||||
The hookshot is dropped by the master stalfos.
|
||||
The master stalfos drops a "key" with, and modifies a bunch of properties:
|
||||
|
||||
ld a, $30 ; $7EE1: $3E $30
|
||||
call SpawnNewEntity_trampoline ; $7EE3: $CD $86 $3B
|
||||
|
||||
And then the dropped key handles the rest with room number specific code.
|
||||
As we patched the dropped key, this requires no extra handling.
|
||||
"""
|
||||
|
||||
|
||||
class HookshotDrop(DroppedKey):
|
||||
def __init__(self):
|
||||
super().__init__(0x180)
|
|
@ -0,0 +1,9 @@
|
|||
from .droppedKey import DroppedKey
|
||||
|
||||
|
||||
class Instrument(DroppedKey):
|
||||
# Thanks to patches, an instrument is just a dropped key as far as the randomizer is concerned.
|
||||
|
||||
def configure(self, options):
|
||||
if not options.instruments and not options.goal == "seashells":
|
||||
self.OPTIONS = ["INSTRUMENT%d" % (self._location.dungeon)]
|
|
@ -0,0 +1,43 @@
|
|||
import typing
|
||||
from ..checkMetadata import checkMetadataTable
|
||||
from .constants import *
|
||||
|
||||
|
||||
class ItemInfo:
|
||||
MULTIWORLD = True
|
||||
|
||||
def __init__(self, room=None, extra=None):
|
||||
self.item = None
|
||||
self._location = None
|
||||
self.room = room
|
||||
self.extra = extra
|
||||
self.metadata = checkMetadataTable.get(self.nameId, checkMetadataTable["None"])
|
||||
self.forced_item = None
|
||||
self.custom_item_name = None
|
||||
|
||||
self.event = None
|
||||
@property
|
||||
def location(self):
|
||||
return self._location
|
||||
|
||||
def setLocation(self, location):
|
||||
self._location = location
|
||||
|
||||
def getOptions(self):
|
||||
return self.OPTIONS
|
||||
|
||||
def configure(self, options):
|
||||
pass
|
||||
|
||||
def read(self, rom):
|
||||
raise NotImplementedError()
|
||||
|
||||
def patch(self, rom, option, *, multiworld=None):
|
||||
raise NotImplementedError()
|
||||
|
||||
def __repr__(self):
|
||||
return self.__class__.__name__
|
||||
|
||||
@property
|
||||
def nameId(self):
|
||||
return "0x%03X" % self.room if self.room is not None else "None"
|
|
@ -0,0 +1,127 @@
|
|||
POWER_BRACELET = "POWER_BRACELET"
|
||||
SHIELD = "SHIELD"
|
||||
BOW = "BOW"
|
||||
HOOKSHOT = "HOOKSHOT"
|
||||
MAGIC_ROD = "MAGIC_ROD"
|
||||
PEGASUS_BOOTS = "PEGASUS_BOOTS"
|
||||
OCARINA = "OCARINA"
|
||||
FEATHER = "FEATHER"
|
||||
SHOVEL = "SHOVEL"
|
||||
MAGIC_POWDER = "MAGIC_POWDER"
|
||||
BOMB = "BOMB"
|
||||
SWORD = "SWORD"
|
||||
FLIPPERS = "FLIPPERS"
|
||||
MAGNIFYING_LENS = "MAGNIFYING_LENS"
|
||||
MEDICINE = "MEDICINE"
|
||||
TAIL_KEY = "TAIL_KEY"
|
||||
ANGLER_KEY = "ANGLER_KEY"
|
||||
FACE_KEY = "FACE_KEY"
|
||||
BIRD_KEY = "BIRD_KEY"
|
||||
SLIME_KEY = "SLIME_KEY"
|
||||
GOLD_LEAF = "GOLD_LEAF"
|
||||
RUPEES_50 = "RUPEES_50"
|
||||
RUPEES_20 = "RUPEES_20"
|
||||
RUPEES_100 = "RUPEES_100"
|
||||
RUPEES_200 = "RUPEES_200"
|
||||
RUPEES_500 = "RUPEES_500"
|
||||
SEASHELL = "SEASHELL"
|
||||
MESSAGE = "MESSAGE"
|
||||
GEL = "GEL"
|
||||
BOOMERANG = "BOOMERANG"
|
||||
HEART_PIECE = "HEART_PIECE"
|
||||
BOWWOW = "BOWWOW"
|
||||
ARROWS_10 = "ARROWS_10"
|
||||
SINGLE_ARROW = "SINGLE_ARROW"
|
||||
ROOSTER = "ROOSTER"
|
||||
|
||||
MAX_POWDER_UPGRADE = "MAX_POWDER_UPGRADE"
|
||||
MAX_BOMBS_UPGRADE = "MAX_BOMBS_UPGRADE"
|
||||
MAX_ARROWS_UPGRADE = "MAX_ARROWS_UPGRADE"
|
||||
|
||||
RED_TUNIC = "RED_TUNIC"
|
||||
BLUE_TUNIC = "BLUE_TUNIC"
|
||||
HEART_CONTAINER = "HEART_CONTAINER"
|
||||
BAD_HEART_CONTAINER = "BAD_HEART_CONTAINER"
|
||||
|
||||
TOADSTOOL = "TOADSTOOL"
|
||||
|
||||
KEY = "KEY"
|
||||
KEY1 = "KEY1"
|
||||
KEY2 = "KEY2"
|
||||
KEY3 = "KEY3"
|
||||
KEY4 = "KEY4"
|
||||
KEY5 = "KEY5"
|
||||
KEY6 = "KEY6"
|
||||
KEY7 = "KEY7"
|
||||
KEY8 = "KEY8"
|
||||
KEY9 = "KEY9"
|
||||
|
||||
NIGHTMARE_KEY = "NIGHTMARE_KEY"
|
||||
NIGHTMARE_KEY1 = "NIGHTMARE_KEY1"
|
||||
NIGHTMARE_KEY2 = "NIGHTMARE_KEY2"
|
||||
NIGHTMARE_KEY3 = "NIGHTMARE_KEY3"
|
||||
NIGHTMARE_KEY4 = "NIGHTMARE_KEY4"
|
||||
NIGHTMARE_KEY5 = "NIGHTMARE_KEY5"
|
||||
NIGHTMARE_KEY6 = "NIGHTMARE_KEY6"
|
||||
NIGHTMARE_KEY7 = "NIGHTMARE_KEY7"
|
||||
NIGHTMARE_KEY8 = "NIGHTMARE_KEY8"
|
||||
NIGHTMARE_KEY9 = "NIGHTMARE_KEY9"
|
||||
|
||||
MAP = "MAP"
|
||||
MAP1 = "MAP1"
|
||||
MAP2 = "MAP2"
|
||||
MAP3 = "MAP3"
|
||||
MAP4 = "MAP4"
|
||||
MAP5 = "MAP5"
|
||||
MAP6 = "MAP6"
|
||||
MAP7 = "MAP7"
|
||||
MAP8 = "MAP8"
|
||||
MAP9 = "MAP9"
|
||||
COMPASS = "COMPASS"
|
||||
COMPASS1 = "COMPASS1"
|
||||
COMPASS2 = "COMPASS2"
|
||||
COMPASS3 = "COMPASS3"
|
||||
COMPASS4 = "COMPASS4"
|
||||
COMPASS5 = "COMPASS5"
|
||||
COMPASS6 = "COMPASS6"
|
||||
COMPASS7 = "COMPASS7"
|
||||
COMPASS8 = "COMPASS8"
|
||||
COMPASS9 = "COMPASS9"
|
||||
STONE_BEAK = "STONE_BEAK"
|
||||
STONE_BEAK1 = "STONE_BEAK1"
|
||||
STONE_BEAK2 = "STONE_BEAK2"
|
||||
STONE_BEAK3 = "STONE_BEAK3"
|
||||
STONE_BEAK4 = "STONE_BEAK4"
|
||||
STONE_BEAK5 = "STONE_BEAK5"
|
||||
STONE_BEAK6 = "STONE_BEAK6"
|
||||
STONE_BEAK7 = "STONE_BEAK7"
|
||||
STONE_BEAK8 = "STONE_BEAK8"
|
||||
STONE_BEAK9 = "STONE_BEAK9"
|
||||
|
||||
SONG1 = "SONG1"
|
||||
SONG2 = "SONG2"
|
||||
SONG3 = "SONG3"
|
||||
|
||||
INSTRUMENT1 = "INSTRUMENT1"
|
||||
INSTRUMENT2 = "INSTRUMENT2"
|
||||
INSTRUMENT3 = "INSTRUMENT3"
|
||||
INSTRUMENT4 = "INSTRUMENT4"
|
||||
INSTRUMENT5 = "INSTRUMENT5"
|
||||
INSTRUMENT6 = "INSTRUMENT6"
|
||||
INSTRUMENT7 = "INSTRUMENT7"
|
||||
INSTRUMENT8 = "INSTRUMENT8"
|
||||
|
||||
TRADING_ITEM_YOSHI_DOLL = "TRADING_ITEM_YOSHI_DOLL"
|
||||
TRADING_ITEM_RIBBON = "TRADING_ITEM_RIBBON"
|
||||
TRADING_ITEM_DOG_FOOD = "TRADING_ITEM_DOG_FOOD"
|
||||
TRADING_ITEM_BANANAS = "TRADING_ITEM_BANANAS"
|
||||
TRADING_ITEM_STICK = "TRADING_ITEM_STICK"
|
||||
TRADING_ITEM_HONEYCOMB = "TRADING_ITEM_HONEYCOMB"
|
||||
TRADING_ITEM_PINEAPPLE = "TRADING_ITEM_PINEAPPLE"
|
||||
TRADING_ITEM_HIBISCUS = "TRADING_ITEM_HIBISCUS"
|
||||
TRADING_ITEM_LETTER = "TRADING_ITEM_LETTER"
|
||||
TRADING_ITEM_BROOM = "TRADING_ITEM_BROOM"
|
||||
TRADING_ITEM_FISHING_HOOK = "TRADING_ITEM_FISHING_HOOK"
|
||||
TRADING_ITEM_NECKLACE = "TRADING_ITEM_NECKLACE"
|
||||
TRADING_ITEM_SCALE = "TRADING_ITEM_SCALE"
|
||||
TRADING_ITEM_MAGNIFYING_GLASS = "TRADING_ITEM_MAGNIFYING_GLASS"
|
|
@ -0,0 +1,18 @@
|
|||
from .itemInfo import ItemInfo
|
||||
|
||||
|
||||
class KeyLocation(ItemInfo):
|
||||
OPTIONS = []
|
||||
|
||||
def __init__(self, key):
|
||||
super().__init__()
|
||||
self.event = key
|
||||
|
||||
def patch(self, rom, option, *, multiworld=None):
|
||||
pass
|
||||
|
||||
def read(self, rom):
|
||||
return self.OPTIONS[0]
|
||||
|
||||
def configure(self, options):
|
||||
pass
|
|
@ -0,0 +1,23 @@
|
|||
from .itemInfo import ItemInfo
|
||||
from .constants import *
|
||||
|
||||
|
||||
class MadBatter(ItemInfo):
|
||||
def configure(self, options):
|
||||
return
|
||||
|
||||
def patch(self, rom, option, *, multiworld=None):
|
||||
rom.banks[0x18][0x0F90 + (self.room & 0x0F)] = CHEST_ITEMS[option]
|
||||
if multiworld is not None:
|
||||
rom.banks[0x3E][0x3300 + self.room] = multiworld
|
||||
|
||||
def read(self, rom):
|
||||
assert self._location is not None, hex(self.room)
|
||||
value = rom.banks[0x18][0x0F90 + (self.room & 0x0F)]
|
||||
for k, v in CHEST_ITEMS.items():
|
||||
if v == value:
|
||||
return k
|
||||
raise ValueError("Could not find mad batter contents in ROM (0x%02x)" % (value))
|
||||
|
||||
def __repr__(self):
|
||||
return "%s:%03x" % (self.__class__.__name__, self.room)
|
|
@ -0,0 +1,41 @@
|
|||
from .itemInfo import ItemInfo
|
||||
from .constants import *
|
||||
|
||||
|
||||
class OwlStatue(ItemInfo):
|
||||
def configure(self, options):
|
||||
if options.owlstatues == "both":
|
||||
return
|
||||
if options.owlstatues == "dungeon" and self.room >= 0x100:
|
||||
return
|
||||
if options.owlstatues == "overworld" and self.room < 0x100:
|
||||
return
|
||||
raise RuntimeError("Tried to configure an owlstatue that was not enabled")
|
||||
self.OPTIONS = [RUPEES_20]
|
||||
|
||||
def patch(self, rom, option, *, multiworld=None):
|
||||
if option.startswith(MAP) or option.startswith(COMPASS) or option.startswith(STONE_BEAK) or option.startswith(NIGHTMARE_KEY) or option.startswith(KEY):
|
||||
if self._location.dungeon == int(option[-1]) and multiworld is not None:
|
||||
option = option[:-1]
|
||||
rom.banks[0x3E][self.room + 0x3B16] = CHEST_ITEMS[option]
|
||||
|
||||
def read(self, rom):
|
||||
assert self._location is not None, hex(self.room)
|
||||
value = rom.banks[0x3E][self.room + 0x3B16]
|
||||
for k, v in CHEST_ITEMS.items():
|
||||
if v == value:
|
||||
if k in [MAP, COMPASS, STONE_BEAK, NIGHTMARE_KEY, KEY]:
|
||||
assert self._location.dungeon is not None, "Dungeon item outside of dungeon? %r" % (self)
|
||||
return "%s%d" % (k, self._location.dungeon)
|
||||
return k
|
||||
raise ValueError("Could not find owl statue contents in ROM (0x%02x)" % (value))
|
||||
|
||||
def __repr__(self):
|
||||
if self._location and self._location.dungeon:
|
||||
return "%s:%03x:%d" % (self.__class__.__name__, self.room, self._location.dungeon)
|
||||
else:
|
||||
return "%s:%03x" % (self.__class__.__name__, self.room)
|
||||
|
||||
@property
|
||||
def nameId(self):
|
||||
return "0x%03X-Owl" % self.room
|
|
@ -0,0 +1,14 @@
|
|||
from .droppedKey import DroppedKey
|
||||
from .items import *
|
||||
|
||||
|
||||
class Seashell(DroppedKey):
|
||||
# Thanks to patches, a seashell is just a dropped key as far as the randomizer is concerned.
|
||||
|
||||
def configure(self, options):
|
||||
if not options.seashells:
|
||||
self.OPTIONS = [SEASHELL]
|
||||
|
||||
|
||||
class SeashellMansion(DroppedKey):
|
||||
pass
|
|
@ -0,0 +1,42 @@
|
|||
from .itemInfo import ItemInfo
|
||||
from .constants import *
|
||||
from ..utils import formatText
|
||||
from ..assembler import ASM
|
||||
|
||||
|
||||
class ShopItem(ItemInfo):
|
||||
def __init__(self, index):
|
||||
self.__index = index
|
||||
# pass in the alternate index for shop 2
|
||||
# The "real" room is at 0x2A1, but we store the second item data as if link were in 0x2A7
|
||||
room = 0x2A1
|
||||
if index == 1:
|
||||
room = 0x2A7
|
||||
super().__init__(room)
|
||||
|
||||
def patch(self, rom, option, *, multiworld=None):
|
||||
mw_text = ""
|
||||
if multiworld:
|
||||
mw_text = f" for player {rom.player_names[multiworld - 1]}"
|
||||
|
||||
if self.__index == 0:
|
||||
# Old index, maybe not needed any more
|
||||
rom.patch(0x04, 0x37C5, "08", "%02X" % (CHEST_ITEMS[option]))
|
||||
rom.texts[0x030] = formatText(f"Deluxe {{%s}} 200 {{RUPEES}}{mw_text}!" % (option), ask="Buy No Way")
|
||||
rom.banks[0x3E][0x3800 + 0x2A1] = CHEST_ITEMS[option]
|
||||
if multiworld:
|
||||
rom.banks[0x3E][0x3300 + 0x2A1] = multiworld
|
||||
elif self.__index == 1:
|
||||
rom.patch(0x04, 0x37C6, "02", "%02X" % (CHEST_ITEMS[option]))
|
||||
rom.texts[0x02C] = formatText(f"{{%s}} Only 980 {{RUPEES}}{mw_text}!" % (option), ask="Buy No Way")
|
||||
|
||||
rom.banks[0x3E][0x3800 + 0x2A7] = CHEST_ITEMS[option]
|
||||
if multiworld:
|
||||
rom.banks[0x3E][0x3300 + 0x2A7] = multiworld
|
||||
|
||||
def read(self, rom):
|
||||
value = rom.banks[0x04][0x37C5 + self.__index]
|
||||
for k, v in CHEST_ITEMS.items():
|
||||
if v == value:
|
||||
return k
|
||||
raise ValueError("Could not find shop item contents in ROM (0x%02x)" % (value))
|
|
@ -0,0 +1,5 @@
|
|||
from .droppedKey import DroppedKey
|
||||
|
||||
|
||||
class Song(DroppedKey):
|
||||
pass
|
|
@ -0,0 +1,38 @@
|
|||
from .itemInfo import ItemInfo
|
||||
from .constants import *
|
||||
from .droppedKey import DroppedKey
|
||||
from ..assembler import ASM
|
||||
from ..utils import formatText
|
||||
from ..roomEditor import RoomEditor
|
||||
|
||||
|
||||
class StartItem(DroppedKey):
|
||||
# We need to give something here that we can use to progress.
|
||||
# FEATHER
|
||||
OPTIONS = [SWORD, SHIELD, POWER_BRACELET, OCARINA, BOOMERANG, MAGIC_ROD, TAIL_KEY, SHOVEL, HOOKSHOT, PEGASUS_BOOTS, MAGIC_POWDER, BOMB]
|
||||
|
||||
MULTIWORLD = False
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(0x2A3)
|
||||
self.give_bowwow = False
|
||||
|
||||
def configure(self, options):
|
||||
if options.bowwow != 'normal':
|
||||
# When we have bowwow mode, we pretend to be a sword for logic reasons
|
||||
self.OPTIONS = [SWORD]
|
||||
self.give_bowwow = True
|
||||
if options.randomstartlocation and options.entranceshuffle != 'none':
|
||||
self.OPTIONS.append(FLIPPERS)
|
||||
|
||||
def patch(self, rom, option, *, multiworld=None):
|
||||
assert multiworld is None
|
||||
|
||||
if self.give_bowwow:
|
||||
option = BOWWOW
|
||||
rom.texts[0xC8] = formatText("Got BowWow!")
|
||||
|
||||
if option != SHIELD:
|
||||
rom.patch(5, 0x0CDA, ASM("ld a, $22"), ASM("ld a, $00")) # do not change links sprite into the one with a shield
|
||||
|
||||
super().patch(rom, option)
|
|
@ -0,0 +1,18 @@
|
|||
from .droppedKey import DroppedKey
|
||||
from .items import *
|
||||
|
||||
|
||||
class Toadstool(DroppedKey):
|
||||
def __init__(self):
|
||||
super().__init__(0x050)
|
||||
|
||||
def configure(self, options):
|
||||
if not options.witch:
|
||||
self.OPTIONS = [TOADSTOOL]
|
||||
else:
|
||||
super().configure(options)
|
||||
|
||||
def read(self, rom):
|
||||
if len(self.OPTIONS) == 1:
|
||||
return TOADSTOOL
|
||||
return super().read(rom)
|
|
@ -0,0 +1,55 @@
|
|||
from .itemInfo import ItemInfo
|
||||
from .constants import *
|
||||
from .droppedKey import DroppedKey
|
||||
|
||||
TradeRequirements = {
|
||||
TRADING_ITEM_YOSHI_DOLL: None,
|
||||
TRADING_ITEM_RIBBON: TRADING_ITEM_YOSHI_DOLL,
|
||||
TRADING_ITEM_DOG_FOOD: TRADING_ITEM_RIBBON,
|
||||
TRADING_ITEM_BANANAS: TRADING_ITEM_DOG_FOOD,
|
||||
TRADING_ITEM_STICK: TRADING_ITEM_BANANAS,
|
||||
TRADING_ITEM_HONEYCOMB: TRADING_ITEM_STICK,
|
||||
TRADING_ITEM_PINEAPPLE: TRADING_ITEM_HONEYCOMB,
|
||||
TRADING_ITEM_HIBISCUS: TRADING_ITEM_PINEAPPLE,
|
||||
TRADING_ITEM_LETTER: TRADING_ITEM_HIBISCUS,
|
||||
TRADING_ITEM_BROOM: TRADING_ITEM_LETTER,
|
||||
TRADING_ITEM_FISHING_HOOK: TRADING_ITEM_BROOM,
|
||||
TRADING_ITEM_NECKLACE: TRADING_ITEM_FISHING_HOOK,
|
||||
TRADING_ITEM_SCALE: TRADING_ITEM_NECKLACE,
|
||||
TRADING_ITEM_MAGNIFYING_GLASS: TRADING_ITEM_SCALE,
|
||||
}
|
||||
class TradeSequenceItem(DroppedKey):
|
||||
def __init__(self, room, default_item):
|
||||
self.unadjusted_room = room
|
||||
if room == 0x2B2:
|
||||
# Offset room for trade items to avoid collisions
|
||||
roomLo = room & 0xFF
|
||||
roomHi = room ^ roomLo
|
||||
roomLo = (roomLo + 2) & 0xFF
|
||||
room = roomHi | roomLo
|
||||
super().__init__(room)
|
||||
self.default_item = default_item
|
||||
|
||||
def configure(self, options):
|
||||
if not options.tradequest:
|
||||
self.OPTIONS = [self.default_item]
|
||||
super().configure(options)
|
||||
|
||||
#def patch(self, rom, option, *, multiworld=None):
|
||||
# rom.banks[0x3E][self.room + 0x3B16] = CHEST_ITEMS[option]
|
||||
|
||||
def read(self, rom):
|
||||
assert(False)
|
||||
assert self._location is not None, hex(self.room)
|
||||
value = rom.banks[0x3E][self.room + 0x3B16]
|
||||
for k, v in CHEST_ITEMS.items():
|
||||
if v == value:
|
||||
return k
|
||||
raise ValueError("Could not find owl statue contents in ROM (0x%02x)" % (value))
|
||||
|
||||
def __repr__(self):
|
||||
return "%s:%03x" % (self.__class__.__name__, self.room)
|
||||
|
||||
@property
|
||||
def nameId(self):
|
||||
return "0x%03X-Trade" % self.unadjusted_room
|
|
@ -0,0 +1,27 @@
|
|||
from .itemInfo import ItemInfo
|
||||
from .constants import *
|
||||
|
||||
|
||||
class TunicFairy(ItemInfo):
|
||||
|
||||
def __init__(self, index):
|
||||
self.index = index
|
||||
super().__init__(0x301)
|
||||
|
||||
def patch(self, rom, option, *, multiworld=None):
|
||||
# Old index, maybe not needed anymore
|
||||
rom.banks[0x36][0x11BF + self.index] = CHEST_ITEMS[option]
|
||||
rom.banks[0x3e][0x3800 + 0x301 + self.index*3] = CHEST_ITEMS[option]
|
||||
if multiworld:
|
||||
rom.banks[0x3e][0x3300 + 0x301 + self.index*3] = multiworld
|
||||
|
||||
def read(self, rom):
|
||||
value = rom.banks[0x36][0x11BF + self.index]
|
||||
for k, v in CHEST_ITEMS.items():
|
||||
if v == value:
|
||||
return k
|
||||
raise ValueError("Could not find tunic fairy contents in ROM (0x%02x)" % (value))
|
||||
|
||||
@property
|
||||
def nameId(self):
|
||||
return "0x%03X-%s" % (self.room, self.index)
|
|
@ -0,0 +1,31 @@
|
|||
from .constants import *
|
||||
from .itemInfo import ItemInfo
|
||||
|
||||
|
||||
class Witch(ItemInfo):
|
||||
def __init__(self):
|
||||
super().__init__(0x2A2)
|
||||
|
||||
def configure(self, options):
|
||||
if not options.witch:
|
||||
self.OPTIONS = [MAGIC_POWDER]
|
||||
|
||||
def patch(self, rom, option, *, multiworld=None):
|
||||
if multiworld or option != MAGIC_POWDER:
|
||||
|
||||
rom.banks[0x3E][self.room + 0x3800] = CHEST_ITEMS[option]
|
||||
if multiworld is not None:
|
||||
rom.banks[0x3E][0x3300 + self.room] = multiworld
|
||||
else:
|
||||
rom.banks[0x3E][0x3300 + self.room] = 0
|
||||
|
||||
#rom.patch(0x05, 0x08D5, "09", "%02x" % (CHEST_ITEMS[option]))
|
||||
|
||||
def read(self, rom):
|
||||
if rom.banks[0x05][0x08EF] != 0x00:
|
||||
return MAGIC_POWDER
|
||||
value = rom.banks[0x05][0x08D5]
|
||||
for k, v in CHEST_ITEMS.items():
|
||||
if v == value:
|
||||
return k
|
||||
raise ValueError("Could not find witch contents in ROM (0x%02x)" % (value))
|
|
@ -0,0 +1,284 @@
|
|||
from . import overworld
|
||||
from . import dungeon1
|
||||
from . import dungeon2
|
||||
from . import dungeon3
|
||||
from . import dungeon4
|
||||
from . import dungeon5
|
||||
from . import dungeon6
|
||||
from . import dungeon7
|
||||
from . import dungeon8
|
||||
from . import dungeonColor
|
||||
from .requirements import AND, OR, COUNT, COUNTS, FOUND, RequirementsSettings
|
||||
from .location import Location
|
||||
from ..locations.items import *
|
||||
from ..locations.keyLocation import KeyLocation
|
||||
from ..worldSetup import WorldSetup
|
||||
from .. import itempool
|
||||
from .. import mapgen
|
||||
|
||||
|
||||
class Logic:
|
||||
def __init__(self, configuration_options, *, world_setup):
|
||||
self.world_setup = world_setup
|
||||
r = RequirementsSettings(configuration_options)
|
||||
|
||||
if configuration_options.overworld == "dungeondive":
|
||||
world = overworld.DungeonDiveOverworld(configuration_options, r)
|
||||
elif configuration_options.overworld == "random":
|
||||
world = mapgen.LogicGenerator(configuration_options, world_setup, r, world_setup.map)
|
||||
else:
|
||||
world = overworld.World(configuration_options, world_setup, r)
|
||||
|
||||
if configuration_options.overworld == "nodungeons":
|
||||
world.updateIndoorLocation("d1", dungeon1.NoDungeon1(configuration_options, world_setup, r).entrance)
|
||||
world.updateIndoorLocation("d2", dungeon2.NoDungeon2(configuration_options, world_setup, r).entrance)
|
||||
world.updateIndoorLocation("d3", dungeon3.NoDungeon3(configuration_options, world_setup, r).entrance)
|
||||
world.updateIndoorLocation("d4", dungeon4.NoDungeon4(configuration_options, world_setup, r).entrance)
|
||||
world.updateIndoorLocation("d5", dungeon5.NoDungeon5(configuration_options, world_setup, r).entrance)
|
||||
world.updateIndoorLocation("d6", dungeon6.NoDungeon6(configuration_options, world_setup, r).entrance)
|
||||
world.updateIndoorLocation("d7", dungeon7.NoDungeon7(configuration_options, world_setup, r).entrance)
|
||||
world.updateIndoorLocation("d8", dungeon8.NoDungeon8(configuration_options, world_setup, r).entrance)
|
||||
world.updateIndoorLocation("d0", dungeonColor.NoDungeonColor(configuration_options, world_setup, r).entrance)
|
||||
elif configuration_options.overworld != "random":
|
||||
world.updateIndoorLocation("d1", dungeon1.Dungeon1(configuration_options, world_setup, r).entrance)
|
||||
world.updateIndoorLocation("d2", dungeon2.Dungeon2(configuration_options, world_setup, r).entrance)
|
||||
world.updateIndoorLocation("d3", dungeon3.Dungeon3(configuration_options, world_setup, r).entrance)
|
||||
world.updateIndoorLocation("d4", dungeon4.Dungeon4(configuration_options, world_setup, r).entrance)
|
||||
world.updateIndoorLocation("d5", dungeon5.Dungeon5(configuration_options, world_setup, r).entrance)
|
||||
world.updateIndoorLocation("d6", dungeon6.Dungeon6(configuration_options, world_setup, r).entrance)
|
||||
world.updateIndoorLocation("d7", dungeon7.Dungeon7(configuration_options, world_setup, r).entrance)
|
||||
world.updateIndoorLocation("d8", dungeon8.Dungeon8(configuration_options, world_setup, r).entrance)
|
||||
world.updateIndoorLocation("d0", dungeonColor.DungeonColor(configuration_options, world_setup, r).entrance)
|
||||
|
||||
if configuration_options.overworld != "random":
|
||||
for k in world.overworld_entrance.keys():
|
||||
assert k in world_setup.entrance_mapping, k
|
||||
for k in world_setup.entrance_mapping.keys():
|
||||
assert k in world.overworld_entrance, k
|
||||
|
||||
for entrance, indoor in world_setup.entrance_mapping.items():
|
||||
exterior = world.overworld_entrance[entrance]
|
||||
if world.indoor_location[indoor] is not None:
|
||||
exterior.location.connect(world.indoor_location[indoor], exterior.requirement)
|
||||
if exterior.enterIsSet():
|
||||
exterior.location.connect(world.indoor_location[indoor], exterior.one_way_enter_requirement, one_way=True)
|
||||
if exterior.exitIsSet():
|
||||
world.indoor_location[indoor].connect(exterior.location, exterior.one_way_exit_requirement, one_way=True)
|
||||
|
||||
egg_trigger = AND(OCARINA, SONG1)
|
||||
if configuration_options.logic == 'glitched' or configuration_options.logic == 'hell':
|
||||
egg_trigger = OR(AND(OCARINA, SONG1), BOMB)
|
||||
|
||||
if world_setup.goal == "seashells":
|
||||
world.nightmare.connect(world.egg, COUNT(SEASHELL, 20))
|
||||
elif world_setup.goal in ("raft", "bingo", "bingo-full"):
|
||||
world.nightmare.connect(world.egg, egg_trigger)
|
||||
else:
|
||||
goal = int(world_setup.goal)
|
||||
if goal < 0:
|
||||
world.nightmare.connect(world.egg, None)
|
||||
elif goal == 0:
|
||||
world.nightmare.connect(world.egg, egg_trigger)
|
||||
elif goal == 8:
|
||||
world.nightmare.connect(world.egg, AND(egg_trigger, INSTRUMENT1, INSTRUMENT2, INSTRUMENT3, INSTRUMENT4, INSTRUMENT5, INSTRUMENT6, INSTRUMENT7, INSTRUMENT8))
|
||||
else:
|
||||
world.nightmare.connect(world.egg, AND(egg_trigger, COUNTS([INSTRUMENT1, INSTRUMENT2, INSTRUMENT3, INSTRUMENT4, INSTRUMENT5, INSTRUMENT6, INSTRUMENT7, INSTRUMENT8], goal)))
|
||||
|
||||
# if configuration_options.dungeon_items == 'keysy':
|
||||
# for n in range(9):
|
||||
# for count in range(9):
|
||||
# world.start.add(KeyLocation("KEY%d" % (n + 1)))
|
||||
# world.start.add(KeyLocation("NIGHTMARE_KEY%d" % (n + 1)))
|
||||
|
||||
self.world = world
|
||||
self.start = world.start
|
||||
self.windfish = world.windfish
|
||||
self.location_list = []
|
||||
self.iteminfo_list = []
|
||||
|
||||
self.__location_set = set()
|
||||
self.__recursiveFindAll(self.start)
|
||||
del self.__location_set
|
||||
|
||||
for ii in self.iteminfo_list:
|
||||
ii.configure(configuration_options)
|
||||
|
||||
def dumpFlatRequirements(self):
|
||||
def __rec(location, req):
|
||||
if hasattr(location, "flat_requirements"):
|
||||
new_flat_requirements = requirements.mergeFlat(location.flat_requirements, requirements.flatten(req))
|
||||
if new_flat_requirements == location.flat_requirements:
|
||||
return
|
||||
location.flat_requirements = new_flat_requirements
|
||||
else:
|
||||
location.flat_requirements = requirements.flatten(req)
|
||||
for connection, requirement in location.simple_connections:
|
||||
__rec(connection, AND(req, requirement) if req else requirement)
|
||||
for connection, requirement in location.gated_connections:
|
||||
__rec(connection, AND(req, requirement) if req else requirement)
|
||||
__rec(self.start, None)
|
||||
for ii in self.iteminfo_list:
|
||||
print(ii)
|
||||
for fr in ii._location.flat_requirements:
|
||||
print(" " + ", ".join(sorted(map(str, fr))))
|
||||
|
||||
def __recursiveFindAll(self, location):
|
||||
if location in self.__location_set:
|
||||
return
|
||||
self.location_list.append(location)
|
||||
self.__location_set.add(location)
|
||||
for ii in location.items:
|
||||
self.iteminfo_list.append(ii)
|
||||
for connection, requirement in location.simple_connections:
|
||||
self.__recursiveFindAll(connection)
|
||||
for connection, requirement in location.gated_connections:
|
||||
self.__recursiveFindAll(connection)
|
||||
|
||||
|
||||
class MultiworldLogic:
|
||||
def __init__(self, settings, rnd=None, *, world_setups=None):
|
||||
assert rnd or world_setups
|
||||
self.worlds = []
|
||||
self.start = Location()
|
||||
self.location_list = [self.start]
|
||||
self.iteminfo_list = []
|
||||
|
||||
for n in range(settings.multiworld):
|
||||
options = settings.multiworld_settings[n]
|
||||
world = None
|
||||
if world_setups:
|
||||
world = Logic(options, world_setup=world_setups[n])
|
||||
else:
|
||||
for cnt in range(1000): # Try the world setup in case entrance randomization generates unsolvable logic
|
||||
world_setup = WorldSetup()
|
||||
world_setup.randomize(options, rnd)
|
||||
world = Logic(options, world_setup=world_setup)
|
||||
if options.entranceshuffle not in ("advanced", "expert", "insanity") or len(world.iteminfo_list) == sum(itempool.ItemPool(options, rnd).toDict().values()):
|
||||
break
|
||||
|
||||
for ii in world.iteminfo_list:
|
||||
ii.world = n
|
||||
|
||||
req_done_set = set()
|
||||
for loc in world.location_list:
|
||||
loc.simple_connections = [(target, addWorldIdToRequirements(req_done_set, n, req)) for target, req in loc.simple_connections]
|
||||
loc.gated_connections = [(target, addWorldIdToRequirements(req_done_set, n, req)) for target, req in loc.gated_connections]
|
||||
loc.items = [MultiworldItemInfoWrapper(n, options, ii) for ii in loc.items]
|
||||
self.iteminfo_list += loc.items
|
||||
|
||||
self.worlds.append(world)
|
||||
self.start.simple_connections += world.start.simple_connections
|
||||
self.start.gated_connections += world.start.gated_connections
|
||||
self.start.items += world.start.items
|
||||
world.start.items.clear()
|
||||
self.location_list += world.location_list
|
||||
|
||||
self.entranceMapping = None
|
||||
|
||||
|
||||
class MultiworldMetadataWrapper:
|
||||
def __init__(self, world, metadata):
|
||||
self.world = world
|
||||
self.metadata = metadata
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.metadata.name
|
||||
|
||||
@property
|
||||
def area(self):
|
||||
return "P%d %s" % (self.world + 1, self.metadata.area)
|
||||
|
||||
|
||||
class MultiworldItemInfoWrapper:
|
||||
def __init__(self, world, configuration_options, target):
|
||||
self.world = world
|
||||
self.world_count = configuration_options.multiworld
|
||||
self.target = target
|
||||
self.dungeon_items = configuration_options.dungeon_items
|
||||
self.MULTIWORLD_OPTIONS = None
|
||||
self.item = None
|
||||
|
||||
@property
|
||||
def nameId(self):
|
||||
return self.target.nameId
|
||||
|
||||
@property
|
||||
def forced_item(self):
|
||||
if self.target.forced_item is None:
|
||||
return None
|
||||
if "_W" in self.target.forced_item:
|
||||
return self.target.forced_item
|
||||
return "%s_W%d" % (self.target.forced_item, self.world)
|
||||
|
||||
@property
|
||||
def room(self):
|
||||
return self.target.room
|
||||
|
||||
@property
|
||||
def metadata(self):
|
||||
return MultiworldMetadataWrapper(self.world, self.target.metadata)
|
||||
|
||||
@property
|
||||
def MULTIWORLD(self):
|
||||
return self.target.MULTIWORLD
|
||||
|
||||
def read(self, rom):
|
||||
world = rom.banks[0x3E][0x3300 + self.target.room] if self.target.MULTIWORLD else self.world
|
||||
return "%s_W%d" % (self.target.read(rom), world)
|
||||
|
||||
def getOptions(self):
|
||||
if self.MULTIWORLD_OPTIONS is None:
|
||||
options = self.target.getOptions()
|
||||
if self.target.MULTIWORLD and len(options) > 1:
|
||||
self.MULTIWORLD_OPTIONS = []
|
||||
for n in range(self.world_count):
|
||||
self.MULTIWORLD_OPTIONS += ["%s_W%d" % (t, n) for t in options if n == self.world or self.canMultiworld(t)]
|
||||
else:
|
||||
self.MULTIWORLD_OPTIONS = ["%s_W%d" % (t, self.world) for t in options]
|
||||
return self.MULTIWORLD_OPTIONS
|
||||
|
||||
def patch(self, rom, option):
|
||||
idx = option.rfind("_W")
|
||||
world = int(option[idx+2:])
|
||||
option = option[:idx]
|
||||
if not self.target.MULTIWORLD:
|
||||
assert self.world == world
|
||||
self.target.patch(rom, option)
|
||||
else:
|
||||
self.target.patch(rom, option, multiworld=world)
|
||||
|
||||
# Return true if the item is allowed to be placed in any world, or false if it is
|
||||
# world specific for this check.
|
||||
def canMultiworld(self, option):
|
||||
if self.dungeon_items in {'', 'smallkeys'}:
|
||||
if option.startswith("MAP"):
|
||||
return False
|
||||
if option.startswith("COMPASS"):
|
||||
return False
|
||||
if option.startswith("STONE_BEAK"):
|
||||
return False
|
||||
if self.dungeon_items in {'', 'localkeys'}:
|
||||
if option.startswith("KEY"):
|
||||
return False
|
||||
if self.dungeon_items in {'', 'localkeys', 'localnightmarekey', 'smallkeys'}:
|
||||
if option.startswith("NIGHTMARE_KEY"):
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
return self.target.location
|
||||
|
||||
def __repr__(self):
|
||||
return "W%d:%s" % (self.world, repr(self.target))
|
||||
|
||||
|
||||
def addWorldIdToRequirements(req_done_set, world, req):
|
||||
if req is None:
|
||||
return None
|
||||
if isinstance(req, str):
|
||||
return "%s_W%d" % (req, world)
|
||||
if req in req_done_set:
|
||||
return req
|
||||
return req.copyWithModifiedItemNames(lambda item: "%s_W%d" % (item, world))
|
|
@ -0,0 +1,46 @@
|
|||
from .requirements import *
|
||||
from .location import Location
|
||||
from ..locations.all import *
|
||||
|
||||
|
||||
class Dungeon1:
|
||||
def __init__(self, options, world_setup, r):
|
||||
entrance = Location(dungeon=1)
|
||||
entrance.add(DungeonChest(0x113), DungeonChest(0x115), DungeonChest(0x10E))
|
||||
Location(dungeon=1).add(DroppedKey(0x116)).connect(entrance, OR(BOMB, r.push_hardhat)) # hardhat beetles (can kill with bomb)
|
||||
Location(dungeon=1).add(DungeonChest(0x10D)).connect(entrance, OR(r.attack_hookshot_powder, SHIELD)) # moldorm spawn chest
|
||||
stalfos_keese_room = Location(dungeon=1).add(DungeonChest(0x114)).connect(entrance, r.attack_hookshot) # 2 stalfos 2 keese room
|
||||
Location(dungeon=1).add(DungeonChest(0x10C)).connect(entrance, BOMB) # hidden seashell room
|
||||
dungeon1_upper_left = Location(dungeon=1).connect(entrance, AND(KEY1, FOUND(KEY1, 3)))
|
||||
if options.owlstatues == "both" or options.owlstatues == "dungeon":
|
||||
Location(dungeon=1).add(OwlStatue(0x103), OwlStatue(0x104)).connect(dungeon1_upper_left, STONE_BEAK1)
|
||||
feather_chest = Location(dungeon=1).add(DungeonChest(0x11D)).connect(dungeon1_upper_left, SHIELD) # feather location, behind spike enemies. can shield bump into pit (only shield works)
|
||||
boss_key = Location(dungeon=1).add(DungeonChest(0x108)).connect(entrance, AND(FEATHER, KEY1, FOUND(KEY1, 3))) # boss key
|
||||
dungeon1_right_side = Location(dungeon=1).connect(entrance, AND(KEY1, FOUND(KEY1, 3)))
|
||||
if options.owlstatues == "both" or options.owlstatues == "dungeon":
|
||||
Location(dungeon=1).add(OwlStatue(0x10A)).connect(dungeon1_right_side, STONE_BEAK1)
|
||||
Location(dungeon=1).add(DungeonChest(0x10A)).connect(dungeon1_right_side, OR(r.attack_hookshot, SHIELD)) # three of a kind, shield stops the suit from changing
|
||||
dungeon1_miniboss = Location(dungeon=1).connect(dungeon1_right_side, AND(r.miniboss_requirements[world_setup.miniboss_mapping[0]], FEATHER))
|
||||
dungeon1_boss = Location(dungeon=1).connect(dungeon1_miniboss, NIGHTMARE_KEY1)
|
||||
Location(dungeon=1).add(HeartContainer(0x106), Instrument(0x102)).connect(dungeon1_boss, r.boss_requirements[world_setup.boss_mapping[0]])
|
||||
|
||||
if options.logic not in ('normal', 'casual'):
|
||||
stalfos_keese_room.connect(entrance, r.attack_hookshot_powder) # stalfos jump away when you press a button.
|
||||
|
||||
if options.logic == 'glitched' or options.logic == 'hell':
|
||||
boss_key.connect(entrance, FEATHER) # super jump
|
||||
dungeon1_miniboss.connect(dungeon1_right_side, r.miniboss_requirements[world_setup.miniboss_mapping[0]]) # damage boost or buffer pause over the pit to cross or mushroom
|
||||
|
||||
if options.logic == 'hell':
|
||||
feather_chest.connect(dungeon1_upper_left, SWORD) # keep slashing the spiked beetles until they keep moving 1 pixel close towards you and the pit, to get them to fall
|
||||
boss_key.connect(entrance, FOUND(KEY1,3)) # damage boost off the hardhat to cross the pit
|
||||
|
||||
self.entrance = entrance
|
||||
|
||||
|
||||
class NoDungeon1:
|
||||
def __init__(self, options, world_setup, r):
|
||||
entrance = Location(dungeon=1)
|
||||
Location(dungeon=1).add(HeartContainer(0x106), Instrument(0x102)).connect(entrance, r.boss_requirements[
|
||||
world_setup.boss_mapping[0]])
|
||||
self.entrance = entrance
|
|
@ -0,0 +1,62 @@
|
|||
from .requirements import *
|
||||
from .location import Location
|
||||
from ..locations.all import *
|
||||
|
||||
|
||||
class Dungeon2:
|
||||
def __init__(self, options, world_setup, r):
|
||||
entrance = Location(dungeon=2)
|
||||
Location(dungeon=2).add(DungeonChest(0x136)).connect(entrance, POWER_BRACELET) # chest at entrance
|
||||
dungeon2_l2 = Location(dungeon=2).connect(entrance, AND(KEY2, FOUND(KEY2, 5))) # towards map chest
|
||||
dungeon2_map_chest = Location(dungeon=2).add(DungeonChest(0x12E)).connect(dungeon2_l2, AND(r.attack_hookshot_powder, OR(FEATHER, HOOKSHOT))) # map chest
|
||||
dungeon2_r2 = Location(dungeon=2).connect(entrance, r.fire)
|
||||
Location(dungeon=2).add(DroppedKey(0x132)).connect(dungeon2_r2, r.attack_skeleton)
|
||||
Location(dungeon=2).add(DungeonChest(0x137)).connect(dungeon2_r2, AND(KEY2, FOUND(KEY2, 5), OR(r.rear_attack, r.rear_attack_range))) # compass chest
|
||||
if options.owlstatues == "both" or options.owlstatues == "dungeon":
|
||||
Location(dungeon=2).add(OwlStatue(0x133)).connect(dungeon2_r2, STONE_BEAK2)
|
||||
dungeon2_r3 = Location(dungeon=2).add(DungeonChest(0x138)).connect(dungeon2_r2, r.attack_hookshot) # first chest with key, can hookshot the switch in previous room
|
||||
dungeon2_r4 = Location(dungeon=2).add(DungeonChest(0x139)).connect(dungeon2_r3, FEATHER) # button spawn chest
|
||||
if options.logic == "casual":
|
||||
shyguy_key_drop = Location(dungeon=2).add(DroppedKey(0x134)).connect(dungeon2_r3, AND(FEATHER, OR(r.rear_attack, r.rear_attack_range))) # shyguy drop key
|
||||
else:
|
||||
shyguy_key_drop = Location(dungeon=2).add(DroppedKey(0x134)).connect(dungeon2_r3, OR(r.rear_attack, AND(FEATHER, r.rear_attack_range))) # shyguy drop key
|
||||
dungeon2_r5 = Location(dungeon=2).connect(dungeon2_r4, AND(KEY2, FOUND(KEY2, 3))) # push two blocks together room with owl statue
|
||||
if options.owlstatues == "both" or options.owlstatues == "dungeon":
|
||||
Location(dungeon=2).add(OwlStatue(0x12F)).connect(dungeon2_r5, STONE_BEAK2) # owl statue is before miniboss
|
||||
miniboss = Location(dungeon=2).add(DungeonChest(0x126)).add(DungeonChest(0x121)).connect(dungeon2_r5, AND(FEATHER, r.miniboss_requirements[world_setup.miniboss_mapping[1]])) # post hinox
|
||||
if options.owlstatues == "both" or options.owlstatues == "dungeon":
|
||||
Location(dungeon=2).add(OwlStatue(0x129)).connect(miniboss, STONE_BEAK2) # owl statue after the miniboss
|
||||
|
||||
dungeon2_ghosts_room = Location(dungeon=2).connect(miniboss, AND(KEY2, FOUND(KEY2, 5)))
|
||||
dungeon2_ghosts_chest = Location(dungeon=2).add(DungeonChest(0x120)).connect(dungeon2_ghosts_room, OR(r.fire, BOW)) # bracelet chest
|
||||
dungeon2_r6 = Location(dungeon=2).add(DungeonChest(0x122)).connect(miniboss, POWER_BRACELET)
|
||||
dungeon2_boss_key = Location(dungeon=2).add(DungeonChest(0x127)).connect(dungeon2_r6, AND(r.attack_hookshot_powder, OR(BOW, BOMB, MAGIC_ROD, AND(OCARINA, SONG1), POWER_BRACELET)))
|
||||
dungeon2_pre_stairs_boss = Location(dungeon=2).connect(dungeon2_r6, AND(POWER_BRACELET, KEY2, FOUND(KEY2, 5)))
|
||||
dungeon2_post_stairs_boss = Location(dungeon=2).connect(dungeon2_pre_stairs_boss, POWER_BRACELET)
|
||||
dungeon2_pre_boss = Location(dungeon=2).connect(dungeon2_post_stairs_boss, FEATHER)
|
||||
# If we can get here, we have everything for the boss. So this is also the goal room.
|
||||
dungeon2_boss = Location(dungeon=2).add(HeartContainer(0x12B), Instrument(0x12a)).connect(dungeon2_pre_boss, AND(NIGHTMARE_KEY2, r.boss_requirements[world_setup.boss_mapping[1]]))
|
||||
|
||||
if options.logic == 'glitched' or options.logic == 'hell':
|
||||
dungeon2_ghosts_chest.connect(dungeon2_ghosts_room, SWORD) # use sword to spawn ghosts on other side of the room so they run away (logically irrelevant because of torches at start)
|
||||
dungeon2_r6.connect(miniboss, FEATHER) # superjump to staircase next to hinox.
|
||||
|
||||
if options.logic == 'hell':
|
||||
dungeon2_map_chest.connect(dungeon2_l2, AND(r.attack_hookshot_powder, PEGASUS_BOOTS)) # use boots to jump over the pits
|
||||
dungeon2_r4.connect(dungeon2_r3, OR(PEGASUS_BOOTS, HOOKSHOT)) # can use both pegasus boots bonks or hookshot spam to cross the pit room
|
||||
dungeon2_r4.connect(shyguy_key_drop, r.rear_attack_range, one_way=True) # adjust for alternate requirements for dungeon2_r4
|
||||
miniboss.connect(dungeon2_r5, AND(PEGASUS_BOOTS, r.miniboss_requirements[world_setup.miniboss_mapping[1]])) # use boots to dash over the spikes in the 2d section
|
||||
dungeon2_pre_stairs_boss.connect(dungeon2_r6, AND(HOOKSHOT, OR(BOW, BOMB, MAGIC_ROD, AND(OCARINA, SONG1)), FOUND(KEY2, 5))) # hookshot clip through the pot using both pol's voice
|
||||
dungeon2_post_stairs_boss.connect(dungeon2_pre_stairs_boss, OR(BOMB, AND(PEGASUS_BOOTS, FEATHER))) # use a bomb to lower the last platform, or boots + feather to cross over top (only relevant in hell logic)
|
||||
dungeon2_pre_boss.connect(dungeon2_post_stairs_boss, AND(PEGASUS_BOOTS, HOOKSHOT)) # boots bonk off bottom wall + hookshot spam across the two 1 tile pits vertically
|
||||
|
||||
self.entrance = entrance
|
||||
|
||||
|
||||
class NoDungeon2:
|
||||
def __init__(self, options, world_setup, r):
|
||||
entrance = Location(dungeon=2)
|
||||
Location(dungeon=2).add(DungeonChest(0x136)).connect(entrance, POWER_BRACELET) # chest at entrance
|
||||
Location(dungeon=2).add(HeartContainer(0x12B), Instrument(0x12a)).connect(entrance, r.boss_requirements[
|
||||
world_setup.boss_mapping[1]])
|
||||
self.entrance = entrance
|
|
@ -0,0 +1,89 @@
|
|||
from .requirements import *
|
||||
from .location import Location
|
||||
from ..locations.all import *
|
||||
|
||||
|
||||
class Dungeon3:
|
||||
def __init__(self, options, world_setup, r):
|
||||
entrance = Location(dungeon=3)
|
||||
dungeon3_reverse_eye = Location(dungeon=3).add(DungeonChest(0x153)).connect(entrance, PEGASUS_BOOTS) # Right side reverse eye
|
||||
area2 = Location(dungeon=3).connect(entrance, POWER_BRACELET)
|
||||
Location(dungeon=3).add(DungeonChest(0x151)).connect(area2, r.attack_hookshot_powder) # First chest with key
|
||||
area2.add(DungeonChest(0x14F)) # Second chest with slime
|
||||
area3 = Location(dungeon=3).connect(area2, OR(r.attack_hookshot_powder, PEGASUS_BOOTS)) # need to kill slimes to continue or pass through left path
|
||||
dungeon3_zol_stalfos = Location(dungeon=3).add(DungeonChest(0x14E)).connect(area3, AND(PEGASUS_BOOTS, r.attack_skeleton)) # 3th chest requires killing the slime behind the crystal pillars
|
||||
|
||||
# now we can go 4 directions,
|
||||
area_up = Location(dungeon=3).connect(area3, AND(KEY3, FOUND(KEY3, 8)))
|
||||
dungeon3_north_key_drop = Location(dungeon=3).add(DroppedKey(0x154)).connect(area_up, r.attack_skeleton) # north key drop
|
||||
if options.owlstatues == "both" or options.owlstatues == "dungeon":
|
||||
Location(dungeon=3).add(OwlStatue(0x154)).connect(area_up, STONE_BEAK3)
|
||||
dungeon3_raised_blocks_north = Location(dungeon=3).add(DungeonChest(0x14C)) # chest locked behind raised blocks near staircase
|
||||
dungeon3_raised_blocks_east = Location(dungeon=3).add(DungeonChest(0x150)) # chest locked behind raised blocks next to slime chest
|
||||
area_up.connect(dungeon3_raised_blocks_north, r.attack_hookshot, one_way=True) # hit switch to reach north chest
|
||||
area_up.connect(dungeon3_raised_blocks_east, r.attack_hookshot, one_way=True) # hit switch to reach east chest
|
||||
|
||||
area_left = Location(dungeon=3).connect(area3, AND(KEY3, FOUND(KEY3, 8)))
|
||||
area_left_key_drop = Location(dungeon=3).add(DroppedKey(0x155)).connect(area_left, r.attack_hookshot) # west key drop (no longer requires feather to get across hole), can use boomerang to knock owls into pit
|
||||
|
||||
area_down = Location(dungeon=3).connect(area3, AND(KEY3, FOUND(KEY3, 8)))
|
||||
dungeon3_south_key_drop = Location(dungeon=3).add(DroppedKey(0x158)).connect(area_down, r.attack_hookshot) # south keydrop, can use boomerang to knock owls into pit
|
||||
|
||||
area_right = Location(dungeon=3).connect(area3, AND(KEY3, FOUND(KEY3, 4))) # We enter the top part of the map here.
|
||||
Location(dungeon=3).add(DroppedKey(0x14D)).connect(area_right, r.attack_hookshot_powder) # key after the stairs.
|
||||
|
||||
dungeon3_nightmare_key_chest = Location(dungeon=3).add(DungeonChest(0x147)).connect(area_right, AND(BOMB, FEATHER, PEGASUS_BOOTS)) # nightmare key chest
|
||||
dungeon3_post_dodongo_chest = Location(dungeon=3).add(DungeonChest(0x146)).connect(area_right, AND(r.attack_hookshot_powder, r.miniboss_requirements[world_setup.miniboss_mapping[2]])) # boots after the miniboss
|
||||
compass_chest = Location(dungeon=3).add(DungeonChest(0x142)).connect(area_right, OR(SWORD, BOMB, AND(SHIELD, r.attack_hookshot_powder))) # bomb only activates with sword, bomb or shield
|
||||
dungeon3_3_bombite_room = Location(dungeon=3).add(DroppedKey(0x141)).connect(compass_chest, BOMB) # 3 bombite room
|
||||
Location(dungeon=3).add(DroppedKey(0x148)).connect(area_right, r.attack_no_boomerang) # 2 zol 2 owl drop key
|
||||
Location(dungeon=3).add(DungeonChest(0x144)).connect(area_right, r.attack_skeleton) # map chest
|
||||
if options.owlstatues == "both" or options.owlstatues == "dungeon":
|
||||
Location(dungeon=3).add(OwlStatue(0x140), OwlStatue(0x147)).connect(area_right, STONE_BEAK3)
|
||||
|
||||
towards_boss1 = Location(dungeon=3).connect(area_right, AND(KEY3, FOUND(KEY3, 5)))
|
||||
towards_boss2 = Location(dungeon=3).connect(towards_boss1, AND(KEY3, FOUND(KEY3, 6)))
|
||||
towards_boss3 = Location(dungeon=3).connect(towards_boss2, AND(KEY3, FOUND(KEY3, 7)))
|
||||
towards_boss4 = Location(dungeon=3).connect(towards_boss3, AND(KEY3, FOUND(KEY3, 8)))
|
||||
|
||||
# Just the whole area before the boss, requirements for the boss itself and the rooms before it are the same.
|
||||
pre_boss = Location(dungeon=3).connect(towards_boss4, AND(r.attack_no_boomerang, FEATHER, PEGASUS_BOOTS))
|
||||
pre_boss.add(DroppedKey(0x15B))
|
||||
|
||||
boss = Location(dungeon=3).add(HeartContainer(0x15A), Instrument(0x159)).connect(pre_boss, AND(NIGHTMARE_KEY3, r.boss_requirements[world_setup.boss_mapping[2]]))
|
||||
|
||||
if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell':
|
||||
dungeon3_3_bombite_room.connect(area_right, BOOMERANG) # 3 bombite room from the left side, grab item with boomerang
|
||||
dungeon3_reverse_eye.connect(entrance, HOOKSHOT) # hookshot the chest to get to the right side
|
||||
dungeon3_north_key_drop.connect(area_up, POWER_BRACELET) # use pots to kill the enemies
|
||||
dungeon3_south_key_drop.connect(area_down, POWER_BRACELET) # use pots to kill enemies
|
||||
|
||||
if options.logic == 'glitched' or options.logic == 'hell':
|
||||
area2.connect(dungeon3_raised_blocks_east, AND(r.attack_hookshot_powder, FEATHER), one_way=True) # use superjump to get over the bottom left block
|
||||
area3.connect(dungeon3_raised_blocks_north, AND(OR(PEGASUS_BOOTS, HOOKSHOT), FEATHER), one_way=True) # use shagjump (unclipped superjump next to movable block) from north wall to get on the blocks. Instead of boots can also get to that area with a hookshot clip past the movable block
|
||||
area3.connect(dungeon3_zol_stalfos, HOOKSHOT, one_way=True) # hookshot clip through the northern push block next to raised blocks chest to get to the zol
|
||||
dungeon3_nightmare_key_chest.connect(area_right, AND(FEATHER, BOMB)) # superjump to right side 3 gap via top wall and jump the 2 gap
|
||||
dungeon3_post_dodongo_chest.connect(area_right, AND(FEATHER, FOUND(KEY3, 6))) # superjump from keyblock path. use 2 keys to open enough blocks TODO: nag messages to skip a key
|
||||
|
||||
if options.logic == 'hell':
|
||||
area2.connect(dungeon3_raised_blocks_east, AND(PEGASUS_BOOTS, OR(BOW, MAGIC_ROD)), one_way=True) # use boots superhop to get over the bottom left block
|
||||
area3.connect(dungeon3_raised_blocks_north, AND(PEGASUS_BOOTS, OR(BOW, MAGIC_ROD)), one_way=True) # use boots superhop off top wall or left wall to get on raised blocks
|
||||
area_up.connect(dungeon3_zol_stalfos, AND(FEATHER, OR(BOW, MAGIC_ROD, SWORD)), one_way=True) # use superjump near top blocks chest to get to zol without boots, keep wall clip on right wall to get a clip on left wall or use obstacles
|
||||
area_left_key_drop.connect(area_left, SHIELD) # knock everything into the pit including the teleporting owls
|
||||
dungeon3_south_key_drop.connect(area_down, SHIELD) # knock everything into the pit including the teleporting owls
|
||||
dungeon3_nightmare_key_chest.connect(area_right, AND(FEATHER, SHIELD)) # superjump into jumping stalfos and shield bump to right ledge
|
||||
dungeon3_nightmare_key_chest.connect(area_right, AND(BOMB, PEGASUS_BOOTS, HOOKSHOT)) # boots bonk across the pits with pit buffering and hookshot to the chest
|
||||
compass_chest.connect(dungeon3_3_bombite_room, OR(BOW, MAGIC_ROD, AND(OR(FEATHER, PEGASUS_BOOTS), OR(SWORD, MAGIC_POWDER))), one_way=True) # 3 bombite room from the left side, use a bombite to blow open the wall without bombs
|
||||
pre_boss.connect(towards_boss4, AND(r.attack_no_boomerang, FEATHER, POWER_BRACELET)) # use bracelet super bounce glitch to pass through first part underground section
|
||||
pre_boss.connect(towards_boss4, AND(r.attack_no_boomerang, PEGASUS_BOOTS, "MEDICINE2")) # use medicine invulnerability to pass through the 2d section with a boots bonk to reach the staircase
|
||||
|
||||
self.entrance = entrance
|
||||
|
||||
|
||||
class NoDungeon3:
|
||||
def __init__(self, options, world_setup, r):
|
||||
entrance = Location(dungeon=3)
|
||||
Location(dungeon=3).add(HeartContainer(0x15A), Instrument(0x159)).connect(entrance, AND(POWER_BRACELET, r.boss_requirements[
|
||||
world_setup.boss_mapping[2]]))
|
||||
|
||||
self.entrance = entrance
|
|
@ -0,0 +1,81 @@
|
|||
from .requirements import *
|
||||
from .location import Location
|
||||
from ..locations.all import *
|
||||
|
||||
|
||||
class Dungeon4:
|
||||
def __init__(self, options, world_setup, r):
|
||||
entrance = Location(dungeon=4)
|
||||
entrance.add(DungeonChest(0x179)) # stone slab chest
|
||||
entrance.add(DungeonChest(0x16A)) # map chest
|
||||
right_of_entrance = Location(dungeon=4).add(DungeonChest(0x178)).connect(entrance, AND(SHIELD, r.attack_hookshot_powder)) # 1 zol 2 spike beetles 1 spark chest
|
||||
Location(dungeon=4).add(DungeonChest(0x17B)).connect(right_of_entrance, AND(SHIELD, SWORD)) # room with key chest
|
||||
rightside_crossroads = Location(dungeon=4).connect(entrance, AND(FEATHER, PEGASUS_BOOTS)) # 2 key chests on the right.
|
||||
pushable_block_chest = Location(dungeon=4).add(DungeonChest(0x171)).connect(rightside_crossroads, BOMB) # lower chest
|
||||
puddle_crack_block_chest = Location(dungeon=4).add(DungeonChest(0x165)).connect(rightside_crossroads, OR(BOMB, FLIPPERS)) # top right chest
|
||||
|
||||
double_locked_room = Location(dungeon=4).connect(right_of_entrance, AND(KEY4, FOUND(KEY4, 5)), one_way=True)
|
||||
right_of_entrance.connect(double_locked_room, KEY4, one_way=True)
|
||||
after_double_lock = Location(dungeon=4).connect(double_locked_room, AND(KEY4, FOUND(KEY4, 4), OR(FEATHER, FLIPPERS)), one_way=True)
|
||||
double_locked_room.connect(after_double_lock, AND(KEY4, FOUND(KEY4, 2), OR(FEATHER, FLIPPERS)), one_way=True)
|
||||
|
||||
dungeon4_puddle_before_crossroads = Location(dungeon=4).add(DungeonChest(0x175)).connect(after_double_lock, FLIPPERS)
|
||||
north_crossroads = Location(dungeon=4).connect(after_double_lock, AND(FEATHER, PEGASUS_BOOTS))
|
||||
before_miniboss = Location(dungeon=4).connect(north_crossroads, AND(KEY4, FOUND(KEY4, 3)))
|
||||
if options.owlstatues == "both" or options.owlstatues == "dungeon":
|
||||
Location(dungeon=4).add(OwlStatue(0x16F)).connect(before_miniboss, STONE_BEAK4)
|
||||
sidescroller_key = Location(dungeon=4).add(DroppedKey(0x169)).connect(before_miniboss, AND(r.attack_hookshot_powder, FLIPPERS)) # key that drops in the hole and needs swim to get
|
||||
center_puddle_chest = Location(dungeon=4).add(DungeonChest(0x16E)).connect(before_miniboss, FLIPPERS) # chest with 50 rupees
|
||||
left_water_area = Location(dungeon=4).connect(before_miniboss, OR(FEATHER, FLIPPERS)) # area left with zol chest and 5 symbol puzzle (water area)
|
||||
left_water_area.add(DungeonChest(0x16D)) # gel chest
|
||||
left_water_area.add(DungeonChest(0x168)) # key chest near the puzzle
|
||||
miniboss = Location(dungeon=4).connect(before_miniboss, AND(KEY4, FOUND(KEY4, 5), r.miniboss_requirements[world_setup.miniboss_mapping[3]]))
|
||||
terrace_zols_chest = Location(dungeon=4).connect(before_miniboss, FLIPPERS) # flippers to move around miniboss through 5 tile room
|
||||
miniboss = Location(dungeon=4).connect(terrace_zols_chest, POWER_BRACELET, one_way=True) # reach flippers chest through the miniboss room
|
||||
terrace_zols_chest.add(DungeonChest(0x160)) # flippers chest
|
||||
terrace_zols_chest.connect(left_water_area, r.attack_hookshot_powder, one_way=True) # can move from flippers chest south to push the block to left area
|
||||
|
||||
to_the_nightmare_key = Location(dungeon=4).connect(left_water_area, AND(FEATHER, OR(FLIPPERS, PEGASUS_BOOTS))) # 5 symbol puzzle (does not need flippers with boots + feather)
|
||||
to_the_nightmare_key.add(DungeonChest(0x176))
|
||||
|
||||
before_boss = Location(dungeon=4).connect(before_miniboss, AND(r.attack_hookshot, FLIPPERS, KEY4, FOUND(KEY4, 5)))
|
||||
boss = Location(dungeon=4).add(HeartContainer(0x166), Instrument(0x162)).connect(before_boss, AND(NIGHTMARE_KEY4, r.boss_requirements[world_setup.boss_mapping[3]]))
|
||||
|
||||
if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell':
|
||||
sidescroller_key.connect(before_miniboss, AND(FEATHER, BOOMERANG)) # grab the key jumping over the water and boomerang downwards
|
||||
sidescroller_key.connect(before_miniboss, AND(POWER_BRACELET, FLIPPERS)) # kill the zols with the pots in the room to spawn the key
|
||||
rightside_crossroads.connect(entrance, FEATHER) # jump across the corners
|
||||
puddle_crack_block_chest.connect(rightside_crossroads, FEATHER) # jump around the bombable block
|
||||
north_crossroads.connect(entrance, FEATHER) # jump across the corners
|
||||
after_double_lock.connect(entrance, FEATHER) # jump across the corners
|
||||
dungeon4_puddle_before_crossroads.connect(after_double_lock, FEATHER) # With a tight jump feather is enough to cross the puddle without flippers
|
||||
center_puddle_chest.connect(before_miniboss, FEATHER) # With a tight jump feather is enough to cross the puddle without flippers
|
||||
miniboss = Location(dungeon=4).connect(terrace_zols_chest, None, one_way=True) # reach flippers chest through the miniboss room without pulling the lever
|
||||
to_the_nightmare_key.connect(left_water_area, FEATHER) # With a tight jump feather is enough to reach the top left switch without flippers, or use flippers for puzzle and boots to get through 2d section
|
||||
before_boss.connect(left_water_area, FEATHER) # jump to the bottom right corner of boss door room
|
||||
|
||||
if options.logic == 'glitched' or options.logic == 'hell':
|
||||
pushable_block_chest.connect(rightside_crossroads, FLIPPERS) # sideways block push to skip bombs
|
||||
sidescroller_key.connect(before_miniboss, AND(FEATHER, OR(r.attack_hookshot_powder, POWER_BRACELET))) # superjump into the hole to grab the key while falling into the water
|
||||
miniboss.connect(before_miniboss, FEATHER) # use jesus jump to transition over the water left of miniboss
|
||||
|
||||
if options.logic == 'hell':
|
||||
rightside_crossroads.connect(entrance, AND(PEGASUS_BOOTS, HOOKSHOT)) # pit buffer into the wall of the first pit, then boots bonk across the center, hookshot to get to the rightmost pit to a second villa buffer on the rightmost pit
|
||||
pushable_block_chest.connect(rightside_crossroads, OR(PEGASUS_BOOTS, FEATHER)) # use feather to water clip into the top right corner of the bombable block, and sideways block push to gain access. Can boots bonk of top right wall, then water buffer to top of chest and boots bonk to water buffer next to chest
|
||||
after_double_lock.connect(double_locked_room, AND(FOUND(KEY4, 4), PEGASUS_BOOTS), one_way=True) # use boots bonks to cross the water gaps
|
||||
north_crossroads.connect(entrance, AND(PEGASUS_BOOTS, HOOKSHOT)) # pit buffer into wall of the first pit, then boots bonk towards the top and hookshot spam to get across (easier with Piece of Power)
|
||||
after_double_lock.connect(entrance, PEGASUS_BOOTS) # boots bonk + pit buffer to the bottom
|
||||
dungeon4_puddle_before_crossroads.connect(after_double_lock, AND(PEGASUS_BOOTS, HOOKSHOT)) # boots bonk across the water bottom wall to the bottom left corner, then hookshot up
|
||||
to_the_nightmare_key.connect(left_water_area, AND(FLIPPERS, PEGASUS_BOOTS)) # Use flippers for puzzle and boots bonk to get through 2d section
|
||||
before_boss.connect(left_water_area, PEGASUS_BOOTS) # boots bonk across bottom wall then boots bonk to the platform before boss door
|
||||
|
||||
self.entrance = entrance
|
||||
|
||||
|
||||
class NoDungeon4:
|
||||
def __init__(self, options, world_setup, r):
|
||||
entrance = Location(dungeon=4)
|
||||
Location(dungeon=4).add(HeartContainer(0x166), Instrument(0x162)).connect(entrance, r.boss_requirements[
|
||||
world_setup.boss_mapping[3]])
|
||||
|
||||
self.entrance = entrance
|
|
@ -0,0 +1,89 @@
|
|||
from .requirements import *
|
||||
from .location import Location
|
||||
from ..locations.all import *
|
||||
|
||||
|
||||
class Dungeon5:
|
||||
def __init__(self, options, world_setup, r):
|
||||
entrance = Location(dungeon=5)
|
||||
start_hookshot_chest = Location(dungeon=5).add(DungeonChest(0x1A0)).connect(entrance, HOOKSHOT)
|
||||
compass = Location(dungeon=5).add(DungeonChest(0x19E)).connect(entrance, r.attack_hookshot_powder)
|
||||
fourth_stalfos_area = Location(dungeon=5).add(DroppedKey(0x181)).connect(compass, AND(SWORD, FEATHER)) # crystal rocks can only be broken by sword
|
||||
|
||||
area2 = Location(dungeon=5).connect(entrance, KEY5)
|
||||
if options.owlstatues == "both" or options.owlstatues == "dungeon":
|
||||
Location(dungeon=5).add(OwlStatue(0x19A)).connect(area2, STONE_BEAK5)
|
||||
Location(dungeon=5).add(DungeonChest(0x19B)).connect(area2, r.attack_hookshot_powder) # map chest
|
||||
blade_trap_chest = Location(dungeon=5).add(DungeonChest(0x197)).connect(area2, HOOKSHOT) # key chest on the left
|
||||
post_gohma = Location(dungeon=5).connect(area2, AND(HOOKSHOT, r.miniboss_requirements[world_setup.miniboss_mapping[4]], KEY5, FOUND(KEY5,2))) # staircase after gohma
|
||||
staircase_before_boss = Location(dungeon=5).connect(post_gohma, AND(HOOKSHOT, FEATHER)) # bottom right section pits room before boss door. Path via gohma
|
||||
after_keyblock_boss = Location(dungeon=5).connect(staircase_before_boss, AND(KEY5, FOUND(KEY5, 3))) # top right section pits room before boss door
|
||||
after_stalfos = Location(dungeon=5).add(DungeonChest(0x196)).connect(area2, AND(SWORD, BOMB)) # Need to defeat master stalfos once for this empty chest; l2 sword beams kill but obscure
|
||||
if options.owlstatues == "both" or options.owlstatues == "dungeon":
|
||||
butterfly_owl = Location(dungeon=5).add(OwlStatue(0x18A)).connect(after_stalfos, AND(FEATHER, STONE_BEAK5))
|
||||
else:
|
||||
butterfly_owl = None
|
||||
after_stalfos.connect(staircase_before_boss, AND(FEATHER, r.attack_hookshot_powder), one_way=True) # pathway from stalfos to staircase: past butterfly room and push the block
|
||||
north_of_crossroads = Location(dungeon=5).connect(after_stalfos, FEATHER)
|
||||
first_bridge_chest = Location(dungeon=5).add(DungeonChest(0x18E)).connect(north_of_crossroads, OR(HOOKSHOT, AND(FEATHER, PEGASUS_BOOTS))) # south of bridge
|
||||
north_bridge_chest = Location(dungeon=5).add(DungeonChest(0x188)).connect(north_of_crossroads, HOOKSHOT) # north bridge chest 50 rupees
|
||||
east_bridge_chest = Location(dungeon=5).add(DungeonChest(0x18F)).connect(north_of_crossroads, HOOKSHOT) # east bridge chest small key
|
||||
third_arena = Location(dungeon=5).connect(north_of_crossroads, AND(SWORD, BOMB)) # can beat 3rd m.stalfos
|
||||
stone_tablet = Location(dungeon=5).add(DungeonChest(0x183)).connect(north_of_crossroads, AND(POWER_BRACELET, r.attack_skeleton)) # stone tablet
|
||||
boss_key = Location(dungeon=5).add(DungeonChest(0x186)).connect(after_stalfos, AND(FLIPPERS, HOOKSHOT)) # nightmare key
|
||||
before_boss = Location(dungeon=5).connect(after_keyblock_boss, HOOKSHOT)
|
||||
boss = Location(dungeon=5).add(HeartContainer(0x185), Instrument(0x182)).connect(before_boss, AND(r.boss_requirements[world_setup.boss_mapping[4]], NIGHTMARE_KEY5))
|
||||
|
||||
# When we can reach the stone tablet chest, we can also reach the final location of master stalfos
|
||||
m_stalfos_drop = Location(dungeon=5).add(HookshotDrop()).connect(third_arena, AND(FEATHER, SWORD, BOMB)) # can reach fourth arena from entrance with feather and sword
|
||||
|
||||
if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell':
|
||||
blade_trap_chest.connect(area2, AND(FEATHER, r.attack_hookshot_powder)) # jump past the blade traps
|
||||
boss_key.connect(after_stalfos, AND(FLIPPERS, FEATHER, PEGASUS_BOOTS)) # boots jump across
|
||||
after_stalfos.connect(after_keyblock_boss, AND(FEATHER, r.attack_hookshot_powder)) # circumvent stalfos by going past gohma and backwards from boss door
|
||||
if butterfly_owl:
|
||||
butterfly_owl.connect(after_stalfos, AND(PEGASUS_BOOTS, STONE_BEAK5)) # boots charge + bonk to cross 2d bridge
|
||||
after_stalfos.connect(staircase_before_boss, AND(PEGASUS_BOOTS, r.attack_hookshot_powder), one_way=True) # pathway from stalfos to staircase: boots charge + bonk to cross bridge, past butterfly room and push the block
|
||||
staircase_before_boss.connect(post_gohma, AND(PEGASUS_BOOTS, HOOKSHOT)) # boots bonk in 2d section to skip feather
|
||||
north_of_crossroads.connect(after_stalfos, HOOKSHOT) # hookshot to the right block to cross pits
|
||||
first_bridge_chest.connect(north_of_crossroads, FEATHER) # tight jump from bottom wall clipped to make it over the pits
|
||||
after_keyblock_boss.connect(after_stalfos, AND(FEATHER, r.attack_hookshot_powder)) # jump from bottom left to top right, skipping the keyblock
|
||||
before_boss.connect(after_stalfos, AND(FEATHER, PEGASUS_BOOTS, r.attack_hookshot_powder)) # cross pits room from bottom left to top left with boots jump
|
||||
|
||||
if options.logic == 'glitched' or options.logic == 'hell':
|
||||
start_hookshot_chest.connect(entrance, FEATHER) # 1 pit buffer to clip bottom wall and jump across the pits
|
||||
post_gohma.connect(area2, HOOKSHOT) # glitch through the blocks/pots with hookshot. Zoomerang can be used but has no logical implications because of 2d section requiring hookshot
|
||||
north_bridge_chest.connect(north_of_crossroads, FEATHER) # 1 pit buffer to clip bottom wall and jump across the pits
|
||||
east_bridge_chest.connect(first_bridge_chest, FEATHER) # 1 pit buffer to clip bottom wall and jump across the pits
|
||||
#after_stalfos.connect(staircase_before_boss, AND(FEATHER, OR(SWORD, BOW, MAGIC_ROD))) # use the keyblock to get a wall clip in right wall to perform a superjump over the pushable block TODO: nagmessages
|
||||
after_stalfos.connect(staircase_before_boss, AND(PEGASUS_BOOTS, FEATHER, OR(SWORD, BOW, MAGIC_ROD))) # charge a boots dash in bottom right corner to the right, jump before hitting the wall and use weapon to the left side before hitting the wall
|
||||
|
||||
if options.logic == 'hell':
|
||||
start_hookshot_chest.connect(entrance, PEGASUS_BOOTS) # use pit buffer to clip into the bottom wall and boots bonk off the wall again
|
||||
fourth_stalfos_area.connect(compass, AND(PEGASUS_BOOTS, SWORD)) # do an incredibly hard boots bonk setup to get across the hanging platforms in the 2d section
|
||||
blade_trap_chest.connect(area2, AND(PEGASUS_BOOTS, r.attack_hookshot_powder)) # boots bonk + pit buffer past the blade traps
|
||||
post_gohma.connect(area2, AND(PEGASUS_BOOTS, FEATHER, POWER_BRACELET, r.attack_hookshot_powder)) # use boots jump in room with 2 zols + flying arrows to pit buffer above pot, then jump across. Sideways block push + pick up pots to reach post_gohma
|
||||
staircase_before_boss.connect(post_gohma, AND(PEGASUS_BOOTS, FEATHER)) # to pass 2d section, tight jump on left screen: hug left wall on little platform, then dash right off platform and jump while in midair to bonk against right wall
|
||||
after_stalfos.connect(staircase_before_boss, AND(FEATHER, SWORD)) # unclipped superjump in bottom right corner of staircase before boss room, jumping left over the pushable block. reverse is push block
|
||||
after_stalfos.connect(area2, SWORD) # knock master stalfos down 255 times (about 23 minutes)
|
||||
north_bridge_chest.connect(north_of_crossroads, PEGASUS_BOOTS) # boots bonk across the pits with pit buffering
|
||||
first_bridge_chest.connect(north_of_crossroads, PEGASUS_BOOTS) # get to first chest via the north chest with pit buffering
|
||||
east_bridge_chest.connect(first_bridge_chest, PEGASUS_BOOTS) # boots bonk across the pits with pit buffering
|
||||
third_arena.connect(north_of_crossroads, SWORD) # can beat 3rd m.stalfos with 255 sword spins
|
||||
m_stalfos_drop.connect(third_arena, AND(FEATHER, SWORD)) # beat master stalfos by knocking it down 255 times x 4 (takes about 1.5h total)
|
||||
m_stalfos_drop.connect(third_arena, AND(PEGASUS_BOOTS, SWORD)) # can reach fourth arena from entrance with pegasus boots and sword
|
||||
boss_key.connect(after_stalfos, FLIPPERS) # pit buffer across
|
||||
if butterfly_owl:
|
||||
after_keyblock_boss.connect(butterfly_owl, STONE_BEAK5, one_way=True) # pit buffer from top right to bottom in right pits room
|
||||
before_boss.connect(after_stalfos, AND(FEATHER, SWORD)) # cross pits room from bottom left to top left by unclipped superjump on bottom wall on top of side wall, then jump across
|
||||
|
||||
self.entrance = entrance
|
||||
|
||||
|
||||
class NoDungeon5:
|
||||
def __init__(self, options, world_setup, r):
|
||||
entrance = Location(dungeon=5)
|
||||
Location(dungeon=5).add(HeartContainer(0x185), Instrument(0x182)).connect(entrance, r.boss_requirements[
|
||||
world_setup.boss_mapping[4]])
|
||||
|
||||
self.entrance = entrance
|
|
@ -0,0 +1,65 @@
|
|||
from .requirements import *
|
||||
from .location import Location
|
||||
from ..locations.all import *
|
||||
|
||||
|
||||
class Dungeon6:
|
||||
def __init__(self, options, world_setup, r, *, raft_game_chest=True):
|
||||
entrance = Location(dungeon=6)
|
||||
Location(dungeon=6).add(DungeonChest(0x1CF)).connect(entrance, OR(BOMB, BOW, MAGIC_ROD, COUNT(POWER_BRACELET, 2))) # 50 rupees
|
||||
Location(dungeon=6).add(DungeonChest(0x1C9)).connect(entrance, COUNT(POWER_BRACELET, 2)) # 100 rupees start
|
||||
if options.owlstatues == "both" or options.owlstatues == "dungeon":
|
||||
Location(dungeon=6).add(OwlStatue(0x1BB)).connect(entrance, STONE_BEAK6)
|
||||
|
||||
# Power bracelet chest
|
||||
bracelet_chest = Location(dungeon=6).add(DungeonChest(0x1CE)).connect(entrance, AND(BOMB, FEATHER))
|
||||
|
||||
# left side
|
||||
Location(dungeon=6).add(DungeonChest(0x1C0)).connect(entrance, AND(POWER_BRACELET, OR(BOMB, BOW, MAGIC_ROD))) # 3 wizrobes raised blocks dont need to hit the switch
|
||||
left_side = Location(dungeon=6).add(DungeonChest(0x1B9)).add(DungeonChest(0x1B3)).connect(entrance, AND(POWER_BRACELET, OR(BOMB, BOOMERANG)))
|
||||
Location(dungeon=6).add(DroppedKey(0x1B4)).connect(left_side, OR(BOMB, BOW, MAGIC_ROD)) # 2 wizrobe drop key
|
||||
top_left = Location(dungeon=6).add(DungeonChest(0x1B0)).connect(left_side, COUNT(POWER_BRACELET, 2)) # top left chest horseheads
|
||||
if raft_game_chest:
|
||||
Location().add(Chest(0x06C)).connect(top_left, POWER_BRACELET) # seashell chest in raft game
|
||||
|
||||
# right side
|
||||
to_miniboss = Location(dungeon=6).connect(entrance, KEY6)
|
||||
miniboss = Location(dungeon=6).connect(to_miniboss, AND(BOMB, r.miniboss_requirements[world_setup.miniboss_mapping[5]]))
|
||||
lower_right_side = Location(dungeon=6).add(DungeonChest(0x1BE)).connect(entrance, AND(OR(BOMB, BOW, MAGIC_ROD), COUNT(POWER_BRACELET, 2))) # waterway key
|
||||
medicine_chest = Location(dungeon=6).add(DungeonChest(0x1D1)).connect(lower_right_side, FEATHER) # ledge chest medicine
|
||||
if options.owlstatues == "both" or options.owlstatues == "dungeon":
|
||||
lower_right_owl = Location(dungeon=6).add(OwlStatue(0x1D7)).connect(lower_right_side, AND(POWER_BRACELET, STONE_BEAK6))
|
||||
|
||||
center_1 = Location(dungeon=6).add(DroppedKey(0x1C3)).connect(miniboss, AND(COUNT(POWER_BRACELET, 2), FEATHER)) # tile room key drop
|
||||
center_2_and_upper_right_side = Location(dungeon=6).add(DungeonChest(0x1B1)).connect(center_1, KEY6) # top right chest horseheads
|
||||
boss_key = Location(dungeon=6).add(DungeonChest(0x1B6)).connect(center_2_and_upper_right_side, AND(KEY6, HOOKSHOT))
|
||||
if options.owlstatues == "both" or options.owlstatues == "dungeon":
|
||||
Location(dungeon=6).add(OwlStatue(0x1B6)).connect(boss_key, STONE_BEAK6)
|
||||
|
||||
boss = Location(dungeon=6).add(HeartContainer(0x1BC), Instrument(0x1b5)).connect(center_1, AND(NIGHTMARE_KEY6, r.boss_requirements[world_setup.boss_mapping[5]]))
|
||||
|
||||
if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell':
|
||||
bracelet_chest.connect(entrance, BOMB) # get through 2d section by "fake" jumping to the ladders
|
||||
center_1.connect(miniboss, AND(COUNT(POWER_BRACELET, 2), PEGASUS_BOOTS)) # use a boots dash to get over the platforms
|
||||
|
||||
if options.logic == 'glitched' or options.logic == 'hell':
|
||||
entrance.connect(left_side, AND(POWER_BRACELET, FEATHER), one_way=True) # path from entrance to left_side: use superjumps to pass raised blocks
|
||||
lower_right_side.connect(center_2_and_upper_right_side, AND(FEATHER, OR(SWORD, BOW, MAGIC_ROD)), one_way=True) # path from lower_right_side to center_2: superjump from waterway towards dodongos. superjump next to corner block, so weapons added
|
||||
center_2_and_upper_right_side.connect(center_1, AND(POWER_BRACELET, FEATHER), one_way=True) # going backwards from dodongos, use a shaq jump to pass by keyblock at tile room
|
||||
boss_key.connect(lower_right_side, FEATHER) # superjump from waterway to the left. POWER_BRACELET is implied from lower_right_side
|
||||
|
||||
if options.logic == 'hell':
|
||||
entrance.connect(left_side, AND(POWER_BRACELET, PEGASUS_BOOTS, OR(BOW, MAGIC_ROD)), one_way=True) # can boots superhop off the top right corner in 3 wizrobe raised blocks room
|
||||
medicine_chest.connect(lower_right_side, AND(PEGASUS_BOOTS, OR(MAGIC_ROD, BOW))) # can boots superhop off the top wall with bow or magic rod
|
||||
center_1.connect(miniboss, AND(COUNT(POWER_BRACELET, 2))) # use a double damage boost from the sparks to get across (first one is free, second one needs to buffer while in midair for spark to get close enough)
|
||||
lower_right_side.connect(center_2_and_upper_right_side, FEATHER, one_way=True) # path from lower_right_side to center_2: superjump from waterway towards dodongos. superjump next to corner block is super tight to get enough horizontal distance
|
||||
|
||||
self.entrance = entrance
|
||||
|
||||
|
||||
class NoDungeon6:
|
||||
def __init__(self, options, world_setup, r):
|
||||
entrance = Location(dungeon=6)
|
||||
Location(dungeon=6).add(HeartContainer(0x1BC), Instrument(0x1b5)).connect(entrance, r.boss_requirements[
|
||||
world_setup.boss_mapping[5]])
|
||||
self.entrance = entrance
|
|
@ -0,0 +1,65 @@
|
|||
from .requirements import *
|
||||
from .location import Location
|
||||
from ..locations.all import *
|
||||
|
||||
|
||||
class Dungeon7:
|
||||
def __init__(self, options, world_setup, r):
|
||||
entrance = Location(dungeon=7)
|
||||
first_key = Location(dungeon=7).add(DroppedKey(0x210)).connect(entrance, r.attack_hookshot_powder)
|
||||
topright_pillar_area = Location(dungeon=7).connect(entrance, KEY7)
|
||||
if options.owlstatues == "both" or options.owlstatues == "dungeon":
|
||||
Location(dungeon=7).add(OwlStatue(0x216)).connect(topright_pillar_area, STONE_BEAK7)
|
||||
topright_pillar = Location(dungeon=7).add(DungeonChest(0x212)).connect(topright_pillar_area, POWER_BRACELET) # map chest
|
||||
if options.owlstatues == "both" or options.owlstatues == "dungeon":
|
||||
Location(dungeon=7).add(OwlStatue(0x204)).connect(topright_pillar_area, STONE_BEAK7)
|
||||
topright_pillar_area.add(DungeonChest(0x209)) # stone slab chest can be reached by dropping down a hole
|
||||
three_of_a_kind_north = Location(dungeon=7).add(DungeonChest(0x211)).connect(topright_pillar_area, OR(r.attack_hookshot, AND(FEATHER, SHIELD))) # compass chest; path without feather with hitting switch by falling on the raised blocks. No bracelet because ball does not reset
|
||||
bottomleftF2_area = Location(dungeon=7).connect(topright_pillar_area, r.attack_hookshot) # area with hinox, be able to hit a switch to reach that area
|
||||
topleftF1_chest = Location(dungeon=7).add(DungeonChest(0x201)) # top left chest on F1
|
||||
bottomleftF2_area.connect(topleftF1_chest, None, one_way = True) # drop down in left most holes of hinox room or tile room
|
||||
Location(dungeon=7).add(DroppedKey(0x21B)).connect(bottomleftF2_area, r.attack_hookshot) # hinox drop key
|
||||
# Most of the dungeon can be accessed at this point.
|
||||
if options.owlstatues == "both" or options.owlstatues == "dungeon":
|
||||
bottomleft_owl = Location(dungeon=7).add(OwlStatue(0x21C)).connect(bottomleftF2_area, AND(BOMB, STONE_BEAK7))
|
||||
nightmare_key = Location(dungeon=7).add(DungeonChest(0x224)).connect(bottomleftF2_area, r.miniboss_requirements[world_setup.miniboss_mapping[6]]) # nightmare key after the miniboss
|
||||
mirror_shield_chest = Location(dungeon=7).add(DungeonChest(0x21A)).connect(bottomleftF2_area, r.attack_hookshot) # mirror shield chest, need to be able to hit a switch to reach or
|
||||
bottomleftF2_area.connect(mirror_shield_chest, AND(KEY7, FOUND(KEY7, 3)), one_way = True) # reach mirror shield chest from hinox area by opening keyblock
|
||||
toprightF1_chest = Location(dungeon=7).add(DungeonChest(0x204)).connect(bottomleftF2_area, r.attack_hookshot) # chest on the F1 right ledge. Added attack_hookshot since switch needs to be hit to get back up
|
||||
final_pillar_area = Location(dungeon=7).add(DungeonChest(0x21C)).connect(bottomleftF2_area, AND(BOMB, HOOKSHOT)) # chest that needs to spawn to get to the last pillar
|
||||
final_pillar = Location(dungeon=7).connect(final_pillar_area, POWER_BRACELET) # decouple chest from pillar
|
||||
|
||||
beamos_horseheads_area = Location(dungeon=7).connect(final_pillar, NIGHTMARE_KEY7) # area behind boss door
|
||||
beamos_horseheads = Location(dungeon=7).add(DungeonChest(0x220)).connect(beamos_horseheads_area, POWER_BRACELET) # 100 rupee chest / medicine chest (DX) behind boss door
|
||||
pre_boss = Location(dungeon=7).connect(beamos_horseheads_area, HOOKSHOT) # raised plateau before boss staircase
|
||||
boss = Location(dungeon=7).add(HeartContainer(0x223), Instrument(0x22c)).connect(pre_boss, r.boss_requirements[world_setup.boss_mapping[6]])
|
||||
|
||||
if options.logic == 'glitched' or options.logic == 'hell':
|
||||
topright_pillar_area.connect(entrance, AND(FEATHER, SWORD)) # superjump in the center to get on raised blocks, superjump in switch room to right side to walk down. center superjump has to be low so sword added
|
||||
toprightF1_chest.connect(topright_pillar_area, FEATHER) # superjump from F1 switch room
|
||||
topleftF2_area = Location(dungeon=7).connect(topright_pillar_area, FEATHER) # superjump in top left pillar room over the blocks from right to left, to reach tile room
|
||||
topleftF2_area.connect(topleftF1_chest, None, one_way = True) # fall down tile room holes on left side to reach top left chest on ground floor
|
||||
topleftF1_chest.connect(bottomleftF2_area, AND(PEGASUS_BOOTS, FEATHER), one_way = True) # without hitting the switch, jump on raised blocks at f1 pegs chest (0x209), and boots jump to stairs to reach hinox area
|
||||
final_pillar_area.connect(bottomleftF2_area, OR(r.attack_hookshot, POWER_BRACELET, AND(FEATHER, SHIELD))) # sideways block push to get to the chest and pillar, kill requirement for 3 of a kind enemies to access chest. Assumes you do not get ball stuck on raised pegs for bracelet path
|
||||
if options.owlstatues == "both" or options.owlstatues == "dungeon":
|
||||
bottomleft_owl.connect(bottomleftF2_area, STONE_BEAK7) # sideways block push to get to the owl statue
|
||||
final_pillar.connect(bottomleftF2_area, BOMB) # bomb trigger pillar
|
||||
pre_boss.connect(final_pillar, FEATHER) # superjump on top of goomba to extend superjump to boss door plateau
|
||||
pre_boss.connect(beamos_horseheads_area, None, one_way=True) # can drop down from raised plateau to beamos horseheads area
|
||||
|
||||
if options.logic == 'hell':
|
||||
topright_pillar_area.connect(entrance, FEATHER) # superjump in the center to get on raised blocks, has to be low
|
||||
topright_pillar_area.connect(entrance, AND(PEGASUS_BOOTS, OR(BOW, MAGIC_ROD))) # boots superhop in the center to get on raised blocks
|
||||
toprightF1_chest.connect(topright_pillar_area, AND(PEGASUS_BOOTS, OR(BOW, MAGIC_ROD))) # boots superhop from F1 switch room
|
||||
pre_boss.connect(final_pillar, AND(PEGASUS_BOOTS, OR(BOW, MAGIC_ROD))) # boots superhop on top of goomba to extend superhop to boss door plateau
|
||||
|
||||
self.entrance = entrance
|
||||
|
||||
|
||||
class NoDungeon7:
|
||||
def __init__(self, options, world_setup, r):
|
||||
entrance = Location(dungeon=7)
|
||||
boss = Location(dungeon=7).add(HeartContainer(0x223), Instrument(0x22c)).connect(entrance, r.boss_requirements[
|
||||
world_setup.boss_mapping[6]])
|
||||
|
||||
self.entrance = entrance
|
|
@ -0,0 +1,107 @@
|
|||
from .requirements import *
|
||||
from .location import Location
|
||||
from ..locations.all import *
|
||||
|
||||
|
||||
class Dungeon8:
|
||||
def __init__(self, options, world_setup, r, *, back_entrance_heartpiece=True):
|
||||
entrance = Location(dungeon=8)
|
||||
entrance_up = Location(dungeon=8).connect(entrance, FEATHER)
|
||||
entrance_left = Location(dungeon=8).connect(entrance, r.attack_hookshot_no_bomb) # past hinox
|
||||
|
||||
# left side
|
||||
entrance_left.add(DungeonChest(0x24D)) # zamboni room chest
|
||||
Location(dungeon=8).add(DungeonChest(0x25C)).connect(entrance_left, r.attack_hookshot) # eye magnet chest
|
||||
vire_drop_key = Location(dungeon=8).add(DroppedKey(0x24C)).connect(entrance_left, r.attack_hookshot_no_bomb) # vire drop key
|
||||
sparks_chest = Location(dungeon=8).add(DungeonChest(0x255)).connect(entrance_left, OR(HOOKSHOT, FEATHER)) # chest before lvl1 miniboss
|
||||
Location(dungeon=8).add(DungeonChest(0x246)).connect(entrance_left, MAGIC_ROD) # key chest that spawns after creating fire
|
||||
|
||||
# right side
|
||||
if options.owlstatues == "both" or options.owlstatues == "dungeon":
|
||||
bottomright_owl = Location(dungeon=8).add(OwlStatue(0x253)).connect(entrance, AND(STONE_BEAK8, FEATHER, POWER_BRACELET)) # Two ways to reach this owl statue, but both require the same (except that one route requires bombs as well)
|
||||
else:
|
||||
bottomright_owl = None
|
||||
slime_chest = Location(dungeon=8).add(DungeonChest(0x259)).connect(entrance, OR(FEATHER, AND(r.attack_hookshot, POWER_BRACELET))) # chest with slime
|
||||
bottom_right = Location(dungeon=8).add(DroppedKey(0x25A)).connect(entrance, AND(FEATHER, OR(BOMB, AND(r.attack_hookshot_powder, POWER_BRACELET)))) # zamboni key drop; bombs for entrance up through switch room, weapon + bracelet for NW zamboni staircase to bottom right past smasher
|
||||
bottomright_pot_chest = Location(dungeon=8).add(DungeonChest(0x25F)).connect(bottom_right, POWER_BRACELET) # 4 ropes pot room chest
|
||||
|
||||
map_chest = Location(dungeon=8).add(DungeonChest(0x24F)).connect(entrance_up, None) # use the zamboni to get to the push blocks
|
||||
lower_center = Location(dungeon=8).connect(entrance_up, KEY8)
|
||||
upper_center = Location(dungeon=8).connect(lower_center, AND(KEY8, FOUND(KEY8, 2)))
|
||||
if options.owlstatues == "both" or options.owlstatues == "dungeon":
|
||||
Location(dungeon=8).add(OwlStatue(0x245)).connect(upper_center, STONE_BEAK8)
|
||||
Location(dungeon=8).add(DroppedKey(0x23E)).connect(upper_center, r.attack_skeleton) # 2 gibdos cracked floor; technically possible to use pits to kill but dumb
|
||||
medicine_chest = Location(dungeon=8).add(DungeonChest(0x235)).connect(upper_center, AND(FEATHER, HOOKSHOT)) # medicine chest
|
||||
|
||||
middle_center_1 = Location(dungeon=8).connect(upper_center, BOMB)
|
||||
middle_center_2 = Location(dungeon=8).connect(middle_center_1, AND(KEY8, FOUND(KEY8, 4)))
|
||||
middle_center_3 = Location(dungeon=8).connect(middle_center_2, KEY8)
|
||||
miniboss_entrance = Location(dungeon=8).connect(middle_center_3, AND(HOOKSHOT, KEY8, FOUND(KEY8, 7))) # hookshot to get across to keyblock, 7 to fix keylock issues if keys are used on other keyblocks
|
||||
miniboss = Location(dungeon=8).connect(miniboss_entrance, AND(FEATHER, r.miniboss_requirements[world_setup.miniboss_mapping[7]])) # feather for 2d section, sword to kill
|
||||
miniboss.add(DungeonChest(0x237)) # fire rod chest
|
||||
|
||||
up_left = Location(dungeon=8).connect(upper_center, AND(r.attack_hookshot_powder, AND(KEY8, FOUND(KEY8, 4))))
|
||||
entrance_up.connect(up_left, AND(FEATHER, MAGIC_ROD), one_way=True) # alternate path with fire rod through 2d section to nightmare key
|
||||
up_left.add(DungeonChest(0x240)) # beamos blocked chest
|
||||
up_left.connect(entrance_left, None, one_way=True) # path from up_left to entrance_left by dropping of the ledge in torch room
|
||||
Location(dungeon=8).add(DungeonChest(0x23D)).connect(up_left, BOMB) # dodongo chest
|
||||
up_left.connect(upper_center, None, one_way=True) # use the outside path of the dungeon to get to the right side
|
||||
if back_entrance_heartpiece:
|
||||
Location().add(HeartPiece(0x000)).connect(up_left, None) # Outside the dungeon on the platform
|
||||
Location(dungeon=8).add(DroppedKey(0x241)).connect(up_left, BOW) # lava statue
|
||||
if options.owlstatues == "both" or options.owlstatues == "dungeon":
|
||||
Location(dungeon=8).add(OwlStatue(0x241)).connect(up_left, STONE_BEAK8)
|
||||
Location(dungeon=8).add(DungeonChest(0x23A)).connect(up_left, HOOKSHOT) # ledge chest left of boss door
|
||||
|
||||
top_left_stairs = Location(dungeon=8).connect(entrance_up, AND(FEATHER, MAGIC_ROD))
|
||||
top_left_stairs.connect(up_left, None, one_way=True) # jump down from the staircase to the right
|
||||
nightmare_key = Location(dungeon=8).add(DungeonChest(0x232)).connect(top_left_stairs, AND(FEATHER, SWORD, KEY8, FOUND(KEY8, 7)))
|
||||
|
||||
# Bombing from the center dark rooms to the left so you can access more keys.
|
||||
# The south walls of center dark room can be bombed from lower_center too with bomb and feather for center dark room access from the south, allowing even more access. Not sure if this should be logic since "obscure"
|
||||
middle_center_2.connect(up_left, AND(BOMB, FEATHER), one_way=True) # does this even skip a key? both middle_center_2 and up_left come from upper_center with 1 extra key
|
||||
|
||||
bossdoor = Location(dungeon=8).connect(entrance_up, AND(FEATHER, MAGIC_ROD))
|
||||
boss = Location(dungeon=8).add(HeartContainer(0x234), Instrument(0x230)).connect(bossdoor, AND(NIGHTMARE_KEY8, r.boss_requirements[world_setup.boss_mapping[7]]))
|
||||
|
||||
if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell':
|
||||
entrance_left.connect(entrance, BOMB) # use bombs to kill vire and hinox
|
||||
vire_drop_key.connect(entrance_left, BOMB) # use bombs to kill rolling bones and vire
|
||||
bottom_right.connect(slime_chest, FEATHER) # diagonal jump over the pits to reach rolling rock / zamboni
|
||||
up_left.connect(lower_center, AND(BOMB, FEATHER)) # blow up hidden walls from peahat room -> dark room -> eye statue room
|
||||
slime_chest.connect(entrance, AND(r.attack_hookshot_powder, POWER_BRACELET)) # kill vire with powder or bombs
|
||||
|
||||
if options.logic == 'glitched' or options.logic == 'hell':
|
||||
sparks_chest.connect(entrance_left, OR(r.attack_hookshot, FEATHER, PEGASUS_BOOTS)) # 1 pit buffer across the pit. Add requirements for all the options to get to this area
|
||||
lower_center.connect(entrance_up, None) # sideways block push in peahat room to get past keyblock
|
||||
miniboss_entrance.connect(lower_center, AND(BOMB, FEATHER, HOOKSHOT)) # blow up hidden wall for darkroom, use feather + hookshot to clip past keyblock in front of stairs
|
||||
miniboss_entrance.connect(lower_center, AND(BOMB, FEATHER, FOUND(KEY8, 7))) # same as above, but without clipping past the keyblock
|
||||
up_left.connect(lower_center, FEATHER) # use jesus jump in refill room left of peahats to clip bottom wall and push bottom block left, to get a place to super jump
|
||||
up_left.connect(upper_center, FEATHER) # from up left you can jesus jump / lava swim around the key door next to the boss.
|
||||
top_left_stairs.connect(up_left, AND(FEATHER, SWORD)) # superjump
|
||||
medicine_chest.connect(upper_center, FEATHER) # jesus super jump
|
||||
up_left.connect(bossdoor, FEATHER, one_way=True) # superjump off the bottom or right wall to jump over to the boss door
|
||||
|
||||
if options.logic == 'hell':
|
||||
if bottomright_owl:
|
||||
bottomright_owl.connect(entrance, AND(SWORD, POWER_BRACELET, PEGASUS_BOOTS, STONE_BEAK8)) # underground section past mimics, boots bonking across the gap to the ladder
|
||||
bottomright_pot_chest.connect(entrance, AND(SWORD, POWER_BRACELET, PEGASUS_BOOTS)) # underground section past mimics, boots bonking across the gap to the ladder
|
||||
entrance.connect(bottomright_pot_chest, AND(FEATHER, SWORD), one_way=True) # use NW zamboni staircase backwards, subpixel manip for superjump past the pots
|
||||
medicine_chest.connect(upper_center, AND(PEGASUS_BOOTS, HOOKSHOT)) # boots bonk + lava buffer to the bottom wall, then bonk onto the middle section
|
||||
miniboss.connect(miniboss_entrance, AND(PEGASUS_BOOTS, r.miniboss_requirements[world_setup.miniboss_mapping[7]])) # get through 2d section with boots bonks
|
||||
top_left_stairs.connect(map_chest, AND(PEGASUS_BOOTS, MAGIC_ROD)) # boots bonk + lava buffer from map chest to entrance_up, then boots bonk through 2d section
|
||||
nightmare_key.connect(top_left_stairs, AND(PEGASUS_BOOTS, SWORD, FOUND(KEY8, 7))) # use a boots bonk to cross the 2d section + the lava in cueball room
|
||||
bottom_right.connect(entrance_up, AND(POWER_BRACELET, PEGASUS_BOOTS), one_way=True) # take staircase to NW zamboni room, boots bonk onto the lava and water buffer all the way down to push the zamboni
|
||||
bossdoor.connect(entrance_up, AND(PEGASUS_BOOTS, MAGIC_ROD)) # boots bonk through 2d section
|
||||
|
||||
self.entrance = entrance
|
||||
|
||||
|
||||
class NoDungeon8:
|
||||
def __init__(self, options, world_setup, r):
|
||||
entrance = Location(dungeon=8)
|
||||
boss = Location(dungeon=8).add(HeartContainer(0x234)).connect(entrance, r.boss_requirements[
|
||||
world_setup.boss_mapping[7]])
|
||||
instrument = Location(dungeon=8).add(Instrument(0x230)).connect(boss, FEATHER) # jump over the lava to get to the instrument
|
||||
|
||||
self.entrance = entrance
|
|
@ -0,0 +1,49 @@
|
|||
from .requirements import *
|
||||
from .location import Location
|
||||
from ..locations.all import *
|
||||
|
||||
|
||||
class DungeonColor:
|
||||
def __init__(self, options, world_setup, r):
|
||||
entrance = Location(dungeon=9)
|
||||
room2 = Location(dungeon=9).connect(entrance, r.attack_hookshot_powder)
|
||||
room2.add(DungeonChest(0x314)) # key
|
||||
if options.owlstatues == "both" or options.owlstatues == "dungeon":
|
||||
Location(dungeon=9).add(OwlStatue(0x308), OwlStatue(0x30F)).connect(room2, STONE_BEAK9)
|
||||
room2_weapon = Location(dungeon=9).connect(room2, r.attack_hookshot)
|
||||
room2_weapon.add(DungeonChest(0x311)) # stone beak
|
||||
room2_lights = Location(dungeon=9).connect(room2, OR(r.attack_hookshot, SHIELD))
|
||||
room2_lights.add(DungeonChest(0x30F)) # compass chest
|
||||
room2_lights.add(DroppedKey(0x308))
|
||||
|
||||
Location(dungeon=9).connect(room2, AND(KEY9, FOUND(KEY9, 3), r.miniboss_requirements[world_setup.miniboss_mapping["c2"]])).add(DungeonChest(0x302)) # nightmare key after slime mini boss
|
||||
room3 = Location(dungeon=9).connect(room2, AND(KEY9, FOUND(KEY9, 2), r.miniboss_requirements[world_setup.miniboss_mapping["c1"]])) # After the miniboss
|
||||
room4 = Location(dungeon=9).connect(room3, POWER_BRACELET) # need to lift a pot to reveal button
|
||||
room4.add(DungeonChest(0x306)) # map
|
||||
room4karakoro = Location(dungeon=9).add(DroppedKey(0x307)).connect(room4, r.attack_hookshot) # require item to knock Karakoro enemies into shell
|
||||
if options.owlstatues == "both" or options.owlstatues == "dungeon":
|
||||
Location(dungeon=9).add(OwlStatue(0x30A)).connect(room4, STONE_BEAK9)
|
||||
room5 = Location(dungeon=9).connect(room4, OR(r.attack_hookshot, SHIELD)) # lights room
|
||||
room6 = Location(dungeon=9).connect(room5, AND(KEY9, FOUND(KEY9, 3))) # room with switch and nightmare door
|
||||
pre_boss = Location(dungeon=9).connect(room6, OR(r.attack_hookshot, AND(PEGASUS_BOOTS, FEATHER))) # before the boss, require item to hit switch or jump past raised blocks
|
||||
boss = Location(dungeon=9).connect(pre_boss, AND(NIGHTMARE_KEY9, r.boss_requirements[world_setup.boss_mapping[8]]))
|
||||
boss.add(TunicFairy(0), TunicFairy(1))
|
||||
|
||||
if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell':
|
||||
room2.connect(entrance, POWER_BRACELET) # throw pots at enemies
|
||||
pre_boss.connect(room6, FEATHER) # before the boss, jump past raised blocks without boots
|
||||
|
||||
if options.logic == 'hell':
|
||||
room2_weapon.connect(room2, SHIELD) # shield bump karakoro into the holes
|
||||
room4karakoro.connect(room4, SHIELD) # shield bump karakoro into the holes
|
||||
|
||||
self.entrance = entrance
|
||||
|
||||
|
||||
class NoDungeonColor:
|
||||
def __init__(self, options, world_setup, r):
|
||||
entrance = Location(dungeon=9)
|
||||
boss = Location(dungeon=9).connect(entrance, r.boss_requirements[world_setup.boss_mapping[8]])
|
||||
boss.add(TunicFairy(0), TunicFairy(1))
|
||||
|
||||
self.entrance = entrance
|
|
@ -0,0 +1,57 @@
|
|||
import typing
|
||||
from .requirements import hasConsumableRequirement, OR
|
||||
from ..locations.itemInfo import ItemInfo
|
||||
|
||||
|
||||
class Location:
|
||||
def __init__(self, name=None, dungeon=None):
|
||||
self.name = name
|
||||
self.items = [] # type: typing.List[ItemInfo]
|
||||
self.dungeon = dungeon
|
||||
self.__connected_to = set()
|
||||
self.simple_connections = []
|
||||
self.gated_connections = []
|
||||
|
||||
def add(self, *item_infos):
|
||||
for ii in item_infos:
|
||||
assert isinstance(ii, ItemInfo)
|
||||
ii.setLocation(self)
|
||||
self.items.append(ii)
|
||||
return self
|
||||
|
||||
def connect(self, other, req, *, one_way=False):
|
||||
assert isinstance(other, Location), type(other)
|
||||
|
||||
if isinstance(req, bool):
|
||||
if req:
|
||||
self.connect(other, None, one_way=one_way)
|
||||
return
|
||||
|
||||
if other in self.__connected_to:
|
||||
for idx, data in enumerate(self.gated_connections):
|
||||
if data[0] == other:
|
||||
if req is None or data[1] is None:
|
||||
self.gated_connections[idx] = (other, None)
|
||||
else:
|
||||
self.gated_connections[idx] = (other, OR(req, data[1]))
|
||||
break
|
||||
for idx, data in enumerate(self.simple_connections):
|
||||
if data[0] == other:
|
||||
if req is None or data[1] is None:
|
||||
self.simple_connections[idx] = (other, None)
|
||||
else:
|
||||
self.simple_connections[idx] = (other, OR(req, data[1]))
|
||||
break
|
||||
else:
|
||||
self.__connected_to.add(other)
|
||||
|
||||
if hasConsumableRequirement(req):
|
||||
self.gated_connections.append((other, req))
|
||||
else:
|
||||
self.simple_connections.append((other, req))
|
||||
if not one_way:
|
||||
other.connect(self, req, one_way=True)
|
||||
return self
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s:%s:%d:%d:%d>" % (self.__class__.__name__, self.dungeon, len(self.items), len(self.simple_connections), len(self.gated_connections))
|
|
@ -0,0 +1,682 @@
|
|||
from .requirements import *
|
||||
from .location import Location
|
||||
from ..locations.all import *
|
||||
from ..worldSetup import ENTRANCE_INFO
|
||||
|
||||
|
||||
class World:
|
||||
def __init__(self, options, world_setup, r):
|
||||
self.overworld_entrance = {}
|
||||
self.indoor_location = {}
|
||||
|
||||
mabe_village = Location("Mabe Village")
|
||||
Location().add(HeartPiece(0x2A4)).connect(mabe_village, r.bush) # well
|
||||
Location().add(FishingMinigame()).connect(mabe_village, AND(r.bush, COUNT("RUPEES", 20))) # fishing game, heart piece is directly done by the minigame.
|
||||
Location().add(Seashell(0x0A3)).connect(mabe_village, r.bush) # bushes below the shop
|
||||
Location().add(Seashell(0x0D2)).connect(mabe_village, PEGASUS_BOOTS) # smash into tree next to lv1
|
||||
Location().add(Song(0x092)).connect(mabe_village, OCARINA) # Marins song
|
||||
rooster_cave = Location("Rooster Cave")
|
||||
Location().add(DroppedKey(0x1E4)).connect(rooster_cave, AND(OCARINA, SONG3))
|
||||
|
||||
papahl_house = Location("Papahl House")
|
||||
papahl_house.connect(Location().add(TradeSequenceItem(0x2A6, TRADING_ITEM_RIBBON)), TRADING_ITEM_YOSHI_DOLL)
|
||||
|
||||
trendy_shop = Location("Trendy Shop").add(TradeSequenceItem(0x2A0, TRADING_ITEM_YOSHI_DOLL))
|
||||
#trendy_shop.connect(Location())
|
||||
|
||||
self._addEntrance("papahl_house_left", mabe_village, papahl_house, None)
|
||||
self._addEntrance("papahl_house_right", mabe_village, papahl_house, None)
|
||||
self._addEntrance("rooster_grave", mabe_village, rooster_cave, COUNT(POWER_BRACELET, 2))
|
||||
self._addEntranceRequirementExit("rooster_grave", None) # if exiting, you do not need l2 bracelet
|
||||
self._addEntrance("madambowwow", mabe_village, None, None)
|
||||
self._addEntrance("ulrira", mabe_village, None, None)
|
||||
self._addEntrance("mabe_phone", mabe_village, None, None)
|
||||
self._addEntrance("library", mabe_village, None, None)
|
||||
self._addEntrance("trendy_shop", mabe_village, trendy_shop, r.bush)
|
||||
self._addEntrance("d1", mabe_village, None, TAIL_KEY)
|
||||
self._addEntranceRequirementExit("d1", None) # if exiting, you do not need the key
|
||||
|
||||
start_house = Location("Start House").add(StartItem())
|
||||
self._addEntrance("start_house", mabe_village, start_house, None)
|
||||
|
||||
shop = Location("Shop")
|
||||
Location().add(ShopItem(0)).connect(shop, OR(COUNT("RUPEES", 500), SWORD))
|
||||
Location().add(ShopItem(1)).connect(shop, OR(COUNT("RUPEES", 1480), SWORD))
|
||||
self._addEntrance("shop", mabe_village, shop, None)
|
||||
|
||||
dream_hut = Location("Dream Hut")
|
||||
dream_hut_right = Location().add(Chest(0x2BF)).connect(dream_hut, SWORD)
|
||||
if options.logic != "casual":
|
||||
dream_hut_right.connect(dream_hut, OR(BOOMERANG, HOOKSHOT, FEATHER))
|
||||
dream_hut_left = Location().add(Chest(0x2BE)).connect(dream_hut_right, PEGASUS_BOOTS)
|
||||
self._addEntrance("dream_hut", mabe_village, dream_hut, POWER_BRACELET)
|
||||
|
||||
kennel = Location("Kennel").connect(Location().add(Seashell(0x2B2)), SHOVEL) # in the kennel
|
||||
kennel.connect(Location().add(TradeSequenceItem(0x2B2, TRADING_ITEM_DOG_FOOD)), TRADING_ITEM_RIBBON)
|
||||
self._addEntrance("kennel", mabe_village, kennel, None)
|
||||
|
||||
sword_beach = Location("Sword Beach").add(BeachSword()).connect(mabe_village, OR(r.bush, SHIELD, r.attack_hookshot))
|
||||
banana_seller = Location("Banana Seller")
|
||||
banana_seller.connect(Location().add(TradeSequenceItem(0x2FE, TRADING_ITEM_BANANAS)), TRADING_ITEM_DOG_FOOD)
|
||||
self._addEntrance("banana_seller", sword_beach, banana_seller, r.bush)
|
||||
boomerang_cave = Location("Boomerang Cave")
|
||||
if options.boomerang == 'trade':
|
||||
Location().add(BoomerangGuy()).connect(boomerang_cave, OR(BOOMERANG, HOOKSHOT, MAGIC_ROD, PEGASUS_BOOTS, FEATHER, SHOVEL))
|
||||
elif options.boomerang == 'gift':
|
||||
Location().add(BoomerangGuy()).connect(boomerang_cave, None)
|
||||
self._addEntrance("boomerang_cave", sword_beach, boomerang_cave, BOMB)
|
||||
self._addEntranceRequirementExit("boomerang_cave", None) # if exiting, you do not need bombs
|
||||
|
||||
sword_beach_to_ghost_hut = Location("Sword Beach to Ghost House").add(Chest(0x0E5)).connect(sword_beach, POWER_BRACELET)
|
||||
ghost_hut_outside = Location("Outside Ghost House").connect(sword_beach_to_ghost_hut, POWER_BRACELET)
|
||||
ghost_hut_inside = Location("Ghost House").connect(Location().add(Seashell(0x1E3)), POWER_BRACELET)
|
||||
self._addEntrance("ghost_house", ghost_hut_outside, ghost_hut_inside, None)
|
||||
|
||||
## Forest area
|
||||
forest = Location("Forest").connect(mabe_village, r.bush) # forest stretches all the way from the start town to the witch hut
|
||||
Location().add(Chest(0x071)).connect(forest, POWER_BRACELET) # chest at start forest with 2 zols
|
||||
forest_heartpiece = Location("Forest Heart Piece").add(HeartPiece(0x044)) # next to the forest, surrounded by pits
|
||||
forest.connect(forest_heartpiece, OR(BOOMERANG, FEATHER, HOOKSHOT, ROOSTER), one_way=True)
|
||||
|
||||
witch_hut = Location().connect(Location().add(Witch()), TOADSTOOL)
|
||||
self._addEntrance("witch", forest, witch_hut, None)
|
||||
crazy_tracy_hut = Location("Outside Crazy Tracy's House").connect(forest, POWER_BRACELET)
|
||||
crazy_tracy_hut_inside = Location("Crazy Tracy's House")
|
||||
Location().add(KeyLocation("MEDICINE2")).connect(crazy_tracy_hut_inside, FOUND("RUPEES", 50))
|
||||
self._addEntrance("crazy_tracy", crazy_tracy_hut, crazy_tracy_hut_inside, None)
|
||||
start_house.connect(crazy_tracy_hut, SONG2, one_way=True) # Manbo's Mambo into the pond outside Tracy
|
||||
|
||||
forest_madbatter = Location("Forest Mad Batter")
|
||||
Location().add(MadBatter(0x1E1)).connect(forest_madbatter, MAGIC_POWDER)
|
||||
self._addEntrance("forest_madbatter", forest, forest_madbatter, POWER_BRACELET)
|
||||
self._addEntranceRequirementExit("forest_madbatter", None) # if exiting, you do not need bracelet
|
||||
|
||||
forest_cave = Location("Forest Cave")
|
||||
Location().add(Chest(0x2BD)).connect(forest_cave, SWORD) # chest in forest cave on route to mushroom
|
||||
log_cave_heartpiece = Location().add(HeartPiece(0x2AB)).connect(forest_cave, POWER_BRACELET) # piece of heart in the forest cave on route to the mushroom
|
||||
forest_toadstool = Location().add(Toadstool())
|
||||
self._addEntrance("toadstool_entrance", forest, forest_cave, None)
|
||||
self._addEntrance("toadstool_exit", forest_toadstool, forest_cave, None)
|
||||
|
||||
hookshot_cave = Location("Hookshot Cave")
|
||||
hookshot_cave_chest = Location().add(Chest(0x2B3)).connect(hookshot_cave, OR(HOOKSHOT, ROOSTER))
|
||||
self._addEntrance("hookshot_cave", forest, hookshot_cave, POWER_BRACELET)
|
||||
|
||||
swamp = Location("Swamp").connect(forest, AND(OR(MAGIC_POWDER, FEATHER, ROOSTER), r.bush))
|
||||
swamp.connect(forest, r.bush, one_way=True) # can go backwards past Tarin
|
||||
swamp.connect(forest_toadstool, OR(FEATHER, ROOSTER))
|
||||
swamp_chest = Location("Swamp Chest").add(Chest(0x034)).connect(swamp, OR(BOWWOW, HOOKSHOT, MAGIC_ROD, BOOMERANG))
|
||||
self._addEntrance("d2", swamp, None, OR(BOWWOW, HOOKSHOT, MAGIC_ROD, BOOMERANG))
|
||||
forest_rear_chest = Location().add(Chest(0x041)).connect(swamp, r.bush) # tail key
|
||||
self._addEntrance("writes_phone", swamp, None, None)
|
||||
|
||||
writes_hut_outside = Location("Outside Write's House").connect(swamp, OR(FEATHER, ROOSTER)) # includes the cave behind the hut
|
||||
writes_house = Location("Write's House")
|
||||
writes_house.connect(Location().add(TradeSequenceItem(0x2a8, TRADING_ITEM_BROOM)), TRADING_ITEM_LETTER)
|
||||
self._addEntrance("writes_house", writes_hut_outside, writes_house, None)
|
||||
if options.owlstatues == "both" or options.owlstatues == "overworld":
|
||||
writes_hut_outside.add(OwlStatue(0x11))
|
||||
writes_cave = Location("Write's Cave")
|
||||
writes_cave_left_chest = Location().add(Chest(0x2AE)).connect(writes_cave, OR(FEATHER, ROOSTER, HOOKSHOT)) # 1st chest in the cave behind the hut
|
||||
Location().add(Chest(0x2AF)).connect(writes_cave, POWER_BRACELET) # 2nd chest in the cave behind the hut.
|
||||
self._addEntrance("writes_cave_left", writes_hut_outside, writes_cave, None)
|
||||
self._addEntrance("writes_cave_right", writes_hut_outside, writes_cave, None)
|
||||
|
||||
graveyard = Location("Graveyard").connect(forest, OR(FEATHER, ROOSTER, POWER_BRACELET)) # whole area from the graveyard up to the moblin cave
|
||||
if options.owlstatues == "both" or options.owlstatues == "overworld":
|
||||
graveyard.add(OwlStatue(0x035)) # Moblin cave owl
|
||||
self._addEntrance("photo_house", graveyard, None, None)
|
||||
self._addEntrance("d0", graveyard, None, POWER_BRACELET)
|
||||
self._addEntranceRequirementExit("d0", None) # if exiting, you do not need bracelet
|
||||
ghost_grave = Location().connect(forest, POWER_BRACELET)
|
||||
Location().add(Seashell(0x074)).connect(ghost_grave, AND(r.bush, SHOVEL)) # next to grave cave, digging spot
|
||||
|
||||
graveyard_cave_left = Location()
|
||||
graveyard_cave_right = Location().connect(graveyard_cave_left, OR(FEATHER, ROOSTER))
|
||||
graveyard_heartpiece = Location().add(HeartPiece(0x2DF)).connect(graveyard_cave_right, OR(AND(BOMB, OR(HOOKSHOT, PEGASUS_BOOTS), FEATHER), ROOSTER)) # grave cave
|
||||
self._addEntrance("graveyard_cave_left", ghost_grave, graveyard_cave_left, POWER_BRACELET)
|
||||
self._addEntrance("graveyard_cave_right", graveyard, graveyard_cave_right, None)
|
||||
moblin_cave = Location().connect(Location().add(Chest(0x2E2)), AND(r.attack_hookshot_powder, r.miniboss_requirements[world_setup.miniboss_mapping["moblin_cave"]]))
|
||||
self._addEntrance("moblin_cave", graveyard, moblin_cave, None)
|
||||
|
||||
# "Ukuku Prairie"
|
||||
ukuku_prairie = Location().connect(mabe_village, POWER_BRACELET).connect(graveyard, POWER_BRACELET)
|
||||
ukuku_prairie.connect(Location().add(TradeSequenceItem(0x07B, TRADING_ITEM_STICK)), TRADING_ITEM_BANANAS)
|
||||
ukuku_prairie.connect(Location().add(TradeSequenceItem(0x087, TRADING_ITEM_HONEYCOMB)), TRADING_ITEM_STICK)
|
||||
self._addEntrance("prairie_left_phone", ukuku_prairie, None, None)
|
||||
self._addEntrance("prairie_right_phone", ukuku_prairie, None, None)
|
||||
self._addEntrance("prairie_left_cave1", ukuku_prairie, Location().add(Chest(0x2CD)), None) # cave next to town
|
||||
self._addEntrance("prairie_left_fairy", ukuku_prairie, None, BOMB)
|
||||
self._addEntranceRequirementExit("prairie_left_fairy", None) # if exiting, you do not need bombs
|
||||
|
||||
prairie_left_cave2 = Location() # Bomb cave
|
||||
Location().add(Chest(0x2F4)).connect(prairie_left_cave2, PEGASUS_BOOTS)
|
||||
Location().add(HeartPiece(0x2E5)).connect(prairie_left_cave2, AND(BOMB, PEGASUS_BOOTS))
|
||||
self._addEntrance("prairie_left_cave2", ukuku_prairie, prairie_left_cave2, BOMB)
|
||||
self._addEntranceRequirementExit("prairie_left_cave2", None) # if exiting, you do not need bombs
|
||||
|
||||
mamu = Location().connect(Location().add(Song(0x2FB)), AND(OCARINA, COUNT("RUPEES", 1480)))
|
||||
self._addEntrance("mamu", ukuku_prairie, mamu, AND(OR(AND(FEATHER, PEGASUS_BOOTS), ROOSTER), OR(HOOKSHOT, ROOSTER), POWER_BRACELET))
|
||||
|
||||
dungeon3_entrance = Location().connect(ukuku_prairie, OR(FEATHER, ROOSTER, FLIPPERS))
|
||||
self._addEntrance("d3", dungeon3_entrance, None, SLIME_KEY)
|
||||
self._addEntranceRequirementExit("d3", None) # if exiting, you do not need to open the door
|
||||
Location().add(Seashell(0x0A5)).connect(dungeon3_entrance, SHOVEL) # above lv3
|
||||
dungeon3_entrance.connect(ukuku_prairie, None, one_way=True) # jump down ledge back to ukuku_prairie
|
||||
|
||||
prairie_island_seashell = Location().add(Seashell(0x0A6)).connect(ukuku_prairie, AND(FLIPPERS, r.bush)) # next to lv3
|
||||
Location().add(Seashell(0x08B)).connect(ukuku_prairie, r.bush) # next to seashell house
|
||||
Location().add(Seashell(0x0A4)).connect(ukuku_prairie, PEGASUS_BOOTS) # smash into tree next to phonehouse
|
||||
self._addEntrance("castle_jump_cave", ukuku_prairie, Location().add(Chest(0x1FD)), OR(AND(FEATHER, PEGASUS_BOOTS), ROOSTER)) # left of the castle, 5 holes turned into 3
|
||||
Location().add(Seashell(0x0B9)).connect(ukuku_prairie, POWER_BRACELET) # under the rock
|
||||
|
||||
left_bay_area = Location()
|
||||
left_bay_area.connect(ghost_hut_outside, OR(AND(FEATHER, PEGASUS_BOOTS), ROOSTER))
|
||||
self._addEntrance("prairie_low_phone", left_bay_area, None, None)
|
||||
|
||||
Location().add(Seashell(0x0E9)).connect(left_bay_area, r.bush) # same screen as mermaid statue
|
||||
tiny_island = Location().add(Seashell(0x0F8)).connect(left_bay_area, AND(OR(FLIPPERS, ROOSTER), r.bush)) # tiny island
|
||||
|
||||
prairie_plateau = Location() # prairie plateau at the owl statue
|
||||
if options.owlstatues == "both" or options.owlstatues == "overworld":
|
||||
prairie_plateau.add(OwlStatue(0x0A8))
|
||||
Location().add(Seashell(0x0A8)).connect(prairie_plateau, SHOVEL) # at the owl statue
|
||||
|
||||
prairie_cave = Location()
|
||||
prairie_cave_secret_exit = Location().connect(prairie_cave, AND(BOMB, OR(FEATHER, ROOSTER)))
|
||||
self._addEntrance("prairie_right_cave_top", ukuku_prairie, prairie_cave, None)
|
||||
self._addEntrance("prairie_right_cave_bottom", left_bay_area, prairie_cave, None)
|
||||
self._addEntrance("prairie_right_cave_high", prairie_plateau, prairie_cave_secret_exit, None)
|
||||
|
||||
bay_madbatter_connector_entrance = Location()
|
||||
bay_madbatter_connector_exit = Location().connect(bay_madbatter_connector_entrance, FLIPPERS)
|
||||
bay_madbatter_connector_outside = Location()
|
||||
bay_madbatter = Location().connect(Location().add(MadBatter(0x1E0)), MAGIC_POWDER)
|
||||
self._addEntrance("prairie_madbatter_connector_entrance", left_bay_area, bay_madbatter_connector_entrance, AND(OR(FEATHER, ROOSTER), OR(SWORD, MAGIC_ROD, BOOMERANG)))
|
||||
self._addEntranceRequirementExit("prairie_madbatter_connector_entrance", AND(OR(FEATHER, ROOSTER), r.bush)) # if exiting, you can pick up the bushes by normal means
|
||||
self._addEntrance("prairie_madbatter_connector_exit", bay_madbatter_connector_outside, bay_madbatter_connector_exit, None)
|
||||
self._addEntrance("prairie_madbatter", bay_madbatter_connector_outside, bay_madbatter, None)
|
||||
|
||||
seashell_mansion = Location()
|
||||
if options.goal != "seashells":
|
||||
Location().add(SeashellMansion(0x2E9)).connect(seashell_mansion, COUNT(SEASHELL, 20))
|
||||
else:
|
||||
seashell_mansion.add(DroppedKey(0x2E9))
|
||||
self._addEntrance("seashell_mansion", ukuku_prairie, seashell_mansion, None)
|
||||
|
||||
bay_water = Location()
|
||||
bay_water.connect(ukuku_prairie, FLIPPERS)
|
||||
bay_water.connect(left_bay_area, FLIPPERS)
|
||||
fisher_under_bridge = Location().add(TradeSequenceItem(0x2F5, TRADING_ITEM_NECKLACE))
|
||||
fisher_under_bridge.connect(bay_water, AND(TRADING_ITEM_FISHING_HOOK, FEATHER, FLIPPERS))
|
||||
bay_water.connect(Location().add(TradeSequenceItem(0x0C9, TRADING_ITEM_SCALE)), AND(TRADING_ITEM_NECKLACE, FLIPPERS))
|
||||
d5_entrance = Location().connect(bay_water, FLIPPERS)
|
||||
self._addEntrance("d5", d5_entrance, None, None)
|
||||
|
||||
# Richard
|
||||
richard_house = Location()
|
||||
richard_cave = Location().connect(richard_house, COUNT(GOLD_LEAF, 5))
|
||||
richard_cave.connect(richard_house, None, one_way=True) # can exit richard's cave even without leaves
|
||||
richard_cave_chest = Location().add(Chest(0x2C8)).connect(richard_cave, OR(FEATHER, HOOKSHOT, ROOSTER))
|
||||
richard_maze = Location()
|
||||
self._addEntrance("richard_house", ukuku_prairie, richard_house, None)
|
||||
self._addEntrance("richard_maze", richard_maze, richard_cave, None)
|
||||
if options.owlstatues == "both" or options.owlstatues == "overworld":
|
||||
Location().add(OwlStatue(0x0C6)).connect(richard_maze, r.bush)
|
||||
Location().add(SlimeKey()).connect(richard_maze, AND(r.bush, SHOVEL))
|
||||
|
||||
next_to_castle = Location()
|
||||
if options.tradequest:
|
||||
ukuku_prairie.connect(next_to_castle, TRADING_ITEM_BANANAS, one_way=True) # can only give bananas from ukuku prairie side
|
||||
else:
|
||||
next_to_castle.connect(ukuku_prairie, None)
|
||||
next_to_castle.connect(ukuku_prairie, FLIPPERS)
|
||||
self._addEntrance("castle_phone", next_to_castle, None, None)
|
||||
castle_secret_entrance_left = Location()
|
||||
castle_secret_entrance_right = Location().connect(castle_secret_entrance_left, FEATHER)
|
||||
castle_courtyard = Location()
|
||||
castle_frontdoor = Location().connect(castle_courtyard, r.bush)
|
||||
castle_frontdoor.connect(ukuku_prairie, "CASTLE_BUTTON") # the button in the castle connector allows access to the castle grounds in ER
|
||||
self._addEntrance("castle_secret_entrance", next_to_castle, castle_secret_entrance_right, OR(BOMB, BOOMERANG, MAGIC_POWDER, MAGIC_ROD, SWORD))
|
||||
self._addEntrance("castle_secret_exit", castle_courtyard, castle_secret_entrance_left, None)
|
||||
|
||||
Location().add(HeartPiece(0x078)).connect(bay_water, FLIPPERS) # in the moat of the castle
|
||||
castle_inside = Location()
|
||||
Location().add(KeyLocation("CASTLE_BUTTON")).connect(castle_inside, None)
|
||||
castle_top_outside = Location()
|
||||
castle_top_inside = Location()
|
||||
self._addEntrance("castle_main_entrance", castle_frontdoor, castle_inside, r.bush)
|
||||
self._addEntrance("castle_upper_left", castle_top_outside, castle_inside, None)
|
||||
self._addEntrance("castle_upper_right", castle_top_outside, castle_top_inside, None)
|
||||
Location().add(GoldLeaf(0x05A)).connect(castle_courtyard, OR(SWORD, BOW, MAGIC_ROD)) # mad bomber, enemy hiding in the 6 holes
|
||||
crow_gold_leaf = Location().add(GoldLeaf(0x058)).connect(castle_courtyard, AND(POWER_BRACELET, r.attack_hookshot_no_bomb)) # bird on tree, can't kill with bomb cause it flies off. immune to magic_powder
|
||||
Location().add(GoldLeaf(0x2D2)).connect(castle_inside, r.attack_hookshot_powder) # in the castle, kill enemies
|
||||
Location().add(GoldLeaf(0x2C5)).connect(castle_inside, AND(BOMB, r.attack_hookshot_powder)) # in the castle, bomb wall to show enemy
|
||||
kanalet_chain_trooper = Location().add(GoldLeaf(0x2C6)) # in the castle, spinning spikeball enemy
|
||||
castle_top_inside.connect(kanalet_chain_trooper, AND(POWER_BRACELET, r.attack_hookshot), one_way=True)
|
||||
|
||||
animal_village = Location()
|
||||
animal_village.connect(Location().add(TradeSequenceItem(0x0CD, TRADING_ITEM_FISHING_HOOK)), TRADING_ITEM_BROOM)
|
||||
cookhouse = Location()
|
||||
cookhouse.connect(Location().add(TradeSequenceItem(0x2D7, TRADING_ITEM_PINEAPPLE)), TRADING_ITEM_HONEYCOMB)
|
||||
goathouse = Location()
|
||||
goathouse.connect(Location().add(TradeSequenceItem(0x2D9, TRADING_ITEM_LETTER)), TRADING_ITEM_HIBISCUS)
|
||||
mermaid_statue = Location()
|
||||
mermaid_statue.connect(animal_village, AND(TRADING_ITEM_SCALE, HOOKSHOT))
|
||||
mermaid_statue.add(TradeSequenceItem(0x297, TRADING_ITEM_MAGNIFYING_GLASS))
|
||||
self._addEntrance("animal_phone", animal_village, None, None)
|
||||
self._addEntrance("animal_house1", animal_village, None, None)
|
||||
self._addEntrance("animal_house2", animal_village, None, None)
|
||||
self._addEntrance("animal_house3", animal_village, goathouse, None)
|
||||
self._addEntrance("animal_house4", animal_village, None, None)
|
||||
self._addEntrance("animal_house5", animal_village, cookhouse, None)
|
||||
animal_village.connect(bay_water, FLIPPERS)
|
||||
animal_village.connect(ukuku_prairie, OR(HOOKSHOT, ROOSTER))
|
||||
animal_village_connector_left = Location()
|
||||
animal_village_connector_right = Location().connect(animal_village_connector_left, PEGASUS_BOOTS)
|
||||
self._addEntrance("prairie_to_animal_connector", ukuku_prairie, animal_village_connector_left, OR(BOMB, BOOMERANG, MAGIC_POWDER, MAGIC_ROD, SWORD)) # passage under river blocked by bush
|
||||
self._addEntrance("animal_to_prairie_connector", animal_village, animal_village_connector_right, None)
|
||||
if options.owlstatues == "both" or options.owlstatues == "overworld":
|
||||
animal_village.add(OwlStatue(0x0DA))
|
||||
Location().add(Seashell(0x0DA)).connect(animal_village, SHOVEL) # owl statue at the water
|
||||
desert = Location().connect(animal_village, r.bush) # Note: We moved the walrus blocking the desert.
|
||||
if options.owlstatues == "both" or options.owlstatues == "overworld":
|
||||
desert.add(OwlStatue(0x0CF))
|
||||
desert_lanmola = Location().add(AnglerKey()).connect(desert, OR(BOW, SWORD, HOOKSHOT, MAGIC_ROD, BOOMERANG))
|
||||
|
||||
animal_village_bombcave = Location()
|
||||
self._addEntrance("animal_cave", desert, animal_village_bombcave, BOMB)
|
||||
self._addEntranceRequirementExit("animal_cave", None) # if exiting, you do not need bombs
|
||||
animal_village_bombcave_heartpiece = Location().add(HeartPiece(0x2E6)).connect(animal_village_bombcave, OR(AND(BOMB, FEATHER, HOOKSHOT), ROOSTER)) # cave in the upper right of animal town
|
||||
|
||||
desert_cave = Location()
|
||||
self._addEntrance("desert_cave", desert, desert_cave, None)
|
||||
desert.connect(desert_cave, None, one_way=True) # Drop down the sinkhole
|
||||
|
||||
Location().add(HeartPiece(0x1E8)).connect(desert_cave, BOMB) # above the quicksand cave
|
||||
Location().add(Seashell(0x0FF)).connect(desert, POWER_BRACELET) # bottom right corner of the map
|
||||
|
||||
armos_maze = Location().connect(animal_village, POWER_BRACELET)
|
||||
armos_temple = Location()
|
||||
Location().add(FaceKey()).connect(armos_temple, r.miniboss_requirements[world_setup.miniboss_mapping["armos_temple"]])
|
||||
if options.owlstatues == "both" or options.owlstatues == "overworld":
|
||||
armos_maze.add(OwlStatue(0x08F))
|
||||
self._addEntrance("armos_maze_cave", armos_maze, Location().add(Chest(0x2FC)), None)
|
||||
self._addEntrance("armos_temple", armos_maze, armos_temple, None)
|
||||
|
||||
armos_fairy_entrance = Location().connect(bay_water, FLIPPERS).connect(animal_village, POWER_BRACELET)
|
||||
self._addEntrance("armos_fairy", armos_fairy_entrance, None, BOMB)
|
||||
self._addEntranceRequirementExit("armos_fairy", None) # if exiting, you do not need bombs
|
||||
|
||||
d6_connector_left = Location()
|
||||
d6_connector_right = Location().connect(d6_connector_left, OR(AND(HOOKSHOT, OR(FLIPPERS, AND(FEATHER, PEGASUS_BOOTS))), ROOSTER))
|
||||
d6_entrance = Location()
|
||||
d6_entrance.connect(bay_water, FLIPPERS, one_way=True)
|
||||
d6_armos_island = Location().connect(bay_water, FLIPPERS)
|
||||
self._addEntrance("d6_connector_entrance", d6_armos_island, d6_connector_right, None)
|
||||
self._addEntrance("d6_connector_exit", d6_entrance, d6_connector_left, None)
|
||||
self._addEntrance("d6", d6_entrance, None, FACE_KEY)
|
||||
self._addEntranceRequirementExit("d6", None) # if exiting, you do not need to open the dungeon
|
||||
|
||||
windfish_egg = Location().connect(swamp, POWER_BRACELET).connect(graveyard, POWER_BRACELET)
|
||||
windfish_egg.connect(graveyard, None, one_way=True) # Ledge jump
|
||||
|
||||
obstacle_cave_entrance = Location()
|
||||
obstacle_cave_inside = Location().connect(obstacle_cave_entrance, SWORD)
|
||||
obstacle_cave_inside.connect(obstacle_cave_entrance, FEATHER, one_way=True) # can get past the rock room from right to left pushing blocks and jumping over the pit
|
||||
obstacle_cave_inside_chest = Location().add(Chest(0x2BB)).connect(obstacle_cave_inside, OR(HOOKSHOT, ROOSTER)) # chest at obstacles
|
||||
obstacle_cave_exit = Location().connect(obstacle_cave_inside, OR(PEGASUS_BOOTS, ROOSTER))
|
||||
|
||||
lower_right_taltal = Location()
|
||||
self._addEntrance("obstacle_cave_entrance", windfish_egg, obstacle_cave_entrance, POWER_BRACELET)
|
||||
self._addEntrance("obstacle_cave_outside_chest", Location().add(Chest(0x018)), obstacle_cave_inside, None)
|
||||
self._addEntrance("obstacle_cave_exit", lower_right_taltal, obstacle_cave_exit, None)
|
||||
|
||||
papahl_cave = Location().add(Chest(0x28A))
|
||||
papahl = Location().connect(lower_right_taltal, None, one_way=True)
|
||||
hibiscus_item = Location().add(TradeSequenceItem(0x019, TRADING_ITEM_HIBISCUS))
|
||||
papahl.connect(hibiscus_item, TRADING_ITEM_PINEAPPLE, one_way=True)
|
||||
self._addEntrance("papahl_entrance", lower_right_taltal, papahl_cave, None)
|
||||
self._addEntrance("papahl_exit", papahl, papahl_cave, None)
|
||||
|
||||
# D4 entrance and related things
|
||||
below_right_taltal = Location().connect(windfish_egg, POWER_BRACELET)
|
||||
below_right_taltal.add(KeyLocation("ANGLER_KEYHOLE"))
|
||||
below_right_taltal.connect(bay_water, FLIPPERS)
|
||||
below_right_taltal.connect(next_to_castle, ROOSTER) # fly from staircase to staircase on the north side of the moat
|
||||
lower_right_taltal.connect(below_right_taltal, FLIPPERS, one_way=True)
|
||||
|
||||
heartpiece_swim_cave = Location().connect(Location().add(HeartPiece(0x1F2)), FLIPPERS)
|
||||
self._addEntrance("heartpiece_swim_cave", below_right_taltal, heartpiece_swim_cave, FLIPPERS) # cave next to level 4
|
||||
d4_entrance = Location().connect(below_right_taltal, FLIPPERS)
|
||||
lower_right_taltal.connect(d4_entrance, AND(ANGLER_KEY, "ANGLER_KEYHOLE"), one_way=True)
|
||||
self._addEntrance("d4", d4_entrance, None, ANGLER_KEY)
|
||||
self._addEntranceRequirementExit("d4", FLIPPERS) # if exiting, you can leave with flippers without opening the dungeon
|
||||
mambo = Location().connect(Location().add(Song(0x2FD)), AND(OCARINA, FLIPPERS)) # Manbo's Mambo
|
||||
self._addEntrance("mambo", d4_entrance, mambo, FLIPPERS)
|
||||
|
||||
# Raft game.
|
||||
raft_house = Location("Raft House")
|
||||
Location().add(KeyLocation("RAFT")).connect(raft_house, COUNT("RUPEES", 100))
|
||||
raft_return_upper = Location()
|
||||
raft_return_lower = Location().connect(raft_return_upper, None, one_way=True)
|
||||
outside_raft_house = Location().connect(below_right_taltal, HOOKSHOT).connect(below_right_taltal, FLIPPERS, one_way=True)
|
||||
raft_game = Location()
|
||||
raft_game.connect(outside_raft_house, "RAFT")
|
||||
raft_game.add(Chest(0x05C), Chest(0x05D)) # Chests in the rafting game
|
||||
raft_exit = Location()
|
||||
if options.logic != "casual": # use raft to reach north armos maze entrances without flippers
|
||||
raft_game.connect(raft_exit, None, one_way=True)
|
||||
raft_game.connect(armos_fairy_entrance, None, one_way=True)
|
||||
self._addEntrance("raft_return_exit", outside_raft_house, raft_return_upper, None)
|
||||
self._addEntrance("raft_return_enter", raft_exit, raft_return_lower, None)
|
||||
raft_exit.connect(armos_fairy_entrance, FLIPPERS)
|
||||
self._addEntrance("raft_house", outside_raft_house, raft_house, None)
|
||||
if options.owlstatues == "both" or options.owlstatues == "overworld":
|
||||
raft_game.add(OwlStatue(0x5D))
|
||||
|
||||
outside_rooster_house = Location().connect(lower_right_taltal, OR(FLIPPERS, ROOSTER))
|
||||
self._addEntrance("rooster_house", outside_rooster_house, None, None)
|
||||
bird_cave = Location()
|
||||
bird_key = Location().add(BirdKey())
|
||||
bird_cave.connect(bird_key, OR(AND(FEATHER, COUNT(POWER_BRACELET, 2)), ROOSTER))
|
||||
if options.logic != "casual":
|
||||
bird_cave.connect(lower_right_taltal, None, one_way=True) # Drop in a hole at bird cave
|
||||
self._addEntrance("bird_cave", outside_rooster_house, bird_cave, None)
|
||||
bridge_seashell = Location().add(Seashell(0x00C)).connect(outside_rooster_house, AND(OR(FEATHER, ROOSTER), POWER_BRACELET)) # seashell right of rooster house, there is a hole in the bridge
|
||||
|
||||
multichest_cave = Location()
|
||||
multichest_cave_secret = Location().connect(multichest_cave, BOMB)
|
||||
water_cave_hole = Location() # Location with the hole that drops you onto the hearth piece under water
|
||||
if options.logic != "casual":
|
||||
water_cave_hole.connect(heartpiece_swim_cave, FLIPPERS, one_way=True)
|
||||
multichest_outside = Location().add(Chest(0x01D)) # chest after multichest puzzle outside
|
||||
self._addEntrance("multichest_left", lower_right_taltal, multichest_cave, OR(FLIPPERS, ROOSTER))
|
||||
self._addEntrance("multichest_right", water_cave_hole, multichest_cave, None)
|
||||
self._addEntrance("multichest_top", multichest_outside, multichest_cave_secret, None)
|
||||
if options.owlstatues == "both" or options.owlstatues == "overworld":
|
||||
water_cave_hole.add(OwlStatue(0x1E)) # owl statue below d7
|
||||
|
||||
right_taltal_connector1 = Location()
|
||||
right_taltal_connector_outside1 = Location()
|
||||
right_taltal_connector2 = Location()
|
||||
right_taltal_connector3 = Location()
|
||||
right_taltal_connector2.connect(right_taltal_connector3, AND(OR(FEATHER, ROOSTER), HOOKSHOT), one_way=True)
|
||||
right_taltal_connector_outside2 = Location()
|
||||
right_taltal_connector4 = Location()
|
||||
d7_platau = Location()
|
||||
d7_tower = Location()
|
||||
d7_platau.connect(d7_tower, AND(POWER_BRACELET, BIRD_KEY), one_way=True)
|
||||
self._addEntrance("right_taltal_connector1", water_cave_hole, right_taltal_connector1, None)
|
||||
self._addEntrance("right_taltal_connector2", right_taltal_connector_outside1, right_taltal_connector1, None)
|
||||
self._addEntrance("right_taltal_connector3", right_taltal_connector_outside1, right_taltal_connector2, None)
|
||||
self._addEntrance("right_taltal_connector4", right_taltal_connector_outside2, right_taltal_connector3, None)
|
||||
self._addEntrance("right_taltal_connector5", right_taltal_connector_outside2, right_taltal_connector4, None)
|
||||
self._addEntrance("right_taltal_connector6", d7_platau, right_taltal_connector4, None)
|
||||
self._addEntrance("right_fairy", right_taltal_connector_outside2, None, BOMB)
|
||||
self._addEntranceRequirementExit("right_fairy", None) # if exiting, you do not need bombs
|
||||
self._addEntrance("d7", d7_tower, None, None)
|
||||
if options.logic != "casual": # D7 area ledge drops
|
||||
d7_platau.connect(heartpiece_swim_cave, FLIPPERS, one_way=True)
|
||||
d7_platau.connect(right_taltal_connector_outside1, None, one_way=True)
|
||||
|
||||
mountain_bridge_staircase = Location().connect(outside_rooster_house, OR(HOOKSHOT, ROOSTER)) # cross bridges to staircase
|
||||
if options.logic != "casual": # ledge drop
|
||||
mountain_bridge_staircase.connect(windfish_egg, None, one_way=True)
|
||||
|
||||
left_right_connector_cave_entrance = Location()
|
||||
left_right_connector_cave_exit = Location()
|
||||
left_right_connector_cave_entrance.connect(left_right_connector_cave_exit, OR(HOOKSHOT, ROOSTER), one_way=True) # pass through the underground passage to left side
|
||||
taltal_boulder_zone = Location()
|
||||
self._addEntrance("left_to_right_taltalentrance", mountain_bridge_staircase, left_right_connector_cave_entrance, OR(BOMB, BOOMERANG, MAGIC_POWDER, MAGIC_ROD, SWORD))
|
||||
self._addEntrance("left_taltal_entrance", taltal_boulder_zone, left_right_connector_cave_exit, None)
|
||||
mountain_heartpiece = Location().add(HeartPiece(0x2BA)) # heartpiece in connecting cave
|
||||
left_right_connector_cave_entrance.connect(mountain_heartpiece, BOMB, one_way=True) # in the connecting cave from right to left. one_way to prevent access to left_side_mountain via glitched logic
|
||||
|
||||
taltal_boulder_zone.add(Chest(0x004)) # top of falling rocks hill
|
||||
taltal_madbatter = Location().connect(Location().add(MadBatter(0x1E2)), MAGIC_POWDER)
|
||||
self._addEntrance("madbatter_taltal", taltal_boulder_zone, taltal_madbatter, POWER_BRACELET)
|
||||
self._addEntranceRequirementExit("madbatter_taltal", None) # if exiting, you do not need bracelet
|
||||
|
||||
outside_fire_cave = Location()
|
||||
if options.logic != "casual":
|
||||
outside_fire_cave.connect(writes_hut_outside, None, one_way=True) # Jump down the ledge
|
||||
taltal_boulder_zone.connect(outside_fire_cave, None, one_way=True)
|
||||
fire_cave_bottom = Location()
|
||||
fire_cave_top = Location().connect(fire_cave_bottom, COUNT(SHIELD, 2))
|
||||
self._addEntrance("fire_cave_entrance", outside_fire_cave, fire_cave_bottom, BOMB)
|
||||
self._addEntranceRequirementExit("fire_cave_entrance", None) # if exiting, you do not need bombs
|
||||
|
||||
d8_entrance = Location()
|
||||
if options.logic != "casual":
|
||||
d8_entrance.connect(writes_hut_outside, None, one_way=True) # Jump down the ledge
|
||||
d8_entrance.connect(outside_fire_cave, None, one_way=True) # Jump down the other ledge
|
||||
self._addEntrance("fire_cave_exit", d8_entrance, fire_cave_top, None)
|
||||
self._addEntrance("phone_d8", d8_entrance, None, None)
|
||||
self._addEntrance("d8", d8_entrance, None, AND(OCARINA, SONG3, SWORD))
|
||||
self._addEntranceRequirementExit("d8", None) # if exiting, you do not need to wake the turtle
|
||||
|
||||
nightmare = Location("Nightmare")
|
||||
windfish = Location("Windfish").connect(nightmare, AND(MAGIC_POWDER, SWORD, OR(BOOMERANG, BOW)))
|
||||
|
||||
if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell':
|
||||
hookshot_cave.connect(hookshot_cave_chest, AND(FEATHER, PEGASUS_BOOTS)) # boots jump the gap to the chest
|
||||
graveyard_cave_left.connect(graveyard_cave_right, HOOKSHOT, one_way=True) # hookshot the block behind the stairs while over the pit
|
||||
swamp_chest.connect(swamp, None) # Clip past the flower
|
||||
self._addEntranceRequirement("d2", POWER_BRACELET) # clip the top wall to walk between the goponga flower and the wall
|
||||
self._addEntranceRequirement("d2", COUNT(SWORD, 2)) # use l2 sword spin to kill goponga flowers
|
||||
swamp.connect(writes_hut_outside, HOOKSHOT, one_way=True) # hookshot the sign in front of writes hut
|
||||
graveyard_heartpiece.connect(graveyard_cave_right, FEATHER) # jump to the bottom right tile around the blocks
|
||||
graveyard_heartpiece.connect(graveyard_cave_right, OR(HOOKSHOT, BOOMERANG)) # push bottom block, wall clip and hookshot/boomerang corner to grab item
|
||||
|
||||
self._addEntranceRequirement("mamu", AND(FEATHER, POWER_BRACELET)) # can clear the gaps at the start with just feather, can reach bottom left sign with a well timed jump while wall clipped
|
||||
self._addEntranceRequirement("prairie_madbatter_connector_entrance", AND(OR(FEATHER, ROOSTER), OR(MAGIC_POWDER, BOMB))) # use bombs or powder to get rid of a bush on the other side by jumping across and placing the bomb/powder before you fall into the pit
|
||||
fisher_under_bridge.connect(bay_water, AND(TRADING_ITEM_FISHING_HOOK, FLIPPERS)) # can talk to the fisherman from the water when the boat is low (requires swimming up out of the water a bit)
|
||||
crow_gold_leaf.connect(castle_courtyard, POWER_BRACELET) # bird on tree at left side kanalet, can use both rocks to kill the crow removing the kill requirement
|
||||
castle_inside.connect(kanalet_chain_trooper, BOOMERANG, one_way=True) # kill the ball and chain trooper from the left side, then use boomerang to grab the dropped item
|
||||
animal_village_bombcave_heartpiece.connect(animal_village_bombcave, AND(PEGASUS_BOOTS, FEATHER)) # jump across horizontal 4 gap to heart piece
|
||||
desert_lanmola.connect(desert, BOMB) # use bombs to kill lanmola
|
||||
|
||||
d6_connector_left.connect(d6_connector_right, AND(OR(FLIPPERS, PEGASUS_BOOTS), FEATHER)) # jump the gap in underground passage to d6 left side to skip hookshot
|
||||
bird_key.connect(bird_cave, COUNT(POWER_BRACELET, 2)) # corner walk past the one pit on the left side to get to the elephant statue
|
||||
fire_cave_bottom.connect(fire_cave_top, PEGASUS_BOOTS, one_way=True) # flame skip
|
||||
|
||||
if options.logic == 'glitched' or options.logic == 'hell':
|
||||
#self._addEntranceRequirement("dream_hut", FEATHER) # text clip TODO: require nag messages
|
||||
self._addEntranceRequirementEnter("dream_hut", HOOKSHOT) # clip past the rocks in front of dream hut
|
||||
dream_hut_right.connect(dream_hut_left, FEATHER) # super jump
|
||||
forest.connect(swamp, BOMB) # bomb trigger tarin
|
||||
forest.connect(forest_heartpiece, BOMB, one_way=True) # bomb trigger heartpiece
|
||||
self._addEntranceRequirementEnter("hookshot_cave", HOOKSHOT) # clip past the rocks in front of hookshot cave
|
||||
swamp.connect(forest_toadstool, None, one_way=True) # villa buffer from top (swamp phonebooth area) to bottom (toadstool area)
|
||||
writes_hut_outside.connect(swamp, None, one_way=True) # villa buffer from top (writes hut) to bottom (swamp phonebooth area) or damage boost
|
||||
graveyard.connect(forest_heartpiece, None, one_way=True) # villa buffer from top.
|
||||
log_cave_heartpiece.connect(forest_cave, FEATHER) # super jump
|
||||
log_cave_heartpiece.connect(forest_cave, BOMB) # bomb trigger
|
||||
graveyard_cave_left.connect(graveyard_heartpiece, BOMB, one_way=True) # bomb trigger the heartpiece from the left side
|
||||
graveyard_heartpiece.connect(graveyard_cave_right, None) # sideways block push from the right staircase.
|
||||
|
||||
prairie_island_seashell.connect(ukuku_prairie, AND(FEATHER, r.bush)) # jesus jump from right side, screen transition on top of the water to reach the island
|
||||
self._addEntranceRequirement("castle_jump_cave", FEATHER) # 1 pit buffer to clip bottom wall and jump across.
|
||||
left_bay_area.connect(ghost_hut_outside, FEATHER) # 1 pit buffer to get across
|
||||
tiny_island.connect(left_bay_area, AND(FEATHER, r.bush)) # jesus jump around
|
||||
bay_madbatter_connector_exit.connect(bay_madbatter_connector_entrance, FEATHER, one_way=True) # jesus jump (3 screen) through the underground passage leading to martha's bay mad batter
|
||||
self._addEntranceRequirement("prairie_madbatter_connector_entrance", AND(FEATHER, POWER_BRACELET)) # villa buffer into the top side of the bush, then pick it up
|
||||
|
||||
ukuku_prairie.connect(richard_maze, OR(BOMB, BOOMERANG, MAGIC_POWDER, MAGIC_ROD, SWORD), one_way=True) # break bushes on north side of the maze, and 1 pit buffer into the maze
|
||||
fisher_under_bridge.connect(bay_water, AND(BOMB, FLIPPERS)) # can bomb trigger the item without having the hook
|
||||
animal_village.connect(ukuku_prairie, FEATHER) # jesus jump
|
||||
below_right_taltal.connect(next_to_castle, FEATHER) # jesus jump (north of kanalet castle phonebooth)
|
||||
animal_village_connector_right.connect(animal_village_connector_left, FEATHER) # text clip past the obstacles (can go both ways), feather to wall clip the obstacle without triggering text or shaq jump in bottom right corner if text is off
|
||||
animal_village_bombcave_heartpiece.connect(animal_village_bombcave, AND(BOMB, OR(HOOKSHOT, FEATHER, PEGASUS_BOOTS))) # bomb trigger from right side, corner walking top right pit is stupid so hookshot or boots added
|
||||
animal_village_bombcave_heartpiece.connect(animal_village_bombcave, FEATHER) # villa buffer across the pits
|
||||
|
||||
d6_entrance.connect(ukuku_prairie, FEATHER, one_way=True) # jesus jump (2 screen) from d6 entrance bottom ledge to ukuku prairie
|
||||
d6_entrance.connect(armos_fairy_entrance, FEATHER, one_way=True) # jesus jump (2 screen) from d6 entrance top ledge to armos fairy entrance
|
||||
armos_fairy_entrance.connect(d6_armos_island, FEATHER, one_way=True) # jesus jump from top (fairy bomb cave) to armos island
|
||||
armos_fairy_entrance.connect(raft_exit, FEATHER) # jesus jump (2-ish screen) from fairy cave to lower raft connector
|
||||
self._addEntranceRequirementEnter("obstacle_cave_entrance", HOOKSHOT) # clip past the rocks in front of obstacle cave entrance
|
||||
obstacle_cave_inside_chest.connect(obstacle_cave_inside, FEATHER) # jump to the rightmost pits + 1 pit buffer to jump across
|
||||
obstacle_cave_exit.connect(obstacle_cave_inside, FEATHER) # 1 pit buffer above boots crystals to get past
|
||||
lower_right_taltal.connect(hibiscus_item, AND(TRADING_ITEM_PINEAPPLE, BOMB), one_way=True) # bomb trigger papahl from below ledge, requires pineapple
|
||||
|
||||
self._addEntranceRequirement("heartpiece_swim_cave", FEATHER) # jesus jump into the cave entrance after jumping down the ledge, can jesus jump back to the ladder 1 screen below
|
||||
self._addEntranceRequirement("mambo", FEATHER) # jesus jump from (unlocked) d4 entrance to mambo's cave entrance
|
||||
outside_raft_house.connect(below_right_taltal, FEATHER, one_way=True) # jesus jump from the ledge at raft to the staircase 1 screen south
|
||||
|
||||
self._addEntranceRequirement("multichest_left", FEATHER) # jesus jump past staircase leading up the mountain
|
||||
outside_rooster_house.connect(lower_right_taltal, FEATHER) # jesus jump (1 or 2 screen depending if angler key is used) to staircase leading up the mountain
|
||||
d7_platau.connect(water_cave_hole, None, one_way=True) # use save and quit menu to gain control while falling to dodge the water cave hole
|
||||
mountain_bridge_staircase.connect(outside_rooster_house, AND(PEGASUS_BOOTS, FEATHER)) # cross bridge to staircase with pit buffer to clip bottom wall and jump across
|
||||
bird_key.connect(bird_cave, AND(FEATHER, HOOKSHOT)) # hookshot jump across the big pits room
|
||||
right_taltal_connector2.connect(right_taltal_connector3, None, one_way=True) # 2 seperate pit buffers so not obnoxious to get past the two pit rooms before d7 area. 2nd pits can pit buffer on top right screen, bottom wall to scroll on top of the wall on bottom screen
|
||||
left_right_connector_cave_exit.connect(left_right_connector_cave_entrance, AND(HOOKSHOT, FEATHER), one_way=True) # pass through the passage in reverse using a superjump to get out of the dead end
|
||||
obstacle_cave_inside.connect(mountain_heartpiece, BOMB, one_way=True) # bomb trigger from boots crystal cave
|
||||
self._addEntranceRequirement("d8", OR(BOMB, AND(OCARINA, SONG3))) # bomb trigger the head and walk trough, or play the ocarina song 3 and walk through
|
||||
|
||||
if options.logic == 'hell':
|
||||
dream_hut_right.connect(dream_hut, None) # alternate diagonal movement with orthogonal movement to control the mimics. Get them clipped into the walls to walk past
|
||||
swamp.connect(forest_toadstool, None) # damage boost from toadstool area across the pit
|
||||
swamp.connect(forest, AND(r.bush, OR(PEGASUS_BOOTS, HOOKSHOT))) # boots bonk / hookshot spam over the pits right of forest_rear_chest
|
||||
forest.connect(forest_heartpiece, PEGASUS_BOOTS, one_way=True) # boots bonk across the pits
|
||||
log_cave_heartpiece.connect(forest_cave, BOOMERANG) # clip the boomerang through the corner gaps on top right to grab the item
|
||||
log_cave_heartpiece.connect(forest_cave, AND(ROOSTER, OR(PEGASUS_BOOTS, SWORD, BOW, MAGIC_ROD))) # boots rooster hop in bottom left corner to "superjump" into the area. use buffers after picking up rooster to gain height / time to throw rooster again facing up
|
||||
writes_hut_outside.connect(swamp, None) # damage boost with moblin arrow next to telephone booth
|
||||
writes_cave_left_chest.connect(writes_cave, None) # damage boost off the zol to get across the pit.
|
||||
graveyard.connect(crazy_tracy_hut, HOOKSHOT, one_way=True) # use hookshot spam to clip the rock on the right with the crow
|
||||
graveyard.connect(forest, OR(PEGASUS_BOOTS, HOOKSHOT)) # boots bonk witches hut, or hookshot spam across the pit
|
||||
graveyard_cave_left.connect(graveyard_cave_right, HOOKSHOT) # hookshot spam over the pit
|
||||
graveyard_cave_right.connect(graveyard_cave_left, PEGASUS_BOOTS, one_way=True) # boots bonk off the cracked block
|
||||
|
||||
self._addEntranceRequirementEnter("mamu", AND(PEGASUS_BOOTS, POWER_BRACELET)) # can clear the gaps at the start with multiple pit buffers, can reach bottom left sign with bonking along the bottom wall
|
||||
self._addEntranceRequirement("castle_jump_cave", PEGASUS_BOOTS) # pit buffer to clip bottom wall and boots bonk across
|
||||
prairie_cave_secret_exit.connect(prairie_cave, AND(BOMB, OR(PEGASUS_BOOTS, HOOKSHOT))) # hookshot spam or boots bonk across pits can go from left to right by pit buffering on top of the bottom wall then boots bonk across
|
||||
richard_cave_chest.connect(richard_cave, None) # use the zol on the other side of the pit to damage boost across (requires damage from pit + zol)
|
||||
castle_secret_entrance_right.connect(castle_secret_entrance_left, AND(PEGASUS_BOOTS, "MEDICINE2")) # medicine iframe abuse to get across spikes with a boots bonk
|
||||
left_bay_area.connect(ghost_hut_outside, PEGASUS_BOOTS) # multiple pit buffers to bonk across the bottom wall
|
||||
tiny_island.connect(left_bay_area, AND(PEGASUS_BOOTS, r.bush)) # jesus jump around with boots bonks, then one final bonk off the bottom wall to get on the staircase (needs to be centered correctly)
|
||||
self._addEntranceRequirement("prairie_madbatter_connector_entrance", AND(PEGASUS_BOOTS, OR(MAGIC_POWDER, BOMB, SWORD, MAGIC_ROD, BOOMERANG))) # Boots bonk across the bottom wall, then remove one of the bushes to get on land
|
||||
self._addEntranceRequirementExit("prairie_madbatter_connector_entrance", AND(PEGASUS_BOOTS, r.bush)) # if exiting, you can pick up the bushes by normal means and boots bonk across the bottom wall
|
||||
|
||||
# bay_water connectors, only left_bay_area, ukuku_prairie and animal_village have to be connected with jesus jumps. below_right_taltal, d6_armos_island and armos_fairy_entrance are accounted for via ukuku prairie in glitch logic
|
||||
left_bay_area.connect(bay_water, FEATHER) # jesus jump (can always reach bay_water with jesus jumping from every way to enter bay_water, so no one_way)
|
||||
animal_village.connect(bay_water, FEATHER) # jesus jump (can always reach bay_water with jesus jumping from every way to enter bay_water, so no one_way)
|
||||
ukuku_prairie.connect(bay_water, FEATHER, one_way=True) # jesus jump
|
||||
bay_water.connect(d5_entrance, FEATHER) # jesus jump into d5 entrance (wall clip), wall clip + jesus jump to get out
|
||||
|
||||
crow_gold_leaf.connect(castle_courtyard, BOMB) # bird on tree at left side kanalet, place a bomb against the tree and the crow flies off. With well placed second bomb the crow can be killed
|
||||
mermaid_statue.connect(animal_village, AND(TRADING_ITEM_SCALE, FEATHER)) # early mermaid statue by buffering on top of the right ledge, then superjumping to the left (horizontal pixel perfect)
|
||||
animal_village_bombcave_heartpiece.connect(animal_village_bombcave, PEGASUS_BOOTS) # boots bonk across bottom wall (both at entrance and in item room)
|
||||
|
||||
d6_armos_island.connect(ukuku_prairie, FEATHER) # jesus jump (3 screen) from seashell mansion to armos island
|
||||
armos_fairy_entrance.connect(d6_armos_island, PEGASUS_BOOTS, one_way=True) # jesus jump from top (fairy bomb cave) to armos island with fast falling
|
||||
d6_connector_right.connect(d6_connector_left, PEGASUS_BOOTS) # boots bonk across bottom wall at water and pits (can do both ways)
|
||||
|
||||
obstacle_cave_entrance.connect(obstacle_cave_inside, OR(HOOKSHOT, AND(FEATHER, PEGASUS_BOOTS, OR(SWORD, MAGIC_ROD, BOW)))) # get past crystal rocks by hookshotting into top pushable block, or boots dashing into top wall where the pushable block is to superjump down
|
||||
obstacle_cave_entrance.connect(obstacle_cave_inside, AND(PEGASUS_BOOTS, ROOSTER)) # get past crystal rocks pushing the top pushable block, then boots dashing up picking up the rooster before bonking. Pause buffer until rooster is fully picked up then throw it down before bonking into wall
|
||||
d4_entrance.connect(below_right_taltal, FEATHER) # jesus jump a long way
|
||||
if options.entranceshuffle in ("default", "simple"): # connector cave from armos d6 area to raft shop may not be randomized to add a flippers path since flippers stop you from jesus jumping
|
||||
below_right_taltal.connect(raft_game, AND(FEATHER, r.attack_hookshot_powder), one_way=True) # jesus jump from heartpiece water cave, around the island and clip past the diagonal gap in the rock, then jesus jump all the way down the waterfall to the chests (attack req for hardlock flippers+feather scenario)
|
||||
outside_raft_house.connect(below_right_taltal, AND(FEATHER, PEGASUS_BOOTS)) #superjump from ledge left to right, can buffer to land on ledge instead of water, then superjump right which is pixel perfect
|
||||
bridge_seashell.connect(outside_rooster_house, AND(PEGASUS_BOOTS, POWER_BRACELET)) # boots bonk
|
||||
bird_key.connect(bird_cave, AND(FEATHER, PEGASUS_BOOTS)) # boots jump above wall, use multiple pit buffers to get across
|
||||
mountain_bridge_staircase.connect(outside_rooster_house, OR(PEGASUS_BOOTS, FEATHER)) # cross bridge to staircase with pit buffer to clip bottom wall and jump or boots bonk across
|
||||
left_right_connector_cave_entrance.connect(left_right_connector_cave_exit, AND(PEGASUS_BOOTS, FEATHER), one_way=True) # boots jump to bottom left corner of pits, pit buffer and jump to left
|
||||
left_right_connector_cave_exit.connect(left_right_connector_cave_entrance, AND(ROOSTER, OR(PEGASUS_BOOTS, SWORD, BOW, MAGIC_ROD)), one_way=True) # pass through the passage in reverse using a boots rooster hop or rooster superjump in the one way passage area
|
||||
|
||||
self.start = start_house
|
||||
self.egg = windfish_egg
|
||||
self.nightmare = nightmare
|
||||
self.windfish = windfish
|
||||
|
||||
def _addEntrance(self, name, outside, inside, requirement):
|
||||
assert name not in self.overworld_entrance, "Duplicate entrance: %s" % name
|
||||
assert name in ENTRANCE_INFO
|
||||
self.overworld_entrance[name] = EntranceExterior(outside, requirement)
|
||||
self.indoor_location[name] = inside
|
||||
|
||||
def _addEntranceRequirement(self, name, requirement):
|
||||
assert name in self.overworld_entrance
|
||||
self.overworld_entrance[name].addRequirement(requirement)
|
||||
|
||||
def _addEntranceRequirementEnter(self, name, requirement):
|
||||
assert name in self.overworld_entrance
|
||||
self.overworld_entrance[name].addEnterRequirement(requirement)
|
||||
|
||||
def _addEntranceRequirementExit(self, name, requirement):
|
||||
assert name in self.overworld_entrance
|
||||
self.overworld_entrance[name].addExitRequirement(requirement)
|
||||
|
||||
def updateIndoorLocation(self, name, location):
|
||||
assert name in self.indoor_location
|
||||
assert self.indoor_location[name] is None
|
||||
self.indoor_location[name] = location
|
||||
|
||||
|
||||
class DungeonDiveOverworld:
|
||||
def __init__(self, options, r):
|
||||
self.overworld_entrance = {}
|
||||
self.indoor_location = {}
|
||||
|
||||
start_house = Location("Start House").add(StartItem())
|
||||
Location().add(ShopItem(0)).connect(start_house, OR(COUNT("RUPEES", 200), SWORD))
|
||||
Location().add(ShopItem(1)).connect(start_house, OR(COUNT("RUPEES", 980), SWORD))
|
||||
Location().add(Song(0x0B1)).connect(start_house, OCARINA) # Marins song
|
||||
start_house.add(DroppedKey(0xB2)) # Sword on the beach
|
||||
egg = Location().connect(start_house, AND(r.bush, BOMB))
|
||||
Location().add(MadBatter(0x1E1)).connect(start_house, MAGIC_POWDER)
|
||||
if options.boomerang == 'trade':
|
||||
Location().add(BoomerangGuy()).connect(start_house, AND(BOMB, OR(BOOMERANG, HOOKSHOT, MAGIC_ROD, PEGASUS_BOOTS, FEATHER, SHOVEL)))
|
||||
elif options.boomerang == 'gift':
|
||||
Location().add(BoomerangGuy()).connect(start_house, BOMB)
|
||||
|
||||
nightmare = Location("Nightmare")
|
||||
windfish = Location("Windfish").connect(nightmare, AND(MAGIC_POWDER, SWORD, OR(BOOMERANG, BOW)))
|
||||
|
||||
self.start = start_house
|
||||
self.overworld_entrance = {
|
||||
"d1": EntranceExterior(start_house, None),
|
||||
"d2": EntranceExterior(start_house, None),
|
||||
"d3": EntranceExterior(start_house, None),
|
||||
"d4": EntranceExterior(start_house, None),
|
||||
"d5": EntranceExterior(start_house, FLIPPERS),
|
||||
"d6": EntranceExterior(start_house, None),
|
||||
"d7": EntranceExterior(start_house, None),
|
||||
"d8": EntranceExterior(start_house, None),
|
||||
"d0": EntranceExterior(start_house, None),
|
||||
}
|
||||
self.egg = egg
|
||||
self.nightmare = nightmare
|
||||
self.windfish = windfish
|
||||
|
||||
def updateIndoorLocation(self, name, location):
|
||||
self.indoor_location[name] = location
|
||||
|
||||
|
||||
class EntranceExterior:
|
||||
def __init__(self, outside, requirement, one_way_enter_requirement="UNSET", one_way_exit_requirement="UNSET"):
|
||||
self.location = outside
|
||||
self.requirement = requirement
|
||||
self.one_way_enter_requirement = one_way_enter_requirement
|
||||
self.one_way_exit_requirement = one_way_exit_requirement
|
||||
|
||||
def addRequirement(self, new_requirement):
|
||||
self.requirement = OR(self.requirement, new_requirement)
|
||||
|
||||
def addExitRequirement(self, new_requirement):
|
||||
if self.one_way_exit_requirement == "UNSET":
|
||||
self.one_way_exit_requirement = new_requirement
|
||||
else:
|
||||
self.one_way_exit_requirement = OR(self.one_way_exit_requirement, new_requirement)
|
||||
|
||||
def addEnterRequirement(self, new_requirement):
|
||||
if self.one_way_enter_requirement == "UNSET":
|
||||
self.one_way_enter_requirement = new_requirement
|
||||
else:
|
||||
self.one_way_enter_requirement = OR(self.one_way_enter_requirement, new_requirement)
|
||||
|
||||
def enterIsSet(self):
|
||||
return self.one_way_enter_requirement != "UNSET"
|
||||
|
||||
def exitIsSet(self):
|
||||
return self.one_way_exit_requirement != "UNSET"
|
|
@ -0,0 +1,318 @@
|
|||
from typing import Optional
|
||||
from ..locations.items import *
|
||||
|
||||
|
||||
class OR:
|
||||
__slots__ = ('__items', '__children')
|
||||
|
||||
def __new__(cls, *args):
|
||||
if True in args:
|
||||
return True
|
||||
return super().__new__(cls)
|
||||
|
||||
def __init__(self, *args):
|
||||
self.__items = [item for item in args if isinstance(item, str)]
|
||||
self.__children = [item for item in args if type(item) not in (bool, str) and item is not None]
|
||||
|
||||
assert self.__items or self.__children, args
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "or%s" % (self.__items+self.__children)
|
||||
|
||||
def remove(self, item) -> None:
|
||||
if item in self.__items:
|
||||
self.__items.remove(item)
|
||||
|
||||
def hasConsumableRequirement(self) -> bool:
|
||||
for item in self.__items:
|
||||
if isConsumable(item):
|
||||
print("Consumable OR requirement? %r" % self)
|
||||
return True
|
||||
for child in self.__children:
|
||||
if child.hasConsumableRequirement():
|
||||
print("Consumable OR requirement? %r" % self)
|
||||
return True
|
||||
return False
|
||||
|
||||
def test(self, inventory) -> bool:
|
||||
for item in self.__items:
|
||||
if item in inventory:
|
||||
return True
|
||||
for child in self.__children:
|
||||
if child.test(inventory):
|
||||
return True
|
||||
return False
|
||||
|
||||
def consume(self, inventory) -> bool:
|
||||
for item in self.__items:
|
||||
if item in inventory:
|
||||
if isConsumable(item):
|
||||
inventory[item] -= 1
|
||||
if inventory[item] == 0:
|
||||
del inventory[item]
|
||||
inventory["%s_USED" % item] = inventory.get("%s_USED" % item, 0) + 1
|
||||
return True
|
||||
for child in self.__children:
|
||||
if child.consume(inventory):
|
||||
return True
|
||||
return False
|
||||
|
||||
def getItems(self, inventory, target_set) -> None:
|
||||
if self.test(inventory):
|
||||
return
|
||||
for item in self.__items:
|
||||
target_set.add(item)
|
||||
for child in self.__children:
|
||||
child.getItems(inventory, target_set)
|
||||
|
||||
def copyWithModifiedItemNames(self, f) -> "OR":
|
||||
return OR(*(f(item) for item in self.__items), *(child.copyWithModifiedItemNames(f) for child in self.__children))
|
||||
|
||||
|
||||
class AND:
|
||||
__slots__ = ('__items', '__children')
|
||||
|
||||
def __new__(cls, *args):
|
||||
if False in args:
|
||||
return False
|
||||
return super().__new__(cls)
|
||||
|
||||
def __init__(self, *args):
|
||||
self.__items = [item for item in args if isinstance(item, str)]
|
||||
self.__children = [item for item in args if type(item) not in (bool, str) and item is not None]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "and%s" % (self.__items+self.__children)
|
||||
|
||||
def remove(self, item) -> None:
|
||||
if item in self.__items:
|
||||
self.__items.remove(item)
|
||||
|
||||
def hasConsumableRequirement(self) -> bool:
|
||||
for item in self.__items:
|
||||
if isConsumable(item):
|
||||
return True
|
||||
for child in self.__children:
|
||||
if child.hasConsumableRequirement():
|
||||
return True
|
||||
return False
|
||||
|
||||
def test(self, inventory) -> bool:
|
||||
for item in self.__items:
|
||||
if item not in inventory:
|
||||
return False
|
||||
for child in self.__children:
|
||||
if not child.test(inventory):
|
||||
return False
|
||||
return True
|
||||
|
||||
def consume(self, inventory) -> bool:
|
||||
for item in self.__items:
|
||||
if isConsumable(item):
|
||||
inventory[item] -= 1
|
||||
if inventory[item] == 0:
|
||||
del inventory[item]
|
||||
inventory["%s_USED" % item] = inventory.get("%s_USED" % item, 0) + 1
|
||||
for child in self.__children:
|
||||
if not child.consume(inventory):
|
||||
return False
|
||||
return True
|
||||
|
||||
def getItems(self, inventory, target_set) -> None:
|
||||
if self.test(inventory):
|
||||
return
|
||||
for item in self.__items:
|
||||
target_set.add(item)
|
||||
for child in self.__children:
|
||||
child.getItems(inventory, target_set)
|
||||
|
||||
def copyWithModifiedItemNames(self, f) -> "AND":
|
||||
return AND(*(f(item) for item in self.__items), *(child.copyWithModifiedItemNames(f) for child in self.__children))
|
||||
|
||||
|
||||
class COUNT:
|
||||
__slots__ = ('__item', '__amount')
|
||||
|
||||
def __init__(self, item: str, amount: int) -> None:
|
||||
self.__item = item
|
||||
self.__amount = amount
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "<%dx%s>" % (self.__amount, self.__item)
|
||||
|
||||
def hasConsumableRequirement(self) -> bool:
|
||||
if isConsumable(self.__item):
|
||||
return True
|
||||
return False
|
||||
|
||||
def test(self, inventory) -> bool:
|
||||
return inventory.get(self.__item, 0) >= self.__amount
|
||||
|
||||
def consume(self, inventory) -> None:
|
||||
if isConsumable(self.__item):
|
||||
inventory[self.__item] -= self.__amount
|
||||
if inventory[self.__item] == 0:
|
||||
del inventory[self.__item]
|
||||
inventory["%s_USED" % self.__item] = inventory.get("%s_USED" % self.__item, 0) + self.__amount
|
||||
|
||||
def getItems(self, inventory, target_set) -> None:
|
||||
if self.test(inventory):
|
||||
return
|
||||
target_set.add(self.__item)
|
||||
|
||||
def copyWithModifiedItemNames(self, f) -> "COUNT":
|
||||
return COUNT(f(self.__item), self.__amount)
|
||||
|
||||
|
||||
class COUNTS:
|
||||
__slots__ = ('__items', '__amount')
|
||||
|
||||
def __init__(self, items, amount):
|
||||
self.__items = items
|
||||
self.__amount = amount
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "<%dx%s>" % (self.__amount, self.__items)
|
||||
|
||||
def hasConsumableRequirement(self) -> bool:
|
||||
for item in self.__items:
|
||||
if isConsumable(item):
|
||||
print("Consumable COUNTS requirement? %r" % (self))
|
||||
return True
|
||||
return False
|
||||
|
||||
def test(self, inventory) -> bool:
|
||||
count = 0
|
||||
for item in self.__items:
|
||||
count += inventory.get(item, 0)
|
||||
return count >= self.__amount
|
||||
|
||||
def consume(self, inventory) -> None:
|
||||
for item in self.__items:
|
||||
if isConsumable(item):
|
||||
inventory[item] -= self.__amount
|
||||
if inventory[item] == 0:
|
||||
del inventory[item]
|
||||
inventory["%s_USED" % item] = inventory.get("%s_USED" % item, 0) + self.__amount
|
||||
|
||||
def getItems(self, inventory, target_set) -> None:
|
||||
if self.test(inventory):
|
||||
return
|
||||
for item in self.__items:
|
||||
target_set.add(item)
|
||||
|
||||
def copyWithModifiedItemNames(self, f) -> "COUNTS":
|
||||
return COUNTS([f(item) for item in self.__items], self.__amount)
|
||||
|
||||
|
||||
class FOUND:
|
||||
__slots__ = ('__item', '__amount')
|
||||
|
||||
def __init__(self, item: str, amount: int) -> None:
|
||||
self.__item = item
|
||||
self.__amount = amount
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "{%dx%s}" % (self.__amount, self.__item)
|
||||
|
||||
def hasConsumableRequirement(self) -> bool:
|
||||
return False
|
||||
|
||||
def test(self, inventory) -> bool:
|
||||
return inventory.get(self.__item, 0) + inventory.get("%s_USED" % self.__item, 0) >= self.__amount
|
||||
|
||||
def consume(self, inventory) -> None:
|
||||
pass
|
||||
|
||||
def getItems(self, inventory, target_set) -> None:
|
||||
if self.test(inventory):
|
||||
return
|
||||
target_set.add(self.__item)
|
||||
|
||||
def copyWithModifiedItemNames(self, f) -> "FOUND":
|
||||
return FOUND(f(self.__item), self.__amount)
|
||||
|
||||
|
||||
def hasConsumableRequirement(requirements) -> bool:
|
||||
if isinstance(requirements, str):
|
||||
return isConsumable(requirements)
|
||||
if requirements is None:
|
||||
return False
|
||||
return requirements.hasConsumableRequirement()
|
||||
|
||||
|
||||
def isConsumable(item) -> bool:
|
||||
if item is None:
|
||||
return False
|
||||
#if item.startswith("RUPEES_") or item == "RUPEES":
|
||||
# return True
|
||||
if item.startswith("KEY"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class RequirementsSettings:
|
||||
def __init__(self, options):
|
||||
self.bush = OR(SWORD, MAGIC_POWDER, MAGIC_ROD, POWER_BRACELET, BOOMERANG)
|
||||
self.attack = OR(SWORD, BOMB, BOW, MAGIC_ROD, BOOMERANG)
|
||||
self.attack_hookshot = OR(SWORD, BOMB, BOW, MAGIC_ROD, BOOMERANG, HOOKSHOT) # switches, hinox, shrouded stalfos
|
||||
self.attack_hookshot_powder = OR(SWORD, BOMB, BOW, MAGIC_ROD, BOOMERANG, HOOKSHOT, MAGIC_POWDER) # zols, keese, moldorm
|
||||
self.attack_no_bomb = OR(SWORD, BOW, MAGIC_ROD, BOOMERANG, HOOKSHOT) # ?
|
||||
self.attack_hookshot_no_bomb = OR(SWORD, BOW, MAGIC_ROD, BOOMERANG, HOOKSHOT) # vire
|
||||
self.attack_no_boomerang = OR(SWORD, BOMB, BOW, MAGIC_ROD, HOOKSHOT) # teleporting owls
|
||||
self.attack_skeleton = OR(SWORD, BOMB, BOW, BOOMERANG, HOOKSHOT) # cannot kill skeletons with the fire rod
|
||||
self.rear_attack = OR(SWORD, BOMB) # mimic
|
||||
self.rear_attack_range = OR(MAGIC_ROD, BOW) # mimic
|
||||
self.fire = OR(MAGIC_POWDER, MAGIC_ROD) # torches
|
||||
self.push_hardhat = OR(SHIELD, SWORD, HOOKSHOT, BOOMERANG)
|
||||
|
||||
self.boss_requirements = [
|
||||
SWORD, # D1 boss
|
||||
AND(OR(SWORD, MAGIC_ROD), POWER_BRACELET), # D2 boss
|
||||
AND(PEGASUS_BOOTS, SWORD), # D3 boss
|
||||
AND(FLIPPERS, OR(SWORD, MAGIC_ROD, BOW)), # D4 boss
|
||||
AND(HOOKSHOT, SWORD), # D5 boss
|
||||
BOMB, # D6 boss
|
||||
AND(OR(MAGIC_ROD, SWORD, HOOKSHOT), COUNT(SHIELD, 2)), # D7 boss
|
||||
MAGIC_ROD, # D8 boss
|
||||
self.attack_hookshot_no_bomb, # D9 boss
|
||||
]
|
||||
self.miniboss_requirements = {
|
||||
"ROLLING_BONES": self.attack_hookshot,
|
||||
"HINOX": self.attack_hookshot,
|
||||
"DODONGO": BOMB,
|
||||
"CUE_BALL": SWORD,
|
||||
"GHOMA": OR(BOW, HOOKSHOT),
|
||||
"SMASHER": POWER_BRACELET,
|
||||
"GRIM_CREEPER": self.attack_hookshot_no_bomb,
|
||||
"BLAINO": SWORD,
|
||||
"AVALAUNCH": self.attack_hookshot,
|
||||
"GIANT_BUZZ_BLOB": MAGIC_POWDER,
|
||||
"MOBLIN_KING": SWORD,
|
||||
"ARMOS_KNIGHT": OR(BOW, MAGIC_ROD, SWORD),
|
||||
}
|
||||
|
||||
# Adjust for options
|
||||
if options.bowwow != 'normal':
|
||||
# We cheat in bowwow mode, we pretend we have the sword, as bowwow can pretty much do all what the sword ca$ # Except for taking out bushes (and crystal pillars are removed)
|
||||
self.bush.remove(SWORD)
|
||||
if options.logic == "casual":
|
||||
# In casual mode, remove the more complex kill methods
|
||||
self.bush.remove(MAGIC_POWDER)
|
||||
self.attack_hookshot_powder.remove(MAGIC_POWDER)
|
||||
self.attack.remove(BOMB)
|
||||
self.attack_hookshot.remove(BOMB)
|
||||
self.attack_hookshot_powder.remove(BOMB)
|
||||
self.attack_no_boomerang.remove(BOMB)
|
||||
self.attack_skeleton.remove(BOMB)
|
||||
if options.logic == "hard":
|
||||
self.boss_requirements[3] = AND(FLIPPERS, OR(SWORD, MAGIC_ROD, BOW, BOMB)) # bomb angler fish
|
||||
self.boss_requirements[6] = OR(MAGIC_ROD, AND(BOMB, BOW), COUNT(SWORD, 2), AND(OR(SWORD, HOOKSHOT, BOW), SHIELD)) # evil eagle 3 cycle magic rod / bomb arrows / l2 sword, and bow kill
|
||||
if options.logic == "glitched":
|
||||
self.boss_requirements[3] = AND(FLIPPERS, OR(SWORD, MAGIC_ROD, BOW, BOMB)) # bomb angler fish
|
||||
self.boss_requirements[6] = OR(MAGIC_ROD, BOMB, BOW, HOOKSHOT, COUNT(SWORD, 2), AND(SWORD, SHIELD)) # evil eagle off screen kill or 3 cycle with bombs
|
||||
if options.logic == "hell":
|
||||
self.boss_requirements[3] = AND(FLIPPERS, OR(SWORD, MAGIC_ROD, BOW, BOMB)) # bomb angler fish
|
||||
self.boss_requirements[6] = OR(MAGIC_ROD, BOMB, BOW, HOOKSHOT, COUNT(SWORD, 2), AND(SWORD, SHIELD)) # evil eagle off screen kill or 3 cycle with bombs
|
||||
self.boss_requirements[7] = OR(MAGIC_ROD, COUNT(SWORD, 2)) # hot head sword beams
|
||||
self.miniboss_requirements["GIANT_BUZZ_BLOB"] = OR(MAGIC_POWDER, COUNT(SWORD,2)) # use sword beams to damage buzz blob
|
|
@ -0,0 +1,52 @@
|
|||
import binascii
|
||||
from .romTables import ROMWithTables
|
||||
import json
|
||||
from . import logic
|
||||
import argparse
|
||||
from .settings import Settings
|
||||
from typing import Optional, List
|
||||
|
||||
def get_parser():
|
||||
|
||||
parser = argparse.ArgumentParser(description='Randomize!')
|
||||
parser.add_argument('input_filename', metavar='input rom', type=str,
|
||||
help="Rom file to use as input.")
|
||||
parser.add_argument('-o', '--output', dest="output_filename", metavar='output rom', type=str, required=False,
|
||||
help="Output filename to use. If not specified [seed].gbc is used.")
|
||||
parser.add_argument('--dump', dest="dump", type=str, nargs="*",
|
||||
help="Dump the logic of the given rom (spoilers!)")
|
||||
parser.add_argument('--spoilerformat', dest="spoilerformat", choices=["none", "console", "text", "json"], default="none",
|
||||
help="Sets the output format for the generated seed's spoiler log")
|
||||
parser.add_argument('--spoilerfilename', dest="spoiler_filename", type=str, required=False,
|
||||
help="Output filename to use for the spoiler log. If not specified, LADXR_[seed].txt/json is used.")
|
||||
parser.add_argument('--test', dest="test", action="store_true",
|
||||
help="Test the logic of the given rom, without showing anything.")
|
||||
parser.add_argument('--romdebugmode', dest="romdebugmode", action="store_true",
|
||||
help="Patch the rom so that debug mode is enabled, this creates a default save with most items and unlocks some debug features.")
|
||||
parser.add_argument('--exportmap', dest="exportmap", action="store_true",
|
||||
help="Export the map (many graphical mistakes)")
|
||||
parser.add_argument('--emptyplan', dest="emptyplan", type=str, required=False,
|
||||
help="Write an unfilled plan file")
|
||||
parser.add_argument('--timeout', type=float, required=False,
|
||||
help="Timeout generating the seed after the specified number of seconds")
|
||||
parser.add_argument('--logdirectory', dest="log_directory", type=str, required=False,
|
||||
help="Directory to write the JSON log file. Generated independently from the spoiler log and omitted by default.")
|
||||
|
||||
parser.add_argument('-s', '--setting', dest="settings", action="append", required=False,
|
||||
help="Set a configuration setting for rom generation")
|
||||
parser.add_argument('--short', dest="shortsettings", type=str, required=False,
|
||||
help="Set a configuration setting for rom generation")
|
||||
parser.add_argument('--settingjson', dest="settingjson", action="store_true",
|
||||
help="Dump a json blob which describes all settings")
|
||||
|
||||
parser.add_argument('--plan', dest="plan", metavar='plandomizer', type=str, required=False,
|
||||
help="Read an item placement plan")
|
||||
parser.add_argument('--multiworld', dest="multiworld", action="append", required=False,
|
||||
help="Set configuration for a multiworld player, supply multiple times for settings per player, requires a short setting string per player.")
|
||||
parser.add_argument('--doubletrouble', dest="doubletrouble", action="store_true",
|
||||
help="Warning, bugged in various ways")
|
||||
parser.add_argument('--pymod', dest="pymod", action='append',
|
||||
help="Load python code mods.")
|
||||
|
||||
return parser
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
from ..romTables import ROMWithTables
|
||||
from ..roomEditor import RoomEditor, ObjectWarp
|
||||
from ..patches import overworld, core
|
||||
from .tileset import loadTileInfo
|
||||
from .map import Map, MazeGen
|
||||
from .wfc import WFCMap, ContradictionException
|
||||
from .roomgen import setup_room_types
|
||||
from .imagegenerator import ImageGen
|
||||
from .util import xyrange
|
||||
from .locations.entrance import DummyEntrance
|
||||
from .locationgen import LocationGenerator
|
||||
from .logic import LogicGenerator
|
||||
from .enemygen import generate_enemies
|
||||
from ..assembler import ASM
|
||||
|
||||
|
||||
def store_map(rom, the_map: Map):
|
||||
# Move all exceptions to room FF
|
||||
# Dig seashells
|
||||
rom.patch(0x03, 0x220F, ASM("cp $DA"), ASM("cp $FF"))
|
||||
rom.patch(0x03, 0x2213, ASM("cp $A5"), ASM("cp $FF"))
|
||||
rom.patch(0x03, 0x2217, ASM("cp $74"), ASM("cp $FF"))
|
||||
rom.patch(0x03, 0x221B, ASM("cp $3A"), ASM("cp $FF"))
|
||||
rom.patch(0x03, 0x221F, ASM("cp $A8"), ASM("cp $FF"))
|
||||
rom.patch(0x03, 0x2223, ASM("cp $B2"), ASM("cp $FF"))
|
||||
# Force tile 04 under bushes and rocks, instead of conditionally tile 3, else seashells won't spawn.
|
||||
rom.patch(0x14, 0x1655, 0x1677, "", fill_nop=True)
|
||||
# Bonk trees
|
||||
rom.patch(0x03, 0x0F03, ASM("cp $A4"), ASM("cp $FF"))
|
||||
rom.patch(0x03, 0x0F07, ASM("cp $D2"), ASM("cp $FF"))
|
||||
# Stairs under rocks
|
||||
rom.patch(0x14, 0x1638, ASM("cp $52"), ASM("cp $FF"))
|
||||
rom.patch(0x14, 0x163C, ASM("cp $04"), ASM("cp $FF"))
|
||||
|
||||
# Patch D6 raft game exit, just remove the exit.
|
||||
re = RoomEditor(rom, 0x1B0)
|
||||
re.removeObject(7, 0)
|
||||
re.store(rom)
|
||||
# Patch D8 back entrance, remove the outside part
|
||||
re = RoomEditor(rom, 0x23A)
|
||||
re.objects = [obj for obj in re.objects if not isinstance(obj, ObjectWarp)] + [ObjectWarp(1, 7, 0x23D, 0x58, 0x10)]
|
||||
re.store(rom)
|
||||
re = RoomEditor(rom, 0x23D)
|
||||
re.objects = [obj for obj in re.objects if not isinstance(obj, ObjectWarp)] + [ObjectWarp(1, 7, 0x23A, 0x58, 0x10)]
|
||||
re.store(rom)
|
||||
|
||||
for room in the_map:
|
||||
for location in room.locations:
|
||||
location.prepare(rom)
|
||||
for n in range(0x00, 0x100):
|
||||
sx = n & 0x0F
|
||||
sy = ((n >> 4) & 0x0F)
|
||||
if sx < the_map.w and sy < the_map.h:
|
||||
tiles = the_map.get(sx, sy).tiles
|
||||
else:
|
||||
tiles = [4] * 80
|
||||
tiles[44] = 0xC6
|
||||
|
||||
re = RoomEditor(rom, n)
|
||||
# tiles = re.getTileArray()
|
||||
re.objects = []
|
||||
re.entities = []
|
||||
room = the_map.get(sx, sy) if sx < the_map.w and sy < the_map.h else None
|
||||
|
||||
tileset = the_map.tilesets[room.tileset_id] if room else None
|
||||
rom.banks[0x3F][0x3F00 + n] = tileset.main_id if tileset else 0x0F
|
||||
rom.banks[0x21][0x02EF + n] = tileset.palette_id if tileset and tileset.palette_id is not None else 0x03
|
||||
rom.banks[0x1A][0x2476 + n] = tileset.attr_bank if tileset and tileset.attr_bank else 0x22
|
||||
rom.banks[0x1A][0x1E76 + n * 2] = (tileset.attr_addr & 0xFF) if tileset and tileset.attr_addr else 0x00
|
||||
rom.banks[0x1A][0x1E77 + n * 2] = (tileset.attr_addr >> 8) if tileset and tileset.attr_addr else 0x60
|
||||
re.animation_id = tileset.animation_id if tileset and tileset.animation_id is not None else 0x03
|
||||
|
||||
re.buildObjectList(tiles)
|
||||
if room:
|
||||
for idx, tile_id in enumerate(tiles):
|
||||
if tile_id == 0x61: # Fix issues with the well being used as chimney as well and causing wrong warps
|
||||
DummyEntrance(room, idx % 10, idx // 10)
|
||||
re.entities += room.entities
|
||||
room.locations.sort(key=lambda loc: (loc.y, loc.x, id(loc)))
|
||||
for location in room.locations:
|
||||
location.update_room(rom, re)
|
||||
else:
|
||||
re.objects.append(ObjectWarp(0x01, 0x10, 0x2A3, 0x50, 0x7C))
|
||||
re.store(rom)
|
||||
|
||||
rom.banks[0x21][0x00BF:0x00BF+3] = [0, 0, 0] # Patch out the "load palette on screen transition" exception code.
|
||||
|
||||
# Fix some tile attribute issues
|
||||
def change_attr(tileset, index, a, b, c, d):
|
||||
rom.banks[the_map.tilesets[tileset].attr_bank][the_map.tilesets[tileset].attr_addr - 0x4000 + index * 4 + 0] = a
|
||||
rom.banks[the_map.tilesets[tileset].attr_bank][the_map.tilesets[tileset].attr_addr - 0x4000 + index * 4 + 1] = b
|
||||
rom.banks[the_map.tilesets[tileset].attr_bank][the_map.tilesets[tileset].attr_addr - 0x4000 + index * 4 + 2] = c
|
||||
rom.banks[the_map.tilesets[tileset].attr_bank][the_map.tilesets[tileset].attr_addr - 0x4000 + index * 4 + 3] = d
|
||||
change_attr("mountains", 0x04, 6, 6, 6, 6)
|
||||
change_attr("mountains", 0x27, 6, 6, 3, 3)
|
||||
change_attr("mountains", 0x28, 6, 6, 3, 3)
|
||||
change_attr("mountains", 0x6E, 1, 1, 1, 1)
|
||||
change_attr("town", 0x59, 2, 2, 2, 2) # Roof tile wrong color
|
||||
|
||||
|
||||
def generate(rom_filename, w, h):
|
||||
rom = ROMWithTables(rom_filename)
|
||||
overworld.patchOverworldTilesets(rom)
|
||||
core.cleanup(rom)
|
||||
tilesets = loadTileInfo(rom)
|
||||
|
||||
the_map = Map(w, h, tilesets)
|
||||
setup_room_types(the_map)
|
||||
|
||||
MazeGen(the_map)
|
||||
imggen = ImageGen(tilesets, the_map, rom)
|
||||
imggen.enabled = False
|
||||
wfcmap = WFCMap(the_map, tilesets) #, step_callback=imggen.on_step)
|
||||
try:
|
||||
wfcmap.initialize()
|
||||
except ContradictionException as e:
|
||||
print(f"Failed on setup {e.x // 10} {e.y // 8} {e.x % 10} {e.y % 8}")
|
||||
imggen.on_step(wfcmap, err=(e.x, e.y))
|
||||
return
|
||||
imggen.on_step(wfcmap)
|
||||
for x, y in xyrange(w, h):
|
||||
for n in range(50):
|
||||
try:
|
||||
wfcmap.build(x * 10, y * 8, 10, 8)
|
||||
imggen.on_step(wfcmap)
|
||||
break
|
||||
except ContradictionException as e:
|
||||
print(f"Failed {x} {y} {e.x%10} {e.y%8} {n}")
|
||||
imggen.on_step(wfcmap, err=(e.x, e.y))
|
||||
wfcmap.clear()
|
||||
if n == 49:
|
||||
raise RuntimeError("Failed to fill chunk")
|
||||
print(f"Done {x} {y}")
|
||||
imggen.on_step(wfcmap)
|
||||
wfcmap.store_tile_data(the_map)
|
||||
|
||||
LocationGenerator(the_map)
|
||||
|
||||
for room in the_map:
|
||||
generate_enemies(room)
|
||||
|
||||
if imggen.enabled:
|
||||
store_map(rom, the_map)
|
||||
from mapexport import MapExport
|
||||
MapExport(rom).export_all(w, h, dungeons=False)
|
||||
rom.save("test.gbc")
|
||||
return the_map
|
|
@ -0,0 +1,59 @@
|
|||
from .tileset import walkable_tiles, entrance_tiles
|
||||
import random
|
||||
|
||||
|
||||
ENEMIES = {
|
||||
"mountains": [
|
||||
(0x0B,),
|
||||
(0x0E,),
|
||||
(0x29,),
|
||||
(0x0E, 0x0E),
|
||||
(0x0E, 0x0E, 0x23),
|
||||
(0x0D,), (0x0D, 0x0D),
|
||||
],
|
||||
"egg": [],
|
||||
"basic": [
|
||||
(), (), (), (), (), (),
|
||||
(0x09,), (0x09, 0x09), # octorock
|
||||
(0x9B, 0x9B), (0x9B, 0x9B, 0x1B), # slimes
|
||||
(0xBB, 0x9B), # bush crawler + slime
|
||||
(0xB9,),
|
||||
(0x0B, 0x23), # likelike + moblin
|
||||
(0x14, 0x0B, 0x0B), # moblins + sword
|
||||
(0x0B, 0x23, 0x23), # likelike + moblin
|
||||
(0xAE, 0xAE), # flying octorock
|
||||
(0xBA, ), # Bomber
|
||||
(0x0D, 0x0D), (0x0D, ),
|
||||
],
|
||||
"town": [
|
||||
(), (), (0x6C, 0x6E), (0x6E,), (0x6E, 0x6E),
|
||||
],
|
||||
"forest": [
|
||||
(0x0B,), # moblins
|
||||
(0x0B, 0x0B), # moblins
|
||||
(0x14, 0x0B, 0x0B), # moblins + sword
|
||||
],
|
||||
"beach": [
|
||||
(0xC6, 0xC6),
|
||||
(0x0E, 0x0E, 0xC6),
|
||||
(0x0E, 0x0E, 0x09),
|
||||
],
|
||||
"water": [],
|
||||
}
|
||||
|
||||
|
||||
def generate_enemies(room):
|
||||
options = ENEMIES[room.tileset_id]
|
||||
if not options:
|
||||
return
|
||||
positions = []
|
||||
for y in range(1, 7):
|
||||
for x in range(1, 9):
|
||||
if room.tiles[x + y * 10] in walkable_tiles and room.tiles[x + (y - 1) * 10] not in entrance_tiles:
|
||||
positions.append((x, y))
|
||||
for type_id in random.choice(options):
|
||||
if not positions:
|
||||
return
|
||||
x, y = random.choice(positions)
|
||||
positions.remove((x, y))
|
||||
room.entities.append((x, y, type_id))
|
|
@ -0,0 +1,95 @@
|
|||
from .tileset import open_tiles, solid_tiles
|
||||
|
||||
|
||||
def tx(x):
|
||||
return x * 16 + x // 10
|
||||
|
||||
|
||||
def ty(y):
|
||||
return y * 16 + y // 8
|
||||
|
||||
|
||||
class ImageGen:
|
||||
def __init__(self, tilesets, the_map, rom):
|
||||
self.tilesets = tilesets
|
||||
self.map = the_map
|
||||
self.rom = rom
|
||||
self.image = None
|
||||
self.draw = None
|
||||
self.count = 0
|
||||
self.enabled = False
|
||||
self.__tile_cache = {}
|
||||
|
||||
def on_step(self, wfc, cur=None, err=None):
|
||||
if not self.enabled:
|
||||
return
|
||||
if self.image is None:
|
||||
import PIL.Image
|
||||
import PIL.ImageDraw
|
||||
self.image = PIL.Image.new("RGB", (self.map.w * 161, self.map.h * 129))
|
||||
self.draw = PIL.ImageDraw.Draw(self.image)
|
||||
self.image.paste(0, (0, 0, wfc.w * 16, wfc.h * 16))
|
||||
for y in range(wfc.h):
|
||||
for x in range(wfc.w):
|
||||
cell = wfc.cell_data[(x, y)]
|
||||
if len(cell.options) == 1:
|
||||
tile_id = next(iter(cell.options))
|
||||
room = self.map.get(x//10, y//8)
|
||||
tile = self.get_tile(room.tileset_id, tile_id)
|
||||
self.image.paste(tile, (tx(x), ty(y)))
|
||||
else:
|
||||
self.draw.text((tx(x) + 3, ty(y) + 3), f"{len(cell.options):2}", (255, 255, 255))
|
||||
if cell.options.issubset(open_tiles):
|
||||
self.draw.rectangle((tx(x), ty(y), tx(x) + 15, ty(y) + 15), outline=(0, 128, 0))
|
||||
elif cell.options.issubset(solid_tiles):
|
||||
self.draw.rectangle((tx(x), ty(y), tx(x) + 15, ty(y) + 15), outline=(0, 0, 192))
|
||||
if cur:
|
||||
self.draw.rectangle((tx(cur[0]),ty(cur[1]),tx(cur[0])+15,ty(cur[1])+15), outline=(0, 255, 0))
|
||||
if err:
|
||||
self.draw.rectangle((tx(err[0]),ty(err[1]),tx(err[0])+15,ty(err[1])+15), outline=(255, 0, 0))
|
||||
self.image.save(f"_map/tmp{self.count:08}.png")
|
||||
self.count += 1
|
||||
|
||||
def get_tile(self, tileset_id, tile_id):
|
||||
tile = self.__tile_cache.get((tileset_id, tile_id), None)
|
||||
if tile is not None:
|
||||
return tile
|
||||
import PIL.Image
|
||||
tile = PIL.Image.new("L", (16, 16))
|
||||
tileset = self.get_tileset(tileset_id)
|
||||
metatile = self.rom.banks[0x1A][0x2749 + tile_id * 4:0x2749 + tile_id * 4+4]
|
||||
|
||||
def draw(ox, oy, t):
|
||||
addr = (t & 0x3FF) << 4
|
||||
tile_data = self.rom.banks[t >> 10][addr:addr+0x10]
|
||||
for y in range(8):
|
||||
a = tile_data[y * 2]
|
||||
b = tile_data[y * 2 + 1]
|
||||
for x in range(8):
|
||||
v = 0
|
||||
bit = 0x80 >> x
|
||||
if a & bit:
|
||||
v |= 0x01
|
||||
if b & bit:
|
||||
v |= 0x02
|
||||
tile.putpixel((ox+x,oy+y), (255, 192, 128, 32)[v])
|
||||
draw(0, 0, tileset[metatile[0]])
|
||||
draw(8, 0, tileset[metatile[1]])
|
||||
draw(0, 8, tileset[metatile[2]])
|
||||
draw(8, 8, tileset[metatile[3]])
|
||||
self.__tile_cache[(tileset_id, tile_id)] = tile
|
||||
return tile
|
||||
|
||||
def get_tileset(self, tileset_id):
|
||||
subtiles = [0] * 0x100
|
||||
for n in range(0, 0x20):
|
||||
subtiles[n] = (0x0F << 10) + (self.tilesets[tileset_id].main_id << 4) + n
|
||||
for n in range(0x20, 0x80):
|
||||
subtiles[n] = (0x0C << 10) + 0x100 + n
|
||||
for n in range(0x80, 0x100):
|
||||
subtiles[n] = (0x0C << 10) + n
|
||||
|
||||
addr = (0x000, 0x000, 0x2B0, 0x2C0, 0x2D0, 0x2E0, 0x2F0, 0x2D0, 0x300, 0x310, 0x320, 0x2A0, 0x330, 0x350, 0x360, 0x340, 0x370)[self.tilesets[tileset_id].animation_id or 3]
|
||||
for n in range(0x6C, 0x70):
|
||||
subtiles[n] = (0x0C << 10) + addr + n - 0x6C
|
||||
return subtiles
|
|
@ -0,0 +1,203 @@
|
|||
from .tileset import entrance_tiles, solid_tiles, walkable_tiles
|
||||
from .map import Map
|
||||
from .util import xyrange
|
||||
from .locations.entrance import Entrance
|
||||
from .locations.chest import Chest, FloorItem
|
||||
from .locations.seashell import HiddenSeashell, DigSeashell, BonkSeashell
|
||||
import random
|
||||
from typing import List
|
||||
|
||||
all_location_constructors = (Chest, FloorItem, HiddenSeashell, DigSeashell, BonkSeashell)
|
||||
|
||||
|
||||
def remove_duplicate_tile(tiles, to_find):
|
||||
try:
|
||||
idx0 = tiles.index(to_find)
|
||||
idx1 = tiles.index(to_find, idx0 + 1)
|
||||
tiles[idx1] = 0x04
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
|
||||
class Dijkstra:
|
||||
def __init__(self, the_map: Map):
|
||||
self.map = the_map
|
||||
self.w = the_map.w * 10
|
||||
self.h = the_map.h * 8
|
||||
self.area = [-1] * (self.w * self.h)
|
||||
self.distance = [0] * (self.w * self.h)
|
||||
self.area_size = []
|
||||
self.next_area_id = 0
|
||||
|
||||
def fill(self, start_x, start_y):
|
||||
size = 0
|
||||
todo = [(start_x, start_y, 0)]
|
||||
while todo:
|
||||
x, y, distance = todo.pop(0)
|
||||
room = self.map.get(x // 10, y // 8)
|
||||
tile_idx = (x % 10) + (y % 8) * 10
|
||||
area_idx = x + y * self.w
|
||||
if room.tiles[tile_idx] not in solid_tiles and self.area[area_idx] == -1:
|
||||
size += 1
|
||||
self.area[area_idx] = self.next_area_id
|
||||
self.distance[area_idx] = distance
|
||||
todo += [(x - 1, y, distance + 1), (x + 1, y, distance + 1), (x, y - 1, distance + 1), (x, y + 1, distance + 1)]
|
||||
self.next_area_id += 1
|
||||
self.area_size.append(size)
|
||||
return self.next_area_id - 1
|
||||
|
||||
def dump(self):
|
||||
print(self.area_size)
|
||||
for y in range(self.map.h * 8):
|
||||
for x in range(self.map.w * 10):
|
||||
n = self.area[x + y * self.map.w * 10]
|
||||
if n < 0:
|
||||
print(' ', end='')
|
||||
else:
|
||||
print(n, end='')
|
||||
print()
|
||||
|
||||
|
||||
class EntranceInfo:
|
||||
def __init__(self, room, x, y):
|
||||
self.room = room
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.tile = room.tiles[x + y * 10]
|
||||
|
||||
@property
|
||||
def map_x(self):
|
||||
return self.room.x * 10 + self.x
|
||||
|
||||
@property
|
||||
def map_y(self):
|
||||
return self.room.y * 8 + self.y
|
||||
|
||||
|
||||
class LocationGenerator:
|
||||
def __init__(self, the_map: Map):
|
||||
# Find all entrances
|
||||
entrances: List[EntranceInfo] = []
|
||||
for room in the_map:
|
||||
# Prevent more then one chest or hole-entrance per map
|
||||
remove_duplicate_tile(room.tiles, 0xA0)
|
||||
remove_duplicate_tile(room.tiles, 0xC6)
|
||||
for x, y in xyrange(10, 8):
|
||||
if room.tiles[x + y * 10] in entrance_tiles:
|
||||
entrances.append(EntranceInfo(room, x, y))
|
||||
if room.tiles[x + y * 10] == 0xA0:
|
||||
Chest(room, x, y)
|
||||
todo_entrances = entrances.copy()
|
||||
|
||||
# Find a place to put the start position
|
||||
start_entrances = [info for info in todo_entrances if info.room.tileset_id == "town"]
|
||||
if not start_entrances:
|
||||
start_entrances = entrances
|
||||
start_entrance = random.choice(start_entrances)
|
||||
todo_entrances.remove(start_entrance)
|
||||
|
||||
# Setup the start position and fill the basic dijkstra flood fill from there.
|
||||
Entrance(start_entrance.room, start_entrance.x, start_entrance.y, "start_house")
|
||||
reachable_map = Dijkstra(the_map)
|
||||
reachable_map.fill(start_entrance.map_x, start_entrance.map_y)
|
||||
|
||||
# Find each entrance that is not reachable from any other spot, and flood fill from that entrance
|
||||
for info in entrances:
|
||||
if reachable_map.area[info.map_x + info.map_y * reachable_map.w] == -1:
|
||||
reachable_map.fill(info.map_x, info.map_y)
|
||||
|
||||
disabled_entrances = ["boomerang_cave", "seashell_mansion"]
|
||||
house_entrances = ["rooster_house", "writes_house", "photo_house", "raft_house", "crazy_tracy", "witch", "dream_hut", "shop", "madambowwow", "kennel", "library", "ulrira", "trendy_shop", "armos_temple", "banana_seller", "ghost_house", "animal_house1", "animal_house2", "animal_house3", "animal_house4", "animal_house5"]
|
||||
cave_entrances = ["madbatter_taltal", "bird_cave", "right_fairy", "moblin_cave", "hookshot_cave", "forest_madbatter", "castle_jump_cave", "rooster_grave", "prairie_left_cave1", "prairie_left_cave2", "prairie_left_fairy", "mamu", "armos_fairy", "armos_maze_cave", "prairie_madbatter", "animal_cave", "desert_cave"]
|
||||
water_entrances = ["mambo", "heartpiece_swim_cave"]
|
||||
phone_entrances = ["phone_d8", "writes_phone", "castle_phone", "mabe_phone", "prairie_left_phone", "prairie_right_phone", "prairie_low_phone", "animal_phone"]
|
||||
dungeon_entrances = ["d7", "d8", "d6", "d5", "d4", "d3", "d2", "d1", "d0"]
|
||||
connector_entrances = [("fire_cave_entrance", "fire_cave_exit"), ("left_to_right_taltalentrance", "left_taltal_entrance"), ("obstacle_cave_entrance", "obstacle_cave_outside_chest", "obstacle_cave_exit"), ("papahl_entrance", "papahl_exit"), ("multichest_left", "multichest_right", "multichest_top"), ("right_taltal_connector1", "right_taltal_connector2"), ("right_taltal_connector3", "right_taltal_connector4"), ("right_taltal_connector5", "right_taltal_connector6"), ("writes_cave_left", "writes_cave_right"), ("raft_return_enter", "raft_return_exit"), ("toadstool_entrance", "toadstool_exit"), ("graveyard_cave_left", "graveyard_cave_right"), ("castle_main_entrance", "castle_upper_left", "castle_upper_right"), ("castle_secret_entrance", "castle_secret_exit"), ("papahl_house_left", "papahl_house_right"), ("prairie_right_cave_top", "prairie_right_cave_bottom", "prairie_right_cave_high"), ("prairie_to_animal_connector", "animal_to_prairie_connector"), ("d6_connector_entrance", "d6_connector_exit"), ("richard_house", "richard_maze"), ("prairie_madbatter_connector_entrance", "prairie_madbatter_connector_exit")]
|
||||
|
||||
# For each area that is not yet reachable from the start area:
|
||||
# add a connector cave from a reachable area to this new area.
|
||||
reachable_areas = [0]
|
||||
unreachable_areas = list(range(1, reachable_map.next_area_id))
|
||||
retry_count = 10000
|
||||
while unreachable_areas:
|
||||
source = random.choice(reachable_areas)
|
||||
target = random.choice(unreachable_areas)
|
||||
|
||||
source_entrances = [info for info in todo_entrances if reachable_map.area[info.map_x + info.map_y * reachable_map.w] == source]
|
||||
target_entrances = [info for info in todo_entrances if reachable_map.area[info.map_x + info.map_y * reachable_map.w] == target]
|
||||
if not source_entrances:
|
||||
retry_count -= 1
|
||||
if retry_count < 1:
|
||||
raise RuntimeError("Failed to add connectors...")
|
||||
continue
|
||||
|
||||
source_info = random.choice(source_entrances)
|
||||
target_info = random.choice(target_entrances)
|
||||
|
||||
connector = random.choice(connector_entrances)
|
||||
connector_entrances.remove(connector)
|
||||
Entrance(source_info.room, source_info.x, source_info.y, connector[0])
|
||||
todo_entrances.remove(source_info)
|
||||
Entrance(target_info.room, target_info.x, target_info.y, connector[1])
|
||||
todo_entrances.remove(target_info)
|
||||
|
||||
for extra_exit in connector[2:]:
|
||||
info = random.choice(todo_entrances)
|
||||
todo_entrances.remove(info)
|
||||
Entrance(info.room, info.x, info.y, extra_exit)
|
||||
|
||||
unreachable_areas.remove(target)
|
||||
reachable_areas.append(target)
|
||||
|
||||
# Find areas that only have a single entrance, and try to force something in there.
|
||||
# As else we have useless dead ends, and that is no fun.
|
||||
for area_id in range(reachable_map.next_area_id):
|
||||
area_entrances = [info for info in entrances if reachable_map.area[info.map_x + info.map_y * reachable_map.w] == area_id]
|
||||
if len(area_entrances) != 1:
|
||||
continue
|
||||
cells = []
|
||||
for y in range(reachable_map.h):
|
||||
for x in range(reachable_map.w):
|
||||
if reachable_map.area[x + y * reachable_map.w] == area_id:
|
||||
if the_map.get(x // 10, y // 8).tiles[(x % 10) + (y % 8) * 10] in walkable_tiles:
|
||||
cells.append((reachable_map.distance[x + y * reachable_map.w], x, y))
|
||||
cells.sort(reverse=True)
|
||||
d, x, y = random.choice(cells[:10])
|
||||
FloorItem(the_map.get(x // 10, y // 8), x % 10, y % 8)
|
||||
|
||||
# Find potential dungeon entrances
|
||||
# Assign some dungeons
|
||||
for n in range(4):
|
||||
if not todo_entrances:
|
||||
break
|
||||
info = random.choice(todo_entrances)
|
||||
todo_entrances.remove(info)
|
||||
dungeon = random.choice(dungeon_entrances)
|
||||
dungeon_entrances.remove(dungeon)
|
||||
Entrance(info.room, info.x, info.y, dungeon)
|
||||
|
||||
# Assign something to all other entrances
|
||||
for info in todo_entrances:
|
||||
options = house_entrances if info.tile == 0xE2 else cave_entrances
|
||||
entrance = random.choice(options)
|
||||
options.remove(entrance)
|
||||
Entrance(info.room, info.x, info.y, entrance)
|
||||
|
||||
# Go over each room, and assign something if nothing is assigned yet
|
||||
todo_list = [room for room in the_map if not room.locations]
|
||||
random.shuffle(todo_list)
|
||||
done_count = {}
|
||||
for room in todo_list:
|
||||
options = []
|
||||
# figure out what things could potentially be placed here
|
||||
for constructor in all_location_constructors:
|
||||
if done_count.get(constructor, 0) >= constructor.MAX_COUNT:
|
||||
continue
|
||||
xy = constructor.check_possible(room, reachable_map)
|
||||
if xy is not None:
|
||||
options.append((*xy, constructor))
|
||||
|
||||
if options:
|
||||
x, y, constructor = random.choice(options)
|
||||
constructor(room, x, y)
|
||||
done_count[constructor] = done_count.get(constructor, 0) + 1
|
|
@ -0,0 +1,24 @@
|
|||
from ...roomEditor import RoomEditor
|
||||
from ..map import RoomInfo
|
||||
|
||||
|
||||
class LocationBase:
|
||||
MAX_COUNT = 9999
|
||||
|
||||
def __init__(self, room: RoomInfo, x, y):
|
||||
self.room = room
|
||||
self.x = x
|
||||
self.y = y
|
||||
room.locations.append(self)
|
||||
|
||||
def prepare(self, rom):
|
||||
pass
|
||||
|
||||
def update_room(self, rom, re: RoomEditor):
|
||||
pass
|
||||
|
||||
def connect_logic(self, logic_location):
|
||||
raise NotImplementedError(self.__class__)
|
||||
|
||||
def get_item_pool(self):
|
||||
raise NotImplementedError(self.__class__)
|
|
@ -0,0 +1,73 @@
|
|||
from .base import LocationBase
|
||||
from ..tileset import solid_tiles, open_tiles, walkable_tiles
|
||||
from ...roomEditor import RoomEditor
|
||||
from ...locations.all import HeartPiece, Chest as ChestLocation
|
||||
import random
|
||||
|
||||
|
||||
class Chest(LocationBase):
|
||||
def __init__(self, room, x, y):
|
||||
super().__init__(room, x, y)
|
||||
room.tiles[x + y * 10] = 0xA0
|
||||
|
||||
def connect_logic(self, logic_location):
|
||||
logic_location.add(ChestLocation(self.room.x + self.room.y * 16))
|
||||
|
||||
def get_item_pool(self):
|
||||
return {None: 1}
|
||||
|
||||
@staticmethod
|
||||
def check_possible(room, reachable_map):
|
||||
# Check if we can potentially place a chest here, and what the best spot would be.
|
||||
options = []
|
||||
for y in range(1, 6):
|
||||
for x in range(1, 9):
|
||||
if room.tiles[x + y * 10 - 10] not in solid_tiles: # Chest needs to be against a "wall" at the top
|
||||
continue
|
||||
if room.tiles[x + y * 10] not in walkable_tiles or room.tiles[x + y * 10 + 10] not in walkable_tiles:
|
||||
continue
|
||||
if room.tiles[x - 1 + y * 10] not in solid_tiles and room.tiles[x - 1 + y * 10 + 10] not in open_tiles:
|
||||
continue
|
||||
if room.tiles[x + 1 + y * 10] not in solid_tiles and room.tiles[x + 1 + y * 10 + 10] not in open_tiles:
|
||||
continue
|
||||
idx = room.x * 10 + x + (room.y * 8 + y) * reachable_map.w
|
||||
if reachable_map.area[idx] == -1:
|
||||
continue
|
||||
options.append((reachable_map.distance[idx], x, y))
|
||||
if not options:
|
||||
return None
|
||||
options.sort(reverse=True)
|
||||
options = [(x, y) for d, x, y in options if d > options[0][0] - 4]
|
||||
return random.choice(options)
|
||||
|
||||
|
||||
class FloorItem(LocationBase):
|
||||
def __init__(self, room, x, y):
|
||||
super().__init__(room, x, y)
|
||||
|
||||
def update_room(self, rom, re: RoomEditor):
|
||||
re.entities.append((self.x, self.y, 0x35))
|
||||
|
||||
def connect_logic(self, logic_location):
|
||||
logic_location.add(HeartPiece(self.room.x + self.room.y * 16))
|
||||
|
||||
def get_item_pool(self):
|
||||
return {None: 1}
|
||||
|
||||
@staticmethod
|
||||
def check_possible(room, reachable_map):
|
||||
# Check if we can potentially place a floor item here, and what the best spot would be.
|
||||
options = []
|
||||
for y in range(1, 7):
|
||||
for x in range(1, 9):
|
||||
if room.tiles[x + y * 10] not in walkable_tiles:
|
||||
continue
|
||||
idx = room.x * 10 + x + (room.y * 8 + y) * reachable_map.w
|
||||
if reachable_map.area[idx] == -1:
|
||||
continue
|
||||
options.append((reachable_map.distance[idx], x, y))
|
||||
if not options:
|
||||
return None
|
||||
options.sort(reverse=True)
|
||||
options = [(x, y) for d, x, y in options if d > options[0][0] - 4]
|
||||
return random.choice(options)
|
|
@ -0,0 +1,107 @@
|
|||
from ...locations.items import BOMB
|
||||
from .base import LocationBase
|
||||
from ...roomEditor import RoomEditor, Object, ObjectWarp
|
||||
from ...entranceInfo import ENTRANCE_INFO
|
||||
from ...assembler import ASM
|
||||
from .entrance_info import INFO
|
||||
|
||||
|
||||
class Entrance(LocationBase):
|
||||
def __init__(self, room, x, y, entrance_name):
|
||||
super().__init__(room, x, y)
|
||||
self.entrance_name = entrance_name
|
||||
self.entrance_info = ENTRANCE_INFO[entrance_name]
|
||||
self.source_warp = None
|
||||
self.target_warp_idx = None
|
||||
|
||||
self.inside_logic = None
|
||||
|
||||
def prepare(self, rom):
|
||||
info = self.entrance_info
|
||||
re = RoomEditor(rom, info.alt_room if info.alt_room is not None else info.room)
|
||||
self.source_warp = re.getWarps()[info.index if info.index not in (None, "all") else 0]
|
||||
re = RoomEditor(rom, self.source_warp.room)
|
||||
for idx, warp in enumerate(re.getWarps()):
|
||||
if warp.room == info.room or warp.room == info.alt_room:
|
||||
self.target_warp_idx = idx
|
||||
|
||||
def update_room(self, rom, re: RoomEditor):
|
||||
re.objects.append(self.source_warp)
|
||||
|
||||
target = RoomEditor(rom, self.source_warp.room)
|
||||
warp = target.getWarps()[self.target_warp_idx]
|
||||
warp.room = self.room.x | (self.room.y << 4)
|
||||
warp.target_x = self.x * 16 + 8
|
||||
warp.target_y = self.y * 16 + 18
|
||||
target.store(rom)
|
||||
|
||||
def prepare_logic(self, configuration_options, world_setup, requirements_settings):
|
||||
if self.entrance_name in INFO and INFO[self.entrance_name].logic is not None:
|
||||
self.inside_logic = INFO[self.entrance_name].logic(configuration_options, world_setup, requirements_settings)
|
||||
|
||||
def connect_logic(self, logic_location):
|
||||
if self.entrance_name not in INFO:
|
||||
raise RuntimeError(f"WARNING: Logic connection to entrance unmapped! {self.entrance_name}")
|
||||
if self.inside_logic:
|
||||
req = None
|
||||
if self.room.tiles[self.x + self.y * 10] == 0xBA:
|
||||
req = BOMB
|
||||
logic_location.connect(self.inside_logic, req)
|
||||
if INFO[self.entrance_name].exits:
|
||||
return [(name, logic(logic_location)) for name, logic in INFO[self.entrance_name].exits]
|
||||
return None
|
||||
|
||||
def get_item_pool(self):
|
||||
if self.entrance_name not in INFO:
|
||||
return {}
|
||||
return INFO[self.entrance_name].items or {}
|
||||
|
||||
|
||||
class DummyEntrance(LocationBase):
|
||||
def __init__(self, room, x, y):
|
||||
super().__init__(room, x, y)
|
||||
|
||||
def update_room(self, rom, re: RoomEditor):
|
||||
re.objects.append(ObjectWarp(0x01, 0x10, 0x2A3, 0x50, 0x7C))
|
||||
|
||||
def connect_logic(self, logic_location):
|
||||
return
|
||||
|
||||
def get_item_pool(self):
|
||||
return {}
|
||||
|
||||
|
||||
class EggEntrance(LocationBase):
|
||||
def __init__(self, room, x, y):
|
||||
super().__init__(room, x, y)
|
||||
|
||||
def update_room(self, rom, re: RoomEditor):
|
||||
# Setup the warps
|
||||
re.objects.insert(0, Object(5, 3, 0xE1)) # Hide an entrance tile under the tile where the egg will open.
|
||||
re.objects.append(ObjectWarp(0x01, 0x08, 0x270, 0x50, 0x7C))
|
||||
re.entities.append((0, 0, 0xDE)) # egg song event
|
||||
|
||||
egg_inside = RoomEditor(rom, 0x270)
|
||||
egg_inside.getWarps()[0].room = self.room.x
|
||||
egg_inside.store(rom)
|
||||
|
||||
# Fix the alt room layout
|
||||
alt = RoomEditor(rom, "Alt06")
|
||||
tiles = re.getTileArray()
|
||||
tiles[25] = 0xC1
|
||||
tiles[35] = 0xCB
|
||||
alt.buildObjectList(tiles, reduce_size=True)
|
||||
alt.store(rom)
|
||||
|
||||
# Patch which room shows as Alt06
|
||||
rom.patch(0x00, 0x31F1, ASM("cp $06"), ASM(f"cp ${self.room.x:02x}"))
|
||||
rom.patch(0x00, 0x31F5, ASM("ld a, [$D806]"), ASM(f"ld a, [${0xD800 + self.room.x:04x}]"))
|
||||
rom.patch(0x20, 0x2DE6, ASM("cp $06"), ASM(f"cp ${self.room.x:02x}"))
|
||||
rom.patch(0x20, 0x2DEA, ASM("ld a, [$D806]"), ASM(f"ld a, [${0xD800 + self.room.x:04x}]"))
|
||||
rom.patch(0x19, 0x0D1A, ASM("ld hl, $D806"), ASM(f"ld hl, ${0xD800 + self.room.x:04x}"))
|
||||
|
||||
def connect_logic(self, logic_location):
|
||||
return
|
||||
|
||||
def get_item_pool(self):
|
||||
return {}
|
|
@ -0,0 +1,341 @@
|
|||
from ...locations.birdKey import BirdKey
|
||||
from ...locations.chest import Chest
|
||||
from ...locations.faceKey import FaceKey
|
||||
from ...locations.goldLeaf import GoldLeaf
|
||||
from ...locations.heartPiece import HeartPiece
|
||||
from ...locations.madBatter import MadBatter
|
||||
from ...locations.song import Song
|
||||
from ...locations.startItem import StartItem
|
||||
from ...locations.tradeSequence import TradeSequenceItem
|
||||
from ...locations.seashell import Seashell
|
||||
from ...locations.shop import ShopItem
|
||||
from ...locations.droppedKey import DroppedKey
|
||||
from ...locations.witch import Witch
|
||||
from ...logic import *
|
||||
from ...logic.dungeon1 import Dungeon1
|
||||
from ...logic.dungeon2 import Dungeon2
|
||||
from ...logic.dungeon3 import Dungeon3
|
||||
from ...logic.dungeon4 import Dungeon4
|
||||
from ...logic.dungeon5 import Dungeon5
|
||||
from ...logic.dungeon6 import Dungeon6
|
||||
from ...logic.dungeon7 import Dungeon7
|
||||
from ...logic.dungeon8 import Dungeon8
|
||||
from ...logic.dungeonColor import DungeonColor
|
||||
|
||||
|
||||
def one_way(loc, req=None):
|
||||
res = Location()
|
||||
loc.connect(res, req, one_way=True)
|
||||
return res
|
||||
|
||||
|
||||
class EntranceInfo:
|
||||
def __init__(self, *, items=None, logic=None, exits=None):
|
||||
self.items = items
|
||||
self.logic = logic
|
||||
self.exits = exits
|
||||
|
||||
|
||||
INFO = {
|
||||
"start_house": EntranceInfo(items={None: 1}, logic=lambda c, w, r: Location().add(StartItem())),
|
||||
"d0": EntranceInfo(
|
||||
items={None: 2, KEY9: 3, MAP9: 1, COMPASS9: 1, STONE_BEAK9: 1, NIGHTMARE_KEY9: 1},
|
||||
logic=lambda c, w, r: DungeonColor(c, w, r).entrance
|
||||
),
|
||||
"d1": EntranceInfo(
|
||||
items={None: 3, KEY1: 3, MAP1: 1, COMPASS1: 1, STONE_BEAK1: 1, NIGHTMARE_KEY1: 1, HEART_CONTAINER: 1, INSTRUMENT1: 1},
|
||||
logic=lambda c, w, r: Dungeon1(c, w, r).entrance
|
||||
),
|
||||
"d2": EntranceInfo(
|
||||
items={None: 3, KEY2: 5, MAP2: 1, COMPASS2: 1, STONE_BEAK2: 1, NIGHTMARE_KEY2: 1, HEART_CONTAINER: 1, INSTRUMENT2: 1},
|
||||
logic=lambda c, w, r: Dungeon2(c, w, r).entrance
|
||||
),
|
||||
"d3": EntranceInfo(
|
||||
items={None: 4, KEY3: 9, MAP3: 1, COMPASS3: 1, STONE_BEAK3: 1, NIGHTMARE_KEY3: 1, HEART_CONTAINER: 1, INSTRUMENT3: 1},
|
||||
logic=lambda c, w, r: Dungeon3(c, w, r).entrance
|
||||
),
|
||||
"d4": EntranceInfo(
|
||||
items={None: 4, KEY4: 5, MAP4: 1, COMPASS4: 1, STONE_BEAK4: 1, NIGHTMARE_KEY4: 1, HEART_CONTAINER: 1, INSTRUMENT4: 1},
|
||||
logic=lambda c, w, r: Dungeon4(c, w, r).entrance
|
||||
),
|
||||
"d5": EntranceInfo(
|
||||
items={None: 5, KEY5: 3, MAP5: 1, COMPASS5: 1, STONE_BEAK5: 1, NIGHTMARE_KEY5: 1, HEART_CONTAINER: 1, INSTRUMENT5: 1},
|
||||
logic=lambda c, w, r: Dungeon5(c, w, r).entrance
|
||||
),
|
||||
"d6": EntranceInfo(
|
||||
items={None: 6, KEY6: 3, MAP6: 1, COMPASS6: 1, STONE_BEAK6: 1, NIGHTMARE_KEY6: 1, HEART_CONTAINER: 1, INSTRUMENT6: 1},
|
||||
logic=lambda c, w, r: Dungeon6(c, w, r, raft_game_chest=False).entrance
|
||||
),
|
||||
"d7": EntranceInfo(
|
||||
items={None: 4, KEY7: 3, MAP7: 1, COMPASS7: 1, STONE_BEAK7: 1, NIGHTMARE_KEY7: 1, HEART_CONTAINER: 1, INSTRUMENT7: 1},
|
||||
logic=lambda c, w, r: Dungeon7(c, w, r).entrance
|
||||
),
|
||||
"d8": EntranceInfo(
|
||||
items={None: 6, KEY8: 7, MAP8: 1, COMPASS8: 1, STONE_BEAK8: 1, NIGHTMARE_KEY8: 1, HEART_CONTAINER: 1, INSTRUMENT8: 1},
|
||||
logic=lambda c, w, r: Dungeon8(c, w, r, back_entrance_heartpiece=False).entrance
|
||||
),
|
||||
|
||||
"writes_cave_left": EntranceInfo(
|
||||
items={None: 2},
|
||||
logic=lambda c, w, r: Location().connect(
|
||||
Location().add(Chest(0x2AE)), OR(FEATHER, ROOSTER, HOOKSHOT)
|
||||
).connect(
|
||||
Location().add(Chest(0x2AF)), POWER_BRACELET
|
||||
),
|
||||
exits=[("writes_cave_right", lambda loc: loc)],
|
||||
),
|
||||
"writes_cave_right": EntranceInfo(),
|
||||
|
||||
"castle_main_entrance": EntranceInfo(
|
||||
items={None: 2},
|
||||
logic=lambda c, w, r: Location().connect(
|
||||
Location().add(GoldLeaf(0x2D2)), r.attack_hookshot_powder # in the castle, kill enemies
|
||||
).connect(
|
||||
Location().add(GoldLeaf(0x2C5)), AND(BOMB, r.attack_hookshot_powder) # in the castle, bomb wall to show enemy
|
||||
),
|
||||
exits=[("castle_upper_left", lambda loc: loc)],
|
||||
),
|
||||
"castle_upper_left": EntranceInfo(),
|
||||
|
||||
"castle_upper_right": EntranceInfo(
|
||||
items={None: 1},
|
||||
logic=lambda c, w, r: Location().connect(Location().add(GoldLeaf(0x2C6)), AND(POWER_BRACELET, r.attack_hookshot)),
|
||||
),
|
||||
|
||||
"right_taltal_connector1": EntranceInfo(
|
||||
logic=lambda c, w, r: Location(),
|
||||
exits=[("right_taltal_connector2", lambda loc: loc)],
|
||||
),
|
||||
"right_taltal_connector2": EntranceInfo(),
|
||||
|
||||
"fire_cave_entrance": EntranceInfo(
|
||||
logic=lambda c, w, r: Location(),
|
||||
exits=[("fire_cave_exit", lambda loc: Location().connect(loc, COUNT(SHIELD, 2)))],
|
||||
),
|
||||
"fire_cave_exit": EntranceInfo(),
|
||||
|
||||
"graveyard_cave_left": EntranceInfo(
|
||||
items={None: 1},
|
||||
logic=lambda c, w, r: Location().connect(Location().add(HeartPiece(0x2DF)), OR(AND(BOMB, OR(HOOKSHOT, PEGASUS_BOOTS), FEATHER), ROOSTER)),
|
||||
exits=[("graveyard_cave_right", lambda loc: Location().connect(loc, OR(FEATHER, ROOSTER)))],
|
||||
),
|
||||
"graveyard_cave_right": EntranceInfo(),
|
||||
|
||||
"raft_return_enter": EntranceInfo(
|
||||
logic=lambda c, w, r: Location(),
|
||||
exits=[("raft_return_exit", one_way)],
|
||||
),
|
||||
"raft_return_exit": EntranceInfo(),
|
||||
|
||||
"prairie_right_cave_top": EntranceInfo(
|
||||
logic=lambda c, w, r: Location(),
|
||||
exits=[("prairie_right_cave_bottom", lambda loc: loc), ("prairie_right_cave_high", lambda loc: Location().connect(loc, AND(BOMB, OR(FEATHER, ROOSTER))))],
|
||||
),
|
||||
"prairie_right_cave_bottom": EntranceInfo(),
|
||||
"prairie_right_cave_high": EntranceInfo(),
|
||||
|
||||
"armos_maze_cave": EntranceInfo(
|
||||
items={None: 1},
|
||||
logic=lambda c, w, r: Location().add(Chest(0x2FC)),
|
||||
),
|
||||
"right_taltal_connector3": EntranceInfo(
|
||||
logic=lambda c, w, r: Location(),
|
||||
exits=[("right_taltal_connector4", lambda loc: one_way(loc, AND(OR(FEATHER, ROOSTER), HOOKSHOT)))],
|
||||
),
|
||||
"right_taltal_connector4": EntranceInfo(),
|
||||
|
||||
"obstacle_cave_entrance": EntranceInfo(
|
||||
items={None: 1},
|
||||
logic=lambda c, w, r: Location().connect(Location().add(Chest(0x2BB)), AND(SWORD, OR(HOOKSHOT, ROOSTER))),
|
||||
exits=[
|
||||
("obstacle_cave_outside_chest", lambda loc: Location().connect(loc, SWORD)),
|
||||
("obstacle_cave_exit", lambda loc: Location().connect(loc, AND(SWORD, OR(PEGASUS_BOOTS, ROOSTER))))
|
||||
],
|
||||
),
|
||||
"obstacle_cave_outside_chest": EntranceInfo(),
|
||||
"obstacle_cave_exit": EntranceInfo(),
|
||||
|
||||
"d6_connector_entrance": EntranceInfo(
|
||||
logic=lambda c, w, r: Location(),
|
||||
exits=[("d6_connector_exit", lambda loc: Location().connect(loc, OR(AND(HOOKSHOT, OR(FLIPPERS, AND(FEATHER, PEGASUS_BOOTS))), ROOSTER)))],
|
||||
),
|
||||
"d6_connector_exit": EntranceInfo(),
|
||||
|
||||
"multichest_left": EntranceInfo(
|
||||
logic=lambda c, w, r: Location(),
|
||||
exits=[
|
||||
("multichest_right", lambda loc: loc),
|
||||
("multichest_top", lambda loc: Location().connect(loc, BOMB)),
|
||||
],
|
||||
),
|
||||
"multichest_right": EntranceInfo(),
|
||||
"multichest_top": EntranceInfo(),
|
||||
|
||||
"prairie_madbatter_connector_entrance": EntranceInfo(
|
||||
logic=lambda c, w, r: Location(),
|
||||
exits=[("prairie_madbatter_connector_exit", lambda loc: Location().connect(loc, FLIPPERS))],
|
||||
),
|
||||
"prairie_madbatter_connector_exit": EntranceInfo(),
|
||||
|
||||
"papahl_house_left": EntranceInfo(
|
||||
logic=lambda c, w, r: Location(),
|
||||
exits=[("papahl_house_right", lambda loc: loc)],
|
||||
),
|
||||
"papahl_house_right": EntranceInfo(),
|
||||
|
||||
"prairie_to_animal_connector": EntranceInfo(
|
||||
logic=lambda c, w, r: Location(),
|
||||
exits=[("animal_to_prairie_connector", lambda loc: Location().connect(loc, PEGASUS_BOOTS))],
|
||||
),
|
||||
"animal_to_prairie_connector": EntranceInfo(),
|
||||
|
||||
"castle_secret_entrance": EntranceInfo(
|
||||
logic=lambda c, w, r: Location(),
|
||||
exits=[("castle_secret_exit", lambda loc: Location().connect(loc, FEATHER))],
|
||||
),
|
||||
"castle_secret_exit": EntranceInfo(),
|
||||
|
||||
"papahl_entrance": EntranceInfo(
|
||||
items={None: 1},
|
||||
logic=lambda c, w, r: Location().add(Chest(0x28A)),
|
||||
exits=[("papahl_exit", lambda loc: loc)],
|
||||
),
|
||||
"papahl_exit": EntranceInfo(),
|
||||
|
||||
"right_taltal_connector5": EntranceInfo(
|
||||
logic=lambda c, w, r: Location(),
|
||||
exits=[("right_taltal_connector6", lambda loc: loc)],
|
||||
),
|
||||
"right_taltal_connector6": EntranceInfo(),
|
||||
|
||||
"toadstool_entrance": EntranceInfo(
|
||||
items={None: 2},
|
||||
logic=lambda c, w, r: Location().connect(Location().add(Chest(0x2BD)), SWORD).connect( # chest in forest cave on route to mushroom
|
||||
Location().add(HeartPiece(0x2AB), POWER_BRACELET)), # piece of heart in the forest cave on route to the mushroom
|
||||
exits=[("right_taltal_connector6", lambda loc: loc)],
|
||||
),
|
||||
"toadstool_exit": EntranceInfo(),
|
||||
|
||||
"richard_house": EntranceInfo(
|
||||
items={None: 1},
|
||||
logic=lambda c, w, r: Location().connect(Location().add(Chest(0x2C8)), AND(COUNT(GOLD_LEAF, 5), OR(FEATHER, HOOKSHOT, ROOSTER))),
|
||||
exits=[("richard_maze", lambda loc: Location().connect(loc, COUNT(GOLD_LEAF, 5)))],
|
||||
),
|
||||
"richard_maze": EntranceInfo(),
|
||||
|
||||
"left_to_right_taltalentrance": EntranceInfo(
|
||||
exits=[("left_taltal_entrance", lambda loc: one_way(loc, OR(HOOKSHOT, ROOSTER)))],
|
||||
),
|
||||
"left_taltal_entrance": EntranceInfo(),
|
||||
|
||||
"boomerang_cave": EntranceInfo(), # TODO boomerang gift
|
||||
"trendy_shop": EntranceInfo(
|
||||
items={None: 1},
|
||||
logic=lambda c, w, r: Location().connect(Location().add(TradeSequenceItem(0x2A0, TRADING_ITEM_YOSHI_DOLL)), FOUND("RUPEES", 50))
|
||||
),
|
||||
"moblin_cave": EntranceInfo(
|
||||
items={None: 1},
|
||||
logic=lambda c, w, r: Location().connect(Location().add(Chest(0x2E2)), AND(r.attack_hookshot_powder, r.miniboss_requirements[w.miniboss_mapping["moblin_cave"]]))
|
||||
),
|
||||
"prairie_madbatter": EntranceInfo(
|
||||
items={None: 1},
|
||||
logic=lambda c, w, r: Location().connect(Location().add(MadBatter(0x1E0)), MAGIC_POWDER)
|
||||
),
|
||||
"ulrira": EntranceInfo(),
|
||||
"rooster_house": EntranceInfo(),
|
||||
"animal_house2": EntranceInfo(),
|
||||
"animal_house4": EntranceInfo(),
|
||||
"armos_fairy": EntranceInfo(),
|
||||
"right_fairy": EntranceInfo(),
|
||||
"photo_house": EntranceInfo(),
|
||||
|
||||
"bird_cave": EntranceInfo(
|
||||
items={None: 1},
|
||||
logic=lambda c, w, r: Location().connect(Location().add(BirdKey()), OR(AND(FEATHER, COUNT(POWER_BRACELET, 2)), ROOSTER))
|
||||
),
|
||||
"mamu": EntranceInfo(
|
||||
items={None: 1},
|
||||
logic=lambda c, w, r: Location().connect(Location().add(Song(0x2FB)), AND(OCARINA, COUNT("RUPEES", 300)))
|
||||
),
|
||||
"armos_temple": EntranceInfo(
|
||||
items={None: 1},
|
||||
logic=lambda c, w, r: Location().connect(Location().add(FaceKey()), r.miniboss_requirements[w.miniboss_mapping["armos_temple"]])
|
||||
),
|
||||
"animal_house1": EntranceInfo(),
|
||||
"madambowwow": EntranceInfo(),
|
||||
"library": EntranceInfo(),
|
||||
"kennel": EntranceInfo(
|
||||
items={None: 1, TRADING_ITEM_RIBBON: 1},
|
||||
logic=lambda c, w, r: Location().connect(Location().add(Seashell(0x2B2)), SHOVEL).connect(Location().add(TradeSequenceItem(0x2B2, TRADING_ITEM_DOG_FOOD)), TRADING_ITEM_RIBBON)
|
||||
),
|
||||
"dream_hut": EntranceInfo(
|
||||
items={None: 2},
|
||||
logic=lambda c, w, r: Location().connect(Location().add(Chest(0x2BF)), OR(SWORD, BOOMERANG, HOOKSHOT, FEATHER)).connect(Location().add(Chest(0x2BE)), AND(OR(SWORD, BOOMERANG, HOOKSHOT, FEATHER), PEGASUS_BOOTS))
|
||||
),
|
||||
"hookshot_cave": EntranceInfo(
|
||||
items={None: 1},
|
||||
logic=lambda c, w, r: Location().connect(Location().add(Chest(0x2B3)), OR(HOOKSHOT, ROOSTER))
|
||||
),
|
||||
"madbatter_taltal": EntranceInfo(
|
||||
items={None: 1},
|
||||
logic=lambda c, w, r: Location().connect(Location().add(MadBatter(0x1E2)), MAGIC_POWDER)
|
||||
),
|
||||
"forest_madbatter": EntranceInfo(
|
||||
items={None: 1},
|
||||
logic=lambda c, w, r: Location().connect(Location().add(MadBatter(0x1E1)), MAGIC_POWDER)
|
||||
),
|
||||
"banana_seller": EntranceInfo(
|
||||
items={TRADING_ITEM_DOG_FOOD: 1},
|
||||
logic=lambda c, w, r: Location().connect(Location().add(TradeSequenceItem(0x2FE, TRADING_ITEM_BANANAS)), TRADING_ITEM_DOG_FOOD)
|
||||
),
|
||||
"shop": EntranceInfo(
|
||||
items={None: 2},
|
||||
logic=lambda c, w, r: Location().connect(Location().add(ShopItem(0)), COUNT("RUPEES", 200)).connect(Location().add(ShopItem(1)), COUNT("RUPEES", 980))
|
||||
),
|
||||
"ghost_house": EntranceInfo(
|
||||
items={None: 1},
|
||||
logic=lambda c, w, r: Location().connect(Location().add(Seashell(0x1E3)), POWER_BRACELET)
|
||||
),
|
||||
"writes_house": EntranceInfo(
|
||||
items={TRADING_ITEM_LETTER: 1},
|
||||
logic=lambda c, w, r: Location().connect(Location().add(TradeSequenceItem(0x2A8, TRADING_ITEM_BROOM)), TRADING_ITEM_LETTER)
|
||||
),
|
||||
"animal_house3": EntranceInfo(
|
||||
items={TRADING_ITEM_HIBISCUS: 1},
|
||||
logic=lambda c, w, r: Location().connect(Location().add(TradeSequenceItem(0x2D9, TRADING_ITEM_LETTER)), TRADING_ITEM_HIBISCUS)
|
||||
),
|
||||
"animal_house5": EntranceInfo(
|
||||
items={TRADING_ITEM_HONEYCOMB: 1},
|
||||
logic=lambda c, w, r: Location().connect(Location().add(TradeSequenceItem(0x2D7, TRADING_ITEM_PINEAPPLE)), TRADING_ITEM_HONEYCOMB)
|
||||
),
|
||||
"crazy_tracy": EntranceInfo(
|
||||
items={"MEDICINE2": 1},
|
||||
logic=lambda c, w, r: Location().connect(Location().add(KeyLocation("MEDICINE2")), FOUND("RUPEES", 50))
|
||||
),
|
||||
"rooster_grave": EntranceInfo(
|
||||
logic=lambda c, w, r: Location().connect(Location().add(DroppedKey(0x1E4)), AND(OCARINA, SONG3))
|
||||
),
|
||||
"desert_cave": EntranceInfo(
|
||||
items={None: 1},
|
||||
logic=lambda c, w, r: Location().connect(Location().add(HeartPiece(0x1E8)), BOMB)
|
||||
),
|
||||
"witch": EntranceInfo(
|
||||
items={TOADSTOOL: 1},
|
||||
logic=lambda c, w, r: Location().connect(Location().add(Witch()), TOADSTOOL)
|
||||
),
|
||||
"prairie_left_cave1": EntranceInfo(
|
||||
items={None: 1},
|
||||
logic=lambda c, w, r: Location().add(Chest(0x2CD))
|
||||
),
|
||||
"prairie_left_cave2": EntranceInfo(
|
||||
items={None: 2},
|
||||
logic=lambda c, w, r: Location().connect(Location().add(Chest(0x2F4)), PEGASUS_BOOTS).connect(Location().add(HeartPiece(0x2E5)), AND(BOMB, PEGASUS_BOOTS))
|
||||
),
|
||||
"castle_jump_cave": EntranceInfo(
|
||||
items={None: 1},
|
||||
logic=lambda c, w, r: Location().add(Chest(0x1FD))
|
||||
),
|
||||
"raft_house": EntranceInfo(),
|
||||
"prairie_left_fairy": EntranceInfo(),
|
||||
"seashell_mansion": EntranceInfo(), # TODO: Not sure if we can guarantee enough shells
|
||||
}
|
|
@ -0,0 +1,172 @@
|
|||
from ..logic import Location, PEGASUS_BOOTS, SHOVEL
|
||||
from .base import LocationBase
|
||||
from ..tileset import solid_tiles, open_tiles, walkable_tiles
|
||||
from ...roomEditor import RoomEditor
|
||||
from ...assembler import ASM
|
||||
from ...locations.all import Seashell
|
||||
import random
|
||||
|
||||
|
||||
class HiddenSeashell(LocationBase):
|
||||
def __init__(self, room, x, y):
|
||||
super().__init__(room, x, y)
|
||||
if room.tiles[x + y * 10] not in (0x20, 0x5C):
|
||||
if random.randint(0, 1):
|
||||
room.tiles[x + y * 10] = 0x20 # rock
|
||||
else:
|
||||
room.tiles[x + y * 10] = 0x5C # bush
|
||||
|
||||
def update_room(self, rom, re: RoomEditor):
|
||||
re.entities.append((self.x, self.y, 0x3D))
|
||||
|
||||
def connect_logic(self, logic_location):
|
||||
logic_location.add(Seashell(self.room.x + self.room.y * 16))
|
||||
|
||||
def get_item_pool(self):
|
||||
return {None: 1}
|
||||
|
||||
@staticmethod
|
||||
def check_possible(room, reachable_map):
|
||||
# Check if we can potentially place a hidden seashell here
|
||||
# First see if we have a nice bush or rock to hide under
|
||||
options = []
|
||||
for y in range(1, 7):
|
||||
for x in range(1, 9):
|
||||
if room.tiles[x + y * 10] not in {0x20, 0x5C}:
|
||||
continue
|
||||
idx = room.x * 10 + x + (room.y * 8 + y) * reachable_map.w
|
||||
if reachable_map.area[idx] == -1:
|
||||
continue
|
||||
options.append((reachable_map.distance[idx], x, y))
|
||||
if not options:
|
||||
# No existing bush, we can always add one. So find a nice spot
|
||||
for y in range(1, 7):
|
||||
for x in range(1, 9):
|
||||
if room.tiles[x + y * 10] not in walkable_tiles:
|
||||
continue
|
||||
if room.tiles[x + y * 10] == 0x1E: # ocean edge
|
||||
continue
|
||||
idx = room.x * 10 + x + (room.y * 8 + y) * reachable_map.w
|
||||
if reachable_map.area[idx] == -1:
|
||||
continue
|
||||
options.append((reachable_map.distance[idx], x, y))
|
||||
if not options:
|
||||
return None
|
||||
options.sort(reverse=True)
|
||||
options = [(x, y) for d, x, y in options if d > options[0][0] - 4]
|
||||
return random.choice(options)
|
||||
|
||||
|
||||
class DigSeashell(LocationBase):
|
||||
MAX_COUNT = 6
|
||||
|
||||
def __init__(self, room, x, y):
|
||||
super().__init__(room, x, y)
|
||||
if room.tileset_id == "beach":
|
||||
room.tiles[x + y * 10] = 0x08
|
||||
for ox, oy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
||||
if room.tiles[x + ox + (y + oy) * 10] != 0x1E:
|
||||
room.tiles[x + ox + (y + oy) * 10] = 0x24
|
||||
else:
|
||||
room.tiles[x + y * 10] = 0x04
|
||||
for ox, oy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
||||
room.tiles[x + ox + (y + oy) * 10] = 0x0A
|
||||
|
||||
def update_room(self, rom, re: RoomEditor):
|
||||
re.entities.append((self.x, self.y, 0x3D))
|
||||
if rom.banks[0x03][0x2210] == 0xFF:
|
||||
rom.patch(0x03, 0x220F, ASM("cp $FF"), ASM(f"cp ${self.room.x | (self.room.y << 4):02x}"))
|
||||
elif rom.banks[0x03][0x2214] == 0xFF:
|
||||
rom.patch(0x03, 0x2213, ASM("cp $FF"), ASM(f"cp ${self.room.x | (self.room.y << 4):02x}"))
|
||||
elif rom.banks[0x03][0x2218] == 0xFF:
|
||||
rom.patch(0x03, 0x2217, ASM("cp $FF"), ASM(f"cp ${self.room.x | (self.room.y << 4):02x}"))
|
||||
elif rom.banks[0x03][0x221C] == 0xFF:
|
||||
rom.patch(0x03, 0x221B, ASM("cp $FF"), ASM(f"cp ${self.room.x | (self.room.y << 4):02x}"))
|
||||
elif rom.banks[0x03][0x2220] == 0xFF:
|
||||
rom.patch(0x03, 0x221F, ASM("cp $FF"), ASM(f"cp ${self.room.x | (self.room.y << 4):02x}"))
|
||||
elif rom.banks[0x03][0x2224] == 0xFF:
|
||||
rom.patch(0x03, 0x2223, ASM("cp $FF"), ASM(f"cp ${self.room.x | (self.room.y << 4):02x}"))
|
||||
|
||||
def connect_logic(self, logic_location):
|
||||
logic_location.connect(Location().add(Seashell(self.room.x + self.room.y * 16)), SHOVEL)
|
||||
|
||||
def get_item_pool(self):
|
||||
return {None: 1}
|
||||
|
||||
@staticmethod
|
||||
def check_possible(room, reachable_map):
|
||||
options = []
|
||||
for y in range(1, 7):
|
||||
for x in range(1, 9):
|
||||
if room.tiles[x + y * 10] not in walkable_tiles:
|
||||
continue
|
||||
if room.tiles[x - 1 + y * 10] not in walkable_tiles:
|
||||
continue
|
||||
if room.tiles[x + 1 + y * 10] not in walkable_tiles:
|
||||
continue
|
||||
if room.tiles[x + (y - 1) * 10] not in walkable_tiles:
|
||||
continue
|
||||
if room.tiles[x + (y + 1) * 10] not in walkable_tiles:
|
||||
continue
|
||||
idx = room.x * 10 + x + (room.y * 8 + y) * reachable_map.w
|
||||
if reachable_map.area[idx] == -1:
|
||||
continue
|
||||
options.append((x, y))
|
||||
if not options:
|
||||
return None
|
||||
return random.choice(options)
|
||||
|
||||
|
||||
class BonkSeashell(LocationBase):
|
||||
MAX_COUNT = 2
|
||||
|
||||
def __init__(self, room, x, y):
|
||||
super().__init__(room, x, y)
|
||||
self.tree_x = x
|
||||
self.tree_y = y
|
||||
for offsetx, offsety in [(-1, 0), (-1, 1), (2, 0), (2, 1), (0, -1), (1, -1), (0, 2), (1, 2)]:
|
||||
if room.tiles[x + offsetx + (y + offsety) * 10] in walkable_tiles:
|
||||
self.x += offsetx
|
||||
self.y += offsety
|
||||
break
|
||||
|
||||
def update_room(self, rom, re: RoomEditor):
|
||||
re.entities.append((self.tree_x, self.tree_y, 0x3D))
|
||||
if rom.banks[0x03][0x0F04] == 0xFF:
|
||||
rom.patch(0x03, 0x0F03, ASM("cp $FF"), ASM(f"cp ${self.room.x|(self.room.y<<4):02x}"))
|
||||
elif rom.banks[0x03][0x0F08] == 0xFF:
|
||||
rom.patch(0x03, 0x0F07, ASM("cp $FF"), ASM(f"cp ${self.room.x|(self.room.y<<4):02x}"))
|
||||
else:
|
||||
raise RuntimeError("To many bonk seashells")
|
||||
|
||||
def connect_logic(self, logic_location):
|
||||
logic_location.connect(Location().add(Seashell(self.room.x + self.room.y * 16)), PEGASUS_BOOTS)
|
||||
|
||||
def get_item_pool(self):
|
||||
return {None: 1}
|
||||
|
||||
@staticmethod
|
||||
def check_possible(room, reachable_map):
|
||||
# Check if we can potentially place a hidden seashell here
|
||||
# Find potential trees
|
||||
options = []
|
||||
for y in range(1, 6):
|
||||
for x in range(1, 8):
|
||||
if room.tiles[x + y * 10] != 0x25:
|
||||
continue
|
||||
if room.tiles[x + y * 10 + 1] != 0x26:
|
||||
continue
|
||||
if room.tiles[x + y * 10 + 10] != 0x27:
|
||||
continue
|
||||
if room.tiles[x + y * 10 + 11] != 0x28:
|
||||
continue
|
||||
idx = room.x * 10 + x + (room.y * 8 + y) * reachable_map.w
|
||||
top_reachable = reachable_map.area[idx - reachable_map.w] != -1 or reachable_map.area[idx - reachable_map.w + 1] != -1
|
||||
bottom_reachable = reachable_map.area[idx + reachable_map.w * 2] != -1 or reachable_map.area[idx + reachable_map.w * 2 + 1] != -1
|
||||
left_reachable = reachable_map.area[idx - 1] != -1 or reachable_map.area[idx + reachable_map.w - 1] != -1
|
||||
right_reachable = reachable_map.area[idx + 2] != -1 or reachable_map.area[idx + reachable_map.w + 2] != -1
|
||||
if (top_reachable and bottom_reachable) or (left_reachable and right_reachable):
|
||||
options.append((x, y))
|
||||
if not options:
|
||||
return None
|
||||
return random.choice(options)
|
|
@ -0,0 +1,146 @@
|
|||
from .map import Map
|
||||
from .locations.entrance import Entrance
|
||||
from ..logic import *
|
||||
from .tileset import walkable_tiles, entrance_tiles
|
||||
|
||||
|
||||
class LogicGenerator:
|
||||
def __init__(self, configuration_options, world_setup, requirements_settings, the_map: Map):
|
||||
self.w = the_map.w * 10
|
||||
self.h = the_map.h * 8
|
||||
self.map = the_map
|
||||
self.logic_map = [None] * (self.w * self.h)
|
||||
self.location_lookup = {}
|
||||
self.configuration_options = configuration_options
|
||||
self.world_setup = world_setup
|
||||
self.requirements_settings = requirements_settings
|
||||
|
||||
self.entrance_map = {}
|
||||
for room in the_map:
|
||||
for location in room.locations:
|
||||
self.location_lookup[(room.x * 10 + location.x, room.y * 8 + location.y)] = location
|
||||
if isinstance(location, Entrance):
|
||||
location.prepare_logic(configuration_options, world_setup, requirements_settings)
|
||||
self.entrance_map[location.entrance_name] = location
|
||||
|
||||
start = self.entrance_map["start_house"]
|
||||
self.start = Location()
|
||||
self.egg = self.start # TODO
|
||||
self.nightmare = Location()
|
||||
self.windfish = Location().connect(self.nightmare, AND(MAGIC_POWDER, SWORD, OR(BOOMERANG, BOW)))
|
||||
self.fill_walkable(self.start, start.room.x * 10 + start.x, start.room.y * 8 + start.y)
|
||||
|
||||
logic_str_map = {None: "."}
|
||||
for y in range(self.h):
|
||||
line = ""
|
||||
for x in range(self.w):
|
||||
if self.logic_map[x + y * self.w] not in logic_str_map:
|
||||
logic_str_map[self.logic_map[x + y * self.w]] = chr(len(logic_str_map)+48)
|
||||
line += logic_str_map[self.logic_map[x + y * self.w]]
|
||||
print(line)
|
||||
|
||||
for room in the_map:
|
||||
for location in room.locations:
|
||||
if self.logic_map[(room.x * 10 + location.x) + (room.y * 8 + location.y) * self.w] is None:
|
||||
raise RuntimeError(f"Location not mapped to logic: {room} {location.__class__.__name__} {location.x} {location.y}")
|
||||
|
||||
tmp = set()
|
||||
def r(n):
|
||||
if n in tmp:
|
||||
return
|
||||
tmp.add(n)
|
||||
for item in n.items:
|
||||
print(item)
|
||||
for o, req in n.simple_connections:
|
||||
r(o)
|
||||
for o, req in n.gated_connections:
|
||||
r(o)
|
||||
r(self.start)
|
||||
|
||||
def fill_walkable(self, location, x, y):
|
||||
tile_options = walkable_tiles | entrance_tiles
|
||||
for x, y in self.flood_fill_logic(location, tile_options, x, y):
|
||||
if self.logic_map[x + y * self.w] is not None:
|
||||
continue
|
||||
tile = self.map.get_tile(x, y)
|
||||
if tile == 0x5C: # bush
|
||||
other_location = Location()
|
||||
location.connect(other_location, self.requirements_settings.bush)
|
||||
self.fill_bush(other_location, x, y)
|
||||
elif tile == 0x20: # rock
|
||||
other_location = Location()
|
||||
location.connect(other_location, POWER_BRACELET)
|
||||
self.fill_rock(other_location, x, y)
|
||||
elif tile == 0xE8: # pit
|
||||
if self.map.get_tile(x - 1, y) in tile_options and self.map.get_tile(x + 1, y) in tile_options:
|
||||
if self.logic_map[x - 1 + y * self.w] == location and self.logic_map[x + 1 + y * self.w] is None:
|
||||
other_location = Location().connect(location, FEATHER)
|
||||
self.fill_walkable(other_location, x + 1, y)
|
||||
if self.logic_map[x - 1 + y * self.w] is None and self.logic_map[x + 1 + y * self.w] == location:
|
||||
other_location = Location().connect(location, FEATHER)
|
||||
self.fill_walkable(other_location, x - 1, y)
|
||||
if self.map.get_tile(x, y - 1) in tile_options and self.map.get_tile(x, y + 1) in tile_options:
|
||||
if self.logic_map[x + (y - 1) * self.w] == location and self.logic_map[x + (y + 1) * self.w] is None:
|
||||
other_location = Location().connect(location, FEATHER)
|
||||
self.fill_walkable(other_location, x, y + 1)
|
||||
if self.logic_map[x + (y - 1) * self.w] is None and self.logic_map[x + (y + 1) * self.w] == location:
|
||||
other_location = Location().connect(location, FEATHER)
|
||||
self.fill_walkable(other_location, x, y - 1)
|
||||
|
||||
def fill_bush(self, location, x, y):
|
||||
for x, y in self.flood_fill_logic(location, {0x5C}, x, y):
|
||||
if self.logic_map[x + y * self.w] is not None:
|
||||
continue
|
||||
tile = self.map.get_tile(x, y)
|
||||
if tile in walkable_tiles or tile in entrance_tiles:
|
||||
other_location = Location()
|
||||
location.connect(other_location, self.requirements_settings.bush)
|
||||
self.fill_walkable(other_location, x, y)
|
||||
|
||||
def fill_rock(self, location, x, y):
|
||||
for x, y in self.flood_fill_logic(location, {0x20}, x, y):
|
||||
if self.logic_map[x + y * self.w] is not None:
|
||||
continue
|
||||
tile = self.map.get_tile(x, y)
|
||||
if tile in walkable_tiles or tile in entrance_tiles:
|
||||
other_location = Location()
|
||||
location.connect(other_location, POWER_BRACELET)
|
||||
self.fill_walkable(other_location, x, y)
|
||||
|
||||
def flood_fill_logic(self, location, tile_types, x, y):
|
||||
assert self.map.get_tile(x, y) in tile_types
|
||||
todo = [(x, y)]
|
||||
entrance_todo = []
|
||||
|
||||
edge_set = set()
|
||||
while todo:
|
||||
x, y = todo.pop()
|
||||
if self.map.get_tile(x, y) not in tile_types:
|
||||
edge_set.add((x, y))
|
||||
continue
|
||||
if self.logic_map[x + y * self.w] is not None:
|
||||
continue
|
||||
self.logic_map[x + y * self.w] = location
|
||||
if (x, y) in self.location_lookup:
|
||||
room_location = self.location_lookup[(x, y)]
|
||||
result = room_location.connect_logic(location)
|
||||
if result:
|
||||
entrance_todo += result
|
||||
|
||||
if x < self.w - 1 and self.logic_map[x + 1 + y * self.w] is None:
|
||||
todo.append((x + 1, y))
|
||||
if x > 0 and self.logic_map[x - 1 + y * self.w] is None:
|
||||
todo.append((x - 1, y))
|
||||
if y < self.h - 1 and self.logic_map[x + y * self.w + self.w] is None:
|
||||
todo.append((x, y + 1))
|
||||
if y > 0 and self.logic_map[x + y * self.w - self.w] is None:
|
||||
if self.map.get_tile(x, y - 1) == 0xA0: # Chest, can only be collected from the south
|
||||
self.location_lookup[(x, y - 1)].connect_logic(location)
|
||||
self.logic_map[x + (y - 1) * self.w] = location
|
||||
todo.append((x, y - 1))
|
||||
|
||||
for entrance_name, logic_connection in entrance_todo:
|
||||
entrance = self.entrance_map[entrance_name]
|
||||
entrance.connect_logic(logic_connection)
|
||||
self.fill_walkable(logic_connection, entrance.room.x * 10 + entrance.x, entrance.room.y * 8 + entrance.y)
|
||||
return edge_set
|
|
@ -0,0 +1,231 @@
|
|||
import random
|
||||
from .tileset import solid_tiles, open_tiles
|
||||
from ..locations.items import *
|
||||
|
||||
|
||||
PRIMARY_ITEMS = [POWER_BRACELET, SHIELD, BOW, HOOKSHOT, MAGIC_ROD, PEGASUS_BOOTS, OCARINA, FEATHER, SHOVEL, MAGIC_POWDER, BOMB, SWORD, FLIPPERS, SONG1]
|
||||
SECONDARY_ITEMS = [BOOMERANG, RED_TUNIC, BLUE_TUNIC, MAX_POWDER_UPGRADE, MAX_BOMBS_UPGRADE, MAX_ARROWS_UPGRADE, GEL]
|
||||
|
||||
HORIZONTAL = 0
|
||||
VERTICAL = 1
|
||||
|
||||
|
||||
class RoomEdge:
|
||||
def __init__(self, direction):
|
||||
self.__solid = False
|
||||
self.__open_range = None
|
||||
self.direction = direction
|
||||
self.__open_min = 2 if direction == HORIZONTAL else 1
|
||||
self.__open_max = 8 if direction == HORIZONTAL else 7
|
||||
|
||||
def force_solid(self):
|
||||
self.__open_min = -1
|
||||
self.__open_max = -1
|
||||
self.__open_range = None
|
||||
self.__solid = True
|
||||
|
||||
def set_open_min(self, value):
|
||||
if self.__open_min < 0:
|
||||
return
|
||||
self.__open_min = max(self.__open_min, value)
|
||||
|
||||
def set_open_max(self, value):
|
||||
if self.__open_max < 0:
|
||||
return
|
||||
self.__open_max = min(self.__open_max, value)
|
||||
|
||||
def set_solid(self):
|
||||
self.__open_range = None
|
||||
self.__solid = True
|
||||
|
||||
def can_open(self):
|
||||
return self.__open_min > -1
|
||||
|
||||
def set_open(self):
|
||||
cnt = random.randint(1, self.__open_max - self.__open_min)
|
||||
if random.randint(1, 100) < 50:
|
||||
cnt = 1
|
||||
offset = random.randint(self.__open_min, self.__open_max - cnt)
|
||||
self.__open_range = (offset, offset + cnt)
|
||||
self.__solid = False
|
||||
|
||||
def is_solid(self):
|
||||
return self.__solid
|
||||
|
||||
def get_open_range(self):
|
||||
return self.__open_range
|
||||
|
||||
def seed(self, wfc, x, y):
|
||||
for offset, cell in self.__cells(wfc, x, y):
|
||||
if self.__open_range and self.__open_range[0] <= offset < self.__open_range[1]:
|
||||
cell.init_options.intersection_update(open_tiles)
|
||||
elif self.__solid:
|
||||
cell.init_options.intersection_update(solid_tiles)
|
||||
|
||||
def __cells(self, wfc, x, y):
|
||||
if self.direction == HORIZONTAL:
|
||||
for n in range(1, 9):
|
||||
yield n, wfc.cell_data[(x + n, y)]
|
||||
else:
|
||||
for n in range(1, 7):
|
||||
yield n, wfc.cell_data[(x, y + n)]
|
||||
|
||||
|
||||
class RoomInfo:
|
||||
def __init__(self, x, y):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.tileset_id = "basic"
|
||||
self.room_type = None
|
||||
self.tiles = None
|
||||
self.edge_left = None
|
||||
self.edge_up = None
|
||||
self.edge_right = RoomEdge(VERTICAL)
|
||||
self.edge_down = RoomEdge(HORIZONTAL)
|
||||
self.room_left = None
|
||||
self.room_up = None
|
||||
self.room_right = None
|
||||
self.room_down = None
|
||||
self.locations = []
|
||||
self.entities = []
|
||||
|
||||
def __repr__(self):
|
||||
return f"Room<{self.x} {self.y}>"
|
||||
|
||||
|
||||
class Map:
|
||||
def __init__(self, w, h, tilesets):
|
||||
self.w = w
|
||||
self.h = h
|
||||
self.tilesets = tilesets
|
||||
self.__rooms = [RoomInfo(x, y) for y in range(h) for x in range(w)]
|
||||
for x in range(w):
|
||||
for y in range(h):
|
||||
room = self.get(x, y)
|
||||
if x == 0:
|
||||
room.edge_left = RoomEdge(VERTICAL)
|
||||
else:
|
||||
room.edge_left = self.get(x - 1, y).edge_right
|
||||
if y == 0:
|
||||
room.edge_up = RoomEdge(HORIZONTAL)
|
||||
else:
|
||||
room.edge_up = self.get(x, y - 1).edge_down
|
||||
if x > 0:
|
||||
room.room_left = self.get(x - 1, y)
|
||||
if x < w - 1:
|
||||
room.room_right = self.get(x + 1, y)
|
||||
if y > 0:
|
||||
room.room_up = self.get(x, y - 1)
|
||||
if y < h - 1:
|
||||
room.room_down = self.get(x, y + 1)
|
||||
for x in range(w):
|
||||
self.get(x, 0).edge_up.set_solid()
|
||||
self.get(x, h-1).edge_down.set_solid()
|
||||
for y in range(h):
|
||||
self.get(0, y).edge_left.set_solid()
|
||||
self.get(w-1, y).edge_right.set_solid()
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.__rooms)
|
||||
|
||||
def get(self, x, y) -> RoomInfo:
|
||||
assert 0 <= x < self.w and 0 <= y < self.h, f"{x} {y}"
|
||||
return self.__rooms[x + y * self.w]
|
||||
|
||||
def get_tile(self, x, y):
|
||||
return self.get(x // 10, y // 8).tiles[(x % 10) + (y % 8) * 10]
|
||||
|
||||
def get_item_pool(self):
|
||||
item_pool = {}
|
||||
for room in self.__rooms:
|
||||
for location in room.locations:
|
||||
print(room, location.get_item_pool(), location.__class__.__name__)
|
||||
for k, v in location.get_item_pool().items():
|
||||
item_pool[k] = item_pool.get(k, 0) + v
|
||||
unmapped_count = item_pool.get(None, 0)
|
||||
del item_pool[None]
|
||||
for item in PRIMARY_ITEMS:
|
||||
if item not in item_pool:
|
||||
item_pool[item] = 1
|
||||
unmapped_count -= 1
|
||||
while item_pool[POWER_BRACELET] < 2:
|
||||
item_pool[POWER_BRACELET] = item_pool.get(POWER_BRACELET, 0) + 1
|
||||
unmapped_count -= 1
|
||||
while item_pool[SHIELD] < 2:
|
||||
item_pool[SHIELD] = item_pool.get(SHIELD, 0) + 1
|
||||
unmapped_count -= 1
|
||||
assert unmapped_count >= 0
|
||||
|
||||
for item in SECONDARY_ITEMS:
|
||||
if unmapped_count > 0:
|
||||
item_pool[item] = item_pool.get(item, 0) + 1
|
||||
unmapped_count -= 1
|
||||
|
||||
# Add a heart container per 10 items "spots" left.
|
||||
heart_piece_count = unmapped_count // 10
|
||||
unmapped_count -= heart_piece_count * 4
|
||||
item_pool[HEART_PIECE] = item_pool.get(HEART_PIECE, 0) + heart_piece_count * 4
|
||||
|
||||
# Add the rest as rupees
|
||||
item_pool[RUPEES_50] = item_pool.get(RUPEES_50, 0) + unmapped_count
|
||||
return item_pool
|
||||
|
||||
def dump(self):
|
||||
for y in range(self.h):
|
||||
for x in range(self.w):
|
||||
if self.get(x, y).edge_right.is_solid():
|
||||
print(" |", end="")
|
||||
elif self.get(x, y).edge_right.get_open_range():
|
||||
print(" ", end="")
|
||||
else:
|
||||
print(" ?", end="")
|
||||
print()
|
||||
for x in range(self.w):
|
||||
if self.get(x, y).edge_down.is_solid():
|
||||
print("-+", end="")
|
||||
elif self.get(x, y).edge_down.get_open_range():
|
||||
print(" +", end="")
|
||||
else:
|
||||
print("?+", end="")
|
||||
print()
|
||||
print()
|
||||
|
||||
|
||||
class MazeGen:
|
||||
UP = 0x01
|
||||
DOWN = 0x02
|
||||
LEFT = 0x04
|
||||
RIGHT = 0x08
|
||||
|
||||
def __init__(self, the_map: Map):
|
||||
self.map = the_map
|
||||
self.visited = set()
|
||||
self.visit(0, 0)
|
||||
|
||||
def visit(self, x, y):
|
||||
self.visited.add((x, y))
|
||||
neighbours = self.get_neighbours(x, y)
|
||||
while any((x, y) not in self.visited for x, y, d in neighbours):
|
||||
x, y, d = random.choice(neighbours)
|
||||
if (x, y) not in self.visited:
|
||||
if d == self.RIGHT and self.map.get(x, y).edge_left.can_open():
|
||||
self.map.get(x, y).edge_left.set_open()
|
||||
elif d == self.LEFT and self.map.get(x, y).edge_right.can_open():
|
||||
self.map.get(x, y).edge_right.set_open()
|
||||
elif d == self.DOWN and self.map.get(x, y).edge_up.can_open():
|
||||
self.map.get(x, y).edge_up.set_open()
|
||||
elif d == self.UP and self.map.get(x, y).edge_down.can_open():
|
||||
self.map.get(x, y).edge_down.set_open()
|
||||
self.visit(x, y)
|
||||
|
||||
def get_neighbours(self, x, y):
|
||||
neighbours = []
|
||||
if x > 0:
|
||||
neighbours.append((x - 1, y, self.LEFT))
|
||||
if x < self.map.w - 1:
|
||||
neighbours.append((x + 1, y, self.RIGHT))
|
||||
if y > 0:
|
||||
neighbours.append((x, y - 1, self.UP))
|
||||
if y < self.map.h - 1:
|
||||
neighbours.append((x, y + 1, self.DOWN))
|
||||
return neighbours
|
|
@ -0,0 +1,78 @@
|
|||
from .map import Map
|
||||
from .roomtype.town import Town
|
||||
from .roomtype.mountain import Mountain, MountainEgg
|
||||
from .roomtype.forest import Forest
|
||||
from .roomtype.base import RoomType
|
||||
from .roomtype.water import Water, Beach
|
||||
import random
|
||||
|
||||
|
||||
def is_area_clear(the_map: Map, x, y, w, h):
|
||||
for y0 in range(y, y+h):
|
||||
for x0 in range(x, x+w):
|
||||
if 0 <= x0 < the_map.w and 0 <= y0 < the_map.h:
|
||||
if the_map.get(x0, y0).room_type is not None:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def find_random_clear_area(the_map: Map, w, h, *, tries):
|
||||
for n in range(tries):
|
||||
x = random.randint(0, the_map.w - w)
|
||||
y = random.randint(0, the_map.h - h)
|
||||
if is_area_clear(the_map, x - 1, y - 1, w + 2, h + 2):
|
||||
return x, y
|
||||
return None, None
|
||||
|
||||
|
||||
def setup_room_types(the_map: Map):
|
||||
# Always make the rop row mountains.
|
||||
egg_x = the_map.w // 2
|
||||
for x in range(the_map.w):
|
||||
if x == egg_x:
|
||||
MountainEgg(the_map.get(x, 0))
|
||||
else:
|
||||
Mountain(the_map.get(x, 0))
|
||||
|
||||
# Add some beach.
|
||||
width = the_map.w if random.random() < 0.5 else random.randint(max(2, the_map.w // 4), the_map.w // 2)
|
||||
beach_x = 0 # current tileset doesn't allow anything else
|
||||
for x in range(beach_x, beach_x+width):
|
||||
# Beach(the_map.get(x, the_map.h - 2))
|
||||
Beach(the_map.get(x, the_map.h - 1))
|
||||
the_map.get(beach_x + width - 1, the_map.h - 1).edge_right.force_solid()
|
||||
|
||||
town_x, town_y = find_random_clear_area(the_map, 2, 2, tries=20)
|
||||
if town_x is not None:
|
||||
for y in range(town_y, town_y + 2):
|
||||
for x in range(town_x, town_x + 2):
|
||||
Town(the_map.get(x, y))
|
||||
|
||||
forest_w, forest_h = 2, 2
|
||||
if random.random() < 0.5:
|
||||
forest_w += 1
|
||||
else:
|
||||
forest_h += 1
|
||||
forest_x, forest_y = find_random_clear_area(the_map, forest_w, forest_h, tries=20)
|
||||
if forest_x is None:
|
||||
forest_w, forest_h = 2, 2
|
||||
forest_x, forest_y = find_random_clear_area(the_map, forest_w, forest_h, tries=20)
|
||||
if forest_x is not None:
|
||||
for y in range(forest_y, forest_y + forest_h):
|
||||
for x in range(forest_x, forest_x + forest_w):
|
||||
Forest(the_map.get(x, y))
|
||||
|
||||
# for n in range(5):
|
||||
# water_w, water_h = 2, 1
|
||||
# if random.random() < 0.5:
|
||||
# water_w, water_h = water_h, water_w
|
||||
# water_x, water_y = find_random_clear_area(the_map, water_w, water_h, tries=20)
|
||||
# if water_x is not None:
|
||||
# for y in range(water_y, water_y + water_h):
|
||||
# for x in range(water_x, water_x + water_w):
|
||||
# Water(the_map.get(x, y))
|
||||
|
||||
for y in range(the_map.h):
|
||||
for x in range(the_map.w):
|
||||
if the_map.get(x, y).room_type is None:
|
||||
RoomType(the_map.get(x, y))
|
|
@ -0,0 +1,54 @@
|
|||
from ..tileset import open_tiles
|
||||
|
||||
|
||||
def plot_line(x0, y0, x1, y1):
|
||||
dx = abs(x1 - x0)
|
||||
sx = 1 if x0 < x1 else -1
|
||||
dy = -abs(y1 - y0)
|
||||
sy = 1 if y0 < y1 else -1
|
||||
error = dx + dy
|
||||
|
||||
yield x0, y0
|
||||
while True:
|
||||
if x0 == x1 and y0 == y1:
|
||||
break
|
||||
e2 = 2 * error
|
||||
if e2 >= dy:
|
||||
error = error + dy
|
||||
x0 = x0 + sx
|
||||
yield x0, y0
|
||||
if e2 <= dx:
|
||||
error = error + dx
|
||||
y0 = y0 + sy
|
||||
yield x0, y0
|
||||
|
||||
yield x1, y1
|
||||
|
||||
|
||||
class RoomType:
|
||||
def __init__(self, room):
|
||||
self.room = room
|
||||
room.room_type = self
|
||||
|
||||
def seed(self, wfc, x, y):
|
||||
open_points = []
|
||||
r = self.room.edge_left.get_open_range()
|
||||
if r:
|
||||
open_points.append((x + 1, y + (r[0] + r[1]) // 2))
|
||||
r = self.room.edge_right.get_open_range()
|
||||
if r:
|
||||
open_points.append((x + 8, y + (r[0] + r[1]) // 2))
|
||||
r = self.room.edge_up.get_open_range()
|
||||
if r:
|
||||
open_points.append((x + (r[0] + r[1]) // 2, y + 1))
|
||||
r = self.room.edge_down.get_open_range()
|
||||
if r:
|
||||
open_points.append((x + (r[0] + r[1]) // 2, y + 6))
|
||||
if len(open_points) < 2:
|
||||
return
|
||||
mid_x = sum([x for x, y in open_points]) // len(open_points)
|
||||
mid_y = sum([y for x, y in open_points]) // len(open_points)
|
||||
|
||||
for x0, y0 in open_points:
|
||||
for px, py in plot_line(x0, y0, mid_x, mid_y):
|
||||
wfc.cell_data[(px, py)].init_options.intersection_update(open_tiles)
|
|
@ -0,0 +1,28 @@
|
|||
from .base import RoomType
|
||||
from ..tileset import open_tiles
|
||||
import random
|
||||
|
||||
|
||||
class Forest(RoomType):
|
||||
def __init__(self, room):
|
||||
super().__init__(room)
|
||||
room.tileset_id = "forest"
|
||||
|
||||
def seed(self, wfc, x, y):
|
||||
if self.room.room_up and isinstance(self.room.room_up.room_type, Forest) and self.room.edge_up.get_open_range() is None:
|
||||
self.room.edge_up.set_solid()
|
||||
if self.room.room_left and isinstance(self.room.room_left.room_type, Forest) and self.room.edge_left.get_open_range() is None:
|
||||
self.room.edge_left.set_solid()
|
||||
|
||||
if self.room.room_up and isinstance(self.room.room_up.room_type, Forest) and random.random() < 0.5:
|
||||
door_x, door_y = x + 5 + random.randint(-1, 1), y + 3 + random.randint(-1, 1)
|
||||
wfc.cell_data[(door_x, door_y)].init_options.intersection_update({0xE3})
|
||||
self.room.edge_up.set_solid()
|
||||
if self.room.edge_left.get_open_range() is not None:
|
||||
for x0 in range(x + 1, door_x):
|
||||
wfc.cell_data[(x0, door_y + 1)].init_options.intersection_update(open_tiles)
|
||||
if self.room.edge_right.get_open_range() is not None:
|
||||
for x0 in range(door_x + 1, x + 10):
|
||||
wfc.cell_data[(x0, door_y + 1)].init_options.intersection_update(open_tiles)
|
||||
else:
|
||||
super().seed(wfc, x, y)
|
|
@ -0,0 +1,38 @@
|
|||
from .base import RoomType
|
||||
from ..locations.entrance import EggEntrance
|
||||
import random
|
||||
|
||||
|
||||
class Mountain(RoomType):
|
||||
def __init__(self, room):
|
||||
super().__init__(room)
|
||||
room.tileset_id = "mountains"
|
||||
room.edge_left.set_open_min(3)
|
||||
room.edge_right.set_open_min(3)
|
||||
|
||||
def seed(self, wfc, x, y):
|
||||
super().seed(wfc, x, y)
|
||||
if y == 0:
|
||||
if x == 0:
|
||||
wfc.cell_data[(0, 1)].init_options.intersection_update({0})
|
||||
if x == wfc.w - 10:
|
||||
wfc.cell_data[(x + 9, 1)].init_options.intersection_update({0})
|
||||
wfc.cell_data[(x + random.randint(3, 6), random.randint(0, 1))].init_options.intersection_update({0})
|
||||
|
||||
|
||||
class MountainEgg(RoomType):
|
||||
def __init__(self, room):
|
||||
super().__init__(room)
|
||||
room.tileset_id = "egg"
|
||||
room.edge_left.force_solid()
|
||||
room.edge_right.force_solid()
|
||||
room.edge_down.set_open_min(5)
|
||||
room.edge_down.set_open_max(6)
|
||||
|
||||
EggEntrance(room, 5, 4)
|
||||
|
||||
def seed(self, wfc, x, y):
|
||||
super().seed(wfc, x, y)
|
||||
wfc.cell_data[(x + 2, y + 1)].init_options.intersection_update({0x00})
|
||||
wfc.cell_data[(x + 2, y + 2)].init_options.intersection_update({0xEF})
|
||||
wfc.cell_data[(x + 5, y + 3)].init_options.intersection_update({0xAA})
|
|
@ -0,0 +1,16 @@
|
|||
from .base import RoomType
|
||||
from ..tileset import solid_tiles
|
||||
import random
|
||||
|
||||
|
||||
class Town(RoomType):
|
||||
def __init__(self, room):
|
||||
super().__init__(room)
|
||||
room.tileset_id = "town"
|
||||
|
||||
def seed(self, wfc, x, y):
|
||||
ex = x + 5 + random.randint(-1, 1)
|
||||
ey = y + 3 + random.randint(-1, 1)
|
||||
wfc.cell_data[(ex, ey)].init_options.intersection_update({0xE2})
|
||||
wfc.cell_data[(ex - 1, ey - 1)].init_options.intersection_update(solid_tiles)
|
||||
wfc.cell_data[(ex + 1, ey - 1)].init_options.intersection_update(solid_tiles)
|
|
@ -0,0 +1,30 @@
|
|||
from .base import RoomType
|
||||
import random
|
||||
|
||||
|
||||
class Water(RoomType):
|
||||
def __init__(self, room):
|
||||
super().__init__(room)
|
||||
room.tileset_id = "water"
|
||||
|
||||
# def seed(self, wfc, x, y):
|
||||
# wfc.cell_data[(x + 5 + random.randint(-1, 1), y + 3 + random.randint(-1, 1))].init_options.intersection_update({0x0E})
|
||||
|
||||
|
||||
class Beach(RoomType):
|
||||
def __init__(self, room):
|
||||
super().__init__(room)
|
||||
room.tileset_id = "beach"
|
||||
if self.room.room_down is None:
|
||||
self.room.edge_left.set_open_max(4)
|
||||
self.room.edge_right.set_open_max(4)
|
||||
self.room.edge_up.set_open_min(4)
|
||||
self.room.edge_up.set_open_max(6)
|
||||
|
||||
def seed(self, wfc, x, y):
|
||||
if self.room.room_down is None:
|
||||
for n in range(1, 9):
|
||||
wfc.cell_data[(x + n, y + 5)].init_options.intersection_update({0x1E})
|
||||
for n in range(1, 9):
|
||||
wfc.cell_data[(x + n, y + 7)].init_options.intersection_update({0x1F})
|
||||
super().seed(wfc, x, y)
|
|
@ -0,0 +1,253 @@
|
|||
from typing import Dict, Set
|
||||
from ..roomEditor import RoomEditor
|
||||
|
||||
|
||||
animated_tiles = {0x0E, 0x1B, 0x1E, 0x1F, 0x44, 0x91, 0xCF, 0xD0, 0xD1, 0xD2, 0xD9, 0xDC, 0xE9, 0xEB, 0xEC, 0xED, 0xEE, 0xEF}
|
||||
entrance_tiles = {0xE1, 0xE2, 0xE3, 0xBA, 0xC6}
|
||||
|
||||
solid_tiles = set()
|
||||
open_tiles = set()
|
||||
walkable_tiles = set()
|
||||
vertical_edge_tiles = set()
|
||||
horizontal_edge_tiles = set()
|
||||
|
||||
|
||||
class TileInfo:
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
self.up = set()
|
||||
self.right = set()
|
||||
self.down = set()
|
||||
self.left = set()
|
||||
self.up_freq = {}
|
||||
self.right_freq = {}
|
||||
self.down_freq = {}
|
||||
self.left_freq = {}
|
||||
self.frequency = 0
|
||||
|
||||
def copy(self):
|
||||
result = TileInfo(self.key)
|
||||
result.up = self.up.copy()
|
||||
result.right = self.right.copy()
|
||||
result.down = self.down.copy()
|
||||
result.left = self.left.copy()
|
||||
result.up_freq = self.up_freq.copy()
|
||||
result.right_freq = self.right_freq.copy()
|
||||
result.down_freq = self.down_freq.copy()
|
||||
result.left_freq = self.left_freq.copy()
|
||||
result.frequency = self.frequency
|
||||
return result
|
||||
|
||||
def remove(self, tile_id):
|
||||
if tile_id in self.up:
|
||||
self.up.remove(tile_id)
|
||||
del self.up_freq[tile_id]
|
||||
if tile_id in self.down:
|
||||
self.down.remove(tile_id)
|
||||
del self.down_freq[tile_id]
|
||||
if tile_id in self.left:
|
||||
self.left.remove(tile_id)
|
||||
del self.left_freq[tile_id]
|
||||
if tile_id in self.right:
|
||||
self.right.remove(tile_id)
|
||||
del self.right_freq[tile_id]
|
||||
|
||||
def update(self, other: "TileInfo", tile_filter: Set[int]):
|
||||
self.frequency += other.frequency
|
||||
self.up.update(other.up.intersection(tile_filter))
|
||||
self.down.update(other.down.intersection(tile_filter))
|
||||
self.left.update(other.left.intersection(tile_filter))
|
||||
self.right.update(other.right.intersection(tile_filter))
|
||||
for k, v in other.up_freq.items():
|
||||
if k not in tile_filter:
|
||||
continue
|
||||
self.up_freq[k] = self.up_freq.get(k, 0) + v
|
||||
for k, v in other.down_freq.items():
|
||||
if k not in tile_filter:
|
||||
continue
|
||||
self.down_freq[k] = self.down_freq.get(k, 0) + v
|
||||
for k, v in other.left_freq.items():
|
||||
if k not in tile_filter:
|
||||
continue
|
||||
self.left_freq[k] = self.left_freq.get(k, 0) + v
|
||||
for k, v in other.down_freq.items():
|
||||
if k not in tile_filter:
|
||||
continue
|
||||
self.right_freq[k] = self.right_freq.get(k, 0) + v
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.key}>\n U{[f'{n:02x}' for n in self.up]}\n R{[f'{n:02x}' for n in self.right]}\n D{[f'{n:02x}' for n in self.down]}\n L{[f'{n:02x}' for n in self.left]}>"
|
||||
|
||||
|
||||
class TileSet:
|
||||
def __init__(self, *, main_id=None, animation_id=None):
|
||||
self.main_id = main_id
|
||||
self.animation_id = animation_id
|
||||
self.palette_id = None
|
||||
self.attr_bank = None
|
||||
self.attr_addr = None
|
||||
self.tiles: Dict[int, "TileInfo"] = {}
|
||||
self.all: Set[int] = set()
|
||||
|
||||
def copy(self) -> "TileSet":
|
||||
result = TileSet(main_id=self.main_id, animation_id=self.animation_id)
|
||||
for k, v in self.tiles.items():
|
||||
result.tiles[k] = v.copy()
|
||||
result.all = self.all.copy()
|
||||
return result
|
||||
|
||||
def remove(self, tile_id):
|
||||
self.all.remove(tile_id)
|
||||
del self.tiles[tile_id]
|
||||
for k, v in self.tiles.items():
|
||||
v.remove(tile_id)
|
||||
|
||||
# Look at the "other" tileset and merge information about tiles known in this tileset
|
||||
def learn_from(self, other: "TileSet"):
|
||||
for key, other_info in other.tiles.items():
|
||||
if key not in self.all:
|
||||
continue
|
||||
self.tiles[key].update(other_info, self.all)
|
||||
|
||||
def combine(self, other: "TileSet"):
|
||||
if other.main_id and not self.main_id:
|
||||
self.main_id = other.main_id
|
||||
if other.animation_id and not self.animation_id:
|
||||
self.animation_id = other.animation_id
|
||||
for key, other_info in other.tiles.items():
|
||||
if key not in self.all:
|
||||
self.tiles[key] = other_info.copy()
|
||||
else:
|
||||
self.tiles[key].update(other_info, self.all)
|
||||
self.all.update(other.all)
|
||||
|
||||
|
||||
def loadTileInfo(rom) -> Dict[str, TileSet]:
|
||||
for n in range(0x100):
|
||||
physics_flag = rom.banks[8][0x0AD4 + n]
|
||||
if n == 0xEF:
|
||||
physics_flag = 0x01 # One of the sky tiles is marked as a pit instead of solid, which messes with the generation of sky
|
||||
if physics_flag in {0x00, 0x05, 0x06, 0x07}:
|
||||
open_tiles.add(n)
|
||||
walkable_tiles.add(n)
|
||||
vertical_edge_tiles.add(n)
|
||||
horizontal_edge_tiles.add(n)
|
||||
elif physics_flag in {0x01, 0x04, 0x60}:
|
||||
solid_tiles.add(n)
|
||||
vertical_edge_tiles.add(n)
|
||||
horizontal_edge_tiles.add(n)
|
||||
elif physics_flag in {0x08}: # Bridge
|
||||
open_tiles.add(n)
|
||||
walkable_tiles.add(n)
|
||||
elif physics_flag in {0x02}: # Stairs
|
||||
open_tiles.add(n)
|
||||
walkable_tiles.add(n)
|
||||
horizontal_edge_tiles.add(n)
|
||||
elif physics_flag in {0x03}: # Entrances
|
||||
open_tiles.add(n)
|
||||
elif physics_flag in {0x30}: # bushes/rocks
|
||||
open_tiles.add(n)
|
||||
elif physics_flag in {0x50}: # pits
|
||||
open_tiles.add(n)
|
||||
world_tiles = {}
|
||||
for ry in range(0, 16):
|
||||
for rx in range(0, 16):
|
||||
tileset_id = rom.banks[0x3F][0x3F00 + rx + (ry << 4)]
|
||||
re = RoomEditor(rom, rx | (ry << 4))
|
||||
tiles = re.getTileArray()
|
||||
for y in range(8):
|
||||
for x in range(10):
|
||||
tile_id = tiles[x+y*10]
|
||||
world_tiles[(rx*10+x, ry*8+y)] = (tile_id, tileset_id, re.animation_id | 0x100)
|
||||
|
||||
# Fix up wrong tiles
|
||||
world_tiles[(150, 24)] = (0x2A, world_tiles[(150, 24)][1], world_tiles[(150, 24)][2]) # Left of the raft house, a tree has the wrong tile.
|
||||
|
||||
rom_tilesets: Dict[int, TileSet] = {}
|
||||
for (x, y), (key, tileset_id, animation_id) in world_tiles.items():
|
||||
if key in animated_tiles:
|
||||
if animation_id not in rom_tilesets:
|
||||
rom_tilesets[animation_id] = TileSet(animation_id=animation_id&0xFF)
|
||||
tileset = rom_tilesets[animation_id]
|
||||
else:
|
||||
if tileset_id not in rom_tilesets:
|
||||
rom_tilesets[tileset_id] = TileSet(main_id=tileset_id)
|
||||
tileset = rom_tilesets[tileset_id]
|
||||
tileset.all.add(key)
|
||||
if key not in tileset.tiles:
|
||||
tileset.tiles[key] = TileInfo(key)
|
||||
ti = tileset.tiles[key]
|
||||
ti.frequency += 1
|
||||
if (x, y - 1) in world_tiles:
|
||||
tile_id = world_tiles[(x, y - 1)][0]
|
||||
ti.up.add(tile_id)
|
||||
ti.up_freq[tile_id] = ti.up_freq.get(tile_id, 0) + 1
|
||||
if (x + 1, y) in world_tiles:
|
||||
tile_id = world_tiles[(x + 1, y)][0]
|
||||
ti.right.add(tile_id)
|
||||
ti.right_freq[tile_id] = ti.right_freq.get(tile_id, 0) + 1
|
||||
if (x, y + 1) in world_tiles:
|
||||
tile_id = world_tiles[(x, y + 1)][0]
|
||||
ti.down.add(tile_id)
|
||||
ti.down_freq[tile_id] = ti.down_freq.get(tile_id, 0) + 1
|
||||
if (x - 1, y) in world_tiles:
|
||||
tile_id = world_tiles[(x - 1, y)][0]
|
||||
ti.left.add(tile_id)
|
||||
ti.left_freq[tile_id] = ti.left_freq.get(tile_id, 0) + 1
|
||||
|
||||
tilesets = {
|
||||
"basic": rom_tilesets[0x0F].copy()
|
||||
}
|
||||
for key, tileset in rom_tilesets.items():
|
||||
tilesets["basic"].learn_from(tileset)
|
||||
tilesets["mountains"] = rom_tilesets[0x3E].copy()
|
||||
tilesets["mountains"].combine(rom_tilesets[0x10B])
|
||||
tilesets["mountains"].remove(0xB6) # Remove the raft house roof
|
||||
tilesets["mountains"].remove(0xB7) # Remove the raft house roof
|
||||
tilesets["mountains"].remove(0x66) # Remove the raft house roof
|
||||
tilesets["mountains"].learn_from(rom_tilesets[0x1C])
|
||||
tilesets["mountains"].learn_from(rom_tilesets[0x3C])
|
||||
tilesets["mountains"].learn_from(rom_tilesets[0x30])
|
||||
tilesets["mountains"].palette_id = 0x15
|
||||
tilesets["mountains"].attr_bank = 0x27
|
||||
tilesets["mountains"].attr_addr = 0x5A20
|
||||
|
||||
tilesets["egg"] = rom_tilesets[0x3C].copy()
|
||||
tilesets["egg"].combine(tilesets["mountains"])
|
||||
tilesets["egg"].palette_id = 0x13
|
||||
tilesets["egg"].attr_bank = 0x27
|
||||
tilesets["egg"].attr_addr = 0x5620
|
||||
|
||||
tilesets["forest"] = rom_tilesets[0x20].copy()
|
||||
tilesets["forest"].palette_id = 0x00
|
||||
tilesets["forest"].attr_bank = 0x25
|
||||
tilesets["forest"].attr_addr = 0x4000
|
||||
|
||||
tilesets["town"] = rom_tilesets[0x26].copy()
|
||||
tilesets["town"].combine(rom_tilesets[0x103])
|
||||
tilesets["town"].palette_id = 0x03
|
||||
tilesets["town"].attr_bank = 0x25
|
||||
tilesets["town"].attr_addr = 0x4C00
|
||||
|
||||
tilesets["swamp"] = rom_tilesets[0x36].copy()
|
||||
tilesets["swamp"].combine(rom_tilesets[0x103])
|
||||
tilesets["swamp"].palette_id = 0x0E
|
||||
tilesets["swamp"].attr_bank = 0x22
|
||||
tilesets["swamp"].attr_addr = 0x7400
|
||||
|
||||
tilesets["beach"] = rom_tilesets[0x22].copy()
|
||||
tilesets["beach"].combine(rom_tilesets[0x102])
|
||||
tilesets["beach"].palette_id = 0x01
|
||||
tilesets["beach"].attr_bank = 0x22
|
||||
tilesets["beach"].attr_addr = 0x5000
|
||||
|
||||
tilesets["water"] = rom_tilesets[0x3E].copy()
|
||||
tilesets["water"].combine(rom_tilesets[0x103])
|
||||
tilesets["water"].learn_from(tilesets["basic"])
|
||||
tilesets["water"].remove(0x7A)
|
||||
tilesets["water"].remove(0xC8)
|
||||
tilesets["water"].palette_id = 0x09
|
||||
tilesets["water"].attr_bank = 0x22
|
||||
tilesets["water"].attr_addr = 0x6400
|
||||
|
||||
return tilesets
|
|
@ -0,0 +1,5 @@
|
|||
|
||||
def xyrange(w, h):
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
yield x, y
|
|
@ -0,0 +1,250 @@
|
|||
from .tileset import TileSet, solid_tiles, open_tiles, vertical_edge_tiles, horizontal_edge_tiles
|
||||
from .map import Map
|
||||
from typing import Set
|
||||
import random
|
||||
|
||||
|
||||
class ContradictionException(Exception):
|
||||
def __init__(self, x, y):
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
|
||||
class Cell:
|
||||
def __init__(self, x, y, tileset: TileSet, options: Set[int]):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.tileset = tileset
|
||||
self.init_options = options
|
||||
self.options = None
|
||||
self.result = None
|
||||
|
||||
def __set_new_options(self, new_options):
|
||||
if new_options != self.options:
|
||||
if self.result is not None:
|
||||
raise ContradictionException(self.x, self.y)
|
||||
if not new_options:
|
||||
raise ContradictionException(self.x, self.y)
|
||||
self.options = new_options
|
||||
return True
|
||||
return False
|
||||
|
||||
def update_options_up(self, cell: "Cell") -> bool:
|
||||
new_options = set()
|
||||
for tile in cell.options:
|
||||
new_options.update(cell.tileset.tiles[tile].up)
|
||||
new_options.intersection_update(self.options)
|
||||
if (self.y % 8) == 7:
|
||||
if cell.options.issubset(solid_tiles):
|
||||
new_options.intersection_update(solid_tiles)
|
||||
if cell.options.issubset(open_tiles):
|
||||
new_options.intersection_update(open_tiles)
|
||||
return self.__set_new_options(new_options)
|
||||
|
||||
def update_options_right(self, cell: "Cell") -> bool:
|
||||
new_options = set()
|
||||
for tile in cell.options:
|
||||
new_options.update(cell.tileset.tiles[tile].right)
|
||||
new_options.intersection_update(self.options)
|
||||
if (self.x % 10) == 0:
|
||||
if cell.options.issubset(solid_tiles):
|
||||
new_options.intersection_update(solid_tiles)
|
||||
if cell.options.issubset(open_tiles):
|
||||
new_options.intersection_update(open_tiles)
|
||||
return self.__set_new_options(new_options)
|
||||
|
||||
def update_options_down(self, cell: "Cell") -> bool:
|
||||
new_options = set()
|
||||
for tile in cell.options:
|
||||
new_options.update(cell.tileset.tiles[tile].down)
|
||||
new_options.intersection_update(self.options)
|
||||
if (self.y % 8) == 0:
|
||||
if cell.options.issubset(solid_tiles):
|
||||
new_options.intersection_update(solid_tiles)
|
||||
if cell.options.issubset(open_tiles):
|
||||
new_options.intersection_update(open_tiles)
|
||||
return self.__set_new_options(new_options)
|
||||
|
||||
def update_options_left(self, cell: "Cell") -> bool:
|
||||
new_options = set()
|
||||
for tile in cell.options:
|
||||
new_options.update(cell.tileset.tiles[tile].left)
|
||||
new_options.intersection_update(self.options)
|
||||
if (self.x % 10) == 9:
|
||||
if cell.options.issubset(solid_tiles):
|
||||
new_options.intersection_update(solid_tiles)
|
||||
if cell.options.issubset(open_tiles):
|
||||
new_options.intersection_update(open_tiles)
|
||||
return self.__set_new_options(new_options)
|
||||
|
||||
def __repr__(self):
|
||||
return f"Cell<{self.options}>"
|
||||
|
||||
|
||||
class WFCMap:
|
||||
def __init__(self, the_map: Map, tilesets, *, step_callback=None):
|
||||
self.cell_data = {}
|
||||
self.on_step = step_callback
|
||||
self.w = the_map.w * 10
|
||||
self.h = the_map.h * 8
|
||||
|
||||
for y in range(self.h):
|
||||
for x in range(self.w):
|
||||
tileset = tilesets[the_map.get(x//10, y//8).tileset_id]
|
||||
new_cell = Cell(x, y, tileset, tileset.all.copy())
|
||||
self.cell_data[(new_cell.x, new_cell.y)] = new_cell
|
||||
for y in range(self.h):
|
||||
self.cell_data[(0, y)].init_options.intersection_update(solid_tiles)
|
||||
self.cell_data[(self.w-1, y)].init_options.intersection_update(solid_tiles)
|
||||
for x in range(self.w):
|
||||
self.cell_data[(x, 0)].init_options.intersection_update(solid_tiles)
|
||||
self.cell_data[(x, self.h-1)].init_options.intersection_update(solid_tiles)
|
||||
|
||||
for x in range(0, self.w, 10):
|
||||
for y in range(self.h):
|
||||
self.cell_data[(x, y)].init_options.intersection_update(vertical_edge_tiles)
|
||||
for x in range(9, self.w, 10):
|
||||
for y in range(self.h):
|
||||
self.cell_data[(x, y)].init_options.intersection_update(vertical_edge_tiles)
|
||||
for y in range(0, self.h, 8):
|
||||
for x in range(self.w):
|
||||
self.cell_data[(x, y)].init_options.intersection_update(horizontal_edge_tiles)
|
||||
for y in range(7, self.h, 8):
|
||||
for x in range(self.w):
|
||||
self.cell_data[(x, y)].init_options.intersection_update(horizontal_edge_tiles)
|
||||
|
||||
for sy in range(the_map.h):
|
||||
for sx in range(the_map.w):
|
||||
the_map.get(sx, sy).room_type.seed(self, sx*10, sy*8)
|
||||
|
||||
for sy in range(the_map.h):
|
||||
for sx in range(the_map.w):
|
||||
room = the_map.get(sx, sy)
|
||||
room.edge_left.seed(self, sx * 10, sy * 8)
|
||||
room.edge_right.seed(self, sx * 10 + 9, sy * 8)
|
||||
room.edge_up.seed(self, sx * 10, sy * 8)
|
||||
room.edge_down.seed(self, sx * 10, sy * 8 + 7)
|
||||
|
||||
def initialize(self):
|
||||
for y in range(self.h):
|
||||
for x in range(self.w):
|
||||
cell = self.cell_data[x, y]
|
||||
cell.options = cell.init_options.copy()
|
||||
if self.on_step:
|
||||
self.on_step(self)
|
||||
propegation_set = set()
|
||||
for y in range(self.h):
|
||||
for x in range(self.w):
|
||||
propegation_set.add((x, y))
|
||||
self.propegate(propegation_set)
|
||||
for y in range(self.h):
|
||||
for x in range(self.w):
|
||||
cell = self.cell_data[x, y]
|
||||
cell.init_options = cell.options.copy()
|
||||
|
||||
def clear(self):
|
||||
for y in range(self.h):
|
||||
for x in range(self.w):
|
||||
cell = self.cell_data[(x, y)]
|
||||
if cell.result is None:
|
||||
cell.options = cell.init_options.copy()
|
||||
|
||||
propegation_set = set()
|
||||
for y in range(self.h):
|
||||
for x in range(self.w):
|
||||
cell = self.cell_data[(x, y)]
|
||||
if cell.result is not None:
|
||||
propegation_set.add((x, y))
|
||||
self.propegate(propegation_set)
|
||||
|
||||
def random_pick(self, cell):
|
||||
pick_list = list(cell.options)
|
||||
if not pick_list:
|
||||
raise ContradictionException(cell.x, cell.y)
|
||||
freqs = {}
|
||||
if (cell.x - 1, cell.y) in self.cell_data and len(self.cell_data[(cell.x - 1, cell.y)].options) == 1:
|
||||
tile_id = next(iter(self.cell_data[(cell.x - 1, cell.y)].options))
|
||||
for k, v in self.cell_data[(cell.x - 1, cell.y)].tileset.tiles[tile_id].right_freq.items():
|
||||
freqs[k] = freqs.get(k, 0) + v
|
||||
if (cell.x + 1, cell.y) in self.cell_data and len(self.cell_data[(cell.x + 1, cell.y)].options) == 1:
|
||||
tile_id = next(iter(self.cell_data[(cell.x + 1, cell.y)].options))
|
||||
for k, v in self.cell_data[(cell.x + 1, cell.y)].tileset.tiles[tile_id].left_freq.items():
|
||||
freqs[k] = freqs.get(k, 0) + v
|
||||
if (cell.x, cell.y - 1) in self.cell_data and len(self.cell_data[(cell.x, cell.y - 1)].options) == 1:
|
||||
tile_id = next(iter(self.cell_data[(cell.x, cell.y - 1)].options))
|
||||
for k, v in self.cell_data[(cell.x, cell.y - 1)].tileset.tiles[tile_id].down_freq.items():
|
||||
freqs[k] = freqs.get(k, 0) + v
|
||||
if (cell.x, cell.y + 1) in self.cell_data and len(self.cell_data[(cell.x, cell.y + 1)].options) == 1:
|
||||
tile_id = next(iter(self.cell_data[(cell.x, cell.y + 1)].options))
|
||||
for k, v in self.cell_data[(cell.x, cell.y + 1)].tileset.tiles[tile_id].up_freq.items():
|
||||
freqs[k] = freqs.get(k, 0) + v
|
||||
if freqs:
|
||||
weights_list = [freqs.get(n, 1) for n in pick_list]
|
||||
else:
|
||||
weights_list = [cell.tileset.tiles[n].frequency for n in pick_list]
|
||||
return random.choices(pick_list, weights_list)[0]
|
||||
|
||||
def build(self, start_x, start_y, w, h):
|
||||
cell_todo_list = []
|
||||
for y in range(start_y, start_y + h):
|
||||
for x in range(start_x, start_x+w):
|
||||
cell_todo_list.append(self.cell_data[(x, y)])
|
||||
|
||||
while cell_todo_list:
|
||||
cell_todo_list.sort(key=lambda c: len(c.options))
|
||||
l0 = len(cell_todo_list[0].options)
|
||||
idx = 1
|
||||
while idx < len(cell_todo_list) and len(cell_todo_list[idx].options) == l0:
|
||||
idx += 1
|
||||
idx = random.randint(0, idx - 1)
|
||||
cell = cell_todo_list[idx]
|
||||
if self.on_step:
|
||||
self.on_step(self, cur=(cell.x, cell.y))
|
||||
pick = self.random_pick(cell)
|
||||
cell_todo_list.pop(idx)
|
||||
cell.options = {pick}
|
||||
self.propegate({(cell.x, cell.y)})
|
||||
|
||||
for y in range(start_y, start_y + h):
|
||||
for x in range(start_x, start_x + w):
|
||||
self.cell_data[(x, y)].result = next(iter(self.cell_data[(x, y)].options))
|
||||
|
||||
def propegate(self, propegation_set):
|
||||
while propegation_set:
|
||||
xy = next(iter(propegation_set))
|
||||
propegation_set.remove(xy)
|
||||
|
||||
cell = self.cell_data[xy]
|
||||
if not cell.options:
|
||||
raise ContradictionException(cell.x, cell.y)
|
||||
x, y = xy
|
||||
if (x, y + 1) in self.cell_data and self.cell_data[(x, y + 1)].update_options_down(cell):
|
||||
propegation_set.add((x, y + 1))
|
||||
if (x + 1, y) in self.cell_data and self.cell_data[(x + 1, y)].update_options_right(cell):
|
||||
propegation_set.add((x + 1, y))
|
||||
if (x, y - 1) in self.cell_data and self.cell_data[(x, y - 1)].update_options_up(cell):
|
||||
propegation_set.add((x, y - 1))
|
||||
if (x - 1, y) in self.cell_data and self.cell_data[(x - 1, y)].update_options_left(cell):
|
||||
propegation_set.add((x - 1, y))
|
||||
|
||||
def store_tile_data(self, the_map: Map):
|
||||
for sy in range(the_map.h):
|
||||
for sx in range(the_map.w):
|
||||
tiles = []
|
||||
for y in range(8):
|
||||
for x in range(10):
|
||||
cell = self.cell_data[(x+sx*10, y+sy*8)]
|
||||
if cell.result is not None:
|
||||
tiles.append(cell.result)
|
||||
elif len(cell.options) == 0:
|
||||
tiles.append(1)
|
||||
else:
|
||||
tiles.append(2)
|
||||
the_map.get(sx, sy).tiles = tiles
|
||||
|
||||
def dump_option_count(self):
|
||||
for y in range(self.h):
|
||||
for x in range(self.w):
|
||||
print(f"{len(self.cell_data[(x, y)].options):2x}", end="")
|
||||
print()
|
||||
print()
|
|
@ -0,0 +1,436 @@
|
|||
from ..assembler import ASM
|
||||
from ..utils import formatText, setReplacementName
|
||||
from ..roomEditor import RoomEditor
|
||||
from .. import entityData
|
||||
import os
|
||||
import bsdiff4
|
||||
|
||||
def imageTo2bpp(filename):
|
||||
import PIL.Image
|
||||
baseimg = PIL.Image.new('P', (1,1))
|
||||
baseimg.putpalette((
|
||||
128, 0, 128,
|
||||
0, 0, 0,
|
||||
128, 128, 128,
|
||||
255, 255, 255,
|
||||
))
|
||||
img = PIL.Image.open(filename)
|
||||
img = img.quantize(colors=4, palette=baseimg)
|
||||
print (f"Palette: {img.getpalette()}")
|
||||
assert (img.size[0] % 8) == 0
|
||||
tileheight = 8 if img.size[1] == 8 else 16
|
||||
assert (img.size[1] % tileheight) == 0
|
||||
|
||||
cols = img.size[0] // 8
|
||||
rows = img.size[1] // tileheight
|
||||
result = bytearray(rows * cols * tileheight * 2)
|
||||
index = 0
|
||||
for ty in range(rows):
|
||||
for tx in range(cols):
|
||||
for y in range(tileheight):
|
||||
a = 0
|
||||
b = 0
|
||||
for x in range(8):
|
||||
c = img.getpixel((tx * 8 + x, ty * 16 + y))
|
||||
if c & 1:
|
||||
a |= 0x80 >> x
|
||||
if c & 2:
|
||||
b |= 0x80 >> x
|
||||
result[index] = a
|
||||
result[index+1] = b
|
||||
index += 2
|
||||
return result
|
||||
|
||||
|
||||
def updateGraphics(rom, bank, offset, data):
|
||||
if offset + len(data) > 0x4000:
|
||||
updateGraphics(rom, bank, offset, data[:0x4000-offset])
|
||||
updateGraphics(rom, bank + 1, 0, data[0x4000 - offset:])
|
||||
else:
|
||||
rom.banks[bank][offset:offset+len(data)] = data
|
||||
if bank < 0x34:
|
||||
rom.banks[bank-0x20][offset:offset + len(data)] = data
|
||||
|
||||
|
||||
def gfxMod(rom, filename):
|
||||
if os.path.exists(filename + ".names"):
|
||||
for line in open(filename + ".names", "rt"):
|
||||
if ":" in line:
|
||||
k, v = line.strip().split(":", 1)
|
||||
setReplacementName(k, v)
|
||||
|
||||
ext = os.path.splitext(filename)[1].lower()
|
||||
if ext == ".bin":
|
||||
updateGraphics(rom, 0x2C, 0, open(filename, "rb").read())
|
||||
elif ext in (".png", ".bmp"):
|
||||
updateGraphics(rom, 0x2C, 0, imageTo2bpp(filename))
|
||||
elif ext == ".bdiff":
|
||||
updateGraphics(rom, 0x2C, 0, prepatch(rom, 0x2C, 0, filename))
|
||||
elif ext == ".json":
|
||||
import json
|
||||
data = json.load(open(filename, "rt"))
|
||||
|
||||
for patch in data:
|
||||
if "gfx" in patch:
|
||||
updateGraphics(rom, int(patch["bank"], 16), int(patch["offset"], 16), imageTo2bpp(os.path.join(os.path.dirname(filename), patch["gfx"])))
|
||||
if "name" in patch:
|
||||
setReplacementName(patch["item"], patch["name"])
|
||||
else:
|
||||
updateGraphics(rom, 0x2C, 0, imageTo2bpp(filename))
|
||||
|
||||
|
||||
def createGfxImage(rom, filename):
|
||||
import PIL.Image
|
||||
bank_count = 8
|
||||
img = PIL.Image.new("P", (32 * 8, 32 * 8 * bank_count))
|
||||
img.putpalette((
|
||||
128, 0, 128,
|
||||
0, 0, 0,
|
||||
128, 128, 128,
|
||||
255, 255, 255,
|
||||
))
|
||||
for bank_nr in range(bank_count):
|
||||
bank = rom.banks[0x2C + bank_nr]
|
||||
for tx in range(32):
|
||||
for ty in range(16):
|
||||
for y in range(16):
|
||||
a = bank[tx * 32 + ty * 32 * 32 + y * 2]
|
||||
b = bank[tx * 32 + ty * 32 * 32 + y * 2 + 1]
|
||||
for x in range(8):
|
||||
c = 0
|
||||
if a & (0x80 >> x):
|
||||
c |= 1
|
||||
if b & (0x80 >> x):
|
||||
c |= 2
|
||||
img.putpixel((tx*8+x, bank_nr * 32 * 8 + ty*16+y), c)
|
||||
img.save(filename)
|
||||
|
||||
def prepatch(rom, bank, offset, filename):
|
||||
bank_count = 8
|
||||
base_sheet = []
|
||||
result = []
|
||||
for bank_nr in range(bank_count):
|
||||
base_sheet[0x4000 * bank_nr:0x4000 * (bank_nr + 1) - 1] = rom.banks[0x2C + bank_nr]
|
||||
with open(filename, "rb") as patch:
|
||||
file = patch.read()
|
||||
result = bsdiff4.patch(src_bytes=bytes(base_sheet), patch_bytes=file)
|
||||
return result
|
||||
|
||||
def noSwordMusic(rom):
|
||||
# Skip no-sword music override
|
||||
# Instead of loading the sword level, we put the value 1 in the A register, indicating we have a sword.
|
||||
rom.patch(2, 0x0151, ASM("ld a, [$DB4E]"), ASM("ld a, $01"), fill_nop=True)
|
||||
rom.patch(2, 0x3AEF, ASM("ld a, [$DB4E]"), ASM("ld a, $01"), fill_nop=True)
|
||||
rom.patch(3, 0x0996, ASM("ld a, [$DB4E]"), ASM("ld a, $01"), fill_nop=True)
|
||||
rom.patch(3, 0x0B35, ASM("ld a, [$DB44]"), ASM("ld a, $01"), fill_nop=True)
|
||||
|
||||
|
||||
def removeNagMessages(rom):
|
||||
# Remove "this object is heavy, bla bla", and other nag messages when touching an object
|
||||
rom.patch(0x02, 0x32BB, ASM("ld a, [$C14A]"), ASM("ld a, $01"), fill_nop=True) # crystal blocks
|
||||
rom.patch(0x02, 0x32EC, ASM("ld a, [$C5A6]"), ASM("ld a, $01"), fill_nop=True) # cracked blocks
|
||||
rom.patch(0x02, 0x32D3, ASM("jr nz, $25"), ASM("jr $25"), fill_nop=True) # stones/pots
|
||||
rom.patch(0x02, 0x2B88, ASM("jr nz, $0F"), ASM("jr $0F"), fill_nop=True) # ice blocks
|
||||
|
||||
|
||||
def removeLowHPBeep(rom):
|
||||
rom.patch(2, 0x233A, ASM("ld hl, $FFF3\nld [hl], $04"), b"", fill_nop=True) # Remove health beep
|
||||
|
||||
|
||||
def slowLowHPBeep(rom):
|
||||
rom.patch(2, 0x2338, ASM("ld a, $30"), ASM("ld a, $60")) # slow slow hp beep
|
||||
|
||||
|
||||
def removeFlashingLights(rom):
|
||||
# Remove the switching between two backgrounds at mamu, always show the spotlights.
|
||||
rom.patch(0x00, 0x01EB, ASM("ldh a, [$E7]\nrrca\nand $80"), ASM("ld a, $80"), fill_nop=True)
|
||||
# Remove flashing colors from shopkeeper killing you after stealing and the mad batter giving items.
|
||||
rom.patch(0x24, 0x3B77, ASM("push bc"), ASM("ret"))
|
||||
|
||||
|
||||
def forceLinksPalette(rom, index):
|
||||
# This forces the link sprite into a specific palette index ignoring the tunic options.
|
||||
rom.patch(0, 0x1D8C,
|
||||
ASM("ld a, [$DC0F]\nand a\njr z, $03\ninc a"),
|
||||
ASM("ld a, $%02X" % (index)), fill_nop=True)
|
||||
rom.patch(0, 0x1DD2,
|
||||
ASM("ld a, [$DC0F]\nand a\njr z, $03\ninc a"),
|
||||
ASM("ld a, $%02X" % (index)), fill_nop=True)
|
||||
# Fix the waking up from bed palette
|
||||
if index == 1:
|
||||
rom.patch(0x21, 0x33FC, "A222", "FF05")
|
||||
elif index == 2:
|
||||
rom.patch(0x21, 0x33FC, "A222", "3F14")
|
||||
elif index == 3:
|
||||
rom.patch(0x21, 0x33FC, "A222", "037E")
|
||||
for n in range(6):
|
||||
rom.patch(0x05, 0x1261 + n * 2, "00", f"{index:02x}")
|
||||
|
||||
|
||||
def fastText(rom):
|
||||
rom.patch(0x00, 0x24CA, ASM("jp $2485"), ASM("call $2485"))
|
||||
|
||||
|
||||
def noText(rom):
|
||||
for idx in range(len(rom.texts)):
|
||||
if not isinstance(rom.texts[idx], int) and (idx < 0x217 or idx > 0x21A):
|
||||
rom.texts[idx] = rom.texts[idx][-1:]
|
||||
|
||||
|
||||
def reduceMessageLengths(rom, rnd):
|
||||
# Into text from Marin. Got to go fast, so less text. (This intro text is very long)
|
||||
rom.texts[0x01] = formatText(rnd.choice([
|
||||
"Let's a go!",
|
||||
"Remember, sword goes on A!",
|
||||
"Avoid the heart piece of shame!",
|
||||
"Marin? No, this is Zelda. Welcome to Hyrule",
|
||||
"Why are you in my bed?",
|
||||
"This is not a Mario game!",
|
||||
"MuffinJets was here...",
|
||||
"Remember, there are no bugs in LADX",
|
||||
"#####, #####, you got to wake up!\nDinner is ready.",
|
||||
"Go find the stepladder",
|
||||
"Pizza power!",
|
||||
"Eastmost penninsula is the secret",
|
||||
"There is no cow level",
|
||||
"You cannot lift rocks with your bear hands",
|
||||
"Thank you, daid!",
|
||||
"There, there now. Just relax. You've been asleep for almost nine hours now."
|
||||
]))
|
||||
|
||||
# Reduce length of a bunch of common texts
|
||||
rom.texts[0xEA] = formatText("You've got a Guardian Acorn!")
|
||||
rom.texts[0xEB] = rom.texts[0xEA]
|
||||
rom.texts[0xEC] = rom.texts[0xEA]
|
||||
rom.texts[0x08] = formatText("You got a Piece of Power!")
|
||||
rom.texts[0xEF] = formatText("You found a {SEASHELL}!")
|
||||
rom.texts[0xA7] = formatText("You've got the {COMPASS}!")
|
||||
|
||||
rom.texts[0x07] = formatText("You need the {NIGHTMARE_KEY}!")
|
||||
rom.texts[0x8C] = formatText("You need a {KEY}!") # keyhole block
|
||||
|
||||
rom.texts[0x09] = formatText("Ahhh... It has the Sleepy {TOADSTOOL}, it does! We'll mix it up something in a jiffy, we will!")
|
||||
rom.texts[0x0A] = formatText("The last thing I kin remember was bitin' into a big juicy {TOADSTOOL}... Then, I had the darndest dream... I was a raccoon! Yeah, sounds strange, but it sure was fun!")
|
||||
rom.texts[0x0F] = formatText("You pick the {TOADSTOOL}... As you hold it over your head, a mellow aroma flows into your nostrils.")
|
||||
rom.texts[0x13] = formatText("You've learned the ^{SONG1}!^ This song will always remain in your heart!")
|
||||
rom.texts[0x18] = formatText("Will you give me 28 {RUPEES} for my secret?", ask="Give Don't")
|
||||
rom.texts[0x19] = formatText("How about it? 42 {RUPEES} for my little secret...", ask="Give Don't")
|
||||
rom.texts[0x1e] = formatText("...You're so cute! I'll give you a 7 {RUPEE} discount!")
|
||||
rom.texts[0x2d] = formatText("{ARROWS_10}\n10 {RUPEES}!", ask="Buy Don't")
|
||||
rom.texts[0x32] = formatText("{SHIELD}\n20 {RUPEES}!", ask="Buy Don't")
|
||||
rom.texts[0x33] = formatText("Ten {BOMB}\n10 {RUPEES}", ask="Buy Don't")
|
||||
rom.texts[0x3d] = formatText("It's a {SHIELD}! There is space for your name!")
|
||||
rom.texts[0x42] = formatText("It's 30 {RUPEES}! You can play the game three more times with this!")
|
||||
rom.texts[0x45] = formatText("How about some fishing, little buddy? I'll only charge you 10 {RUPEES}...", ask="Fish Not Now")
|
||||
rom.texts[0x4b] = formatText("Wow! Nice Fish! It's a lunker!! I'll give you a 20 {RUPEE} prize! Try again?", ask="Cast Not Now")
|
||||
rom.texts[0x4e] = formatText("You're short of {RUPEES}? Don't worry about it. You just come back when you have more money, little buddy.")
|
||||
rom.texts[0x4f] = formatText("You've got a {HEART_PIECE}! Press SELECT on the Subscreen to see.")
|
||||
rom.texts[0x8e] = formatText("Well, it's an {OCARINA}, but you don't know how to play it...")
|
||||
rom.texts[0x90] = formatText("You found the {POWER_BRACELET}! At last, you can pick up pots and stones!")
|
||||
rom.texts[0x91] = formatText("You got your {SHIELD} back! Press the button and repel enemies with it!")
|
||||
rom.texts[0x93] = formatText("You've got the {HOOKSHOT}! Its chain stretches long when you use it!")
|
||||
rom.texts[0x94] = formatText("You've got the {MAGIC_ROD}! Now you can burn things! Burn it! Burn, baby burn!")
|
||||
rom.texts[0x95] = formatText("You've got the {PEGASUS_BOOTS}! If you hold down the Button, you can dash!")
|
||||
rom.texts[0x96] = formatText("You've got the {OCARINA}! You should learn to play many songs!")
|
||||
rom.texts[0x97] = formatText("You've got the {FEATHER}! It feels like your body is a lot lighter!")
|
||||
rom.texts[0x98] = formatText("You've got a {SHOVEL}! Now you can feel the joy of digging!")
|
||||
rom.texts[0x99] = formatText("You've got some {MAGIC_POWDER}! Try sprinkling it on a variety of things!")
|
||||
rom.texts[0x9b] = formatText("You found your {SWORD}! It must be yours because it has your name engraved on it!")
|
||||
rom.texts[0x9c] = formatText("You've got the {FLIPPERS}! If you press the B Button while you swim, you can dive underwater!")
|
||||
rom.texts[0x9e] = formatText("You've got a new {SWORD}! You should put your name on it right away!")
|
||||
rom.texts[0x9f] = formatText("You've got a new {SWORD}! You should put your name on it right away!")
|
||||
rom.texts[0xa0] = formatText("You found the {MEDICINE}! You should apply this and see what happens!")
|
||||
rom.texts[0xa1] = formatText("You've got the {TAIL_KEY}! Now you can open the Tail Cave gate!")
|
||||
rom.texts[0xa2] = formatText("You've got the {SLIME_KEY}! Now you can open the gate in Ukuku Prairie!")
|
||||
rom.texts[0xa3] = formatText("You've got the {ANGLER_KEY}!")
|
||||
rom.texts[0xa4] = formatText("You've got the {FACE_KEY}!")
|
||||
rom.texts[0xa5] = formatText("You've got the {BIRD_KEY}!")
|
||||
rom.texts[0xa6] = formatText("At last, you got a {MAP}! Press the START Button to look at it!")
|
||||
rom.texts[0xa8] = formatText("You found a {STONE_BEAK}! Let's find the owl statue that belongs to it.")
|
||||
rom.texts[0xa9] = formatText("You've got the {NIGHTMARE_KEY}! Now you can open the door to the Nightmare's Lair!")
|
||||
rom.texts[0xaa] = formatText("You got a {KEY}! You can open a locked door.")
|
||||
rom.texts[0xab] = formatText("You got 20 {RUPEES}! JOY!", center=True)
|
||||
rom.texts[0xac] = formatText("You got 50 {RUPEES}! Very Nice!", center=True)
|
||||
rom.texts[0xad] = formatText("You got 100 {RUPEES}! You're Happy!", center=True)
|
||||
rom.texts[0xae] = formatText("You got 200 {RUPEES}! You're Ecstatic!", center=True)
|
||||
rom.texts[0xdc] = formatText("Ribbit! Ribbit! I'm Mamu, on vocals! But I don't need to tell you that, do I? Everybody knows me! Want to hang out and listen to us jam? For 300 Rupees, we'll let you listen to a previously unreleased cut! What do you do?", ask="Pay Leave")
|
||||
rom.texts[0xe8] = formatText("You've found a {GOLD_LEAF}! Press START to see how many you've collected!")
|
||||
rom.texts[0xed] = formatText("You've got the Mirror Shield! You can now turnback the beams you couldn't block before!")
|
||||
rom.texts[0xee] = formatText("You've got a more Powerful {POWER_BRACELET}! Now you can almost lift a whale!")
|
||||
rom.texts[0xf0] = formatText("Want to go on a raft ride for a hundred {RUPEES}?", ask="Yes No Way")
|
||||
|
||||
|
||||
def allowColorDungeonSpritesEverywhere(rom):
|
||||
# Set sprite set numbers $01-$40 to map to the color dungeon sprites
|
||||
rom.patch(0x00, 0x2E6F, "00", "15")
|
||||
# Patch the spriteset loading code to load the 4 entries from the normal table instead of skipping this for color dungeon specific exception weirdness
|
||||
rom.patch(0x00, 0x0DA4, ASM("jr nc, $05"), ASM("jr nc, $41"))
|
||||
rom.patch(0x00, 0x0DE5, ASM("""
|
||||
ldh a, [$F7]
|
||||
cp $FF
|
||||
jr nz, $06
|
||||
ld a, $01
|
||||
ldh [$91], a
|
||||
jr $40
|
||||
"""), ASM("""
|
||||
jr $0A ; skip over the rest of the code
|
||||
cp $FF ; check if color dungeon
|
||||
jp nz, $0DAB
|
||||
inc d
|
||||
jp $0DAA
|
||||
"""), fill_nop=True)
|
||||
# Disable color dungeon specific tile load hacks
|
||||
rom.patch(0x00, 0x06A7, ASM("jr nz, $22"), ASM("jr $22"))
|
||||
rom.patch(0x00, 0x2E77, ASM("jr nz, $0B"), ASM("jr $0B"))
|
||||
|
||||
# Finally fill in the sprite data for the color dungeon
|
||||
for n in range(22):
|
||||
data = bytearray()
|
||||
for m in range(4):
|
||||
idx = rom.banks[0x20][0x06AA + 44 * m + n * 2]
|
||||
bank = rom.banks[0x20][0x06AA + 44 * m + n * 2 + 1]
|
||||
if idx == 0 and bank == 0:
|
||||
v = 0xFF
|
||||
elif bank == 0x35:
|
||||
v = idx - 0x40
|
||||
elif bank == 0x31:
|
||||
v = idx
|
||||
elif bank == 0x2E:
|
||||
v = idx + 0x40
|
||||
else:
|
||||
assert False, "%02x %02x" % (idx, bank)
|
||||
data += bytes([v])
|
||||
rom.room_sprite_data_indoor[0x200 + n] = data
|
||||
|
||||
# Patch the graphics loading code to use DMA and load all sets that need to be reloaded, not just the first and last
|
||||
rom.patch(0x00, 0x06FA, 0x07AF, ASM("""
|
||||
;We enter this code with the right bank selected for tile data copy,
|
||||
;d = tile row (source addr = (d*$100+$4000))
|
||||
;e = $00
|
||||
;$C197 = index of sprite set to update (target addr = ($8400 + $100 * [$C197]))
|
||||
ld a, d
|
||||
add a, $40
|
||||
ldh [$51], a
|
||||
xor a
|
||||
ldh [$52], a
|
||||
ldh [$54], a
|
||||
ld a, [$C197]
|
||||
add a, $84
|
||||
ldh [$53], a
|
||||
ld a, $0F
|
||||
ldh [$55], a
|
||||
|
||||
; See if we need to do anything next
|
||||
ld a, [$C10E] ; check the 2nd update flag
|
||||
and a
|
||||
jr nz, getNext
|
||||
ldh [$91], a ; no 2nd update flag, so clear primary update flag
|
||||
ret
|
||||
getNext:
|
||||
ld hl, $C197
|
||||
inc [hl]
|
||||
res 2, [hl]
|
||||
ld a, [$C10D]
|
||||
cp [hl]
|
||||
ret nz
|
||||
xor a ; clear the 2nd update flag when we prepare to update the last spriteset
|
||||
ld [$C10E], a
|
||||
ret
|
||||
"""), fill_nop=True)
|
||||
rom.patch(0x00, 0x0738, "00" * (0x073E - 0x0738), ASM("""
|
||||
; we get here by some color dungeon specific code jumping to this position
|
||||
; We still need that color dungeon specific code as it loads background tiles
|
||||
xor a
|
||||
ldh [$91], a
|
||||
ldh [$93], a
|
||||
ret
|
||||
"""))
|
||||
rom.patch(0x00, 0x073E, "00" * (0x07AF - 0x073E), ASM("""
|
||||
;If we get here, only the 2nd flag is filled and the primary is not. So swap those around.
|
||||
ld a, [$C10D] ;copy the index number
|
||||
ld [$C197], a
|
||||
xor a
|
||||
ld [$C10E], a ; clear the 2nd update flag
|
||||
inc a
|
||||
ldh [$91], a ; set the primary update flag
|
||||
ret
|
||||
"""), fill_nop=True)
|
||||
|
||||
|
||||
def updateSpriteData(rom):
|
||||
# Change the special sprite change exceptions
|
||||
rom.patch(0x00, 0x0DAD, 0x0DDB, ASM("""
|
||||
; Check for indoor
|
||||
ld a, d
|
||||
and a
|
||||
jr nz, noChange
|
||||
ldh a, [$F6] ; hMapRoom
|
||||
cp $C9
|
||||
jr nz, sirenRoomEnd
|
||||
ld a, [$D8C9] ; wOverworldRoomStatus + ROOM_OW_SIREN
|
||||
and $20
|
||||
jr z, noChange
|
||||
ld hl, $7837
|
||||
jp $0DFE
|
||||
sirenRoomEnd:
|
||||
ldh a, [$F6] ; hMapRoom
|
||||
cp $D8
|
||||
jr nz, noChange
|
||||
ld a, [$D8FD] ; wOverworldRoomStatus + ROOM_OW_WALRUS
|
||||
and $20
|
||||
jr z, noChange
|
||||
ld hl, $783B
|
||||
jp $0DFE
|
||||
noChange:
|
||||
"""), fill_nop=True)
|
||||
rom.patch(0x20, 0x3837, "A4FF8BFF", "A461FF72")
|
||||
rom.patch(0x20, 0x383B, "A44DFFFF", "A4C5FF70")
|
||||
|
||||
# For each room update the sprite load data based on which entities are in there.
|
||||
for room_nr in range(0x316):
|
||||
if room_nr == 0x2FF:
|
||||
continue
|
||||
values = [None, None, None, None]
|
||||
if room_nr == 0x00E: # D7 entrance opening
|
||||
values[2] = 0xD6
|
||||
values[3] = 0xD7
|
||||
if 0x211 <= room_nr <= 0x21E: # D7 throwing ball thing.
|
||||
values[0] = 0x66
|
||||
r = RoomEditor(rom, room_nr)
|
||||
for obj in r.objects:
|
||||
if obj.type_id == 0xC5 and room_nr < 0x100: # Pushable Gravestone
|
||||
values[3] = 0x82
|
||||
for x, y, entity in r.entities:
|
||||
sprite_data = entityData.SPRITE_DATA[entity]
|
||||
if callable(sprite_data):
|
||||
sprite_data = sprite_data(r)
|
||||
if sprite_data is None:
|
||||
continue
|
||||
for m in range(0, len(sprite_data), 2):
|
||||
idx, value = sprite_data[m:m+2]
|
||||
if values[idx] is None:
|
||||
values[idx] = value
|
||||
elif isinstance(values[idx], set) and isinstance(value, set):
|
||||
values[idx] = values[idx].intersection(value)
|
||||
assert len(values[idx]) > 0
|
||||
elif isinstance(values[idx], set) and value in values[idx]:
|
||||
values[idx] = value
|
||||
elif isinstance(value, set) and values[idx] in value:
|
||||
pass
|
||||
elif values[idx] == value:
|
||||
pass
|
||||
else:
|
||||
assert False, "Room: %03x cannot load graphics for entity: %02x (Index: %d Failed: %s, Active: %s)" % (room_nr, entity, idx, value, values[idx])
|
||||
|
||||
data = bytearray()
|
||||
for v in values:
|
||||
if isinstance(v, set):
|
||||
v = next(iter(v))
|
||||
elif v is None:
|
||||
v = 0xff
|
||||
data.append(v)
|
||||
|
||||
if room_nr < 0x100:
|
||||
rom.room_sprite_data_overworld[room_nr] = data
|
||||
else:
|
||||
rom.room_sprite_data_indoor[room_nr - 0x100] = data
|
|
@ -0,0 +1,125 @@
|
|||
import os
|
||||
import binascii
|
||||
from ..assembler import ASM
|
||||
from ..utils import formatText
|
||||
|
||||
ItemNameLookupTable = 0x0100
|
||||
ItemNameLookupSize = 2
|
||||
TotalRoomCount = 0x316
|
||||
|
||||
AnItemText = "an item"
|
||||
ItemNameStringBufferStart = ItemNameLookupTable + \
|
||||
TotalRoomCount * ItemNameLookupSize
|
||||
|
||||
|
||||
def addBank34(rom, item_list):
|
||||
my_path = os.path.dirname(__file__)
|
||||
rom.patch(0x34, 0x0000, ItemNameLookupTable, ASM("""
|
||||
; Get the pointer in the lookup table, doubled as it's two bytes
|
||||
ld hl, $2080
|
||||
push de
|
||||
call OffsetPointerByRoomNumber
|
||||
pop de
|
||||
add hl, hl
|
||||
|
||||
ldi a, [hl] ; hl = *hl
|
||||
ld h, [hl]
|
||||
ld l, a
|
||||
|
||||
; If there's no data, bail
|
||||
ld a, l
|
||||
or h
|
||||
jp z, SwitchBackTo3E
|
||||
|
||||
ld de, wCustomMessage
|
||||
; Copy "Got " to de
|
||||
ld a, 71
|
||||
ld [de], a
|
||||
inc de
|
||||
ld a, 111
|
||||
ld [de], a
|
||||
inc de
|
||||
ld a, 116
|
||||
ld [de], a
|
||||
inc de
|
||||
ld a, 32
|
||||
ld [de], a
|
||||
inc de
|
||||
; Copy in our item name
|
||||
call MessageCopyString
|
||||
SwitchBackTo3E:
|
||||
; Bail
|
||||
ld a, $3e ; Set bank number
|
||||
jp $080C ; switch bank
|
||||
|
||||
; this should be shared but I got link errors
|
||||
OffsetPointerByRoomNumber:
|
||||
ldh a, [$F6] ; map room
|
||||
ld e, a
|
||||
ld a, [$DBA5] ; is indoor
|
||||
ld d, a
|
||||
ldh a, [$F7] ; mapId
|
||||
cp $FF
|
||||
jr nz, .notColorDungeon
|
||||
|
||||
ld d, $03
|
||||
jr .notCavesA
|
||||
|
||||
.notColorDungeon:
|
||||
cp $1A
|
||||
jr nc, .notCavesA
|
||||
cp $06
|
||||
jr c, .notCavesA
|
||||
inc d
|
||||
.notCavesA:
|
||||
add hl, de
|
||||
ret
|
||||
""" + open(os.path.join(my_path, "bank3e.asm/message.asm"), "rt").read(), 0x4000), fill_nop=True)
|
||||
|
||||
nextItemLookup = ItemNameStringBufferStart
|
||||
nameLookup = {
|
||||
|
||||
}
|
||||
|
||||
name = AnItemText
|
||||
|
||||
def add_or_get_name(name):
|
||||
nonlocal nextItemLookup
|
||||
if name in nameLookup:
|
||||
return nameLookup[name]
|
||||
if len(name) + 1 + nextItemLookup >= 0x4000:
|
||||
return nameLookup[AnItemText]
|
||||
asm = ASM(f'db "{name}", $ff\n')
|
||||
rom.patch(0x34, nextItemLookup, None, asm)
|
||||
patch_len = len(binascii.unhexlify(asm))
|
||||
nameLookup[name] = nextItemLookup + 0x4000
|
||||
nextItemLookup += patch_len
|
||||
return nameLookup[name]
|
||||
|
||||
item_text_addr = add_or_get_name(AnItemText)
|
||||
#error_text_addr = add_or_get_name("Please report this check to #bug-reports in the AP discord")
|
||||
def swap16(x):
|
||||
assert x <= 0xFFFF
|
||||
return (x >> 8) | ((x & 0xFF) << 8)
|
||||
|
||||
def to_hex_address(x):
|
||||
return f"{swap16(x):04x}"
|
||||
|
||||
# Set defaults for every room
|
||||
for i in range(TotalRoomCount):
|
||||
rom.patch(0x34, ItemNameLookupTable + i *
|
||||
ItemNameLookupSize, None, to_hex_address(0))
|
||||
|
||||
for item in item_list:
|
||||
if not item.custom_item_name:
|
||||
continue
|
||||
assert item.room < TotalRoomCount, item.room
|
||||
# Item names of exactly 255 characters will cause overwrites to occur in the text box
|
||||
# assert len(item.custom_item_name) < 0x100
|
||||
# Custom text is only 95 bytes long, restrict to 50
|
||||
addr = add_or_get_name(item.custom_item_name[:50])
|
||||
rom.patch(0x34, ItemNameLookupTable + item.room *
|
||||
ItemNameLookupSize, None, to_hex_address(addr))
|
||||
if item.extra:
|
||||
rom.patch(0x34, ItemNameLookupTable + item.extra *
|
||||
ItemNameLookupSize, None, to_hex_address(addr))
|
|
@ -0,0 +1,303 @@
|
|||
CheckIfLoadBowWow:
|
||||
; Check has bowwow flag
|
||||
ld a, [$DB56]
|
||||
cp $01
|
||||
jr nz, .noLoadBowwow
|
||||
|
||||
ldh a, [$F6] ; load map number
|
||||
cp $22
|
||||
jr z, .loadBowwow
|
||||
cp $23
|
||||
jr z, .loadBowwow
|
||||
cp $24
|
||||
jr z, .loadBowwow
|
||||
cp $32
|
||||
jr z, .loadBowwow
|
||||
cp $33
|
||||
jr z, .loadBowwow
|
||||
cp $34
|
||||
jr z, .loadBowwow
|
||||
|
||||
.noLoadBowwow:
|
||||
ld e, $00
|
||||
ret
|
||||
|
||||
.loadBowwow:
|
||||
ld e, $01
|
||||
ret
|
||||
|
||||
|
||||
; Special handler for when Bowwow tries to eat an entity.
|
||||
; Our target entity index is loaded in BC.
|
||||
BowwowEat:
|
||||
; Load the entity type into A
|
||||
ld hl, $C3A0 ; entity type
|
||||
add hl, bc
|
||||
ld a, [hl]
|
||||
|
||||
; Check if we need special handling for bosses
|
||||
cp $59 ; Moldorm
|
||||
jr z, BowwowHurtEnemy
|
||||
cp $5C ; Genie
|
||||
jr z, BowwowEatGenie
|
||||
cp $5B ; SlimeEye
|
||||
jp z, BowwowEatSlimeEye
|
||||
cp $65 ; AnglerFish
|
||||
jr z, BowwowHurtEnemy
|
||||
cp $5D ; SlimeEel
|
||||
jp z, BowwowEatSlimeEel
|
||||
cp $5A ; Facade
|
||||
jr z, BowwowHurtEnemy
|
||||
cp $63 ; Eagle
|
||||
jr z, BowwowHurtEnemy
|
||||
cp $62 ; Hot head
|
||||
jp z, BowwowEatHotHead
|
||||
cp $F9 ; Hardhit beetle
|
||||
jr z, BowwowHurtEnemy
|
||||
cp $E6 ; Nightmare (all forms)
|
||||
jp z, BowwowEatNightmare
|
||||
|
||||
; Check for special handling for minibosses
|
||||
cp $87 ; Lanmola
|
||||
jr z, BowwowHurtEnemy
|
||||
; cp $88 ; Armos knight
|
||||
; No special handling, just eat him, solves the fight real quick.
|
||||
cp $81 ; rolling bones
|
||||
jr z, BowwowHurtEnemy
|
||||
cp $89 ; Hinox
|
||||
jr z, BowwowHurtEnemy
|
||||
cp $8E ; Cue ball
|
||||
jr z, BowwowHurtEnemy
|
||||
;cp $5E ; Gnoma
|
||||
;jr z, BowwowHurtEnemy
|
||||
cp $5F ; Master stalfos
|
||||
jr z, BowwowHurtEnemy
|
||||
cp $92 ; Smasher
|
||||
jp z, BowwowEatSmasher
|
||||
cp $BC ; Grim Creeper
|
||||
jp z, BowwowEatGrimCreeper
|
||||
cp $BE ; Blaino
|
||||
jr z, BowwowHurtEnemy
|
||||
cp $F8 ; Giant buzz blob
|
||||
jr z, BowwowHurtEnemy
|
||||
cp $F4 ; Avalaunch
|
||||
jr z, BowwowHurtEnemy
|
||||
|
||||
; Some enemies
|
||||
cp $E9 ; Color dungeon shell
|
||||
jr z, BowwowHurtEnemy
|
||||
cp $EA ; Color dungeon shell
|
||||
jr z, BowwowHurtEnemy
|
||||
cp $EB ; Color dungeon shell
|
||||
jr z, BowwowHurtEnemy
|
||||
|
||||
; Play SFX
|
||||
ld a, $03
|
||||
ldh [$F2], a
|
||||
; Call normal "destroy entity and drop item" handler
|
||||
jp $3F50
|
||||
|
||||
BowwowHurtEnemy:
|
||||
; Hurt enemy with damage type zero (sword)
|
||||
ld a, $00
|
||||
ld [$C19E], a
|
||||
rst $18
|
||||
; Play SFX
|
||||
ld a, $03
|
||||
ldh [$F2], a
|
||||
ret
|
||||
|
||||
BowwowEatGenie:
|
||||
; Get private state to find out if this is a bottle or the genie
|
||||
ld hl, $C2B0
|
||||
add hl, bc
|
||||
ld a, [hl]
|
||||
; Prepare loading state from hl
|
||||
ld hl, $C290
|
||||
add hl, bc
|
||||
|
||||
cp $00
|
||||
jr z, .bottle
|
||||
cp $01
|
||||
jr z, .ghost
|
||||
ret
|
||||
|
||||
.ghost:
|
||||
; Get current state
|
||||
ld a, [hl]
|
||||
cp $04 ; Flying around without bottle
|
||||
jr z, BowwowHurtEnemy
|
||||
ret
|
||||
|
||||
.bottle:
|
||||
; Get current state
|
||||
ld a, [hl]
|
||||
cp $03 ; Hopping around in bottle
|
||||
jr z, BowwowHurtEnemy
|
||||
ret
|
||||
|
||||
BowwowEatSlimeEye:
|
||||
; On set privateCountdown2 to $0C to split, when privateState1 is $04 and state is $03
|
||||
ld hl, $C290 ; state
|
||||
add hl, bc
|
||||
ld a, [hl]
|
||||
cp $03
|
||||
jr nz, .skipSplit
|
||||
|
||||
ld hl, $C2B0 ; private state1
|
||||
add hl, bc
|
||||
ld a, [hl]
|
||||
cp $04
|
||||
jr nz, .skipSplit
|
||||
|
||||
ld hl, $C300 ; private countdown 2
|
||||
add hl, bc
|
||||
ld [hl], $0C
|
||||
|
||||
.skipSplit:
|
||||
jp BowwowHurtEnemy
|
||||
|
||||
BowwowEatSlimeEel:
|
||||
; Get private state to find out if this is the tail or the head
|
||||
ld hl, $C2B0
|
||||
add hl, bc
|
||||
ld a, [hl]
|
||||
cp $01 ; not the head, so, skip.
|
||||
ret nz
|
||||
|
||||
; Check if we are pulled out of the wall
|
||||
ld hl, $C290
|
||||
add hl, bc
|
||||
ld a, [hl]
|
||||
cp $03 ; pulled out of the wall
|
||||
jr nz, .knockOutOfWall
|
||||
|
||||
ld hl, $D204
|
||||
ld a, [hl]
|
||||
cp $07
|
||||
jr nc, .noExtraDamage
|
||||
inc [hl]
|
||||
.noExtraDamage:
|
||||
jp BowwowHurtEnemy
|
||||
|
||||
.knockOutOfWall:
|
||||
ld [hl], $03 ; set state to $03
|
||||
ld hl, $C210 ; Y position
|
||||
add hl, bc
|
||||
ld a, [hl]
|
||||
ld [hl], $60
|
||||
cp $48
|
||||
jp nc, BowwowHurtEnemy
|
||||
ld [hl], $30
|
||||
jp BowwowHurtEnemy
|
||||
|
||||
|
||||
BowwowEatHotHead:
|
||||
; Load health of hothead
|
||||
ld hl, $C360
|
||||
add hl, bc
|
||||
ld a, [hl]
|
||||
cp $20
|
||||
jr c, .lowHp
|
||||
ld [hl], $20
|
||||
.lowHp:
|
||||
jp BowwowHurtEnemy
|
||||
|
||||
BowwowEatSmasher:
|
||||
; Check if this is the ball or the monster
|
||||
ld hl, $C440
|
||||
add hl, bc
|
||||
ld a, [hl]
|
||||
and a
|
||||
ret nz
|
||||
jp BowwowHurtEnemy
|
||||
|
||||
BowwowEatGrimCreeper:
|
||||
; Check if this is the main enemy or the smaller ones. Only kill the small ones
|
||||
ld hl, $C2B0
|
||||
add hl, bc
|
||||
ld a, [hl]
|
||||
and a
|
||||
ret z
|
||||
jp BowwowHurtEnemy
|
||||
|
||||
BowwowEatNightmare:
|
||||
; Check if this is the staircase.
|
||||
ld hl, $C390
|
||||
add hl, bc
|
||||
ld a, [hl]
|
||||
cp $02
|
||||
ret z
|
||||
|
||||
; Prepare loading state from hl
|
||||
ld hl, $C290
|
||||
add hl, bc
|
||||
|
||||
ld a, [$D219] ; which form has the nightmare
|
||||
cp $01
|
||||
jr z, .slimeForm
|
||||
cp $02
|
||||
jr z, .agahnimForm
|
||||
cp $03 ; moldormForm
|
||||
jp z, BowwowHurtEnemy
|
||||
cp $04 ; ganon and lanmola
|
||||
jp z, BowwowHurtEnemy
|
||||
cp $05 ; dethl
|
||||
jp z, BowwowHurtEnemy
|
||||
; 0 is the intro form
|
||||
ret
|
||||
|
||||
.slimeForm:
|
||||
ld a, [hl]
|
||||
cp $02
|
||||
jr z, .canHurtSlime
|
||||
cp $03
|
||||
ret nz
|
||||
|
||||
.canHurtSlime:
|
||||
; We need quite some custom handling, normally the nightmare checks very directly if you use powder.
|
||||
; No idea why this insta kills the slime form...
|
||||
; Change state to hurt state
|
||||
ld [hl], $07
|
||||
; Set flash count
|
||||
ld hl, $C420
|
||||
add hl, bc
|
||||
ld [hl], $14
|
||||
; play proper sfx
|
||||
ld a, $07
|
||||
ldh [$F3], a
|
||||
ld a, $37
|
||||
ldh [$F2], a
|
||||
; No idea why this is done, but it happens when you use powder on the slime
|
||||
ld a, $03
|
||||
ld [$D220], a
|
||||
ret
|
||||
|
||||
.agahnimForm:
|
||||
ld a, [hl]
|
||||
; only damage in states 2 to 4
|
||||
cp $02
|
||||
ret c
|
||||
cp $04
|
||||
ret nc
|
||||
|
||||
; Decrease health
|
||||
ld a, [$D220]
|
||||
inc a
|
||||
ld [$D220], a
|
||||
; If dead, do stuff
|
||||
cp $04
|
||||
jr c, .agahnimNotDeadYet
|
||||
ld [hl], $07
|
||||
ld hl, $C2E0
|
||||
add hl, bc
|
||||
ld [hl], $C0
|
||||
ld a, $36
|
||||
ldh [$F2], a
|
||||
.agahnimNotDeadYet:
|
||||
ld hl, $C420
|
||||
add hl, bc
|
||||
ld [hl], $14
|
||||
ld a, $07
|
||||
ldh [$F3], a
|
||||
ret
|
|
@ -0,0 +1,993 @@
|
|||
RenderChestItem:
|
||||
ldh a, [$F1] ; active sprite
|
||||
and $80
|
||||
jr nz, .renderLargeItem
|
||||
|
||||
ld de, ItemSpriteTable
|
||||
call $3C77 ; RenderActiveEntitySprite
|
||||
ret
|
||||
.renderLargeItem:
|
||||
ld de, LargeItemSpriteTable
|
||||
dec d
|
||||
dec d
|
||||
call $3BC0 ; RenderActiveEntitySpritePair
|
||||
|
||||
; If we are an instrument
|
||||
ldh a, [$F1]
|
||||
cp $8E
|
||||
ret c
|
||||
cp $96
|
||||
ret nc
|
||||
|
||||
; But check if we are not state >3 before that, else the fade-out at the instrument room breaks.
|
||||
ldh a, [$F0] ; hActiveEntityState
|
||||
cp $03
|
||||
ret nc
|
||||
|
||||
; Call the color cycling code
|
||||
xor a
|
||||
ld [$DC82], a
|
||||
ld [$DC83], a
|
||||
ld a, $3e
|
||||
call $0AD2
|
||||
ret
|
||||
|
||||
GiveItemFromChestMultiworld:
|
||||
call IncreaseCheckCounter
|
||||
; Check our "item is for other player" flag
|
||||
ld hl, $7300
|
||||
call OffsetPointerByRoomNumber
|
||||
ld a, [hl]
|
||||
ld hl, $0055
|
||||
cp [hl]
|
||||
ret nz
|
||||
|
||||
GiveItemFromChest:
|
||||
ldh a, [$F1] ; Load active sprite variant
|
||||
|
||||
rst 0 ; JUMP TABLE
|
||||
dw ChestPowerBracelet; CHEST_POWER_BRACELET
|
||||
dw ChestShield ; CHEST_SHIELD
|
||||
dw ChestBow ; CHEST_BOW
|
||||
dw ChestWithItem ; CHEST_HOOKSHOT
|
||||
dw ChestWithItem ; CHEST_MAGIC_ROD
|
||||
dw ChestWithItem ; CHEST_PEGASUS_BOOTS
|
||||
dw ChestWithItem ; CHEST_OCARINA
|
||||
dw ChestWithItem ; CHEST_FEATHER
|
||||
dw ChestWithItem ; CHEST_SHOVEL
|
||||
dw ChestMagicPowder ; CHEST_MAGIC_POWDER_BAG
|
||||
dw ChestBomb ; CHEST_BOMB
|
||||
dw ChestSword ; CHEST_SWORD
|
||||
dw Flippers ; CHEST_FLIPPERS
|
||||
dw NoItem ; CHEST_MAGNIFYING_LENS
|
||||
dw ChestWithItem ; Boomerang (used to be unused)
|
||||
dw SlimeKey ; ?? right side of your trade quest item
|
||||
dw Medicine ; CHEST_MEDICINE
|
||||
dw TailKey ; CHEST_TAIL_KEY
|
||||
dw AnglerKey ; CHEST_ANGLER_KEY
|
||||
dw FaceKey ; CHEST_FACE_KEY
|
||||
dw BirdKey ; CHEST_BIRD_KEY
|
||||
dw GoldenLeaf ; CHEST_GOLD_LEAF
|
||||
dw ChestWithCurrentDungeonItem ; CHEST_MAP
|
||||
dw ChestWithCurrentDungeonItem ; CHEST_COMPASS
|
||||
dw ChestWithCurrentDungeonItem ; CHEST_STONE_BEAK
|
||||
dw ChestWithCurrentDungeonItem ; CHEST_NIGHTMARE_KEY
|
||||
dw ChestWithCurrentDungeonItem ; CHEST_SMALL_KEY
|
||||
dw AddRupees50 ; CHEST_RUPEES_50
|
||||
dw AddRupees20 ; CHEST_RUPEES_20
|
||||
dw AddRupees100 ; CHEST_RUPEES_100
|
||||
dw AddRupees200 ; CHEST_RUPEES_200
|
||||
dw AddRupees500 ; CHEST_RUPEES_500
|
||||
dw AddSeashell ; CHEST_SEASHELL
|
||||
dw NoItem ; CHEST_MESSAGE
|
||||
dw NoItem ; CHEST_GEL
|
||||
dw AddKey ; KEY1
|
||||
dw AddKey ; KEY2
|
||||
dw AddKey ; KEY3
|
||||
dw AddKey ; KEY4
|
||||
dw AddKey ; KEY5
|
||||
dw AddKey ; KEY6
|
||||
dw AddKey ; KEY7
|
||||
dw AddKey ; KEY8
|
||||
dw AddKey ; KEY9
|
||||
dw AddMap ; MAP1
|
||||
dw AddMap ; MAP2
|
||||
dw AddMap ; MAP3
|
||||
dw AddMap ; MAP4
|
||||
dw AddMap ; MAP5
|
||||
dw AddMap ; MAP6
|
||||
dw AddMap ; MAP7
|
||||
dw AddMap ; MAP8
|
||||
dw AddMap ; MAP9
|
||||
dw AddCompass ; COMPASS1
|
||||
dw AddCompass ; COMPASS2
|
||||
dw AddCompass ; COMPASS3
|
||||
dw AddCompass ; COMPASS4
|
||||
dw AddCompass ; COMPASS5
|
||||
dw AddCompass ; COMPASS6
|
||||
dw AddCompass ; COMPASS7
|
||||
dw AddCompass ; COMPASS8
|
||||
dw AddCompass ; COMPASS9
|
||||
dw AddStoneBeak ; STONE_BEAK1
|
||||
dw AddStoneBeak ; STONE_BEAK2
|
||||
dw AddStoneBeak ; STONE_BEAK3
|
||||
dw AddStoneBeak ; STONE_BEAK4
|
||||
dw AddStoneBeak ; STONE_BEAK5
|
||||
dw AddStoneBeak ; STONE_BEAK6
|
||||
dw AddStoneBeak ; STONE_BEAK7
|
||||
dw AddStoneBeak ; STONE_BEAK8
|
||||
dw AddStoneBeak ; STONE_BEAK9
|
||||
dw AddNightmareKey ; NIGHTMARE_KEY1
|
||||
dw AddNightmareKey ; NIGHTMARE_KEY2
|
||||
dw AddNightmareKey ; NIGHTMARE_KEY3
|
||||
dw AddNightmareKey ; NIGHTMARE_KEY4
|
||||
dw AddNightmareKey ; NIGHTMARE_KEY5
|
||||
dw AddNightmareKey ; NIGHTMARE_KEY6
|
||||
dw AddNightmareKey ; NIGHTMARE_KEY7
|
||||
dw AddNightmareKey ; NIGHTMARE_KEY8
|
||||
dw AddNightmareKey ; NIGHTMARE_KEY9
|
||||
dw AddToadstool ; Toadstool
|
||||
dw NoItem ; $51
|
||||
dw NoItem ; $52
|
||||
dw NoItem ; $53
|
||||
dw NoItem ; $54
|
||||
dw NoItem ; $55
|
||||
dw NoItem ; $56
|
||||
dw NoItem ; $57
|
||||
dw NoItem ; $58
|
||||
dw NoItem ; $59
|
||||
dw NoItem ; $5A
|
||||
dw NoItem ; $5B
|
||||
dw NoItem ; $5C
|
||||
dw NoItem ; $5D
|
||||
dw NoItem ; $5E
|
||||
dw NoItem ; $5F
|
||||
dw NoItem ; $60
|
||||
dw NoItem ; $61
|
||||
dw NoItem ; $62
|
||||
dw NoItem ; $63
|
||||
dw NoItem ; $64
|
||||
dw NoItem ; $65
|
||||
dw NoItem ; $66
|
||||
dw NoItem ; $67
|
||||
dw NoItem ; $68
|
||||
dw NoItem ; $69
|
||||
dw NoItem ; $6A
|
||||
dw NoItem ; $6B
|
||||
dw NoItem ; $6C
|
||||
dw NoItem ; $6D
|
||||
dw NoItem ; $6E
|
||||
dw NoItem ; $6F
|
||||
dw NoItem ; $70
|
||||
dw NoItem ; $71
|
||||
dw NoItem ; $72
|
||||
dw NoItem ; $73
|
||||
dw NoItem ; $74
|
||||
dw NoItem ; $75
|
||||
dw NoItem ; $76
|
||||
dw NoItem ; $77
|
||||
dw NoItem ; $78
|
||||
dw NoItem ; $79
|
||||
dw NoItem ; $7A
|
||||
dw NoItem ; $7B
|
||||
dw NoItem ; $7C
|
||||
dw NoItem ; $7D
|
||||
dw NoItem ; $7E
|
||||
dw NoItem ; $7F
|
||||
dw PieceOfHeart ; Heart piece
|
||||
dw GiveBowwow
|
||||
dw Give10Arrows
|
||||
dw Give1Arrow
|
||||
dw UpgradeMaxPowder
|
||||
dw UpgradeMaxBombs
|
||||
dw UpgradeMaxArrows
|
||||
dw GiveRedTunic
|
||||
dw GiveBlueTunic
|
||||
dw GiveExtraHeart
|
||||
dw TakeHeart
|
||||
dw GiveSong1
|
||||
dw GiveSong2
|
||||
dw GiveSong3
|
||||
dw GiveInstrument
|
||||
dw GiveInstrument
|
||||
dw GiveInstrument
|
||||
dw GiveInstrument
|
||||
dw GiveInstrument
|
||||
dw GiveInstrument
|
||||
dw GiveInstrument
|
||||
dw GiveInstrument
|
||||
dw GiveRooster
|
||||
dw GiveTradeItem1
|
||||
dw GiveTradeItem2
|
||||
dw GiveTradeItem3
|
||||
dw GiveTradeItem4
|
||||
dw GiveTradeItem5
|
||||
dw GiveTradeItem6
|
||||
dw GiveTradeItem7
|
||||
dw GiveTradeItem8
|
||||
dw GiveTradeItem9
|
||||
dw GiveTradeItem10
|
||||
dw GiveTradeItem11
|
||||
dw GiveTradeItem12
|
||||
dw GiveTradeItem13
|
||||
dw GiveTradeItem14
|
||||
|
||||
NoItem:
|
||||
ret
|
||||
|
||||
ChestPowerBracelet:
|
||||
ld hl, $DB43 ; power bracelet level
|
||||
jr ChestIncreaseItemLevel
|
||||
|
||||
ChestShield:
|
||||
ld hl, $DB44 ; shield level
|
||||
jr ChestIncreaseItemLevel
|
||||
|
||||
ChestSword:
|
||||
ld hl, $DB4E ; sword level
|
||||
jr ChestIncreaseItemLevel
|
||||
|
||||
ChestIncreaseItemLevel:
|
||||
ld a, [hl]
|
||||
cp $02
|
||||
jr z, DoNotIncreaseItemLevel
|
||||
inc [hl]
|
||||
DoNotIncreaseItemLevel:
|
||||
jp ChestWithItem
|
||||
|
||||
ChestBomb:
|
||||
ld a, [$DB4D] ; bomb count
|
||||
add a, $10
|
||||
daa
|
||||
ld hl, $DB77 ; max bombs
|
||||
cp [hl]
|
||||
jr c, .bombsNotFull
|
||||
ld a, [hl]
|
||||
.bombsNotFull:
|
||||
ld [$DB4D], a
|
||||
jp ChestWithItem
|
||||
|
||||
ChestBow:
|
||||
ld a, [$DB45]
|
||||
cp $20
|
||||
jp nc, ChestWithItem
|
||||
ld a, $20
|
||||
ld [$DB45], a
|
||||
jp ChestWithItem
|
||||
|
||||
ChestMagicPowder:
|
||||
; Reset the toadstool state
|
||||
ld a, $0B
|
||||
ldh [$A5], a
|
||||
xor a
|
||||
ld [$DB4B], a ; has toadstool
|
||||
|
||||
ld a, [$DB4C] ; powder count
|
||||
add a, $10
|
||||
daa
|
||||
ld hl, $DB76 ; max powder
|
||||
cp [hl]
|
||||
jr c, .magicPowderNotFull
|
||||
ld a, [hl]
|
||||
.magicPowderNotFull:
|
||||
ld [$DB4C], a
|
||||
jp ChestWithItem
|
||||
|
||||
|
||||
Flippers:
|
||||
ld a, $01
|
||||
ld [wHasFlippers], a
|
||||
ret
|
||||
|
||||
Medicine:
|
||||
ld a, $01
|
||||
ld [wHasMedicine], a
|
||||
ret
|
||||
|
||||
TailKey:
|
||||
ld a, $01
|
||||
ld [$DB11], a
|
||||
ret
|
||||
|
||||
AnglerKey:
|
||||
ld a, $01
|
||||
ld [$DB12], a
|
||||
ret
|
||||
|
||||
FaceKey:
|
||||
ld a, $01
|
||||
ld [$DB13], a
|
||||
ret
|
||||
|
||||
BirdKey:
|
||||
ld a, $01
|
||||
ld [$DB14], a
|
||||
ret
|
||||
|
||||
SlimeKey:
|
||||
ld a, $01
|
||||
ld [$DB15], a
|
||||
ret
|
||||
|
||||
GoldenLeaf:
|
||||
ld hl, wGoldenLeaves
|
||||
inc [hl]
|
||||
ret
|
||||
|
||||
AddSeaShell:
|
||||
ld a, [wSeashellsCount]
|
||||
inc a
|
||||
daa
|
||||
ld [wSeashellsCount], a
|
||||
ret
|
||||
|
||||
PieceOfHeart:
|
||||
#IF HARD_MODE
|
||||
ld a, $FF
|
||||
ld [$DB93], a
|
||||
#ENDIF
|
||||
|
||||
ld a, [$DB5C]
|
||||
inc a
|
||||
cp $04
|
||||
jr z, .FullHeart
|
||||
ld [$DB5C], a
|
||||
ret
|
||||
.FullHeart:
|
||||
xor a
|
||||
ld [$DB5C], a
|
||||
jp GiveExtraHeart
|
||||
|
||||
GiveBowwow:
|
||||
ld a, $01
|
||||
ld [$DB56], a
|
||||
ret
|
||||
|
||||
ChestInventoryTable:
|
||||
db $03 ; CHEST_POWER_BRACELET
|
||||
db $04 ; CHEST_SHIELD
|
||||
db $05 ; CHEST_BOW
|
||||
db $06 ; CHEST_HOOKSHOT
|
||||
db $07 ; CHEST_MAGIC_ROD
|
||||
db $08 ; CHEST_PEGASUS_BOOTS
|
||||
db $09 ; CHEST_OCARINA
|
||||
db $0A ; CHEST_FEATHER
|
||||
db $0B ; CHEST_SHOVEL
|
||||
db $0C ; CHEST_MAGIC_POWDER_BAG
|
||||
db $02 ; CHEST_BOMB
|
||||
db $01 ; CHEST_SWORD
|
||||
db $00 ; - (flippers slot)
|
||||
db $00 ; - (magnifier lens slot)
|
||||
db $0D ; Boomerang
|
||||
|
||||
ChestWithItem:
|
||||
ldh a, [$F1] ; Load active sprite variant
|
||||
ld d, $00
|
||||
ld e, a
|
||||
ld hl, ChestInventoryTable
|
||||
add hl, de
|
||||
ld d, [hl]
|
||||
call $3E6B ; Give Inventory
|
||||
ret
|
||||
|
||||
ChestWithCurrentDungeonItem:
|
||||
sub $16 ; a -= CHEST_MAP
|
||||
ld e, a
|
||||
ld d, $00
|
||||
ld hl, $DBCC ; hasDungeonMap
|
||||
add hl, de
|
||||
inc [hl]
|
||||
call $2802 ; Sync current dungeon items with dungeon specific table
|
||||
ret
|
||||
|
||||
AddToadstool:
|
||||
ld d, $0E
|
||||
call $3E6B ; Give Inventory
|
||||
ret
|
||||
|
||||
AddKey:
|
||||
sub $23 ; Make 'A' target dungeon index
|
||||
ld de, $0004
|
||||
jr AddDungeonItem
|
||||
|
||||
AddMap:
|
||||
sub $2C ; Make 'A' target dungeon index
|
||||
ld de, $0000
|
||||
jr AddDungeonItem
|
||||
|
||||
AddCompass:
|
||||
sub $35 ; Make 'A' target dungeon index
|
||||
ld de, $0001
|
||||
jr AddDungeonItem
|
||||
|
||||
AddStoneBeak:
|
||||
sub $3E ; Make 'A' target dungeon index
|
||||
ld de, $0002
|
||||
jr AddDungeonItem
|
||||
|
||||
AddNightmareKey:
|
||||
sub $47 ; Make 'A' target dungeon index
|
||||
ld de, $0003
|
||||
jr AddDungeonItem
|
||||
|
||||
AddDungeonItem:
|
||||
cp $08
|
||||
jr z, .colorDungeon
|
||||
; hl = dungeonitems + type_type + dungeon * 8
|
||||
ld hl, $DB16
|
||||
add hl, de
|
||||
push de
|
||||
ld e, a
|
||||
add hl, de
|
||||
add hl, de
|
||||
add hl, de
|
||||
add hl, de
|
||||
add hl, de
|
||||
pop de
|
||||
inc [hl]
|
||||
; Check if we are in this specific dungeon, and then increase the copied counters as well.
|
||||
ld hl, $FFF7 ; is current map == target map
|
||||
cp [hl]
|
||||
ret nz
|
||||
ld a, [$DBA5] ; is indoor
|
||||
and a
|
||||
ret z
|
||||
|
||||
ld hl, $DBCC
|
||||
add hl, de
|
||||
inc [hl]
|
||||
ret
|
||||
.colorDungeon:
|
||||
; Special case for the color dungeon, which is in a different location in memory.
|
||||
ld hl, $DDDA
|
||||
add hl, de
|
||||
inc [hl]
|
||||
ldh a, [$F7] ; is current map == color dungeon
|
||||
cp $ff
|
||||
ret nz
|
||||
ld hl, $DBCC
|
||||
add hl, de
|
||||
inc [hl]
|
||||
ret
|
||||
|
||||
AddRupees20:
|
||||
xor a
|
||||
ld h, $14
|
||||
jr AddRupees
|
||||
|
||||
AddRupees50:
|
||||
xor a
|
||||
ld h, $32
|
||||
jr AddRupees
|
||||
|
||||
AddRupees100:
|
||||
xor a
|
||||
ld h, $64
|
||||
jr AddRupees
|
||||
|
||||
AddRupees200:
|
||||
xor a
|
||||
ld h, $C8
|
||||
jr AddRupees
|
||||
|
||||
AddRupees500:
|
||||
ld a, $01
|
||||
ld h, $F4
|
||||
jr AddRupees
|
||||
|
||||
AddRupees:
|
||||
ld [$DB8F], a
|
||||
ld a, h
|
||||
ld [$DB90], a
|
||||
ld a, $18
|
||||
ld [$C3CE], a
|
||||
ret
|
||||
|
||||
Give1Arrow:
|
||||
ld a, [$DB45]
|
||||
inc a
|
||||
jp FinishGivingArrows
|
||||
|
||||
Give10Arrows:
|
||||
ld a, [$DB45]
|
||||
add a, $0A
|
||||
FinishGivingArrows:
|
||||
daa
|
||||
ld [$DB45], a
|
||||
ld hl, $DB78
|
||||
cp [hl]
|
||||
ret c
|
||||
ld a, [hl]
|
||||
ld [$DB45], a
|
||||
ret
|
||||
|
||||
UpgradeMaxPowder:
|
||||
ld a, $40
|
||||
ld [$DB76], a
|
||||
; If we have no powder, we should not increase the current amount, as that would prevent
|
||||
; The toadstool from showing up.
|
||||
ld a, [$DB4C]
|
||||
and a
|
||||
ret z
|
||||
ld a, $40
|
||||
ld [$DB4C], a
|
||||
ret
|
||||
|
||||
UpgradeMaxBombs:
|
||||
ld a, $60
|
||||
ld [$DB77], a
|
||||
ld [$DB4D], a
|
||||
ret
|
||||
|
||||
UpgradeMaxArrows:
|
||||
ld a, $60
|
||||
ld [$DB78], a
|
||||
ld [$DB45], a
|
||||
ret
|
||||
|
||||
GiveRedTunic:
|
||||
ld a, $01
|
||||
ld [$DC0F], a
|
||||
; We use DB6D to store which tunics we have available.
|
||||
ld a, [wCollectedTunics]
|
||||
or $01
|
||||
ld [wCollectedTunics], a
|
||||
ret
|
||||
|
||||
GiveBlueTunic:
|
||||
ld a, $02
|
||||
ld [$DC0F], a
|
||||
; We use DB6D to store which tunics we have available.
|
||||
ld a, [wCollectedTunics]
|
||||
or $02
|
||||
ld [wCollectedTunics], a
|
||||
ret
|
||||
|
||||
GiveExtraHeart:
|
||||
; Regen all health
|
||||
ld hl, $DB93
|
||||
ld [hl], $FF
|
||||
; Increase max health if health is lower then 14 hearts
|
||||
ld hl, $DB5B
|
||||
ld a, $0E
|
||||
cp [hl]
|
||||
ret z
|
||||
inc [hl]
|
||||
ret
|
||||
|
||||
TakeHeart:
|
||||
; First, reduce the max HP
|
||||
ld hl, $DB5B
|
||||
ld a, [hl]
|
||||
cp $01
|
||||
ret z
|
||||
dec a
|
||||
ld [$DB5B], a
|
||||
|
||||
; Next, check if we need to reduce our actual HP to keep it below the maximum.
|
||||
rlca
|
||||
rlca
|
||||
rlca
|
||||
sub $01
|
||||
ld hl, $DB5A
|
||||
cp [hl]
|
||||
jr nc, .noNeedToReduceHp
|
||||
ld [hl], a
|
||||
.noNeedToReduceHp:
|
||||
; Finally, give all health back.
|
||||
ld hl, $DB93
|
||||
ld [hl], $FF
|
||||
ret
|
||||
|
||||
GiveSong1:
|
||||
ld hl, $DB49
|
||||
set 2, [hl]
|
||||
ld a, $00
|
||||
ld [$DB4A], a
|
||||
ret
|
||||
|
||||
GiveSong2:
|
||||
ld hl, $DB49
|
||||
set 1, [hl]
|
||||
ld a, $01
|
||||
ld [$DB4A], a
|
||||
ret
|
||||
|
||||
GiveSong3:
|
||||
ld hl, $DB49
|
||||
set 0, [hl]
|
||||
ld a, $02
|
||||
ld [$DB4A], a
|
||||
ret
|
||||
|
||||
GiveInstrument:
|
||||
ldh a, [$F1] ; Load active sprite variant
|
||||
sub $8E
|
||||
ld d, $00
|
||||
ld e, a
|
||||
ld hl, $db65 ; has instrument table
|
||||
add hl, de
|
||||
set 1, [hl]
|
||||
ret
|
||||
|
||||
GiveRooster:
|
||||
ld d, $0F
|
||||
call $3E6B ; Give Inventory (rooster item)
|
||||
|
||||
;ld a, $01
|
||||
;ld [$DB7B], a ; has rooster
|
||||
ldh a, [$F9] ; do not spawn rooster in sidescroller
|
||||
and a
|
||||
ret z
|
||||
|
||||
ld a, $D5 ; ENTITY_ROOSTER
|
||||
call $3B86 ; SpawnNewEntity_trampoline
|
||||
ldh a, [$98] ; LinkX
|
||||
ld hl, $C200 ; wEntitiesPosXTable
|
||||
add hl, de
|
||||
ld [hl], a
|
||||
ldh a, [$99] ; LinkY
|
||||
ld hl, $C210 ; wEntitiesPosYTable
|
||||
add hl, de
|
||||
ld [hl], a
|
||||
|
||||
ret
|
||||
|
||||
GiveTradeItem1:
|
||||
ld hl, wTradeSequenceItem
|
||||
set 0, [hl]
|
||||
ret
|
||||
GiveTradeItem2:
|
||||
ld hl, wTradeSequenceItem
|
||||
set 1, [hl]
|
||||
ret
|
||||
GiveTradeItem3:
|
||||
ld hl, wTradeSequenceItem
|
||||
set 2, [hl]
|
||||
ret
|
||||
GiveTradeItem4:
|
||||
ld hl, wTradeSequenceItem
|
||||
set 3, [hl]
|
||||
ret
|
||||
GiveTradeItem5:
|
||||
ld hl, wTradeSequenceItem
|
||||
set 4, [hl]
|
||||
ret
|
||||
GiveTradeItem6:
|
||||
ld hl, wTradeSequenceItem
|
||||
set 5, [hl]
|
||||
ret
|
||||
GiveTradeItem7:
|
||||
ld hl, wTradeSequenceItem
|
||||
set 6, [hl]
|
||||
ret
|
||||
GiveTradeItem8:
|
||||
ld hl, wTradeSequenceItem
|
||||
set 7, [hl]
|
||||
ret
|
||||
GiveTradeItem9:
|
||||
ld hl, wTradeSequenceItem2
|
||||
set 0, [hl]
|
||||
ret
|
||||
GiveTradeItem10:
|
||||
ld hl, wTradeSequenceItem2
|
||||
set 1, [hl]
|
||||
ret
|
||||
GiveTradeItem11:
|
||||
ld hl, wTradeSequenceItem2
|
||||
set 2, [hl]
|
||||
ret
|
||||
GiveTradeItem12:
|
||||
ld hl, wTradeSequenceItem2
|
||||
set 3, [hl]
|
||||
ret
|
||||
GiveTradeItem13:
|
||||
ld hl, wTradeSequenceItem2
|
||||
set 4, [hl]
|
||||
ret
|
||||
GiveTradeItem14:
|
||||
ld hl, wTradeSequenceItem2
|
||||
set 5, [hl]
|
||||
ret
|
||||
|
||||
ItemMessageMultiworld:
|
||||
; Check our "item is for other player" flag
|
||||
ld hl, $7300
|
||||
call OffsetPointerByRoomNumber
|
||||
ld a, [hl]
|
||||
ld hl, $0055
|
||||
cp [hl]
|
||||
jr nz, ItemMessageForOtherPlayer
|
||||
|
||||
ItemMessage:
|
||||
; Fill the custom message slot with this item message.
|
||||
call BuildItemMessage
|
||||
ldh a, [$F1]
|
||||
ld d, $00
|
||||
ld e, a
|
||||
ld hl, ItemMessageTable
|
||||
add hl, de
|
||||
ld a, [hl]
|
||||
cp $90
|
||||
jr z, .powerBracelet
|
||||
cp $3D
|
||||
jr z, .shield
|
||||
jp $2385 ; Opendialog in $000-$0FF range
|
||||
|
||||
.powerBracelet:
|
||||
; Check the power bracelet level, and give a different message when we get the lv2 bracelet
|
||||
ld hl, $DB43 ; power bracelet level
|
||||
bit 1, [hl]
|
||||
jp z, $2385 ; Opendialog in $000-$0FF range
|
||||
ld a, $EE
|
||||
jp $2385 ; Opendialog in $000-$0FF range
|
||||
|
||||
.shield:
|
||||
; Check the shield level, and give a different message when we get the lv2 shield
|
||||
ld hl, $DB44 ; shield level
|
||||
bit 1, [hl]
|
||||
jp z, $2385 ; Opendialog in $000-$0FF range
|
||||
ld a, $ED
|
||||
jp $2385 ; Opendialog in $000-$0FF range
|
||||
|
||||
ItemMessageForOtherPlayer:
|
||||
push bc
|
||||
push hl
|
||||
push af
|
||||
call BuildRemoteItemMessage
|
||||
ld hl, SpaceFor
|
||||
call MessageCopyString
|
||||
pop af
|
||||
call MessageAddPlayerName
|
||||
pop hl
|
||||
pop bc
|
||||
;dec de
|
||||
ld a, $C9
|
||||
jp $2385 ; Opendialog in $000-$0FF range
|
||||
|
||||
ItemSpriteTable:
|
||||
db $82, $15 ; CHEST_POWER_BRACELET
|
||||
db $86, $15 ; CHEST_SHIELD
|
||||
db $88, $14 ; CHEST_BOW
|
||||
db $8A, $14 ; CHEST_HOOKSHOT
|
||||
db $8C, $14 ; CHEST_MAGIC_ROD
|
||||
db $98, $16 ; CHEST_PEGASUS_BOOTS
|
||||
db $10, $1F ; CHEST_OCARINA
|
||||
db $12, $1D ; CHEST_FEATHER
|
||||
db $96, $17 ; CHEST_SHOVEL
|
||||
db $0E, $1C ; CHEST_MAGIC_POWDER_BAG
|
||||
db $80, $15 ; CHEST_BOMB
|
||||
db $84, $15 ; CHEST_SWORD
|
||||
db $94, $15 ; CHEST_FLIPPERS
|
||||
db $9A, $10 ; CHEST_MAGNIFYING_LENS
|
||||
db $24, $1C ; Boomerang
|
||||
db $4E, $1C ; Slime key
|
||||
db $A0, $14 ; CHEST_MEDICINE
|
||||
db $30, $1C ; CHEST_TAIL_KEY
|
||||
db $32, $1C ; CHEST_ANGLER_KEY
|
||||
db $34, $1C ; CHEST_FACE_KEY
|
||||
db $36, $1C ; CHEST_BIRD_KEY
|
||||
db $3A, $1C ; CHEST_GOLD_LEAF
|
||||
db $40, $1C ; CHEST_MAP
|
||||
db $42, $1D ; CHEST_COMPASS
|
||||
db $44, $1C ; CHEST_STONE_BEAK
|
||||
db $46, $1C ; CHEST_NIGHTMARE_KEY
|
||||
db $4A, $1F ; CHEST_SMALL_KEY
|
||||
db $A6, $15 ; CHEST_RUPEES_50 (normal blue)
|
||||
db $38, $19 ; CHEST_RUPEES_20 (red)
|
||||
db $38, $18 ; CHEST_RUPEES_100 (green)
|
||||
db $38, $1A ; CHEST_RUPEES_200 (yellow)
|
||||
db $38, $1A ; CHEST_RUPEES_500 (yellow)
|
||||
db $9E, $14 ; CHEST_SEASHELL
|
||||
db $8A, $14 ; CHEST_MESSAGE
|
||||
db $A0, $14 ; CHEST_GEL
|
||||
db $4A, $1D ; KEY1
|
||||
db $4A, $1D ; KEY2
|
||||
db $4A, $1D ; KEY3
|
||||
db $4A, $1D ; KEY4
|
||||
db $4A, $1D ; KEY5
|
||||
db $4A, $1D ; KEY6
|
||||
db $4A, $1D ; KEY7
|
||||
db $4A, $1D ; KEY8
|
||||
db $4A, $1D ; KEY9
|
||||
db $40, $1C ; MAP1
|
||||
db $40, $1C ; MAP2
|
||||
db $40, $1C ; MAP3
|
||||
db $40, $1C ; MAP4
|
||||
db $40, $1C ; MAP5
|
||||
db $40, $1C ; MAP6
|
||||
db $40, $1C ; MAP7
|
||||
db $40, $1C ; MAP8
|
||||
db $40, $1C ; MAP9
|
||||
db $42, $1D ; COMPASS1
|
||||
db $42, $1D ; COMPASS2
|
||||
db $42, $1D ; COMPASS3
|
||||
db $42, $1D ; COMPASS4
|
||||
db $42, $1D ; COMPASS5
|
||||
db $42, $1D ; COMPASS6
|
||||
db $42, $1D ; COMPASS7
|
||||
db $42, $1D ; COMPASS8
|
||||
db $42, $1D ; COMPASS9
|
||||
db $44, $1C ; STONE_BEAK1
|
||||
db $44, $1C ; STONE_BEAK2
|
||||
db $44, $1C ; STONE_BEAK3
|
||||
db $44, $1C ; STONE_BEAK4
|
||||
db $44, $1C ; STONE_BEAK5
|
||||
db $44, $1C ; STONE_BEAK6
|
||||
db $44, $1C ; STONE_BEAK7
|
||||
db $44, $1C ; STONE_BEAK8
|
||||
db $44, $1C ; STONE_BEAK9
|
||||
db $46, $1C ; NIGHTMARE_KEY1
|
||||
db $46, $1C ; NIGHTMARE_KEY2
|
||||
db $46, $1C ; NIGHTMARE_KEY3
|
||||
db $46, $1C ; NIGHTMARE_KEY4
|
||||
db $46, $1C ; NIGHTMARE_KEY5
|
||||
db $46, $1C ; NIGHTMARE_KEY6
|
||||
db $46, $1C ; NIGHTMARE_KEY7
|
||||
db $46, $1C ; NIGHTMARE_KEY8
|
||||
db $46, $1C ; NIGHTMARE_KEY9
|
||||
db $4C, $1C ; Toadstool
|
||||
|
||||
LargeItemSpriteTable:
|
||||
db $AC, $02, $AC, $22 ; heart piece
|
||||
db $54, $0A, $56, $0A ; bowwow
|
||||
db $2A, $41, $2A, $61 ; 10 arrows
|
||||
db $2A, $41, $2A, $61 ; single arrow
|
||||
db $0E, $1C, $22, $0C ; powder upgrade
|
||||
db $00, $0D, $22, $0C ; bomb upgrade
|
||||
db $08, $1C, $22, $0C ; arrow upgrade
|
||||
db $48, $0A, $48, $2A ; red tunic
|
||||
db $48, $0B, $48, $2B ; blue tunic
|
||||
db $2A, $0C, $2A, $2C ; heart container
|
||||
db $2A, $0F, $2A, $2F ; bad heart container
|
||||
db $70, $09, $70, $29 ; song 1
|
||||
db $72, $0B, $72, $2B ; song 2
|
||||
db $74, $08, $74, $28 ; song 3
|
||||
db $80, $0E, $82, $0E ; Instrument1
|
||||
db $84, $0E, $86, $0E ; Instrument2
|
||||
db $88, $0E, $8A, $0E ; Instrument3
|
||||
db $8C, $0E, $8E, $0E ; Instrument4
|
||||
db $90, $0E, $92, $0E ; Instrument5
|
||||
db $94, $0E, $96, $0E ; Instrument6
|
||||
db $98, $0E, $9A, $0E ; Instrument7
|
||||
db $9C, $0E, $9E, $0E ; Instrument8
|
||||
db $A6, $2B, $A4, $2B ; Rooster
|
||||
db $1A, $0E, $1C, $0E ; TradeItem1
|
||||
db $B0, $0C, $B2, $0C ; TradeItem2
|
||||
db $B4, $0C, $B6, $0C ; TradeItem3
|
||||
db $B8, $0C, $BA, $0C ; TradeItem4
|
||||
db $BC, $0C, $BE, $0C ; TradeItem5
|
||||
db $C0, $0C, $C2, $0C ; TradeItem6
|
||||
db $C4, $0C, $C6, $0C ; TradeItem7
|
||||
db $C8, $0C, $CA, $0C ; TradeItem8
|
||||
db $CC, $0C, $CE, $0C ; TradeItem9
|
||||
db $D0, $0C, $D2, $0C ; TradeItem10
|
||||
db $D4, $0D, $D6, $0D ; TradeItem11
|
||||
db $D8, $0D, $DA, $0D ; TradeItem12
|
||||
db $DC, $0D, $DE, $0D ; TradeItem13
|
||||
db $E0, $0D, $E2, $0D ; TradeItem14
|
||||
|
||||
ItemMessageTable:
|
||||
db $90, $3D, $89, $93, $94, $95, $96, $97, $98, $99, $9A, $9B, $9C, $9D, $D9, $A2
|
||||
db $A0, $A1, $A3, $A4, $A5, $E8, $A6, $A7, $A8, $A9, $AA, $AC, $AB, $AD, $AE, $C9
|
||||
db $EF, $BE, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9
|
||||
db $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9
|
||||
; $40
|
||||
db $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9
|
||||
db $0F, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00
|
||||
db $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00
|
||||
db $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00
|
||||
; $80
|
||||
db $4F, $C8, $CA, $CB, $E2, $E3, $E4, $CC, $CD, $2A, $2B, $C9, $C9, $C9, $C9, $C9
|
||||
db $C9, $C9, $C9, $C9, $C9, $C9, $B8, $44, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9
|
||||
db $C9, $C9, $C9, $C9, $9D
|
||||
|
||||
RenderDroppedKey:
|
||||
;TODO: See EntityInitKeyDropPoint for a few special cases to unload.
|
||||
|
||||
RenderHeartPiece:
|
||||
; Check if our chest type is already loaded
|
||||
ld hl, $C2C0
|
||||
add hl, bc
|
||||
ld a, [hl]
|
||||
and a
|
||||
jr nz, .droppedKeyTypeLoaded
|
||||
inc [hl]
|
||||
|
||||
;Load the chest type from the chest table.
|
||||
ld hl, $7800
|
||||
call OffsetPointerByRoomNumber
|
||||
|
||||
ld a, [hl]
|
||||
ldh [$F1], a ; set currentEntitySpriteVariant
|
||||
call $3B0C ; SetEntitySpriteVariant
|
||||
|
||||
and $80
|
||||
ld hl, $C340
|
||||
add hl, bc
|
||||
ld a, [hl]
|
||||
jr z, .singleSprite
|
||||
; We potentially need to fix the physics flags table to allocate 2 sprites for us
|
||||
and $F8
|
||||
or $02
|
||||
ld [hl], a
|
||||
jr .droppedKeyTypeLoaded
|
||||
.singleSprite:
|
||||
and $F8
|
||||
or $01
|
||||
ld [hl], a
|
||||
.droppedKeyTypeLoaded:
|
||||
jp RenderChestItem
|
||||
|
||||
|
||||
OffsetPointerByRoomNumber:
|
||||
ldh a, [$F6] ; map room
|
||||
ld e, a
|
||||
ld a, [$DBA5] ; is indoor
|
||||
ld d, a
|
||||
ldh a, [$F7] ; mapId
|
||||
cp $FF
|
||||
jr nz, .notColorDungeon
|
||||
|
||||
ld d, $03
|
||||
jr .notCavesA
|
||||
|
||||
.notColorDungeon:
|
||||
cp $1A
|
||||
jr nc, .notCavesA
|
||||
cp $06
|
||||
jr c, .notCavesA
|
||||
inc d
|
||||
.notCavesA:
|
||||
add hl, de
|
||||
ret
|
||||
|
||||
GiveItemAndMessageForRoom:
|
||||
;Load the chest type from the chest table.
|
||||
ld hl, $7800
|
||||
call OffsetPointerByRoomNumber
|
||||
ld a, [hl]
|
||||
ldh [$F1], a
|
||||
call GiveItemFromChest
|
||||
jp ItemMessage
|
||||
|
||||
GiveItemAndMessageForRoomMultiworld:
|
||||
;Load the chest type from the chest table.
|
||||
ld hl, $7800
|
||||
call OffsetPointerByRoomNumber
|
||||
ld a, [hl]
|
||||
ldh [$F1], a
|
||||
call GiveItemFromChestMultiworld
|
||||
jp ItemMessageMultiworld
|
||||
|
||||
RenderItemForRoom:
|
||||
;Load the chest type from the chest table.
|
||||
ld hl, $7800
|
||||
call OffsetPointerByRoomNumber
|
||||
ld a, [hl]
|
||||
ldh [$F1], a
|
||||
jp RenderChestItem
|
||||
|
||||
; Increase the amount of checks we completed, unless we are on the multichest room.
|
||||
IncreaseCheckCounter:
|
||||
ldh a, [$F6] ; map room
|
||||
cp $F2
|
||||
jr nz, .noMultiChest
|
||||
ld a, [$DBA5] ; is indoor
|
||||
and a
|
||||
jr z, .noMultiChest
|
||||
ldh a, [$F7] ; mapId
|
||||
cp $0A
|
||||
ret z
|
||||
|
||||
.noMultiChest:
|
||||
call $27D0 ; Enable SRAM
|
||||
ld hl, $B010
|
||||
.loop:
|
||||
ld a, [hl]
|
||||
and a ; clear carry flag
|
||||
inc a
|
||||
daa
|
||||
ldi [hl], a
|
||||
ret nc
|
||||
jr .loop
|
|
@ -0,0 +1,494 @@
|
|||
|
||||
BuildRemoteItemMessage:
|
||||
ld de, wCustomMessage
|
||||
call CustomItemMessageThreeFour
|
||||
ld a, $A0 ; low of wCustomMessage
|
||||
cp e
|
||||
ret nz
|
||||
|
||||
BuildItemMessage:
|
||||
ld hl, ItemNamePointers
|
||||
ldh a, [$F1]
|
||||
ld d, $00
|
||||
ld e, a
|
||||
add hl, de
|
||||
add hl, de
|
||||
ldi a, [hl]
|
||||
ld h, [hl]
|
||||
ld l, a
|
||||
ld de, wCustomMessage
|
||||
jp MessageCopyString
|
||||
|
||||
; And then see if the custom item message func wants to override
|
||||
|
||||
; add hl, de
|
||||
|
||||
|
||||
CustomItemMessageThreeFour:
|
||||
; the stack _should_ have the address to return to here, so we can just pop it when we're done
|
||||
ld a, $34 ; Set bank number
|
||||
ld hl, $4000 ; Set next address
|
||||
push hl
|
||||
jp $080C ; switch bank
|
||||
|
||||
FoundItemForOtherPlayerPostfix:
|
||||
db m" for player X", $ff
|
||||
GotItemFromOtherPlayerPostfix:
|
||||
db m" from player X", $ff
|
||||
SpaceFrom:
|
||||
db " from ", $ff, $ff
|
||||
SpaceFor:
|
||||
db " for ", $ff, $ff
|
||||
MessagePad:
|
||||
jr .start ; goto start
|
||||
.loop:
|
||||
ld a, $20 ; a = ' '
|
||||
ld [de], a ; *de = ' '
|
||||
inc de ; de++
|
||||
ld a, $ff ; a = 0xFF
|
||||
ld [de], a ; *de = 0xff
|
||||
.start:
|
||||
ld a, e ; a = de & 0xF
|
||||
and $0F ; a &= 0x0xF
|
||||
jr nz, .loop ; if a != 0, goto loop
|
||||
ret
|
||||
|
||||
MessageAddTargetPlayer:
|
||||
call MessagePad
|
||||
ld hl, FoundItemForOtherPlayerPostfix
|
||||
call MessageCopyString
|
||||
ret
|
||||
|
||||
MessageAddFromPlayerOld:
|
||||
call MessagePad
|
||||
ld hl, GotItemFromOtherPlayerPostfix
|
||||
call MessageCopyString
|
||||
ret
|
||||
|
||||
; hahaha none of this follows calling conventions
|
||||
MessageAddPlayerName:
|
||||
; call MessagePad
|
||||
ld h, 0 ; bc = a, hl = a
|
||||
ld l, a
|
||||
ld b, 0
|
||||
ld c, a
|
||||
add hl, hl ; 2
|
||||
add hl, hl ; 4
|
||||
add hl, hl ; 8
|
||||
add hl, hl ; 16
|
||||
add hl, bc ; 17
|
||||
ld bc, MultiNamePointers
|
||||
add hl, bc ; hl = MultiNamePointers + wLinkGiveItemFrom * 17
|
||||
call MessageCopyString
|
||||
ret
|
||||
|
||||
ItemNamePointers:
|
||||
dw ItemNamePowerBracelet
|
||||
dw ItemNameShield
|
||||
dw ItemNameBow
|
||||
dw ItemNameHookshot
|
||||
dw ItemNameMagicRod
|
||||
dw ItemNamePegasusBoots
|
||||
dw ItemNameOcarina
|
||||
dw ItemNameFeather
|
||||
dw ItemNameShovel
|
||||
dw ItemNameMagicPowder
|
||||
dw ItemNameBomb
|
||||
dw ItemNameSword
|
||||
dw ItemNameFlippers
|
||||
dw ItemNameNone
|
||||
dw ItemNameBoomerang
|
||||
dw ItemNameSlimeKey
|
||||
dw ItemNameMedicine
|
||||
dw ItemNameTailKey
|
||||
dw ItemNameAnglerKey
|
||||
dw ItemNameFaceKey
|
||||
dw ItemNameBirdKey
|
||||
dw ItemNameGoldLeaf
|
||||
dw ItemNameMap
|
||||
dw ItemNameCompass
|
||||
dw ItemNameStoneBeak
|
||||
dw ItemNameNightmareKey
|
||||
dw ItemNameSmallKey
|
||||
dw ItemNameRupees50
|
||||
dw ItemNameRupees20
|
||||
dw ItemNameRupees100
|
||||
dw ItemNameRupees200
|
||||
dw ItemNameRupees500
|
||||
dw ItemNameSeashell
|
||||
dw ItemNameMessage
|
||||
dw ItemNameGel
|
||||
dw ItemNameKey1
|
||||
dw ItemNameKey2
|
||||
dw ItemNameKey3
|
||||
dw ItemNameKey4
|
||||
dw ItemNameKey5
|
||||
dw ItemNameKey6
|
||||
dw ItemNameKey7
|
||||
dw ItemNameKey8
|
||||
dw ItemNameKey9
|
||||
dw ItemNameMap1
|
||||
dw ItemNameMap2
|
||||
dw ItemNameMap3
|
||||
dw ItemNameMap4
|
||||
dw ItemNameMap5
|
||||
dw ItemNameMap6
|
||||
dw ItemNameMap7
|
||||
dw ItemNameMap8
|
||||
dw ItemNameMap9
|
||||
dw ItemNameCompass1
|
||||
dw ItemNameCompass2
|
||||
dw ItemNameCompass3
|
||||
dw ItemNameCompass4
|
||||
dw ItemNameCompass5
|
||||
dw ItemNameCompass6
|
||||
dw ItemNameCompass7
|
||||
dw ItemNameCompass8
|
||||
dw ItemNameCompass9
|
||||
dw ItemNameStoneBeak1
|
||||
dw ItemNameStoneBeak2
|
||||
dw ItemNameStoneBeak3
|
||||
dw ItemNameStoneBeak4
|
||||
dw ItemNameStoneBeak5
|
||||
dw ItemNameStoneBeak6
|
||||
dw ItemNameStoneBeak7
|
||||
dw ItemNameStoneBeak8
|
||||
dw ItemNameStoneBeak9
|
||||
dw ItemNameNightmareKey1
|
||||
dw ItemNameNightmareKey2
|
||||
dw ItemNameNightmareKey3
|
||||
dw ItemNameNightmareKey4
|
||||
dw ItemNameNightmareKey5
|
||||
dw ItemNameNightmareKey6
|
||||
dw ItemNameNightmareKey7
|
||||
dw ItemNameNightmareKey8
|
||||
dw ItemNameNightmareKey9
|
||||
dw ItemNameToadstool
|
||||
dw ItemNameNone ; 0x51
|
||||
dw ItemNameNone ; 0x52
|
||||
dw ItemNameNone ; 0x53
|
||||
dw ItemNameNone ; 0x54
|
||||
dw ItemNameNone ; 0x55
|
||||
dw ItemNameNone ; 0x56
|
||||
dw ItemNameNone ; 0x57
|
||||
dw ItemNameNone ; 0x58
|
||||
dw ItemNameNone ; 0x59
|
||||
dw ItemNameNone ; 0x5a
|
||||
dw ItemNameNone ; 0x5b
|
||||
dw ItemNameNone ; 0x5c
|
||||
dw ItemNameNone ; 0x5d
|
||||
dw ItemNameNone ; 0x5e
|
||||
dw ItemNameNone ; 0x5f
|
||||
dw ItemNameNone ; 0x60
|
||||
dw ItemNameNone ; 0x61
|
||||
dw ItemNameNone ; 0x62
|
||||
dw ItemNameNone ; 0x63
|
||||
dw ItemNameNone ; 0x64
|
||||
dw ItemNameNone ; 0x65
|
||||
dw ItemNameNone ; 0x66
|
||||
dw ItemNameNone ; 0x67
|
||||
dw ItemNameNone ; 0x68
|
||||
dw ItemNameNone ; 0x69
|
||||
dw ItemNameNone ; 0x6a
|
||||
dw ItemNameNone ; 0x6b
|
||||
dw ItemNameNone ; 0x6c
|
||||
dw ItemNameNone ; 0x6d
|
||||
dw ItemNameNone ; 0x6e
|
||||
dw ItemNameNone ; 0x6f
|
||||
dw ItemNameNone ; 0x70
|
||||
dw ItemNameNone ; 0x71
|
||||
dw ItemNameNone ; 0x72
|
||||
dw ItemNameNone ; 0x73
|
||||
dw ItemNameNone ; 0x74
|
||||
dw ItemNameNone ; 0x75
|
||||
dw ItemNameNone ; 0x76
|
||||
dw ItemNameNone ; 0x77
|
||||
dw ItemNameNone ; 0x78
|
||||
dw ItemNameNone ; 0x79
|
||||
dw ItemNameNone ; 0x7a
|
||||
dw ItemNameNone ; 0x7b
|
||||
dw ItemNameNone ; 0x7c
|
||||
dw ItemNameNone ; 0x7d
|
||||
dw ItemNameNone ; 0x7e
|
||||
dw ItemNameNone ; 0x7f
|
||||
dw ItemNameHeartPiece ; 0x80
|
||||
dw ItemNameBowwow
|
||||
dw ItemName10Arrows
|
||||
dw ItemNameSingleArrow
|
||||
dw ItemNamePowderUpgrade
|
||||
dw ItemNameBombUpgrade
|
||||
dw ItemNameArrowUpgrade
|
||||
dw ItemNameRedTunic
|
||||
dw ItemNameBlueTunic
|
||||
dw ItemNameHeartContainer
|
||||
dw ItemNameBadHeartContainer
|
||||
dw ItemNameSong1
|
||||
dw ItemNameSong2
|
||||
dw ItemNameSong3
|
||||
dw ItemInstrument1
|
||||
dw ItemInstrument2
|
||||
dw ItemInstrument3
|
||||
dw ItemInstrument4
|
||||
dw ItemInstrument5
|
||||
dw ItemInstrument6
|
||||
dw ItemInstrument7
|
||||
dw ItemInstrument8
|
||||
dw ItemRooster
|
||||
dw ItemTradeQuest1
|
||||
dw ItemTradeQuest2
|
||||
dw ItemTradeQuest3
|
||||
dw ItemTradeQuest4
|
||||
dw ItemTradeQuest5
|
||||
dw ItemTradeQuest6
|
||||
dw ItemTradeQuest7
|
||||
dw ItemTradeQuest8
|
||||
dw ItemTradeQuest9
|
||||
dw ItemTradeQuest10
|
||||
dw ItemTradeQuest11
|
||||
dw ItemTradeQuest12
|
||||
dw ItemTradeQuest13
|
||||
dw ItemTradeQuest14
|
||||
|
||||
ItemNameNone:
|
||||
db m"NONE", $ff
|
||||
|
||||
ItemNamePowerBracelet:
|
||||
db m"Got the {POWER_BRACELET}", $ff
|
||||
ItemNameShield:
|
||||
db m"Got a {SHIELD}", $ff
|
||||
ItemNameBow:
|
||||
db m"Got the {BOW}", $ff
|
||||
ItemNameHookshot:
|
||||
db m"Got the {HOOKSHOT}", $ff
|
||||
ItemNameMagicRod:
|
||||
db m"Got the {MAGIC_ROD}", $ff
|
||||
ItemNamePegasusBoots:
|
||||
db m"Got the {PEGASUS_BOOTS}", $ff
|
||||
ItemNameOcarina:
|
||||
db m"Got the {OCARINA}", $ff
|
||||
ItemNameFeather:
|
||||
db m"Got the {FEATHER}", $ff
|
||||
ItemNameShovel:
|
||||
db m"Got the {SHOVEL}", $ff
|
||||
ItemNameMagicPowder:
|
||||
db m"Got {MAGIC_POWDER}", $ff
|
||||
ItemNameBomb:
|
||||
db m"Got {BOMB}", $ff
|
||||
ItemNameSword:
|
||||
db m"Got a {SWORD}", $ff
|
||||
ItemNameFlippers:
|
||||
db m"Got the {FLIPPERS}", $ff
|
||||
ItemNameBoomerang:
|
||||
db m"Got the {BOOMERANG}", $ff
|
||||
ItemNameSlimeKey:
|
||||
db m"Got the {SLIME_KEY}", $ff
|
||||
ItemNameMedicine:
|
||||
db m"Got some {MEDICINE}", $ff
|
||||
ItemNameTailKey:
|
||||
db m"Got the {TAIL_KEY}", $ff
|
||||
ItemNameAnglerKey:
|
||||
db m"Got the {ANGLER_KEY}", $ff
|
||||
ItemNameFaceKey:
|
||||
db m"Got the {FACE_KEY}", $ff
|
||||
ItemNameBirdKey:
|
||||
db m"Got the {BIRD_KEY}", $ff
|
||||
ItemNameGoldLeaf:
|
||||
db m"Got the {GOLD_LEAF}", $ff
|
||||
ItemNameMap:
|
||||
db m"Got the {MAP}", $ff
|
||||
ItemNameCompass:
|
||||
db m"Got the {COMPASS}", $ff
|
||||
ItemNameStoneBeak:
|
||||
db m"Got the {STONE_BEAK}", $ff
|
||||
ItemNameNightmareKey:
|
||||
db m"Got the {NIGHTMARE_KEY}", $ff
|
||||
ItemNameSmallKey:
|
||||
db m"Got a {KEY}", $ff
|
||||
ItemNameRupees50:
|
||||
db m"Got 50 {RUPEES}", $ff
|
||||
ItemNameRupees20:
|
||||
db m"Got 20 {RUPEES}", $ff
|
||||
ItemNameRupees100:
|
||||
db m"Got 100 {RUPEES}", $ff
|
||||
ItemNameRupees200:
|
||||
db m"Got 200 {RUPEES}", $ff
|
||||
ItemNameRupees500:
|
||||
db m"Got 500 {RUPEES}", $ff
|
||||
ItemNameSeashell:
|
||||
db m"Got a {SEASHELL}", $ff
|
||||
ItemNameGel:
|
||||
db m"Got a Zol Attack", $ff
|
||||
ItemNameMessage:
|
||||
db m"Got ... nothing?", $ff
|
||||
ItemNameKey1:
|
||||
db m"Got a {KEY1}", $ff
|
||||
ItemNameKey2:
|
||||
db m"Got a {KEY2}", $ff
|
||||
ItemNameKey3:
|
||||
db m"Got a {KEY3}", $ff
|
||||
ItemNameKey4:
|
||||
db m"Got a {KEY4}", $ff
|
||||
ItemNameKey5:
|
||||
db m"Got a {KEY5}", $ff
|
||||
ItemNameKey6:
|
||||
db m"Got a {KEY6}", $ff
|
||||
ItemNameKey7:
|
||||
db m"Got a {KEY7}", $ff
|
||||
ItemNameKey8:
|
||||
db m"Got a {KEY8}", $ff
|
||||
ItemNameKey9:
|
||||
db m"Got a {KEY9}", $ff
|
||||
ItemNameMap1:
|
||||
db m"Got the {MAP1}", $ff
|
||||
ItemNameMap2:
|
||||
db m"Got the {MAP2}", $ff
|
||||
ItemNameMap3:
|
||||
db m"Got the {MAP3}", $ff
|
||||
ItemNameMap4:
|
||||
db m"Got the {MAP4}", $ff
|
||||
ItemNameMap5:
|
||||
db m"Got the {MAP5}", $ff
|
||||
ItemNameMap6:
|
||||
db m"Got the {MAP6}", $ff
|
||||
ItemNameMap7:
|
||||
db m"Got the {MAP7}", $ff
|
||||
ItemNameMap8:
|
||||
db m"Got the {MAP8}", $ff
|
||||
ItemNameMap9:
|
||||
db m"Got the {MAP9}", $ff
|
||||
ItemNameCompass1:
|
||||
db m"Got the {COMPASS1}", $ff
|
||||
ItemNameCompass2:
|
||||
db m"Got the {COMPASS2}", $ff
|
||||
ItemNameCompass3:
|
||||
db m"Got the {COMPASS3}", $ff
|
||||
ItemNameCompass4:
|
||||
db m"Got the {COMPASS4}", $ff
|
||||
ItemNameCompass5:
|
||||
db m"Got the {COMPASS5}", $ff
|
||||
ItemNameCompass6:
|
||||
db m"Got the {COMPASS6}", $ff
|
||||
ItemNameCompass7:
|
||||
db m"Got the {COMPASS7}", $ff
|
||||
ItemNameCompass8:
|
||||
db m"Got the {COMPASS8}", $ff
|
||||
ItemNameCompass9:
|
||||
db m"Got the {COMPASS9}", $ff
|
||||
ItemNameStoneBeak1:
|
||||
db m"Got the {STONE_BEAK1}", $ff
|
||||
ItemNameStoneBeak2:
|
||||
db m"Got the {STONE_BEAK2}", $ff
|
||||
ItemNameStoneBeak3:
|
||||
db m"Got the {STONE_BEAK3}", $ff
|
||||
ItemNameStoneBeak4:
|
||||
db m"Got the {STONE_BEAK4}", $ff
|
||||
ItemNameStoneBeak5:
|
||||
db m"Got the {STONE_BEAK5}", $ff
|
||||
ItemNameStoneBeak6:
|
||||
db m"Got the {STONE_BEAK6}", $ff
|
||||
ItemNameStoneBeak7:
|
||||
db m"Got the {STONE_BEAK7}", $ff
|
||||
ItemNameStoneBeak8:
|
||||
db m"Got the {STONE_BEAK8}", $ff
|
||||
ItemNameStoneBeak9:
|
||||
db m"Got the {STONE_BEAK9}", $ff
|
||||
ItemNameNightmareKey1:
|
||||
db m"Got the {NIGHTMARE_KEY1}", $ff
|
||||
ItemNameNightmareKey2:
|
||||
db m"Got the {NIGHTMARE_KEY2}", $ff
|
||||
ItemNameNightmareKey3:
|
||||
db m"Got the {NIGHTMARE_KEY3}", $ff
|
||||
ItemNameNightmareKey4:
|
||||
db m"Got the {NIGHTMARE_KEY4}", $ff
|
||||
ItemNameNightmareKey5:
|
||||
db m"Got the {NIGHTMARE_KEY5}", $ff
|
||||
ItemNameNightmareKey6:
|
||||
db m"Got the {NIGHTMARE_KEY6}", $ff
|
||||
ItemNameNightmareKey7:
|
||||
db m"Got the {NIGHTMARE_KEY7}", $ff
|
||||
ItemNameNightmareKey8:
|
||||
db m"Got the {NIGHTMARE_KEY8}", $ff
|
||||
ItemNameNightmareKey9:
|
||||
db m"Got the {NIGHTMARE_KEY9}", $ff
|
||||
ItemNameToadstool:
|
||||
db m"Got the {TOADSTOOL}", $ff
|
||||
|
||||
ItemNameHeartPiece:
|
||||
db m"Got the {HEART_PIECE}", $ff
|
||||
ItemNameBowwow:
|
||||
db m"Got the {BOWWOW}", $ff
|
||||
ItemName10Arrows:
|
||||
db m"Got {ARROWS_10}", $ff
|
||||
ItemNameSingleArrow:
|
||||
db m"Got the {SINGLE_ARROW}", $ff
|
||||
ItemNamePowderUpgrade:
|
||||
db m"Got the {MAX_POWDER_UPGRADE}", $ff
|
||||
ItemNameBombUpgrade:
|
||||
db m"Got the {MAX_BOMBS_UPGRADE}", $ff
|
||||
ItemNameArrowUpgrade:
|
||||
db m"Got the {MAX_ARROWS_UPGRADE}", $ff
|
||||
ItemNameRedTunic:
|
||||
db m"Got the {RED_TUNIC}", $ff
|
||||
ItemNameBlueTunic:
|
||||
db m"Got the {BLUE_TUNIC}", $ff
|
||||
ItemNameHeartContainer:
|
||||
db m"Got a {HEART_CONTAINER}", $ff
|
||||
ItemNameBadHeartContainer:
|
||||
db m"Got the {BAD_HEART_CONTAINER}", $ff
|
||||
ItemNameSong1:
|
||||
db m"Got the {SONG1}", $ff
|
||||
ItemNameSong2:
|
||||
db m"Got {SONG2}", $ff
|
||||
ItemNameSong3:
|
||||
db m"Got {SONG3}", $ff
|
||||
|
||||
ItemInstrument1:
|
||||
db m"You've got the {INSTRUMENT1}", $ff
|
||||
ItemInstrument2:
|
||||
db m"You've got the {INSTRUMENT2}", $ff
|
||||
ItemInstrument3:
|
||||
db m"You've got the {INSTRUMENT3}", $ff
|
||||
ItemInstrument4:
|
||||
db m"You've got the {INSTRUMENT4}", $ff
|
||||
ItemInstrument5:
|
||||
db m"You've got the {INSTRUMENT5}", $ff
|
||||
ItemInstrument6:
|
||||
db m"You've got the {INSTRUMENT6}", $ff
|
||||
ItemInstrument7:
|
||||
db m"You've got the {INSTRUMENT7}", $ff
|
||||
ItemInstrument8:
|
||||
db m"You've got the {INSTRUMENT8}", $ff
|
||||
|
||||
ItemRooster:
|
||||
db m"You've got the {ROOSTER}", $ff
|
||||
|
||||
ItemTradeQuest1:
|
||||
db m"You've got the Yoshi Doll", $ff
|
||||
ItemTradeQuest2:
|
||||
db m"You've got the Ribbon", $ff
|
||||
ItemTradeQuest3:
|
||||
db m"You've got the Dog Food", $ff
|
||||
ItemTradeQuest4:
|
||||
db m"You've got the Bananas", $ff
|
||||
ItemTradeQuest5:
|
||||
db m"You've got the Stick", $ff
|
||||
ItemTradeQuest6:
|
||||
db m"You've got the Honeycomb", $ff
|
||||
ItemTradeQuest7:
|
||||
db m"You've got the Pineapple", $ff
|
||||
ItemTradeQuest8:
|
||||
db m"You've got the Hibiscus", $ff
|
||||
ItemTradeQuest9:
|
||||
db m"You've got the Letter", $ff
|
||||
ItemTradeQuest10:
|
||||
db m"You've got the Broom", $ff
|
||||
ItemTradeQuest11:
|
||||
db m"You've got the Fishing Hook", $ff
|
||||
ItemTradeQuest12:
|
||||
db m"You've got the Necklace", $ff
|
||||
ItemTradeQuest13:
|
||||
db m"You've got the Scale", $ff
|
||||
ItemTradeQuest14:
|
||||
db m"You've got the Magnifying Lens", $ff
|
||||
|
||||
MultiNamePointers:
|
|
@ -0,0 +1,89 @@
|
|||
; Handle the serial link cable
|
||||
#IF HARDWARE_LINK
|
||||
; FF> = Idle
|
||||
; D6> = Read: D0><[L] D1><[H] [HL]>
|
||||
; D9> = Write: D8><[L] D9><[H] DA><[^DATA] DB><[DATA]
|
||||
; DD> = OrW: D8><[L] D9><[H] DA><[^DATA] DB><[DATA] (used to set flags without requiring a slow read,modify,write race condition)
|
||||
|
||||
handleSerialLink:
|
||||
; Check if we got a byte from hardware
|
||||
ldh a, [$01]
|
||||
|
||||
cp $D6
|
||||
jr z, serialReadMem
|
||||
cp $D9
|
||||
jr z, serialWriteMem
|
||||
cp $DD
|
||||
jr z, serialOrMem
|
||||
|
||||
finishSerialLink:
|
||||
; Do a new idle transfer.
|
||||
ld a, $E4
|
||||
ldh [$01], a
|
||||
ld a, $81
|
||||
ldh [$02], a
|
||||
ret
|
||||
|
||||
serialReadMem:
|
||||
ld a, $D0
|
||||
call serialTransfer
|
||||
ld h, a
|
||||
ld a, $D1
|
||||
call serialTransfer
|
||||
ld l, a
|
||||
ld a, [hl]
|
||||
call serialTransfer
|
||||
jr finishSerialLink
|
||||
|
||||
serialWriteMem:
|
||||
ld a, $D8
|
||||
call serialTransfer
|
||||
ld h, a
|
||||
ld a, $D9
|
||||
call serialTransfer
|
||||
ld l, a
|
||||
ld a, $DA
|
||||
call serialTransfer
|
||||
cpl
|
||||
ld c, a
|
||||
ld a, $DB
|
||||
call serialTransfer
|
||||
cp c
|
||||
jr nz, finishSerialLink
|
||||
ld [hl], a
|
||||
jr finishSerialLink
|
||||
|
||||
serialOrMem:
|
||||
ld a, $D8
|
||||
call serialTransfer
|
||||
ld h, a
|
||||
ld a, $D9
|
||||
call serialTransfer
|
||||
ld l, a
|
||||
ld a, $DA
|
||||
call serialTransfer
|
||||
cpl
|
||||
ld c, a
|
||||
ld a, $DB
|
||||
call serialTransfer
|
||||
cp c
|
||||
jr nz, finishSerialLink
|
||||
ld c, a
|
||||
ld a, [hl]
|
||||
or c
|
||||
ld [hl], a
|
||||
jr finishSerialLink
|
||||
|
||||
; Transfer A to the serial link and wait for it to be done and return the result in A
|
||||
serialTransfer:
|
||||
ldh [$01], a
|
||||
ld a, $81
|
||||
ldh [$02], a
|
||||
.loop:
|
||||
ldh a, [$02]
|
||||
and $80
|
||||
jr nz, .loop
|
||||
ldh a, [$01]
|
||||
ret
|
||||
|
||||
#ENDIF
|
|
@ -0,0 +1,16 @@
|
|||
MessageCopyString:
|
||||
.loop:
|
||||
ldi a, [hl]
|
||||
ld [de], a
|
||||
cp $ff
|
||||
ret z
|
||||
inc de
|
||||
jr .loop
|
||||
|
||||
MessageAddSpace:
|
||||
ld a, $20
|
||||
ld [de], a
|
||||
inc de
|
||||
ld a, $ff
|
||||
ld [de], a
|
||||
ret
|
|
@ -0,0 +1,355 @@
|
|||
; Handle the multiworld link
|
||||
|
||||
MainLoop:
|
||||
#IF HARDWARE_LINK
|
||||
call handleSerialLink
|
||||
#ENDIF
|
||||
; Check if the gameplay is world
|
||||
ld a, [$DB95]
|
||||
cp $0B
|
||||
ret nz
|
||||
; Check if the world subtype is the normal one
|
||||
ld a, [$DB96]
|
||||
cp $07
|
||||
ret nz
|
||||
; Check if we are moving between rooms
|
||||
ld a, [$C124]
|
||||
and a
|
||||
ret nz
|
||||
; Check if link is in a normal walking/swimming state
|
||||
ld a, [$C11C]
|
||||
cp $02
|
||||
ret nc
|
||||
; Check if a dialog is open
|
||||
ld a, [$C19F]
|
||||
and a
|
||||
ret nz
|
||||
; Check if interaction is blocked
|
||||
ldh a, [$A1]
|
||||
and a
|
||||
ret nz
|
||||
|
||||
ld a, [wLinkSpawnDelay]
|
||||
and a
|
||||
jr z, .allowSpawn
|
||||
dec a
|
||||
ld [wLinkSpawnDelay], a
|
||||
jr .noSpawn
|
||||
|
||||
.allowSpawn:
|
||||
ld a, [wZolSpawnCount]
|
||||
and a
|
||||
call nz, LinkSpawnSlime
|
||||
ld a, [wCuccoSpawnCount]
|
||||
and a
|
||||
call nz, LinkSpawnCucco
|
||||
ld a, [wDropBombSpawnCount]
|
||||
and a
|
||||
call nz, LinkSpawnBomb
|
||||
.noSpawn:
|
||||
|
||||
; Have an item to give?
|
||||
ld hl, wLinkStatusBits
|
||||
bit 0, [hl]
|
||||
ret z
|
||||
|
||||
; Give an item to the player
|
||||
ld a, [wLinkGiveItem]
|
||||
; if zol:
|
||||
cp $22 ; zol item
|
||||
jr z, LinkGiveSlime
|
||||
; if special item
|
||||
cp $F0
|
||||
jr nc, HandleSpecialItem
|
||||
; tmpChestItem = a
|
||||
ldh [$F1], a
|
||||
; Give the item
|
||||
call GiveItemFromChest
|
||||
; Paste the item text
|
||||
call BuildItemMessage
|
||||
; Paste " from "
|
||||
ld hl, SpaceFrom
|
||||
call MessageCopyString
|
||||
; Paste the player name
|
||||
ld a, [wLinkGiveItemFrom]
|
||||
call MessageAddPlayerName
|
||||
ld a, $C9
|
||||
; hl = $wLinkStatusBits
|
||||
ld hl, wLinkStatusBits
|
||||
; clear the 0 bit of *hl
|
||||
res 0, [hl]
|
||||
; OpenDialog()
|
||||
jp $2385 ; Opendialog in $000-$0FF range
|
||||
|
||||
LinkGiveSlime:
|
||||
ld a, $05
|
||||
ld [wZolSpawnCount], a
|
||||
ld hl, wLinkStatusBits
|
||||
res 0, [hl]
|
||||
ret
|
||||
|
||||
HandleSpecialItem:
|
||||
ld hl, wLinkStatusBits
|
||||
res 0, [hl]
|
||||
|
||||
and $0F
|
||||
rst 0
|
||||
dw SpecialSlimeStorm
|
||||
dw SpecialCuccoParty
|
||||
dw SpecialPieceOfPower
|
||||
dw SpecialHealth
|
||||
dw SpecialRandomTeleport
|
||||
dw .ret
|
||||
dw .ret
|
||||
dw .ret
|
||||
dw .ret
|
||||
dw .ret
|
||||
dw .ret
|
||||
dw .ret
|
||||
dw .ret
|
||||
dw .ret
|
||||
dw .ret
|
||||
dw .ret
|
||||
.ret:
|
||||
ret
|
||||
|
||||
SpecialSlimeStorm:
|
||||
ld a, $20
|
||||
ld [wZolSpawnCount], a
|
||||
ret
|
||||
SpecialCuccoParty:
|
||||
ld a, $20
|
||||
ld [wCuccoSpawnCount], a
|
||||
ret
|
||||
SpecialPieceOfPower:
|
||||
; Give the piece of power and the music
|
||||
ld a, $01
|
||||
ld [$D47C], a
|
||||
ld a, $27
|
||||
ld [$D368], a
|
||||
ld a, $49
|
||||
ldh [$BD], a
|
||||
ldh [$BF], a
|
||||
ret
|
||||
SpecialHealth:
|
||||
; Regen all health
|
||||
ld hl, $DB93
|
||||
ld [hl], $FF
|
||||
ret
|
||||
|
||||
LinkSpawnSlime:
|
||||
ld a, $1B
|
||||
ld e, $08
|
||||
call $3B98 ; SpawnNewEntity in range
|
||||
ret c
|
||||
|
||||
; Place somewhere random
|
||||
call placeRandom
|
||||
|
||||
ld hl, $C310
|
||||
add hl, de
|
||||
ld [hl], $7F
|
||||
|
||||
ld hl, wZolSpawnCount
|
||||
dec [hl]
|
||||
|
||||
call $280D
|
||||
and $03
|
||||
ld [wLinkSpawnDelay], a
|
||||
ret
|
||||
|
||||
LinkSpawnCucco:
|
||||
ld a, $6C
|
||||
ld e, $04
|
||||
call $3B98 ; SpawnNewEntity in range
|
||||
ret c
|
||||
|
||||
; Place where link is at.
|
||||
ld hl, $C200
|
||||
add hl, de
|
||||
ldh a, [$98]
|
||||
ld [hl], a
|
||||
ld hl, $C210
|
||||
add hl, de
|
||||
ldh a, [$99]
|
||||
ld [hl], a
|
||||
|
||||
; Set the "hits till cucco killer attack" much lower
|
||||
ld hl, $C2B0
|
||||
add hl, de
|
||||
ld a, $21
|
||||
ld [hl], a
|
||||
|
||||
ld hl, wCuccoSpawnCount
|
||||
dec [hl]
|
||||
|
||||
call $280D
|
||||
and $07
|
||||
ld [wLinkSpawnDelay], a
|
||||
ret
|
||||
|
||||
LinkSpawnBomb:
|
||||
ld a, $02
|
||||
ld e, $08
|
||||
call $3B98 ; SpawnNewEntity in range
|
||||
ret c
|
||||
|
||||
call placeRandom
|
||||
|
||||
ld hl, $C310 ; z pos
|
||||
add hl, de
|
||||
ld [hl], $4F
|
||||
|
||||
ld hl, $C430 ; wEntitiesOptions1Table
|
||||
add hl, de
|
||||
res 0, [hl]
|
||||
ld hl, $C2E0 ; wEntitiesTransitionCountdownTable
|
||||
add hl, de
|
||||
ld [hl], $80
|
||||
ld hl, $C440 ; wEntitiesPrivateState4Table
|
||||
add hl, de
|
||||
ld [hl], $01
|
||||
|
||||
ld hl, wDropBombSpawnCount
|
||||
dec [hl]
|
||||
|
||||
call $280D
|
||||
and $1F
|
||||
ld [wLinkSpawnDelay], a
|
||||
ret
|
||||
|
||||
placeRandom:
|
||||
; Place somewhere random
|
||||
ld hl, $C200
|
||||
add hl, de
|
||||
call $280D ; random number
|
||||
and $7F
|
||||
add a, $08
|
||||
ld [hl], a
|
||||
ld hl, $C210
|
||||
add hl, de
|
||||
call $280D ; random number
|
||||
and $3F
|
||||
add a, $20
|
||||
ld [hl], a
|
||||
ret
|
||||
|
||||
SpecialRandomTeleport:
|
||||
xor a
|
||||
; Warp data
|
||||
ld [$D401], a
|
||||
ld [$D402], a
|
||||
call $280D ; random number
|
||||
ld [$D403], a
|
||||
ld hl, RandomTeleportPositions
|
||||
ld d, $00
|
||||
ld e, a
|
||||
add hl, de
|
||||
ld e, [hl]
|
||||
ld a, e
|
||||
and $0F
|
||||
swap a
|
||||
add a, $08
|
||||
ld [$D404], a
|
||||
ld a, e
|
||||
and $F0
|
||||
add a, $10
|
||||
ld [$D405], a
|
||||
|
||||
ldh a, [$98]
|
||||
swap a
|
||||
and $0F
|
||||
ld e, a
|
||||
ldh a, [$99]
|
||||
sub $08
|
||||
and $F0
|
||||
or e
|
||||
ld [$D416], a ; wWarp0PositionTileIndex
|
||||
|
||||
call $0C7D
|
||||
ld a, $07
|
||||
ld [$DB96], a ; wGameplaySubtype
|
||||
|
||||
ret
|
||||
|
||||
Data_004_7AE5: ; @TODO Palette data
|
||||
db $33, $62, $1A, $01, $FF, $0F, $FF, $7F
|
||||
|
||||
|
||||
Deathlink:
|
||||
; Spawn the entity
|
||||
ld a, $CA ; $7AF3: $3E $CA
|
||||
call $3B86 ; $7AF5: $CD $86 $3B ;SpawnEntityTrampoline
|
||||
ld a, $26 ; $7AF8: $3E $26 ;
|
||||
ldh [$F4], a ; $7AFA: $E0 $F4 ; set noise
|
||||
; Set posX = linkX
|
||||
ldh a, [$98] ; LinkX
|
||||
ld hl, $C200 ; wEntitiesPosXTable
|
||||
add hl, de
|
||||
ld [hl], a
|
||||
; set posY = linkY - 54
|
||||
ldh a, [$99] ; LinkY
|
||||
sub a, 54
|
||||
ld hl, $C210 ; wEntitiesPosYTable
|
||||
add hl, de
|
||||
ld [hl], a
|
||||
; wEntitiesPrivateState3Table
|
||||
ld hl, $C2D0 ; $7B0A: $21 $D0 $C2
|
||||
add hl, de ; $7B0D: $19
|
||||
ld [hl], $01 ; $7B0E: $36 $01
|
||||
; wEntitiesTransitionCountdownTable
|
||||
ld hl, $C2E0 ; $7B10: $21 $E0 $C2
|
||||
add hl, de ; $7B13: $19
|
||||
ld [hl], $C0 ; $7B14: $36 $C0
|
||||
; GetEntityTransitionCountdown
|
||||
call $0C05 ; $7B16: $CD $05 $0C
|
||||
ld [hl], $C0 ; $7B19: $36 $C0
|
||||
; IncrementEntityState
|
||||
call $3B12 ; $7B1B: $CD $12 $3B
|
||||
|
||||
; Remove medicine
|
||||
xor a ; $7B1E: $AF
|
||||
ld [$DB0D], a ; $7B1F: $EA $0D $DB ; ld [wHasMedicine], a
|
||||
; Reduce health by a lot
|
||||
ld a, $FF ; $7B22: $3E $FF
|
||||
ld [$DB94], a ; $7B24: $EA $94 $DB ; ld [wSubtractHealthBuffer], a
|
||||
|
||||
ld hl, $DC88 ; $7B2C: $21 $88 $DC
|
||||
; Set palette
|
||||
ld de, Data_004_7AE5 ; $7B2F: $11 $E5 $7A
|
||||
|
||||
loop_7B32:
|
||||
ld a, [de] ; $7B32: $1A
|
||||
; ld [hl+], a ; $7B33: $22
|
||||
db $22
|
||||
inc de ; $7B34: $13
|
||||
ld a, l ; $7B35: $7D
|
||||
and $07 ; $7B36: $E6 $07
|
||||
jr nz, loop_7B32 ; $7B38: $20 $F8
|
||||
|
||||
ld a, $02 ; $7B3A: $3E $02
|
||||
ld [$DDD1], a ; $7B3C: $EA $D1 $DD
|
||||
|
||||
ret
|
||||
|
||||
; probalby wants
|
||||
; ld a, $02 ; $7B40: $3E $02
|
||||
;ldh [hLinkInteractiveMotionBlocked], a
|
||||
|
||||
RandomTeleportPositions:
|
||||
db $55, $54, $54, $54, $55, $55, $55, $54, $65, $55, $54, $65, $56, $56, $55, $55
|
||||
db $55, $45, $65, $54, $55, $55, $55, $55, $55, $55, $55, $58, $43, $57, $55, $55
|
||||
db $55, $55, $55, $55, $55, $54, $55, $53, $54, $56, $65, $65, $56, $55, $57, $65
|
||||
db $45, $55, $55, $55, $55, $55, $55, $55, $48, $45, $43, $34, $35, $35, $36, $34
|
||||
db $65, $55, $55, $54, $54, $54, $55, $54, $56, $65, $55, $55, $55, $55, $54, $54
|
||||
db $55, $55, $55, $55, $56, $55, $55, $54, $55, $55, $55, $53, $45, $35, $53, $46
|
||||
db $56, $55, $55, $55, $53, $55, $54, $54, $55, $55, $55, $54, $44, $55, $55, $54
|
||||
db $55, $55, $45, $55, $55, $54, $45, $45, $63, $55, $65, $55, $45, $45, $44, $54
|
||||
db $56, $56, $54, $55, $54, $55, $55, $55, $55, $55, $55, $56, $54, $55, $65, $56
|
||||
db $54, $54, $55, $65, $56, $54, $55, $56, $55, $55, $55, $66, $65, $65, $55, $56
|
||||
db $65, $55, $55, $75, $55, $55, $55, $54, $55, $55, $65, $57, $55, $54, $53, $45
|
||||
db $55, $56, $55, $55, $55, $45, $54, $55, $54, $55, $56, $55, $55, $55, $55, $54
|
||||
db $55, $55, $65, $55, $55, $54, $53, $58, $55, $05, $58, $55, $55, $55, $74, $55
|
||||
db $55, $55, $55, $55, $46, $55, $55, $56, $55, $55, $55, $54, $55, $45, $55, $55
|
||||
db $55, $55, $54, $55, $55, $55, $65, $55, $55, $46, $55, $55, $56, $55, $55, $55
|
||||
db $55, $55, $54, $55, $55, $55, $45, $36, $53, $51, $57, $53, $56, $54, $45, $46
|
|
@ -0,0 +1,63 @@
|
|||
HandleOwlStatue:
|
||||
call GetRoomStatusAddressInHL
|
||||
ld a, [hl]
|
||||
and $20
|
||||
ret nz
|
||||
ld a, [hl]
|
||||
or $20
|
||||
ld [hl], a
|
||||
|
||||
ld hl, $7B16
|
||||
call OffsetPointerByRoomNumber
|
||||
ld a, [hl]
|
||||
ldh [$F1], a
|
||||
call ItemMessage
|
||||
call GiveItemFromChest
|
||||
ret
|
||||
|
||||
|
||||
|
||||
GetRoomStatusAddressInHL:
|
||||
ld a, [$DBA5] ; isIndoor
|
||||
ld d, a
|
||||
ld hl, $D800
|
||||
ldh a, [$F6] ; room nr
|
||||
ld e, a
|
||||
ldh a, [$F7] ; map nr
|
||||
cp $FF
|
||||
jr nz, .notColorDungeon
|
||||
|
||||
ld d, $00
|
||||
ld hl, $DDE0
|
||||
jr .notIndoorB
|
||||
|
||||
.notColorDungeon:
|
||||
cp $1A
|
||||
jr nc, .notIndoorB
|
||||
|
||||
cp $06
|
||||
jr c, .notIndoorB
|
||||
|
||||
inc d
|
||||
|
||||
.notIndoorB:
|
||||
add hl, de
|
||||
ret
|
||||
|
||||
|
||||
RenderOwlStatueItem:
|
||||
ldh a, [$F6] ; map room
|
||||
cp $B2
|
||||
jr nz, .NotYipYip
|
||||
; Add 2 to room to set room pointer to an empty room for trade items
|
||||
add a, 2
|
||||
ldh [$F6], a
|
||||
call RenderItemForRoom
|
||||
ldh a, [$F6] ; map room
|
||||
; ...and undo it
|
||||
sub a, 2
|
||||
ldh [$F6], a
|
||||
ret
|
||||
.NotYipYip:
|
||||
call RenderItemForRoom
|
||||
ret
|
|
@ -0,0 +1,225 @@
|
|||
import os
|
||||
import binascii
|
||||
from ..assembler import ASM
|
||||
from ..utils import formatText
|
||||
|
||||
|
||||
def hasBank3E(rom):
|
||||
return rom.banks[0x3E][0] != 0x00
|
||||
|
||||
def generate_name(l, i):
|
||||
if i < len(l):
|
||||
name = l[i]
|
||||
else:
|
||||
name = f"player {i}"
|
||||
name = name[:16]
|
||||
assert(len(name) <= 16)
|
||||
return 'db "' + name + '"' + ', $ff' * (17 - len(name)) + '\n'
|
||||
|
||||
|
||||
# Bank $3E is used for large chunks of custom code.
|
||||
# Mainly for new chest and dropped items handling.
|
||||
def addBank3E(rom, seed, player_id, player_name_list):
|
||||
# No default text for getting the bow, so use an unused slot.
|
||||
rom.texts[0x89] = formatText("Found the {BOW}!")
|
||||
rom.texts[0xD9] = formatText("Found the {BOOMERANG}!") # owl text slot reuse
|
||||
rom.texts[0xBE] = rom.texts[0x111] # owl text slot reuse to get the master skull message in the first dialog group
|
||||
rom.texts[0xC8] = formatText("Found {BOWWOW}! Which monster put him in a chest? He is a good boi, and waits for you at the Swamp.")
|
||||
rom.texts[0xC9] = 0xC0A0 # Custom message slot
|
||||
rom.texts[0xCA] = formatText("Found {ARROWS_10}!")
|
||||
rom.texts[0xCB] = formatText("Found a {SINGLE_ARROW}... joy?")
|
||||
|
||||
# Create a trampoline to bank 0x3E in bank 0x00.
|
||||
# There is very little room in bank 0, so we set this up as a single trampoline for multiple possible usages.
|
||||
# the A register is preserved and can directly be used as a jumptable in page 3E.
|
||||
# Trampoline at rst 8
|
||||
# the A register is preserved and can directly be used as a jumptable in page 3E.
|
||||
rom.patch(0, 0x0008, "0000000000000000000000000000", ASM("""
|
||||
ld h, a
|
||||
ld a, [$DBAF]
|
||||
push af
|
||||
ld a, $3E
|
||||
call $080C ; switch bank
|
||||
ld a, h
|
||||
jp $4000
|
||||
"""), fill_nop=True)
|
||||
|
||||
# Special trampoline to jump to the damage-entity code, we use this from bowwow to damage instead of eat.
|
||||
rom.patch(0x00, 0x0018, "000000000000000000000000000000", ASM("""
|
||||
ld a, $03
|
||||
ld [$2100], a
|
||||
call $71C0
|
||||
ld a, [$DBAF]
|
||||
ld [$2100], a
|
||||
ret
|
||||
"""))
|
||||
|
||||
my_path = os.path.dirname(__file__)
|
||||
rom.patch(0x3E, 0x0000, 0x2F00, ASM("""
|
||||
call MainJumpTable
|
||||
pop af
|
||||
jp $080C ; switch bank and return to normal code.
|
||||
|
||||
MainJumpTable:
|
||||
rst 0 ; JUMP TABLE
|
||||
dw MainLoop ; 0
|
||||
dw RenderChestItem ; 1
|
||||
dw GiveItemFromChest ; 2
|
||||
dw ItemMessage ; 3
|
||||
dw RenderDroppedKey ; 4
|
||||
dw RenderHeartPiece ; 5
|
||||
dw GiveItemFromChestMultiworld ; 6
|
||||
dw CheckIfLoadBowWow ; 7
|
||||
dw BowwowEat ; 8
|
||||
dw HandleOwlStatue ; 9
|
||||
dw ItemMessageMultiworld ; A
|
||||
dw GiveItemAndMessageForRoom ; B
|
||||
dw RenderItemForRoom ; C
|
||||
dw StartGameMarinMessage ; D
|
||||
dw GiveItemAndMessageForRoomMultiworld ; E
|
||||
dw RenderOwlStatueItem ; F
|
||||
dw UpdateInventoryMenu ; 10
|
||||
dw LocalOnlyItemAndMessage ; 11
|
||||
StartGameMarinMessage:
|
||||
; Injection to reset our frame counter
|
||||
call $27D0 ; Enable SRAM
|
||||
ld hl, $B000
|
||||
xor a
|
||||
ldi [hl], a ;subsecond counter
|
||||
ld a, $08 ;(We set the counter to 8 seconds, as it takes 8 seconds before link wakes up and marin talks to him)
|
||||
ldi [hl], a ;second counter
|
||||
xor a
|
||||
ldi [hl], a ;minute counter
|
||||
ldi [hl], a ;hour counter
|
||||
|
||||
ld hl, $B010
|
||||
ldi [hl], a ;check counter low
|
||||
ldi [hl], a ;check counter high
|
||||
|
||||
; Show the normal message
|
||||
ld a, $01
|
||||
jp $2385
|
||||
|
||||
TradeSequenceItemData:
|
||||
; tile attributes
|
||||
db $0D, $0A, $0D, $0D, $0E, $0E, $0D, $0D, $0D, $0E, $09, $0A, $0A, $0D
|
||||
; tile index
|
||||
db $1A, $B0, $B4, $B8, $BC, $C0, $C4, $C8, $CC, $D0, $D4, $D8, $DC, $E0
|
||||
|
||||
UpdateInventoryMenu:
|
||||
ld a, [wTradeSequenceItem]
|
||||
ld hl, wTradeSequenceItem2
|
||||
or [hl]
|
||||
ret z
|
||||
|
||||
ld hl, TradeSequenceItemData
|
||||
ld a, [$C109]
|
||||
ld e, a
|
||||
ld d, $00
|
||||
add hl, de
|
||||
|
||||
; Check if we need to increase the counter
|
||||
ldh a, [$E7] ; frame counter
|
||||
and $0F
|
||||
jr nz, .noInc
|
||||
ld a, e
|
||||
inc a
|
||||
cp 14
|
||||
jr nz, .noWrap
|
||||
xor a
|
||||
.noWrap:
|
||||
ld [$C109], a
|
||||
.noInc:
|
||||
|
||||
; Check if we have the item
|
||||
ld b, e
|
||||
inc b
|
||||
ld a, $01
|
||||
|
||||
ld de, wTradeSequenceItem
|
||||
.shiftLoop:
|
||||
dec b
|
||||
jr z, .shiftLoopDone
|
||||
sla a
|
||||
jr nz, .shiftLoop
|
||||
; switching to second byte
|
||||
ld de, wTradeSequenceItem2
|
||||
ld a, $01
|
||||
jr .shiftLoop
|
||||
.shiftLoopDone:
|
||||
ld b, a
|
||||
ld a, [de]
|
||||
and b
|
||||
ret z ; skip this item
|
||||
|
||||
ld b, [hl]
|
||||
push hl
|
||||
|
||||
; Write the tile attribute data
|
||||
ld a, $01
|
||||
ldh [$4F], a
|
||||
|
||||
ld hl, $9C6E
|
||||
call WriteToVRAM
|
||||
inc hl
|
||||
call WriteToVRAM
|
||||
ld de, $001F
|
||||
add hl, de
|
||||
call WriteToVRAM
|
||||
inc hl
|
||||
call WriteToVRAM
|
||||
|
||||
; Write the tile data
|
||||
xor a
|
||||
ldh [$4F], a
|
||||
|
||||
pop hl
|
||||
ld de, 14
|
||||
add hl, de
|
||||
ld b, [hl]
|
||||
|
||||
ld hl, $9C6E
|
||||
call WriteToVRAM
|
||||
inc b
|
||||
inc b
|
||||
inc hl
|
||||
call WriteToVRAM
|
||||
ld de, $001F
|
||||
add hl, de
|
||||
dec b
|
||||
call WriteToVRAM
|
||||
inc hl
|
||||
inc b
|
||||
inc b
|
||||
call WriteToVRAM
|
||||
ret
|
||||
|
||||
WriteToVRAM:
|
||||
ldh a, [$41]
|
||||
and $02
|
||||
jr nz, WriteToVRAM
|
||||
ld [hl], b
|
||||
ret
|
||||
LocalOnlyItemAndMessage:
|
||||
call GiveItemFromChest
|
||||
call ItemMessage
|
||||
ret
|
||||
""" + open(os.path.join(my_path, "bank3e.asm/multiworld.asm"), "rt").read()
|
||||
+ open(os.path.join(my_path, "bank3e.asm/link.asm"), "rt").read()
|
||||
+ open(os.path.join(my_path, "bank3e.asm/chest.asm"), "rt").read()
|
||||
+ open(os.path.join(my_path, "bank3e.asm/bowwow.asm"), "rt").read()
|
||||
+ open(os.path.join(my_path, "bank3e.asm/message.asm"), "rt").read()
|
||||
+ open(os.path.join(my_path, "bank3e.asm/itemnames.asm"), "rt").read()
|
||||
+ "".join(generate_name(["The Server"] + player_name_list, i ) for i in range(100)) # allocate
|
||||
+ 'db "another world", $ff\n'
|
||||
+ open(os.path.join(my_path, "bank3e.asm/owl.asm"), "rt").read(), 0x4000), fill_nop=True)
|
||||
# 3E:3300-3616: Multiworld flags per room (for both chests and dropped keys)
|
||||
# 3E:3800-3B16: DroppedKey item types
|
||||
# 3E:3B16-3E2C: Owl statue or trade quest items
|
||||
|
||||
# Put 20 rupees in all owls by default.
|
||||
rom.patch(0x3E, 0x3B16, "00" * 0x316, "1C" * 0x316)
|
||||
|
||||
|
||||
# Prevent the photo album from crashing due to serial interrupts
|
||||
rom.patch(0x28, 0x00D2, ASM("ld a, $09"), ASM("ld a, $01"))
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue