diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c51f155049..849e752305 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,10 +2,20 @@ name: Build -on: workflow_dispatch +on: + push: + paths: + - '.github/workflows/build.yml' + - 'setup.py' + - 'requirements.txt' + pull_request: + paths: + - '.github/workflows/build.yml' + - 'setup.py' + - 'requirements.txt' + workflow_dispatch: env: - SNI_VERSION: v0.0.88 ENEMIZER_VERSION: 7.1 APPIMAGETOOL_VERSION: 13 @@ -15,21 +25,18 @@ jobs: build-win-py38: # RCs will still be built and signed by hand runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Install python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: '3.8' - name: Download run-time dependencies run: | - Invoke-WebRequest -Uri https://github.com/alttpo/sni/releases/download/${Env:SNI_VERSION}/sni-${Env:SNI_VERSION}-windows-amd64.zip -OutFile sni.zip - Expand-Archive -Path sni.zip -DestinationPath SNI -Force Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip Expand-Archive -Path enemizer.zip -DestinationPath EnemizerCLI -Force - name: Build run: | - python -m pip install --upgrade pip setuptools - pip install -r requirements.txt + python -m pip install --upgrade pip python setup.py build_exe --yes $NAME="$(ls build)".Split('.',2)[1] $ZIP_NAME="Archipelago_$NAME.7z" @@ -39,24 +46,24 @@ jobs: Rename-Item exe.$NAME Archipelago 7z a -mx=9 -mhe=on -ms "../dist/$ZIP_NAME" Archipelago - name: Store 7z - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: ${{ env.ZIP_NAME }} path: dist/${{ env.ZIP_NAME }} retention-days: 7 # keep for 7 days, should be enough - build-ubuntu1804: - runs-on: ubuntu-18.04 + build-ubuntu2004: + runs-on: ubuntu-20.04 steps: # - copy code below to release.yml - - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Install base dependencies run: | sudo apt update sudo apt -y install build-essential p7zip xz-utils wget libglib2.0-0 sudo apt -y install python3-gi libgirepository1.0-dev # should pull dependencies for gi installation below - name: Get a recent python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: '3.9' - name: Install build-time dependencies @@ -69,10 +76,6 @@ jobs: chmod a+rx appimagetool - name: Download run-time dependencies run: | - wget -nv https://github.com/alttpo/sni/releases/download/$SNI_VERSION/sni-$SNI_VERSION-manylinux2014-amd64.tar.xz - tar xf sni-*.tar.xz - rm sni-*.tar.xz - mv sni-* SNI wget -nv https://github.com/Ijwu/Enemizer/releases/download/$ENEMIZER_VERSION/ubuntu.16.04-x64.7z 7za x -oEnemizerCLI/ ubuntu.16.04-x64.7z - name: Build @@ -81,8 +84,7 @@ jobs: # charset-normalizer was somehow incomplete in the github runner "${{ env.PYTHON }}" -m venv venv source venv/bin/activate - "${{ env.PYTHON }}" -m pip install --upgrade pip PyGObject setuptools charset-normalizer - pip install -r requirements.txt + "${{ env.PYTHON }}" -m pip install --upgrade pip PyGObject charset-normalizer python setup.py build_exe --yes bdist_appimage --yes echo -e "setup.py build output:\n `ls build`" echo -e "setup.py dist output:\n `ls dist`" @@ -92,14 +94,18 @@ jobs: echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV # - copy code above to release.yml - + - name: Build Again + run: | + source venv/bin/activate + python setup.py build_exe --yes - name: Store AppImage - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: ${{ env.APPIMAGE_NAME }} path: dist/${{ env.APPIMAGE_NAME }} retention-days: 7 - name: Store .tar.gz - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: ${{ env.TAR_NAME }} path: dist/${{ env.TAR_NAME }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index b331c25506..6aeb477a22 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -14,9 +14,17 @@ name: "CodeQL" on: push: branches: [ main ] + paths: + - '**.py' + - '**.js' + - '.github/workflows/codeql-analysis.yml' pull_request: # The branches below must be a subset of the branches above branches: [ main ] + paths: + - '**.py' + - '**.js' + - '.github/workflows/codeql-analysis.yml' schedule: - cron: '44 8 * * 1' @@ -35,11 +43,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -50,7 +58,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v2 # â„šī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -64,4 +72,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 28adb50026..c20d244ad9 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -3,23 +3,29 @@ name: lint -on: [push, pull_request] +on: + push: + paths: + - '**.py' + pull_request: + paths: + - '**.py' jobs: - build: + flake8: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.9 - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: 3.9 - name: Install dependencies run: | python -m pip install --upgrade pip wheel - pip install flake8 pytest pytest-subtests + pip install flake8 if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with flake8 run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e9559f7856..42594721d0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,7 +8,6 @@ on: - '*.*.*' env: - SNI_VERSION: v0.0.88 ENEMIZER_VERSION: 7.1 APPIMAGETOOL_VERSION: 13 @@ -30,20 +29,20 @@ jobs: # build-release-windows: # this is done by hand because of signing # build-release-macos: # LF volunteer - build-release-ubuntu1804: - runs-on: ubuntu-18.04 + build-release-ubuntu2004: + runs-on: ubuntu-20.04 steps: - name: Set env run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV # - code below copied from build.yml - - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Install base dependencies run: | sudo apt update sudo apt -y install build-essential p7zip xz-utils wget libglib2.0-0 sudo apt -y install python3-gi libgirepository1.0-dev # should pull dependencies for gi installation below - name: Get a recent python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: '3.9' - name: Install build-time dependencies @@ -56,10 +55,6 @@ jobs: chmod a+rx appimagetool - name: Download run-time dependencies run: | - wget -nv https://github.com/alttpo/sni/releases/download/$SNI_VERSION/sni-$SNI_VERSION-manylinux2014-amd64.tar.xz - tar xf sni-*.tar.xz - rm sni-*.tar.xz - mv sni-* SNI wget -nv https://github.com/Ijwu/Enemizer/releases/download/$ENEMIZER_VERSION/ubuntu.16.04-x64.7z 7za x -oEnemizerCLI/ ubuntu.16.04-x64.7z - name: Build @@ -68,9 +63,8 @@ jobs: # charset-normalizer was somehow incomplete in the github runner "${{ env.PYTHON }}" -m venv venv source venv/bin/activate - "${{ env.PYTHON }}" -m pip install --upgrade pip PyGObject setuptools charset-normalizer - pip install -r requirements.txt - python setup.py build --yes bdist_appimage --yes + "${{ env.PYTHON }}" -m pip install --upgrade pip PyGObject charset-normalizer + python setup.py build_exe --yes bdist_appimage --yes echo -e "setup.py build output:\n `ls build`" echo -e "setup.py dist output:\n `ls dist`" cd dist && export APPIMAGE_NAME="`ls *.AppImage`" && cd .. diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index c86d637243..254d92dd6f 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -3,7 +3,25 @@ name: unittests -on: [push, pull_request] +on: + push: + paths: + - '**' + - '!docs/**' + - '!setup.py' + - '!*.iss' + - '!.gitignore' + - '!.github/workflows/**' + - '.github/workflows/unittests.yml' + pull_request: + paths: + - '**' + - '!docs/**' + - '!setup.py' + - '!*.iss' + - '!.gitignore' + - '!.github/workflows/**' + - '.github/workflows/unittests.yml' jobs: build: @@ -23,17 +41,19 @@ jobs: os: windows-latest - python: {version: '3.10'} # current os: windows-latest + - python: {version: '3.10'} # current + os: macos-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python.version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python.version }} - name: Install dependencies run: | - python -m pip install --upgrade pip wheel - pip install flake8 pytest pytest-subtests + python -m pip install --upgrade pip + pip install pytest pytest-subtests python ModuleUpdate.py --yes --force --append "WebHostLib/requirements.txt" - name: Unittests run: | diff --git a/.gitignore b/.gitignore index e269202db9..5f8ad6b917 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ *.apm3 *.apmc *.apz5 +*.aptloz *.pyc *.pyd *.sfc @@ -25,6 +26,7 @@ *multisave *.archipelago *.apsave +*.BIN build bundle/components.wxs @@ -51,6 +53,7 @@ Output Logs/ /setup.ini /installdelete.iss /data/user.kv +/datapackage # Byte-compiled / optimized / DLL files __pycache__/ @@ -138,6 +141,7 @@ ENV/ env.bak/ venv.bak/ .code-workspace +shell.nix # Spyder project settings .spyderproject @@ -167,6 +171,7 @@ cython_debug/ jdk*/ minecraft*/ minecraft_versions.json +!worlds/minecraft/ # pyenv .python-version diff --git a/AdventureClient.py b/AdventureClient.py new file mode 100644 index 0000000000..06eea5215c --- /dev/null +++ b/AdventureClient.py @@ -0,0 +1,516 @@ +import asyncio +import hashlib +import json +import time +import os +import bsdiff4 +import subprocess +import zipfile +from asyncio import StreamReader, StreamWriter, CancelledError +from typing import List + + +import Utils +from NetUtils import ClientStatus +from Utils import async_start +from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \ + get_base_parser +from worlds.adventure import AdventureDeltaPatch + +from worlds.adventure.Locations import base_location_id +from worlds.adventure.Rom import AdventureForeignItemInfo, AdventureAutoCollectLocation, BatNoTouchLocation +from worlds.adventure.Items import base_adventure_item_id, standard_item_max, item_table +from worlds.adventure.Offsets import static_item_element_size, connector_port_offset + +SYSTEM_MESSAGE_ID = 0 + +CONNECTION_TIMING_OUT_STATUS = \ + "Connection timing out. Please restart your emulator, then restart adventure_connector.lua" +CONNECTION_REFUSED_STATUS = \ + "Connection Refused. Please start your emulator and make sure adventure_connector.lua is running" +CONNECTION_RESET_STATUS = \ + "Connection was reset. Please restart your emulator, then restart adventure_connector.lua" +CONNECTION_TENTATIVE_STATUS = "Initial Connection Made" +CONNECTION_CONNECTED_STATUS = "Connected" +CONNECTION_INITIAL_STATUS = "Connection has not been initiated" + +SCRIPT_VERSION = 1 + + +class AdventureCommandProcessor(ClientCommandProcessor): + def __init__(self, ctx: CommonContext): + super().__init__(ctx) + + def _cmd_2600(self): + """Check 2600 Connection State""" + if isinstance(self.ctx, AdventureContext): + logger.info(f"2600 Status: {self.ctx.atari_status}") + + def _cmd_aconnect(self): + """Discard current atari 2600 connection state""" + if isinstance(self.ctx, AdventureContext): + self.ctx.atari_sync_task.cancel() + + +class AdventureContext(CommonContext): + command_processor = AdventureCommandProcessor + game = 'Adventure' + lua_connector_port: int = 17242 + + def __init__(self, server_address, password): + super().__init__(server_address, password) + self.freeincarnates_used: int = -1 + self.freeincarnate_pending: int = 0 + self.foreign_items: [AdventureForeignItemInfo] = [] + self.autocollect_items: [AdventureAutoCollectLocation] = [] + self.atari_streams: (StreamReader, StreamWriter) = None + self.atari_sync_task = None + self.messages = {} + self.locations_array = None + self.atari_status = CONNECTION_INITIAL_STATUS + self.awaiting_rom = False + self.display_msgs = True + self.deathlink_pending = False + self.set_deathlink = False + self.client_compatibility_mode = 0 + self.items_handling = 0b111 + self.checked_locations_sent: bool = False + self.port_offset = 0 + self.bat_no_touch_locations: [BatNoTouchLocation] = [] + self.local_item_locations = {} + self.dragon_speed_info = {} + + options = Utils.get_options() + self.display_msgs = options["adventure_options"]["display_msgs"] + + async def server_auth(self, password_requested: bool = False): + if password_requested and not self.password: + await super(AdventureContext, self).server_auth(password_requested) + if not self.auth: + self.auth = self.player_name + if not self.auth: + self.awaiting_rom = True + logger.info('Awaiting connection to adventure_connector to get Player information') + return + + await self.send_connect() + + def _set_message(self, msg: str, msg_id: int): + if self.display_msgs: + self.messages[(time.time(), msg_id)] = msg + + def on_package(self, cmd: str, args: dict): + if cmd == 'Connected': + self.locations_array = None + if Utils.get_options()["adventure_options"].get("death_link", False): + self.set_deathlink = True + async_start(self.get_freeincarnates_used()) + elif cmd == "RoomInfo": + self.seed_name = args['seed_name'] + elif cmd == 'Print': + msg = args['text'] + if ': !' not in msg: + self._set_message(msg, SYSTEM_MESSAGE_ID) + elif cmd == "ReceivedItems": + msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}" + self._set_message(msg, SYSTEM_MESSAGE_ID) + elif cmd == "Retrieved": + self.freeincarnates_used = args["keys"][f"adventure_{self.auth}_freeincarnates_used"] + if self.freeincarnates_used is None: + self.freeincarnates_used = 0 + self.freeincarnates_used += self.freeincarnate_pending + self.send_pending_freeincarnates() + elif cmd == "SetReply": + if args["key"] == f"adventure_{self.auth}_freeincarnates_used": + self.freeincarnates_used = args["value"] + if self.freeincarnates_used is None: + self.freeincarnates_used = 0 + self.freeincarnates_used += self.freeincarnate_pending + self.send_pending_freeincarnates() + + def on_deathlink(self, data: dict): + self.deathlink_pending = True + super().on_deathlink(data) + + def run_gui(self): + from kvui import GameManager + + class AdventureManager(GameManager): + logging_pairs = [ + ("Client", "Archipelago") + ] + base_title = "Archipelago Adventure Client" + + self.ui = AdventureManager(self) + self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + + async def get_freeincarnates_used(self): + if self.server and not self.server.socket.closed: + await self.send_msgs([{"cmd": "SetNotify", "keys": [f"adventure_{self.auth}_freeincarnates_used"]}]) + await self.send_msgs([{"cmd": "Get", "keys": [f"adventure_{self.auth}_freeincarnates_used"]}]) + + def send_pending_freeincarnates(self): + if self.freeincarnate_pending > 0: + async_start(self.send_pending_freeincarnates_impl(self.freeincarnate_pending)) + self.freeincarnate_pending = 0 + + async def send_pending_freeincarnates_impl(self, send_val: int) -> None: + await self.send_msgs([{"cmd": "Set", "key": f"adventure_{self.auth}_freeincarnates_used", + "default": 0, "want_reply": False, + "operations": [{"operation": "add", "value": send_val}]}]) + + async def used_freeincarnate(self) -> None: + if self.server and not self.server.socket.closed: + await self.send_msgs([{"cmd": "Set", "key": f"adventure_{self.auth}_freeincarnates_used", + "default": 0, "want_reply": True, + "operations": [{"operation": "add", "value": 1}]}]) + else: + self.freeincarnate_pending = self.freeincarnate_pending + 1 + + +def convert_item_id(ap_item_id: int): + static_item_index = ap_item_id - base_adventure_item_id + return static_item_index * static_item_element_size + + +def get_payload(ctx: AdventureContext): + current_time = time.time() + items = [] + dragon_speed_update = {} + diff_a_locked = ctx.diff_a_mode > 0 + diff_b_locked = ctx.diff_b_mode > 0 + freeincarnate_count = 0 + for item in ctx.items_received: + item_id_str = str(item.item) + if base_adventure_item_id < item.item <= standard_item_max: + items.append(convert_item_id(item.item)) + elif item_id_str in ctx.dragon_speed_info: + if item.item in dragon_speed_update: + last_index = len(ctx.dragon_speed_info[item_id_str]) - 1 + dragon_speed_update[item.item] = ctx.dragon_speed_info[item_id_str][last_index] + else: + dragon_speed_update[item.item] = ctx.dragon_speed_info[item_id_str][0] + elif item.item == item_table["Left Difficulty Switch"].id: + diff_a_locked = False + elif item.item == item_table["Right Difficulty Switch"].id: + diff_b_locked = False + elif item.item == item_table["Freeincarnate"].id: + freeincarnate_count = freeincarnate_count + 1 + freeincarnates_available = 0 + + if ctx.freeincarnates_used >= 0: + freeincarnates_available = freeincarnate_count - (ctx.freeincarnates_used + ctx.freeincarnate_pending) + ret = json.dumps( + { + "items": items, + "messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items() + if key[0] > current_time - 10}, + "deathlink": ctx.deathlink_pending, + "dragon_speeds": dragon_speed_update, + "difficulty_a_locked": diff_a_locked, + "difficulty_b_locked": diff_b_locked, + "freeincarnates_available": freeincarnates_available, + "bat_logic": ctx.bat_logic + } + ) + ctx.deathlink_pending = False + return ret + + +async def parse_locations(data: List, ctx: AdventureContext): + locations = data + + # for loc_name, loc_data in location_table.items(): + + # if flags["EventFlag"][280] & 1 and not ctx.finished_game: + # await ctx.send_msgs([ + # {"cmd": "StatusUpdate", + # "status": 30} + # ]) + # ctx.finished_game = True + if locations == ctx.locations_array: + return + ctx.locations_array = locations + if locations is not None: + await ctx.send_msgs([{"cmd": "LocationChecks", "locations": locations}]) + + +def send_ap_foreign_items(adventure_context): + foreign_item_json_list = [] + autocollect_item_json_list = [] + bat_no_touch_locations_json_list = [] + for fi in adventure_context.foreign_items: + foreign_item_json_list.append(fi.get_dict()) + for fi in adventure_context.autocollect_items: + autocollect_item_json_list.append(fi.get_dict()) + for ntl in adventure_context.bat_no_touch_locations: + bat_no_touch_locations_json_list.append(ntl.get_dict()) + payload = json.dumps( + { + "foreign_items": foreign_item_json_list, + "autocollect_items": autocollect_item_json_list, + "local_item_locations": adventure_context.local_item_locations, + "bat_no_touch_locations": bat_no_touch_locations_json_list + } + ) + print("sending foreign items") + msg = payload.encode() + (reader, writer) = adventure_context.atari_streams + writer.write(msg) + writer.write(b'\n') + + +def send_checked_locations_if_needed(adventure_context): + if not adventure_context.checked_locations_sent and adventure_context.checked_locations is not None: + if len(adventure_context.checked_locations) == 0: + return + checked_short_ids = [] + for location in adventure_context.checked_locations: + checked_short_ids.append(location - base_location_id) + print("Sending checked locations") + payload = json.dumps( + { + "checked_locations": checked_short_ids, + } + ) + msg = payload.encode() + (reader, writer) = adventure_context.atari_streams + writer.write(msg) + writer.write(b'\n') + adventure_context.checked_locations_sent = True + + +async def atari_sync_task(ctx: AdventureContext): + logger.info("Starting Atari 2600 connector. Use /2600 for status information") + while not ctx.exit_event.is_set(): + try: + error_status = None + if ctx.atari_streams: + (reader, writer) = ctx.atari_streams + msg = get_payload(ctx).encode() + writer.write(msg) + writer.write(b'\n') + try: + await asyncio.wait_for(writer.drain(), timeout=1.5) + try: + # Data will return a dict with 1+ fields + # 1. A keepalive response of the Players Name (always) + # 2. romhash field with sha256 hash of the ROM memory region + # 3. locations, messages, and deathLink + # 4. freeincarnate, to indicate a freeincarnate was used + data = await asyncio.wait_for(reader.readline(), timeout=5) + data_decoded = json.loads(data.decode()) + if 'scriptVersion' not in data_decoded or data_decoded['scriptVersion'] != SCRIPT_VERSION: + msg = "You are connecting with an incompatible Lua script version. Ensure your connector " \ + "Lua and AdventureClient are from the same Archipelago installation." + logger.info(msg, extra={'compact_gui': True}) + ctx.gui_error('Error', msg) + error_status = CONNECTION_RESET_STATUS + if ctx.seed_name and bytes(ctx.seed_name, encoding='ASCII') != ctx.seed_name_from_data: + msg = "The server is running a different multiworld than your client is. " \ + "(invalid seed_name)" + logger.info(msg, extra={'compact_gui': True}) + ctx.gui_error('Error', msg) + error_status = CONNECTION_RESET_STATUS + if 'romhash' in data_decoded: + if ctx.rom_hash.upper() != data_decoded['romhash'].upper(): + msg = "The rom hash does not match the client rom hash data" + print("got " + data_decoded['romhash']) + print("expected " + str(ctx.rom_hash)) + logger.info(msg, extra={'compact_gui': True}) + ctx.gui_error('Error', msg) + error_status = CONNECTION_RESET_STATUS + if ctx.auth is None: + ctx.auth = ctx.player_name + if ctx.awaiting_rom: + await ctx.server_auth(False) + if 'locations' in data_decoded and ctx.game and ctx.atari_status == CONNECTION_CONNECTED_STATUS \ + and not error_status and ctx.auth: + # Not just a keep alive ping, parse + async_start(parse_locations(data_decoded['locations'], ctx)) + if 'deathLink' in data_decoded and data_decoded['deathLink'] > 0 and 'DeathLink' in ctx.tags: + dragon_name = "a dragon" + if data_decoded['deathLink'] == 1: + dragon_name = "Rhindle" + elif data_decoded['deathLink'] == 2: + dragon_name = "Yorgle" + elif data_decoded['deathLink'] == 3: + dragon_name = "Grundle" + print (ctx.auth + " has been eaten by " + dragon_name ) + await ctx.send_death(ctx.auth + " has been eaten by " + dragon_name) + # TODO - also if player reincarnates with a dragon onscreen ' dies to avoid being eaten by ' + if 'victory' in data_decoded and not ctx.finished_game: + await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) + ctx.finished_game = True + if 'freeincarnate' in data_decoded: + await ctx.used_freeincarnate() + if ctx.set_deathlink: + await ctx.update_death_link(True) + send_checked_locations_if_needed(ctx) + except asyncio.TimeoutError: + logger.debug("Read Timed Out, Reconnecting") + error_status = CONNECTION_TIMING_OUT_STATUS + writer.close() + ctx.atari_streams = None + except ConnectionResetError as e: + logger.debug("Read failed due to Connection Lost, Reconnecting") + error_status = CONNECTION_RESET_STATUS + writer.close() + ctx.atari_streams = None + except TimeoutError: + logger.debug("Connection Timed Out, Reconnecting") + error_status = CONNECTION_TIMING_OUT_STATUS + writer.close() + ctx.atari_streams = None + except ConnectionResetError: + logger.debug("Connection Lost, Reconnecting") + error_status = CONNECTION_RESET_STATUS + writer.close() + ctx.atari_streams = None + except CancelledError: + logger.debug("Connection Cancelled, Reconnecting") + error_status = CONNECTION_RESET_STATUS + writer.close() + ctx.atari_streams = None + pass + except Exception as e: + print("unknown exception " + e) + raise + if ctx.atari_status == CONNECTION_TENTATIVE_STATUS: + if not error_status: + logger.info("Successfully Connected to 2600") + ctx.atari_status = CONNECTION_CONNECTED_STATUS + ctx.checked_locations_sent = False + send_ap_foreign_items(ctx) + send_checked_locations_if_needed(ctx) + else: + ctx.atari_status = f"Was tentatively connected but error occurred: {error_status}" + elif error_status: + ctx.atari_status = error_status + logger.info("Lost connection to 2600 and attempting to reconnect. Use /2600 for status updates") + else: + try: + port = ctx.lua_connector_port + ctx.port_offset + logger.debug(f"Attempting to connect to 2600 on port {port}") + print(f"Attempting to connect to 2600 on port {port}") + ctx.atari_streams = await asyncio.wait_for( + asyncio.open_connection("localhost", + port), + timeout=10) + ctx.atari_status = CONNECTION_TENTATIVE_STATUS + except TimeoutError: + logger.debug("Connection Timed Out, Trying Again") + ctx.atari_status = CONNECTION_TIMING_OUT_STATUS + continue + except ConnectionRefusedError: + logger.debug("Connection Refused, Trying Again") + ctx.atari_status = CONNECTION_REFUSED_STATUS + continue + except CancelledError: + pass + except CancelledError: + pass + print("exiting atari sync task") + + +async def run_game(romfile): + auto_start = Utils.get_options()["adventure_options"].get("rom_start", True) + rom_args = Utils.get_options()["adventure_options"].get("rom_args") + if auto_start is True: + import webbrowser + webbrowser.open(romfile) + elif os.path.isfile(auto_start): + open_args = [auto_start, romfile] + if rom_args is not None: + open_args.insert(1, rom_args) + subprocess.Popen(open_args, + stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + +async def patch_and_run_game(patch_file, ctx): + base_name = os.path.splitext(patch_file)[0] + comp_path = base_name + '.a26' + try: + base_rom = AdventureDeltaPatch.get_source_data() + except Exception as msg: + logger.info(msg, extra={'compact_gui': True}) + ctx.gui_error('Error', msg) + + with open(Utils.local_path("data", "adventure_basepatch.bsdiff4"), "rb") as file: + basepatch = bytes(file.read()) + + base_patched_rom_data = bsdiff4.patch(base_rom, basepatch) + + with zipfile.ZipFile(patch_file, 'r') as patch_archive: + if not AdventureDeltaPatch.check_version(patch_archive): + logger.error("apadvn version doesn't match this client. Make sure your generator and client are the same") + raise Exception("apadvn version doesn't match this client.") + + ctx.foreign_items = AdventureDeltaPatch.read_foreign_items(patch_archive) + ctx.autocollect_items = AdventureDeltaPatch.read_autocollect_items(patch_archive) + ctx.local_item_locations = AdventureDeltaPatch.read_local_item_locations(patch_archive) + ctx.dragon_speed_info = AdventureDeltaPatch.read_dragon_speed_info(patch_archive) + ctx.seed_name_from_data, ctx.player_name = AdventureDeltaPatch.read_rom_info(patch_archive) + ctx.diff_a_mode, ctx.diff_b_mode = AdventureDeltaPatch.read_difficulty_switch_info(patch_archive) + ctx.bat_logic = AdventureDeltaPatch.read_bat_logic(patch_archive) + ctx.bat_no_touch_locations = AdventureDeltaPatch.read_bat_no_touch(patch_archive) + ctx.rom_deltas = AdventureDeltaPatch.read_rom_deltas(patch_archive) + ctx.auth = ctx.player_name + + patched_rom_data = AdventureDeltaPatch.apply_rom_deltas(base_patched_rom_data, ctx.rom_deltas) + rom_hash = hashlib.sha256() + rom_hash.update(patched_rom_data) + ctx.rom_hash = rom_hash.hexdigest() + ctx.port_offset = patched_rom_data[connector_port_offset] + + with open(comp_path, "wb") as patched_rom_file: + patched_rom_file.write(patched_rom_data) + + async_start(run_game(comp_path)) + + +if __name__ == '__main__': + + Utils.init_logging("AdventureClient") + + async def main(): + parser = get_base_parser() + parser.add_argument('patch_file', default="", type=str, nargs="?", + help='Path to an ADVNTURE.BIN rom file') + parser.add_argument('port', default=17242, type=int, nargs="?", + help='port for adventure_connector connection') + args = parser.parse_args() + + ctx = AdventureContext(args.connect, args.password) + ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") + if gui_enabled: + ctx.run_gui() + ctx.run_cli() + ctx.atari_sync_task = asyncio.create_task(atari_sync_task(ctx), name="Adventure Sync") + + if args.patch_file: + ext = args.patch_file.split(".")[len(args.patch_file.split(".")) - 1].lower() + if ext == "apadvn": + logger.info("apadvn file supplied, beginning patching process...") + async_start(patch_and_run_game(args.patch_file, ctx)) + else: + logger.warning(f"Unknown patch file extension {ext}") + if args.port is int: + ctx.lua_connector_port = args.port + + await ctx.exit_event.wait() + ctx.server_address = None + + await ctx.shutdown() + + if ctx.atari_sync_task: + await ctx.atari_sync_task + print("finished atari_sync_task (main)") + + + import colorama + + colorama.init() + + asyncio.run(main()) + colorama.deinit() diff --git a/BaseClasses.py b/BaseClasses.py index 707f7bd76e..4e012429c2 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -2,13 +2,12 @@ from __future__ import annotations import copy import functools -import json import logging import random import secrets import typing # this can go away when Python 3.8 support is dropped from argparse import Namespace -from collections import OrderedDict, Counter, deque +from collections import OrderedDict, Counter, deque, ChainMap from enum import IntEnum, IntFlag from typing import List, Dict, Optional, Set, Iterable, Union, Any, Tuple, TypedDict, Callable, NamedTuple @@ -72,6 +71,11 @@ class MultiWorld(): completion_condition: Dict[int, Callable[[CollectionState], bool]] indirect_connections: Dict[Region, Set[Entrance]] exclude_locations: Dict[int, Options.ExcludeLocations] + priority_locations: Dict[int, Options.PriorityLocations] + start_inventory: Dict[int, Options.StartInventory] + start_hints: Dict[int, Options.StartHints] + start_location_hints: Dict[int, Options.StartLocationHints] + item_links: Dict[int, Options.ItemLinks] game: Dict[int, str] @@ -332,7 +336,7 @@ class MultiWorld(): return self.player_name[player] def get_file_safe_player_name(self, player: int) -> str: - return ''.join(c for c in self.get_player_name(player) if c not in '<>:"/\\|?*') + return Utils.get_file_safe_name(self.get_player_name(player)) def get_out_file_name_base(self, player: int) -> str: """ the base name (without file extension) for each player's output file for a seed """ @@ -761,169 +765,9 @@ class CollectionState(): found += self.prog_items[item_name, player] return found - def can_buy_unlimited(self, item: str, player: int) -> bool: - return any(shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(self) for - shop in self.multiworld.shops) - - def can_buy(self, item: str, player: int) -> bool: - return any(shop.region.player == player and shop.has(item) and shop.region.can_reach(self) for - shop in self.multiworld.shops) - def item_count(self, item: str, player: int) -> int: return self.prog_items[item, player] - def has_triforce_pieces(self, count: int, player: int) -> bool: - return self.item_count('Triforce Piece', player) + self.item_count('Power Star', player) >= count - - def has_crystals(self, count: int, player: int) -> bool: - found: int = 0 - for crystalnumber in range(1, 8): - found += self.prog_items[f"Crystal {crystalnumber}", player] - if found >= count: - return True - return False - - def can_lift_rocks(self, player: int): - return self.has('Power Glove', player) or self.has('Titans Mitts', player) - - def bottle_count(self, player: int) -> int: - return min(self.multiworld.difficulty_requirements[player].progressive_bottle_limit, - self.count_group("Bottles", player)) - - def has_hearts(self, player: int, count: int) -> int: - # Warning: This only considers items that are marked as advancement items - return self.heart_count(player) >= count - - def heart_count(self, player: int) -> int: - # Warning: This only considers items that are marked as advancement items - diff = self.multiworld.difficulty_requirements[player] - return min(self.item_count('Boss Heart Container', player), diff.boss_heart_container_limit) \ - + self.item_count('Sanctuary Heart Container', player) \ - + min(self.item_count('Piece of Heart', player), diff.heart_piece_limit) // 4 \ - + 3 # starting hearts - - def can_lift_heavy_rocks(self, player: int) -> bool: - return self.has('Titans Mitts', player) - - def can_extend_magic(self, player: int, smallmagic: int = 16, - fullrefill: bool = False): # This reflects the total magic Link has, not the total extra he has. - basemagic = 8 - if self.has('Magic Upgrade (1/4)', player): - basemagic = 32 - elif self.has('Magic Upgrade (1/2)', player): - basemagic = 16 - if self.can_buy_unlimited('Green Potion', player) or self.can_buy_unlimited('Blue Potion', player): - if self.multiworld.item_functionality[player] == 'hard' and not fullrefill: - basemagic = basemagic + int(basemagic * 0.5 * self.bottle_count(player)) - elif self.multiworld.item_functionality[player] == 'expert' and not fullrefill: - basemagic = basemagic + int(basemagic * 0.25 * self.bottle_count(player)) - else: - basemagic = basemagic + basemagic * self.bottle_count(player) - return basemagic >= smallmagic - - def can_kill_most_things(self, player: int, enemies: int = 5) -> bool: - return (self.has_melee_weapon(player) - or self.has('Cane of Somaria', player) - or (self.has('Cane of Byrna', player) and (enemies < 6 or self.can_extend_magic(player))) - or self.can_shoot_arrows(player) - or self.has('Fire Rod', player) - or (self.has('Bombs (10)', player) and enemies < 6)) - - def can_shoot_arrows(self, player: int) -> bool: - if self.multiworld.retro_bow[player]: - return (self.has('Bow', player) or self.has('Silver Bow', player)) and self.can_buy('Single Arrow', player) - return self.has('Bow', player) or self.has('Silver Bow', player) - - def can_get_good_bee(self, player: int) -> bool: - cave = self.multiworld.get_region('Good Bee Cave', player) - return ( - self.has_group("Bottles", player) and - self.has('Bug Catching Net', player) and - (self.has('Pegasus Boots', player) or (self.has_sword(player) and self.has('Quake', player))) and - cave.can_reach(self) and - self.is_not_bunny(cave, player) - ) - - def can_retrieve_tablet(self, player: int) -> bool: - return self.has('Book of Mudora', player) and (self.has_beam_sword(player) or - (self.multiworld.swordless[player] and - self.has("Hammer", player))) - - def has_sword(self, player: int) -> bool: - return self.has('Fighter Sword', player) \ - or self.has('Master Sword', player) \ - or self.has('Tempered Sword', player) \ - or self.has('Golden Sword', player) - - def has_beam_sword(self, player: int) -> bool: - return self.has('Master Sword', player) or self.has('Tempered Sword', player) or self.has('Golden Sword', - player) - - def has_melee_weapon(self, player: int) -> bool: - return self.has_sword(player) or self.has('Hammer', player) - - def has_fire_source(self, player: int) -> bool: - return self.has('Fire Rod', player) or self.has('Lamp', player) - - def can_melt_things(self, player: int) -> bool: - return self.has('Fire Rod', player) or \ - (self.has('Bombos', player) and - (self.multiworld.swordless[player] or - self.has_sword(player))) - - def can_avoid_lasers(self, player: int) -> bool: - return self.has('Mirror Shield', player) or self.has('Cane of Byrna', player) or self.has('Cape', player) - - def is_not_bunny(self, region: Region, player: int) -> bool: - if self.has('Moon Pearl', player): - return True - - return region.is_light_world if self.multiworld.mode[player] != 'inverted' else region.is_dark_world - - def can_reach_light_world(self, player: int) -> bool: - if True in [i.is_light_world for i in self.reachable_regions[player]]: - return True - return False - - def can_reach_dark_world(self, player: int) -> bool: - if True in [i.is_dark_world for i in self.reachable_regions[player]]: - return True - return False - - def has_misery_mire_medallion(self, player: int) -> bool: - return self.has(self.multiworld.required_medallions[player][0], player) - - def has_turtle_rock_medallion(self, player: int) -> bool: - return self.has(self.multiworld.required_medallions[player][1], player) - - def can_boots_clip_lw(self, player: int) -> bool: - if self.multiworld.mode[player] == 'inverted': - return self.has('Pegasus Boots', player) and self.has('Moon Pearl', player) - return self.has('Pegasus Boots', player) - - def can_boots_clip_dw(self, player: int) -> bool: - if self.multiworld.mode[player] != 'inverted': - return self.has('Pegasus Boots', player) and self.has('Moon Pearl', player) - return self.has('Pegasus Boots', player) - - def can_get_glitched_speed_lw(self, player: int) -> bool: - rules = [self.has('Pegasus Boots', player), any([self.has('Hookshot', player), self.has_sword(player)])] - if self.multiworld.mode[player] == 'inverted': - rules.append(self.has('Moon Pearl', player)) - return all(rules) - - def can_superbunny_mirror_with_sword(self, player: int) -> bool: - return self.has('Magic Mirror', player) and self.has_sword(player) - - def can_get_glitched_speed_dw(self, player: int) -> bool: - rules = [self.has('Pegasus Boots', player), any([self.has('Hookshot', player), self.has_sword(player)])] - if self.multiworld.mode[player] != 'inverted': - rules.append(self.has('Moon Pearl', player)) - return all(rules) - - def can_bomb_clip(self, region: Region, player: int) -> bool: - return self.is_not_bunny(region, player) and self.has('Pegasus Boots', player) - def collect(self, item: Item, event: bool = False, location: Optional[Location] = None) -> bool: if location: self.locations_checked.add(location) @@ -959,12 +803,6 @@ class Region: exits: List[Entrance] locations: List[Location] dungeon: Optional[Dungeon] = None - shop: Optional = None - - # LttP specific. TODO: move to a LttPRegion - # will be set after making connections. - is_light_world: bool = False - is_dark_world: bool = False def __init__(self, name: str, player: int, multiworld: MultiWorld, hint: Optional[str] = None): self.name = name @@ -1129,7 +967,7 @@ class Location: self.parent_region = parent def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool: - return (self.always_allow(state, item) + return ((self.always_allow(state, item) and item.name not in state.multiworld.non_local_items[item.player]) or ((self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful)) and self.item_rule(item) and (not check_access or self.can_reach(state)))) @@ -1371,7 +1209,7 @@ class Spoiler(): raise RuntimeError(f'Not all required items reachable. Unreachable locations: {required_locations}') # we can finally output our playthrough - self.playthrough = {"0": sorted([str(item) for item in + self.playthrough = {"0": sorted([self.multiworld.get_name_string_for_object(item) for item in chain.from_iterable(multiworld.precollected_items.values()) if item.advancement])} @@ -1429,7 +1267,7 @@ class Spoiler(): res = getattr(self.multiworld, option_key)[player] display_name = getattr(option_obj, "display_name", option_key) try: - outfile.write(f'{display_name + ":":33}{res.get_current_option_name()}\n') + outfile.write(f'{display_name + ":":33}{res.current_option_name}\n') except: raise Exception @@ -1446,12 +1284,11 @@ class Spoiler(): if self.multiworld.players > 1: outfile.write('\nPlayer %d: %s\n' % (player, self.multiworld.get_player_name(player))) outfile.write('Game: %s\n' % self.multiworld.game[player]) - for f_option, option in Options.per_game_common_options.items(): + + options = ChainMap(Options.per_game_common_options, self.multiworld.worlds[player].option_definitions) + for f_option, option in options.items(): write_option(f_option, option) - options = self.multiworld.worlds[player].option_definitions - if options: - for f_option, option in options.items(): - write_option(f_option, option) + AutoWorld.call_single(self.multiworld, "write_spoiler_header", player, outfile) if self.entrances: @@ -1465,7 +1302,7 @@ class Spoiler(): AutoWorld.call_all(self.multiworld, "write_spoiler", outfile) locations = [(str(location), str(location.item) if location.item is not None else "Nothing") - for location in self.multiworld.get_locations()] + for location in self.multiworld.get_locations() if location.show_in_spoiler] outfile.write('\n\nLocations:\n\n') outfile.write('\n'.join( ['%s: %s' % (location, item) for location, item in locations])) diff --git a/CommonClient.py b/CommonClient.py index 92f8d76a66..4892f69f06 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -63,19 +63,22 @@ class ClientCommandProcessor(CommandProcessor): def _cmd_received(self) -> bool: """List all received items""" - logger.info(f'{len(self.ctx.items_received)} received items:') + self.output(f'{len(self.ctx.items_received)} received items:') for index, item in enumerate(self.ctx.items_received, 1): self.output(f"{self.ctx.item_names[item.item]} from {self.ctx.player_names[item.player]}") return True - def _cmd_missing(self) -> bool: - """List all missing location checks, from your local game state""" + def _cmd_missing(self, filter_text = "") -> bool: + """List all missing location checks, from your local game state. + Can be given text, which will be used as filter.""" if not self.ctx.game: self.output("No game set, cannot determine missing checks.") return False count = 0 checked_count = 0 for location, location_id in AutoWorldRegister.world_types[self.ctx.game].location_name_to_id.items(): + if filter_text and filter_text not in location: + continue if location_id < 0: continue if location_id not in self.ctx.locations_checked: @@ -136,7 +139,7 @@ class CommonContext: items_handling: typing.Optional[int] = None want_slot_data: bool = True # should slot_data be retrieved via Connect - # datapackage + # data package # Contents in flux until connection to server is made, to download correct data for this multiworld. item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})') location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})') @@ -223,7 +226,7 @@ class CommonContext: self.watcher_event = asyncio.Event() self.jsontotextparser = JSONtoTextParser(self) - self.update_datapackage(network_data_package) + self.update_data_package(network_data_package) # execution self.keep_alive_task = asyncio.create_task(keep_alive(self), name="Bouncy") @@ -399,32 +402,40 @@ class CommonContext: self.input_task.cancel() # DataPackage - async def prepare_datapackage(self, relevant_games: typing.Set[str], - remote_datepackage_versions: typing.Dict[str, int]): + async def prepare_data_package(self, relevant_games: typing.Set[str], + remote_date_package_versions: typing.Dict[str, int], + remote_data_package_checksums: typing.Dict[str, str]): """Validate that all data is present for the current multiworld. Download, assimilate and cache missing data from the server.""" # by documentation any game can use Archipelago locations/items -> always relevant relevant_games.add("Archipelago") - cache_package = Utils.persistent_load().get("datapackage", {}).get("games", {}) needed_updates: typing.Set[str] = set() for game in relevant_games: - if game not in remote_datepackage_versions: + if game not in remote_date_package_versions and game not in remote_data_package_checksums: continue - remote_version: int = remote_datepackage_versions[game] - if remote_version == 0: # custom datapackage for this game + remote_version: int = remote_date_package_versions.get(game, 0) + remote_checksum: typing.Optional[str] = remote_data_package_checksums.get(game) + + if remote_version == 0 and not remote_checksum: # custom data package and no checksum for this game needed_updates.add(game) continue + local_version: int = network_data_package["games"].get(game, {}).get("version", 0) + local_checksum: typing.Optional[str] = network_data_package["games"].get(game, {}).get("checksum") # no action required if local version is new enough - if remote_version > local_version: - cache_version: int = cache_package.get(game, {}).get("version", 0) + if (not remote_checksum and (remote_version > local_version or remote_version == 0)) \ + or remote_checksum != local_checksum: + cached_game = Utils.load_data_package_for_checksum(game, remote_checksum) + cache_version: int = cached_game.get("version", 0) + cache_checksum: typing.Optional[str] = cached_game.get("checksum") # download remote version if cache is not new enough - if remote_version > cache_version: + if (not remote_checksum and (remote_version > cache_version or remote_version == 0)) \ + or remote_checksum != cache_checksum: needed_updates.add(game) else: - self.update_game(cache_package[game]) + self.update_game(cached_game) if needed_updates: await self.send_msgs([{"cmd": "GetDataPackage", "games": list(needed_updates)}]) @@ -434,15 +445,17 @@ class CommonContext: for location_name, location_id in game_package["location_name_to_id"].items(): self.location_names[location_id] = location_name - def update_datapackage(self, data_package: dict): - for game, gamedata in data_package["games"].items(): - self.update_game(gamedata) + def update_data_package(self, data_package: dict): + for game, game_data in data_package["games"].items(): + self.update_game(game_data) - def consume_network_datapackage(self, data_package: dict): - self.update_datapackage(data_package) + def consume_network_data_package(self, data_package: dict): + self.update_data_package(data_package) current_cache = Utils.persistent_load().get("datapackage", {}).get("games", {}) current_cache.update(data_package["games"]) Utils.persistent_store("datapackage", "games", current_cache) + for game, game_data in data_package["games"].items(): + Utils.store_data_package_for_checksum(game, game_data) # DeathLink hooks @@ -661,14 +674,16 @@ async def process_server_cmd(ctx: CommonContext, args: dict): current_team = network_player.team logger.info(' %s (Player %d)' % (network_player.alias, network_player.slot)) - # update datapackage - await ctx.prepare_datapackage(set(args["games"]), args["datapackage_versions"]) + # update data package + data_package_versions = args.get("datapackage_versions", {}) + data_package_checksums = args.get("datapackage_checksums", {}) + await ctx.prepare_data_package(set(args["games"]), data_package_versions, data_package_checksums) await ctx.server_auth(args['password']) elif cmd == 'DataPackage': logger.info("Got new ID/Name DataPackage") - ctx.consume_network_datapackage(args['data']) + ctx.consume_network_data_package(args['data']) elif cmd == 'ConnectionRefused': errors = args["errors"] diff --git a/Fill.py b/Fill.py index ac3ae8fc6d..6fa5ecb00d 100644 --- a/Fill.py +++ b/Fill.py @@ -1,11 +1,10 @@ -import logging -import typing import collections import itertools +import logging +import typing from collections import Counter, deque -from BaseClasses import CollectionState, Location, LocationProgressType, MultiWorld, Item, ItemClassification - +from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld from worlds.AutoWorld import call_all from worlds.generic.Rules import add_item_rule @@ -23,15 +22,27 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item] def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: typing.List[Location], - itempool: typing.List[Item], single_player_placement: bool = False, lock: bool = False, + item_pool: typing.List[Item], single_player_placement: bool = False, lock: bool = False, swap: bool = True, on_place: typing.Optional[typing.Callable[[Location], None]] = None, - allow_partial: bool = False) -> None: + allow_partial: bool = False, allow_excluded: bool = False) -> None: + """ + :param world: Multiworld to be filled. + :param base_state: State assumed before fill. + :param locations: Locations to be filled with item_pool + :param item_pool: Items to fill into the locations + :param single_player_placement: if true, can speed up placement if everything belongs to a single player + :param lock: locations are set to locked as they are filled + :param swap: if true, swaps of already place items are done in the event of a dead end + :param on_place: callback that is called when a placement happens + :param allow_partial: only place what is possible. Remaining items will be in the item_pool list. + :param allow_excluded: if true and placement fails, it is re-attempted while ignoring excluded on Locations + """ unplaced_items: typing.List[Item] = [] placements: typing.List[Location] = [] swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter() reachable_items: typing.Dict[int, typing.Deque[Item]] = {} - for item in itempool: + for item in item_pool: reachable_items.setdefault(item.player, deque()).append(item) while any(reachable_items.values()) and locations: @@ -39,9 +50,9 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: items_to_place = [items.pop() for items in reachable_items.values() if items] for item in items_to_place: - itempool.remove(item) + item_pool.remove(item) maximum_exploration_state = sweep_from_pool( - base_state, itempool + unplaced_items) + base_state, item_pool + unplaced_items) has_beaten_game = world.has_beaten_game(maximum_exploration_state) @@ -111,7 +122,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: reachable_items[placed_item.player].appendleft( placed_item) - itempool.append(placed_item) + item_pool.append(placed_item) break @@ -133,6 +144,21 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: if on_place: on_place(spot_to_fill) + if allow_excluded: + # check if partial fill is the result of excluded locations, in which case retry + excluded_locations = [ + location for location in locations + if location.progress_type == location.progress_type.EXCLUDED and not location.item + ] + if excluded_locations: + for location in excluded_locations: + location.progress_type = location.progress_type.DEFAULT + fill_restrictive(world, base_state, excluded_locations, unplaced_items, single_player_placement, lock, + swap, on_place, allow_partial, False) + for location in excluded_locations: + if not location.item: + location.progress_type = location.progress_type.EXCLUDED + if not allow_partial and len(unplaced_items) > 0 and len(locations) > 0: # There are leftover unplaceable items and locations that won't accept them if world.can_beat_game(): @@ -142,7 +168,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: raise FillError(f'No more spots to place {unplaced_items}, locations {locations} are invalid. ' f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}') - itempool.extend(unplaced_items) + item_pool.extend(unplaced_items) def remaining_fill(world: MultiWorld, @@ -499,16 +525,16 @@ def balance_multiworld_progression(world: MultiWorld) -> None: checked_locations: typing.Set[Location] = set() unchecked_locations: typing.Set[Location] = set(world.get_locations()) - reachable_locations_count: typing.Dict[int, int] = { - player: 0 - for player in world.player_ids - if len(world.get_filled_locations(player)) != 0 - } total_locations_count: typing.Counter[int] = Counter( location.player for location in world.get_locations() if not location.locked ) + reachable_locations_count: typing.Dict[int, int] = { + player: 0 + for player in world.player_ids + if total_locations_count[player] and len(world.get_filled_locations(player)) != 0 + } balanceable_players = { player: balanceable_players[player] for player in balanceable_players @@ -525,6 +551,10 @@ def balance_multiworld_progression(world: MultiWorld) -> None: def item_percentage(player: int, num: int) -> float: return num / total_locations_count[player] + # If there are no locations that aren't locked, there's no point in attempting to balance progression. + if len(total_locations_count) == 0: + return + while True: # Gather non-locked locations. # This ensures that only shuffled locations get counted for progression balancing, @@ -798,7 +828,6 @@ def distribute_planned(world: MultiWorld) -> None: for player in worlds: locations += non_early_locations[player] - block['locations'] = locations if not block['count']: @@ -840,8 +869,7 @@ def distribute_planned(world: MultiWorld) -> None: maxcount = placement['count']['target'] from_pool = placement['from_pool'] - candidates = list(location for location in world.get_unfilled_locations_for_players(locations, - worlds)) + candidates = list(world.get_unfilled_locations_for_players(locations, sorted(worlds))) world.random.shuffle(candidates) world.random.shuffle(items) count = 0 diff --git a/Generate.py b/Generate.py index dadabd7ac6..afb34f11c6 100644 --- a/Generate.py +++ b/Generate.py @@ -107,7 +107,7 @@ def main(args=None, callback=ERmain): player_files = {} for file in os.scandir(args.player_files_path): fname = file.name - if file.is_file() and not file.name.startswith(".") and \ + if file.is_file() and not fname.startswith(".") and \ os.path.join(args.player_files_path, fname) not in {args.meta_file_path, args.weights_file_path}: path = os.path.join(args.player_files_path, fname) try: diff --git a/KH2Client.py b/KH2Client.py new file mode 100644 index 0000000000..5223d8a111 --- /dev/null +++ b/KH2Client.py @@ -0,0 +1,906 @@ +import os +import asyncio +import ModuleUpdate +import json +import Utils +from pymem import pymem +from worlds.kh2.Items import exclusionItem_table, CheckDupingItems +from worlds.kh2 import all_locations, item_dictionary_table, exclusion_table + +from worlds.kh2.WorldLocations import * + +from worlds import network_data_package + +if __name__ == "__main__": + Utils.init_logging("KH2Client", exception_logger="Client") + +from NetUtils import ClientStatus +from CommonClient import gui_enabled, logger, get_base_parser, ClientCommandProcessor, \ + CommonContext, server_loop + +ModuleUpdate.update() + +kh2_loc_name_to_id = network_data_package["games"]["Kingdom Hearts 2"]["location_name_to_id"] + + +# class KH2CommandProcessor(ClientCommandProcessor): + + +class KH2Context(CommonContext): + # command_processor: int = KH2CommandProcessor + game = "Kingdom Hearts 2" + items_handling = 0b101 # Indicates you get items sent from other worlds. + + def __init__(self, server_address, password): + super(KH2Context, self).__init__(server_address, password) + self.kh2LocalItems = None + self.ability = None + self.growthlevel = None + self.KH2_sync_task = None + self.syncing = False + self.kh2connected = False + self.serverconneced = False + self.item_name_to_data = {name: data for name, data, in item_dictionary_table.items()} + self.location_name_to_data = {name: data for name, data, in all_locations.items()} + self.lookup_id_to_item: typing.Dict[int, str] = {data.code: item_name for item_name, data in + item_dictionary_table.items() if data.code} + self.lookup_id_to_Location: typing.Dict[int, str] = {data.code: item_name for item_name, data in + all_locations.items() if data.code} + self.location_name_to_worlddata = {name: data for name, data, in all_world_locations.items()} + + self.location_table = {} + self.collectible_table = {} + self.collectible_override_flags_address = 0 + self.collectible_offsets = {} + self.sending = [] + # flag for if the player has gotten their starting inventory from the server + self.hasStartingInvo = False + # list used to keep track of locations+items player has. Used for disoneccting + self.kh2seedsave = {"checked_locations": {"0": []}, + "starting_inventory": self.hasStartingInvo, + + # Character: [back of invo, front of invo] + "SoraInvo": [0x25CC, 0x2546], + "DonaldInvo": [0x2678, 0x2658], + "GoofyInvo": [0x278E, 0x276C], + "AmountInvo": { + "ServerItems": { + "Ability": {}, + "Amount": {}, + "Growth": {"High Jump": 0, "Quick Run": 0, "Dodge Roll": 0, "Aerial Dodge": 0, + "Glide": 0}, + "Bitmask": [], + "Weapon": {"Sora": [], "Donald": [], "Goofy": []}, + "Equipment": [], + "Magic": {}, + "StatIncrease": {}, + "Boost": {}, + }, + "LocalItems": { + "Ability": {}, + "Amount": {}, + "Growth": {"High Jump": 0, "Quick Run": 0, "Dodge Roll": 0, + "Aerial Dodge": 0, "Glide": 0}, + "Bitmask": [], + "Weapon": {"Sora": [], "Donald": [], "Goofy": []}, + "Equipment": [], + "Magic": {}, + "StatIncrease": {}, + "Boost": {}, + }}, + # 1,3,255 are in this list in case the player gets locations in those "worlds" and I need to still have them checked + "worldIdChecks": { + "1": [], # world of darkness (story cutscenes) + "2": [], + "3": [], # destiny island doesn't have checks to ima put tt checks here + "4": [], + "5": [], + "6": [], + "7": [], + "8": [], + "9": [], + "10": [], + "11": [], + # atlantica isn't a supported world. if you go in atlantica it will check dc + "12": [], + "13": [], + "14": [], + "15": [], + # world map, but you only go to the world map while on the way to goa so checking hb + "16": [], + "17": [], + "18": [], + "255": [], # starting screen + }, + "Levels": { + "SoraLevel": 0, + "ValorLevel": 0, + "WisdomLevel": 0, + "LimitLevel": 0, + "MasterLevel": 0, + "FinalLevel": 0, + }, + "SoldEquipment": [], + "SoldBoosts": {"Power Boost": 0, + "Magic Boost": 0, + "Defense Boost": 0, + "AP Boost": 0} + } + self.slotDataProgressionNames = {} + self.kh2seedname = None + self.kh2slotdata = None + self.itemamount = {} + # sora equipped, valor equipped, master equipped, final equipped + self.keybladeAnchorList = (0x24F0, 0x32F4, 0x339C, 0x33D4) + if "localappdata" in os.environ: + self.game_communication_path = os.path.expandvars(r"%localappdata%\KH2AP") + self.amountOfPieces = 0 + # hooked object + self.kh2 = None + self.ItemIsSafe = False + self.game_connected = False + self.finalxemnas = False + self.worldid = { + # 1: {}, # world of darkness (story cutscenes) + 2: TT_Checks, + # 3: {}, # destiny island doesn't have checks to ima put tt checks here + 4: HB_Checks, + 5: BC_Checks, + 6: Oc_Checks, + 7: AG_Checks, + 8: LoD_Checks, + 9: HundredAcreChecks, + 10: PL_Checks, + 11: DC_Checks, # atlantica isn't a supported world. if you go in atlantica it will check dc + 12: DC_Checks, + 13: TR_Checks, + 14: HT_Checks, + 15: HB_Checks, # world map, but you only go to the world map while on the way to goa so checking hb + 16: PR_Checks, + 17: SP_Checks, + 18: TWTNW_Checks, + # 255: {}, # starting screen + } + # 0x2A09C00+0x40 is the sve anchor. +1 is the last saved room + self.sveroom = 0x2A09C00 + 0x41 + # 0 not in battle 1 in yellow battle 2 red battle #short + self.inBattle = 0x2A0EAC4 + 0x40 + self.onDeath = 0xAB9078 + # PC Address anchors + self.Now = 0x0714DB8 + self.Save = 0x09A70B0 + self.Sys3 = 0x2A59DF0 + self.Bt10 = 0x2A74880 + self.BtlEnd = 0x2A0D3E0 + self.Slot1 = 0x2A20C98 + + self.chest_set = set(exclusion_table["Chests"]) + + self.keyblade_set = set(CheckDupingItems["Weapons"]["Keyblades"]) + self.staff_set = set(CheckDupingItems["Weapons"]["Staffs"]) + self.shield_set = set(CheckDupingItems["Weapons"]["Shields"]) + + self.all_weapons = self.keyblade_set.union(self.staff_set).union(self.shield_set) + + self.equipment_categories = CheckDupingItems["Equipment"] + self.armor_set = set(self.equipment_categories["Armor"]) + self.accessories_set = set(self.equipment_categories["Accessories"]) + self.all_equipment = self.armor_set.union(self.accessories_set) + + self.Equipment_Anchor_Dict = { + "Armor": [0x2504, 0x2506, 0x2508, 0x250A], + "Accessories": [0x2514, 0x2516, 0x2518, 0x251A]} + + self.AbilityQuantityDict = {} + self.ability_categories = CheckDupingItems["Abilities"] + + self.sora_ability_set = set(self.ability_categories["Sora"]) + self.donald_ability_set = set(self.ability_categories["Donald"]) + self.goofy_ability_set = set(self.ability_categories["Goofy"]) + + self.all_abilities = self.sora_ability_set.union(self.donald_ability_set).union(self.goofy_ability_set) + + self.boost_set = set(CheckDupingItems["Boosts"]) + self.stat_increase_set = set(CheckDupingItems["Stat Increases"]) + + self.AbilityQuantityDict = {item: self.item_name_to_data[item].quantity for item in self.all_abilities} + # Growth:[level 1,level 4,slot] + self.growth_values_dict = {"High Jump": [0x05E, 0x061, 0x25CE], + "Quick Run": [0x62, 0x65, 0x25D0], + "Dodge Roll": [0x234, 0x237, 0x25D2], + "Aerial Dodge": [0x066, 0x069, 0x25D4], + "Glide": [0x6A, 0x6D, 0x25D6]} + self.boost_to_anchor_dict = { + "Power Boost": 0x24F9, + "Magic Boost": 0x24FA, + "Defense Boost": 0x24FB, + "AP Boost": 0x24F8} + + self.AbilityCodeList = [self.item_name_to_data[item].code for item in exclusionItem_table["Ability"]] + self.master_growth = {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"} + + self.bitmask_item_code = [ + 0x130000, 0x130001, 0x130002, 0x130003, 0x130004, 0x130005, 0x130006, 0x130007 + , 0x130008, 0x130009, 0x13000A, 0x13000B, 0x13000C + , 0x13001F, 0x130020, 0x130021, 0x130022, 0x130023 + , 0x13002A, 0x13002B, 0x13002C, 0x13002D] + + async def server_auth(self, password_requested: bool = False): + if password_requested and not self.password: + await super(KH2Context, self).server_auth(password_requested) + await self.get_username() + await self.send_connect() + + async def connection_closed(self): + self.kh2connected = False + self.serverconneced = False + if self.kh2seedname is not None and self.auth is not None: + with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"), + 'w') as f: + f.write(json.dumps(self.kh2seedsave, indent=4)) + await super(KH2Context, self).connection_closed() + + async def disconnect(self, allow_autoreconnect: bool = False): + self.kh2connected = False + self.serverconneced = False + if self.kh2seedname not in {None} and self.auth not in {None}: + with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"), + 'w') as f: + f.write(json.dumps(self.kh2seedsave, indent=4)) + await super(KH2Context, self).disconnect() + + @property + def endpoints(self): + if self.server: + return [self.server] + else: + return [] + + async def shutdown(self): + if self.kh2seedname not in {None} and self.auth not in {None}: + with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"), + 'w') as f: + f.write(json.dumps(self.kh2seedsave, indent=4)) + await super(KH2Context, self).shutdown() + + def on_package(self, cmd: str, args: dict): + if cmd in {"RoomInfo"}: + self.kh2seedname = args['seed_name'] + if not os.path.exists(self.game_communication_path): + os.makedirs(self.game_communication_path) + if not os.path.exists(self.game_communication_path + f"\kh2save{self.kh2seedname}{self.auth}.json"): + with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"), + 'wt') as f: + pass + elif os.path.exists(self.game_communication_path + f"\kh2save{self.kh2seedname}{self.auth}.json"): + with open(self.game_communication_path + f"\kh2save{self.kh2seedname}{self.auth}.json", 'r') as f: + self.kh2seedsave = json.load(f) + + if cmd in {"Connected"}: + for player in args['players']: + if str(player.slot) not in self.kh2seedsave["checked_locations"]: + self.kh2seedsave["checked_locations"].update({str(player.slot): []}) + self.kh2slotdata = args['slot_data'] + self.serverconneced = True + self.kh2LocalItems = {int(location): item for location, item in self.kh2slotdata["LocalItems"].items()} + try: + self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX") + logger.info("You are now auto-tracking") + self.kh2connected = True + except Exception as e: + logger.info("Line 247") + if self.kh2connected: + logger.info("Connection Lost") + self.kh2connected = False + logger.info(e) + + if cmd in {"ReceivedItems"}: + start_index = args["index"] + if start_index != len(self.items_received): + for item in args['items']: + # starting invo from server + if item.location in {-2}: + if not self.kh2seedsave["starting_inventory"]: + asyncio.create_task(self.give_item(item.item)) + # if location is not already given or is !getitem + elif item.location not in self.kh2seedsave["checked_locations"][str(item.player)] \ + or item.location in {-1}: + asyncio.create_task(self.give_item(item.item)) + if item.location not in self.kh2seedsave["checked_locations"][str(item.player)] \ + and item.location not in {-1, -2}: + self.kh2seedsave["checked_locations"][str(item.player)].append(item.location) + if not self.kh2seedsave["starting_inventory"]: + self.kh2seedsave["starting_inventory"] = True + + if cmd in {"RoomUpdate"}: + if "checked_locations" in args: + new_locations = set(args["checked_locations"]) + # TODO: make this take locations from other players on the same slot so proper coop happens + # items_to_give = [self.kh2slotdata["LocalItems"][str(location_id)] for location_id in new_locations if + # location_id in self.kh2LocalItems.keys()] + self.checked_locations |= new_locations + + async def checkWorldLocations(self): + try: + currentworldint = int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x0714DB8, 1), "big") + if currentworldint in self.worldid: + curworldid = self.worldid[currentworldint] + for location, data in curworldid.items(): + if location not in self.locations_checked \ + and (int.from_bytes( + self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1), + "big") & 0x1 << data.bitIndex) > 0: + self.locations_checked.add(location) + self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))] + except Exception as e: + logger.info("Line 285") + if self.kh2connected: + logger.info("Connection Lost.") + self.kh2connected = False + logger.info(e) + + async def checkLevels(self): + try: + for location, data in SoraLevels.items(): + currentLevel = int.from_bytes( + self.kh2.read_bytes(self.kh2.base_address + self.Save + 0x24FF, 1), "big") + if location not in self.locations_checked \ + and currentLevel >= data.bitIndex: + if self.kh2seedsave["Levels"]["SoraLevel"] < currentLevel: + self.kh2seedsave["Levels"]["SoraLevel"] = currentLevel + self.locations_checked.add(location) + self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))] + formDict = { + 0: ["ValorLevel", ValorLevels], 1: ["WisdomLevel", WisdomLevels], 2: ["LimitLevel", LimitLevels], + 3: ["MasterLevel", MasterLevels], 4: ["FinalLevel", FinalLevels]} + for i in range(5): + for location, data in formDict[i][1].items(): + formlevel = int.from_bytes( + self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1), "big") + if location not in self.locations_checked \ + and formlevel >= data.bitIndex: + if formlevel > self.kh2seedsave["Levels"][formDict[i][0]]: + self.kh2seedsave["Levels"][formDict[i][0]] = formlevel + self.locations_checked.add(location) + self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))] + except Exception as e: + logger.info("Line 312") + if self.kh2connected: + logger.info("Connection Lost.") + self.kh2connected = False + logger.info(e) + + async def checkSlots(self): + try: + for location, data in weaponSlots.items(): + if location not in self.locations_checked: + if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1), + "big") > 0: + self.locations_checked.add(location) + self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))] + + for location, data in formSlots.items(): + if location not in self.locations_checked: + if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1), + "big") & 0x1 << data.bitIndex > 0: + self.locations_checked.add(location) + self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))] + except Exception as e: + if self.kh2connected: + logger.info("Line 333") + logger.info("Connection Lost.") + self.kh2connected = False + logger.info(e) + + async def verifyChests(self): + try: + currentworld = str(int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x0714DB8, 1), "big")) + for location in self.kh2seedsave["worldIdChecks"][currentworld]: + locationName = self.lookup_id_to_Location[location] + if locationName in self.chest_set: + if locationName in self.location_name_to_worlddata.keys(): + locationData = self.location_name_to_worlddata[locationName] + if int.from_bytes( + self.kh2.read_bytes(self.kh2.base_address + self.Save + locationData.addrObtained, 1), + "big") & 0x1 << locationData.bitIndex == 0: + roomData = int.from_bytes( + self.kh2.read_bytes(self.kh2.base_address + self.Save + locationData.addrObtained, + 1), "big") + self.kh2.write_bytes(self.kh2.base_address + self.Save + locationData.addrObtained, + (roomData | 0x01 << locationData.bitIndex).to_bytes(1, 'big'), 1) + + except Exception as e: + if self.kh2connected: + logger.info("Line 350") + logger.info("Connection Lost.") + self.kh2connected = False + logger.info(e) + + async def verifyLevel(self): + for leveltype, anchor in {"SoraLevel": 0x24FF, + "ValorLevel": 0x32F6, + "WisdomLevel": 0x332E, + "LimitLevel": 0x3366, + "MasterLevel": 0x339E, + "FinalLevel": 0x33D6}.items(): + if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + anchor, 1), "big") < \ + self.kh2seedsave["Levels"][leveltype]: + self.kh2.write_bytes(self.kh2.base_address + self.Save + anchor, + (self.kh2seedsave["Levels"][leveltype]).to_bytes(1, 'big'), 1) + + def verifyLocation(self, location): + locationData = self.location_name_to_worlddata[location] + locationName = self.lookup_id_to_Location[location] + isChecked = True + + if locationName not in levels_locations: + if (int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + locationData.addrObtained, 1), + "big") & 0x1 << locationData.bitIndex) == 0: + isChecked = False + elif locationName in SoraLevels: + if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + 0x24FF, 1), + "big") < locationData.bitIndex: + isChecked = False + elif int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + locationData.addrObtained, 1), + "big") < locationData.bitIndex: + isChecked = False + return isChecked + + async def give_item(self, item, ItemType="ServerItems"): + try: + itemname = self.lookup_id_to_item[item] + itemcode = self.item_name_to_data[itemname] + if itemcode.ability: + abilityInvoType = 0 + TwilightZone = 2 + if ItemType == "LocalItems": + abilityInvoType = 1 + TwilightZone = -2 + if itemname in {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"}: + self.kh2seedsave["AmountInvo"][ItemType]["Growth"][itemname] += 1 + return + + if itemname not in self.kh2seedsave["AmountInvo"][ItemType]["Ability"]: + self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname] = [] + # appending the slot that the ability should be in + + if len(self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname]) < \ + self.AbilityQuantityDict[itemname]: + if itemname in self.sora_ability_set: + self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname].append( + self.kh2seedsave["SoraInvo"][abilityInvoType]) + self.kh2seedsave["SoraInvo"][abilityInvoType] -= TwilightZone + elif itemname in self.donald_ability_set: + self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname].append( + self.kh2seedsave["DonaldInvo"][abilityInvoType]) + self.kh2seedsave["DonaldInvo"][abilityInvoType] -= TwilightZone + else: + self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname].append( + self.kh2seedsave["GoofyInvo"][abilityInvoType]) + self.kh2seedsave["GoofyInvo"][abilityInvoType] -= TwilightZone + + elif itemcode.code in self.bitmask_item_code: + + if itemname not in self.kh2seedsave["AmountInvo"][ItemType]["Bitmask"]: + self.kh2seedsave["AmountInvo"][ItemType]["Bitmask"].append(itemname) + + elif itemcode.memaddr in {0x3594, 0x3595, 0x3596, 0x3597, 0x35CF, 0x35D0}: + + if itemname in self.kh2seedsave["AmountInvo"][ItemType]["Magic"]: + self.kh2seedsave["AmountInvo"][ItemType]["Magic"][itemname] += 1 + else: + self.kh2seedsave["AmountInvo"][ItemType]["Magic"][itemname] = 1 + elif itemname in self.all_equipment: + + self.kh2seedsave["AmountInvo"][ItemType]["Equipment"].append(itemname) + + elif itemname in self.all_weapons: + if itemname in self.keyblade_set: + self.kh2seedsave["AmountInvo"][ItemType]["Weapon"]["Sora"].append(itemname) + elif itemname in self.staff_set: + self.kh2seedsave["AmountInvo"][ItemType]["Weapon"]["Donald"].append(itemname) + else: + self.kh2seedsave["AmountInvo"][ItemType]["Weapon"]["Goofy"].append(itemname) + + elif itemname in self.boost_set: + if itemname in self.kh2seedsave["AmountInvo"][ItemType]["Boost"]: + self.kh2seedsave["AmountInvo"][ItemType]["Boost"][itemname] += 1 + else: + self.kh2seedsave["AmountInvo"][ItemType]["Boost"][itemname] = 1 + + elif itemname in self.stat_increase_set: + + if itemname in self.kh2seedsave["AmountInvo"][ItemType]["StatIncrease"]: + self.kh2seedsave["AmountInvo"][ItemType]["StatIncrease"][itemname] += 1 + else: + self.kh2seedsave["AmountInvo"][ItemType]["StatIncrease"][itemname] = 1 + + else: + if itemname in self.kh2seedsave["AmountInvo"][ItemType]["Amount"]: + self.kh2seedsave["AmountInvo"][ItemType]["Amount"][itemname] += 1 + else: + self.kh2seedsave["AmountInvo"][ItemType]["Amount"][itemname] = 1 + + except Exception as e: + if self.kh2connected: + logger.info("Line 398") + logger.info("Connection Lost.") + self.kh2connected = False + logger.info(e) + + def run_gui(self): + """Import kivy UI system and start running it as self.ui_task.""" + from kvui import GameManager + + class KH2Manager(GameManager): + logging_pairs = [ + ("Client", "Archipelago") + ] + base_title = "Archipelago KH2 Client" + + self.ui = KH2Manager(self) + self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + + async def IsInShop(self, sellable, master_boost): + # journal = 0x741230 shop = 0x741320 + # if journal=-1 and shop = 5 then in shop + # if journam !=-1 and shop = 10 then journal + journal = self.kh2.read_short(self.kh2.base_address + 0x741230) + shop = self.kh2.read_short(self.kh2.base_address + 0x741320) + if (journal == -1 and shop == 5) or (journal != -1 and shop == 10): + # print("your in the shop") + sellable_dict = {} + for itemName in sellable: + itemdata = self.item_name_to_data[itemName] + amount = int.from_bytes( + self.kh2.read_bytes(self.kh2.base_address + self.Save + itemdata.memaddr, 1), "big") + sellable_dict[itemName] = amount + while (journal == -1 and shop == 5) or (journal != -1 and shop == 10): + journal = self.kh2.read_short(self.kh2.base_address + 0x741230) + shop = self.kh2.read_short(self.kh2.base_address + 0x741320) + await asyncio.sleep(0.5) + for item, amount in sellable_dict.items(): + itemdata = self.item_name_to_data[item] + afterShop = int.from_bytes( + self.kh2.read_bytes(self.kh2.base_address + self.Save + itemdata.memaddr, 1), "big") + if afterShop < amount: + if item in master_boost: + self.kh2seedsave["SoldBoosts"][item] += (amount - afterShop) + else: + self.kh2seedsave["SoldEquipment"].append(item) + + async def verifyItems(self): + try: + local_amount = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Amount"].keys()) + server_amount = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Amount"].keys()) + master_amount = local_amount | server_amount + + local_ability = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Ability"].keys()) + server_ability = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Ability"].keys()) + master_ability = local_ability | server_ability + + local_bitmask = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Bitmask"]) + server_bitmask = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Bitmask"]) + master_bitmask = local_bitmask | server_bitmask + + local_keyblade = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Weapon"]["Sora"]) + local_staff = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Weapon"]["Donald"]) + local_shield = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Weapon"]["Goofy"]) + + server_keyblade = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Weapon"]["Sora"]) + server_staff = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Weapon"]["Donald"]) + server_shield = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Weapon"]["Goofy"]) + + master_keyblade = local_keyblade | server_keyblade + master_staff = local_staff | server_staff + master_shield = local_shield | server_shield + + local_equipment = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Equipment"]) + server_equipment = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Equipment"]) + master_equipment = local_equipment | server_equipment + + local_magic = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Magic"].keys()) + server_magic = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Magic"].keys()) + master_magic = local_magic | server_magic + + local_stat = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["StatIncrease"].keys()) + server_stat = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["StatIncrease"].keys()) + master_stat = local_stat | server_stat + + local_boost = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Boost"].keys()) + server_boost = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Boost"].keys()) + master_boost = local_boost | server_boost + + master_sell = master_equipment | master_staff | master_shield | master_boost + await asyncio.create_task(self.IsInShop(master_sell, master_boost)) + for itemName in master_amount: + itemData = self.item_name_to_data[itemName] + amountOfItems = 0 + if itemName in local_amount: + amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["Amount"][itemName] + if itemName in server_amount: + amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["Amount"][itemName] + + if itemName == "Torn Page": + # Torn Pages are handled differently because they can be consumed. + # Will check the progression in 100 acre and - the amount of visits + # amountofitems-amount of visits done + for location, data in tornPageLocks.items(): + if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1), + "big") & 0x1 << data.bitIndex > 0: + amountOfItems -= 1 + if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), + "big") != amountOfItems and amountOfItems >= 0: + self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, + amountOfItems.to_bytes(1, 'big'), 1) + + for itemName in master_keyblade: + itemData = self.item_name_to_data[itemName] + # if the inventory slot for that keyblade is less than the amount they should have + if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), + "big") != 1 and int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x1CFF, 1), + "big") != 13: + # Checking form anchors for the keyblade + if self.kh2.read_short(self.kh2.base_address + self.Save + 0x24F0) == itemData.kh2id \ + or self.kh2.read_short(self.kh2.base_address + self.Save + 0x32F4) == itemData.kh2id \ + or self.kh2.read_short(self.kh2.base_address + self.Save + 0x339C) == itemData.kh2id \ + or self.kh2.read_short(self.kh2.base_address + self.Save + 0x33D4) == itemData.kh2id: + self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, + (0).to_bytes(1, 'big'), 1) + else: + self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, + (1).to_bytes(1, 'big'), 1) + for itemName in master_staff: + itemData = self.item_name_to_data[itemName] + if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), + "big") != 1 \ + and self.kh2.read_short(self.kh2.base_address + self.Save + 0x2604) != itemData.kh2id \ + and itemName not in self.kh2seedsave["SoldEquipment"]: + self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, + (1).to_bytes(1, 'big'), 1) + + for itemName in master_shield: + itemData = self.item_name_to_data[itemName] + if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), + "big") != 1 \ + and self.kh2.read_short(self.kh2.base_address + self.Save + 0x2718) != itemData.kh2id \ + and itemName not in self.kh2seedsave["SoldEquipment"]: + self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, + (1).to_bytes(1, 'big'), 1) + + for itemName in master_ability: + itemData = self.item_name_to_data[itemName] + ability_slot = [] + if itemName in local_ability: + ability_slot += self.kh2seedsave["AmountInvo"]["LocalItems"]["Ability"][itemName] + if itemName in server_ability: + ability_slot += self.kh2seedsave["AmountInvo"]["ServerItems"]["Ability"][itemName] + for slot in ability_slot: + current = self.kh2.read_short(self.kh2.base_address + self.Save + slot) + ability = current & 0x0FFF + if ability | 0x8000 != (0x8000 + itemData.memaddr): + self.kh2.write_short(self.kh2.base_address + self.Save + slot, itemData.memaddr) + + for itemName in self.master_growth: + growthLevel = self.kh2seedsave["AmountInvo"]["ServerItems"]["Growth"][itemName] \ + + self.kh2seedsave["AmountInvo"]["LocalItems"]["Growth"][itemName] + if growthLevel > 0: + slot = self.growth_values_dict[itemName][2] + min_growth = self.growth_values_dict[itemName][0] + max_growth = self.growth_values_dict[itemName][1] + if growthLevel > 4: + growthLevel = 4 + current_growth_level = self.kh2.read_short(self.kh2.base_address + self.Save + slot) + ability = current_growth_level & 0x0FFF + # if the player should be getting a growth ability + if ability | 0x8000 != 0x8000 + min_growth - 1 + growthLevel: + # if it should be level one of that growth + if 0x8000 + min_growth - 1 + growthLevel <= 0x8000 + min_growth or ability < min_growth: + self.kh2.write_short(self.kh2.base_address + self.Save + slot, min_growth) + # if it is already in the inventory + elif ability | 0x8000 < (0x8000 + max_growth): + self.kh2.write_short(self.kh2.base_address + self.Save + slot, current_growth_level + 1) + + for itemName in master_bitmask: + itemData = self.item_name_to_data[itemName] + itemMemory = int.from_bytes( + self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), "big") + if (int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), + "big") & 0x1 << itemData.bitmask) == 0: + self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, + (itemMemory | 0x01 << itemData.bitmask).to_bytes(1, 'big'), 1) + + for itemName in master_equipment: + itemData = self.item_name_to_data[itemName] + isThere = False + if itemName in self.accessories_set: + Equipment_Anchor_List = self.Equipment_Anchor_Dict["Accessories"] + else: + Equipment_Anchor_List = self.Equipment_Anchor_Dict["Armor"] + # Checking form anchors for the equipment + for slot in Equipment_Anchor_List: + if self.kh2.read_short(self.kh2.base_address + self.Save + slot) == itemData.kh2id: + isThere = True + if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), + "big") != 0: + self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, + (0).to_bytes(1, 'big'), 1) + break + if not isThere and itemName not in self.kh2seedsave["SoldEquipment"]: + if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), + "big") != 1: + self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, + (1).to_bytes(1, 'big'), 1) + + for itemName in master_magic: + itemData = self.item_name_to_data[itemName] + amountOfItems = 0 + if itemName in local_magic: + amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["Magic"][itemName] + if itemName in server_magic: + amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["Magic"][itemName] + if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), + "big") != amountOfItems \ + and int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x741320, 1), "big") in {10, 8}: + self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, + amountOfItems.to_bytes(1, 'big'), 1) + + for itemName in master_stat: + itemData = self.item_name_to_data[itemName] + amountOfItems = 0 + if itemName in local_stat: + amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["StatIncrease"][itemName] + if itemName in server_stat: + amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["StatIncrease"][itemName] + + if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), + "big") != amountOfItems \ + and int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Slot1 + 0x1B2, 1), + "big") >= 5: + self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, + amountOfItems.to_bytes(1, 'big'), 1) + + for itemName in master_boost: + itemData = self.item_name_to_data[itemName] + amountOfItems = 0 + if itemName in local_boost: + amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["Boost"][itemName] + if itemName in server_boost: + amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["Boost"][itemName] + amountOfBoostsInInvo = int.from_bytes( + self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), + "big") + amountOfUsedBoosts = int.from_bytes( + self.kh2.read_bytes(self.kh2.base_address + self.Save + self.boost_to_anchor_dict[itemName], 1), + "big") + # Ap Boots start at +50 for some reason + if itemName == "AP Boost": + amountOfUsedBoosts -= 50 + totalBoosts = (amountOfBoostsInInvo + amountOfUsedBoosts) + if totalBoosts <= amountOfItems - self.kh2seedsave["SoldBoosts"][itemName] and amountOfBoostsInInvo < 255: + self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, + (amountOfBoostsInInvo + 1).to_bytes(1, 'big'), 1) + + except Exception as e: + logger.info("Line 573") + if self.kh2connected: + logger.info("Connection Lost.") + self.kh2connected = False + logger.info(e) + + +def finishedGame(ctx: KH2Context, message): + if ctx.kh2slotdata['FinalXemnas'] == 1: + if 0x1301ED in message[0]["locations"]: + ctx.finalxemnas = True + # three proofs + if ctx.kh2slotdata['Goal'] == 0: + if int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x36B2, 1), "big") > 0 \ + and int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x36B3, 1), "big") > 0 \ + and int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x36B4, 1), "big") > 0: + if ctx.kh2slotdata['FinalXemnas'] == 1: + if ctx.finalxemnas: + return True + else: + return False + else: + return True + else: + return False + elif ctx.kh2slotdata['Goal'] == 1: + if int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x3641, 1), "big") >= \ + ctx.kh2slotdata['LuckyEmblemsRequired']: + ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B2, (1).to_bytes(1, 'big'), 1) + ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B3, (1).to_bytes(1, 'big'), 1) + ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B4, (1).to_bytes(1, 'big'), 1) + if ctx.kh2slotdata['FinalXemnas'] == 1: + if ctx.finalxemnas: + return True + else: + return False + else: + return True + else: + return False + elif ctx.kh2slotdata['Goal'] == 2: + for boss in ctx.kh2slotdata["hitlist"]: + if boss in message[0]["locations"]: + ctx.amountOfPieces += 1 + if ctx.amountOfPieces >= ctx.kh2slotdata["BountyRequired"]: + ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B2, (1).to_bytes(1, 'big'), 1) + ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B3, (1).to_bytes(1, 'big'), 1) + ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B4, (1).to_bytes(1, 'big'), 1) + if ctx.kh2slotdata['FinalXemnas'] == 1: + if ctx.finalxemnas: + return True + else: + return False + else: + return True + else: + return False + + +async def kh2_watcher(ctx: KH2Context): + while not ctx.exit_event.is_set(): + try: + if ctx.kh2connected and ctx.serverconneced: + ctx.sending = [] + await asyncio.create_task(ctx.checkWorldLocations()) + await asyncio.create_task(ctx.checkLevels()) + await asyncio.create_task(ctx.checkSlots()) + await asyncio.create_task(ctx.verifyChests()) + await asyncio.create_task(ctx.verifyItems()) + await asyncio.create_task(ctx.verifyLevel()) + message = [{"cmd": 'LocationChecks', "locations": ctx.sending}] + if finishedGame(ctx, message): + await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) + ctx.finished_game = True + location_ids = [] + location_ids = [location for location in message[0]["locations"] if location not in location_ids] + for location in location_ids: + currentWorld = int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + 0x0714DB8, 1), "big") + if location not in ctx.kh2seedsave["worldIdChecks"][str(currentWorld)]: + ctx.kh2seedsave["worldIdChecks"][str(currentWorld)].append(location) + if location in ctx.kh2LocalItems: + item = ctx.kh2slotdata["LocalItems"][str(location)] + await asyncio.create_task(ctx.give_item(item, "LocalItems")) + await ctx.send_msgs(message) + elif not ctx.kh2connected and ctx.serverconneced: + logger.info("Game is not open. Disconnecting from Server.") + await ctx.disconnect() + except Exception as e: + logger.info("Line 661") + if ctx.kh2connected: + logger.info("Connection Lost.") + ctx.kh2connected = False + logger.info(e) + await asyncio.sleep(0.5) + + +if __name__ == '__main__': + async def main(args): + ctx = KH2Context(args.connect, args.password) + ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop") + if gui_enabled: + ctx.run_gui() + ctx.run_cli() + progression_watcher = asyncio.create_task( + kh2_watcher(ctx), name="KH2ProgressionWatcher") + + await ctx.exit_event.wait() + ctx.server_address = None + + await progression_watcher + + await ctx.shutdown() + + + import colorama + + parser = get_base_parser(description="KH2 Client, for text interfacing.") + + args, rest = parser.parse_known_args() + colorama.init() + asyncio.run(main(args)) + colorama.deinit() diff --git a/Launcher.py b/Launcher.py index d5ade1f184..be40987e32 100644 --- a/Launcher.py +++ b/Launcher.py @@ -14,10 +14,11 @@ import itertools import shlex import subprocess import sys -from enum import Enum, auto from os.path import isfile from shutil import which -from typing import Iterable, Sequence, Callable, Union, Optional +from typing import Sequence, Union, Optional + +from worlds.LauncherComponents import Component, components, Type, SuffixIdentifier if __name__ == "__main__": import ModuleUpdate @@ -70,101 +71,12 @@ def browse_files(): webbrowser.open(file) -# noinspection PyArgumentList -class Type(Enum): - TOOL = auto() - FUNC = auto() # not a real component - CLIENT = auto() - ADJUSTER = auto() - - -class SuffixIdentifier: - suffixes: Iterable[str] - - def __init__(self, *args: str): - self.suffixes = args - - def __call__(self, path: str): - if isinstance(path, str): - for suffix in self.suffixes: - if path.endswith(suffix): - return True - return False - - -class Component: - display_name: str - type: Optional[Type] - script_name: Optional[str] - frozen_name: Optional[str] - icon: str # just the name, no suffix - cli: bool - func: Optional[Callable] - file_identifier: Optional[Callable[[str], bool]] - - def __init__(self, display_name: str, script_name: Optional[str] = None, frozen_name: Optional[str] = None, - cli: bool = False, icon: str = 'icon', component_type: Type = None, func: Optional[Callable] = None, - file_identifier: Optional[Callable[[str], bool]] = None): - self.display_name = display_name - self.script_name = script_name - self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None - self.icon = icon - self.cli = cli - self.type = component_type or \ - None if not display_name else \ - Type.FUNC if func else \ - Type.CLIENT if 'Client' in display_name else \ - Type.ADJUSTER if 'Adjuster' in display_name else Type.TOOL - self.func = func - self.file_identifier = file_identifier - - def handles_file(self, path: str): - return self.file_identifier(path) if self.file_identifier else False - - -components: Iterable[Component] = ( - # Launcher - Component('', 'Launcher'), - # Core - Component('Host', 'MultiServer', 'ArchipelagoServer', cli=True, - file_identifier=SuffixIdentifier('.archipelago', '.zip')), - Component('Generate', 'Generate', cli=True), - Component('Text Client', 'CommonClient', 'ArchipelagoTextClient'), - # SNI - Component('SNI Client', 'SNIClient', - file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3', '.apsmw')), - Component('LttP Adjuster', 'LttPAdjuster'), - # Factorio - Component('Factorio Client', 'FactorioClient'), - # Minecraft - Component('Minecraft Client', 'MinecraftClient', icon='mcicon', cli=True, - file_identifier=SuffixIdentifier('.apmc')), - # Ocarina of Time - Component('OoT Client', 'OoTClient', - file_identifier=SuffixIdentifier('.apz5')), - Component('OoT Adjuster', 'OoTAdjuster'), - # FF1 - Component('FF1 Client', 'FF1Client'), - # PokÊmon - Component('Pokemon Client', 'PokemonClient', file_identifier=SuffixIdentifier('.apred', '.apblue')), - # ChecksFinder - Component('ChecksFinder Client', 'ChecksFinderClient'), - # Starcraft 2 - Component('Starcraft 2 Client', 'Starcraft2Client'), - # Wargroove - Component('Wargroove Client', 'WargrooveClient'), - # Zillion - Component('Zillion Client', 'ZillionClient', - file_identifier=SuffixIdentifier('.apzl')), +components.extend([ # Functions Component('Open host.yaml', func=open_host_yaml), Component('Open Patch', func=open_patch), Component('Browse Files', func=browse_files), -) -icon_paths = { - 'icon': local_path('data', 'icon.ico' if is_windows else 'icon.png'), - 'mcicon': local_path('data', 'mcicon.ico') -} +]) def identify(path: Union[None, str]): diff --git a/LinksAwakeningClient.py b/LinksAwakeningClient.py new file mode 100644 index 0000000000..e0557e4af4 --- /dev/null +++ b/LinksAwakeningClient.py @@ -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() diff --git a/Main.py b/Main.py index 04a7e3bff6..03b2e1b155 100644 --- a/Main.py +++ b/Main.py @@ -38,7 +38,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No world = MultiWorld(args.multi) logger = logging.getLogger() - world.set_seed(seed, args.race, str(args.outputname if args.outputname else world.seed)) + world.set_seed(seed, args.race, str(args.outputname) if args.outputname else None) world.plando_options = args.plando_options world.shuffle = args.shuffle.copy() @@ -53,7 +53,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No world.enemy_damage = args.enemy_damage.copy() world.beemizer_total_chance = args.beemizer_total_chance.copy() world.beemizer_trap_chance = args.beemizer_trap_chance.copy() - world.timer = args.timer.copy() world.countdown_start_time = args.countdown_start_time.copy() world.red_clock_time = args.red_clock_time.copy() world.blue_clock_time = args.blue_clock_time.copy() @@ -79,7 +78,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No world.state = CollectionState(world) logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed) - logger.info("Found World Types:") + logger.info(f"Found {len(AutoWorld.AutoWorldRegister.world_types)} World Types:") longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types) max_item = 0 @@ -356,12 +355,11 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No for player in world.groups.get(location.item.player, {}).get("players", [])]): precollect_hint(location) - # custom datapackage - datapackage = {} - for game_world in world.worlds.values(): - if game_world.data_version == 0 and game_world.game not in datapackage: - datapackage[game_world.game] = worlds.network_data_package["games"][game_world.game] - datapackage[game_world.game]["item_name_groups"] = game_world.item_name_groups + # embedded data package + data_package = { + game_world.game: worlds.network_data_package["games"][game_world.game] + for game_world in world.worlds.values() + } multidata = { "slot_data": slot_data, @@ -378,7 +376,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No "tags": ["AP"], "minimum_versions": minimum_versions, "seed_name": world.seed_name, - "datapackage": datapackage, + "datapackage": data_package, } AutoWorld.call_all(world, "modify_multidata", multidata) diff --git a/MinecraftClient.py b/MinecraftClient.py index 16b283f9d9..dd7a5cfd3e 100644 --- a/MinecraftClient.py +++ b/MinecraftClient.py @@ -77,49 +77,34 @@ def read_apmc_file(apmc_file): return json.loads(b64decode(f.read())) -def update_mod(forge_dir, minecraft_version: str, get_prereleases=False): +def update_mod(forge_dir, url: str): """Check mod version, download new mod from GitHub releases page if needed. """ ap_randomizer = find_ap_randomizer_jar(forge_dir) - - client_releases_endpoint = "https://api.github.com/repos/KonoTyran/Minecraft_AP_Randomizer/releases" - resp = requests.get(client_releases_endpoint) - if resp.status_code == 200: # OK - try: - latest_release = next(filter(lambda release: (not release['prerelease'] or get_prereleases) and - (minecraft_version in release['assets'][0]['name']), - resp.json())) - if ap_randomizer != latest_release['assets'][0]['name']: - logging.info(f"A new release of the Minecraft AP randomizer mod was found: " - f"{latest_release['assets'][0]['name']}") - if ap_randomizer is not None: - logging.info(f"Your current mod is {ap_randomizer}.") - else: - logging.info(f"You do not have the AP randomizer mod installed.") - if prompt_yes_no("Would you like to update?"): - old_ap_mod = os.path.join(forge_dir, 'mods', ap_randomizer) if ap_randomizer is not None else None - new_ap_mod = os.path.join(forge_dir, 'mods', latest_release['assets'][0]['name']) - logging.info("Downloading AP randomizer mod. This may take a moment...") - apmod_resp = requests.get(latest_release['assets'][0]['browser_download_url']) - if apmod_resp.status_code == 200: - with open(new_ap_mod, 'wb') as f: - f.write(apmod_resp.content) - logging.info(f"Wrote new mod file to {new_ap_mod}") - if old_ap_mod is not None: - os.remove(old_ap_mod) - logging.info(f"Removed old mod file from {old_ap_mod}") - else: - logging.error(f"Error retrieving the randomizer mod (status code {apmod_resp.status_code}).") - logging.error(f"Please report this issue on the Archipelago Discord server.") - sys.exit(1) - except StopIteration: - logging.warning(f"No compatible mod version found for {minecraft_version}.") - if not prompt_yes_no("Run server anyway?"): - sys.exit(0) + os.path.basename(url) + if ap_randomizer is not None: + logging.info(f"Your current mod is {ap_randomizer}.") else: - logging.error(f"Error checking for randomizer mod updates (status code {resp.status_code}).") - logging.error(f"If this was not expected, please report this issue on the Archipelago Discord server.") - if not prompt_yes_no("Continue anyways?"): - sys.exit(0) + logging.info(f"You do not have the AP randomizer mod installed.") + + if ap_randomizer != os.path.basename(url): + logging.info(f"A new release of the Minecraft AP randomizer mod was found: " + f"{os.path.basename(url)}") + if prompt_yes_no("Would you like to update?"): + old_ap_mod = os.path.join(forge_dir, 'mods', ap_randomizer) if ap_randomizer is not None else None + new_ap_mod = os.path.join(forge_dir, 'mods', os.path.basename(url)) + logging.info("Downloading AP randomizer mod. This may take a moment...") + apmod_resp = requests.get(url) + if apmod_resp.status_code == 200: + with open(new_ap_mod, 'wb') as f: + f.write(apmod_resp.content) + logging.info(f"Wrote new mod file to {new_ap_mod}") + if old_ap_mod is not None: + os.remove(old_ap_mod) + logging.info(f"Removed old mod file from {old_ap_mod}") + else: + logging.error(f"Error retrieving the randomizer mod (status code {apmod_resp.status_code}).") + logging.error(f"Please report this issue on the Archipelago Discord server.") + sys.exit(1) def check_eula(forge_dir): @@ -264,8 +249,13 @@ def get_minecraft_versions(version, release_channel="release"): return next(filter(lambda entry: entry["version"] == version, data[release_channel])) else: return resp.json()[release_channel][0] - except StopIteration: - logging.error(f"No compatible mod version found for client version {version}.") + except (StopIteration, KeyError): + logging.error(f"No compatible mod version found for client version {version} on \"{release_channel}\" channel.") + if release_channel != "release": + logging.error("Consider switching \"release_channel\" to \"release\" in your Host.yaml file") + else: + logging.error("No suitable mod found on the \"release\" channel. Please Contact us on discord to report this error.") + sys.exit(0) def is_correct_forge(forge_dir) -> bool: @@ -286,6 +276,8 @@ if __name__ == '__main__': help="specify java version.") parser.add_argument('--forge', '-f', metavar='1.18.2-40.1.0', dest='forge', type=str, default=False, action='store', help="specify forge version. (Minecraft Version-Forge Version)") + parser.add_argument('--version', '-v', metavar='9', dest='data_version', type=int, action='store', + help="specify Mod data version to download.") args = parser.parse_args() apmc_file = os.path.abspath(args.apmc_file) if args.apmc_file else None @@ -296,12 +288,12 @@ if __name__ == '__main__': options = Utils.get_options() channel = args.channel or options["minecraft_options"]["release_channel"] apmc_data = None - data_version = None + data_version = args.data_version or None if apmc_file is None and not args.install: apmc_file = Utils.open_filename('Select APMC file', (('APMC File', ('.apmc',)),)) - if apmc_file is not None: + if apmc_file is not None and data_version is None: apmc_data = read_apmc_file(apmc_file) data_version = apmc_data.get('client_version', '') @@ -311,6 +303,7 @@ if __name__ == '__main__': max_heap = options["minecraft_options"]["max_heap_size"] forge_version = args.forge or versions["forge"] java_version = args.java or versions["java"] + mod_url = versions["url"] java_dir = find_jdk_dir(java_version) if args.install: @@ -344,7 +337,7 @@ if __name__ == '__main__': if not max_heap_re.match(max_heap): raise Exception(f"Max heap size {max_heap} in incorrect format. Use a number followed by M or G, e.g. 512M or 2G.") - update_mod(forge_dir, f"MC{forge_version.split('-')[0]}", channel != "release") + update_mod(forge_dir, mod_url) replace_apmc_files(forge_dir, apmc_file) check_eula(forge_dir) server_process = run_forge_server(forge_dir, java_version, max_heap) diff --git a/ModuleUpdate.py b/ModuleUpdate.py index a8258ce17e..ac40dbd66b 100644 --- a/ModuleUpdate.py +++ b/ModuleUpdate.py @@ -1,7 +1,6 @@ import os import sys import subprocess -import pkg_resources import warnings local_dir = os.path.dirname(__file__) @@ -22,18 +21,50 @@ if not update_ran: requirements_files.add(req_file) +def check_pip(): + # detect if pip is available + try: + import pip # noqa: F401 + except ImportError: + raise RuntimeError("pip not available. Please install pip.") + + +def confirm(msg: str): + try: + input(f"\n{msg}") + except KeyboardInterrupt: + print("\nAborting") + sys.exit(1) + + def update_command(): + check_pip() for file in requirements_files: - subprocess.call([sys.executable, '-m', 'pip', 'install', '-r', file, '--upgrade']) + subprocess.call([sys.executable, "-m", "pip", "install", "-r", file, "--upgrade"]) + + +def install_pkg_resources(yes=False): + try: + import pkg_resources # noqa: F401 + except ImportError: + check_pip() + if not yes: + confirm("pkg_resources not found, press enter to install it") + subprocess.call([sys.executable, "-m", "pip", "install", "--upgrade", "setuptools"]) def update(yes=False, force=False): global update_ran if not update_ran: update_ran = True + if force: update_command() return + + install_pkg_resources(yes=yes) + import pkg_resources + for req_file in requirements_files: path = os.path.join(os.path.dirname(sys.argv[0]), req_file) if not os.path.exists(path): @@ -52,7 +83,7 @@ def update(yes=False, force=False): egg = egg.split(";", 1)[0].rstrip() if any(compare in egg for compare in ("==", ">=", ">", "<", "<=", "!=")): warnings.warn(f"Specifying version as #egg={egg} will become unavailable in pip 25.0. " - "Use name @ url#version instead.", DeprecationWarning) + "Use name @ url#version instead.", DeprecationWarning) line = egg else: egg = "" @@ -79,11 +110,7 @@ def update(yes=False, force=False): if not yes: import traceback traceback.print_exc() - try: - input(f"\nRequirement {requirement} is not satisfied, press enter to install it") - except KeyboardInterrupt: - print("\nAborting") - sys.exit(1) + confirm(f"Requirement {requirement} is not satisfied, press enter to install it") update_command() return diff --git a/MultiServer.py b/MultiServer.py index faeb1b220b..ea055b662e 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -7,17 +7,20 @@ import functools import logging import zlib import collections -import typing -import inspect -import weakref import datetime -import threading -import random -import pickle -import itertools -import time -import operator +import functools import hashlib +import inspect +import itertools +import logging +import operator +import pickle +import random +import threading +import time +import typing +import weakref +import zlib import ModuleUpdate @@ -160,10 +163,13 @@ class Context: stored_data_notification_clients: typing.Dict[str, typing.Set[Client]] slot_info: typing.Dict[int, NetworkSlot] + checksums: typing.Dict[str, str] item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})') item_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]] location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})') + location_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]] all_item_and_group_names: typing.Dict[str, typing.Set[str]] + all_location_and_group_names: typing.Dict[str, typing.Set[str]] non_hintable_names: typing.Dict[str, typing.Set[str]] def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int, @@ -231,30 +237,39 @@ class Context: # init empty to satisfy linter, I suppose self.gamespackage = {} + self.checksums = {} self.item_name_groups = {} + self.location_name_groups = {} self.all_item_and_group_names = {} + self.all_location_and_group_names = {} self.non_hintable_names = collections.defaultdict(frozenset) self._load_game_data() - # Datapackage retrieval + # Data package retrieval def _load_game_data(self): import worlds self.gamespackage = worlds.network_data_package["games"] self.item_name_groups = {world_name: world.item_name_groups for world_name, world in worlds.AutoWorldRegister.world_types.items()} + self.location_name_groups = {world_name: world.location_name_groups for world_name, world in + worlds.AutoWorldRegister.world_types.items()} for world_name, world in worlds.AutoWorldRegister.world_types.items(): self.non_hintable_names[world_name] = world.hint_blacklist def _init_game_data(self): for game_name, game_package in self.gamespackage.items(): + if "checksum" in game_package: + self.checksums[game_name] = game_package["checksum"] for item_name, item_id in game_package["item_name_to_id"].items(): self.item_names[item_id] = item_name for location_name, location_id in game_package["location_name_to_id"].items(): self.location_names[location_id] = location_name self.all_item_and_group_names[game_name] = \ set(game_package["item_name_to_id"]) | set(self.item_name_groups[game_name]) + self.all_location_and_group_names[game_name] = \ + set(game_package["location_name_to_id"]) | set(self.location_name_groups.get(game_name, [])) def item_names_for_game(self, game: str) -> typing.Optional[typing.Dict[str, int]]: return self.gamespackage[game]["item_name_to_id"] if game in self.gamespackage else None @@ -343,7 +358,6 @@ class Context: for text in texts])) # loading - def load(self, multidatapath: str, use_embedded_server_options: bool = False): if multidatapath.lower().endswith(".zip"): import zipfile @@ -358,7 +372,7 @@ class Context: with open(multidatapath, 'rb') as f: data = f.read() - self._load(self.decompress(data), use_embedded_server_options) + self._load(self.decompress(data), {}, use_embedded_server_options) self.data_filename = multidatapath @staticmethod @@ -368,7 +382,8 @@ class Context: raise Utils.VersionException("Incompatible multidata.") return restricted_loads(zlib.decompress(data[1:])) - def _load(self, decoded_obj: dict, use_embedded_server_options: bool): + def _load(self, decoded_obj: dict, game_data_packages: typing.Dict[str, typing.Any], + use_embedded_server_options: bool): self.read_data = {} mdata_ver = decoded_obj["minimum_versions"]["server"] if mdata_ver > Utils.version_tuple: @@ -423,15 +438,22 @@ class Context: server_options = decoded_obj.get("server_options", {}) self._set_options(server_options) - # custom datapackage + # embedded data package for game_name, data in decoded_obj.get("datapackage", {}).items(): - logging.info(f"Loading custom datapackage for game {game_name}") + if game_name in game_data_packages: + data = game_data_packages[game_name] + logging.info(f"Loading embedded data package for game {game_name}") self.gamespackage[game_name] = data self.item_name_groups[game_name] = data["item_name_groups"] - del data["item_name_groups"] # remove from datapackage, but keep in self.item_name_groups + if "location_name_groups" in data: + self.location_name_groups[game_name] = data["location_name_groups"] + del data["location_name_groups"] + del data["item_name_groups"] # remove from data package, but keep in self.item_name_groups self._init_game_data() for game_name, data in self.item_name_groups.items(): self.read_data[f"item_name_groups_{game_name}"] = lambda lgame=game_name: self.item_name_groups[lgame] + for game_name, data in self.location_name_groups.items(): + self.read_data[f"location_name_groups_{game_name}"] = lambda lgame=game_name: self.location_name_groups[lgame] # saving @@ -575,7 +597,7 @@ class Context: def get_hint_cost(self, slot): if self.hint_cost: - return max(0, int(self.hint_cost * 0.01 * len(self.locations[slot]))) + return max(1, int(self.hint_cost * 0.01 * len(self.locations[slot]))) return 0 def recheck_hints(self, team: typing.Optional[int] = None, slot: typing.Optional[int] = None): @@ -723,10 +745,12 @@ async def on_client_connected(ctx: Context, client: Client): NetworkPlayer(team, slot, ctx.name_aliases.get((team, slot), name), name) ) + games = {ctx.games[x] for x in range(1, len(ctx.games) + 1)} + games.add("Archipelago") await ctx.send_msgs(client, [{ 'cmd': 'RoomInfo', 'password': bool(ctx.password), - 'games': {ctx.games[x] for x in range(1, len(ctx.games) + 1)}, + 'games': games, # tags are for additional features in the communication. # Name them by feature or fork, as you feel is appropriate. 'tags': ctx.tags, @@ -735,7 +759,9 @@ async def on_client_connected(ctx: Context, client: Client): 'hint_cost': ctx.hint_cost, 'location_check_points': ctx.location_check_points, 'datapackage_versions': {game: game_data["version"] for game, game_data - in ctx.gamespackage.items()}, + in ctx.gamespackage.items() if game in games}, + 'datapackage_checksums': {game: game_data["checksum"] for game, game_data + in ctx.gamespackage.items() if game in games and "checksum" in game_data}, 'seed_name': ctx.seed_name, 'time': time.time(), }]) @@ -756,7 +782,8 @@ async def on_client_disconnected(ctx: Context, client: Client): async def on_client_joined(ctx: Context, client: Client): - update_client_status(ctx, client, ClientStatus.CLIENT_CONNECTED) + if ctx.client_game_state[client.team, client.slot] == ClientStatus.CLIENT_UNKNOWN: + update_client_status(ctx, client, ClientStatus.CLIENT_CONNECTED) version_str = '.'.join(str(x) for x in client.version) verb = "tracking" if "Tracker" in client.tags else "playing" ctx.broadcast_text_all( @@ -773,11 +800,12 @@ async def on_client_joined(ctx: Context, client: Client): async def on_client_left(ctx: Context, client: Client): - update_client_status(ctx, client, ClientStatus.CLIENT_UNKNOWN) + if len(ctx.clients[client.team][client.slot]) < 1: + update_client_status(ctx, client, ClientStatus.CLIENT_UNKNOWN) + ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc) ctx.broadcast_text_all( "%s (Team #%d) has left the game" % (ctx.get_aliased_name(client.team, client.slot), client.team + 1), {"type": "Part", "team": client.team, "slot": client.slot}) - ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc) async def countdown(ctx: Context, timer: int): @@ -1299,27 +1327,41 @@ class ClientMessageProcessor(CommonCommandProcessor): "Sorry, !remaining requires you to have beaten the game on this server") return False - def _cmd_missing(self) -> bool: - """List all missing location checks from the server's perspective""" + def _cmd_missing(self, filter_text="") -> bool: + """List all missing location checks from the server's perspective. + Can be given text, which will be used as filter.""" locations = get_missing_checks(self.ctx, self.client.team, self.client.slot) if locations: - texts = [f'Missing: {self.ctx.location_names[location]}' for location in locations] - texts.append(f"Found {len(locations)} missing location checks") + names = [self.ctx.location_names[location] for location in locations] + if filter_text: + names = [name for name in names if filter_text in name] + texts = [f'Missing: {name}' for name in names] + if filter_text: + texts.append(f"Found {len(locations)} missing location checks, displaying {len(names)} of them.") + else: + texts.append(f"Found {len(locations)} missing location checks") self.output_multiple(texts) else: self.output("No missing location checks found.") return True - def _cmd_checked(self) -> bool: - """List all done location checks from the server's perspective""" + def _cmd_checked(self, filter_text="") -> bool: + """List all done location checks from the server's perspective. + Can be given text, which will be used as filter.""" locations = get_checked_checks(self.ctx, self.client.team, self.client.slot) if locations: - texts = [f'Checked: {self.ctx.location_names[location]}' for location in locations] - texts.append(f"Found {len(locations)} done location checks") + names = [self.ctx.location_names[location] for location in locations] + if filter_text: + names = [name for name in names if filter_text in name] + texts = [f'Checked: {name}' for name in names] + if filter_text: + texts.append(f"Found {len(locations)} done location checks, displaying {len(names)} of them.") + else: + texts.append(f"Found {len(locations)} done location checks") self.output_multiple(texts) else: self.output("No done location checks found.") @@ -1403,7 +1445,7 @@ class ClientMessageProcessor(CommonCommandProcessor): if game not in self.ctx.all_item_and_group_names: self.output("Can't look up item/location for unknown game. Hint for ID instead.") return False - names = self.ctx.location_names_for_game(game) \ + names = self.ctx.all_location_and_group_names[game] \ if for_location else \ self.ctx.all_item_and_group_names[game] hint_name, usable, response = get_intended_text(input_text, names) @@ -1419,6 +1461,11 @@ class ClientMessageProcessor(CommonCommandProcessor): hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item_name)) elif not for_location and hint_name in self.ctx.item_names_for_game(game): # item name hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name) + elif hint_name in self.ctx.location_name_groups[game]: # location group name + hints = [] + for loc_name in self.ctx.location_name_groups[game][hint_name]: + if loc_name in self.ctx.location_names_for_game(game): + hints.extend(collect_hint_location_name(self.ctx, self.client.team, self.client.slot, loc_name)) else: # location name hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name) diff --git a/NetUtils.py b/NetUtils.py index ca44fdea22..2b9a653123 100644 --- a/NetUtils.py +++ b/NetUtils.py @@ -35,7 +35,7 @@ class SlotType(enum.IntFlag): @property def always_goal(self) -> bool: - """Mark this slot has having reached its goal instantly.""" + """Mark this slot as having reached its goal instantly.""" return self.value != 0b01 diff --git a/OoTClient.py b/OoTClient.py index 696bf39016..f8a052402f 100644 --- a/OoTClient.py +++ b/OoTClient.py @@ -289,7 +289,10 @@ async def patch_and_run_game(apz5_file): decomp_path = base_name + '-decomp.z64' comp_path = base_name + '.z64' # Load vanilla ROM, patch file, compress ROM - rom = Rom(Utils.local_path(Utils.get_options()["oot_options"]["rom_file"])) + rom_file_name = Utils.get_options()["oot_options"]["rom_file"] + if not os.path.exists(rom_file_name): + rom_file_name = Utils.user_path(rom_file_name) + rom = Rom(rom_file_name) apply_patch_file(rom, apz5_file, sub_file=(os.path.basename(base_name) + '.zpf' if zipfile.is_zipfile(apz5_file) diff --git a/Options.py b/Options.py index 78cbd3f034..b6c55468ca 100644 --- a/Options.py +++ b/Options.py @@ -1,5 +1,6 @@ from __future__ import annotations import abc +import logging from copy import deepcopy import math import numbers @@ -9,6 +10,10 @@ import random from schema import Schema, And, Or, Optional from Utils import get_fuzzy_results +if typing.TYPE_CHECKING: + from BaseClasses import PlandoOptions + from worlds.AutoWorld import World + class AssembleOptions(abc.ABCMeta): def __new__(mcs, name, bases, attrs): @@ -95,11 +100,11 @@ class Option(typing.Generic[T], metaclass=AssembleOptions): supports_weighting = True # filled by AssembleOptions: - name_lookup: typing.Dict[int, str] + name_lookup: typing.Dict[T, str] options: typing.Dict[str, int] def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.get_current_option_name()})" + return f"{self.__class__.__name__}({self.current_option_name})" def __hash__(self) -> int: return hash(self.value) @@ -109,7 +114,14 @@ class Option(typing.Generic[T], metaclass=AssembleOptions): return self.name_lookup[self.value] def get_current_option_name(self) -> str: - """For display purposes.""" + """Deprecated. use current_option_name instead. TODO remove around 0.4""" + logging.warning(DeprecationWarning(f"get_current_option_name for {self.__class__.__name__} is deprecated." + f" use current_option_name instead. Worlds should use {self}.current_key")) + return self.current_option_name + + @property + def current_option_name(self) -> str: + """For display purposes. Worlds should be using current_key.""" return self.get_option_name(self.value) @classmethod @@ -131,17 +143,14 @@ class Option(typing.Generic[T], metaclass=AssembleOptions): ... if typing.TYPE_CHECKING: - from Generate import PlandoOptions - from worlds.AutoWorld import World - - def verify(self, world: World, player_name: str, plando_options: PlandoOptions) -> None: + def verify(self, world: typing.Type[World], player_name: str, plando_options: PlandoOptions) -> None: pass else: def verify(self, *args, **kwargs) -> None: pass -class FreeText(Option): +class FreeText(Option[str]): """Text option that allows users to enter strings. Needs to be validated by the world or option definition.""" @@ -162,7 +171,7 @@ class FreeText(Option): return cls.from_text(str(data)) @classmethod - def get_option_name(cls, value: T) -> str: + def get_option_name(cls, value: str) -> str: return value @@ -424,6 +433,7 @@ class Choice(NumericOption): class TextChoice(Choice): """Allows custom string input and offers choices. Choices will resolve to int and text will resolve to string""" + value: typing.Union[str, int] def __init__(self, value: typing.Union[str, int]): assert isinstance(value, str) or isinstance(value, int), \ @@ -434,8 +444,7 @@ class TextChoice(Choice): def current_key(self) -> str: if isinstance(self.value, str): return self.value - else: - return self.name_lookup[self.value] + return super().current_key @classmethod def from_text(cls, text: str) -> TextChoice: @@ -450,7 +459,7 @@ class TextChoice(Choice): def get_option_name(cls, value: T) -> str: if isinstance(value, str): return value - return cls.name_lookup[value] + return super().get_option_name(value) def __eq__(self, other: typing.Any): if isinstance(other, self.__class__): @@ -573,12 +582,11 @@ class PlandoBosses(TextChoice, metaclass=BossMeta): def valid_location_name(cls, value: str) -> bool: return value in cls.locations - def verify(self, world, player_name: str, plando_options) -> None: + def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None: if isinstance(self.value, int): return - from Generate import PlandoOptions + from BaseClasses import PlandoOptions if not(PlandoOptions.bosses & plando_options): - import logging # plando is disabled but plando options were given so pull the option and change it to an int option = self.value.split(";")[-1] self.value = self.options[option] @@ -716,7 +724,7 @@ class VerifyKeys: value: typing.Any @classmethod - def verify_keys(cls, data): + def verify_keys(cls, data: typing.List[str]): if cls.valid_keys: data = set(data) dataset = set(word.casefold() for word in data) if cls.valid_keys_casefold else set(data) @@ -725,12 +733,17 @@ class VerifyKeys: raise Exception(f"Found unexpected key {', '.join(extra)} in {cls}. " f"Allowed keys: {cls.valid_keys}.") - def verify(self, world, player_name: str, plando_options) -> None: + def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None: if self.convert_name_groups and self.verify_item_name: new_value = type(self.value)() # empty container of whatever value is for item_name in self.value: new_value |= world.item_name_groups.get(item_name, {item_name}) self.value = new_value + elif self.convert_name_groups and self.verify_location_name: + new_value = type(self.value)() + for loc_name in self.value: + new_value |= world.location_name_groups.get(loc_name, {loc_name}) + self.value = new_value if self.verify_item_name: for item_name in self.value: if item_name not in world.item_names: @@ -830,7 +843,9 @@ class OptionSet(Option[typing.Set[str]], VerifyKeys): return item in self.value -local_objective = Toggle # local triforce pieces, local dungeon prizes etc. +class ItemSet(OptionSet): + verify_item_name = True + convert_name_groups = True class Accessibility(Choice): @@ -873,11 +888,6 @@ common_options = { } -class ItemSet(OptionSet): - verify_item_name = True - convert_name_groups = True - - class LocalItems(ItemSet): """Forces these items to be in their native world.""" display_name = "Local Items" @@ -899,22 +909,24 @@ class StartHints(ItemSet): display_name = "Start Hints" -class StartLocationHints(OptionSet): +class LocationSet(OptionSet): + verify_location_name = True + convert_name_groups = True + + +class StartLocationHints(LocationSet): """Start with these locations and their item prefilled into the !hint command""" display_name = "Start Location Hints" - verify_location_name = True -class ExcludeLocations(OptionSet): +class ExcludeLocations(LocationSet): """Prevent these locations from having an important item""" display_name = "Excluded Locations" - verify_location_name = True -class PriorityLocations(OptionSet): +class PriorityLocations(LocationSet): """Prevent these locations from having an unimportant item""" display_name = "Priority Locations" - verify_location_name = True class DeathLink(Toggle): @@ -955,7 +967,7 @@ class ItemLinks(OptionList): pool |= {item_name} return pool - def verify(self, world, player_name: str, plando_options) -> None: + def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None: link: dict super(ItemLinks, self).verify(world, player_name, plando_options) existing_links = set() diff --git a/PokemonClient.py b/PokemonClient.py index eb1f124391..e78e76fa00 100644 --- a/PokemonClient.py +++ b/PokemonClient.py @@ -17,7 +17,7 @@ from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandP from worlds.pokemon_rb.locations import location_data from worlds.pokemon_rb.rom import RedDeltaPatch, BlueDeltaPatch -location_map = {"Rod": {}, "EventFlag": {}, "Missable": {}, "Hidden": {}, "list": {}} +location_map = {"Rod": {}, "EventFlag": {}, "Missable": {}, "Hidden": {}, "list": {}, "DexSanityFlag": {}} location_bytes_bits = {} for location in location_data: if location.ram_address is not None: @@ -40,7 +40,7 @@ CONNECTION_INITIAL_STATUS = "Connection has not been initiated" DISPLAY_MSGS = True -SCRIPT_VERSION = 1 +SCRIPT_VERSION = 3 class GBCommandProcessor(ClientCommandProcessor): @@ -70,6 +70,8 @@ class GBContext(CommonContext): self.set_deathlink = False self.client_compatibility_mode = 0 self.items_handling = 0b001 + self.sent_release = False + self.sent_collect = False async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: @@ -124,7 +126,8 @@ def get_payload(ctx: GBContext): "items": [item.item for item in ctx.items_received], "messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items() if key[0] > current_time - 10}, - "deathlink": ctx.deathlink_pending + "deathlink": ctx.deathlink_pending, + "options": ((ctx.permissions['release'] in ('goal', 'enabled')) * 2) + (ctx.permissions['collect'] in ('goal', 'enabled')) } ) ctx.deathlink_pending = False @@ -134,10 +137,13 @@ def get_payload(ctx: GBContext): async def parse_locations(data: List, ctx: GBContext): locations = [] flags = {"EventFlag": data[:0x140], "Missable": data[0x140:0x140 + 0x20], - "Hidden": data[0x140 + 0x20: 0x140 + 0x20 + 0x0E], "Rod": data[0x140 + 0x20 + 0x0E:]} + "Hidden": data[0x140 + 0x20: 0x140 + 0x20 + 0x0E], + "Rod": data[0x140 + 0x20 + 0x0E:0x140 + 0x20 + 0x0E + 0x01]} - if len(flags['Rod']) > 1: - return + if len(data) > 0x140 + 0x20 + 0x0E + 0x01: + flags["DexSanityFlag"] = data[0x140 + 0x20 + 0x0E + 0x01:] + else: + flags["DexSanityFlag"] = [0] * 19 for flag_type, loc_map in location_map.items(): for flag, loc_id in loc_map.items(): @@ -207,6 +213,16 @@ async def gb_sync_task(ctx: GBContext): async_start(parse_locations(data_decoded['locations'], ctx)) if 'deathLink' in data_decoded and data_decoded['deathLink'] and 'DeathLink' in ctx.tags: await ctx.send_death(ctx.auth + " is out of usable PokÊmon! " + ctx.auth + " blacked out!") + if 'options' in data_decoded: + msgs = [] + if data_decoded['options'] & 4 and not ctx.sent_release: + ctx.sent_release = True + msgs.append({"cmd": "Say", "text": "!release"}) + if data_decoded['options'] & 8 and not ctx.sent_collect: + ctx.sent_collect = True + msgs.append({"cmd": "Say", "text": "!collect"}) + if msgs: + await ctx.send_msgs(msgs) if ctx.set_deathlink: await ctx.update_death_link(True) except asyncio.TimeoutError: diff --git a/README.md b/README.md index 42493b5904..9454d0f168 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,15 @@ Currently, the following games are supported: * Overcooked! 2 * Zillion * Lufia II Ancient Cave +* Blasphemous +* Wargroove +* Stardew Valley +* The Legend of Zelda +* The Messenger +* Kingdom Hearts 2 +* The Legend of Zelda: Link's Awakening DX +* Clique +* Adventure For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/SNIClient.py b/SNIClient.py index 8d402b1d5f..f4ad53c617 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -115,8 +115,8 @@ class SNIClientCommandProcessor(ClientCommandProcessor): class SNIContext(CommonContext): command_processor: typing.Type[SNIClientCommandProcessor] = SNIClientCommandProcessor - game = None # set in validate_rom - items_handling = None # set in game_watcher + game: typing.Optional[str] = None # set in validate_rom + items_handling: typing.Optional[int] = None # set in game_watcher snes_connect_task: "typing.Optional[asyncio.Task[None]]" = None snes_autoreconnect_task: typing.Optional["asyncio.Task[None]"] = None diff --git a/Starcraft2Client.py b/Starcraft2Client.py index 3b05f5aa87..cf16405766 100644 --- a/Starcraft2Client.py +++ b/Starcraft2Client.py @@ -52,9 +52,9 @@ class StarcraftClientProcessor(ClientCommandProcessor): """Overrides the current difficulty set for the seed. Takes the argument casual, normal, hard, or brutal""" options = difficulty.split() num_options = len(options) - difficulty_choice = options[0].lower() if num_options > 0: + difficulty_choice = options[0].lower() if difficulty_choice == "casual": self.ctx.difficulty_override = 0 elif difficulty_choice == "normal": @@ -71,7 +71,11 @@ class StarcraftClientProcessor(ClientCommandProcessor): return True else: - self.output("Difficulty needs to be specified in the command.") + if self.ctx.difficulty == -1: + self.output("Please connect to a seed before checking difficulty.") + else: + self.output("Current difficulty: " + ["Casual", "Normal", "Hard", "Brutal"][self.ctx.difficulty]) + self.output("To change the difficulty, add the name of the difficulty after the command.") return False def _cmd_disable_mission_check(self) -> bool: diff --git a/Utils.py b/Utils.py index 098d5f01e7..60b3904ff6 100644 --- a/Utils.py +++ b/Utils.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +import json import typing import builtins import os @@ -38,7 +39,7 @@ class Version(typing.NamedTuple): build: int -__version__ = "0.3.9" +__version__ = "0.4.0" version_tuple = tuplize_version(__version__) is_linux = sys.platform.startswith("linux") @@ -87,7 +88,10 @@ def is_frozen() -> bool: def local_path(*path: str) -> str: - """Returns path to a file in the local Archipelago installation or source.""" + """ + Returns path to a file in the local Archipelago installation or source. + This might be read-only and user_path should be used instead for ROMs, configuration, etc. + """ if hasattr(local_path, 'cached_path'): pass elif is_frozen(): @@ -142,6 +146,17 @@ def user_path(*path: str) -> str: return os.path.join(user_path.cached_path, *path) +def cache_path(*path: str) -> str: + """Returns path to a file in the user's Archipelago cache directory.""" + if hasattr(cache_path, "cached_path"): + pass + else: + import platformdirs + cache_path.cached_path = platformdirs.user_cache_dir("Archipelago", False) + + return os.path.join(cache_path.cached_path, *path) + + def output_path(*path: str) -> str: if hasattr(output_path, 'cached_path'): return os.path.join(output_path.cached_path, *path) @@ -195,11 +210,11 @@ def get_public_ipv4() -> str: ip = socket.gethostbyname(socket.gethostname()) ctx = get_cert_none_ssl_context() try: - ip = urllib.request.urlopen("https://checkip.amazonaws.com/", context=ctx).read().decode("utf8").strip() + ip = urllib.request.urlopen("https://checkip.amazonaws.com/", context=ctx, timeout=10).read().decode("utf8").strip() except Exception as e: # noinspection PyBroadException try: - ip = urllib.request.urlopen("https://v4.ident.me", context=ctx).read().decode("utf8").strip() + ip = urllib.request.urlopen("https://v4.ident.me", context=ctx, timeout=10).read().decode("utf8").strip() except Exception: logging.exception(e) pass # we could be offline, in a local game, so no point in erroring out @@ -213,7 +228,7 @@ def get_public_ipv6() -> str: ip = socket.gethostbyname(socket.gethostname()) ctx = get_cert_none_ssl_context() try: - ip = urllib.request.urlopen("https://v6.ident.me", context=ctx).read().decode("utf8").strip() + ip = urllib.request.urlopen("https://v6.ident.me", context=ctx, timeout=10).read().decode("utf8").strip() except Exception as e: logging.exception(e) pass # we could be offline, in a local game, or ipv6 may not be available @@ -248,6 +263,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, @@ -310,9 +328,20 @@ def get_default_options() -> OptionsType: "lufia2ac_options": { "rom_file": "Lufia II - Rise of the Sinistrals (USA).sfc", }, + "tloz_options": { + "rom_file": "Legend of Zelda, The (U) (PRG0) [!].nes", + "rom_start": True, + "display_msgs": True, + }, "wargroove_options": { "root_directory": "C:/Program Files (x86)/Steam/steamapps/common/Wargroove" - } + }, + "adventure_options": { + "rom_file": "ADVNTURE.BIN", + "display_msgs": True, + "rom_start": True, + "rom_args": "" + }, } return options @@ -380,6 +409,45 @@ def persistent_load() -> typing.Dict[str, dict]: return storage +def get_file_safe_name(name: str) -> str: + return "".join(c for c in name if c not in '<>:"/\\|?*') + + +def load_data_package_for_checksum(game: str, checksum: typing.Optional[str]) -> Dict[str, Any]: + if checksum and game: + if checksum != get_file_safe_name(checksum): + raise ValueError(f"Bad symbols in checksum: {checksum}") + path = cache_path("datapackage", get_file_safe_name(game), f"{checksum}.json") + if os.path.exists(path): + try: + with open(path, "r", encoding="utf-8-sig") as f: + return json.load(f) + except Exception as e: + logging.debug(f"Could not load data package: {e}") + + # fall back to old cache + cache = persistent_load().get("datapackage", {}).get("games", {}).get(game, {}) + if cache.get("checksum") == checksum: + return cache + + # cache does not match + return {} + + +def store_data_package_for_checksum(game: str, data: typing.Dict[str, Any]) -> None: + checksum = data.get("checksum") + if checksum and game: + if checksum != get_file_safe_name(checksum): + raise ValueError(f"Bad symbols in checksum: {checksum}") + game_folder = cache_path("datapackage", get_file_safe_name(game)) + os.makedirs(game_folder, exist_ok=True) + try: + with open(os.path.join(game_folder, f"{checksum}.json"), "w", encoding="utf-8-sig") as f: + json.dump(data, f, ensure_ascii=False, separators=(",", ":")) + except Exception as e: + logging.debug(f"Could not store data package: {e}") + + def get_adjuster_settings(game_name: str) -> typing.Dict[str, typing.Any]: adjuster_settings = persistent_load().get("adjuster", {}).get(game_name, {}) return adjuster_settings diff --git a/WargrooveClient.py b/WargrooveClient.py index fec20cc861..16bfeb15ab 100644 --- a/WargrooveClient.py +++ b/WargrooveClient.py @@ -1,4 +1,6 @@ from __future__ import annotations + +import atexit import os import sys import asyncio @@ -78,17 +80,18 @@ class WargrooveContext(CommonContext): # self.game_communication_path: files go in this path to pass data between us and the actual game if "appdata" in os.environ: options = Utils.get_options() - root_directory = options["wargroove_options"]["root_directory"].replace("/", "\\") - data_directory = "lib\\worlds\\wargroove\\data\\" - dev_data_directory = "worlds\\wargroove\\data\\" - appdata_wargroove = os.path.expandvars("%APPDATA%\\Chucklefish\\Wargroove\\") - if not os.path.isfile(root_directory + "\\win64_bin\\wargroove64.exe"): + root_directory = os.path.join(options["wargroove_options"]["root_directory"]) + data_directory = os.path.join("lib", "worlds", "wargroove", "data") + dev_data_directory = os.path.join("worlds", "wargroove", "data") + appdata_wargroove = os.path.expandvars(os.path.join("%APPDATA%", "Chucklefish", "Wargroove")) + if not os.path.isfile(os.path.join(root_directory, "win64_bin", "wargroove64.exe")): print_error_and_close("WargrooveClient couldn't find wargroove64.exe. " "Unable to infer required game_communication_path") - self.game_communication_path = root_directory + "\\AP" + self.game_communication_path = os.path.join(root_directory, "AP") if not os.path.exists(self.game_communication_path): os.makedirs(self.game_communication_path) - + self.remove_communication_files() + atexit.register(self.remove_communication_files) if not os.path.isdir(appdata_wargroove): print_error_and_close("WargrooveClient couldn't find Wargoove in appdata!" "Boot Wargroove and then close it to attempt to fix this error") @@ -109,10 +112,7 @@ class WargrooveContext(CommonContext): async def connection_closed(self): await super(WargrooveContext, self).connection_closed() - for root, dirs, files in os.walk(self.game_communication_path): - for file in files: - if file.find("obtain") <= -1: - os.remove(root + "/" + file) + self.remove_communication_files() @property def endpoints(self): @@ -123,10 +123,12 @@ class WargrooveContext(CommonContext): async def shutdown(self): await super(WargrooveContext, self).shutdown() + self.remove_communication_files() + + def remove_communication_files(self): for root, dirs, files in os.walk(self.game_communication_path): for file in files: - if file.find("obtain") <= -1: - os.remove(root+"/"+file) + os.remove(root + "/" + file) def on_package(self, cmd: str, args: dict): if cmd in {"Connected"}: diff --git a/WebHost.py b/WebHost.py index d098f6e7fb..40d366a02f 100644 --- a/WebHost.py +++ b/WebHost.py @@ -33,6 +33,11 @@ def get_app(): import yaml app.config.from_file(configpath, yaml.safe_load) logging.info(f"Updated config from {configpath}") + if not app.config["HOST_ADDRESS"]: + logging.info("Getting public IP, as HOST_ADDRESS is empty.") + app.config["HOST_ADDRESS"] = Utils.get_public_ipv4() + logging.info(f"HOST_ADDRESS was set to {app.config['HOST_ADDRESS']}") + db.bind(**app.config["PONY"]) db.generate_mapping(create_tables=True) return app diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index e8e5b59d89..8bd3609c1d 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -51,7 +51,7 @@ app.config["PONY"] = { app.config["MAX_ROLL"] = 20 app.config["CACHE_TYPE"] = "flask_caching.backends.SimpleCache" app.config["JSON_AS_ASCII"] = False -app.config["PATCH_TARGET"] = "archipelago.gg" +app.config["HOST_ADDRESS"] = "" cache = Cache(app) Compress(app) diff --git a/WebHostLib/api/__init__.py b/WebHostLib/api/__init__.py index eac19d8456..102c3a49f6 100644 --- a/WebHostLib/api/__init__.py +++ b/WebHostLib/api/__init__.py @@ -39,12 +39,21 @@ def get_datapackage(): @api_endpoints.route('/datapackage_version') @cache.cached() - def get_datapackage_versions(): - from worlds import network_data_package, AutoWorldRegister + from worlds import AutoWorldRegister version_package = {game: world.data_version for game, world in AutoWorldRegister.world_types.items()} return version_package +@api_endpoints.route('/datapackage_checksum') +@cache.cached() +def get_datapackage_checksums(): + from worlds import network_data_package + version_package = { + game: game_data["checksum"] for game, game_data in network_data_package["games"].items() + } + return version_package + + from . import generate, user # trigger registration diff --git a/WebHostLib/autolauncher.py b/WebHostLib/autolauncher.py index 4cf7243302..484755b3c3 100644 --- a/WebHostLib/autolauncher.py +++ b/WebHostLib/autolauncher.py @@ -179,6 +179,7 @@ class MultiworldInstance(): self.ponyconfig = config["PONY"] self.cert = config["SELFLAUNCHCERT"] self.key = config["SELFLAUNCHKEY"] + self.host = config["HOST_ADDRESS"] def start(self): if self.process and self.process.is_alive(): @@ -187,7 +188,7 @@ class MultiworldInstance(): logging.info(f"Spinning up {self.room_id}") process = multiprocessing.Process(group=None, target=run_server_process, args=(self.room_id, self.ponyconfig, get_static_server_data(), - self.cert, self.key), + self.cert, self.key, self.host), name="MultiHost") process.start() # bind after start to prevent thread sync issues with guardian. diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index 9c21fca4f9..b34e196178 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -19,7 +19,7 @@ import Utils from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor, load_server_cert from Utils import get_public_ipv4, get_public_ipv6, restricted_loads, cache_argsless -from .models import Room, Command, db +from .models import Command, GameDataPackage, Room, db class CustomClientMessageProcessor(ClientMessageProcessor): @@ -92,7 +92,21 @@ class WebHostContext(Context): else: self.port = get_random_port() - return self._load(self.decompress(room.seed.multidata), True) + multidata = self.decompress(room.seed.multidata) + game_data_packages = {} + for game in list(multidata.get("datapackage", {})): + game_data = multidata["datapackage"][game] + if "checksum" in game_data: + if self.gamespackage.get(game, {}).get("checksum") == game_data["checksum"]: + # non-custom. remove from multidata + # games package could be dropped from static data once all rooms embed data package + del multidata["datapackage"][game] + else: + row = GameDataPackage.get(checksum=game_data["checksum"]) + if row: # None if rolled on >= 0.3.9 but uploaded to <= 0.3.8. multidata should be complete + game_data_packages[game] = Utils.restricted_loads(row.data) + + return self._load(multidata, game_data_packages, True) @db_session def init_save(self, enabled: bool = True): @@ -131,6 +145,8 @@ def get_static_server_data() -> dict: "gamespackage": worlds.network_data_package["games"], "item_name_groups": {world_name: world.item_name_groups for world_name, world in worlds.AutoWorldRegister.world_types.items()}, + "location_name_groups": {world_name: world.location_name_groups for world_name, world in + worlds.AutoWorldRegister.world_types.items()}, } for world_name, world in worlds.AutoWorldRegister.world_types.items(): @@ -140,7 +156,8 @@ def get_static_server_data() -> dict: def run_server_process(room_id, ponyconfig: dict, static_server_data: dict, - cert_file: typing.Optional[str], cert_key_file: typing.Optional[str]): + cert_file: typing.Optional[str], cert_key_file: typing.Optional[str], + host: str): # establish DB connection for multidata and multisave db.bind(**ponyconfig) db.generate_mapping(check_tables=False) @@ -165,17 +182,18 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict, for wssocket in ctx.server.ws_server.sockets: socketname = wssocket.getsockname() if wssocket.family == socket.AF_INET6: - logging.info(f'Hosting game at [{get_public_ipv6()}]:{socketname[1]}') # Prefer IPv4, as most users seem to not have working ipv6 support if not port: port = socketname[1] elif wssocket.family == socket.AF_INET: - logging.info(f'Hosting game at {get_public_ipv4()}:{socketname[1]}') port = socketname[1] if port: + logging.info(f'Hosting game at {host}:{port}') with db_session: room = Room.get(id=ctx.room_id) room.last_port = port + else: + logging.exception("Could not determine port. Likely hosting failure.") with db_session: ctx.auto_shutdown = Room.get(id=room_id).timeout ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, [])) @@ -186,6 +204,11 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict, with Locker(room_id): try: asyncio.run(main()) + except KeyboardInterrupt: + with db_session: + room = Room.get(id=room_id) + # ensure the Room does not spin up again on its own, minute of safety buffer + room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(minutes=1, seconds=room.timeout) except: with db_session: room = Room.get(id=room_id) diff --git a/WebHostLib/downloads.py b/WebHostLib/downloads.py index d9600d2d16..5cf503be1b 100644 --- a/WebHostLib/downloads.py +++ b/WebHostLib/downloads.py @@ -26,7 +26,7 @@ def download_patch(room_id, patch_id): with zipfile.ZipFile(filelike, "a") as zf: with zf.open("archipelago.json", "r") as f: manifest = json.load(f) - manifest["server"] = f"{app.config['PATCH_TARGET']}:{last_port}" if last_port else None + manifest["server"] = f"{app.config['HOST_ADDRESS']}:{last_port}" if last_port else None with zipfile.ZipFile(new_file, "w") as new_zip: for file in zf.infolist(): if file.filename == "archipelago.json": @@ -64,7 +64,7 @@ def download_slot_file(room_id, player_id: int): if slot_data.game == "Minecraft": from worlds.minecraft import mc_update_output fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apmc" - data = mc_update_output(slot_data.data, server=app.config['PATCH_TARGET'], port=room.last_port) + data = mc_update_output(slot_data.data, server=app.config['HOST_ADDRESS'], port=room.last_port) return send_file(io.BytesIO(data), as_attachment=True, download_name=fname) elif slot_data.game == "Factorio": with zipfile.ZipFile(io.BytesIO(slot_data.data)) as zf: @@ -88,6 +88,8 @@ def download_slot_file(room_id, player_id: int): fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_SP.apsm64ex" elif slot_data.game == "Dark Souls III": fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}.json" + elif slot_data.game == "Kingdom Hearts 2": + fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.zip" else: return "Game download not supported." return send_file(io.BytesIO(slot_data.data), as_attachment=True, download_name=fname) diff --git a/WebHostLib/models.py b/WebHostLib/models.py index dbd03b166c..eba5c4eb4d 100644 --- a/WebHostLib/models.py +++ b/WebHostLib/models.py @@ -56,3 +56,8 @@ class Generation(db.Entity): options = Required(buffer, lazy=True) meta = Required(LongStr, default=lambda: "{\"race\": false}") state = Required(int, default=0, index=True) + + +class GameDataPackage(db.Entity): + checksum = PrimaryKey(str) + data = Required(bytes) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 8f366d4fbf..a4d7ccc17c 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -11,7 +11,7 @@ from Utils import __version__, local_path from worlds.AutoWorld import AutoWorldRegister handled_in_js = {"start_inventory", "local_items", "non_local_items", "start_hints", "start_location_hints", - "exclude_locations"} + "exclude_locations", "priority_locations"} def create(): @@ -88,7 +88,7 @@ def create(): if option_name in handled_in_js: pass - elif option.options: + elif issubclass(option, Options.Choice) or issubclass(option, Options.Toggle): game_options[option_name] = this_option = { "type": "select", "displayName": option.display_name if hasattr(option, "display_name") else option_name, @@ -98,15 +98,15 @@ def create(): } for sub_option_id, sub_option_name in option.name_lookup.items(): - this_option["options"].append({ - "name": option.get_option_name(sub_option_id), - "value": sub_option_name, - }) - + if sub_option_name != "random": + this_option["options"].append({ + "name": option.get_option_name(sub_option_id), + "value": sub_option_name, + }) if sub_option_id == option.default: this_option["defaultValue"] = sub_option_name - if option.default == "random": + if not this_option["defaultValue"]: this_option["defaultValue"] = "random" elif issubclass(option, Options.Range): @@ -126,21 +126,21 @@ def create(): for key, val in option.special_range_names.items(): game_options[option_name]["value_names"][key] = val - elif getattr(option, "verify_item_name", False): + elif issubclass(option, Options.ItemSet): game_options[option_name] = { "type": "items-list", "displayName": option.display_name if hasattr(option, "display_name") else option_name, "description": get_html_doc(option), } - elif getattr(option, "verify_location_name", False): + elif issubclass(option, Options.LocationSet): game_options[option_name] = { "type": "locations-list", "displayName": option.display_name if hasattr(option, "display_name") else option_name, "description": get_html_doc(option), } - elif issubclass(option, Options.OptionList) or issubclass(option, Options.OptionSet): + elif issubclass(option, Options.VerifyKeys): if option.valid_keys: game_options[option_name] = { "type": "custom-list", @@ -160,6 +160,14 @@ def create(): json.dump(player_settings, f, indent=2, separators=(',', ': ')) if not world.hidden and world.web.settings_page is True: + # Add the random option to Choice, TextChoice, and Toggle settings + for option in game_options.values(): + if option["type"] == "select": + option["options"].append({"name": "Random", "value": "random"}) + + if not option["defaultValue"]: + option["defaultValue"] = "random" + weighted_settings["baseOptions"]["game"][game_name] = 0 weighted_settings["games"][game_name] = {} weighted_settings["games"][game_name]["gameSettings"] = game_options diff --git a/WebHostLib/requirements.txt b/WebHostLib/requirements.txt index 2397bf91b4..d5c1719863 100644 --- a/WebHostLib/requirements.txt +++ b/WebHostLib/requirements.txt @@ -1,7 +1,7 @@ -flask>=2.2.2 +flask>=2.2.3 pony>=0.7.16 waitress>=2.1.2 -Flask-Caching>=2.0.1 +Flask-Caching>=2.0.2 Flask-Compress>=1.13 -Flask-Limiter>=2.8.1 -bokeh>=3.0.2 +Flask-Limiter>=3.3.0 +bokeh>=3.1.0 diff --git a/WebHostLib/static/assets/baseHeader.js b/WebHostLib/static/assets/baseHeader.js new file mode 100644 index 0000000000..7c9be77840 --- /dev/null +++ b/WebHostLib/static/assets/baseHeader.js @@ -0,0 +1,40 @@ +window.addEventListener('load', () => { + // Mobile menu handling + const menuButton = document.getElementById('base-header-mobile-menu-button'); + const mobileMenu = document.getElementById('base-header-mobile-menu'); + + menuButton.addEventListener('click', (evt) => { + evt.preventDefault(); + evt.stopPropagation(); + + if (!mobileMenu.style.display || mobileMenu.style.display === 'none') { + return mobileMenu.style.display = 'flex'; + } + + mobileMenu.style.display = 'none'; + }); + + window.addEventListener('resize', () => { + mobileMenu.style.display = 'none'; + }); + + // Popover handling + const popoverText = document.getElementById('base-header-popover-text'); + const popoverMenu = document.getElementById('base-header-popover-menu'); + + popoverText.addEventListener('click', (evt) => { + evt.preventDefault(); + evt.stopPropagation(); + + if (!popoverMenu.style.display || popoverMenu.style.display === 'none') { + return popoverMenu.style.display = 'flex'; + } + + popoverMenu.style.display = 'none'; + }); + + document.body.addEventListener('click', () => { + mobileMenu.style.display = 'none'; + popoverMenu.style.display = 'none'; + }); +}); diff --git a/WebHostLib/static/assets/checksfinderTracker.js b/WebHostLib/static/assets/checksfinderTracker.js new file mode 100644 index 0000000000..61cf1e1559 --- /dev/null +++ b/WebHostLib/static/assets/checksfinderTracker.js @@ -0,0 +1,49 @@ +window.addEventListener('load', () => { + // Reload tracker every 60 seconds + const url = window.location; + setInterval(() => { + const ajax = new XMLHttpRequest(); + ajax.onreadystatechange = () => { + if (ajax.readyState !== 4) { return; } + + // Create a fake DOM using the returned HTML + const domParser = new DOMParser(); + const fakeDOM = domParser.parseFromString(ajax.responseText, 'text/html'); + + // Update item tracker + document.getElementById('inventory-table').innerHTML = fakeDOM.getElementById('inventory-table').innerHTML; + // Update only counters in the location-table + let counters = document.getElementsByClassName('counter'); + const fakeCounters = fakeDOM.getElementsByClassName('counter'); + for (let i = 0; i < counters.length; i++) { + counters[i].innerHTML = fakeCounters[i].innerHTML; + } + }; + ajax.open('GET', url); + ajax.send(); +}, 60000) + + // Collapsible advancement sections + const categories = document.getElementsByClassName("location-category"); + for (let i = 0; i < categories.length; i++) { + let hide_id = categories[i].id.split('-')[0]; + if (hide_id == 'Total') { + continue; + } + categories[i].addEventListener('click', function() { + // Toggle the advancement list + document.getElementById(hide_id).classList.toggle("hide"); + // Change text of the header + const tab_header = document.getElementById(hide_id+'-header').children[0]; + const orig_text = tab_header.innerHTML; + let new_text; + if (orig_text.includes("â–ŧ")) { + new_text = orig_text.replace("â–ŧ", "▲"); + } + else { + new_text = orig_text.replace("▲", "â–ŧ"); + } + tab_header.innerHTML = new_text; + }); + } +}); diff --git a/WebHostLib/static/assets/lttpMultiTracker.js b/WebHostLib/static/assets/lttpMultiTracker.js new file mode 100644 index 0000000000..e90331028d --- /dev/null +++ b/WebHostLib/static/assets/lttpMultiTracker.js @@ -0,0 +1,6 @@ +window.addEventListener('load', () => { + $(".table-wrapper").scrollsync({ + y_sync: true, + x_sync: true + }); +}); diff --git a/WebHostLib/static/assets/tracker.js b/WebHostLib/static/assets/trackerCommon.js similarity index 97% rename from WebHostLib/static/assets/tracker.js rename to WebHostLib/static/assets/trackerCommon.js index 23e7f979a5..c08590cbf7 100644 --- a/WebHostLib/static/assets/tracker.js +++ b/WebHostLib/static/assets/trackerCommon.js @@ -1,5 +1,7 @@ const adjustTableHeight = () => { const tablesContainer = document.getElementById('tables-container'); + if (!tablesContainer) + return; const upperDistance = tablesContainer.getBoundingClientRect().top; const containerHeight = window.innerHeight - upperDistance; @@ -108,7 +110,7 @@ window.addEventListener('load', () => { const update = () => { const target = $("
"); console.log("Updating Tracker..."); - target.load("/tracker/" + tracker, function (response, status) { + target.load(location.href, function (response, status) { if (status === "success") { target.find(".table").each(function (i, new_table) { const new_trs = $(new_table).find("tbody>tr"); @@ -135,10 +137,5 @@ window.addEventListener('load', () => { tables.draw(); }); - $(".table-wrapper").scrollsync({ - y_sync: true, - x_sync: true - }); - adjustTableHeight(); }); diff --git a/WebHostLib/static/assets/weighted-settings.js b/WebHostLib/static/assets/weighted-settings.js index da4d60fcad..e471e0837a 100644 --- a/WebHostLib/static/assets/weighted-settings.js +++ b/WebHostLib/static/assets/weighted-settings.js @@ -78,8 +78,6 @@ const createDefaultSettings = (settingData) => { break; case 'range': case 'special_range': - newSettings[game][gameSetting][setting.min] = 0; - newSettings[game][gameSetting][setting.max] = 0; newSettings[game][gameSetting]['random'] = 0; newSettings[game][gameSetting]['random-low'] = 0; newSettings[game][gameSetting]['random-high'] = 0; @@ -103,6 +101,7 @@ const createDefaultSettings = (settingData) => { newSettings[game].start_inventory = {}; newSettings[game].exclude_locations = []; + newSettings[game].priority_locations = []; newSettings[game].local_items = []; newSettings[game].non_local_items = []; newSettings[game].start_hints = []; @@ -138,21 +137,28 @@ const buildUI = (settingData) => { expandButton.classList.add('invisible'); gameDiv.appendChild(expandButton); - const weightedSettingsDiv = buildWeightedSettingsDiv(game, settingData.games[game].gameSettings); + settingData.games[game].gameItems.sort((a, b) => (a > b ? 1 : (a < b ? -1 : 0))); + settingData.games[game].gameLocations.sort((a, b) => (a > b ? 1 : (a < b ? -1 : 0))); + + const weightedSettingsDiv = buildWeightedSettingsDiv(game, settingData.games[game].gameSettings, + settingData.games[game].gameItems, settingData.games[game].gameLocations); gameDiv.appendChild(weightedSettingsDiv); - const itemsDiv = buildItemsDiv(game, settingData.games[game].gameItems); - gameDiv.appendChild(itemsDiv); + const itemPoolDiv = buildItemsDiv(game, settingData.games[game].gameItems); + gameDiv.appendChild(itemPoolDiv); const hintsDiv = buildHintsDiv(game, settingData.games[game].gameItems, settingData.games[game].gameLocations); gameDiv.appendChild(hintsDiv); + const locationsDiv = buildLocationsDiv(game, settingData.games[game].gameLocations); + gameDiv.appendChild(locationsDiv); + gamesWrapper.appendChild(gameDiv); collapseButton.addEventListener('click', () => { collapseButton.classList.add('invisible'); weightedSettingsDiv.classList.add('invisible'); - itemsDiv.classList.add('invisible'); + itemPoolDiv.classList.add('invisible'); hintsDiv.classList.add('invisible'); expandButton.classList.remove('invisible'); }); @@ -160,7 +166,7 @@ const buildUI = (settingData) => { expandButton.addEventListener('click', () => { collapseButton.classList.remove('invisible'); weightedSettingsDiv.classList.remove('invisible'); - itemsDiv.classList.remove('invisible'); + itemPoolDiv.classList.remove('invisible'); hintsDiv.classList.remove('invisible'); expandButton.classList.add('invisible'); }); @@ -228,7 +234,7 @@ const buildGameChoice = (games) => { gameChoiceDiv.appendChild(table); }; -const buildWeightedSettingsDiv = (game, settings) => { +const buildWeightedSettingsDiv = (game, settings, gameItems, gameLocations) => { const currentSettings = JSON.parse(localStorage.getItem('weighted-settings')); const settingsWrapper = document.createElement('div'); settingsWrapper.classList.add('settings-wrapper'); @@ -270,7 +276,7 @@ const buildWeightedSettingsDiv = (game, settings) => { range.setAttribute('data-type', setting.type); range.setAttribute('min', 0); range.setAttribute('max', 50); - range.addEventListener('change', updateGameSetting); + range.addEventListener('change', updateRangeSetting); range.value = currentSettings[game][settingName][option.value]; tdMiddle.appendChild(range); tr.appendChild(tdMiddle); @@ -296,33 +302,33 @@ const buildWeightedSettingsDiv = (game, settings) => { if (((setting.max - setting.min) + 1) < 11) { for (let i=setting.min; i <= setting.max; ++i) { const tr = document.createElement('tr'); - const tdLeft = document.createElement('td'); - tdLeft.classList.add('td-left'); - tdLeft.innerText = i; - tr.appendChild(tdLeft); + const tdLeft = document.createElement('td'); + tdLeft.classList.add('td-left'); + tdLeft.innerText = i; + tr.appendChild(tdLeft); - const tdMiddle = document.createElement('td'); - tdMiddle.classList.add('td-middle'); - const range = document.createElement('input'); - range.setAttribute('type', 'range'); - range.setAttribute('id', `${game}-${settingName}-${i}-range`); - range.setAttribute('data-game', game); - range.setAttribute('data-setting', settingName); - range.setAttribute('data-option', i); - range.setAttribute('min', 0); - range.setAttribute('max', 50); - range.addEventListener('change', updateGameSetting); - range.value = currentSettings[game][settingName][i]; - tdMiddle.appendChild(range); - tr.appendChild(tdMiddle); + const tdMiddle = document.createElement('td'); + tdMiddle.classList.add('td-middle'); + const range = document.createElement('input'); + range.setAttribute('type', 'range'); + range.setAttribute('id', `${game}-${settingName}-${i}-range`); + range.setAttribute('data-game', game); + range.setAttribute('data-setting', settingName); + range.setAttribute('data-option', i); + range.setAttribute('min', 0); + range.setAttribute('max', 50); + range.addEventListener('change', updateRangeSetting); + range.value = currentSettings[game][settingName][i] || 0; + tdMiddle.appendChild(range); + tr.appendChild(tdMiddle); - const tdRight = document.createElement('td'); - tdRight.setAttribute('id', `${game}-${settingName}-${i}`) - tdRight.classList.add('td-right'); - tdRight.innerText = range.value; - tr.appendChild(tdRight); + const tdRight = document.createElement('td'); + tdRight.setAttribute('id', `${game}-${settingName}-${i}`) + tdRight.classList.add('td-right'); + tdRight.innerText = range.value; + tr.appendChild(tdRight); - rangeTbody.appendChild(tr); + rangeTbody.appendChild(tr); } } else { const hintText = document.createElement('p'); @@ -379,7 +385,7 @@ const buildWeightedSettingsDiv = (game, settings) => { range.setAttribute('data-option', option); range.setAttribute('min', 0); range.setAttribute('max', 50); - range.addEventListener('change', updateGameSetting); + range.addEventListener('change', updateRangeSetting); range.value = currentSettings[game][settingName][parseInt(option, 10)]; tdMiddle.appendChild(range); tr.appendChild(tdMiddle); @@ -430,7 +436,7 @@ const buildWeightedSettingsDiv = (game, settings) => { range.setAttribute('data-option', option); range.setAttribute('min', 0); range.setAttribute('max', 50); - range.addEventListener('change', updateGameSetting); + range.addEventListener('change', updateRangeSetting); range.value = currentSettings[game][settingName][parseInt(option, 10)]; tdMiddle.appendChild(range); tr.appendChild(tdMiddle); @@ -464,7 +470,17 @@ const buildWeightedSettingsDiv = (game, settings) => { const tr = document.createElement('tr'); const tdLeft = document.createElement('td'); tdLeft.classList.add('td-left'); - tdLeft.innerText = option; + switch(option){ + case 'random': + tdLeft.innerText = 'Random'; + break; + case 'random-low': + tdLeft.innerText = "Random (Low)"; + break; + case 'random-high': + tdLeft.innerText = "Random (High)"; + break; + } tr.appendChild(tdLeft); const tdMiddle = document.createElement('td'); @@ -477,7 +493,7 @@ const buildWeightedSettingsDiv = (game, settings) => { range.setAttribute('data-option', option); range.setAttribute('min', 0); range.setAttribute('max', 50); - range.addEventListener('change', updateGameSetting); + range.addEventListener('change', updateRangeSetting); range.value = currentSettings[game][settingName][option]; tdMiddle.appendChild(range); tr.appendChild(tdMiddle); @@ -495,15 +511,108 @@ const buildWeightedSettingsDiv = (game, settings) => { break; case 'items-list': - // TODO + const itemsList = document.createElement('div'); + itemsList.classList.add('simple-list'); + + Object.values(gameItems).forEach((item) => { + const itemRow = document.createElement('div'); + itemRow.classList.add('list-row'); + + const itemLabel = document.createElement('label'); + itemLabel.setAttribute('for', `${game}-${settingName}-${item}`) + + const itemCheckbox = document.createElement('input'); + itemCheckbox.setAttribute('id', `${game}-${settingName}-${item}`); + itemCheckbox.setAttribute('type', 'checkbox'); + itemCheckbox.setAttribute('data-game', game); + itemCheckbox.setAttribute('data-setting', settingName); + itemCheckbox.setAttribute('data-option', item.toString()); + itemCheckbox.addEventListener('change', updateListSetting); + if (currentSettings[game][settingName].includes(item)) { + itemCheckbox.setAttribute('checked', '1'); + } + + const itemName = document.createElement('span'); + itemName.innerText = item.toString(); + + itemLabel.appendChild(itemCheckbox); + itemLabel.appendChild(itemName); + + itemRow.appendChild(itemLabel); + itemsList.appendChild((itemRow)); + }); + + settingWrapper.appendChild(itemsList); break; case 'locations-list': - // TODO + const locationsList = document.createElement('div'); + locationsList.classList.add('simple-list'); + + Object.values(gameLocations).forEach((location) => { + const locationRow = document.createElement('div'); + locationRow.classList.add('list-row'); + + const locationLabel = document.createElement('label'); + locationLabel.setAttribute('for', `${game}-${settingName}-${location}`) + + const locationCheckbox = document.createElement('input'); + locationCheckbox.setAttribute('id', `${game}-${settingName}-${location}`); + locationCheckbox.setAttribute('type', 'checkbox'); + locationCheckbox.setAttribute('data-game', game); + locationCheckbox.setAttribute('data-setting', settingName); + locationCheckbox.setAttribute('data-option', location.toString()); + locationCheckbox.addEventListener('change', updateListSetting); + if (currentSettings[game][settingName].includes(location)) { + locationCheckbox.setAttribute('checked', '1'); + } + + const locationName = document.createElement('span'); + locationName.innerText = location.toString(); + + locationLabel.appendChild(locationCheckbox); + locationLabel.appendChild(locationName); + + locationRow.appendChild(locationLabel); + locationsList.appendChild((locationRow)); + }); + + settingWrapper.appendChild(locationsList); break; case 'custom-list': - // TODO + const customList = document.createElement('div'); + customList.classList.add('simple-list'); + + Object.values(settings[settingName].options).forEach((listItem) => { + const customListRow = document.createElement('div'); + customListRow.classList.add('list-row'); + + const customItemLabel = document.createElement('label'); + customItemLabel.setAttribute('for', `${game}-${settingName}-${listItem}`) + + const customItemCheckbox = document.createElement('input'); + customItemCheckbox.setAttribute('id', `${game}-${settingName}-${listItem}`); + customItemCheckbox.setAttribute('type', 'checkbox'); + customItemCheckbox.setAttribute('data-game', game); + customItemCheckbox.setAttribute('data-setting', settingName); + customItemCheckbox.setAttribute('data-option', listItem.toString()); + customItemCheckbox.addEventListener('change', updateListSetting); + if (currentSettings[game][settingName].includes(listItem)) { + customItemCheckbox.setAttribute('checked', '1'); + } + + const customItemName = document.createElement('span'); + customItemName.innerText = listItem.toString(); + + customItemLabel.appendChild(customItemCheckbox); + customItemLabel.appendChild(customItemName); + + customListRow.appendChild(customItemLabel); + customList.appendChild((customListRow)); + }); + + settingWrapper.appendChild(customList); break; default: @@ -729,21 +838,22 @@ const buildHintsDiv = (game, items, locations) => { const hintsDescription = document.createElement('p'); hintsDescription.classList.add('setting-description'); hintsDescription.innerText = 'Choose any items or locations to begin the game with the knowledge of where those ' + - ' items are, or what those locations contain. Excluded locations will not contain progression items.'; + ' items are, or what those locations contain.'; hintsDiv.appendChild(hintsDescription); const itemHintsContainer = document.createElement('div'); itemHintsContainer.classList.add('hints-container'); + // Item Hints const itemHintsWrapper = document.createElement('div'); itemHintsWrapper.classList.add('hints-wrapper'); itemHintsWrapper.innerText = 'Starting Item Hints'; const itemHintsDiv = document.createElement('div'); - itemHintsDiv.classList.add('item-container'); + itemHintsDiv.classList.add('simple-list'); items.forEach((item) => { - const itemDiv = document.createElement('div'); - itemDiv.classList.add('hint-div'); + const itemRow = document.createElement('div'); + itemRow.classList.add('list-row'); const itemLabel = document.createElement('label'); itemLabel.setAttribute('for', `${game}-start_hints-${item}`); @@ -757,29 +867,30 @@ const buildHintsDiv = (game, items, locations) => { if (currentSettings[game].start_hints.includes(item)) { itemCheckbox.setAttribute('checked', 'true'); } - itemCheckbox.addEventListener('change', hintChangeHandler); + itemCheckbox.addEventListener('change', updateListSetting); itemLabel.appendChild(itemCheckbox); const itemName = document.createElement('span'); itemName.innerText = item; itemLabel.appendChild(itemName); - itemDiv.appendChild(itemLabel); - itemHintsDiv.appendChild(itemDiv); + itemRow.appendChild(itemLabel); + itemHintsDiv.appendChild(itemRow); }); itemHintsWrapper.appendChild(itemHintsDiv); itemHintsContainer.appendChild(itemHintsWrapper); + // Starting Location Hints const locationHintsWrapper = document.createElement('div'); locationHintsWrapper.classList.add('hints-wrapper'); locationHintsWrapper.innerText = 'Starting Location Hints'; const locationHintsDiv = document.createElement('div'); - locationHintsDiv.classList.add('item-container'); + locationHintsDiv.classList.add('simple-list'); locations.forEach((location) => { - const locationDiv = document.createElement('div'); - locationDiv.classList.add('hint-div'); + const locationRow = document.createElement('div'); + locationRow.classList.add('list-row'); const locationLabel = document.createElement('label'); locationLabel.setAttribute('for', `${game}-start_location_hints-${location}`); @@ -793,29 +904,89 @@ const buildHintsDiv = (game, items, locations) => { if (currentSettings[game].start_location_hints.includes(location)) { locationCheckbox.setAttribute('checked', '1'); } - locationCheckbox.addEventListener('change', hintChangeHandler); + locationCheckbox.addEventListener('change', updateListSetting); locationLabel.appendChild(locationCheckbox); const locationName = document.createElement('span'); locationName.innerText = location; locationLabel.appendChild(locationName); - locationDiv.appendChild(locationLabel); - locationHintsDiv.appendChild(locationDiv); + locationRow.appendChild(locationLabel); + locationHintsDiv.appendChild(locationRow); }); locationHintsWrapper.appendChild(locationHintsDiv); itemHintsContainer.appendChild(locationHintsWrapper); + hintsDiv.appendChild(itemHintsContainer); + return hintsDiv; +}; + +const buildLocationsDiv = (game, locations) => { + const currentSettings = JSON.parse(localStorage.getItem('weighted-settings')); + locations.sort(); // Sort alphabetical, in-place + + const locationsDiv = document.createElement('div'); + locationsDiv.classList.add('locations-div'); + const locationsHeader = document.createElement('h3'); + locationsHeader.innerText = 'Priority & Exclusion Locations'; + locationsDiv.appendChild(locationsHeader); + const locationsDescription = document.createElement('p'); + locationsDescription.classList.add('setting-description'); + locationsDescription.innerText = 'Priority locations guarantee a progression item will be placed there while ' + + 'excluded locations will not contain progression or useful items.'; + locationsDiv.appendChild(locationsDescription); + + const locationsContainer = document.createElement('div'); + locationsContainer.classList.add('locations-container'); + + // Priority Locations + const priorityLocationsWrapper = document.createElement('div'); + priorityLocationsWrapper.classList.add('locations-wrapper'); + priorityLocationsWrapper.innerText = 'Priority Locations'; + + const priorityLocationsDiv = document.createElement('div'); + priorityLocationsDiv.classList.add('simple-list'); + locations.forEach((location) => { + const locationRow = document.createElement('div'); + locationRow.classList.add('list-row'); + + const locationLabel = document.createElement('label'); + locationLabel.setAttribute('for', `${game}-priority_locations-${location}`); + + const locationCheckbox = document.createElement('input'); + locationCheckbox.setAttribute('type', 'checkbox'); + locationCheckbox.setAttribute('id', `${game}-priority_locations-${location}`); + locationCheckbox.setAttribute('data-game', game); + locationCheckbox.setAttribute('data-setting', 'priority_locations'); + locationCheckbox.setAttribute('data-option', location); + if (currentSettings[game].priority_locations.includes(location)) { + locationCheckbox.setAttribute('checked', '1'); + } + locationCheckbox.addEventListener('change', updateListSetting); + locationLabel.appendChild(locationCheckbox); + + const locationName = document.createElement('span'); + locationName.innerText = location; + locationLabel.appendChild(locationName); + + locationRow.appendChild(locationLabel); + priorityLocationsDiv.appendChild(locationRow); + }); + + priorityLocationsWrapper.appendChild(priorityLocationsDiv); + locationsContainer.appendChild(priorityLocationsWrapper); + + // Exclude Locations const excludeLocationsWrapper = document.createElement('div'); - excludeLocationsWrapper.classList.add('hints-wrapper'); + excludeLocationsWrapper.classList.add('locations-wrapper'); excludeLocationsWrapper.innerText = 'Exclude Locations'; const excludeLocationsDiv = document.createElement('div'); - excludeLocationsDiv.classList.add('item-container'); + excludeLocationsDiv.classList.add('simple-list'); locations.forEach((location) => { - const locationDiv = document.createElement('div'); - locationDiv.classList.add('hint-div'); + const locationRow = document.createElement('div'); + locationRow.classList.add('list-row'); const locationLabel = document.createElement('label'); locationLabel.setAttribute('for', `${game}-exclude_locations-${location}`); @@ -829,40 +1000,22 @@ const buildHintsDiv = (game, items, locations) => { if (currentSettings[game].exclude_locations.includes(location)) { locationCheckbox.setAttribute('checked', '1'); } - locationCheckbox.addEventListener('change', hintChangeHandler); + locationCheckbox.addEventListener('change', updateListSetting); locationLabel.appendChild(locationCheckbox); const locationName = document.createElement('span'); locationName.innerText = location; locationLabel.appendChild(locationName); - locationDiv.appendChild(locationLabel); - excludeLocationsDiv.appendChild(locationDiv); + locationRow.appendChild(locationLabel); + excludeLocationsDiv.appendChild(locationRow); }); excludeLocationsWrapper.appendChild(excludeLocationsDiv); - itemHintsContainer.appendChild(excludeLocationsWrapper); + locationsContainer.appendChild(excludeLocationsWrapper); - hintsDiv.appendChild(itemHintsContainer); - return hintsDiv; -}; - -const hintChangeHandler = (evt) => { - const currentSettings = JSON.parse(localStorage.getItem('weighted-settings')); - const game = evt.target.getAttribute('data-game'); - const setting = evt.target.getAttribute('data-setting'); - const option = evt.target.getAttribute('data-option'); - - if (evt.target.checked) { - if (!currentSettings[game][setting].includes(option)) { - currentSettings[game][setting].push(option); - } - } else { - if (currentSettings[game][setting].includes(option)) { - currentSettings[game][setting].splice(currentSettings[game][setting].indexOf(option), 1); - } - } - localStorage.setItem('weighted-settings', JSON.stringify(currentSettings)); + locationsDiv.appendChild(locationsContainer); + return locationsDiv; }; const updateVisibleGames = () => { @@ -908,13 +1061,12 @@ const updateBaseSetting = (event) => { localStorage.setItem('weighted-settings', JSON.stringify(settings)); }; -const updateGameSetting = (evt) => { +const updateRangeSetting = (evt) => { const options = JSON.parse(localStorage.getItem('weighted-settings')); const game = evt.target.getAttribute('data-game'); const setting = evt.target.getAttribute('data-setting'); const option = evt.target.getAttribute('data-option'); document.getElementById(`${game}-${setting}-${option}`).innerText = evt.target.value; - console.log(event); if (evt.action && evt.action === 'rangeDelete') { delete options[game][setting][option]; } else { @@ -923,6 +1075,26 @@ const updateGameSetting = (evt) => { localStorage.setItem('weighted-settings', JSON.stringify(options)); }; +const updateListSetting = (evt) => { + const options = JSON.parse(localStorage.getItem('weighted-settings')); + const game = evt.target.getAttribute('data-game'); + const setting = evt.target.getAttribute('data-setting'); + const option = evt.target.getAttribute('data-option'); + + if (evt.target.checked) { + // If the option is to be enabled and it is already enabled, do nothing + if (options[game][setting].includes(option)) { return; } + + options[game][setting].push(option); + } else { + // If the option is to be disabled and it is already disabled, do nothing + if (!options[game][setting].includes(option)) { return; } + + options[game][setting].splice(options[game][setting].indexOf(option), 1); + } + localStorage.setItem('weighted-settings', JSON.stringify(options)); +}; + const updateItemSetting = (evt) => { const options = JSON.parse(localStorage.getItem('weighted-settings')); const game = evt.target.getAttribute('data-game'); diff --git a/WebHostLib/static/static/button-images/hamburger-menu-icon.png b/WebHostLib/static/static/button-images/hamburger-menu-icon.png new file mode 100644 index 0000000000..f1c9631635 Binary files /dev/null and b/WebHostLib/static/static/button-images/hamburger-menu-icon.png differ diff --git a/WebHostLib/static/static/button-images/popover.png b/WebHostLib/static/static/button-images/popover.png new file mode 100644 index 0000000000..cbc8634104 Binary files /dev/null and b/WebHostLib/static/static/button-images/popover.png differ diff --git a/WebHostLib/static/styles/checksfinderTracker.css b/WebHostLib/static/styles/checksfinderTracker.css new file mode 100644 index 0000000000..e0cde61241 --- /dev/null +++ b/WebHostLib/static/styles/checksfinderTracker.css @@ -0,0 +1,30 @@ +#player-tracker-wrapper{ + margin: 0; +} + +#inventory-table{ + padding: 8px 10px 2px 6px; + background-color: #42b149; + border-radius: 4px; + border: 2px solid black; +} + +#inventory-table tr.column-headers td { + font-size: 1rem; + padding: 0 5rem 0 0; +} + +#inventory-table td{ + padding: 0 0.5rem 0.5rem; + font-family: LexendDeca-Light, monospace; + font-size: 2.5rem; + color: #ffffff; +} + +#inventory-table td img{ + vertical-align: middle; +} + +.hide { + display: none; +} diff --git a/WebHostLib/static/styles/islandFooter.css b/WebHostLib/static/styles/islandFooter.css index 96611f4eac..7d5344a9bb 100644 --- a/WebHostLib/static/styles/islandFooter.css +++ b/WebHostLib/static/styles/islandFooter.css @@ -15,3 +15,33 @@ padding-left: 0.5rem; color: #dfedc6; } +@media all and (max-width: 900px) { + #island-footer{ + font-size: 17px; + font-size: 2vw; + } +} +@media all and (max-width: 768px) { + #island-footer{ + font-size: 15px; + font-size: 2vw; + } +} +@media all and (max-width: 650px) { + #island-footer{ + font-size: 13px; + font-size: 2vw; + } +} +@media all and (max-width: 580px) { + #island-footer{ + font-size: 11px; + font-size: 2vw; + } +} +@media all and (max-width: 512px) { + #island-footer{ + font-size: 9px; + font-size: 2vw; + } +} diff --git a/WebHostLib/static/styles/landing.css b/WebHostLib/static/styles/landing.css index ea142942e1..202c43badd 100644 --- a/WebHostLib/static/styles/landing.css +++ b/WebHostLib/static/styles/landing.css @@ -21,7 +21,6 @@ html{ margin-right: auto; margin-top: 10px; height: 140px; - z-index: 10; } #landing-header h4{ @@ -223,7 +222,7 @@ html{ } #landing{ - width: 700px; + max-width: 700px; min-height: 280px; margin-left: auto; margin-right: auto; diff --git a/WebHostLib/static/styles/themes/base.css b/WebHostLib/static/styles/themes/base.css index fca65a51c1..fdfe56af20 100644 --- a/WebHostLib/static/styles/themes/base.css +++ b/WebHostLib/static/styles/themes/base.css @@ -30,6 +30,8 @@ html{ } #base-header-right{ + display: flex; + flex-direction: row; margin-top: 4px; } @@ -42,7 +44,7 @@ html{ margin-top: 4px; } -#base-header a{ +#base-header a, #base-header-mobile-menu a, #base-header-popover-text{ color: #2f6b83; text-decoration: none; cursor: pointer; @@ -51,3 +53,126 @@ html{ font-family: LondrinaSolid-Light, sans-serif; text-transform: uppercase; } + +#base-header-right-mobile{ + display: none; + margin-top: 2rem; + margin-right: 1rem; +} + +#base-header-mobile-menu{ + display: none; + flex-direction: column; + background-color: #ffffff; + text-align: center; + overflow-y: auto; + z-index: 10000; + width: 100vw; + border-bottom-left-radius: 20px; + border-bottom-right-radius: 20px; + + position: absolute; + top: 7rem; + right: 0; +} + +#base-header-mobile-menu a{ + padding: 3rem 1.5rem; + font-size: 4rem; + line-height: 5rem; + color: #699ca8; + border-top: 1px solid #d3d3d3; +} + +#base-header-mobile-menu :first-child, #base-header-popover-menu :first-child{ + border-top: none; +} + +#base-header-right-mobile img{ + height: 3rem; +} + +#base-header-popover-menu{ + display: none; + flex-direction: column; + position: absolute; + background-color: #fff; + margin-left: -108px; + margin-top: 2.25rem; + border-radius: 10px; + border-left: 2px solid #d0ebe6; + border-bottom: 2px solid #d0ebe6; + border-right: 1px solid #d0ebe6; + filter: drop-shadow(-6px 6px 2px #2e3e83); +} + +#base-header-popover-menu a{ + color: #699ca8; + border-top: 1px solid #d3d3d3; + text-align: center; + font-size: 1.5rem; + line-height: 3rem; + margin-right: 2px; + padding: 0.25rem 1rem; +} + +#base-header-popover-icon { + width: 14px; + margin-bottom: 3px; + margin-left: 2px; +} + +@media all and (max-width: 960px), only screen and (max-device-width: 768px) { + #base-header-right{ + display: none; + } + + #base-header-right-mobile{ + display: unset; + } +} + +@media all and (max-width: 960px){ + #base-header-right-mobile{ + margin-top: 0.5rem; + margin-right: 0; + } + + #base-header-right-mobile img{ + height: 1.5rem; + } + + #base-header-mobile-menu{ + top: 3.3rem; + width: unset; + border-left: 2px solid #d0ebe6; + border-bottom: 2px solid #d0ebe6; + filter: drop-shadow(-6px 6px 2px #2e3e83); + border-top-left-radius: 10px; + } + + #base-header-mobile-menu a{ + font-size: 1.5rem; + line-height: 3rem; + margin: 0; + padding: 0.25rem 1rem; + } +} + +@media only screen and (max-device-width: 768px){ + html{ + padding-top: 260px; + scroll-padding-top: 230px; + } + + #base-header{ + height: 200px; + background-size: auto 200px; + } + + #base-header #site-title img{ + height: calc(38px * 2); + margin-top: 30px; + margin-left: 20px; + } +} diff --git a/WebHostLib/static/styles/tracker.css b/WebHostLib/static/styles/tracker.css index e203d9e97d..0e00553c72 100644 --- a/WebHostLib/static/styles/tracker.css +++ b/WebHostLib/static/styles/tracker.css @@ -119,6 +119,33 @@ img.alttp-sprite { background-color: #d3c97d; } +#tracker-navigation { + display: inline-flex; + background-color: #b0a77d; + margin: 0.5rem; + border-radius: 4px; +} + +.tracker-navigation-button { + display: block; + margin: 4px; + padding-left: 12px; + padding-right: 12px; + border-radius: 4px; + text-align: center; + font-size: 14px; + color: #000; + font-weight: lighter; +} + +.tracker-navigation-button:hover { + background-color: #e2eabb !important; +} + +.tracker-navigation-button.selected { + background-color: rgb(220, 226, 189); +} + @media all and (max-width: 1700px) { table.dataTable thead th.upper-row{ position: -webkit-sticky; diff --git a/WebHostLib/static/styles/weighted-settings.css b/WebHostLib/static/styles/weighted-settings.css index 7639fa1c72..cc5231634e 100644 --- a/WebHostLib/static/styles/weighted-settings.css +++ b/WebHostLib/static/styles/weighted-settings.css @@ -157,41 +157,29 @@ html{ background-color: rgba(0, 0, 0, 0.1); } -#weighted-settings .hints-div{ +#weighted-settings .hints-div, #weighted-settings .locations-div{ margin-top: 2rem; } -#weighted-settings .hints-div h3{ +#weighted-settings .hints-div h3, #weighted-settings .locations-div h3{ margin-bottom: 0.5rem; } -#weighted-settings .hints-div .hints-container{ +#weighted-settings .hints-container, #weighted-settings .locations-container{ display: flex; flex-direction: row; justify-content: space-between; +} + +#weighted-settings .hints-wrapper, #weighted-settings .locations-wrapper{ + width: calc(50% - 0.5rem); font-weight: bold; } -#weighted-settings .hints-div .hints-wrapper{ - width: 32.5%; -} - -#weighted-settings .hints-div .hints-wrapper .hint-div{ - display: flex; - flex-direction: row; - cursor: pointer; - user-select: none; - -moz-user-select: none; -} - -#weighted-settings .hints-div .hints-wrapper .hint-div:hover{ - background-color: rgba(0, 0, 0, 0.1); -} - -#weighted-settings .hints-div .hints-wrapper .hint-div label{ - flex-grow: 1; - padding: 0.125rem 0.5rem; - cursor: pointer; +#weighted-settings .hints-wrapper .simple-list, #weighted-settings .locations-wrapper .simple-list{ + margin-top: 0.25rem; + height: 300px; + font-weight: normal; } #weighted-settings #weighted-settings-button-row{ @@ -280,6 +268,30 @@ html{ flex-direction: column; } +#weighted-settings .simple-list{ + display: flex; + flex-direction: column; + + max-height: 300px; + overflow-y: auto; + border: 1px solid #ffffff; + border-radius: 4px; +} + +#weighted-settings .simple-list .list-row label{ + display: block; + width: calc(100% - 0.5rem); + padding: 0.0625rem 0.25rem; +} + +#weighted-settings .simple-list .list-row label:hover{ + background-color: rgba(0, 0, 0, 0.1); +} + +#weighted-settings .simple-list .list-row label input[type=checkbox]{ + margin-right: 0.5rem; +} + #weighted-settings .invisible{ display: none; } diff --git a/WebHostLib/templates/checksfinderTracker.html b/WebHostLib/templates/checksfinderTracker.html new file mode 100644 index 0000000000..5df77f5e74 --- /dev/null +++ b/WebHostLib/templates/checksfinderTracker.html @@ -0,0 +1,35 @@ + + + + {{ player_name }}'s Tracker + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
Checks Available:Map Bombs:
Checks Available{{ checks_available }}Bombs Remaining{{ bombs_display }}/20
Map Width:Map Height:
Map Width{{ width_display }}/10Map Height{{ height_display }}/10
+
+ + diff --git a/WebHostLib/templates/genericTracker.html b/WebHostLib/templates/genericTracker.html index 508c084e7f..1c2fcd44c0 100644 --- a/WebHostLib/templates/genericTracker.html +++ b/WebHostLib/templates/genericTracker.html @@ -4,7 +4,7 @@ {{ player_name }}'s Tracker - + {% endblock %} {% block body %} diff --git a/WebHostLib/templates/header/baseHeader.html b/WebHostLib/templates/header/baseHeader.html index a76835b5d9..4090ff477f 100644 --- a/WebHostLib/templates/header/baseHeader.html +++ b/WebHostLib/templates/header/baseHeader.html @@ -1,5 +1,6 @@ {% block head %} + {% endblock %} {% block header %} @@ -10,10 +11,32 @@
+
+ Popover Menu + get started +
+
+ supported games + setup guides + generate game + host game + user content +
+ f.a.q + discord +
+
+ + Menu + +
+
supported games setup guides - start playing f.a.q. + generate game + host game + user content discord
diff --git a/WebHostLib/templates/hostRoom.html b/WebHostLib/templates/hostRoom.html index 1c8f3de255..6f02dc0944 100644 --- a/WebHostLib/templates/hostRoom.html +++ b/WebHostLib/templates/hostRoom.html @@ -14,7 +14,7 @@
{% endif %} {% if room.tracker %} - This room has a Multiworld Tracker enabled. + This room has a Multiworld Tracker enabled.
{% endif %} The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity. @@ -25,8 +25,8 @@ The most likely failure reason is that the multiworld is too old to be loaded now. {% elif room.last_port %} You can connect to this room by using - '/connect {{ config['PATCH_TARGET'] }}:{{ room.last_port }}' + data-tooltip="This means address/ip is {{ config['HOST_ADDRESS'] }} and port is {{ room.last_port }}."> + '/connect {{ config['HOST_ADDRESS'] }}:{{ room.last_port }}' in the client.
{% endif %} diff --git a/WebHostLib/templates/tracker.html b/WebHostLib/templates/lttpMultiTracker.html similarity index 97% rename from WebHostLib/templates/tracker.html rename to WebHostLib/templates/lttpMultiTracker.html index 96148e3454..276e1de3ce 100644 --- a/WebHostLib/templates/tracker.html +++ b/WebHostLib/templates/lttpMultiTracker.html @@ -1,14 +1,16 @@ {% extends 'tablepage.html' %} {% block head %} {{ super() }} - Multiworld Tracker + ALttP Multiworld Tracker - + + {% endblock %} {% block body %} {% include 'header/dirtHeader.html' %} + {% include 'multiTrackerNavigation.html' %}
diff --git a/WebHostLib/templates/macros.html b/WebHostLib/templates/macros.html index ba6f33a9d8..b2a0c73344 100644 --- a/WebHostLib/templates/macros.html +++ b/WebHostLib/templates/macros.html @@ -22,7 +22,7 @@ {% for patch in room.seed.slots|list|sort(attribute="player_id") %} {{ patch.player_id }} - {{ patch.player_name }} + {{ patch.player_name }} {{ patch.game }} {% if patch.game == "Minecraft" %} @@ -31,6 +31,9 @@ {% elif patch.game == "Factorio" %} Download Factorio Mod... + {% elif patch.game == "Kingdom Hearts 2" %} + + Download Kingdom Hearts 2 Mod... {% elif patch.game == "Ocarina of Time" %} Download APZ5 File... diff --git a/WebHostLib/templates/multiFactorioTracker.html b/WebHostLib/templates/multiFactorioTracker.html new file mode 100644 index 0000000000..bc0a977ab6 --- /dev/null +++ b/WebHostLib/templates/multiFactorioTracker.html @@ -0,0 +1,44 @@ +{% extends "multiTracker.html" %} +{% block custom_table_headers %} + + Logistic Science Pack + + + Military Science Pack + + + Chemical Science Pack + + + Production Science Pack + + + Utility Science Pack + + + Space Science Pack + +{% endblock %} +{% block custom_table_row scoped %} +{% if games[player] == "Factorio" %} +{% if inventory[team][player][131161] or inventory[team][player][131281] %}✔{% endif %} +{% if inventory[team][player][131172] or inventory[team][player][131281] > 1%}✔{% endif %} +{% if inventory[team][player][131195] or inventory[team][player][131281] > 2%}✔{% endif %} +{% if inventory[team][player][131240] or inventory[team][player][131281] > 3%}✔{% endif %} +{% if inventory[team][player][131240] or inventory[team][player][131281] > 4%}✔{% endif %} +{% if inventory[team][player][131220] or inventory[team][player][131281] > 5%}✔{% endif %} +{% else %} +❌ +❌ +❌ +❌ +❌ +❌ +{% endif %} +{% endblock%} diff --git a/WebHostLib/templates/multiTracker.html b/WebHostLib/templates/multiTracker.html new file mode 100644 index 0000000000..d1e9d8764f --- /dev/null +++ b/WebHostLib/templates/multiTracker.html @@ -0,0 +1,98 @@ +{% extends 'tablepage.html' %} +{% block head %} + {{ super() }} + Multiworld Tracker + + +{% endblock %} + +{% block body %} + {% include 'header/dirtHeader.html' %} + {% include 'multiTrackerNavigation.html' %} +
+
+ + + + Multistream + + + Clicking on a slot's number will bring up a slot-specific auto-tracker. This tracker will automatically update itself periodically. +
+
+ {% for team, players in checks_done.items() %} +
+ + + + + + + {% block custom_table_headers %} + {# implement this block in game-specific multi trackers #} + {% endblock %} + + + + + + + + {%- for player, checks in players.items() -%} + + + + + {% block custom_table_row scoped %} + {# implement this block in game-specific multi trackers #} + {% endblock %} + + + + {%- if activity_timers[team, player] -%} + + {%- else -%} + + {%- endif -%} + + {%- endfor -%} + +
#NameGameChecks%StatusLast
Activity
{{ loop.index }}{{ player_names[(team, loop.index)]|e }}{{ games[player] }}{{ checks["Total"] }}/{{ checks_in_area[player]["Total"] }}{{ percent_total_checks_done[team][player] }}{{ {0: "Disconnected", 5: "Connected", 10: "Ready", 20: "Playing", + 30: "Goal Completed"}.get(states[team, player], "Unknown State") }}{{ activity_timers[team, player].total_seconds() }}None
+
+ {% endfor %} + {% for team, hints in hints.items() %} +
+ + + + + + + + + + + + + {%- for hint in hints -%} + + + + + + + + + {%- endfor -%} + +
FinderReceiverItemLocationEntranceFound
{{ long_player_names[team, hint.finding_player] }}{{ long_player_names[team, hint.receiving_player] }}{{ hint.item|item_name }}{{ hint.location|location_name }}{% if hint.entrance %}{{ hint.entrance }}{% else %}Vanilla{% endif %}{% if hint.found %}✔{% endif %}
+
+ {% endfor %} +
+
+{% endblock %} diff --git a/WebHostLib/templates/multiTrackerNavigation.html b/WebHostLib/templates/multiTrackerNavigation.html new file mode 100644 index 0000000000..7fc405b6fb --- /dev/null +++ b/WebHostLib/templates/multiTrackerNavigation.html @@ -0,0 +1,9 @@ +{%- if enabled_multiworld_trackers|length > 1 -%} +
+ {% for enabled_tracker in enabled_multiworld_trackers %} + {% set tracker_url = url_for(enabled_tracker.endpoint, tracker=room.tracker) %} + {{ enabled_tracker.name }} + {% endfor %} +
+{%- endif -%} diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 4565f6083b..8f9fb14881 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -11,10 +11,10 @@ from werkzeug.exceptions import abort from MultiServer import Context, get_saving_second from NetUtils import SlotType from Utils import restricted_loads -from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name +from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name, network_data_package from worlds.alttp import Items from . import app, cache -from .models import Room +from .models import GameDataPackage, Room alttp_icons = { "Blue Shield": r"https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png", @@ -210,14 +210,6 @@ del data del item -def attribute_item(inventory, team, recipient, item): - target_item = links.get(item, item) - if item in levels: # non-progressive - inventory[team][recipient][target_item] = max(inventory[team][recipient][target_item], levels[item]) - else: - inventory[team][recipient][target_item] += 1 - - def attribute_item_solo(inventory, item): """Adds item to inventory counter, converts everything to progressive.""" target_item = links.get(item, item) @@ -237,14 +229,15 @@ def render_timedelta(delta: datetime.timedelta): @pass_context def get_location_name(context: runtime.Context, loc: int) -> str: + # once all rooms embed data package, the chain lookup can be dropped context_locations = context.get("custom_locations", {}) - return collections.ChainMap(lookup_any_location_id_to_name, context_locations).get(loc, loc) + return collections.ChainMap(context_locations, lookup_any_location_id_to_name).get(loc, loc) @pass_context def get_item_name(context: runtime.Context, item: int) -> str: context_items = context.get("custom_items", {}) - return collections.ChainMap(lookup_any_item_id_to_name, context_items).get(item, item) + return collections.ChainMap(context_items, lookup_any_item_id_to_name).get(item, item) app.jinja_env.filters["location_name"] = get_location_name @@ -282,11 +275,21 @@ def get_static_room_data(room: Room): if slot_info.type == SlotType.group} for game in games.values(): - if game in multidata["datapackage"]: - custom_locations.update( - {id: name for name, id in multidata["datapackage"][game]["location_name_to_id"].items()}) - custom_items.update( - {id: name for name, id in multidata["datapackage"][game]["item_name_to_id"].items()}) + if game not in multidata["datapackage"]: + continue + game_data = multidata["datapackage"][game] + if "checksum" in game_data: + if network_data_package["games"].get(game, {}).get("checksum") == game_data["checksum"]: + # non-custom. remove from multidata + # network_data_package import could be skipped once all rooms embed data package + del multidata["datapackage"][game] + continue + else: + game_data = restricted_loads(GameDataPackage.get(checksum=game_data["checksum"]).data) + custom_locations.update( + {id_: name for name, id_ in game_data["location_name_to_id"].items()}) + custom_items.update( + {id_: name for name, id_ in game_data["item_name_to_id"].items()}) elif "games" in multidata: games = multidata["games"] seed_checks_in_area = checks_in_area.copy() @@ -486,7 +489,7 @@ def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: D "Water Bottle": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/75/Water_Bottle_JE2_BE2.png", "Spyglass": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c1/Spyglass_JE2_BE1.png", "Dragon Egg Shard": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/38/Dragon_Egg_JE4.png", - "Lead": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/1/1f/Lead_JE2_BE2.png", + "Lead": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/1/1f/Lead_JE2_BE2.png", "Saddle": "https://i.imgur.com/2QtDyR0.png", "Channeling Book": "https://i.imgur.com/J3WsYZw.png", "Silk Touch Book": "https://i.imgur.com/iqERxHQ.png", @@ -494,7 +497,7 @@ def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: D } minecraft_location_ids = { - "Story": [42073, 42023, 42027, 42039, 42002, 42009, 42010, 42070, + "Story": [42073, 42023, 42027, 42039, 42002, 42009, 42010, 42070, 42041, 42049, 42004, 42031, 42025, 42029, 42051, 42077], "Nether": [42017, 42044, 42069, 42058, 42034, 42060, 42066, 42076, 42064, 42071, 42021, 42062, 42008, 42061, 42033, 42011, 42006, 42019, 42000, 42040, 42001, 42015, 42104, 42014], @@ -656,7 +659,7 @@ def __renderOoTTracker(multisave: Dict[str, Any], room: Room, locations: Dict[in if base_name == "hookshot": display_data['hookshot_length'] = {0: '', 1: 'H', 2: 'L'}.get(level) - if base_name == "wallet": + if base_name == "wallet": display_data['wallet_size'] = {0: '99', 1: '200', 2: '500', 3: '999'}.get(level) # Determine display for bottles. Show letter if it's obtained, determine bottle count @@ -674,7 +677,6 @@ def __renderOoTTracker(multisave: Dict[str, Any], room: Room, locations: Dict[in } for item_name, item_id in multi_items.items(): base_name = item_name.split()[-1].lower() - count = inventory[item_id] display_data[base_name+"_count"] = inventory[item_id] # Gather dungeon locations @@ -804,7 +806,7 @@ def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations: } timespinner_location_ids = { - "Present": [ + "Present": [ 1337000, 1337001, 1337002, 1337003, 1337004, 1337005, 1337006, 1337007, 1337008, 1337009, 1337010, 1337011, 1337012, 1337013, 1337014, 1337015, 1337016, 1337017, 1337018, 1337019, 1337020, 1337021, 1337022, 1337023, 1337024, 1337025, 1337026, 1337027, 1337028, 1337029, @@ -825,20 +827,20 @@ def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations: 1337150, 1337151, 1337152, 1337153, 1337154, 1337155, 1337171, 1337172, 1337173, 1337174, 1337175], "Ancient Pyramid": [ - 1337236, + 1337236, 1337246, 1337247, 1337248, 1337249] } if(slot_data["DownloadableItems"]): timespinner_location_ids["Present"] += [ 1337156, 1337157, 1337159, - 1337160, 1337161, 1337162, 1337163, 1337164, 1337165, 1337166, 1337167, 1337168, 1337169, + 1337160, 1337161, 1337162, 1337163, 1337164, 1337165, 1337166, 1337167, 1337168, 1337169, 1337170] if(slot_data["Cantoran"]): timespinner_location_ids["Past"].append(1337176) if(slot_data["LoreChecks"]): timespinner_location_ids["Present"] += [ - 1337177, 1337178, 1337179, + 1337177, 1337178, 1337179, 1337180, 1337181, 1337182, 1337183, 1337184, 1337185, 1337186, 1337187] timespinner_location_ids["Past"] += [ 1337188, 1337189, @@ -1219,6 +1221,84 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info, **display_data) +def __renderChecksfinder(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], + inventory: Counter, team: int, player: int, playerName: str, + seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict, saving_second: int) -> str: + + icons = { + "Checks Available": "https://0rganics.org/archipelago/cf/spr_tiles_3.png", + "Map Width": "https://0rganics.org/archipelago/cf/spr_tiles_4.png", + "Map Height": "https://0rganics.org/archipelago/cf/spr_tiles_5.png", + "Map Bombs": "https://0rganics.org/archipelago/cf/spr_tiles_6.png", + + "Nothing": "", + } + + checksfinder_location_ids = { + "Tile 1": 81000, + "Tile 2": 81001, + "Tile 3": 81002, + "Tile 4": 81003, + "Tile 5": 81004, + "Tile 6": 81005, + "Tile 7": 81006, + "Tile 8": 81007, + "Tile 9": 81008, + "Tile 10": 81009, + "Tile 11": 81010, + "Tile 12": 81011, + "Tile 13": 81012, + "Tile 14": 81013, + "Tile 15": 81014, + "Tile 16": 81015, + "Tile 17": 81016, + "Tile 18": 81017, + "Tile 19": 81018, + "Tile 20": 81019, + "Tile 21": 81020, + "Tile 22": 81021, + "Tile 23": 81022, + "Tile 24": 81023, + "Tile 25": 81024, + } + + display_data = {} + + # Multi-items + multi_items = { + "Map Width": 80000, + "Map Height": 80001, + "Map Bombs": 80002 + } + for item_name, item_id in multi_items.items(): + base_name = item_name.split()[-1].lower() + count = inventory[item_id] + display_data[base_name + "_count"] = count + display_data[base_name + "_display"] = count + 5 + + # Get location info + checked_locations = multisave.get("location_checks", {}).get((team, player), set()) + lookup_name = lambda id: lookup_any_location_id_to_name[id] + location_info = {tile_name: {lookup_name(tile_location): (tile_location in checked_locations)} for tile_name, tile_location in checksfinder_location_ids.items() if tile_location in set(locations[player])} + checks_done = {tile_name: len([tile_location]) for tile_name, tile_location in checksfinder_location_ids.items() if tile_location in checked_locations and tile_location in set(locations[player])} + checks_done['Total'] = len(checked_locations) + checks_in_area = checks_done + + # Calculate checks available + display_data["checks_unlocked"] = min(display_data["width_count"] + display_data["height_count"] + display_data["bombs_count"] + 5, 25) + display_data["checks_available"] = max(display_data["checks_unlocked"] - len(checked_locations), 0) + + # Victory condition + game_state = multisave.get("client_game_state", {}).get((team, player), 0) + display_data['game_finished'] = game_state == 30 + + return render_template("checksfinderTracker.html", + inventory=inventory, icons=icons, + acquired_items={lookup_any_item_id_to_name[id] for id in inventory if + id in lookup_any_item_id_to_name}, + player=player, team=team, room=room, player_name=playerName, + checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info, + **display_data) def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], inventory: Counter, team: int, player: int, playerName: str, @@ -1245,19 +1325,33 @@ def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dic custom_items=custom_items, custom_locations=custom_locations) -@app.route('/tracker/') -@cache.memoize(timeout=1) # multisave is currently created at most every minute -def getTracker(tracker: UUID): +def get_enabled_multiworld_trackers(room: Room, current: str): + enabled = [ + { + "name": "Generic", + "endpoint": "get_multiworld_tracker", + "current": current == "Generic" + } + ] + for game_name, endpoint in multi_trackers.items(): + if any(slot.game == game_name for slot in room.seed.slots) or current == game_name: + enabled.append({ + "name": game_name, + "endpoint": endpoint.__name__, + "current": current == game_name} + ) + return enabled + + +def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[str, typing.Any]]: room: Room = Room.get(tracker=tracker) if not room: - abort(404) - locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \ + return None + + locations, names, use_door_tracker, checks_in_area, player_location_to_area, \ precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \ get_static_room_data(room) - inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1) if playernumber not in groups} - for teamnumber, team in enumerate(names)} - checks_done = {teamnumber: {playernumber: {loc_name: 0 for loc_name in default_locations} for playernumber in range(1, len(team) + 1) if playernumber not in groups} for teamnumber, team in enumerate(names)} @@ -1266,7 +1360,6 @@ def getTracker(tracker: UUID): for playernumber in range(1, len(team) + 1) if playernumber not in groups} for teamnumber, team in enumerate(names)} - hints = {team: set() for team in range(len(names))} if room.multisave: multisave = restricted_loads(room.multisave) @@ -1276,6 +1369,128 @@ def getTracker(tracker: UUID): for (team, slot), slot_hints in multisave["hints"].items(): hints[team] |= set(slot_hints) + for (team, player), locations_checked in multisave.get("location_checks", {}).items(): + if player in groups: + continue + player_locations = locations[player] + checks_done[team][player]["Total"] = sum(1 for loc in locations_checked if loc in player_locations) + percent_total_checks_done[team][player] = int(checks_done[team][player]["Total"] / + checks_in_area[player]["Total"] * 100) \ + if checks_in_area[player]["Total"] else 100 + + activity_timers = {} + now = datetime.datetime.utcnow() + for (team, player), timestamp in multisave.get("client_activity_timers", []): + activity_timers[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp) + + player_names = {} + states: typing.Dict[typing.Tuple[int, int], int] = {} + for team, names in enumerate(names): + for player, name in enumerate(names, 1): + player_names[team, player] = name + states[team, player] = multisave.get("client_game_state", {}).get((team, player), 0) + long_player_names = player_names.copy() + for (team, player), alias in multisave.get("name_aliases", {}).items(): + player_names[team, player] = alias + long_player_names[(team, player)] = f"{alias} ({long_player_names[team, player]})" + + video = {} + for (team, player), data in multisave.get("video", []): + video[team, player] = data + + return dict(player_names=player_names, room=room, checks_done=checks_done, + percent_total_checks_done=percent_total_checks_done, checks_in_area=checks_in_area, + activity_timers=activity_timers, video=video, hints=hints, + long_player_names=long_player_names, + multisave=multisave, precollected_items=precollected_items, groups=groups, + locations=locations, games=games, states=states) + + +def _get_inventory_data(data: typing.Dict[str, typing.Any]) -> typing.Dict[int, typing.Dict[int, int]]: + inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in team_data} + for teamnumber, team_data in data["checks_done"].items()} + + groups = data["groups"] + + for (team, player), locations_checked in data["multisave"].get("location_checks", {}).items(): + if player in data["groups"]: + continue + player_locations = data["locations"][player] + precollected = data["precollected_items"][player] + for item_id in precollected: + inventory[team][player][item_id] += 1 + for location in locations_checked: + item_id, recipient, flags = player_locations[location] + recipients = groups.get(recipient, [recipient]) + for recipient in recipients: + inventory[team][recipient][item_id] += 1 + return inventory + + +@app.route('/tracker/') +@cache.memoize(timeout=60) # multisave is currently created at most every minute +def get_multiworld_tracker(tracker: UUID): + data = _get_multiworld_tracker_data(tracker) + if not data: + abort(404) + + data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], "Generic") + + return render_template("multiTracker.html", **data) + + +@app.route('/tracker//Factorio') +@cache.memoize(timeout=60) # multisave is currently created at most every minute +def get_Factorio_multiworld_tracker(tracker: UUID): + data = _get_multiworld_tracker_data(tracker) + if not data: + abort(404) + + data["inventory"] = _get_inventory_data(data) + data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], "Factorio") + + return render_template("multiFactorioTracker.html", **data) + + +@app.route('/tracker//A Link to the Past') +@cache.memoize(timeout=60) # multisave is currently created at most every minute +def get_LttP_multiworld_tracker(tracker: UUID): + room: Room = Room.get(tracker=tracker) + if not room: + abort(404) + locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \ + precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \ + get_static_room_data(room) + + inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1) if + playernumber not in groups} + for teamnumber, team in enumerate(names)} + + checks_done = {teamnumber: {playernumber: {loc_name: 0 for loc_name in default_locations} + for playernumber in range(1, len(team) + 1) if playernumber not in groups} + for teamnumber, team in enumerate(names)} + + percent_total_checks_done = {teamnumber: {playernumber: 0 + for playernumber in range(1, len(team) + 1) if playernumber not in groups} + for teamnumber, team in enumerate(names)} + + hints = {team: set() for team in range(len(names))} + if room.multisave: + multisave = restricted_loads(room.multisave) + else: + multisave = {} + if "hints" in multisave: + for (team, slot), slot_hints in multisave["hints"].items(): + hints[team] |= set(slot_hints) + + def attribute_item(team: int, recipient: int, item: int): + nonlocal inventory + target_item = links.get(item, item) + if item in levels: # non-progressive + inventory[team][recipient][target_item] = max(inventory[team][recipient][target_item], levels[item]) + else: + inventory[team][recipient][target_item] += 1 + for (team, player), locations_checked in multisave.get("location_checks", {}).items(): if player in groups: continue @@ -1283,18 +1498,19 @@ def getTracker(tracker: UUID): if precollected_items: precollected = precollected_items[player] for item_id in precollected: - attribute_item(inventory, team, player, item_id) + attribute_item(team, player, item_id) for location in locations_checked: if location not in player_locations or location not in player_location_to_area[player]: continue - item, recipient, flags = player_locations[location] - - if recipient in names: - attribute_item(inventory, team, recipient, item) - checks_done[team][player][player_location_to_area[player][location]] += 1 - checks_done[team][player]["Total"] += 1 - percent_total_checks_done[team][player] = int(checks_done[team][player]["Total"] / seed_checks_in_area[player]["Total"] * 100) if seed_checks_in_area[player]["Total"] else 100 + recipients = groups.get(recipient, [recipient]) + for recipient in recipients: + attribute_item(team, recipient, item) + checks_done[team][player][player_location_to_area[player][location]] += 1 + checks_done[team][player]["Total"] += 1 + percent_total_checks_done[team][player] = int( + checks_done[team][player]["Total"] / seed_checks_in_area[player]["Total"] * 100) if \ + seed_checks_in_area[player]["Total"] else 100 for (team, player), game_state in multisave.get("client_game_state", {}).items(): if player in groups: @@ -1336,14 +1552,19 @@ def getTracker(tracker: UUID): for (team, player), data in multisave.get("video", []): video[(team, player)] = data - return render_template("tracker.html", inventory=inventory, get_item_name_from_id=lookup_any_item_id_to_name, + enabled_multiworld_trackers = get_enabled_multiworld_trackers(room, "A Link to the Past") + + return render_template("lttpMultiTracker.html", inventory=inventory, get_item_name_from_id=lookup_any_item_id_to_name, lookup_id_to_name=Items.lookup_id_to_name, player_names=player_names, tracking_names=tracking_names, tracking_ids=tracking_ids, room=room, icons=alttp_icons, - multi_items=multi_items, checks_done=checks_done, percent_total_checks_done=percent_total_checks_done, - ordered_areas=ordered_areas, checks_in_area=seed_checks_in_area, activity_timers=activity_timers, + multi_items=multi_items, checks_done=checks_done, + percent_total_checks_done=percent_total_checks_done, + ordered_areas=ordered_areas, checks_in_area=seed_checks_in_area, + activity_timers=activity_timers, key_locations=group_key_locations, small_key_ids=small_key_ids, big_key_ids=big_key_ids, video=video, big_key_locations=group_big_key_locations, - hints=hints, long_player_names=long_player_names) + hints=hints, long_player_names=long_player_names, + enabled_multiworld_trackers=enabled_multiworld_trackers) game_specific_trackers: typing.Dict[str, typing.Callable] = { @@ -1351,6 +1572,12 @@ game_specific_trackers: typing.Dict[str, typing.Callable] = { "Ocarina of Time": __renderOoTTracker, "Timespinner": __renderTimespinnerTracker, "A Link to the Past": __renderAlttpTracker, + "ChecksFinder": __renderChecksfinder, "Super Metroid": __renderSuperMetroidTracker, "Starcraft 2 Wings of Liberty": __renderSC2WoLTracker } + +multi_trackers: typing.Dict[str, typing.Callable] = { + "A Link to the Past": get_LttP_multiworld_tracker, + "Factorio": get_Factorio_multiworld_tracker, +} diff --git a/WebHostLib/upload.py b/WebHostLib/upload.py index dd0d218ed2..0314d64ab1 100644 --- a/WebHostLib/upload.py +++ b/WebHostLib/upload.py @@ -1,19 +1,22 @@ import base64 import json +import pickle import typing import uuid import zipfile -from io import BytesIO +import zlib +from io import BytesIO from flask import request, flash, redirect, url_for, session, render_template, Markup -from pony.orm import flush, select +from pony.orm import commit, flush, select, rollback +from pony.orm.core import TransactionIntegrityError import MultiServer from NetUtils import NetworkSlot, SlotType from Utils import VersionException, __version__ from worlds.Files import AutoPatchRegister from . import app -from .models import Seed, Room, Slot +from .models import Seed, Room, Slot, GameDataPackage banned_zip_contents = (".sfc", ".z64", ".n64", ".sms", ".gb") @@ -78,6 +81,27 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s # Load multi data. if multidata: decompressed_multidata = MultiServer.Context.decompress(multidata) + recompress = False + + if "datapackage" in decompressed_multidata: + # strip datapackage from multidata, leaving only the checksums + game_data_packages: typing.List[GameDataPackage] = [] + for game, game_data in decompressed_multidata["datapackage"].items(): + if game_data.get("checksum"): + game_data_package = GameDataPackage(checksum=game_data["checksum"], + data=pickle.dumps(game_data)) + decompressed_multidata["datapackage"][game] = { + "version": game_data.get("version", 0), + "checksum": game_data["checksum"] + } + recompress = True + try: + commit() # commit game data package + game_data_packages.append(game_data_package) + except TransactionIntegrityError: + del game_data_package + rollback() + if "slot_info" in decompressed_multidata: for slot, slot_info in decompressed_multidata["slot_info"].items(): # Ignore Player Groups (e.g. item links) @@ -90,6 +114,9 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s flush() # commit slots + if recompress: + multidata = multidata[0:1] + zlib.compress(pickle.dumps(decompressed_multidata), 9) + seed = Seed(multidata=multidata, spoiler=spoiler, slots=slots, owner=owner, meta=json.dumps(meta), id=sid if sid else uuid.uuid4()) flush() # create seed diff --git a/Zelda1Client.py b/Zelda1Client.py new file mode 100644 index 0000000000..a325e4aebe --- /dev/null +++ b/Zelda1Client.py @@ -0,0 +1,393 @@ +# Based (read: copied almost wholesale and edited) off the FF1 Client. + +import asyncio +import copy +import json +import logging +import os +import subprocess +import time +import typing +from asyncio import StreamReader, StreamWriter +from typing import List + +import Utils +from Utils import async_start +from worlds import lookup_any_location_id_to_name +from CommonClient import CommonContext, server_loop, gui_enabled, console_loop, ClientCommandProcessor, logger, \ + get_base_parser + +from worlds.tloz.Items import item_game_ids +from worlds.tloz.Locations import location_ids +from worlds.tloz import Items, Locations, Rom + +SYSTEM_MESSAGE_ID = 0 + +CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart Zelda_connector.lua" +CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator and make sure Zelda_connector.lua is running" +CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart Zelda_connector.lua" +CONNECTION_TENTATIVE_STATUS = "Initial Connection Made" +CONNECTION_CONNECTED_STATUS = "Connected" +CONNECTION_INITIAL_STATUS = "Connection has not been initiated" + +DISPLAY_MSGS = True + +item_ids = item_game_ids +location_ids = location_ids +items_by_id = {id: item for item, id in item_ids.items()} +locations_by_id = {id: location for location, id in location_ids.items()} + + +class ZeldaCommandProcessor(ClientCommandProcessor): + + def _cmd_nes(self): + """Check NES Connection State""" + if isinstance(self.ctx, ZeldaContext): + logger.info(f"NES Status: {self.ctx.nes_status}") + + def _cmd_toggle_msgs(self): + """Toggle displaying messages in bizhawk""" + global DISPLAY_MSGS + DISPLAY_MSGS = not DISPLAY_MSGS + logger.info(f"Messages are now {'enabled' if DISPLAY_MSGS else 'disabled'}") + + +class ZeldaContext(CommonContext): + command_processor = ZeldaCommandProcessor + items_handling = 0b101 # get sent remote and starting items + # Infinite Hyrule compatibility + overworld_item = 0x5F + armos_item = 0x24 + + def __init__(self, server_address, password): + super().__init__(server_address, password) + self.bonus_items = [] + self.nes_streams: (StreamReader, StreamWriter) = None + self.nes_sync_task = None + self.messages = {} + self.locations_array = None + self.nes_status = CONNECTION_INITIAL_STATUS + self.game = 'The Legend of Zelda' + self.awaiting_rom = False + self.shop_slots_left = 0 + self.shop_slots_middle = 0 + self.shop_slots_right = 0 + self.shop_slots = [self.shop_slots_left, self.shop_slots_middle, self.shop_slots_right] + self.slot_data = dict() + + async def server_auth(self, password_requested: bool = False): + if password_requested and not self.password: + await super(ZeldaContext, self).server_auth(password_requested) + if not self.auth: + self.awaiting_rom = True + logger.info('Awaiting connection to NES to get Player information') + return + + await self.send_connect() + + def _set_message(self, msg: str, msg_id: int): + if DISPLAY_MSGS: + self.messages[(time.time(), msg_id)] = msg + + def on_package(self, cmd: str, args: dict): + if cmd == 'Connected': + self.slot_data = args.get("slot_data", {}) + asyncio.create_task(parse_locations(self.locations_array, self, True)) + elif cmd == 'Print': + msg = args['text'] + if ': !' not in msg: + self._set_message(msg, SYSTEM_MESSAGE_ID) + + def on_print_json(self, args: dict): + if self.ui: + self.ui.print_json(copy.deepcopy(args["data"])) + else: + text = self.jsontotextparser(copy.deepcopy(args["data"])) + logger.info(text) + relevant = args.get("type", None) in {"Hint", "ItemSend"} + if relevant: + item = args["item"] + # goes to this world + if self.slot_concerns_self(args["receiving"]): + relevant = True + # found in this world + elif self.slot_concerns_self(item.player): + relevant = True + # not related + else: + relevant = False + if relevant: + item = args["item"] + msg = self.raw_text_parser(copy.deepcopy(args["data"])) + self._set_message(msg, item.item) + + def run_gui(self): + from kvui import GameManager + + class ZeldaManager(GameManager): + logging_pairs = [ + ("Client", "Archipelago") + ] + base_title = "Archipelago Zelda 1 Client" + + self.ui = ZeldaManager(self) + self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + + +def get_payload(ctx: ZeldaContext): + current_time = time.time() + bonus_items = [item for item in ctx.bonus_items] + return json.dumps( + { + "items": [item.item for item in ctx.items_received], + "messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items() + if key[0] > current_time - 10}, + "shops": { + "left": ctx.shop_slots_left, + "middle": ctx.shop_slots_middle, + "right": ctx.shop_slots_right + }, + "bonusItems": bonus_items + } + ) + + +def reconcile_shops(ctx: ZeldaContext): + checked_location_names = [lookup_any_location_id_to_name[location] for location in ctx.checked_locations] + shops = [location for location in checked_location_names if "Shop" in location] + left_slots = [shop for shop in shops if "Left" in shop] + middle_slots = [shop for shop in shops if "Middle" in shop] + right_slots = [shop for shop in shops if "Right" in shop] + for shop in left_slots: + ctx.shop_slots_left |= get_shop_bit_from_name(shop) + for shop in middle_slots: + ctx.shop_slots_middle |= get_shop_bit_from_name(shop) + for shop in right_slots: + ctx.shop_slots_right |= get_shop_bit_from_name(shop) + + +def get_shop_bit_from_name(location_name): + if "Potion" in location_name: + return Rom.potion_shop + elif "Arrow" in location_name: + return Rom.arrow_shop + elif "Shield" in location_name: + return Rom.shield_shop + elif "Ring" in location_name: + return Rom.ring_shop + elif "Candle" in location_name: + return Rom.candle_shop + elif "Take" in location_name: + return Rom.take_any + return 0 # this should never be hit + + +async def parse_locations(locations_array, ctx: ZeldaContext, force: bool, zone="None"): + if locations_array == ctx.locations_array and not force: + return + else: + # print("New values") + ctx.locations_array = locations_array + locations_checked = [] + location = None + for location in ctx.missing_locations: + location_name = lookup_any_location_id_to_name[location] + + if location_name in Locations.overworld_locations and zone == "overworld": + status = locations_array[Locations.major_location_offsets[location_name]] + if location_name == "Ocean Heart Container": + status = locations_array[ctx.overworld_item] + if location_name == "Armos Knights": + status = locations_array[ctx.armos_item] + if status & 0x10: + ctx.locations_checked.add(location) + locations_checked.append(location) + elif location_name in Locations.underworld1_locations and zone == "underworld1": + status = locations_array[Locations.floor_location_game_offsets_early[location_name]] + if status & 0x10: + ctx.locations_checked.add(location) + locations_checked.append(location) + elif location_name in Locations.underworld2_locations and zone == "underworld2": + status = locations_array[Locations.floor_location_game_offsets_late[location_name]] + if status & 0x10: + ctx.locations_checked.add(location) + locations_checked.append(location) + elif (location_name in Locations.shop_locations or "Take" in location_name) and zone == "caves": + shop_bit = get_shop_bit_from_name(location_name) + slot = 0 + context_slot = 0 + if "Left" in location_name: + slot = "slot1" + context_slot = 0 + elif "Middle" in location_name: + slot = "slot2" + context_slot = 1 + elif "Right" in location_name: + slot = "slot3" + context_slot = 2 + if locations_array[slot] & shop_bit > 0: + locations_checked.append(location) + ctx.shop_slots[context_slot] |= shop_bit + if locations_array["takeAnys"] and locations_array["takeAnys"] >= 4: + if "Take Any" in location_name: + short_name = None + if "Left" in location_name: + short_name = "TakeAnyLeft" + elif "Middle" in location_name: + short_name = "TakeAnyMiddle" + elif "Right" in location_name: + short_name = "TakeAnyRight" + if short_name is not None: + item_code = ctx.slot_data[short_name] + if item_code > 0: + ctx.bonus_items.append(item_code) + locations_checked.append(location) + if locations_checked: + await ctx.send_msgs([ + {"cmd": "LocationChecks", + "locations": locations_checked} + ]) + + +async def nes_sync_task(ctx: ZeldaContext): + logger.info("Starting nes connector. Use /nes for status information") + while not ctx.exit_event.is_set(): + error_status = None + if ctx.nes_streams: + (reader, writer) = ctx.nes_streams + msg = get_payload(ctx).encode() + writer.write(msg) + writer.write(b'\n') + try: + await asyncio.wait_for(writer.drain(), timeout=1.5) + try: + # Data will return a dict with up to two fields: + # 1. A keepalive response of the Players Name (always) + # 2. An array representing the memory values of the locations area (if in game) + data = await asyncio.wait_for(reader.readline(), timeout=5) + data_decoded = json.loads(data.decode()) + if data_decoded["overworldHC"] is not None: + ctx.overworld_item = data_decoded["overworldHC"] + if data_decoded["overworldPB"] is not None: + ctx.armos_item = data_decoded["overworldPB"] + if data_decoded['gameMode'] == 19 and ctx.finished_game == False: + await ctx.send_msgs([ + {"cmd": "StatusUpdate", + "status": 30} + ]) + ctx.finished_game = True + if ctx.game is not None and 'overworld' in data_decoded: + # Not just a keep alive ping, parse + asyncio.create_task(parse_locations(data_decoded['overworld'], ctx, False, "overworld")) + if ctx.game is not None and 'underworld1' in data_decoded: + asyncio.create_task(parse_locations(data_decoded['underworld1'], ctx, False, "underworld1")) + if ctx.game is not None and 'underworld2' in data_decoded: + asyncio.create_task(parse_locations(data_decoded['underworld2'], ctx, False, "underworld2")) + if ctx.game is not None and 'caves' in data_decoded: + asyncio.create_task(parse_locations(data_decoded['caves'], ctx, False, "caves")) + if not ctx.auth: + ctx.auth = ''.join([chr(i) for i in data_decoded['playerName'] if i != 0]) + if ctx.auth == '': + logger.info("Invalid ROM detected. No player name built into the ROM. Please regenerate" + "the ROM using the same link but adding your slot name") + if ctx.awaiting_rom: + await ctx.server_auth(False) + reconcile_shops(ctx) + except asyncio.TimeoutError: + logger.debug("Read Timed Out, Reconnecting") + error_status = CONNECTION_TIMING_OUT_STATUS + writer.close() + ctx.nes_streams = None + except ConnectionResetError as e: + logger.debug("Read failed due to Connection Lost, Reconnecting") + error_status = CONNECTION_RESET_STATUS + writer.close() + ctx.nes_streams = None + except TimeoutError: + logger.debug("Connection Timed Out, Reconnecting") + error_status = CONNECTION_TIMING_OUT_STATUS + writer.close() + ctx.nes_streams = None + except ConnectionResetError: + logger.debug("Connection Lost, Reconnecting") + error_status = CONNECTION_RESET_STATUS + writer.close() + ctx.nes_streams = None + if ctx.nes_status == CONNECTION_TENTATIVE_STATUS: + if not error_status: + logger.info("Successfully Connected to NES") + ctx.nes_status = CONNECTION_CONNECTED_STATUS + else: + ctx.nes_status = f"Was tentatively connected but error occured: {error_status}" + elif error_status: + ctx.nes_status = error_status + logger.info("Lost connection to nes and attempting to reconnect. Use /nes for status updates") + else: + try: + logger.debug("Attempting to connect to NES") + ctx.nes_streams = await asyncio.wait_for(asyncio.open_connection("localhost", 52980), timeout=10) + ctx.nes_status = CONNECTION_TENTATIVE_STATUS + except TimeoutError: + logger.debug("Connection Timed Out, Trying Again") + ctx.nes_status = CONNECTION_TIMING_OUT_STATUS + continue + except ConnectionRefusedError: + logger.debug("Connection Refused, Trying Again") + ctx.nes_status = CONNECTION_REFUSED_STATUS + continue + + +if __name__ == '__main__': + # Text Mode to use !hint and such with games that have no text entry + Utils.init_logging("ZeldaClient") + + options = Utils.get_options() + DISPLAY_MSGS = options["tloz_options"]["display_msgs"] + + + async def run_game(romfile: str) -> None: + auto_start = typing.cast(typing.Union[bool, str], + Utils.get_options()["tloz_options"].get("rom_start", True)) + if auto_start is True: + import webbrowser + webbrowser.open(romfile) + elif isinstance(auto_start, str) and os.path.isfile(auto_start): + subprocess.Popen([auto_start, romfile], + stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + + async def main(args): + if args.diff_file: + import Patch + logging.info("Patch file was supplied. Creating nes rom..") + meta, romfile = Patch.create_rom_file(args.diff_file) + if "server" in meta: + args.connect = meta["server"] + logging.info(f"Wrote rom file to {romfile}") + async_start(run_game(romfile)) + ctx = ZeldaContext(args.connect, args.password) + ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") + if gui_enabled: + ctx.run_gui() + ctx.run_cli() + ctx.nes_sync_task = asyncio.create_task(nes_sync_task(ctx), name="NES Sync") + + await ctx.exit_event.wait() + ctx.server_address = None + + await ctx.shutdown() + + if ctx.nes_sync_task: + await ctx.nes_sync_task + + + import colorama + + parser = get_base_parser() + parser.add_argument('diff_file', default="", type=str, nargs="?", + help='Path to a Archipelago Binary Patch file') + args = parser.parse_args() + colorama.init() + + asyncio.run(main(args)) + colorama.deinit() diff --git a/data/adventure_basepatch.bsdiff4 b/data/adventure_basepatch.bsdiff4 new file mode 100644 index 0000000000..038dff1e64 Binary files /dev/null and b/data/adventure_basepatch.bsdiff4 differ diff --git a/data/icon.ico b/data/icon.ico index a142b56b14..9e13df8df1 100644 Binary files a/data/icon.ico and b/data/icon.ico differ diff --git a/data/icon.png b/data/icon.png index 0cf21041e6..4fd9334dff 100644 Binary files a/data/icon.png and b/data/icon.png differ diff --git a/data/lua/ADVENTURE/adventure_connector.lua b/data/lua/ADVENTURE/adventure_connector.lua new file mode 100644 index 0000000000..598d6d74ff --- /dev/null +++ b/data/lua/ADVENTURE/adventure_connector.lua @@ -0,0 +1,851 @@ +local socket = require("socket") +local json = require('json') +local math = require('math') + +local STATE_OK = "Ok" +local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected" +local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made" +local STATE_UNINITIALIZED = "Uninitialized" + +local SCRIPT_VERSION = 1 + +local APItemValue = 0xA2 +local APItemRam = 0xE7 +local BatAPItemValue = 0xAB +local BatAPItemRam = 0xEA +local PlayerRoomAddr = 0x8A -- if in number room, we're not in play mode +local WinAddr = 0xDE -- if not 0 (I think if 0xff specifically), we won (and should update once, immediately) + +-- If any of these are 2, that dragon ate the player (should send update immediately +-- once, and reset that when none of them are 2 again) + +local DragonState = {0xA8, 0xAD, 0xB2} +local last_dragon_state = {0, 0, 0} +local carryAddress = 0x9D -- uses rom object table +local batRoomAddr = 0xCB +local batCarryAddress = 0xD0 -- uses ram object location +local batInvalidCarryItem = 0x78 +local batItemCheckAddr = 0xf69f +local batMatrixLen = 11 -- number of pairs +local last_carry_item = 0xB4 +local frames_with_no_item = 0 +local ItemTableStart = 0xfe9d +local PlayerSlotAddress = 0xfff9 + +local itemMessages = {} + +local nullObjectId = 0xB4 +local ItemsReceived = nil +local sha256hash = nil +local foreign_items = nil +local foreign_items_by_room = {} +local bat_no_touch_locations_by_room = {} +local bat_no_touch_items = {} +local autocollect_items = {} +local localItemLocations = {} + +local prev_bat_room = 0xff +local prev_player_room = 0 +local prev_ap_room_index = nil + +local pending_foreign_items_collected = {} +local pending_local_items_collected = {} +local rendering_foreign_item = nil +local skip_inventory_items = {} + +local inventory = {} +local next_inventory_item = nil + +local input_button_address = 0xD7 + +local deathlink_rec = nil +local deathlink_send = 0 + +local deathlink_sent = false + +local prevstate = "" +local curstate = STATE_UNINITIALIZED +local atariSocket = nil +local frame = 0 + +local ItemIndex = 0 + +local yorgle_speed_address = 0xf725 +local grundle_speed_address = 0xf740 +local rhindle_speed_address = 0xf70A + +local read_switch_a = 0xf780 +local read_switch_b = 0xf764 + +local yorgle_speed = nil +local grundle_speed = nil +local rhindle_speed = nil + +local slow_yorgle_id = tostring(118000000 + 0x103) +local slow_grundle_id = tostring(118000000 + 0x104) +local slow_rhindle_id = tostring(118000000 + 0x105) + +local yorgle_dead = false +local grundle_dead = false +local rhindle_dead = false + +local diff_a_locked = false +local diff_b_locked = false + +local bat_logic = 0 + +local is_dead = 0 +local freeincarnates_available = 0 +local send_freeincarnate_used = false +local current_bat_ap_item = nil + +local was_in_number_room = false + +local u8 = nil +local wU8 = nil +local u16 + +local bizhawk_version = client.getversion() +local is23Or24Or25 = (bizhawk_version=="2.3.1") or (bizhawk_version:sub(1,3)=="2.4") or (bizhawk_version:sub(1,3)=="2.5") +local is26To28 = (bizhawk_version:sub(1,3)=="2.6") or (bizhawk_version:sub(1,3)=="2.7") or (bizhawk_version:sub(1,3)=="2.8") + +u8 = memory.read_u8 +wU8 = memory.write_u8 +u16 = memory.read_u16_le +function uRangeRam(address, bytes) + data = memory.read_bytes_as_array(address, bytes, "Main RAM") + return data +end +function uRangeRom(address, bytes) + data = memory.read_bytes_as_array(address+0xf000, bytes, "System Bus") + return data +end +function uRangeAddress(address, bytes) + data = memory.read_bytes_as_array(address, bytes, "System Bus") + return data +end + + +function table.empty (self) + for _, _ in pairs(self) do + return false + end + return true +end + +function slice (tbl, s, e) + local pos, new = 1, {} + for i = s + 1, e do + new[pos] = tbl[i] + pos = pos + 1 + end + return new +end + +local function createForeignItemsByRoom() + foreign_items_by_room = {} + if foreign_items == nil then + return + end + for _, foreign_item in pairs(foreign_items) do + if foreign_items_by_room[foreign_item.room_id] == nil then + foreign_items_by_room[foreign_item.room_id] = {} + end + new_foreign_item = {} + new_foreign_item.room_id = foreign_item.room_id + new_foreign_item.room_x = foreign_item.room_x + new_foreign_item.room_y = foreign_item.room_y + new_foreign_item.short_location_id = foreign_item.short_location_id + + table.insert(foreign_items_by_room[foreign_item.room_id], new_foreign_item) + end +end + +function debugPrintNoTouchLocations() + for room_id, list in pairs(bat_no_touch_locations_by_room) do + for index, notouch_location in ipairs(list) do + print("ROOM "..tostring(room_id).. "["..tostring(index).."]: "..tostring(notouch_location.short_location_id)) + end + end +end + +function processBlock(block) + if block == nil then + return + end + local block_identified = 0 + local msgBlock = block['messages'] + if msgBlock ~= nil then + block_identified = 1 + for i, v in pairs(msgBlock) do + if itemMessages[i] == nil then + local msg = {TTL=450, message=v, color=0xFFFF0000} + itemMessages[i] = msg + end + end + end + local itemsBlock = block["items"] + if itemsBlock ~= nil then + block_identified = 1 + ItemsReceived = itemsBlock + end + local apItemsBlock = block["foreign_items"] + if apItemsBlock ~= nil then + block_identified = 1 + print("got foreign items block") + foreign_items = apItemsBlock + createForeignItemsByRoom() + end + local autocollectItems = block["autocollect_items"] + if autocollectItems ~= nil then + block_identified = 1 + autocollect_items = {} + for _, acitem in pairs(autocollectItems) do + if autocollect_items[acitem.room_id] == nil then + autocollect_items[acitem.room_id] = {} + end + table.insert(autocollect_items[acitem.room_id], acitem) + end + end + local localLocalItemLocations = block["local_item_locations"] + if localLocalItemLocations ~= nil then + block_identified = 1 + localItemLocations = localLocalItemLocations + print("got local item locations") + end + local checkedLocationsBlock = block["checked_locations"] + if checkedLocationsBlock ~= nil then + block_identified = 1 + for room_id, foreign_item_list in pairs(foreign_items_by_room) do + for i, foreign_item in pairs(foreign_item_list) do + short_id = foreign_item.short_location_id + for j, checked_id in pairs(checkedLocationsBlock) do + if checked_id == short_id then + table.remove(foreign_item_list, i) + break + end + end + end + end + if foreign_items ~= nil then + for i, foreign_item in pairs(foreign_items) do + short_id = foreign_item.short_location_id + for j, checked_id in pairs(checkedLocationsBlock) do + if checked_id == short_id then + foreign_items[i] = nil + break + end + end + end + end + end + local dragon_speeds_block = block["dragon_speeds"] + if dragon_speeds_block ~= nil then + block_identified = 1 + yorgle_speed = dragon_speeds_block[slow_yorgle_id] + grundle_speed = dragon_speeds_block[slow_grundle_id] + rhindle_speed = dragon_speeds_block[slow_rhindle_id] + end + local diff_a_block = block["difficulty_a_locked"] + if diff_a_block ~= nil then + block_identified = 1 + diff_a_locked = diff_a_block + end + local diff_b_block = block["difficulty_b_locked"] + if diff_b_block ~= nil then + block_identified = 1 + diff_b_locked = diff_b_block + end + local freeincarnates_available_block = block["freeincarnates_available"] + if freeincarnates_available_block ~= nil then + block_identified = 1 + if freeincarnates_available ~= freeincarnates_available_block then + freeincarnates_available = freeincarnates_available_block + local msg = {TTL=450, message="freeincarnates: "..tostring(freeincarnates_available), color=0xFFFF0000} + itemMessages[-2] = msg + end + end + local bat_logic_block = block["bat_logic"] + if bat_logic_block ~= nil then + block_identified = 1 + bat_logic = bat_logic_block + end + local bat_no_touch_locations_block = block["bat_no_touch_locations"] + if bat_no_touch_locations_block ~= nil then + block_identified = 1 + for _, notouch_location in pairs(bat_no_touch_locations_block) do + local room_id = tonumber(notouch_location.room_id) + if bat_no_touch_locations_by_room[room_id] == nil then + bat_no_touch_locations_by_room[room_id] = {} + end + table.insert(bat_no_touch_locations_by_room[room_id], notouch_location) + + if notouch_location.local_item ~= nil and notouch_location.local_item ~= 255 then + bat_no_touch_items[tonumber(notouch_location.local_item)] = true + -- print("no touch: "..tostring(notouch_location.local_item)) + end + end + -- debugPrintNoTouchLocations() + end + deathlink_rec = deathlink_rec or block["deathlink"] + if( block_identified == 0 ) then + print("unidentified block") + print(block) + end +end + +local function clearScreen() + if is23Or24Or25 then + return + elseif is26To28 then + drawText(0, 0, "", "black") + end +end + +local function getMaxMessageLength() + if is23Or24Or25 then + return client.screenwidth()/11 + elseif is26To28 then + return client.screenwidth()/12 + end +end + +function drawText(x, y, message, color) + if is23Or24Or25 then + gui.addmessage(message) + elseif is26To28 then + gui.drawText(x, y, message, color, 0xB0000000, 18, "Courier New", nil, nil, nil, "client") + end +end + +local function drawMessages() + if table.empty(itemMessages) then + clearScreen() + return + end + local y = 10 + found = false + maxMessageLength = getMaxMessageLength() + for k, v in pairs(itemMessages) do + if v["TTL"] > 0 then + message = v["message"] + while true do + drawText(5, y, message:sub(1, maxMessageLength), v["color"]) + y = y + 16 + + message = message:sub(maxMessageLength + 1, message:len()) + if message:len() == 0 then + break + end + end + newTTL = 0 + if is26To28 then + newTTL = itemMessages[k]["TTL"] - 1 + end + itemMessages[k]["TTL"] = newTTL + found = true + end + end + if found == false then + clearScreen() + end +end + +function difference(a, b) + local aa = {} + for k,v in pairs(a) do aa[v]=true end + for k,v in pairs(b) do aa[v]=nil end + local ret = {} + local n = 0 + for k,v in pairs(a) do + if aa[v] then n=n+1 ret[n]=v end + end + return ret +end + +function getAllRam() + uRangeRAM(0,128); + return data +end + +local function arrayEqual(a1, a2) + if #a1 ~= #a2 then + return false + end + + for i, v in ipairs(a1) do + if v ~= a2[i] then + return false + end + end + + return true +end + +local function alive_mode() + return (u8(PlayerRoomAddr) ~= 0x00 and u8(WinAddr) == 0x00) +end + +local function generateLocationsChecked() + list_of_locations = {} + for s, f in pairs(pending_foreign_items_collected) do + table.insert(list_of_locations, f.short_location_id + 118000000) + end + for s, f in pairs(pending_local_items_collected) do + table.insert(list_of_locations, f + 118000000) + end + return list_of_locations +end + +function receive() + l, e = atariSocket:receive() + if e == 'closed' then + if curstate == STATE_OK then + print("Connection closed") + end + curstate = STATE_UNINITIALIZED + return + elseif e == 'timeout' then + return + elseif e ~= nil then + print(e) + curstate = STATE_UNINITIALIZED + return + end + if l ~= nil then + processBlock(json.decode(l)) + end + -- Determine Message to send back + + newSha256 = memory.hash_region(0xF000, 0x1000, "System Bus") + if (sha256hash ~= nil and sha256hash ~= newSha256) then + print("ROM changed, quitting") + curstate = STATE_UNINITIALIZED + return + end + sha256hash = newSha256 + local retTable = {} + retTable["scriptVersion"] = SCRIPT_VERSION + retTable["romhash"] = sha256hash + if (alive_mode()) then + retTable["locations"] = generateLocationsChecked() + end + if (u8(WinAddr) ~= 0x00) then + retTable["victory"] = 1 + end + if( deathlink_sent or deathlink_send == 0 ) then + retTable["deathLink"] = 0 + else + print("Sending deathlink "..tostring(deathlink_send)) + retTable["deathLink"] = deathlink_send + deathlink_sent = true + end + deathlink_send = 0 + + if send_freeincarnate_used == true then + print("Sending freeincarnate used") + retTable["freeincarnate"] = true + send_freeincarnate_used = false + end + + msg = json.encode(retTable).."\n" + local ret, error = atariSocket:send(msg) + if ret == nil then + print(error) + elseif curstate == STATE_INITIAL_CONNECTION_MADE then + curstate = STATE_TENTATIVELY_CONNECTED + elseif curstate == STATE_TENTATIVELY_CONNECTED then + print("Connected!") + curstate = STATE_OK + end +end + +function AutocollectFromRoom() + if autocollect_items ~= nil and autocollect_items[prev_player_room] ~= nil then + for _, item in pairs(autocollect_items[prev_player_room]) do + pending_foreign_items_collected[item.short_location_id] = item + end + end +end + +function SetYorgleSpeed() + if yorgle_speed ~= nil then + emu.setregister("A", yorgle_speed); + end +end + +function SetGrundleSpeed() + if grundle_speed ~= nil then + emu.setregister("A", grundle_speed); + end +end + +function SetRhindleSpeed() + if rhindle_speed ~= nil then + emu.setregister("A", rhindle_speed); + end +end + +function SetDifficultySwitchB() + if diff_b_locked then + local a = emu.getregister("A") + if a < 128 then + emu.setregister("A", a + 128) + end + end +end + +function SetDifficultySwitchA() + if diff_a_locked then + local a = emu.getregister("A") + if (a > 128 and a < 128 + 64) or (a < 64) then + emu.setregister("A", a + 64) + end + end +end + +function TryFreeincarnate() + if freeincarnates_available > 0 then + freeincarnates_available = freeincarnates_available - 1 + for index, state_addr in pairs(DragonState) do + if last_dragon_state[index] == 1 then + send_freeincarnate_used = true + memory.write_u8(state_addr, 1, "System Bus") + local msg = {TTL=450, message="used freeincarnate", color=0xFF00FF00} + itemMessages[-1] = msg + end + end + + end +end + +function GetLinkedObject() + if emu.getregister("X") == batRoomAddr then + bat_interest_item = emu.getregister("A") + -- if the bat can't touch that item, we'll switch it to the number item, which should never be + -- in the same room as the bat. + if bat_no_touch_items[bat_interest_item] ~= nil then + emu.setregister("A", 0xDD ) + emu.setregister("Y", 0xDD ) + end + end +end + +function CheckCollectAPItem(carry_item, target_item_value, target_item_ram, rendering_foreign_item) + if( carry_item == target_item_value and rendering_foreign_item ~= nil ) then + memory.write_u8(carryAddress, nullObjectId, "System Bus") + memory.write_u8(target_item_ram, 0xFF, "System Bus") + pending_foreign_items_collected[rendering_foreign_item.short_location_id] = rendering_foreign_item + for index, fi in pairs(foreign_items_by_room[rendering_foreign_item.room_id]) do + if( fi.short_location_id == rendering_foreign_item.short_location_id ) then + table.remove(foreign_items_by_room[rendering_foreign_item.room_id], index) + break + end + end + for index, fi in pairs(foreign_items) do + if( fi.short_location_id == rendering_foreign_item.short_location_id ) then + foreign_items[index] = nil + break + end + end + prev_ap_room_index = 0 + return true + end + return false +end + +function BatCanTouchForeign(foreign_item, bat_room) + if bat_no_touch_locations_by_room[bat_room] == nil or bat_no_touch_locations_by_room[bat_room][1] == nil then + return true + end + + for index, location in ipairs(bat_no_touch_locations_by_room[bat_room]) do + if location.short_location_id == foreign_item.short_location_id then + return false + end + end + return true; +end + +function main() + memory.usememorydomain("System Bus") + if (is23Or24Or25 or is26To28) == false then + print("Must use a version of bizhawk 2.3.1 or higher") + return + end + local playerSlot = memory.read_u8(PlayerSlotAddress) + local port = 17242 + playerSlot + print("Using port"..tostring(port)) + server, error = socket.bind('localhost', port) + if( error ~= nil ) then + print(error) + end + event.onmemoryexecute(SetYorgleSpeed, yorgle_speed_address); + event.onmemoryexecute(SetGrundleSpeed, grundle_speed_address); + event.onmemoryexecute(SetRhindleSpeed, rhindle_speed_address); + event.onmemoryexecute(SetDifficultySwitchA, read_switch_a) + event.onmemoryexecute(SetDifficultySwitchB, read_switch_b) + event.onmemoryexecute(GetLinkedObject, batItemCheckAddr) + -- TODO: Add an onmemoryexecute event to intercept the bat reading item rooms, and don't 'see' an item in the + -- room if it is in bat_no_touch_locations_by_room. Although realistically, I may have to handle this in the rom + -- for it to be totally reliable, because it won't work before the script connects (I might have to reset them?) + -- TODO: Also remove those items from the bat_no_touch_locations_by_room if they have been collected + while true do + frame = frame + 1 + drawMessages() + if not (curstate == prevstate) then + print("Current state: "..curstate) + prevstate = curstate + end + + local current_player_room = u8(PlayerRoomAddr) + local bat_room = u8(batRoomAddr) + local bat_carrying_item = u8(batCarryAddress) + local bat_carrying_ap_item = (BatAPItemRam == bat_carrying_item) + + if current_player_room == 0x1E then + if u8(PlayerRoomAddr + 1) > 0x4B then + memory.write_u8(PlayerRoomAddr + 1, 0x4B) + end + end + + if current_player_room == 0x00 then + if not was_in_number_room then + print("reset "..tostring(bat_carrying_ap_item).." "..tostring(bat_carrying_item)) + memory.write_u8(batCarryAddress, batInvalidCarryItem) + memory.write_u8(batCarryAddress+ 1, 0) + createForeignItemsByRoom() + memory.write_u8(BatAPItemRam, 0xff) + memory.write_u8(APItemRam, 0xff) + prev_ap_room_index = 0 + prev_player_room = 0 + rendering_foreign_item = nil + was_in_number_room = true + end + else + was_in_number_room = false + end + + if bat_room ~= prev_bat_room then + if bat_carrying_ap_item then + if foreign_items_by_room[prev_bat_room] ~= nil then + for r,f in pairs(foreign_items_by_room[prev_bat_room]) do + if f.short_location_id == current_bat_ap_item.short_location_id then + -- print("removing item from "..tostring(r).." in "..tostring(prev_bat_room)) + table.remove(foreign_items_by_room[prev_bat_room], r) + break + end + end + end + if foreign_items_by_room[bat_room] == nil then + foreign_items_by_room[bat_room] = {} + end + -- print("adding item to "..tostring(bat_room)) + table.insert(foreign_items_by_room[bat_room], current_bat_ap_item) + else + -- set AP item room and position for new room, or to invalid room + if foreign_items_by_room[bat_room] ~= nil and foreign_items_by_room[bat_room][1] ~= nil + and BatCanTouchForeign(foreign_items_by_room[bat_room][1], bat_room) then + if current_bat_ap_item ~= foreign_items_by_room[bat_room][1] then + current_bat_ap_item = foreign_items_by_room[bat_room][1] + -- print("Changing bat item to "..tostring(current_bat_ap_item.short_location_id)) + end + memory.write_u8(BatAPItemRam, bat_room) + memory.write_u8(BatAPItemRam + 1, current_bat_ap_item.room_x) + memory.write_u8(BatAPItemRam + 2, current_bat_ap_item.room_y) + else + memory.write_u8(BatAPItemRam, 0xff) + if current_bat_ap_item ~= nil then + -- print("clearing bat item") + end + current_bat_ap_item = nil + end + end + end + prev_bat_room = bat_room + + -- update foreign_items_by_room position and room id for bat item if bat carrying an item + if bat_carrying_ap_item then + -- this is setting the item using the bat's position, which is somewhat wrong, but I think + -- there will be more problems with the room not matching sometimes if I use the actual item position + current_bat_ap_item.room_id = bat_room + current_bat_ap_item.room_x = u8(batRoomAddr + 1) + current_bat_ap_item.room_y = u8(batRoomAddr + 2) + end + + if (alive_mode()) then + if (current_player_room ~= prev_player_room) then + memory.write_u8(APItemRam, 0xFF, "System Bus") + prev_ap_room_index = 0 + prev_player_room = current_player_room + AutocollectFromRoom() + end + local carry_item = memory.read_u8(carryAddress, "System Bus") + bat_no_touch_items[carry_item] = nil + if (next_inventory_item ~= nil) then + if ( carry_item == nullObjectId and last_carry_item == nullObjectId ) then + frames_with_no_item = frames_with_no_item + 1 + if (frames_with_no_item > 10) then + frames_with_no_item = 10 + local input_value = memory.read_u8(input_button_address, "System Bus") + if( input_value >= 64 and input_value < 128 ) then -- high bit clear, second highest bit set + memory.write_u8(carryAddress, next_inventory_item) + local item_ram_location = memory.read_u8(ItemTableStart + next_inventory_item) + if( memory.read_u8(batCarryAddress) ~= 0x78 and + memory.read_u8(batCarryAddress) == item_ram_location) then + memory.write_u8(batCarryAddress, batInvalidCarryItem) + memory.write_u8(batCarryAddress+ 1, 0) + memory.write_u8(item_ram_location, current_player_room) + memory.write_u8(item_ram_location + 1, memory.read_u8(PlayerRoomAddr + 1)) + memory.write_u8(item_ram_location + 2, memory.read_u8(PlayerRoomAddr + 2)) + end + ItemIndex = ItemIndex + 1 + next_inventory_item = nil + end + end + else + frames_with_no_item = 0 + end + end + if( carry_item ~= last_carry_item ) then + if ( localItemLocations ~= nil and localItemLocations[tostring(carry_item)] ~= nil ) then + pending_local_items_collected[localItemLocations[tostring(carry_item)]] = + localItemLocations[tostring(carry_item)] + table.remove(localItemLocations, tostring(carry_item)) + skip_inventory_items[carry_item] = carry_item + end + end + last_carry_item = carry_item + + CheckCollectAPItem(carry_item, APItemValue, APItemRam, rendering_foreign_item) + if CheckCollectAPItem(carry_item, BatAPItemValue, BatAPItemRam, current_bat_ap_item) and bat_carrying_ap_item then + memory.write_u8(batCarryAddress, batInvalidCarryItem) + memory.write_u8(batCarryAddress+ 1, 0) + end + + + rendering_foreign_item = nil + if( foreign_items_by_room[current_player_room] ~= nil ) then + if( foreign_items_by_room[current_player_room][prev_ap_room_index] ~= nil ) and memory.read_u8(APItemRam) ~= 0xff then + foreign_items_by_room[current_player_room][prev_ap_room_index].room_x = memory.read_u8(APItemRam + 1) + foreign_items_by_room[current_player_room][prev_ap_room_index].room_y = memory.read_u8(APItemRam + 2) + end + prev_ap_room_index = prev_ap_room_index + 1 + local invalid_index = -1 + if( foreign_items_by_room[current_player_room][prev_ap_room_index] == nil ) then + prev_ap_room_index = 1 + end + if( foreign_items_by_room[current_player_room][prev_ap_room_index] ~= nil and current_bat_ap_item ~= nil and + foreign_items_by_room[current_player_room][prev_ap_room_index].short_location_id == current_bat_ap_item.short_location_id) then + invalid_index = prev_ap_room_index + prev_ap_room_index = prev_ap_room_index + 1 + if( foreign_items_by_room[current_player_room][prev_ap_room_index] == nil ) then + prev_ap_room_index = 1 + end + end + + if( foreign_items_by_room[current_player_room][prev_ap_room_index] ~= nil and prev_ap_room_index ~= invalid_index ) then + memory.write_u8(APItemRam, current_player_room) + rendering_foreign_item = foreign_items_by_room[current_player_room][prev_ap_room_index] + memory.write_u8(APItemRam + 1, rendering_foreign_item.room_x) + memory.write_u8(APItemRam + 2, rendering_foreign_item.room_y) + else + memory.write_u8(APItemRam, 0xFF, "System Bus") + end + end + if is_dead == 0 then + dragons_revived = false + player_dead = false + new_dragon_state = {0,0,0} + for index, dragon_state_addr in pairs(DragonState) do + new_dragon_state[index] = memory.read_u8(dragon_state_addr, "System Bus" ) + if last_dragon_state[index] == 1 and new_dragon_state[index] ~= 1 then + dragons_revived = true + elseif last_dragon_state[index] ~= 1 and new_dragon_state[index] == 1 then + dragon_real_index = index - 1 + print("Killed dragon: "..tostring(dragon_real_index)) + local dragon_item = {} + dragon_item["short_location_id"] = 0xD0 + dragon_real_index + pending_foreign_items_collected[dragon_item.short_location_id] = dragon_item + end + if new_dragon_state[index] == 2 then + player_dead = true + end + end + if dragons_revived and player_dead == false then + TryFreeincarnate() + end + last_dragon_state = new_dragon_state + end + elseif (u8(PlayerRoomAddr) == 0x00) then -- not alive mode, in number room + ItemIndex = 0 -- reset our inventory + next_inventory_item = nil + skip_inventory_items = {} + end + if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then + if (frame % 5 == 0) then + receive() + if alive_mode() then + local was_dead = is_dead + is_dead = 0 + for index, dragonStateAddr in pairs(DragonState) do + local dragonstateval = memory.read_u8(dragonStateAddr, "System Bus") + if ( dragonstateval == 2) then + is_dead = index + end + end + if was_dead ~= 0 and is_dead == 0 then + TryFreeincarnate() + end + if deathlink_rec == true and is_dead == 0 then + print("setting dead from deathlink") + deathlink_rec = false + deathlink_sent = true + is_dead = 1 + memory.write_u8(carryAddress, nullObjectId, "System Bus") + memory.write_u8(DragonState[1], 2, "System Bus") + end + if (is_dead > 0 and deathlink_send == 0 and not deathlink_sent) then + deathlink_send = is_dead + print("setting deathlink_send to "..tostring(is_dead)) + elseif (is_dead == 0) then + deathlink_send = 0 + deathlink_sent = false + end + if ItemsReceived ~= nil and ItemsReceived[ItemIndex + 1] ~= nil then + while ItemsReceived[ItemIndex + 1] ~= nil and skip_inventory_items[ItemsReceived[ItemIndex + 1]] ~= nil do + print("skip") + ItemIndex = ItemIndex + 1 + end + local static_id = ItemsReceived[ItemIndex + 1] + if static_id ~= nil then + inventory[static_id] = 1 + if next_inventory_item == nil then + next_inventory_item = static_id + end + end + end + end + end + elseif (curstate == STATE_UNINITIALIZED) then + if (frame % 60 == 0) then + + print("Waiting for client.") + + emu.frameadvance() + server:settimeout(2) + print("Attempting to connect") + local client, timeout = server:accept() + if timeout == nil then + print("Initial connection made") + curstate = STATE_INITIAL_CONNECTION_MADE + atariSocket = client + atariSocket:settimeout(0) + end + end + end + emu.frameadvance() + end +end + +main() diff --git a/data/lua/ADVENTURE/json.lua b/data/lua/ADVENTURE/json.lua new file mode 100644 index 0000000000..0833bf6fb4 --- /dev/null +++ b/data/lua/ADVENTURE/json.lua @@ -0,0 +1,380 @@ +-- +-- json.lua +-- +-- Copyright (c) 2015 rxi +-- +-- This library is free software; you can redistribute it and/or modify it +-- under the terms of the MIT license. See LICENSE for details. +-- + +local json = { _version = "0.1.0" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\\\", + [ "\"" ] = "\\\"", + [ "\b" ] = "\\b", + [ "\f" ] = "\\f", + [ "\n" ] = "\\n", + [ "\r" ] = "\\r", + [ "\t" ] = "\\t", +} + +local escape_char_map_inv = { [ "\\/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return escape_char_map[c] or string.format("\\u%04x", c:byte()) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if val[1] ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + --local line_count = 1 + --local col_count = 1 + --for i = 1, idx - 1 do + -- col_count = col_count + 1 + -- if str:sub(i, i) == "\n" then + -- line_count = line_count + 1 + -- col_count = 1 + -- end + -- end + -- emu.message( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(3, 6), 16 ) + local n2 = tonumber( s:sub(9, 12), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local has_unicode_escape = false + local has_surrogate_escape = false + local has_escape = false + local last + for j = i + 1, #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + end + + if last == 92 then -- "\\" (escape char) + if x == 117 then -- "u" (unicode escape sequence) + local hex = str:sub(j + 1, j + 5) + if not hex:find("%x%x%x%x") then + decode_error(str, j, "invalid unicode escape in string") + end + if hex:find("^[dD][89aAbB]") then + has_surrogate_escape = true + else + has_unicode_escape = true + end + else + local c = string.char(x) + if not escape_chars[c] then + decode_error(str, j, "invalid escape char '" .. c .. "' in string") + end + has_escape = true + end + last = nil + + elseif x == 34 then -- '"' (end of string) + local s = str:sub(i + 1, j - 1) + if has_surrogate_escape then + s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape) + end + if has_unicode_escape then + s = s:gsub("\\u....", parse_unicode_escape) + end + if has_escape then + s = s:gsub("\\.", escape_char_map_inv) + end + return s, j + 1 + + else + last = x + end + end + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + return ( parse(str, next_char(str, 1, space_chars, true)) ) +end + + +return json \ No newline at end of file diff --git a/data/lua/ADVENTURE/socket.lua b/data/lua/ADVENTURE/socket.lua new file mode 100644 index 0000000000..a98e952115 --- /dev/null +++ b/data/lua/ADVENTURE/socket.lua @@ -0,0 +1,132 @@ +----------------------------------------------------------------------------- +-- LuaSocket helper module +-- Author: Diego Nehab +-- RCS ID: $Id: socket.lua,v 1.22 2005/11/22 08:33:29 diego Exp $ +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Declare module and import dependencies +----------------------------------------------------------------------------- +local base = _G +local string = require("string") +local math = require("math") +local socket = require("socket.core") +module("socket") + +----------------------------------------------------------------------------- +-- Exported auxiliar functions +----------------------------------------------------------------------------- +function connect(address, port, laddress, lport) + local sock, err = socket.tcp() + if not sock then return nil, err end + if laddress then + local res, err = sock:bind(laddress, lport, -1) + if not res then return nil, err end + end + local res, err = sock:connect(address, port) + if not res then return nil, err end + return sock +end + +function bind(host, port, backlog) + local sock, err = socket.tcp() + if not sock then return nil, err end + sock:setoption("reuseaddr", true) + local res, err = sock:bind(host, port) + if not res then return nil, err end + res, err = sock:listen(backlog) + if not res then return nil, err end + return sock +end + +try = newtry() + +function choose(table) + return function(name, opt1, opt2) + if base.type(name) ~= "string" then + name, opt1, opt2 = "default", name, opt1 + end + local f = table[name or "nil"] + if not f then base.error("unknown key (".. base.tostring(name) ..")", 3) + else return f(opt1, opt2) end + end +end + +----------------------------------------------------------------------------- +-- Socket sources and sinks, conforming to LTN12 +----------------------------------------------------------------------------- +-- create namespaces inside LuaSocket namespace +sourcet = {} +sinkt = {} + +BLOCKSIZE = 2048 + +sinkt["close-when-done"] = function(sock) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function(self, chunk, err) + if not chunk then + sock:close() + return 1 + else return sock:send(chunk) end + end + }) +end + +sinkt["keep-open"] = function(sock) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function(self, chunk, err) + if chunk then return sock:send(chunk) + else return 1 end + end + }) +end + +sinkt["default"] = sinkt["keep-open"] + +sink = choose(sinkt) + +sourcet["by-length"] = function(sock, length) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function() + if length <= 0 then return nil end + local size = math.min(socket.BLOCKSIZE, length) + local chunk, err = sock:receive(size) + if err then return nil, err end + length = length - string.len(chunk) + return chunk + end + }) +end + +sourcet["until-closed"] = function(sock) + local done + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function() + if done then return nil end + local chunk, err, partial = sock:receive(socket.BLOCKSIZE) + if not err then return chunk + elseif err == "closed" then + sock:close() + done = 1 + return partial + else return nil, err end + end + }) +end + + +sourcet["default"] = sourcet["until-closed"] + +source = choose(sourcet) diff --git a/data/lua/PKMN_RB/pkmn_rb.lua b/data/lua/PKMN_RB/pkmn_rb.lua index eaf7516547..036f7a6255 100644 --- a/data/lua/PKMN_RB/pkmn_rb.lua +++ b/data/lua/PKMN_RB/pkmn_rb.lua @@ -7,7 +7,7 @@ local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected" local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made" local STATE_UNINITIALIZED = "Uninitialized" -local SCRIPT_VERSION = 1 +local SCRIPT_VERSION = 3 local APIndex = 0x1A6E local APDeathLinkAddress = 0x00FD @@ -16,7 +16,8 @@ local EventFlagAddress = 0x1735 local MissableAddress = 0x161A local HiddenItemsAddress = 0x16DE local RodAddress = 0x1716 -local InGame = 0x1A71 +local DexSanityAddress = 0x1A71 +local InGameAddress = 0x1A84 local ClientCompatibilityAddress = 0xFF00 local ItemsReceived = nil @@ -34,6 +35,7 @@ local frame = 0 local u8 = nil local wU8 = nil local u16 +local compat = nil local function defineMemoryFunctions() local memDomain = {} @@ -70,18 +72,6 @@ function slice (tbl, s, e) return new end -function processBlock(block) - if block == nil then - return - end - local itemsBlock = block["items"] - memDomain.wram() - if itemsBlock ~= nil then - ItemsReceived = itemsBlock - end - deathlink_rec = block["deathlink"] -end - function difference(a, b) local aa = {} for k,v in pairs(a) do aa[v]=true end @@ -99,6 +89,7 @@ function generateLocationsChecked() events = uRange(EventFlagAddress, 0x140) missables = uRange(MissableAddress, 0x20) hiddenitems = uRange(HiddenItemsAddress, 0x0E) + dexsanity = uRange(DexSanityAddress, 19) rod = u8(RodAddress) data = {} @@ -108,6 +99,9 @@ function generateLocationsChecked() table.foreach(hiddenitems, function(k, v) table.insert(data, v) end) table.insert(data, rod) + if compat > 1 then + table.foreach(dexsanity, function(k, v) table.insert(data, v) end) + end return data end @@ -141,7 +135,15 @@ function receive() return end if l ~= nil then - processBlock(json.decode(l)) + block = json.decode(l) + if block ~= nil then + local itemsBlock = block["items"] + if itemsBlock ~= nil then + ItemsReceived = itemsBlock + end + deathlink_rec = block["deathlink"] + + end end -- Determine Message to send back memDomain.rom() @@ -156,15 +158,31 @@ function receive() seedName = newSeedName local retTable = {} retTable["scriptVersion"] = SCRIPT_VERSION - retTable["clientCompatibilityVersion"] = u8(ClientCompatibilityAddress) + + if compat == nil then + compat = u8(ClientCompatibilityAddress) + if compat < 2 then + InGameAddress = 0x1A71 + end + end + + retTable["clientCompatibilityVersion"] = compat retTable["playerName"] = playerName retTable["seedName"] = seedName memDomain.wram() - if u8(InGame) == 0xAC then + + in_game = u8(InGameAddress) + if in_game == 0x2A or in_game == 0xAC then retTable["locations"] = generateLocationsChecked() + elseif in_game ~= 0 then + print("Game may have crashed") + curstate = STATE_UNINITIALIZED + return end + retTable["deathLink"] = deathlink_send deathlink_send = false + msg = json.encode(retTable).."\n" local ret, error = gbSocket:send(msg) if ret == nil then @@ -193,16 +211,23 @@ function main() if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then if (frame % 5 == 0) then receive() - if u8(InGame) == 0xAC and u8(APItemAddress) == 0x00 then - ItemIndex = u16(APIndex) - if deathlink_rec == true then - wU8(APDeathLinkAddress, 1) - elseif u8(APDeathLinkAddress) == 3 then - wU8(APDeathLinkAddress, 0) - deathlink_send = true - end - if ItemsReceived[ItemIndex + 1] ~= nil then - wU8(APItemAddress, ItemsReceived[ItemIndex + 1] - 172000000) + in_game = u8(InGameAddress) + if in_game == 0x2A or in_game == 0xAC then + if u8(APItemAddress) == 0x00 then + ItemIndex = u16(APIndex) + if deathlink_rec == true then + wU8(APDeathLinkAddress, 1) + elseif u8(APDeathLinkAddress) == 3 then + wU8(APDeathLinkAddress, 0) + deathlink_send = true + end + if ItemsReceived[ItemIndex + 1] ~= nil then + item_id = ItemsReceived[ItemIndex + 1] - 172000000 + if item_id > 255 then + item_id = item_id - 256 + end + wU8(APItemAddress, item_id) + end end end end diff --git a/data/lua/TLoZ/TheLegendOfZeldaConnector.lua b/data/lua/TLoZ/TheLegendOfZeldaConnector.lua new file mode 100644 index 0000000000..ac33ed3cc4 --- /dev/null +++ b/data/lua/TLoZ/TheLegendOfZeldaConnector.lua @@ -0,0 +1,702 @@ +--Shamelessly based off the FF1 lua + +local socket = require("socket") +local json = require('json') +local math = require('math') + +local STATE_OK = "Ok" +local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected" +local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made" +local STATE_UNINITIALIZED = "Uninitialized" + +local itemMessages = {} +local consumableStacks = nil +local prevstate = "" +local curstate = STATE_UNINITIALIZED +local zeldaSocket = nil +local frame = 0 +local gameMode = 0 + +local cave_index +local triforce_byte +local game_state + +local u8 = nil +local wU8 = nil +local isNesHawk = false + +local shopsChecked = {} +local shopSlotLeft = 0x0628 +local shopSlotMiddle = 0x0629 +local shopSlotRight = 0x062A + +--N.B.: you won't find these in a RAM map. They're flag values that the base patch derives from the cave ID. +local blueRingShopBit = 0x40 +local potionShopBit = 0x02 +local arrowShopBit = 0x08 +local candleShopBit = 0x10 +local shieldShopBit = 0x20 +local takeAnyCaveBit = 0x01 + + +local sword = 0x0657 +local bombs = 0x0658 +local maxBombs = 0x067C +local keys = 0x066E +local arrow = 0x0659 +local bow = 0x065A +local candle = 0x065B +local recorder = 0x065C +local food = 0x065D +local waterOfLife = 0x065E +local magicalRod = 0x065F +local raft = 0x0660 +local bookOfMagic = 0x0661 +local ring = 0x0662 +local stepladder = 0x0663 +local magicalKey = 0x0664 +local powerBracelet = 0x0665 +local letter = 0x0666 +local clockItem = 0x066C +local heartContainers = 0x066F +local partialHearts = 0x0670 +local triforceFragments = 0x0671 +local boomerang = 0x0674 +local magicalBoomerang = 0x0675 +local magicalShield = 0x0676 +local rupeesToAdd = 0x067D +local rupeesToSubtract = 0x067E +local itemsObtained = 0x0677 +local takeAnyCavesChecked = 0x0678 +local localTriforce = 0x0679 +local bonusItemsObtained = 0x067A + +itemAPids = { + ["Boomerang"] = 7100, + ["Bow"] = 7101, + ["Magical Boomerang"] = 7102, + ["Raft"] = 7103, + ["Stepladder"] = 7104, + ["Recorder"] = 7105, + ["Magical Rod"] = 7106, + ["Red Candle"] = 7107, + ["Book of Magic"] = 7108, + ["Magical Key"] = 7109, + ["Red Ring"] = 7110, + ["Silver Arrow"] = 7111, + ["Sword"] = 7112, + ["White Sword"] = 7113, + ["Magical Sword"] = 7114, + ["Heart Container"] = 7115, + ["Letter"] = 7116, + ["Magical Shield"] = 7117, + ["Candle"] = 7118, + ["Arrow"] = 7119, + ["Food"] = 7120, + ["Water of Life (Blue)"] = 7121, + ["Water of Life (Red)"] = 7122, + ["Blue Ring"] = 7123, + ["Triforce Fragment"] = 7124, + ["Power Bracelet"] = 7125, + ["Small Key"] = 7126, + ["Bomb"] = 7127, + ["Recovery Heart"] = 7128, + ["Five Rupees"] = 7129, + ["Rupee"] = 7130, + ["Clock"] = 7131, + ["Fairy"] = 7132 +} + +itemCodes = { + ["Boomerang"] = 0x1D, + ["Bow"] = 0x0A, + ["Magical Boomerang"] = 0x1E, + ["Raft"] = 0x0C, + ["Stepladder"] = 0x0D, + ["Recorder"] = 0x05, + ["Magical Rod"] = 0x10, + ["Red Candle"] = 0x07, + ["Book of Magic"] = 0x11, + ["Magical Key"] = 0x0B, + ["Red Ring"] = 0x13, + ["Silver Arrow"] = 0x09, + ["Sword"] = 0x01, + ["White Sword"] = 0x02, + ["Magical Sword"] = 0x03, + ["Heart Container"] = 0x1A, + ["Letter"] = 0x15, + ["Magical Shield"] = 0x1C, + ["Candle"] = 0x06, + ["Arrow"] = 0x08, + ["Food"] = 0x04, + ["Water of Life (Blue)"] = 0x1F, + ["Water of Life (Red)"] = 0x20, + ["Blue Ring"] = 0x12, + ["Triforce Fragment"] = 0x1B, + ["Power Bracelet"] = 0x14, + ["Small Key"] = 0x19, + ["Bomb"] = 0x00, + ["Recovery Heart"] = 0x22, + ["Five Rupees"] = 0x0F, + ["Rupee"] = 0x18, + ["Clock"] = 0x21, + ["Fairy"] = 0x23 +} + + +--Sets correct memory access functions based on whether NesHawk or QuickNES is loaded +local function defineMemoryFunctions() + local memDomain = {} + local domains = memory.getmemorydomainlist() + if domains[1] == "System Bus" then + --NesHawk + isNesHawk = true + memDomain["systembus"] = function() memory.usememorydomain("System Bus") end + memDomain["ram"] = function() memory.usememorydomain("RAM") end + memDomain["saveram"] = function() memory.usememorydomain("Battery RAM") end + memDomain["rom"] = function() memory.usememorydomain("PRG ROM") end + elseif domains[1] == "WRAM" then + --QuickNES + memDomain["systembus"] = function() memory.usememorydomain("System Bus") end + memDomain["ram"] = function() memory.usememorydomain("RAM") end + memDomain["saveram"] = function() memory.usememorydomain("WRAM") end + memDomain["rom"] = function() memory.usememorydomain("PRG ROM") end + end + return memDomain +end + +local memDomain = defineMemoryFunctions() +u8 = memory.read_u8 +wU8 = memory.write_u8 +uRange = memory.readbyterange + +itemIDNames = {} + +for key, value in pairs(itemAPids) do + itemIDNames[value] = key +end + + + +local function determineItem(array) + memdomain.ram() + currentItemsObtained = u8(itemsObtained) + +end + +local function gotSword() + local currentSword = u8(sword) + wU8(sword, math.max(currentSword, 1)) +end + +local function gotWhiteSword() + local currentSword = u8(sword) + wU8(sword, math.max(currentSword, 2)) +end + +local function gotMagicalSword() + wU8(sword, 3) +end + +local function gotBomb() + local currentBombs = u8(bombs) + local currentMaxBombs = u8(maxBombs) + wU8(bombs, math.min(currentBombs + 4, currentMaxBombs)) + wU8(0x505, 0x29) -- Fake bomb to show item get. +end + +local function gotArrow() + local currentArrow = u8(arrow) + wU8(arrow, math.max(currentArrow, 1)) +end + +local function gotSilverArrow() + wU8(arrow, 2) +end + +local function gotBow() + wU8(bow, 1) +end + +local function gotCandle() + local currentCandle = u8(candle) + wU8(candle, math.max(currentCandle, 1)) +end + +local function gotRedCandle() + wU8(candle, 2) +end + +local function gotRecorder() + wU8(recorder, 1) +end + +local function gotFood() + wU8(food, 1) +end + +local function gotWaterOfLifeBlue() + local currentWaterOfLife = u8(waterOfLife) + wU8(waterOfLife, math.max(currentWaterOfLife, 1)) +end + +local function gotWaterOfLifeRed() + wU8(waterOfLife, 2) +end + +local function gotMagicalRod() + wU8(magicalRod, 1) +end + +local function gotBookOfMagic() + wU8(bookOfMagic, 1) +end + +local function gotRaft() + wU8(raft, 1) +end + +local function gotBlueRing() + local currentRing = u8(ring) + wU8(ring, math.max(currentRing, 1)) + memDomain.saveram() + local currentTunicColor = u8(0x0B92) + if currentTunicColor == 0x29 then + wU8(0x0B92, 0x32) + wU8(0x0804, 0x32) + end +end + +local function gotRedRing() + wU8(ring, 2) + memDomain.saveram() + wU8(0x0B92, 0x16) + wU8(0x0804, 0x16) +end + +local function gotStepladder() + wU8(stepladder, 1) +end + +local function gotMagicalKey() + wU8(magicalKey, 1) +end + +local function gotPowerBracelet() + wU8(powerBracelet, 1) +end + +local function gotLetter() + wU8(letter, 1) +end + +local function gotHeartContainer() + local currentHeartContainers = bit.rshift(bit.band(u8(heartContainers), 0xF0), 4) + if currentHeartContainers < 16 then + currentHeartContainers = math.min(currentHeartContainers + 1, 16) + local currentHearts = bit.band(u8(heartContainers), 0x0F) + 1 + wU8(heartContainers, bit.lshift(currentHeartContainers, 4) + currentHearts) + end +end + +local function gotTriforceFragment() + local triforceByte = 0xFF + local newTriforceCount = u8(localTriforce) + 1 + wU8(localTriforce, newTriforceCount) +end + +local function gotBoomerang() + wU8(boomerang, 1) +end + +local function gotMagicalBoomerang() + wU8(magicalBoomerang, 1) +end + +local function gotMagicalShield() + wU8(magicalShield, 1) +end + +local function gotRecoveryHeart() + local currentHearts = bit.band(u8(heartContainers), 0x0F) + local currentHeartContainers = bit.rshift(bit.band(u8(heartContainers), 0xF0), 4) + if currentHearts < currentHeartContainers then + currentHearts = currentHearts + 1 + else + wU8(partialHearts, 0xFF) + end + currentHearts = bit.bor(bit.band(u8(heartContainers), 0xF0), currentHearts) + wU8(heartContainers, currentHearts) +end + +local function gotFairy() + local currentHearts = bit.band(u8(heartContainers), 0x0F) + local currentHeartContainers = bit.rshift(bit.band(u8(heartContainers), 0xF0), 4) + if currentHearts < currentHeartContainers then + currentHearts = currentHearts + 3 + if currentHearts > currentHeartContainers then + currentHearts = currentHeartContainers + wU8(partialHearts, 0xFF) + end + else + wU8(partialHearts, 0xFF) + end + currentHearts = bit.bor(bit.band(u8(heartContainers), 0xF0), currentHearts) + wU8(heartContainers, currentHearts) +end + +local function gotClock() + wU8(clockItem, 1) +end + +local function gotFiveRupees() + local currentRupeesToAdd = u8(rupeesToAdd) + wU8(rupeesToAdd, math.min(currentRupeesToAdd + 5, 255)) +end + +local function gotSmallKey() + wU8(keys, math.min(u8(keys) + 1, 9)) +end + +local function gotItem(item) + --Write itemCode to itemToLift + --Write 128 to itemLiftTimer + --Write 4 to sound effect queue + itemName = itemIDNames[item] + itemCode = itemCodes[itemName] + wU8(0x505, itemCode) + wU8(0x506, 128) + wU8(0x602, 4) + numberObtained = u8(itemsObtained) + 1 + wU8(itemsObtained, numberObtained) + if itemName == "Boomerang" then gotBoomerang() end + if itemName == "Bow" then gotBow() end + if itemName == "Magical Boomerang" then gotMagicalBoomerang() end + if itemName == "Raft" then gotRaft() end + if itemName == "Stepladder" then gotStepladder() end + if itemName == "Recorder" then gotRecorder() end + if itemName == "Magical Rod" then gotMagicalRod() end + if itemName == "Red Candle" then gotRedCandle() end + if itemName == "Book of Magic" then gotBookOfMagic() end + if itemName == "Magical Key" then gotMagicalKey() end + if itemName == "Red Ring" then gotRedRing() end + if itemName == "Silver Arrow" then gotSilverArrow() end + if itemName == "Sword" then gotSword() end + if itemName == "White Sword" then gotWhiteSword() end + if itemName == "Magical Sword" then gotMagicalSword() end + if itemName == "Heart Container" then gotHeartContainer() end + if itemName == "Letter" then gotLetter() end + if itemName == "Magical Shield" then gotMagicalShield() end + if itemName == "Candle" then gotCandle() end + if itemName == "Arrow" then gotArrow() end + if itemName == "Food" then gotFood() end + if itemName == "Water of Life (Blue)" then gotWaterOfLifeBlue() end + if itemName == "Water of Life (Red)" then gotWaterOfLifeRed() end + if itemName == "Blue Ring" then gotBlueRing() end + if itemName == "Triforce Fragment" then gotTriforceFragment() end + if itemName == "Power Bracelet" then gotPowerBracelet() end + if itemName == "Small Key" then gotSmallKey() end + if itemName == "Bomb" then gotBomb() end + if itemName == "Recovery Heart" then gotRecoveryHeart() end + if itemName == "Five Rupees" then gotFiveRupees() end + if itemName == "Fairy" then gotFairy() end + if itemName == "Clock" then gotClock() end +end + + +local function StateOKForMainLoop() + memDomain.ram() + local gameMode = u8(0x12) + return gameMode == 5 +end + +local function checkCaveItemObtained() + memDomain.ram() + local returnTable = {} + returnTable["slot1"] = u8(shopSlotLeft) + returnTable["slot2"] = u8(shopSlotMiddle) + returnTable["slot3"] = u8(shopSlotRight) + returnTable["takeAnys"] = u8(takeAnyCavesChecked) + return returnTable +end + +function table.empty (self) + for _, _ in pairs(self) do + return false + end + return true +end + +function slice (tbl, s, e) + local pos, new = 1, {} + for i = s + 1, e do + new[pos] = tbl[i] + pos = pos + 1 + end + return new +end + +local bizhawk_version = client.getversion() +local is23Or24Or25 = (bizhawk_version=="2.3.1") or (bizhawk_version:sub(1,3)=="2.4") or (bizhawk_version:sub(1,3)=="2.5") +local is26To28 = (bizhawk_version:sub(1,3)=="2.6") or (bizhawk_version:sub(1,3)=="2.7") or (bizhawk_version:sub(1,3)=="2.8") + +local function getMaxMessageLength() + if is23Or24Or25 then + return client.screenwidth()/11 + elseif is26To28 then + return client.screenwidth()/12 + end +end + +local function drawText(x, y, message, color) + if is23Or24Or25 then + gui.addmessage(message) + elseif is26To28 then + gui.drawText(x, y, message, color, 0xB0000000, 18, "Courier New", "middle", "bottom", nil, "client") + end +end + +local function clearScreen() + if is23Or24Or25 then + return + elseif is26To28 then + drawText(0, 0, "", "black") + end +end + +local function drawMessages() + if table.empty(itemMessages) then + clearScreen() + return + end + local y = 10 + found = false + maxMessageLength = getMaxMessageLength() + for k, v in pairs(itemMessages) do + if v["TTL"] > 0 then + message = v["message"] + while true do + drawText(5, y, message:sub(1, maxMessageLength), v["color"]) + y = y + 16 + + message = message:sub(maxMessageLength + 1, message:len()) + if message:len() == 0 then + break + end + end + newTTL = 0 + if is26To28 then + newTTL = itemMessages[k]["TTL"] - 1 + end + itemMessages[k]["TTL"] = newTTL + found = true + end + end + if found == false then + clearScreen() + end +end + +function generateOverworldLocationChecked() + memDomain.ram() + data = uRange(0x067E, 0x81) + data[0] = nil + return data +end + +function getHCLocation() + memDomain.rom() + data = u8(0x1789A) + return data +end + +function getPBLocation() + memDomain.rom() + data = u8(0x10CB2) + return data +end + +function generateUnderworld16LocationChecked() + memDomain.ram() + data = uRange(0x06FE, 0x81) + data[0] = nil + return data +end + +function generateUnderworld79LocationChecked() + memDomain.ram() + data = uRange(0x077E, 0x81) + data[0] = nil + return data +end + +function updateTriforceFragments() + memDomain.ram() + local triforceByte = 0xFF + totalTriforceCount = u8(localTriforce) + local currentPieces = bit.rshift(triforceByte, 8 - math.min(8, totalTriforceCount)) + wU8(triforceFragments, currentPieces) +end + +function processBlock(block) + if block ~= nil then + local msgBlock = block['messages'] + if msgBlock ~= nil then + for i, v in pairs(msgBlock) do + if itemMessages[i] == nil then + local msg = {TTL=450, message=v, color=0xFFFF0000} + itemMessages[i] = msg + end + end + end + local bonusItems = block["bonusItems"] + if bonusItems ~= nil and isInGame then + for i, item in ipairs(bonusItems) do + memDomain.ram() + if i > u8(bonusItemsObtained) then + if u8(0x505) == 0 then + gotItem(item) + wU8(itemsObtained, u8(itemsObtained) - 1) + wU8(bonusItemsObtained, u8(bonusItemsObtained) + 1) + end + end + end + end + local itemsBlock = block["items"] + memDomain.saveram() + isInGame = StateOKForMainLoop() + updateTriforceFragments() + if itemsBlock ~= nil and isInGame then + memDomain.ram() + --get item from item code + --get function from item + --do function + for i, item in ipairs(itemsBlock) do + memDomain.ram() + if u8(0x505) == 0 then + if i > u8(itemsObtained) then + gotItem(item) + end + end + end + end + local shopsBlock = block["shops"] + if shopsBlock ~= nil then + wU8(shopSlotLeft, bit.bor(u8(shopSlotLeft), shopsBlock["left"])) + wU8(shopSlotMiddle, bit.bor(u8(shopSlotMiddle), shopsBlock["middle"])) + wU8(shopSlotRight, bit.bor(u8(shopSlotRight), shopsBlock["right"])) + end + end +end + +function difference(a, b) + local aa = {} + for k,v in pairs(a) do aa[v]=true end + for k,v in pairs(b) do aa[v]=nil end + local ret = {} + local n = 0 + for k,v in pairs(a) do + if aa[v] then n=n+1 ret[n]=v end + end + return ret +end + +function receive() + l, e = zeldaSocket:receive() + if e == 'closed' then + if curstate == STATE_OK then + print("Connection closed") + end + curstate = STATE_UNINITIALIZED + return + elseif e == 'timeout' then + print("timeout") + return + elseif e ~= nil then + print(e) + curstate = STATE_UNINITIALIZED + return + end + processBlock(json.decode(l)) + + -- Determine Message to send back + memDomain.rom() + local playerName = uRange(0x1F, 0x11) + playerName[0] = nil + local retTable = {} + retTable["playerName"] = playerName + if StateOKForMainLoop() then + retTable["overworld"] = generateOverworldLocationChecked() + retTable["underworld1"] = generateUnderworld16LocationChecked() + retTable["underworld2"] = generateUnderworld79LocationChecked() + end + retTable["caves"] = checkCaveItemObtained() + memDomain.ram() + if gameMode ~= 19 then + gameMode = u8(0x12) + end + retTable["gameMode"] = gameMode + retTable["overworldHC"] = getHCLocation() + retTable["overworldPB"] = getPBLocation() + retTable["itemsObtained"] = u8(itemsObtained) + msg = json.encode(retTable).."\n" + local ret, error = zeldaSocket:send(msg) + if ret == nil then + print(error) + elseif curstate == STATE_INITIAL_CONNECTION_MADE then + curstate = STATE_TENTATIVELY_CONNECTED + elseif curstate == STATE_TENTATIVELY_CONNECTED then + print("Connected!") + itemMessages["(0,0)"] = {TTL=240, message="Connected", color="green"} + curstate = STATE_OK + end +end + +function main() + if (is23Or24Or25 or is26To28) == false then + print("Must use a version of bizhawk 2.3.1 or higher") + return + end + server, error = socket.bind('localhost', 52980) + + while true do + gui.drawEllipse(248, 9, 6, 6, "Black", "Yellow") + frame = frame + 1 + drawMessages() + if not (curstate == prevstate) then + -- console.log("Current state: "..curstate) + prevstate = curstate + end + if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then + if (frame % 60 == 0) then + gui.drawEllipse(248, 9, 6, 6, "Black", "Blue") + receive() + else + gui.drawEllipse(248, 9, 6, 6, "Black", "Green") + end + elseif (curstate == STATE_UNINITIALIZED) then + gui.drawEllipse(248, 9, 6, 6, "Black", "White") + if (frame % 60 == 0) then + gui.drawEllipse(248, 9, 6, 6, "Black", "Yellow") + + drawText(5, 8, "Waiting for client", 0xFFFF0000) + drawText(5, 32, "Please start Zelda1Client.exe", 0xFFFF0000) + + -- Advance so the messages are drawn + emu.frameadvance() + server:settimeout(2) + print("Attempting to connect") + local client, timeout = server:accept() + if timeout == nil then + -- print('Initial Connection Made') + curstate = STATE_INITIAL_CONNECTION_MADE + zeldaSocket = client + zeldaSocket:settimeout(0) + end + end + end + emu.frameadvance() + end +end + +main() \ No newline at end of file diff --git a/data/lua/TLoZ/core.dll b/data/lua/TLoZ/core.dll new file mode 100644 index 0000000000..3e9569571a Binary files /dev/null and b/data/lua/TLoZ/core.dll differ diff --git a/data/lua/TLoZ/json.lua b/data/lua/TLoZ/json.lua new file mode 100644 index 0000000000..0833bf6fb4 --- /dev/null +++ b/data/lua/TLoZ/json.lua @@ -0,0 +1,380 @@ +-- +-- json.lua +-- +-- Copyright (c) 2015 rxi +-- +-- This library is free software; you can redistribute it and/or modify it +-- under the terms of the MIT license. See LICENSE for details. +-- + +local json = { _version = "0.1.0" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\\\", + [ "\"" ] = "\\\"", + [ "\b" ] = "\\b", + [ "\f" ] = "\\f", + [ "\n" ] = "\\n", + [ "\r" ] = "\\r", + [ "\t" ] = "\\t", +} + +local escape_char_map_inv = { [ "\\/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return escape_char_map[c] or string.format("\\u%04x", c:byte()) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if val[1] ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + --local line_count = 1 + --local col_count = 1 + --for i = 1, idx - 1 do + -- col_count = col_count + 1 + -- if str:sub(i, i) == "\n" then + -- line_count = line_count + 1 + -- col_count = 1 + -- end + -- end + -- emu.message( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(3, 6), 16 ) + local n2 = tonumber( s:sub(9, 12), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local has_unicode_escape = false + local has_surrogate_escape = false + local has_escape = false + local last + for j = i + 1, #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + end + + if last == 92 then -- "\\" (escape char) + if x == 117 then -- "u" (unicode escape sequence) + local hex = str:sub(j + 1, j + 5) + if not hex:find("%x%x%x%x") then + decode_error(str, j, "invalid unicode escape in string") + end + if hex:find("^[dD][89aAbB]") then + has_surrogate_escape = true + else + has_unicode_escape = true + end + else + local c = string.char(x) + if not escape_chars[c] then + decode_error(str, j, "invalid escape char '" .. c .. "' in string") + end + has_escape = true + end + last = nil + + elseif x == 34 then -- '"' (end of string) + local s = str:sub(i + 1, j - 1) + if has_surrogate_escape then + s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape) + end + if has_unicode_escape then + s = s:gsub("\\u....", parse_unicode_escape) + end + if has_escape then + s = s:gsub("\\.", escape_char_map_inv) + end + return s, j + 1 + + else + last = x + end + end + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + return ( parse(str, next_char(str, 1, space_chars, true)) ) +end + + +return json \ No newline at end of file diff --git a/data/lua/TLoZ/socket.lua b/data/lua/TLoZ/socket.lua new file mode 100644 index 0000000000..a98e952115 --- /dev/null +++ b/data/lua/TLoZ/socket.lua @@ -0,0 +1,132 @@ +----------------------------------------------------------------------------- +-- LuaSocket helper module +-- Author: Diego Nehab +-- RCS ID: $Id: socket.lua,v 1.22 2005/11/22 08:33:29 diego Exp $ +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Declare module and import dependencies +----------------------------------------------------------------------------- +local base = _G +local string = require("string") +local math = require("math") +local socket = require("socket.core") +module("socket") + +----------------------------------------------------------------------------- +-- Exported auxiliar functions +----------------------------------------------------------------------------- +function connect(address, port, laddress, lport) + local sock, err = socket.tcp() + if not sock then return nil, err end + if laddress then + local res, err = sock:bind(laddress, lport, -1) + if not res then return nil, err end + end + local res, err = sock:connect(address, port) + if not res then return nil, err end + return sock +end + +function bind(host, port, backlog) + local sock, err = socket.tcp() + if not sock then return nil, err end + sock:setoption("reuseaddr", true) + local res, err = sock:bind(host, port) + if not res then return nil, err end + res, err = sock:listen(backlog) + if not res then return nil, err end + return sock +end + +try = newtry() + +function choose(table) + return function(name, opt1, opt2) + if base.type(name) ~= "string" then + name, opt1, opt2 = "default", name, opt1 + end + local f = table[name or "nil"] + if not f then base.error("unknown key (".. base.tostring(name) ..")", 3) + else return f(opt1, opt2) end + end +end + +----------------------------------------------------------------------------- +-- Socket sources and sinks, conforming to LTN12 +----------------------------------------------------------------------------- +-- create namespaces inside LuaSocket namespace +sourcet = {} +sinkt = {} + +BLOCKSIZE = 2048 + +sinkt["close-when-done"] = function(sock) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function(self, chunk, err) + if not chunk then + sock:close() + return 1 + else return sock:send(chunk) end + end + }) +end + +sinkt["keep-open"] = function(sock) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function(self, chunk, err) + if chunk then return sock:send(chunk) + else return 1 end + end + }) +end + +sinkt["default"] = sinkt["keep-open"] + +sink = choose(sinkt) + +sourcet["by-length"] = function(sock, length) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function() + if length <= 0 then return nil end + local size = math.min(socket.BLOCKSIZE, length) + local chunk, err = sock:receive(size) + if err then return nil, err end + length = length - string.len(chunk) + return chunk + end + }) +end + +sourcet["until-closed"] = function(sock) + local done + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function() + if done then return nil end + local chunk, err, partial = sock:receive(socket.BLOCKSIZE) + if not err then return chunk + elseif err == "closed" then + sock:close() + done = 1 + return partial + else return nil, err end + end + }) +end + + +sourcet["default"] = sourcet["until-closed"] + +source = choose(sourcet) diff --git a/data/lua/connector_ladx_bizhawk.lua b/data/lua/connector_ladx_bizhawk.lua new file mode 100644 index 0000000000..e318015cb0 --- /dev/null +++ b/data/lua/connector_ladx_bizhawk.lua @@ -0,0 +1,137 @@ +-- SPDX-FileCopyrightText: 2023 Wilhelm SchÃŧrmann +-- +-- SPDX-License-Identifier: MIT + +-- This script attempts to implement the basic functionality needed in order for +-- the LADXR Archipelago client to be able to talk to BizHawk instead of RetroArch +-- by reproducing the RetroArch API with BizHawk's Lua interface. +-- +-- RetroArch UDP API: https://github.com/libretro/RetroArch/blob/master/command.c +-- +-- Only +-- VERSION +-- GET_STATUS +-- READ_CORE_MEMORY +-- WRITE_CORE_MEMORY +-- commands are supported right now. +-- +-- USAGE: +-- Load this script in BizHawk ("Tools" -> "Lua Console" -> "Script" -> "Open Script") +-- +-- All inconsistencies (like missing newlines for some commands) of the RetroArch +-- UDP API (network_cmd_enable) are reproduced as-is in order for clients written to work with +-- RetroArch's current API to "just work"(tm). +-- +-- This script has only been tested on GB(C). If you have made sure it works for N64 or other +-- cores supported by BizHawk, please let me know. Note that GET_STATUS, at the very least, will +-- have to be adjusted. +-- +-- +-- NOTE: +-- BizHawk's Lua API is very trigger-happy on throwing exceptions. +-- Emulation will continue fine, but the RetroArch API layer will stop working. This +-- is indicated only by an exception visible in the Lua console, which most players +-- will probably not have in the foreground. +-- +-- pcall(), the usual way to catch exceptions in Lua, doesn't appear to be supported at all, +-- meaning that error/exception handling is not easily possible. +-- +-- This means that a lot more error checking would need to happen before e.g. reading/writing +-- memory. Since the end goal, according to AP's Discord, seems to be SNI integration of GB(C), +-- no further fault-proofing has been done on this. +-- + + +local socket = require("socket") +local udp = socket.udp() + +udp:setsockname('127.0.0.1', 55355) +udp:settimeout(0) + + +while true do + -- Attempt to lessen the CPU load by only polling the UDP socket every x frames. + -- x = 10 is entirely arbitrary, very little thought went into it. + -- We could try to make use of client.get_approx_framerate() here, but the values returned + -- seemed more or less arbitrary as well. + -- + -- NOTE: Never mind the above, the LADXR Archipelago client appears to run into problems with + -- interwoven GET_STATUS calls, leading to stopped communication. + -- For GB(C), polling the socket on every frame is OK-ish, so we just do that. + -- + --while emu.framecount() % 10 ~= 0 do + -- emu.frameadvance() + --end + + local data, msg_or_ip, port_or_nil = udp:receivefrom() + if data then + -- "data" format is "COMMAND [PARAMETERS] [...]" + local command = string.match(data, "%S+") + if command == "VERSION" then + -- 1.14 is the latest RetroArch release at the time of writing this, no other reason + -- for choosing this here. + udp:sendto("1.14.0\n", msg_or_ip, port_or_nil) + elseif command == "GET_STATUS" then + local status = "PLAYING" + if client.ispaused() then + status = "PAUSED" + end + + if emu.getsystemid() == "GBC" then + -- Actual reply from RetroArch's API: + -- "GET_STATUS PLAYING game_boy,AP_62468482466172374046_P1_Lonk,crc32=3ecb7b6f" + -- CRC32 isn't readily available through the Lua API. We could calculate + -- it ourselves, but since LADXR doesn't make use of this field it is + -- simply replaced by the hash that BizHawk _does_ make available. + + udp:sendto( + "GET_STATUS " .. status .. " game_boy," .. + string.gsub(gameinfo.getromname(), "[%s,]", "_") .. + ",romhash=" .. + gameinfo.getromhash() .. "\n", + msg_or_ip, port_or_nil + ) + else -- No ROM loaded + -- NOTE: No newline is intentional here for 1:1 RetroArch compatibility + udp:sendto("GET_STATUS CONTENTLESS", msg_or_ip, port_or_nil) + end + elseif command == "READ_CORE_MEMORY" then + local _, address, length = string.match(data, "(%S+) (%S+) (%S+)") + address = tonumber(address, 16) + length = tonumber(length) + + -- NOTE: mainmemory.read_bytes_as_array() would seem to be the obvious choice + -- here instead, but it isn't. At least for Sameboy and Gambatte, the "main" + -- memory differs (ROM vs WRAM). + -- Using memory.read_bytes_as_array() and explicitly using the System Bus + -- as the active memory domain solves this incompatibility, allowing us + -- to hopefully use whatever GB(C) emulator we want. + local mem = memory.read_bytes_as_array(address, length, "System Bus") + local hex_string = "" + for _, v in ipairs(mem) do + hex_string = hex_string .. string.format("%02X ", v) + end + hex_string = hex_string:sub(1, -2) -- Hang head in shame, remove last " " + local reply = string.format("%s %02x %s\n", command, address, hex_string) + udp:sendto(reply, msg_or_ip, port_or_nil) + elseif command == "WRITE_CORE_MEMORY" then + local _, address = string.match(data, "(%S+) (%S+)") + address = tonumber(address, 16) + + local to_write = {} + local i = 1 + for byte_str in string.gmatch(data, "%S+") do + if i > 2 then + table.insert(to_write, tonumber(byte_str, 16)) + end + i = i + 1 + end + + memory.write_bytes_as_array(address, to_write, "System Bus") + local reply = string.format("%s %02x %d\n", command, address, i - 3) + udp:sendto(reply, msg_or_ip, port_or_nil) + end + end + + emu.frameadvance() +end diff --git a/data/lua/core.dll b/data/lua/core.dll new file mode 100644 index 0000000000..3e9569571a Binary files /dev/null and b/data/lua/core.dll differ diff --git a/data/lua/socket.lua b/data/lua/socket.lua new file mode 100644 index 0000000000..a98e952115 --- /dev/null +++ b/data/lua/socket.lua @@ -0,0 +1,132 @@ +----------------------------------------------------------------------------- +-- LuaSocket helper module +-- Author: Diego Nehab +-- RCS ID: $Id: socket.lua,v 1.22 2005/11/22 08:33:29 diego Exp $ +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Declare module and import dependencies +----------------------------------------------------------------------------- +local base = _G +local string = require("string") +local math = require("math") +local socket = require("socket.core") +module("socket") + +----------------------------------------------------------------------------- +-- Exported auxiliar functions +----------------------------------------------------------------------------- +function connect(address, port, laddress, lport) + local sock, err = socket.tcp() + if not sock then return nil, err end + if laddress then + local res, err = sock:bind(laddress, lport, -1) + if not res then return nil, err end + end + local res, err = sock:connect(address, port) + if not res then return nil, err end + return sock +end + +function bind(host, port, backlog) + local sock, err = socket.tcp() + if not sock then return nil, err end + sock:setoption("reuseaddr", true) + local res, err = sock:bind(host, port) + if not res then return nil, err end + res, err = sock:listen(backlog) + if not res then return nil, err end + return sock +end + +try = newtry() + +function choose(table) + return function(name, opt1, opt2) + if base.type(name) ~= "string" then + name, opt1, opt2 = "default", name, opt1 + end + local f = table[name or "nil"] + if not f then base.error("unknown key (".. base.tostring(name) ..")", 3) + else return f(opt1, opt2) end + end +end + +----------------------------------------------------------------------------- +-- Socket sources and sinks, conforming to LTN12 +----------------------------------------------------------------------------- +-- create namespaces inside LuaSocket namespace +sourcet = {} +sinkt = {} + +BLOCKSIZE = 2048 + +sinkt["close-when-done"] = function(sock) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function(self, chunk, err) + if not chunk then + sock:close() + return 1 + else return sock:send(chunk) end + end + }) +end + +sinkt["keep-open"] = function(sock) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function(self, chunk, err) + if chunk then return sock:send(chunk) + else return 1 end + end + }) +end + +sinkt["default"] = sinkt["keep-open"] + +sink = choose(sinkt) + +sourcet["by-length"] = function(sock, length) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function() + if length <= 0 then return nil end + local size = math.min(socket.BLOCKSIZE, length) + local chunk, err = sock:receive(size) + if err then return nil, err end + length = length - string.len(chunk) + return chunk + end + }) +end + +sourcet["until-closed"] = function(sock) + local done + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function() + if done then return nil end + local chunk, err, partial = sock:receive(socket.BLOCKSIZE) + if not err then return chunk + elseif err == "closed" then + sock:close() + done = 1 + return partial + else return nil, err end + end + }) +end + + +sourcet["default"] = sourcet["until-closed"] + +source = choose(sourcet) diff --git a/data/sprites/ladx/Bowwow.bdiff b/data/sprites/ladx/Bowwow.bdiff new file mode 100644 index 0000000000..bdfe9f42f2 Binary files /dev/null and b/data/sprites/ladx/Bowwow.bdiff differ diff --git a/data/sprites/ladx/Bunny.bdiff b/data/sprites/ladx/Bunny.bdiff new file mode 100644 index 0000000000..848829d030 Binary files /dev/null and b/data/sprites/ladx/Bunny.bdiff differ diff --git a/data/sprites/ladx/Luigi.bdiff b/data/sprites/ladx/Luigi.bdiff new file mode 100644 index 0000000000..1897a65d01 Binary files /dev/null and b/data/sprites/ladx/Luigi.bdiff differ diff --git a/data/sprites/ladx/Mario.bdiff b/data/sprites/ladx/Mario.bdiff new file mode 100644 index 0000000000..389fa94ab1 Binary files /dev/null and b/data/sprites/ladx/Mario.bdiff differ diff --git a/data/sprites/ladx/Matty_LA.bdiff b/data/sprites/ladx/Matty_LA.bdiff new file mode 100644 index 0000000000..f926d46d8d Binary files /dev/null and b/data/sprites/ladx/Matty_LA.bdiff differ diff --git a/data/sprites/ladx/Richard.bdiff b/data/sprites/ladx/Richard.bdiff new file mode 100644 index 0000000000..a244d7026b Binary files /dev/null and b/data/sprites/ladx/Richard.bdiff differ diff --git a/data/sprites/ladx/Tarin.bdiff b/data/sprites/ladx/Tarin.bdiff new file mode 100644 index 0000000000..e30e2d1f3c Binary files /dev/null and b/data/sprites/ladx/Tarin.bdiff differ diff --git a/docs/network diagram/network diagram.jpg b/docs/network diagram/network diagram.jpg index 15495e2724..0027db57ba 100644 Binary files a/docs/network diagram/network diagram.jpg and b/docs/network diagram/network diagram.jpg differ diff --git a/docs/network diagram/network diagram.md b/docs/network diagram/network diagram.md index 2bffd9f295..926c8723a0 100644 --- a/docs/network diagram/network diagram.md +++ b/docs/network diagram/network diagram.md @@ -75,6 +75,18 @@ flowchart LR end SNI <-- Various, depending on SNES device --> DK3 + %% Super Mario World + subgraph Super Mario World + SMW[SNES] + end + SNI <-- Various, depending on SNES device --> SMW + + %% Lufia II Ancient Cave + subgraph Lufia II Ancient Cave + L2AC[SNES] + end + SNI <-- Various, depending on SNES device --> L2AC + %% Native Clients or Games %% Games or clients which compile to native or which the client is integrated in the game. subgraph "Native" diff --git a/docs/network diagram/network diagram.svg b/docs/network diagram/network diagram.svg index 38d3cc0713..ba29b744d5 100644 --- a/docs/network diagram/network diagram.svg +++ b/docs/network diagram/network diagram.svg @@ -1 +1 @@ -
Factorio
Secret of Evermore
WebHost (archipelago.gg)
.NET
Java
Native
Donkey Kong Country 3
Super Metroid/A Link to the Past Combo Randomizer
Super Metroid
Ocarina of Time
Final Fantasy 1
A Link to the Past
ChecksFinder
Starcraft 2
FNA/XNA
Unity
Minecraft
Secret of Evermore
WebSockets
WebSockets
Integrated
Integrated
Various, depending on SNES device
LuaSockets
Integrated
LuaSockets
Integrated
Integrated
WebSockets
Various, depending on SNES device
Various, depending on SNES device
Various, depending on SNES device
The Witness Randomizer
Various, depending on SNES device
WebSockets
WebSockets
Mod the Spire
TCP
Forge Mod Loader
WebSockets
TsRandomizer
RogueLegacyRandomizer
BepInEx
QModLoader (BepInEx)
HK Modding API
WebSockets
SQL
Subprocesses
SQL
Deposit Generated Worlds
Provide Generation Instructions
Subprocesses
Subprocesses
RCON
UDP
Integrated
Factorio Server
FactorioClient
Factorio Games
Factorio Mod Generated by AP
Factorio Modding API
SNES
Configurable (waitress, gunicorn, flask)
AutoHoster
PonyORM DB
WebHost
Flask WebContent
AutoGenerator
Mod with Archipelago.MultiClient.Net
Risk of Rain 2
Subnautica
Hollow Knight
Raft
Timespinner
Rogue Legacy
Mod with Archipelago.MultiClient.Java
Slay the Spire
Minecraft Forge Server
Any Java Minecraft Clients
Game using apclientpp Client Library
Game using Apcpp Client Library
Super Mario 64 Ex
VVVVVV
Meritous
The Witness
Sonic Adventure 2: Battle
Dark Souls 3
ap-soeclient
SNES
SNES
SNES
OoTClient
Lua Connector
BizHawk with Ocarina of Time Loaded
FF1Client
Lua Connector
BizHawk with Final Fantasy Loaded
SNES
ChecksFinderClient
ChecksFinder
Starcraft 2 Game Client
Starcraft2Client.py
apsc2 Python Package
Archipelago Server
CommonClient.py
Super Nintendo Interface (SNI)
SNIClient
\ No newline at end of file +
Factorio
Secret of Evermore
WebHost (archipelago.gg)
.NET
Java
Native
Lufia II Ancient Cave
Super Mario World
Donkey Kong Country 3
SMZ3
Super Metroid
Ocarina of Time
Final Fantasy 1
A Link to the Past
ChecksFinder
Starcraft 2
FNA/XNA
Unity
Minecraft
Secret of Evermore
WebSockets
WebSockets
Integrated
Integrated
Various, depending on SNES device
LuaSockets
Integrated
LuaSockets
Integrated
Integrated
WebSockets
Various, depending on SNES device
Various, depending on SNES device
Various, depending on SNES device
Various, depending on SNES device
Various, depending on SNES device
The Witness Randomizer
Various, depending on SNES device
WebSockets
WebSockets
Mod the Spire
TCP
Forge Mod Loader
WebSockets
TsRandomizer
RogueLegacyRandomizer
BepInEx
QModLoader (BepInEx)
HK Modding API
WebSockets
SQL
Subprocesses
SQL
Deposit Generated Worlds
Provide Generation Instructions
Subprocesses
Subprocesses
RCON
UDP
Integrated
Factorio Server
FactorioClient
Factorio Games
Factorio Mod Generated by AP
Factorio Modding API
SNES
Configurable (waitress, gunicorn, flask)
AutoHoster
PonyORM DB
WebHost
Flask WebContent
AutoGenerator
Mod with Archipelago.MultiClient.Net
Risk of Rain 2
Subnautica
Hollow Knight
Raft
Timespinner
Rogue Legacy
Mod with Archipelago.MultiClient.Java
Slay the Spire
Minecraft Forge Server
Any Java Minecraft Clients
Game using apclientpp Client Library
Game using Apcpp Client Library
Super Mario 64 Ex
VVVVVV
Meritous
The Witness
Sonic Adventure 2: Battle
Dark Souls 3
ap-soeclient
SNES
SNES
SNES
SNES
SNES
OoTClient
Lua Connector
BizHawk with Ocarina of Time Loaded
FF1Client
Lua Connector
BizHawk with Final Fantasy Loaded
SNES
ChecksFinderClient
ChecksFinder
Starcraft 2 Game Client
Starcraft2Client.py
apsc2 Python Package
Archipelago Server
CommonClient.py
Super Nintendo Interface (SNI)
SNIClient
\ No newline at end of file diff --git a/docs/network protocol.md b/docs/network protocol.md index bfffcc580a..f4e261dcee 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -64,18 +64,19 @@ These packets are are sent from the multiworld server to the client. They are no ### RoomInfo Sent to clients when they connect to an Archipelago server. #### Arguments -| Name | Type | Notes | -| ---- | ---- | ----- | -| version | [NetworkVersion](#NetworkVersion) | Object denoting the version of Archipelago which the server is running. | -| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. Example: `WebHost` | -| password | bool | Denoted whether a password is required to join this room.| -| permissions | dict\[str, [Permission](#Permission)\[int\]\] | Mapping of permission name to [Permission](#Permission), keys are: "release", "collect" and "remaining". | -| hint_cost | int | The amount of points it costs to receive a hint from the server. | -| location_check_points | int | The amount of hint points you receive per item/location check completed. || -| games | list\[str\] | List of games present in this multiworld. | -| datapackage_versions | dict\[str, int\] | Data versions of the individual games' data packages the server will send. Used to decide which games' caches are outdated. See [Data Package Contents](#Data-Package-Contents). | -| seed_name | str | uniquely identifying name of this generation | -| time | float | Unix time stamp of "now". Send for time synchronization if wanted for things like the DeathLink Bounce. | +| Name | Type | Notes | +|-----------------------|-----------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| version | [NetworkVersion](#NetworkVersion) | Object denoting the version of Archipelago which the server is running. | +| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. Example: `WebHost` | +| password | bool | Denoted whether a password is required to join this room. | +| permissions | dict\[str, [Permission](#Permission)\[int\]\] | Mapping of permission name to [Permission](#Permission), keys are: "release", "collect" and "remaining". | +| hint_cost | int | The percentage of total locations that need to be checked to receive a hint from the server. | +| location_check_points | int | The amount of hint points you receive per item/location check completed. | +| games | list\[str\] | List of games present in this multiworld. | +| datapackage_versions | dict\[str, int\] | Data versions of the individual games' data packages the server will send. Used to decide which games' caches are outdated. See [Data Package Contents](#Data-Package-Contents). **Deprecated. Use `datapackage_checksums` instead.** | +| datapackage_checksums | dict[str, str] | Checksum hash of the individual games' data packages the server will send. Used by newer clients to decide which games' caches are outdated. See [Data Package Contents](#Data-Package-Contents) for more information. | +| seed_name | str | Uniquely identifying name of this generation | +| time | float | Unix time stamp of "now". Send for time synchronization if wanted for things like the DeathLink Bounce. | #### release Dictates what is allowed when it comes to a player releasing their run. A release is an action which distributes the rest of the items in a player's run to those other players awaiting them. @@ -106,8 +107,8 @@ Dictates what is allowed when it comes to a player querying the items remaining ### ConnectionRefused Sent to clients when the server refuses connection. This is sent during the initial connection handshake. #### Arguments -| Name | Type | Notes | -| ---- | ---- | ----- | +| Name | Type | Notes | +|--------|-------------|--------------------------------------------------------------------------------------------------------------------------------------------------------| | errors | list\[str\] | Optional. When provided, should contain any one of: `InvalidSlot`, `InvalidGame`, `IncompatibleVersion`, `InvalidPassword`, or `InvalidItemsHandling`. | InvalidSlot indicates that the sent 'name' field did not match any auth entry on the server. @@ -554,12 +555,16 @@ Color options: `flags` contains the [NetworkItem](#NetworkItem) flags that belong to the item ### Client States -An enumeration containing the possible client states that may be used to inform the server in [StatusUpdate](#StatusUpdate). +An enumeration containing the possible client states that may be used to inform +the server in [StatusUpdate](#StatusUpdate). The MultiServer automatically sets +the client state to `ClientStatus.CLIENT_CONNECTED` on the first active connection +to a slot. ```python import enum class ClientStatus(enum.IntEnum): CLIENT_UNKNOWN = 0 + CLIENT_CONNECTED = 5 CLIENT_READY = 10 CLIENT_PLAYING = 20 CLIENT_GOAL = 30 @@ -644,11 +649,12 @@ Note: #### GameData GameData is a **dict** but contains these keys and values. It's broken out into another "type" for ease of documentation. -| Name | Type | Notes | -| ---- | ---- | ----- | -| item_name_to_id | dict[str, int] | Mapping of all item names to their respective ID. | -| location_name_to_id | dict[str, int] | Mapping of all location names to their respective ID. | -| version | int | Version number of this game's data | +| Name | Type | Notes | +|---------------------|----------------|-------------------------------------------------------------------------------------------------------------------------------| +| item_name_to_id | dict[str, int] | Mapping of all item names to their respective ID. | +| location_name_to_id | dict[str, int] | Mapping of all location names to their respective ID. | +| version | int | Version number of this game's data. Deprecated. Used by older clients to request an updated datapackage if cache is outdated. | +| checksum | str | A checksum hash of this game's data. | ### Tags Tags are represented as a list of strings, the common Client tags follow: diff --git a/docs/options api.md b/docs/options api.md new file mode 100644 index 0000000000..a1407f2ceb --- /dev/null +++ b/docs/options api.md @@ -0,0 +1,188 @@ +# Archipelago Options API + +This document covers some of the generic options available using Archipelago's options handling system. + +For more information on where these options go in your world please refer to: + - [world api.md](/docs/world%20api.md) + +Archipelago will be abbreviated as "AP" from now on. + +## Option Definitions +Option parsing in AP is done using different Option classes. For each option you would like to have in your game, you +need to create: +- A new option class with a docstring detailing what the option will do to your user. +- A `display_name` to be displayed on the webhost. +- A new entry in the `option_definitions` dict for your World. +By style and convention, the internal names should be snake_case. If the option supports having multiple sub_options +such as Choice options, these can be defined with `option_my_sub_option`, where the preceding `option_` is required and +stripped for users, so will show as `my_sub_option` in yaml files and if `auto_display_name` is True `My Sub Option` +on the webhost. All options support `random` as a generic option. `random` chooses from any of the available +values for that option, and is reserved by AP. You can set this as your default value but you cannot define your own +new `option_random`. + +### Option Creation +As an example, suppose we want an option that lets the user start their game with a sword in their inventory. Let's +create our option class (with a docstring), give it a `display_name`, and add it to a dictionary that keeps track of our +options: + +```python +# Options.py +class StartingSword(Toggle): + """Adds a sword to your starting inventory.""" + display_name = "Start With Sword" + + +example_options = { + "starting_sword": StartingSword +} +``` + +This will create a `Toggle` option, internally called `starting_sword`. To then submit this to the multiworld, we add it +to our world's `__init__.py`: + +```python +from worlds.AutoWorld import World +from .Options import options + + +class ExampleWorld(World): + option_definitions = options +``` + +### Option Checking +Options are parsed by `Generate.py` before the worlds are created, and then the option classes are created shortly after +world instantiation. These are created as attributes on the MultiWorld and can be accessed with +`self.multiworld.my_option_name[self.player]`. This is the option class, which supports direct comparison methods to +relevant objects (like comparing a Toggle class to a `bool`). If you need to access the option result directly, this is +the option class's `value` attribute. For our example above we can do a simple check: +```python +if self.multiworld.starting_sword[self.player]: + do_some_things() +``` + +or if I need a boolean object, such as in my slot_data I can access it as: +```python +start_with_sword = bool(self.multiworld.starting_sword[self.player].value) +``` + +## Generic Option Classes +These options are generically available to every game automatically, but can be overridden for slightly different +behavior, if desired. See `worlds/soe/Options.py` for an example. + +### Accessibility +Sets rules for availability of locations for the player. `Items` is for all items available but not necessarily all +locations, such as self-locking keys, but needs to be set by the world for this to be different from locations access. + +### ProgressionBalancing +Algorithm for moving progression items into earlier spheres to make the gameplay experience a bit smoother. Can be +overridden if you want a different default value. + +### LocalItems +Forces the players' items local to their world. + +### NonLocalItems +Forces the players' items outside their world. + +### StartInventory +Allows the player to define a dictionary of starting items with item name and quantity. + +### StartHints +Gives the player starting hints for where the items defined here are. + +### StartLocationHints +Gives the player starting hints for the items on locations defined here. + +### ExcludeLocations +Marks locations given here as `LocationProgressType.Excluded` so that progression items can't be placed on them. + +### PriorityLocations +Marks locations given here as `LocationProgressType.Priority` forcing progression items on them. + +### ItemLinks +Allows users to share their item pool with other players. Currently item links are per game. A link of one game between +two players will combine their items in the link into a single item, which then gets replaced with `World.create_filler()`. + +## Basic Option Classes +### Toggle +The example above. This simply has 0 and 1 as its available results with 0 (false) being the default value. Cannot be +compared to strings but can be directly compared to True and False. + +### DefaultOnToggle +Like Toggle, but 1 (true) is the default value. + +### Choice +A numeric option allowing you to define different sub options. Values are stored as integers, but you can also do +comparison methods with the class and strings, so if you have an `option_early_sword`, this can be compared with: +```python +if self.multiworld.sword_availability[self.player] == "early_sword": + do_early_sword_things() +``` + +or: +```python +from .Options import SwordAvailability + +if self.multiworld.sword_availability[self.player] == SwordAvailability.option_early_sword: + do_early_sword_things() +``` + +### Range +A numeric option allowing a variety of integers including the endpoints. Has a default `range_start` of 0 and default +`range_end` of 1. Allows for negative values as well. This will always be an integer and has no methods for string +comparisons. + +### SpecialRange +Like range but also allows you to define a dictionary of special names the user can use to equate to a specific value. +For example: +```python +special_range_names: { + "normal": 20, + "extreme": 99, +} +``` + +will let users use the names "normal" or "extreme" in their options selections, but will still return those as integers +to you. Useful if you want special handling regarding those specified values. + +## More Advanced Options +### FreeText +This is an option that allows the user to enter any possible string value. Can only be compared with strings, and has +no validation step, so if this needs to be validated, you can either add a validation step to the option class or +within the world. + +### TextChoice +Like choice allows you to predetermine options and has all of the same comparison methods and handling. Also accepts any +user defined string as a valid option, so will either need to be validated by adding a validation step to the option +class or within world, if necessary. Value for this class is `Union[str, int]` so if you need the value at a specified +point, `self.multiworld.my_option[self.player].current_key` will always return a string. + +### PlandoBosses +An option specifically built for handling boss rando, if your game can use it. Is a subclass of TextChoice so supports +everything it does, as well as having multiple validation steps to automatically support boss plando from users. If +using this class, you must define `bosses`, a set of valid boss names, and `locations`, a set of valid boss location +names, and `def can_place_boss`, which passes a boss and location, allowing you to check if that placement is valid for +your game. When this function is called, `bosses`, `locations`, and the passed strings will all be lowercase. There is +also a `duplicate_bosses` attribute allowing you to define if a boss can be placed multiple times in your world. False +by default, and will reject duplicate boss names from the user. For an example of using this class, refer to +`worlds.alttp.options.py` + +### OptionDict +This option returns a dictionary. Setting a default here is recommended as it will output the dictionary to the +template. If you set a [Schema](https://pypi.org/project/schema/) on the class with `schema = Schema()`, then the +options system will automatically validate the user supplied data against the schema to ensure it's in the correct +format. + +### ItemDict +Like OptionDict, except this will verify that every key in the dictionary is a valid name for an item for your world. + +### OptionList +This option defines a List, where the user can add any number of strings to said list, allowing duplicate values. You +can define a set of keys in `valid_keys`, and a default list if you want certain options to be available without editing +for this. If `valid_keys_casefold` is true, the verification will be case-insensitive; `verify_item_name` will check +that each value is a valid item name; and`verify_location_name` will check that each value is a valid location name. + +### OptionSet +Like OptionList, but returns a set, preventing duplicates. + +### ItemSet +Like OptionSet, but will verify that all the items in the set are a valid name for an item for your world. diff --git a/docs/running from source.md b/docs/running from source.md index 2bda62ec1a..cb1a8fa50b 100644 --- a/docs/running from source.md +++ b/docs/running from source.md @@ -7,10 +7,11 @@ use that version. These steps are for developers or platforms without compiled r ## General What you'll need: - * Python 3.8.7 or newer - * pip (Depending on platform may come included) - * A C compiler - * possibly optional, read OS-specific sections + * [Python 3.8.7 or newer](https://www.python.org/downloads/), not the Windows Store version + * **Python 3.11 does not work currently** + * pip: included in downloads from python.org, separate in many Linux distributions + * Matching C compiler + * possibly optional, read operating system specific sections Then run any of the starting point scripts, like Generate.py, and the included ModuleUpdater should prompt to install or update the required modules and after pressing enter proceed to install everything automatically. @@ -29,6 +30,8 @@ After this, you should be able to run the programs. Recommended steps * Download and install a "Windows installer (64-bit)" from the [Python download page](https://www.python.org/downloads) + * **Python 3.11 does not work currently** + * Download and install full Visual Studio from [Visual Studio Downloads](https://visualstudio.microsoft.com/downloads/) or an older "Build Tools for Visual Studio" from @@ -40,6 +43,8 @@ Recommended steps * It is recommended to use [PyCharm IDE](https://www.jetbrains.com/pycharm/) * Run Generate.py which will prompt installation of missing modules, press enter to confirm + * In PyCharm: right-click Generate.py and select `Run 'Generate'` + * Without PyCharm: open a command prompt in the source folder and type `py Generate.py` ## macOS @@ -59,7 +64,7 @@ setting in host.yaml at your Enemizer executable. ## Optional: SNI -SNI is required to use SNIClient. If not integrated into the project, it has to be started manually. +[SNI](https://github.com/alttpo/sni/blob/main/README.md) is required to use SNIClient. If not integrated into the project, it has to be started manually. You can get the latest SNI release at [SNI Github releases](https://github.com/alttpo/sni/releases). It should be dropped as "SNI" into the root folder of the project. Alternatively, you can point the sni setting in diff --git a/docs/webhost configuration sample.yaml b/docs/webhost configuration sample.yaml index f007805b9e..70050b0590 100644 --- a/docs/webhost configuration sample.yaml +++ b/docs/webhost configuration sample.yaml @@ -48,5 +48,5 @@ # TODO #JSON_AS_ASCII: false -# Patch target. This is the address encoded into the patch that will be used for client auto-connect. -#PATCH_TARGET: archipelago.gg \ No newline at end of file +# Host Address. This is the address encoded into the patch that will be used for client auto-connect. +#HOST_ADDRESS: archipelago.gg diff --git a/docs/world api.md b/docs/world api.md index 922674fd29..66a639f1b8 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -364,14 +364,9 @@ class MyGameLocation(Location): # or from Locations import MyGameLocation class MyGameWorld(World): """Insert description of the world/game here.""" - game: str = "My Game" # name of the game/world + game = "My Game" # name of the game/world option_definitions = mygame_options # options the player can set - topology_present: bool = True # show path to required location checks in spoiler - - # data_version is used to signal that items, locations or their names - # changed. Set this to 0 during development so other games' clients do not - # cache any texts, then increase by 1 for each release that makes changes. - data_version = 0 + topology_present = True # show path to required location checks in spoiler # ID of first item and location, could be hard-coded but code may be easier # to read with this as a propery. diff --git a/host.yaml b/host.yaml index 78fff669e1..fd5c759032 100644 --- a/host.yaml +++ b/host.yaml @@ -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" @@ -107,7 +111,7 @@ factorio_options: filter_item_sends: false # Whether to send chat messages from players on the Factorio server to Archipelago. bridge_chat_out: true -minecraft_options: +minecraft_options: forge_directory: "Minecraft Forge server" max_heap_size: "2G" # release channel, currently "release", or "beta" @@ -125,6 +129,15 @@ soe_options: rom_file: "Secret of Evermore (USA).sfc" ffr_options: display_msgs: true +tloz_options: + # File name of the Zelda 1 + rom_file: "Legend of Zelda, The (U) (PRG0) [!].nes" + # Set this to false to never autostart a rom (such as after patching) + # true for operating system default program + # Alternatively, a path to a program to open the .nes file with + rom_start: true + # Display message inside of Bizhawk + display_msgs: true dkc3_options: # File name of the DKC3 US rom rom_file: "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc" @@ -154,3 +167,25 @@ 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" + +adventure_options: + # File name of the standard NTSC Adventure rom. + # The licensed "The 80 Classic Games" CD-ROM contains this. + # It may also have a .a26 extension + rom_file: "ADVNTURE.BIN" + # Set this to false to never autostart a rom (such as after patching) + # True for operating system default program for '.a26' + # Alternatively, a path to a program to open the .a26 file with (generally EmuHawk for multiworld) + rom_start: true + # Optional, additional args passed into rom_start before the .bin file + # For example, this can be used to autoload the connector script in BizHawk + # (see BizHawk --lua= option) + # Windows example: + # rom_args: "--lua=C:/ProgramData/Archipelago/data/lua/ADVENTURE/adventure_connector.lua" + rom_args: " " + # Set this to true to display item received messages in Emuhawk + display_msgs: true + + + + diff --git a/inno_setup.iss b/inno_setup.iss index f7748ced02..866399d322 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -25,9 +25,9 @@ OutputBaseFilename=Setup {#MyAppName} {#MyAppVersionText} Compression=lzma2 SolidCompression=yes LZMANumBlockThreads=8 -ArchitecturesInstallIn64BitMode=x64 +ArchitecturesInstallIn64BitMode=x64 arm64 ChangesAssociations=yes -ArchitecturesAllowed=x64 +ArchitecturesAllowed=x64 arm64 AllowNoIcons=yes SetupIconFile={#MyAppIcon} UninstallDisplayIcon={app}\{#MyAppExeName} @@ -63,6 +63,8 @@ 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: "generator/tloz"; Description: "The Legend of Zelda ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 135168; Flags: disablenouninstallwarning Name: "server"; Description: "Server"; Types: full hosting Name: "client"; Description: "Clients"; Types: full playing Name: "client/sni"; Description: "SNI Client"; Types: full playing @@ -72,15 +74,20 @@ Name: "client/sni/dkc3"; Description: "SNI Client - Donkey Kong Country 3 Patch Name: "client/sni/smw"; Description: "SNI Client - Super Mario World Patch Setup"; Types: full playing; Flags: disablenouninstallwarning Name: "client/sni/l2ac"; Description: "SNI Client - Lufia II Ancient Cave Patch Setup"; Types: full playing; Flags: disablenouninstallwarning Name: "client/factorio"; Description: "Factorio"; Types: full playing +Name: "client/kh2"; Description: "Kingdom Hearts 2"; Types: full playing Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278 Name: "client/oot"; Description: "Ocarina of Time"; Types: full playing 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/wargroove"; Description: "Wargroove"; Types: full playing Name: "client/zl"; Description: "Zillion"; Types: full playing +Name: "client/tloz"; Description: "The Legend of Zelda"; Types: full playing +Name: "client/advn"; Description: "Adventure"; Types: full playing Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing [Dirs] @@ -97,15 +104,20 @@ 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: "{code:GetTLoZROMPath}"; DestDir: "{app}"; DestName: "Legend of Zelda, The (U) (PRG0) [!].nes"; Flags: external; Components: client/tloz or generator/tloz +Source: "{code:GetAdvnROMPath}"; DestDir: "{app}"; DestName: "ADVNTURE.BIN"; Flags: external; Components: client/advn 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 +Source: "{#source_path}\ArchipelagoLauncher.exe"; DestDir: "{app}"; Flags: ignoreversion; Source: "{#source_path}\ArchipelagoGenerate.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: generator Source: "{#source_path}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: server 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/ladx 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 @@ -115,6 +127,10 @@ Source: "{#source_path}\ArchipelagoFF1Client.exe"; DestDir: "{app}"; Flags: igno Source: "{#source_path}\ArchipelagoPokemonClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/pkmn Source: "{#source_path}\ArchipelagoChecksFinderClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/cf Source: "{#source_path}\ArchipelagoStarcraft2Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sc2 +Source: "{#source_path}\ArchipelagoZelda1Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/tloz +Source: "{#source_path}\ArchipelagoWargrooveClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/wargroove +Source: "{#source_path}\ArchipelagoKH2Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/kh2 +Source: "{#source_path}\ArchipelagoAdventureClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/advn Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall [Icons] @@ -130,6 +146,10 @@ Name: "{group}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\Archipelag Name: "{group}\{#MyAppName} Pokemon Client"; Filename: "{app}\ArchipelagoPokemonClient.exe"; Components: client/pkmn Name: "{group}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Components: client/cf Name: "{group}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoStarcraft2Client.exe"; Components: client/sc2 +Name: "{group}\{#MyAppName} The Legend of Zelda Client"; Filename: "{app}\ArchipelagoZelda1Client.exe"; Components: client/tloz +Name: "{group}\{#MyAppName} Kingdom Hearts 2 Client"; Filename: "{app}\ArchipelagoKH2Client.exe"; Components: client/kh2 +Name: "{group}\{#MyAppName} Adventure Client"; Filename: "{app}\ArchipelagoAdventureClient.exe"; Components: client/advn +Name: "{group}\{#MyAppName} Wargroove Client"; Filename: "{app}\ArchipelagoWargrooveClient.exe"; Components: client/wargroove Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon; Components: server @@ -142,6 +162,10 @@ Name: "{commondesktop}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\Ar Name: "{commondesktop}\{#MyAppName} Pokemon Client"; Filename: "{app}\ArchipelagoPokemonClient.exe"; Tasks: desktopicon; Components: client/pkmn Name: "{commondesktop}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Tasks: desktopicon; Components: client/cf Name: "{commondesktop}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoStarcraft2Client.exe"; Tasks: desktopicon; Components: client/sc2 +Name: "{commondesktop}\{#MyAppName} The Legend of Zelda Client"; Filename: "{app}\ArchipelagoZelda1Client.exe"; Tasks: desktopicon; Components: client/tloz +Name: "{commondesktop}\{#MyAppName} Wargroove Client"; Filename: "{app}\ArchipelagoWargrooveClient.exe"; Tasks: desktopicon; Components: client/wargroove +Name: "{commondesktop}\{#MyAppName} Kingdom Hearts 2 Client"; Filename: "{app}\ArchipelagoKH2Client.exe"; Tasks: desktopicon; Components: client/kh2 +Name: "{commondesktop}\{#MyAppName} Adventure Client"; Filename: "{app}\ArchipelagoAdventureClient.exe"; Tasks: desktopicon; Components: client/advn [Run] @@ -219,6 +243,21 @@ 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: ".aptloz"; ValueData: "{#MyAppName}tlozpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/tloz +Root: HKCR; Subkey: "{#MyAppName}tlozpatch"; ValueData: "Archipelago The Legend of Zelda Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/tloz +Root: HKCR; Subkey: "{#MyAppName}tlozpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoZelda1Client.exe,0"; ValueType: string; ValueName: ""; Components: client/tloz +Root: HKCR; Subkey: "{#MyAppName}tlozpatch\shell\open\command"; ValueData: """{app}\ArchipelagoZelda1Client.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/tloz + +Root: HKCR; Subkey: ".apadvn"; ValueData: "{#MyAppName}advnpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/advn +Root: HKCR; Subkey: "{#MyAppName}advnpatch"; ValueData: "Archipelago Adventure Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/advn +Root: HKCR; Subkey: "{#MyAppName}advnpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoAdventureClient.exe,0"; ValueType: string; ValueName: ""; Components: client/advn +Root: HKCR; Subkey: "{#MyAppName}advnpatch\shell\open\command"; ValueData: """{app}\ArchipelagoAdventureClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/advn + 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 +325,15 @@ var RedROMFilePage: TInputFileWizardPage; var bluerom: string; var BlueROMFilePage: TInputFileWizardPage; +var ladxrom: string; +var LADXROMFilePage: TInputFileWizardPage; + +var tlozrom: string; +var TLoZROMFilePage: TInputFileWizardPage; + +var advnrom: string; +var AdvnROMFilePage: TInputFileWizardPage; + function GetSNESMD5OfFile(const rom: string): string; var data: AnsiString; begin @@ -346,6 +394,25 @@ begin end; end; +function CheckNESRom(name: string; hash: string): string; +var rom: string; +begin + log('Handling ' + name) + rom := FileSearch(name, WizardDirValue()); + if Length(rom) > 0 then + begin + log('existing ROM found'); + log(IntToStr(CompareStr(GetSMSMD5OfFile(rom), hash))); + if CompareStr(GetSMSMD5OfFile(rom), hash) = 0 then + begin + log('existing ROM verified'); + Result := rom; + exit; + end; + log('existing ROM failed verification'); + end; +end; + function AddRomPage(name: string): TInputFileWizardPage; begin Result := @@ -392,6 +459,21 @@ begin '.sms'); end; +function AddNESRomPage(name: string): TInputFileWizardPage; +begin + Result := + CreateInputFilePage( + wpSelectComponents, + 'Select ROM File', + 'Where is your ' + name + ' located?', + 'Select the file, then click Next.'); + + Result.Add( + 'Location of ROM file:', + 'NES ROM files|*.nes|All files|*.*', + '.nes'); +end; + procedure AddOoTRomPage(); begin ootrom := FileSearch('The Legend of Zelda - Ocarina of Time.z64', WizardDirValue()); @@ -422,6 +504,21 @@ begin '.z64'); end; +function AddA26Page(name: string): TInputFileWizardPage; +begin + Result := + CreateInputFilePage( + wpSelectComponents, + 'Select ROM File', + 'Where is your ' + name + ' located?', + 'Select the file, then click Next.'); + + Result.Add( + 'Location of ROM file:', + 'A2600 ROM files|*.BIN;*.a26|All files|*.*', + '.BIN'); +end; + function NextButtonClick(CurPageID: Integer): Boolean; begin if (assigned(LttPROMFilePage)) and (CurPageID = LttPROMFilePage.ID) then @@ -440,6 +537,16 @@ 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 if (assigned(TLoZROMFilePage)) and (CurPageID = TLoZROMFilePage.ID) then + Result := not (TLoZROMFilePage.Values[0] = '') + else if (assigned(AdvnROMFilePage)) and (CurPageID = AdvnROMFilePage.ID) then + Result := not (AdvnROMFilePage.Values[0] = '') else Result := True; end; @@ -576,7 +683,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 +699,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 @@ -603,6 +710,54 @@ begin else Result := ''; end; + +function GetTLoZROMPath(Param: string): string; +begin + if Length(tlozrom) > 0 then + Result := tlozrom + else if Assigned(TLoZROMFilePage) then + begin + R := CompareStr(GetMD5OfFile(TLoZROMFilePage.Values[0]), '337bd6f1a1163df31bf2633665589ab0'); + if R <> 0 then + MsgBox('The Legend of Zelda ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); + + Result := TLoZROMFilePage.Values[0] + end + else + 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; + +function GetAdvnROMPath(Param: string): string; +begin + if Length(advnrom) > 0 then + Result := advnrom + else if Assigned(AdvnROMFilePage) then + begin + R := CompareStr(GetMD5OfFile(AdvnROMFilePage.Values[0]), '157bddb7192754a45372be196797f284'); + if R <> 0 then + MsgBox('Adventure ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); + + Result := AdvnROMFilePage.Values[0] + end + else + Result := ''; +end; procedure InitializeWizard(); begin @@ -640,9 +795,21 @@ 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'); + + tlozrom := CheckNESROM('Legend of Zelda, The (U) (PRG0) [!].nes', '337bd6f1a1163df31bf2633665589ab0'); + if Length(tlozrom) = 0 then + TLoZROMFilePage:= AddNESRomPage('Legend of Zelda, The (U) (PRG0) [!].nes'); + + advnrom := CheckSMSRom('ADVNTURE.BIN', '157bddb7192754a45372be196797f284'); + if Length(advnrom) = 0 then + AdvnROMFilePage:= AddA26Page('ADVNTURE.BIN'); end; @@ -669,4 +836,10 @@ 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')); + if (assigned(TLoZROMFilePage)) and (PageID = TLoZROMFilePage.ID) then + Result := not (WizardIsComponentSelected('generator/tloz') or WizardIsComponentSelected('client/tloz')); + if (assigned(AdvnROMFilePage)) and (PageID = AdvnROMFilePage.ID) then + Result := not (WizardIsComponentSelected('client/advn')); end; diff --git a/kvui.py b/kvui.py index a1c6959d6b..66da8c16a3 100644 --- a/kvui.py +++ b/kvui.py @@ -148,9 +148,11 @@ class ServerLabel(HovererableLabel): for permission_name, permission_data in ctx.permissions.items(): text += f"\n {permission_name}: {permission_data}" if ctx.hint_cost is not None and ctx.total_locations: + min_cost = int(ctx.server_version >= (0, 3, 9)) text += f"\nA new !hint costs {ctx.hint_cost}% of checks made. " \ - f"For you this means every {max(0, int(ctx.hint_cost * 0.01 * ctx.total_locations))} " \ - "location checks." + f"For you this means every " \ + f"{max(min_cost, int(ctx.hint_cost * 0.01 * ctx.total_locations))}" \ + f" location checks." elif ctx.hint_cost == 0: text += "\n!hint is free to use." @@ -486,6 +488,10 @@ class GameManager(App): if hasattr(self, "energy_link_label"): self.energy_link_label.text = f"EL: {Utils.format_SI_prefix(self.ctx.current_energy_link_value)}J" + # default F1 keybind, opens a settings menu, that seems to break the layout engine once closed + def open_settings(self, *largs): + pass + class LogtoUI(logging.Handler): def __init__(self, on_log): @@ -606,7 +612,7 @@ class KivyJSONtoTextParser(JSONtoTextParser): ExceptionManager.add_handler(E()) Builder.load_file(Utils.local_path("data", "client.kv")) -user_file = Utils.local_path("data", "user.kv") +user_file = Utils.user_path("data", "user.kv") if os.path.exists(user_file): logging.info("Loading user.kv into builder.") Builder.load_file(user_file) diff --git a/requirements.txt b/requirements.txt index 6c9e3b9d2d..5b50664475 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,9 @@ colorama>=0.4.5 websockets>=10.3 PyYAML>=6.0 -jellyfish>=0.9.0 +jellyfish>=0.11.0 jinja2>=3.1.2 schema>=0.7.5 kivy>=2.1.0 -bsdiff4>=1.2.2 \ No newline at end of file +bsdiff4>=1.2.3 +platformdirs>=3.2.0 diff --git a/setup.py b/setup.py index 509981da37..e455c04bad 100644 --- a/setup.py +++ b/setup.py @@ -7,22 +7,43 @@ import sys import sysconfig import typing import zipfile +import urllib.request +import io +import json +import threading +import subprocess + from collections.abc import Iterable from hashlib import sha3_512 from pathlib import Path -import subprocess -import pkg_resources + # This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it try: - requirement = 'cx-Freeze>=6.14.1' - pkg_resources.require(requirement) - import cx_Freeze -except pkg_resources.ResolutionError: + requirement = 'cx-Freeze>=6.14.7' + import pkg_resources + try: + pkg_resources.require(requirement) + install_cx_freeze = False + except pkg_resources.ResolutionError: + install_cx_freeze = True +except ImportError: + install_cx_freeze = True + pkg_resources = None # type: ignore [assignment] + +if install_cx_freeze: + # check if pip is available + try: + import pip # noqa: F401 + except ImportError: + raise RuntimeError("pip not available. Please install pip.") + # install and import cx_freeze if '--yes' not in sys.argv and '-y' not in sys.argv: input(f'Requirement {requirement} is not satisfied, press enter to install it') subprocess.call([sys.executable, '-m', 'pip', 'install', requirement, '--upgrade']) - import cx_Freeze + import pkg_resources + +import cx_Freeze # .build only exists if cx-Freeze is the right version, so we have to update/install that first before this line import setuptools.command.build @@ -34,7 +55,7 @@ if __name__ == "__main__": ModuleUpdate.update(yes="--yes" in sys.argv or "-y" in sys.argv) ModuleUpdate.update_ran = False # restore for later -from Launcher import components, icon_paths +from worlds.LauncherComponents import components, icon_paths from Utils import version_tuple, is_windows, is_linux @@ -43,12 +64,78 @@ apworlds: set = { "Subnautica", "Factorio", "Rogue Legacy", + "Sonic Adventure 2 Battle", "Donkey Kong Country 3", "Super Mario World", "Stardew Valley", "Timespinner", + "Minecraft", + "The Messenger", } + +def download_SNI(): + print("Updating SNI") + machine_to_go = { + "x86_64": "amd64", + "aarch64": "arm64", + "armv7l": "arm" + } + platform_name = platform.system().lower() + machine_name = platform.machine().lower() + # force amd64 on macos until we have universal2 sni, otherwise resolve to GOARCH + machine_name = "amd64" if platform_name == "darwin" else machine_to_go.get(machine_name, machine_name) + with urllib.request.urlopen("https://api.github.com/repos/alttpo/sni/releases/latest") as request: + data = json.load(request) + files = data["assets"] + + source_url = None + + for file in files: + download_url: str = file["browser_download_url"] + machine_match = download_url.rsplit("-", 1)[1].split(".", 1)[0] == machine_name + if platform_name in download_url and machine_match: + # prefer "many" builds + if "many" in download_url: + source_url = download_url + break + source_url = download_url + + if source_url and source_url.endswith(".zip"): + with urllib.request.urlopen(source_url) as download: + with zipfile.ZipFile(io.BytesIO(download.read()), "r") as zf: + for member in zf.infolist(): + zf.extract(member, path="SNI") + print(f"Downloaded SNI from {source_url}") + + elif source_url and (source_url.endswith(".tar.xz") or source_url.endswith(".tar.gz")): + import tarfile + mode = "r:xz" if source_url.endswith(".tar.xz") else "r:gz" + with urllib.request.urlopen(source_url) as download: + sni_dir = None + with tarfile.open(fileobj=io.BytesIO(download.read()), mode=mode) as tf: + for member in tf.getmembers(): + if member.name.startswith("/") or "../" in member.name: + raise ValueError(f"Unexpected file '{member.name}' in {source_url}") + elif member.isdir() and not sni_dir: + sni_dir = member.name + elif member.isfile() and not sni_dir or not member.name.startswith(sni_dir): + raise ValueError(f"Expected folder before '{member.name}' in {source_url}") + elif member.isfile() and sni_dir: + tf.extract(member) + # sadly SNI is in its own folder on non-windows, so we need to rename + shutil.rmtree("SNI", True) + os.rename(sni_dir, "SNI") + print(f"Downloaded SNI from {source_url}") + + elif source_url: + print(f"Don't know how to extract SNI from {source_url}") + + else: + print(f"No SNI found for system spec {platform_name} {machine_name}") + + +signtool: typing.Optional[str] if os.path.exists("X:/pw.txt"): print("Using signtool") with open("X:/pw.txt", encoding="utf-8-sig") as f: @@ -73,7 +160,7 @@ exes = [ target_name=c.frozen_name + (".exe" if is_windows else ""), icon=icon_paths[c.icon], base="Win32GUI" if is_windows and not c.cli else None - ) for c in components if c.script_name + ) for c in components if c.script_name and c.frozen_name ] extra_data = ["LICENSE", "data", "EnemizerCLI", "host.yaml", "SNI"] @@ -174,6 +261,10 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE): print("Created Manifest") def run(self): + # start downloading sni asap + sni_thread = threading.Thread(target=download_SNI, name="SNI Downloader") + sni_thread.start() + # pre build steps print(f"Outputting to: {self.buildfolder}") os.makedirs(self.buildfolder, exist_ok=True) @@ -185,6 +276,9 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE): self.buildtime = datetime.datetime.utcnow() super().run() + # need to finish download before copying + sni_thread.join() + # include_files seems to not be done automatically. implement here for src, dst in self.include_files: print(f"copying {src} -> {self.buildfolder / dst}") @@ -229,7 +323,6 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE): # which should be ok with zipfile.ZipFile(self.libfolder / "worlds" / (file_name + ".apworld"), "x", zipfile.ZIP_DEFLATED, compresslevel=9) as zf: - entry: os.DirEntry for path in world_directory.rglob("*.*"): relative_path = os.path.join(*path.parts[path.parts.index("worlds")+1:]) zf.write(path, relative_path) @@ -252,9 +345,9 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE): for exe in self.distribution.executables: print(f"Signing {exe.target_name}") os.system(signtool + os.path.join(self.buildfolder, exe.target_name)) - print(f"Signing SNI") + print("Signing SNI") os.system(signtool + os.path.join(self.buildfolder, "SNI", "SNI.exe")) - print(f"Signing OoT Utils") + print("Signing OoT Utils") for exe_path in (("Compress", "Compress.exe"), ("Decompress", "Decompress.exe")): os.system(signtool + os.path.join(self.buildfolder, "lib", "worlds", "oot", "data", *exe_path)) @@ -308,7 +401,8 @@ class AppImageCommand(setuptools.Command): yes: bool def write_desktop(self): - desktop_filename = self.app_dir / f'{self.app_id}.desktop' + assert self.app_dir, "Invalid app_dir" + desktop_filename = self.app_dir / f"{self.app_id}.desktop" with open(desktop_filename, 'w', encoding="utf-8") as f: f.write("\n".join(( "[Desktop Entry]", @@ -322,7 +416,8 @@ class AppImageCommand(setuptools.Command): desktop_filename.chmod(0o755) def write_launcher(self, default_exe: Path): - launcher_filename = self.app_dir / f'AppRun' + assert self.app_dir, "Invalid app_dir" + launcher_filename = self.app_dir / "AppRun" with open(launcher_filename, 'w', encoding="utf-8") as f: f.write(f"""#!/bin/sh exe="{default_exe}" @@ -344,11 +439,12 @@ $APPDIR/$exe "$@" launcher_filename.chmod(0o755) def install_icon(self, src: Path, name: typing.Optional[str] = None, symlink: typing.Optional[Path] = None): + assert self.app_dir, "Invalid app_dir" try: from PIL import Image except ModuleNotFoundError: if not self.yes: - input(f'Requirement PIL is not satisfied, press enter to install it') + input("Requirement PIL is not satisfied, press enter to install it") subprocess.call([sys.executable, '-m', 'pip', 'install', 'Pillow', '--upgrade']) from PIL import Image im = Image.open(src) @@ -425,8 +521,12 @@ def find_libs(*args: str) -> typing.Sequence[typing.Tuple[str, str]]: return (lib, lib_arch, lib_libc), path if not hasattr(find_libs, "cache"): - data = subprocess.run([shutil.which('ldconfig'), '-p'], capture_output=True, text=True).stdout.split('\n')[1:] - find_libs.cache = {k: v for k, v in (parse(line) for line in data if '=>' in line)} + ldconfig = shutil.which("ldconfig") + assert ldconfig, "Make sure ldconfig is in PATH" + data = subprocess.run([ldconfig, "-p"], capture_output=True, text=True).stdout.split("\n")[1:] + find_libs.cache = { # type: ignore [attr-defined] + k: v for k, v in (parse(line) for line in data if "=>" in line) + } def find_lib(lib, arch, libc): for k, v in find_libs.cache.items(): diff --git a/test/TestBase.py b/test/TestBase.py index fb8031a900..17fe6425df 100644 --- a/test/TestBase.py +++ b/test/TestBase.py @@ -112,7 +112,11 @@ class WorldTestBase(unittest.TestCase): self.world_setup() def world_setup(self, seed: typing.Optional[int] = None) -> None: - if type(self) is WorldTestBase: + if type(self) is WorldTestBase or \ + (hasattr(WorldTestBase, self._testMethodName) + and not self.run_default_tests and + getattr(self, self._testMethodName).__code__ is + getattr(WorldTestBase, self._testMethodName, None).__code__): return # setUp gets called for tests defined in the base class. We skip world_setup here. if not hasattr(self, "game"): raise NotImplementedError("didn't define game name") @@ -195,11 +199,15 @@ class WorldTestBase(unittest.TestCase): self.collect_all_but(all_items) for location in self.multiworld.get_locations(): - self.assertEqual(self.multiworld.state.can_reach(location), location.name not in locations) + loc_reachable = self.multiworld.state.can_reach(location) + self.assertEqual(loc_reachable, location.name not in locations, + f"{location.name} is reachable without {all_items}" if loc_reachable + else f"{location.name} is not reachable without {all_items}") for item_names in possible_items: items = self.collect_by_name(item_names) for location in locations: - self.assertTrue(self.can_reach_location(location)) + self.assertTrue(self.can_reach_location(location), + f"{location} not reachable with {item_names}") self.remove(items) def assertBeatable(self, beatable: bool): @@ -208,16 +216,20 @@ class WorldTestBase(unittest.TestCase): # following tests are automatically run @property - def skip_default_tests(self) -> bool: + def run_default_tests(self) -> bool: """Not possible or identical to the base test that's always being run already""" - constructed = hasattr(self, "game") and hasattr(self, "multiworld") - return not constructed or (not self.options - and self.setUp is WorldTestBase.setUp - and self.world_setup is WorldTestBase.world_setup) + return (self.options + or self.setUp.__code__ is not WorldTestBase.setUp.__code__ + or self.world_setup.__code__ is not WorldTestBase.world_setup.__code__) + + @property + def constructed(self) -> bool: + """A multiworld has been constructed by this point""" + return hasattr(self, "game") and hasattr(self, "multiworld") def testAllStateCanReachEverything(self): - """Ensure all state can reach everything with the defined options""" - if self.skip_default_tests: + """Ensure all state can reach everything and complete the game with the defined options""" + if not (self.run_default_tests and self.constructed): return with self.subTest("Game", game=self.game): excluded = self.multiworld.exclude_locations[1].value @@ -226,10 +238,13 @@ class WorldTestBase(unittest.TestCase): if location.name not in excluded: with self.subTest("Location should be reached", location=location): self.assertTrue(location.can_reach(state), f"{location.name} unreachable") + with self.subTest("Beatable"): + self.multiworld.state = state + self.assertBeatable(True) def testEmptyStateCanReachSomething(self): """Ensure empty state can reach at least one location with the defined options""" - if self.skip_default_tests: + if not (self.run_default_tests and self.constructed): return with self.subTest("Game", game=self.game): state = CollectionState(self.multiworld) diff --git a/test/general/TestLocations.py b/test/general/TestLocations.py index 5dbb1d55fc..8e4e3ab1be 100644 --- a/test/general/TestLocations.py +++ b/test/general/TestLocations.py @@ -1,18 +1,24 @@ import unittest from collections import Counter -from worlds.AutoWorld import AutoWorldRegister +from worlds.AutoWorld import AutoWorldRegister, call_all from . import setup_solo_multiworld class TestBase(unittest.TestCase): def testCreateDuplicateLocations(self): - """Tests that no two Locations share a name.""" + """Tests that no two Locations share a name or ID.""" for game_name, world_type in AutoWorldRegister.world_types.items(): multiworld = setup_solo_multiworld(world_type) - locations = Counter(multiworld.get_locations()) + locations = Counter(location.name for location in multiworld.get_locations()) if locations: self.assertLessEqual(locations.most_common(1)[0][1], 1, - f"{world_type.game} has duplicate of location {locations.most_common(1)}") + f"{world_type.game} has duplicate of location name {locations.most_common(1)}") + + locations = Counter(location.address for location in multiworld.get_locations() + if type(location.address) is int) + if locations: + self.assertLessEqual(locations.most_common(1)[0][1], 1, + f"{world_type.game} has duplicate of location ID {locations.most_common(1)}") def testLocationsInDatapackage(self): """Tests that created locations not filled before fill starts exist in the datapackage.""" @@ -23,3 +29,33 @@ class TestBase(unittest.TestCase): for location in locations: self.assertIn(location.name, world_type.location_name_to_id) self.assertEqual(location.address, world_type.location_name_to_id[location.name]) + + def testLocationCreationSteps(self): + """Tests that Regions and Locations aren't created after `create_items`.""" + gen_steps = ("generate_early", "create_regions", "create_items") + for game_name, world_type in AutoWorldRegister.world_types.items(): + with self.subTest("Game", game_name=game_name): + multiworld = setup_solo_multiworld(world_type, gen_steps) + multiworld._recache() + region_count = len(multiworld.get_regions()) + location_count = len(multiworld.get_locations()) + + call_all(multiworld, "set_rules") + self.assertEqual(region_count, len(multiworld.get_regions()), + f"{game_name} modified region count during rule creation") + self.assertEqual(location_count, len(multiworld.get_locations()), + f"{game_name} modified locations count during rule creation") + + multiworld._recache() + call_all(multiworld, "generate_basic") + self.assertEqual(region_count, len(multiworld.get_regions()), + f"{game_name} modified region count during generate_basic") + self.assertGreaterEqual(location_count, len(multiworld.get_locations()), + f"{game_name} modified locations count during generate_basic") + + multiworld._recache() + call_all(multiworld, "pre_fill") + self.assertEqual(region_count, len(multiworld.get_regions()), + f"{game_name} modified region count during pre_fill") + self.assertGreaterEqual(location_count, len(multiworld.get_locations()), + f"{game_name} modified locations count during pre_fill") diff --git a/test/general/__init__.py b/test/general/__init__.py index 970c4ef936..b0fb7ca32e 100644 --- a/test/general/__init__.py +++ b/test/general/__init__.py @@ -1,12 +1,13 @@ from argparse import Namespace +from typing import Type, Tuple from BaseClasses import MultiWorld -from worlds.AutoWorld import call_all +from worlds.AutoWorld import call_all, World -gen_steps = ["generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill"] +gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill") -def setup_solo_multiworld(world_type) -> MultiWorld: +def setup_solo_multiworld(world_type: Type[World], steps: Tuple[str, ...] = gen_steps) -> MultiWorld: multiworld = MultiWorld(1) multiworld.game[1] = world_type.game multiworld.player_name = {1: "Tester"} @@ -16,6 +17,6 @@ def setup_solo_multiworld(world_type) -> MultiWorld: setattr(args, name, {1: option.from_any(option.default)}) multiworld.set_options(args) multiworld.set_default_common_options() - for step in gen_steps: + for step in steps: call_all(multiworld, step) return multiworld diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index f2a639eebf..d8f1bfd474 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -1,20 +1,24 @@ from __future__ import annotations +import hashlib import logging -import sys import pathlib -from typing import Dict, FrozenSet, Set, Tuple, List, Optional, TextIO, Any, Callable, Type, Union, TYPE_CHECKING, \ - ClassVar +import sys +from typing import Any, Callable, ClassVar, Dict, FrozenSet, List, Optional, Set, TYPE_CHECKING, TextIO, Tuple, Type, \ + Union -from Options import AssembleOptions from BaseClasses import CollectionState +from Options import AssembleOptions if TYPE_CHECKING: from BaseClasses import MultiWorld, Item, Location, Tutorial + from . import GamesPackage class AutoWorldRegister(type): world_types: Dict[str, Type[World]] = {} + __file__: str + zip_path: Optional[str] def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoWorldRegister: if "web" in dct: @@ -32,6 +36,9 @@ class AutoWorldRegister(type): in dct.get("item_name_groups", {}).items()} dct["item_name_groups"]["Everything"] = dct["item_names"] dct["location_names"] = frozenset(dct["location_name_to_id"]) + dct["location_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set + in dct.get("location_name_groups", {}).items()} + dct["location_name_groups"]["Everywhere"] = dct["location_names"] dct["all_item_and_group_names"] = frozenset(dct["item_names"] | set(dct.get("item_name_groups", {}))) # move away from get_required_client_version function @@ -149,11 +156,19 @@ class World(metaclass=AutoWorldRegister): item_name_groups: ClassVar[Dict[str, Set[str]]] = {} """maps item group names to sets of items. Example: {"Weapons": {"Sword", "Bow"}}""" - data_version: ClassVar[int] = 1 + location_name_groups: ClassVar[Dict[str, Set[str]]] = {} + """maps location group names to sets of locations. Example: {"Sewer": {"Sewer Key Drop 1", "Sewer Key Drop 2"}}""" + + data_version: ClassVar[int] = 0 """ - increment this every time something in your world's names/id mappings changes. - While this is set to 0, this world's DataPackage is considered in testing mode and will be inserted to the multidata - and retrieved by clients on every connection. + Increment this every time something in your world's names/id mappings changes. + + When this is set to 0, that world's DataPackage is considered in "testing mode", which signals to servers/clients + that it should not be cached, and clients should request that world's DataPackage every connection. Not + recommended for production-ready worlds. + + Deprecated. Clients should utilize `checksum` to determine if DataPackage has changed since last connection and + request a new DataPackage, if necessary. """ required_client_version: Tuple[int, int, int] = (0, 1, 6) @@ -340,8 +355,35 @@ class World(metaclass=AutoWorldRegister): def create_filler(self) -> "Item": return self.create_item(self.get_filler_item_name()) + @classmethod + def get_data_package_data(cls) -> "GamesPackage": + sorted_item_name_groups = { + name: sorted(cls.item_name_groups[name]) for name in sorted(cls.item_name_groups) + } + sorted_location_name_groups = { + name: sorted(cls.location_name_groups[name]) for name in sorted(cls.location_name_groups) + } + res: "GamesPackage" = { + # sorted alphabetically + "item_name_groups": sorted_item_name_groups, + "item_name_to_id": cls.item_name_to_id, + "location_name_groups": sorted_location_name_groups, + "location_name_to_id": cls.location_name_to_id, + "version": cls.data_version, + } + res["checksum"] = data_package_checksum(res) + return res + # any methods attached to this can be used as part of CollectionState, # please use a prefix as all of them get clobbered together class LogicMixin(metaclass=AutoLogicRegister): pass + + +def data_package_checksum(data: "GamesPackage") -> str: + """Calculates the data package checksum for a game from a dict""" + assert "checksum" not in data, "Checksum already in data" + assert sorted(data) == list(data), "Data not ordered" + from NetUtils import encode + return hashlib.sha1(encode(data).encode()).hexdigest() diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py new file mode 100644 index 0000000000..7bf3ea291e --- /dev/null +++ b/worlds/LauncherComponents.py @@ -0,0 +1,106 @@ +from enum import Enum, auto +from typing import Optional, Callable, List, Iterable + +from Utils import local_path, is_windows + + +class Type(Enum): + TOOL = auto() + FUNC = auto() # not a real component + CLIENT = auto() + ADJUSTER = auto() + + +class Component: + display_name: str + type: Optional[Type] + script_name: Optional[str] + frozen_name: Optional[str] + icon: str # just the name, no suffix + cli: bool + func: Optional[Callable] + file_identifier: Optional[Callable[[str], bool]] + + def __init__(self, display_name: str, script_name: Optional[str] = None, frozen_name: Optional[str] = None, + cli: bool = False, icon: str = 'icon', component_type: Type = None, func: Optional[Callable] = None, + file_identifier: Optional[Callable[[str], bool]] = None): + self.display_name = display_name + self.script_name = script_name + self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None + self.icon = icon + self.cli = cli + self.type = component_type or \ + None if not display_name else \ + Type.FUNC if func else \ + Type.CLIENT if 'Client' in display_name else \ + Type.ADJUSTER if 'Adjuster' in display_name else Type.TOOL + self.func = func + self.file_identifier = file_identifier + + def handles_file(self, path: str): + return self.file_identifier(path) if self.file_identifier else False + + def __repr__(self): + return f"{self.__class__.__name__}({self.display_name})" + + +class SuffixIdentifier: + suffixes: Iterable[str] + + def __init__(self, *args: str): + self.suffixes = args + + def __call__(self, path: str): + if isinstance(path, str): + for suffix in self.suffixes: + if path.endswith(suffix): + return True + return False + + +components: List[Component] = [ + # Launcher + Component('', 'Launcher'), + # Core + Component('Host', 'MultiServer', 'ArchipelagoServer', cli=True, + file_identifier=SuffixIdentifier('.archipelago', '.zip')), + Component('Generate', 'Generate', cli=True), + Component('Text Client', 'CommonClient', 'ArchipelagoTextClient'), + # SNI + 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'), + # Minecraft + Component('Minecraft Client', 'MinecraftClient', icon='mcicon', cli=True, + file_identifier=SuffixIdentifier('.apmc')), + # Ocarina of Time + Component('OoT Client', 'OoTClient', + file_identifier=SuffixIdentifier('.apz5')), + Component('OoT Adjuster', 'OoTAdjuster'), + # FF1 + Component('FF1 Client', 'FF1Client'), + # PokÊmon + Component('Pokemon Client', 'PokemonClient', file_identifier=SuffixIdentifier('.apred', '.apblue')), + # TLoZ + Component('Zelda 1 Client', 'Zelda1Client'), + # ChecksFinder + Component('ChecksFinder Client', 'ChecksFinderClient'), + # Starcraft 2 + Component('Starcraft 2 Client', 'Starcraft2Client'), + # Wargroove + Component('Wargroove Client', 'WargrooveClient'), + # Zillion + Component('Zillion Client', 'ZillionClient', + file_identifier=SuffixIdentifier('.apzl')), + #Kingdom Hearts 2 + Component('KH2 Client', "KH2Client"), +] + + +icon_paths = { + 'icon': local_path('data', 'icon.ico' if is_windows else 'icon.png'), + 'mcicon': local_path('data', 'mcicon.ico') +} diff --git a/worlds/__init__.py b/worlds/__init__.py index 34dece0e40..e2ebb78610 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -20,14 +20,19 @@ if typing.TYPE_CHECKING: from .AutoWorld import World -class GamesPackage(typing.TypedDict): +class GamesData(typing.TypedDict): + item_name_groups: typing.Dict[str, typing.List[str]] item_name_to_id: typing.Dict[str, int] + location_name_groups: typing.Dict[str, typing.List[str]] location_name_to_id: typing.Dict[str, int] version: int +class GamesPackage(GamesData, total=False): + checksum: str + + class DataPackage(typing.TypedDict): - version: int games: typing.Dict[str, GamesPackage] @@ -35,6 +40,9 @@ class WorldSource(typing.NamedTuple): path: str # typically relative path from this module is_zip: bool = False + def __repr__(self): + return f"{self.__class__.__name__}({self.path}, is_zip={self.is_zip})" + # find potential world containers, currently folders and zip-importable .apworld's world_sources: typing.List[WorldSource] = [] @@ -50,24 +58,35 @@ for file in os.scandir(folder): # import all submodules to trigger AutoWorldRegister world_sources.sort() for world_source in world_sources: - if world_source.is_zip: - importer = zipimport.zipimporter(os.path.join(folder, world_source.path)) - if hasattr(importer, "find_spec"): # new in Python 3.10 - spec = importer.find_spec(world_source.path.split(".", 1)[0]) - mod = importlib.util.module_from_spec(spec) - else: # TODO: remove with 3.8 support - mod = importer.load_module(world_source.path.split(".", 1)[0]) + try: + if world_source.is_zip: + importer = zipimport.zipimporter(os.path.join(folder, world_source.path)) + if hasattr(importer, "find_spec"): # new in Python 3.10 + spec = importer.find_spec(world_source.path.split(".", 1)[0]) + mod = importlib.util.module_from_spec(spec) + else: # TODO: remove with 3.8 support + mod = importer.load_module(world_source.path.split(".", 1)[0]) - mod.__package__ = f"worlds.{mod.__package__}" - mod.__name__ = f"worlds.{mod.__name__}" - sys.modules[mod.__name__] = mod - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", message="__package__ != __spec__.parent") - # Found no equivalent for < 3.10 - if hasattr(importer, "exec_module"): - importer.exec_module(mod) - else: - importlib.import_module(f".{world_source.path}", "worlds") + mod.__package__ = f"worlds.{mod.__package__}" + mod.__name__ = f"worlds.{mod.__name__}" + sys.modules[mod.__name__] = mod + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", message="__package__ != __spec__.parent") + # Found no equivalent for < 3.10 + if hasattr(importer, "exec_module"): + importer.exec_module(mod) + else: + importlib.import_module(f".{world_source.path}", "worlds") + except Exception as e: + # A single world failing can still mean enough is working for the user, log and carry on + import traceback + import io + file_like = io.StringIO() + print(f"Could not load world {world_source}:", file=file_like) + traceback.print_exc(file=file_like) + file_like.seek(0) + import logging + logging.exception(file_like.read()) lookup_any_item_id_to_name = {} lookup_any_location_id_to_name = {} @@ -75,14 +94,9 @@ games: typing.Dict[str, GamesPackage] = {} from .AutoWorld import AutoWorldRegister +# Build the data package for each game. for world_name, world in AutoWorldRegister.world_types.items(): - games[world_name] = { - "item_name_to_id": world.item_name_to_id, - "location_name_to_id": world.location_name_to_id, - "version": world.data_version, - # seems clients don't actually want this. Keeping it here in case someone changes their mind. - # "item_name_groups": {name: tuple(items) for name, items in world.item_name_groups.items()} - } + games[world_name] = world.get_data_package_data() lookup_any_item_id_to_name.update(world.item_id_to_name) lookup_any_location_id_to_name.update(world.location_id_to_name) diff --git a/worlds/adventure/Items.py b/worlds/adventure/Items.py new file mode 100644 index 0000000000..76d7d6fd8b --- /dev/null +++ b/worlds/adventure/Items.py @@ -0,0 +1,53 @@ +from typing import Optional +from BaseClasses import ItemClassification, Item + +base_adventure_item_id = 118000000 + + +class AdventureItem(Item): + def __init__(self, name: str, classification: ItemClassification, code: Optional[int], player: int): + super().__init__(name, classification, code, player) + + +class ItemData: + def __init__(self, id: int, classification: ItemClassification): + self.classification = classification + self.id = None if id is None else id + base_adventure_item_id + self.table_index = id + + +nothing_item_id = base_adventure_item_id + +# base IDs are the index in the static item data table, which is +# not the same order as the items in RAM (but offset 0 is a 16-bit address of +# location of room and position data) +item_table = { + "Yellow Key": ItemData(0xB, ItemClassification.progression_skip_balancing), + "White Key": ItemData(0xC, ItemClassification.progression), + "Black Key": ItemData(0xD, ItemClassification.progression), + "Bridge": ItemData(0xA, ItemClassification.progression), + "Magnet": ItemData(0x11, ItemClassification.progression), + "Sword": ItemData(0x9, ItemClassification.progression), + "Chalice": ItemData(0x10, ItemClassification.progression_skip_balancing), + # Non-ROM Adventure items, managed by lua + "Left Difficulty Switch": ItemData(0x100, ItemClassification.filler), + "Right Difficulty Switch": ItemData(0x101, ItemClassification.filler), + # Can use these instead of 'nothing' + "Freeincarnate": ItemData(0x102, ItemClassification.filler), + # These should only be enabled if fast dragons is on? + "Slow Yorgle": ItemData(0x103, ItemClassification.filler), + "Slow Grundle": ItemData(0x104, ItemClassification.filler), + "Slow Rhindle": ItemData(0x105, ItemClassification.filler), + # this should only be enabled if opted into? For now, I'll just exclude them + "Revive Dragons": ItemData(0x106, ItemClassification.trap), + "nothing": ItemData(0x0, ItemClassification.filler) + # Bat Trap + # Bat Time Out + # "Revive Dragons": ItemData(0x110, ItemClassification.trap) +} + +standard_item_max = item_table["Magnet"].id + + +event_table = { +} \ No newline at end of file diff --git a/worlds/adventure/Locations.py b/worlds/adventure/Locations.py new file mode 100644 index 0000000000..2ef561b1e3 --- /dev/null +++ b/worlds/adventure/Locations.py @@ -0,0 +1,214 @@ +from BaseClasses import Location + +base_location_id = 118000000 + + +class AdventureLocation(Location): + game: str = "Adventure" + + +class WorldPosition: + room_id: int + room_x: int + room_y: int + + def __init__(self, room_id: int, room_x: int = None, room_y: int = None): + self.room_id = room_id + self.room_x = room_x + self.room_y = room_y + + def get_position(self, random): + if self.room_x is None or self.room_y is None: + return random.choice(standard_positions) + else: + return self.room_x, self.room_y + + +class LocationData: + def __init__(self, region, name, location_id, world_positions: [WorldPosition] = None, event=False, + needs_bat_logic: bool = False): + self.region: str = region + self.name: str = name + self.world_positions: [WorldPosition] = world_positions + self.room_id: int = None + self.room_x: int = None + self.room_y: int = None + self.location_id: int = location_id + if location_id is None: + self.short_location_id: int = None + self.location_id: int = None + else: + self.short_location_id: int = location_id + self.location_id: int = location_id + base_location_id + self.event: bool = event + if world_positions is None and not event: + self.room_id: int = self.short_location_id + self.needs_bat_logic: int = needs_bat_logic + self.local_item: int = None + + def get_position(self, random): + if self.world_positions is None or len(self.world_positions) == 0: + if self.room_id is None: + return None + self.room_x, self.room_y = random.choice(standard_positions) + if self.room_id is None: + selected_pos = random.choice(self.world_positions) + self.room_id = selected_pos.room_id + self.room_x, self.room_y = selected_pos.get_position(random) + return self.room_x, self.room_y + + def get_room_id(self, random): + if self.world_positions is None or len(self.world_positions) == 0: + return None + if self.room_id is None: + selected_pos = random.choice(self.world_positions) + self.room_id = selected_pos.room_id + self.room_x, self.room_y = selected_pos.get_position(random) + return self.room_id + + +standard_positions = [ + (0x80, 0x20), + (0x20, 0x20), + (0x20, 0x40), + (0x20, 0x40), + (0x30, 0x20) +] + + +# Gives the most difficult region the dragon can reach and get stuck in from the provided room without the +# player unlocking something for it +def dragon_room_to_region(room: int) -> str: + if room <= 0x11: + return "Overworld" + elif room <= 0x12: + return "YellowCastle" + elif room <= 0x16 or room == 0x1B: + return "BlackCastle" + elif room <= 0x1A: + return "WhiteCastleVault" + elif room <= 0x1D: + return "Overworld" + elif room <= 0x1E: + return "CreditsRoom" + + +def get_random_room_in_regions(regions: [str], random) -> int: + possible_rooms = {} + for locname in location_table: + if location_table[locname].region in regions: + room = location_table[locname].get_room_id(random) + if room is not None: + possible_rooms[room] = location_table[locname].room_id + return random.choice(list(possible_rooms.keys())) + + +location_table = { + "Blue Labyrinth 0": LocationData("Overworld", "Blue Labyrinth 0", 0x4, + [WorldPosition(0x4, 0x83, 0x47), # exit upper right + WorldPosition(0x4, 0x12, 0x47), # exit upper left + WorldPosition(0x4, 0x65, 0x20), # exit bottom right + WorldPosition(0x4, 0x2A, 0x20), # exit bottom left + WorldPosition(0x5, 0x4B, 0x60), # T room, top + WorldPosition(0x5, 0x28, 0x1F), # T room, bottom left + WorldPosition(0x5, 0x70, 0x1F), # T room, bottom right + ]), + "Blue Labyrinth 1": LocationData("Overworld", "Blue Labyrinth 1", 0x6, + [WorldPosition(0x6, 0x8C, 0x20), # final turn bottom right + WorldPosition(0x6, 0x03, 0x20), # final turn bottom left + WorldPosition(0x6, 0x4B, 0x30), # final turn center + WorldPosition(0x7, 0x4B, 0x40), # straightaway center + WorldPosition(0x8, 0x40, 0x40), # entrance middle loop + WorldPosition(0x8, 0x4B, 0x60), # entrance upper loop + WorldPosition(0x8, 0x8C, 0x5E), # entrance right loop + ]), + "Catacombs": LocationData("Overworld", "Catacombs", 0x9, + [WorldPosition(0x9, 0x49, 0x40), + WorldPosition(0x9, 0x4b, 0x20), + WorldPosition(0xA), + WorldPosition(0xA), + WorldPosition(0xB, 0x40, 0x40), + WorldPosition(0xB, 0x22, 0x1f), + WorldPosition(0xB, 0x70, 0x1f)]), + "Adjacent to Catacombs": LocationData("Overworld", "Adjacent to Catacombs", 0xC, + [WorldPosition(0xC), + WorldPosition(0xD)]), + "Southwest of Catacombs": LocationData("Overworld", "Southwest of Catacombs", 0xE), + "White Castle Gate": LocationData("Overworld", "White Castle Gate", 0xF), + "Black Castle Gate": LocationData("Overworld", "Black Castle Gate", 0x10), + "Yellow Castle Gate": LocationData("Overworld", "Yellow Castle Gate", 0x11), + "Inside Yellow Castle": LocationData("YellowCastle", "Inside Yellow Castle", 0x12), + "Dungeon0": LocationData("BlackCastle", "Dungeon0", 0x13, + [WorldPosition(0x13), + WorldPosition(0x14)]), + "Dungeon Vault": LocationData("BlackCastleVault", "Dungeon Vault", 0xB5, + [WorldPosition(0x15, 0x46, 0x1B)], + needs_bat_logic=True), + "Dungeon1": LocationData("BlackCastle", "Dungeon1", 0x15, + [WorldPosition(0x15), + WorldPosition(0x16)]), + "RedMaze0": LocationData("WhiteCastle", "RedMaze0", 0x17, + [WorldPosition(0x17, 0x70, 0x40), # right side third room + WorldPosition(0x17, 0x18, 0x40), # left side third room + WorldPosition(0x18, 0x20, 0x40), + WorldPosition(0x18, 0x1A, 0x3F), # left side second room + WorldPosition(0x18, 0x70, 0x3F), # right side second room + ]), + "Red Maze Vault Entrance": LocationData("WhiteCastlePreVaultPeek", "Red Maze Vault Entrance", 0xB7, + [WorldPosition(0x17, 0x50, 0x60)], + needs_bat_logic=True), + "Red Maze Vault": LocationData("WhiteCastleVault", "Red Maze Vault", 0x19, + [WorldPosition(0x19, 0x4E, 0x35)], + needs_bat_logic=True), + "RedMaze1": LocationData("WhiteCastle", "RedMaze1", 0x1A), # entrance + "Black Castle Foyer": LocationData("BlackCastle", "Black Castle Foyer", 0x1B), + "Northeast of Catacombs": LocationData("Overworld", "Northeast of Catacombs", 0x1C), + "Southeast of Catacombs": LocationData("Overworld", "Southeast of Catacombs", 0x1D), + "Credits Left Side": LocationData("CreditsRoom", "Credits Left Side", 0x1E, + [WorldPosition(0x1E, 0x25, 0x50)]), + "Credits Right Side": LocationData("CreditsRoomFarSide", "Credits Right Side", 0xBE, + [WorldPosition(0x1E, 0x70, 0x40)], + needs_bat_logic=True), + "Chalice Home": LocationData("YellowCastle", "Chalice Home", None, event=True), + "Slay Yorgle": LocationData("Varies", "Slay Yorgle", 0xD1, event=False), + "Slay Grundle": LocationData("Varies", "Slay Grundle", 0xD2, event=False), + "Slay Rhindle": LocationData("Varies", "Slay Rhindle", 0xD0, event=False), +} + +# the old location table, for reference +location_table_old = { + "Blue Labyrinth 0": LocationData("Overworld", "Blue Labyrinth 0", 0x4), + "Blue Labyrinth 1": LocationData("Overworld", "Blue Labyrinth 1", 0x5), + "Blue Labyrinth 2": LocationData("Overworld", "Blue Labyrinth 2", 0x6), + "Blue Labyrinth 3": LocationData("Overworld", "Blue Labyrinth 3", 0x7), + "Blue Labyrinth 4": LocationData("Overworld", "Blue Labyrinth 4", 0x8), + "Catacombs0": LocationData("Overworld", "Catacombs0", 0x9), + "Catacombs1": LocationData("Overworld", "Catacombs1", 0xA), + "Catacombs2": LocationData("Overworld", "Catacombs2", 0xB), + "East of Catacombs": LocationData("Overworld", "East of Catacombs", 0xC), + "West of Catacombs": LocationData("Overworld", "West of Catacombs", 0xD), + "Southwest of Catacombs": LocationData("Overworld", "Southwest of Catacombs", 0xE), + "White Castle Gate": LocationData("Overworld", "White Castle Gate", 0xF), + "Black Castle Gate": LocationData("Overworld", "Black Castle Gate", 0x10), + "Yellow Castle Gate": LocationData("Overworld", "Yellow Castle Gate", 0x11), + "Inside Yellow Castle": LocationData("YellowCastle", "Inside Yellow Castle", 0x12), + "Dungeon0": LocationData("BlackCastle", "Dungeon0", 0x13), + "Dungeon1": LocationData("BlackCastle", "Dungeon1", 0x14), + "Dungeon Vault": LocationData("BlackCastleVault", "Dungeon Vault", 0x15, + [WorldPosition(0xB5, 0x46, 0x1B)]), + "Dungeon2": LocationData("BlackCastle", "Dungeon2", 0x15), + "Dungeon3": LocationData("BlackCastle", "Dungeon3", 0x16), + "RedMaze0": LocationData("WhiteCastle", "RedMaze0", 0x17, [WorldPosition(0x17, 0x70, 0x40)]), + "RedMaze1": LocationData("WhiteCastle", "RedMaze1", 0x18, [WorldPosition(0x18, 0x20, 0x40)]), + "Red Maze Vault Entrance": LocationData("WhiteCastlePreVaultPeek", "Red Maze Vault Entrance", + 0x17, [WorldPosition(0xB7, 0x50, 0x60)]), + "Red Maze Vault": LocationData("WhiteCastleVault", "Red Maze Vault", 0x19, [WorldPosition(0x19, 0x4E, 0x35)]), + "RedMaze3": LocationData("WhiteCastle", "RedMaze3", 0x1A), + "Black Castle Foyer": LocationData("BlackCastle", "Black Castle Foyer", 0x1B), + "Northeast of Catacombs": LocationData("Overworld", "Northeast of Catacombs", 0x1C), + "Southeast of Catacombs": LocationData("Overworld", "Southeast of Catacombs", 0x1D), + "Credits Left Side": LocationData("CreditsRoom", "Credits Left Side", 0x1E, [WorldPosition(0x1E, 0x25, 0x50)]), + "Credits Right Side": LocationData("CreditsRoomFarSide", "Credits Right Side", 0x1E, + [WorldPosition(0xBE, 0x70, 0x40)]), + "Chalice Home": LocationData("YellowCastle", "Chalice Home", None, event=True) +} diff --git a/worlds/adventure/Offsets.py b/worlds/adventure/Offsets.py new file mode 100644 index 0000000000..c1e74aee9b --- /dev/null +++ b/worlds/adventure/Offsets.py @@ -0,0 +1,46 @@ +# probably I should generate this from the list file + +static_item_data_location = 0xe9d +static_item_element_size = 9 +static_first_dragon_index = 6 +item_position_table = 0x402 +items_ram_start = 0xa1 +connector_port_offset = 0xff9 +# dragon speeds are hardcoded directly in their respective movement subroutines, not in their item table or state data +# so this is the second byte of an LDA immediate instruction +yorgle_speed_data_location = 0x724 +grundle_speed_data_location = 0x73f +rhindle_speed_data_location = 0x709 + + +# in case I need to place a rom address in the rom +rom_address_space_start = 0xf000 + +start_castle_offset = 0x39c +start_castle_values = [0x11, 0x10, 0x0F] +"""yellow, black, white castle gate rooms""" + +# indexed by static item table index. 0x00 indicates the position data is in ROM and is irrelevant to the randomizer +item_ram_addresses = [ + 0xD9, # lamp + 0x00, # portcullis 1 + 0x00, # portcullis 2 + 0x00, # portcullis 3 + 0x00, # author name + 0x00, # GO object + 0xA4, # Rhindle + 0xA9, # Yorgle + 0xAE, # Grundle + 0xB6, # Sword + 0xBC, # Bridge + 0xBF, # Yellow Key + 0xC2, # White key + 0xC5, # Black key + 0xCB, # Bat + 0xA1, # Dot + 0xB9, # Chalice + 0xB3, # Magnet + 0xE7, # AP object 1 + 0xEA, # AP bat object + 0xBC, # NULL object (end of table) +] diff --git a/worlds/adventure/Options.py b/worlds/adventure/Options.py new file mode 100644 index 0000000000..a8016fc287 --- /dev/null +++ b/worlds/adventure/Options.py @@ -0,0 +1,244 @@ +from __future__ import annotations + +from typing import Dict + +from Options import Choice, Option, DefaultOnToggle, DeathLink, Range, Toggle + + +class FreeincarnateMax(Range): + """How many maximum freeincarnate items to allow + + When done generating items, any remaining item slots will be filled + with freeincarnates, up to this maximum amount. Any remaining item + slots after that will be 'nothing' items placed locally, so in multigame + multiworlds, keeping this value high will allow more items from other games + into Adventure. + """ + display_name = "Freeincarnate Maximum" + range_start = 0 + range_end = 17 + default = 17 + + +class ItemRandoType(Choice): + """Choose how items are placed in the game + + Not yet implemented. Currently only traditional supported + Traditional: Adventure items are not in the map until + they are collected (except local items) and are dropped + on the player when collected. Adventure items are not checks. + Inactive: Every item is placed, but is inactive until collected. + Each item touched is a check. The bat ignores inactive items. + + Supported values: traditional, inactive + Default value: traditional + """ + + display_name = "Item type" + option_traditional = 0x00 + option_inactive = 0x01 + default = option_traditional + + +class DragonSlayCheck(DefaultOnToggle): + """If true, slaying each dragon for the first time is a check + """ + display_name = "Slay Dragon Checks" + + +class TrapBatCheck(Choice): + """ + Locking the bat inside a castle may be a check + + Not yet implemented + If set to yes, the bat will not start inside a castle. + Setting with_key requires the matching castle key to also be + in the castle with the bat, achieved by dropping the key in the + path of the portcullis as it falls. This setting is not recommended with the bat use_logic setting + + Supported values: no, yes, with_key + Default value: yes + """ + display_name = "Trap bat check" + option_no_check = 0x0 + option_yes_key_optional = 0x1 + option_with_key = 0x2 + default = option_yes_key_optional + + +class DragonRandoType(Choice): + """ + How to randomize the dragon starting locations + + normal: Grundle is in the overworld, Yorgle in the white castle, and Rhindle in the black castle + shuffle: A random dragon is placed in the overworld, one in the white castle, and one in the black castle + overworldplus: Dragons can be placed anywhere, but at least one will be in the overworld + randomized: Dragons can be anywhere except the credits room + + + Supported values: normal, shuffle, overworldplus, randomized + Default value: shuffle + """ + display_name = "Dragon Randomization" + option_normal = 0x0 + option_shuffle = 0x1 + option_overworldplus = 0x2 + option_randomized = 0x3 + default = option_shuffle + + +class BatLogic(Choice): + """How the bat is considered for logic + + With cannot_break, the bat cannot pick up an item that starts out-of-logic until the player touches it + With can_break, the bat is free to pick up any items, even if they are out-of-logic + With use_logic, the bat can pick up anything just like can_break, and locations are no longer considered to require + the magnet or bridge to collect, since the bat can retrieve these. + A future option may allow the bat itself to be placed as an item. + + Supported values: cannot_break, can_break, use_logic + Default value: can_break + """ + display_name = "Bat Logic" + option_cannot_break = 0x0 + option_can_break = 0x1 + option_use_logic = 0x2 + default = option_can_break + + +class YorgleStartingSpeed(Range): + """ + Sets Yorgle's initial speed. Yorgle has a speed of 2 in the original game + Default value: 2 + """ + display_name = "Yorgle MaxSpeed" + range_start = 1 + range_end = 9 + default = 2 + + +class YorgleMinimumSpeed(Range): + """ + Sets Yorgle's speed when all speed reducers are found. Yorgle has a speed of 2 in the original game + Default value: 2 + """ + display_name = "Yorgle Min Speed" + range_start = 1 + range_end = 9 + default = 1 + + +class GrundleStartingSpeed(Range): + """ + Sets Grundle's initial speed. Grundle has a speed of 2 in the original game + Default value: 2 + """ + display_name = "Grundle MaxSpeed" + range_start = 1 + range_end = 9 + default = 2 + + +class GrundleMinimumSpeed(Range): + """ + Sets Grundle's speed when all speed reducers are found. Grundle has a speed of 2 in the original game + Default value: 2 + """ + display_name = "Grundle Min Speed" + range_start = 1 + range_end = 9 + default = 1 + + +class RhindleStartingSpeed(Range): + """ + Sets Rhindle's initial speed. Rhindle has a speed of 3 in the original game + Default value: 3 + """ + display_name = "Rhindle MaxSpeed" + range_start = 1 + range_end = 9 + default = 3 + + +class RhindleMinimumSpeed(Range): + """ + Sets Rhindle's speed when all speed reducers are found. Rhindle has a speed of 3 in the original game + Default value: 2 + """ + display_name = "Rhindle Min Speed" + range_start = 1 + range_end = 9 + default = 2 + + +class ConnectorMultiSlot(Toggle): + """If true, the client and lua connector will add lowest 8 bits of the player slot + to the port number used to connect to each other, to simplify connecting multiple local + clients to local BizHawks. + Set in the yaml, since the connector has to read this out of the rom file before connecting. + """ + display_name = "Connector Multi-Slot" + + +class DifficultySwitchA(Choice): + """Set availability of left difficulty switch + This controls the speed of the dragons' bite animation + + """ + display_name = "Left Difficulty Switch" + option_normal = 0x0 + option_locked_hard = 0x1 + option_hard_with_unlock_item = 0x2 + default = option_hard_with_unlock_item + + +class DifficultySwitchB(Choice): + """Set availability of right difficulty switch + On hard, dragons will run away from the sword + + """ + display_name = "Right Difficulty Switch" + option_normal = 0x0 + option_locked_hard = 0x1 + option_hard_with_unlock_item = 0x2 + default = option_hard_with_unlock_item + + +class StartCastle(Choice): + """Choose or randomize which castle to start in front of. + + This affects both normal start and reincarnation. Starting + at the black castle may give easy dot runs, while starting + at the white castle may make them more dangerous! Also, not + starting at the yellow castle can make delivering the chalice + with a full inventory slightly less trivial. + + This doesn't affect logic since all the castles are reachable + from each other. + """ + display_name = "Start Castle" + option_yellow = 0 + option_black = 1 + option_white = 2 + default = option_yellow + + +adventure_option_definitions: Dict[str, type(Option)] = { + "dragon_slay_check": DragonSlayCheck, + "death_link": DeathLink, + "bat_logic": BatLogic, + "freeincarnate_max": FreeincarnateMax, + "dragon_rando_type": DragonRandoType, + "connector_multi_slot": ConnectorMultiSlot, + "yorgle_speed": YorgleStartingSpeed, + "yorgle_min_speed": YorgleMinimumSpeed, + "grundle_speed": GrundleStartingSpeed, + "grundle_min_speed": GrundleMinimumSpeed, + "rhindle_speed": RhindleStartingSpeed, + "rhindle_min_speed": RhindleMinimumSpeed, + "difficulty_switch_a": DifficultySwitchA, + "difficulty_switch_b": DifficultySwitchB, + "start_castle": StartCastle, + +} \ No newline at end of file diff --git a/worlds/adventure/Regions.py b/worlds/adventure/Regions.py new file mode 100644 index 0000000000..4a62518fbd --- /dev/null +++ b/worlds/adventure/Regions.py @@ -0,0 +1,160 @@ +from BaseClasses import MultiWorld, Region, Entrance, LocationProgressType +from .Locations import location_table, LocationData, AdventureLocation, dragon_room_to_region + + +def connect(world: MultiWorld, player: int, source: str, target: str, rule: callable = lambda state: True, + one_way=False, name=None): + source_region = world.get_region(source, player) + target_region = world.get_region(target, player) + + if name is None: + name = source + " to " + target + + connection = Entrance( + player, + name, + source_region + ) + + connection.access_rule = rule + + source_region.exits.append(connection) + connection.connect(target_region) + if not one_way: + connect(world, player, target, source, rule, True) + + +def create_regions(multiworld: MultiWorld, player: int, dragon_rooms: []) -> None: + for name, locdata in location_table.items(): + locdata.get_position(multiworld.random) + + menu = Region("Menu", player, multiworld) + + menu.exits.append(Entrance(player, "GameStart", menu)) + multiworld.regions.append(menu) + + overworld = Region("Overworld", player, multiworld) + overworld.exits.append(Entrance(player, "YellowCastlePort", overworld)) + overworld.exits.append(Entrance(player, "WhiteCastlePort", overworld)) + overworld.exits.append(Entrance(player, "BlackCastlePort", overworld)) + overworld.exits.append(Entrance(player, "CreditsWall", overworld)) + multiworld.regions.append(overworld) + + yellow_castle = Region("YellowCastle", player, multiworld, "Yellow Castle") + yellow_castle.exits.append(Entrance(player, "YellowCastleExit", yellow_castle)) + multiworld.regions.append(yellow_castle) + + white_castle = Region("WhiteCastle", player, multiworld, "White Castle") + white_castle.exits.append(Entrance(player, "WhiteCastleExit", white_castle)) + white_castle.exits.append(Entrance(player, "WhiteCastleSecretPassage", white_castle)) + white_castle.exits.append(Entrance(player, "WhiteCastlePeekPassage", white_castle)) + multiworld.regions.append(white_castle) + + white_castle_pre_vault_peek = Region("WhiteCastlePreVaultPeek", player, multiworld, "White Castle Secret Peek") + white_castle_pre_vault_peek.exits.append(Entrance(player, "WhiteCastleFromPeek", white_castle_pre_vault_peek)) + multiworld.regions.append(white_castle_pre_vault_peek) + + white_castle_secret_room = Region("WhiteCastleVault", player, multiworld, "White Castle Vault",) + white_castle_secret_room.exits.append(Entrance(player, "WhiteCastleReturnPassage", white_castle_secret_room)) + multiworld.regions.append(white_castle_secret_room) + + black_castle = Region("BlackCastle", player, multiworld, "Black Castle") + black_castle.exits.append(Entrance(player, "BlackCastleExit", black_castle)) + black_castle.exits.append(Entrance(player, "BlackCastleVaultEntrance", black_castle)) + multiworld.regions.append(black_castle) + + black_castle_secret_room = Region("BlackCastleVault", player, multiworld, "Black Castle Vault") + black_castle_secret_room.exits.append(Entrance(player, "BlackCastleReturnPassage", black_castle_secret_room)) + multiworld.regions.append(black_castle_secret_room) + + credits_room = Region("CreditsRoom", player, multiworld, "Credits Room") + credits_room.exits.append(Entrance(player, "CreditsExit", credits_room)) + credits_room.exits.append(Entrance(player, "CreditsToFarSide", credits_room)) + multiworld.regions.append(credits_room) + + credits_room_far_side = Region("CreditsRoomFarSide", player, multiworld, "Credits Far Side") + credits_room_far_side.exits.append(Entrance(player, "CreditsFromFarSide", credits_room_far_side)) + multiworld.regions.append(credits_room_far_side) + + dragon_slay_check = multiworld.dragon_slay_check[player].value + priority_locations = determine_priority_locations(multiworld, dragon_slay_check) + + for name, location_data in location_table.items(): + require_sword = False + if location_data.region == "Varies": + if location_data.name == "Slay Yorgle": + if not dragon_slay_check: + continue + region_name = dragon_room_to_region(dragon_rooms[0]) + elif location_data.name == "Slay Grundle": + if not dragon_slay_check: + continue + region_name = dragon_room_to_region(dragon_rooms[1]) + elif location_data.name == "Slay Rhindle": + if not dragon_slay_check: + continue + region_name = dragon_room_to_region(dragon_rooms[2]) + else: + raise Exception(f"Unknown location region for {location_data.name}") + r = multiworld.get_region(region_name, player) + else: + r = multiworld.get_region(location_data.region, player) + + adventure_loc = AdventureLocation(player, location_data.name, location_data.location_id, r) + if adventure_loc.name in priority_locations: + adventure_loc.progress_type = LocationProgressType.PRIORITY + r.locations.append(adventure_loc) + + # In a tracker and plando-free world, I'd determine unused locations here and not add them. + # But that would cause problems with both plandos and trackers. So I guess I'll stick + # with filling in with 'nothing' in pre_fill. + + # in the future, I may randomize the map some, and that will require moving + # connections to later, probably + + multiworld.get_entrance("GameStart", player) \ + .connect(multiworld.get_region("Overworld", player)) + + multiworld.get_entrance("YellowCastlePort", player) \ + .connect(multiworld.get_region("YellowCastle", player)) + multiworld.get_entrance("YellowCastleExit", player) \ + .connect(multiworld.get_region("Overworld", player)) + + multiworld.get_entrance("WhiteCastlePort", player) \ + .connect(multiworld.get_region("WhiteCastle", player)) + multiworld.get_entrance("WhiteCastleExit", player) \ + .connect(multiworld.get_region("Overworld", player)) + + multiworld.get_entrance("WhiteCastleSecretPassage", player) \ + .connect(multiworld.get_region("WhiteCastleVault", player)) + multiworld.get_entrance("WhiteCastleReturnPassage", player) \ + .connect(multiworld.get_region("WhiteCastle", player)) + multiworld.get_entrance("WhiteCastlePeekPassage", player) \ + .connect(multiworld.get_region("WhiteCastlePreVaultPeek", player)) + multiworld.get_entrance("WhiteCastleFromPeek", player) \ + .connect(multiworld.get_region("WhiteCastle", player)) + + multiworld.get_entrance("BlackCastlePort", player) \ + .connect(multiworld.get_region("BlackCastle", player)) + multiworld.get_entrance("BlackCastleExit", player) \ + .connect(multiworld.get_region("Overworld", player)) + multiworld.get_entrance("BlackCastleVaultEntrance", player) \ + .connect(multiworld.get_region("BlackCastleVault", player)) + multiworld.get_entrance("BlackCastleReturnPassage", player) \ + .connect(multiworld.get_region("BlackCastle", player)) + + multiworld.get_entrance("CreditsWall", player) \ + .connect(multiworld.get_region("CreditsRoom", player)) + multiworld.get_entrance("CreditsExit", player) \ + .connect(multiworld.get_region("Overworld", player)) + + multiworld.get_entrance("CreditsToFarSide", player) \ + .connect(multiworld.get_region("CreditsRoomFarSide", player)) + multiworld.get_entrance("CreditsFromFarSide", player) \ + .connect(multiworld.get_region("CreditsRoom", player)) + + +# Placeholder for adding sets of priority locations at generation, possibly as an option in the future +def determine_priority_locations(world: MultiWorld, dragon_slay_check: bool) -> {}: + priority_locations = {} + return priority_locations diff --git a/worlds/adventure/Rom.py b/worlds/adventure/Rom.py new file mode 100644 index 0000000000..62c4019718 --- /dev/null +++ b/worlds/adventure/Rom.py @@ -0,0 +1,321 @@ +import hashlib +import json +import os +import zipfile +from typing import Optional, Any + +import Utils +from .Locations import AdventureLocation, LocationData +from Utils import OptionsType +from worlds.Files import APDeltaPatch, AutoPatchRegister, APContainer +from itertools import chain + +import bsdiff4 + +ADVENTUREHASH: str = "157bddb7192754a45372be196797f284" + + +class AdventureAutoCollectLocation: + short_location_id: int = 0 + room_id: int = 0 + + def __init__(self, short_location_id: int, room_id: int): + self.short_location_id = short_location_id + self.room_id = room_id + + def get_dict(self): + return { + "short_location_id": self.short_location_id, + "room_id": self.room_id, + } + + +class AdventureForeignItemInfo: + short_location_id: int = 0 + room_id: int = 0 + room_x: int = 0 + room_y: int = 0 + + def __init__(self, short_location_id: int, room_id: int, room_x: int, room_y: int): + self.short_location_id = short_location_id + self.room_id = room_id + self.room_x = room_x + self.room_y = room_y + + def get_dict(self): + return { + "short_location_id": self.short_location_id, + "room_id": self.room_id, + "room_x": self.room_x, + "room_y": self.room_y, + } + + +class BatNoTouchLocation: + short_location_id: int + room_id: int + room_x: int + room_y: int + local_item: int + + def __init__(self, short_location_id: int, room_id: int, room_x: int, room_y: int, local_item: int = None): + self.short_location_id = short_location_id + self.room_id = room_id + self.room_x = room_x + self.room_y = room_y + self.local_item = local_item + + def get_dict(self): + ret_dict = { + "short_location_id": self.short_location_id, + "room_id": self.room_id, + "room_x": self.room_x, + "room_y": self.room_y, + } + if self.local_item is not None: + ret_dict["local_item"] = self.local_item + else: + ret_dict["local_item"] = 255 + return ret_dict + + +class AdventureDeltaPatch(APContainer, metaclass=AutoPatchRegister): + hash = ADVENTUREHASH + game = "Adventure" + patch_file_ending = ".apadvn" + zip_version: int = 2 + + # locations: [], autocollect: [], seed_name: bytes, + def __init__(self, *args: Any, **kwargs: Any) -> None: + patch_only = True + if "autocollect" in kwargs: + patch_only = False + self.foreign_items: [AdventureForeignItemInfo] = [AdventureForeignItemInfo(loc.short_location_id, loc.room_id, loc.room_x, loc.room_y) + for loc in kwargs["locations"]] + + self.autocollect_items: [AdventureAutoCollectLocation] = kwargs["autocollect"] + self.seedName: bytes = kwargs["seed_name"] + self.local_item_locations: {} = kwargs["local_item_locations"] + self.dragon_speed_reducer_info: {} = kwargs["dragon_speed_reducer_info"] + self.diff_a_mode: int = kwargs["diff_a_mode"] + self.diff_b_mode: int = kwargs["diff_b_mode"] + self.bat_logic: int = kwargs["bat_logic"] + self.bat_no_touch_locations: [LocationData] = kwargs["bat_no_touch_locations"] + self.rom_deltas: {int, int} = kwargs["rom_deltas"] + del kwargs["locations"] + del kwargs["autocollect"] + del kwargs["seed_name"] + del kwargs["local_item_locations"] + del kwargs["dragon_speed_reducer_info"] + del kwargs["diff_a_mode"] + del kwargs["diff_b_mode"] + del kwargs["bat_logic"] + del kwargs["bat_no_touch_locations"] + del kwargs["rom_deltas"] + super(AdventureDeltaPatch, self).__init__(*args, **kwargs) + + def write_contents(self, opened_zipfile: zipfile.ZipFile): + super(AdventureDeltaPatch, self).write_contents(opened_zipfile) + # write Delta + opened_zipfile.writestr("zip_version", + self.zip_version.to_bytes(1, "little"), + compress_type=zipfile.ZIP_STORED) + if self.foreign_items is not None: + loc_bytes = [] + for foreign_item in self.foreign_items: + loc_bytes.append(foreign_item.short_location_id) + loc_bytes.append(foreign_item.room_id) + loc_bytes.append(foreign_item.room_x) + loc_bytes.append(foreign_item.room_y) + opened_zipfile.writestr("adventure_locations", + bytes(loc_bytes), + compress_type=zipfile.ZIP_LZMA) + if self.autocollect_items is not None: + loc_bytes = [] + for item in self.autocollect_items: + loc_bytes.append(item.short_location_id) + loc_bytes.append(item.room_id) + opened_zipfile.writestr("adventure_autocollect", + bytes(loc_bytes), + compress_type=zipfile.ZIP_LZMA) + if self.player_name is not None: + opened_zipfile.writestr("player", + self.player_name, # UTF-8 + compress_type=zipfile.ZIP_STORED) + if self.seedName is not None: + opened_zipfile.writestr("seedName", + self.seedName, + compress_type=zipfile.ZIP_STORED) + if self.local_item_locations is not None: + opened_zipfile.writestr("local_item_locations", + json.dumps(self.local_item_locations), + compress_type=zipfile.ZIP_LZMA) + if self.dragon_speed_reducer_info is not None: + opened_zipfile.writestr("dragon_speed_reducer_info", + json.dumps(self.dragon_speed_reducer_info), + compress_type=zipfile.ZIP_LZMA) + if self.diff_a_mode is not None: + opened_zipfile.writestr("diff_a_mode", + self.diff_a_mode.to_bytes(1, "little"), + compress_type=zipfile.ZIP_STORED) + if self.diff_b_mode is not None: + opened_zipfile.writestr("diff_b_mode", + self.diff_b_mode.to_bytes(1, "little"), + compress_type=zipfile.ZIP_STORED) + if self.bat_logic is not None: + opened_zipfile.writestr("bat_logic", + self.bat_logic.to_bytes(1, "little"), + compress_type=zipfile.ZIP_STORED) + if self.bat_no_touch_locations is not None: + loc_bytes = [] + for loc in self.bat_no_touch_locations: + loc_bytes.append(loc.short_location_id) # used for AP items managed by script + loc_bytes.append(loc.room_id) # used for local items placed in rom + loc_bytes.append(loc.room_x) + loc_bytes.append(loc.room_y) + loc_bytes.append(0xff if loc.local_item is None else loc.local_item) + opened_zipfile.writestr("bat_no_touch_locations", + bytes(loc_bytes), + compress_type=zipfile.ZIP_LZMA) + if self.rom_deltas is not None: + # this is not an efficient way to do this AT ALL, but Adventure's data is so tiny it shouldn't matter + # if you're looking at doing something like this for another game, consider encoding your rom changes + # in a more efficient way + opened_zipfile.writestr("rom_deltas", + json.dumps(self.rom_deltas), + compress_type=zipfile.ZIP_LZMA) + + def read_contents(self, opened_zipfile: zipfile.ZipFile): + super(AdventureDeltaPatch, self).read_contents(opened_zipfile) + self.foreign_items = AdventureDeltaPatch.read_foreign_items(opened_zipfile) + self.autocollect_items = AdventureDeltaPatch.read_autocollect_items(opened_zipfile) + + @classmethod + def get_source_data(cls) -> bytes: + return get_base_rom_bytes() + + @classmethod + def check_version(cls, opened_zipfile: zipfile.ZipFile) -> bool: + version_bytes = opened_zipfile.read("zip_version") + version = 0 + if version_bytes is not None: + version = int.from_bytes(version_bytes, "little") + if version != cls.zip_version: + return False + return True + + @classmethod + def read_rom_info(cls, opened_zipfile: zipfile.ZipFile) -> (bytes, bytes, str): + seedbytes: bytes = opened_zipfile.read("seedName") + namebytes: bytes = opened_zipfile.read("player") + namestr: str = namebytes.decode("utf-8") + return seedbytes, namestr + + @classmethod + def read_difficulty_switch_info(cls, opened_zipfile: zipfile.ZipFile) -> (int, int): + diff_a_bytes = opened_zipfile.read("diff_a_mode") + diff_b_bytes = opened_zipfile.read("diff_b_mode") + diff_a = 0 + diff_b = 0 + if diff_a_bytes is not None: + diff_a = int.from_bytes(diff_a_bytes, "little") + if diff_b_bytes is not None: + diff_b = int.from_bytes(diff_b_bytes, "little") + return diff_a, diff_b + + @classmethod + def read_bat_logic(cls, opened_zipfile: zipfile.ZipFile) -> int: + bat_logic = opened_zipfile.read("bat_logic") + if bat_logic is None: + return 0 + return int.from_bytes(bat_logic, "little") + + @classmethod + def read_foreign_items(cls, opened_zipfile: zipfile.ZipFile) -> [AdventureForeignItemInfo]: + foreign_items = [] + readbytes: bytes = opened_zipfile.read("adventure_locations") + bytelist = list(readbytes) + for i in range(round(len(bytelist) / 4)): + offset = i * 4 + foreign_items.append(AdventureForeignItemInfo(bytelist[offset], + bytelist[offset + 1], + bytelist[offset + 2], + bytelist[offset + 3])) + return foreign_items + + @classmethod + def read_bat_no_touch(cls, opened_zipfile: zipfile.ZipFile) -> [BatNoTouchLocation]: + locations = [] + readbytes: bytes = opened_zipfile.read("bat_no_touch_locations") + bytelist = list(readbytes) + for i in range(round(len(bytelist) / 5)): + offset = i * 5 + locations.append(BatNoTouchLocation(bytelist[offset], + bytelist[offset + 1], + bytelist[offset + 2], + bytelist[offset + 3], + bytelist[offset + 4])) + return locations + + @classmethod + def read_autocollect_items(cls, opened_zipfile: zipfile.ZipFile) -> [AdventureForeignItemInfo]: + autocollect_items = [] + readbytes: bytes = opened_zipfile.read("adventure_autocollect") + bytelist = list(readbytes) + for i in range(round(len(bytelist) / 2)): + offset = i * 2 + autocollect_items.append(AdventureAutoCollectLocation(bytelist[offset], bytelist[offset + 1])) + return autocollect_items + + @classmethod + def read_local_item_locations(cls, opened_zipfile: zipfile.ZipFile) -> [AdventureForeignItemInfo]: + readbytes: bytes = opened_zipfile.read("local_item_locations") + readstr: str = readbytes.decode() + return json.loads(readstr) + + @classmethod + def read_dragon_speed_info(cls, opened_zipfile: zipfile.ZipFile) -> {}: + readbytes: bytes = opened_zipfile.read("dragon_speed_reducer_info") + readstr: str = readbytes.decode() + return json.loads(readstr) + + @classmethod + def read_rom_deltas(cls, opened_zipfile: zipfile.ZipFile) -> {int, int}: + readbytes: bytes = opened_zipfile.read("rom_deltas") + readstr: str = readbytes.decode() + return json.loads(readstr) + + @classmethod + def apply_rom_deltas(cls, base_bytes: bytes, rom_deltas: {int, int}) -> bytearray: + rom_bytes = bytearray(base_bytes) + for offset, value in rom_deltas.items(): + int_offset = int(offset) + rom_bytes[int_offset:int_offset+1] = int.to_bytes(value, 1, "little") + return rom_bytes + + +def apply_basepatch(base_rom_bytes: bytes) -> bytes: + with open(os.path.join(os.path.dirname(__file__), "../../data/adventure_basepatch.bsdiff4"), "rb") as basepatch: + delta: bytes = basepatch.read() + return bsdiff4.patch(base_rom_bytes, delta) + + +def get_base_rom_bytes(file_name: str = "") -> bytes: + file_name = get_base_rom_path(file_name) + with open(file_name, "rb") as file: + base_rom_bytes = bytes(file.read()) + basemd5 = hashlib.md5() + basemd5.update(base_rom_bytes) + if ADVENTUREHASH != basemd5.hexdigest(): + raise Exception(f"Supplied Base Rom does not match known MD5 for Adventure. " + "Get the correct game and version, then dump it") + return base_rom_bytes + + +def get_base_rom_path(file_name: str = "") -> str: + options: OptionsType = Utils.get_options() + if not file_name: + file_name = options["adventure_options"]["rom_file"] + if not os.path.exists(file_name): + file_name = Utils.user_path(file_name) + return file_name diff --git a/worlds/adventure/Rules.py b/worlds/adventure/Rules.py new file mode 100644 index 0000000000..6f4b53faa1 --- /dev/null +++ b/worlds/adventure/Rules.py @@ -0,0 +1,98 @@ +from worlds.adventure import location_table +from worlds.adventure.Options import BatLogic, DifficultySwitchB, DifficultySwitchA +from worlds.generic.Rules import add_rule, set_rule, forbid_item +from BaseClasses import LocationProgressType + + +def set_rules(self) -> None: + world = self.multiworld + use_bat_logic = world.bat_logic[self.player].value == BatLogic.option_use_logic + + set_rule(world.get_entrance("YellowCastlePort", self.player), + lambda state: state.has("Yellow Key", self.player)) + set_rule(world.get_entrance("BlackCastlePort", self.player), + lambda state: state.has("Black Key", self.player)) + set_rule(world.get_entrance("WhiteCastlePort", self.player), + lambda state: state.has("White Key", self.player)) + + # a future thing would be to make the bat an actual item, or at least allow it to + # be placed in a castle, which would require some additions to the rules when + # use_bat_logic is true + if not use_bat_logic: + set_rule(world.get_entrance("WhiteCastleSecretPassage", self.player), + lambda state: state.has("Bridge", self.player)) + set_rule(world.get_entrance("WhiteCastlePeekPassage", self.player), + lambda state: state.has("Bridge", self.player) or + state.has("Magnet", self.player)) + set_rule(world.get_entrance("BlackCastleVaultEntrance", self.player), + lambda state: state.has("Bridge", self.player) or + state.has("Magnet", self.player)) + + dragon_slay_check = world.dragon_slay_check[self.player].value + if dragon_slay_check: + if self.difficulty_switch_b == DifficultySwitchB.option_hard_with_unlock_item: + set_rule(world.get_location("Slay Yorgle", self.player), + lambda state: state.has("Sword", self.player) and + state.has("Right Difficulty Switch", self.player)) + set_rule(world.get_location("Slay Grundle", self.player), + lambda state: state.has("Sword", self.player) and + state.has("Right Difficulty Switch", self.player)) + set_rule(world.get_location("Slay Rhindle", self.player), + lambda state: state.has("Sword", self.player) and + state.has("Right Difficulty Switch", self.player)) + else: + set_rule(world.get_location("Slay Yorgle", self.player), + lambda state: state.has("Sword", self.player)) + set_rule(world.get_location("Slay Grundle", self.player), + lambda state: state.has("Sword", self.player)) + set_rule(world.get_location("Slay Rhindle", self.player), + lambda state: state.has("Sword", self.player)) + + # really this requires getting the dot item, and having another item or enemy + # in the room, but the dot would be *super evil* + # to actually make randomized, since it is invisible. May add some options + # for how that works in the distant future, but for now, just say you need + # the bridge and black key to get to it, as that simplifies things a lot + set_rule(world.get_entrance("CreditsWall", self.player), + lambda state: state.has("Bridge", self.player) and + state.has("Black Key", self.player)) + + if not use_bat_logic: + set_rule(world.get_entrance("CreditsToFarSide", self.player), + lambda state: state.has("Magnet", self.player)) + + # bridge literally does not fit in this space, I think. I'll just exclude it + forbid_item(world.get_location("Dungeon Vault", self.player), "Bridge", self.player) + # don't put magnet in locations that can pull in-logic items out of reach unless the bat is in play + if not use_bat_logic: + forbid_item(world.get_location("Dungeon Vault", self.player), "Magnet", self.player) + forbid_item(world.get_location("Red Maze Vault Entrance", self.player), "Magnet", self.player) + forbid_item(world.get_location("Credits Right Side", self.player), "Magnet", self.player) + + # and obviously we don't want to start with the game already won + forbid_item(world.get_location("Inside Yellow Castle", self.player), "Chalice", self.player) + overworld = world.get_region("Overworld", self.player) + + for loc in overworld.locations: + forbid_item(loc, "Chalice", self.player) + + add_rule(world.get_location("Chalice Home", self.player), + lambda state: state.has("Chalice", self.player) and state.has("Yellow Key", self.player)) + + # world.random.choice(overworld.locations).progress_type = LocationProgressType.PRIORITY + + # all_locations = world.get_locations(self.player).copy() + # while priority_count < get_num_items(): + # loc = world.random.choice(all_locations) + # if loc.progress_type == LocationProgressType.DEFAULT: + # loc.progress_type = LocationProgressType.PRIORITY + # priority_count += 1 + # all_locations.remove(loc) + + # TODO: Add events for dragon_slay_check and trap_bat_check. Here? Elsewhere? + # if self.dragon_slay_check == 1: + # TODO - Randomize bat and dragon start rooms and use those to determine rules + # TODO - for the requirements for the slay event (since we have to get to the + # TODO - dragons and sword to kill them). Unless the dragons are set to be items, + # TODO - which might be a funny option, then they can just be randoed like normal + # TODO - just forbidden from the vaults and all credits room locations diff --git a/worlds/adventure/__init__.py b/worlds/adventure/__init__.py new file mode 100644 index 0000000000..b9d9d5f13c --- /dev/null +++ b/worlds/adventure/__init__.py @@ -0,0 +1,391 @@ +import base64 +import copy +import itertools +import math +import os +from enum import IntFlag +from typing import Any, ClassVar, Dict, List, Optional, Set, Tuple + +from BaseClasses import Entrance, Item, ItemClassification, MultiWorld, Region, Tutorial, \ + LocationProgressType +from Main import __version__ +from Options import AssembleOptions +from worlds.AutoWorld import WebWorld, World +from Fill import fill_restrictive +from worlds.generic.Rules import add_rule, set_rule +from .Options import adventure_option_definitions, DragonRandoType, DifficultySwitchA, DifficultySwitchB +from .Rom import get_base_rom_bytes, get_base_rom_path, AdventureDeltaPatch, apply_basepatch, \ + AdventureAutoCollectLocation +from .Items import item_table, ItemData, nothing_item_id, event_table, AdventureItem, standard_item_max +from .Locations import location_table, base_location_id, LocationData, get_random_room_in_regions +from .Offsets import static_item_data_location, items_ram_start, static_item_element_size, item_position_table, \ + static_first_dragon_index, connector_port_offset, yorgle_speed_data_location, grundle_speed_data_location, \ + rhindle_speed_data_location, item_ram_addresses, start_castle_values, start_castle_offset +from .Regions import create_regions +from .Rules import set_rules + + +from worlds.LauncherComponents import Component, components, SuffixIdentifier + +# Adventure +components.append(Component('Adventure Client', 'AdventureClient', file_identifier=SuffixIdentifier('.apadvn'))) + + +class AdventureWeb(WebWorld): + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to setting up Adventure for MultiWorld.", + "English", + "setup_en.md", + "setup/en", + ["JusticePS"] + )] + theme = "dirt" + + +def get_item_position_data_start(table_index: int): + item_ram_address = item_ram_addresses[table_index]; + return item_position_table + item_ram_address - items_ram_start + + +class AdventureWorld(World): + """ + Adventure for the Atari 2600 is an early graphical adventure game. + Find the enchanted chalice and return it to the yellow castle, + using magic items to enter hidden rooms, retrieve out of + reach items, or defeat the three dragons. Beware the bat + who likes to steal your equipment! + """ + game: ClassVar[str] = "Adventure" + web: ClassVar[WebWorld] = AdventureWeb() + + option_definitions: ClassVar[Dict[str, AssembleOptions]] = adventure_option_definitions + item_name_to_id: ClassVar[Dict[str, int]] = {name: data.id for name, data in item_table.items()} + location_name_to_id: ClassVar[Dict[str, int]] = {name: data.location_id for name, data in location_table.items()} + data_version: ClassVar[int] = 1 + required_client_version: Tuple[int, int, int] = (0, 3, 9) + + def __init__(self, world: MultiWorld, player: int): + super().__init__(world, player) + self.rom_name: Optional[bytearray] = bytearray("", "utf8" ) + self.dragon_rooms: [int] = [0x14, 0x19, 0x4] + self.dragon_slay_check: Optional[int] = 0 + self.connector_multi_slot: Optional[int] = 0 + self.dragon_rando_type: Optional[int] = 0 + self.yorgle_speed: Optional[int] = 2 + self.yorgle_min_speed: Optional[int] = 2 + self.grundle_speed: Optional[int] = 2 + self.grundle_min_speed: Optional[int] = 2 + self.rhindle_speed: Optional[int] = 3 + self.rhindle_min_speed: Optional[int] = 3 + self.difficulty_switch_a: Optional[int] = 0 + self.difficulty_switch_b: Optional[int] = 0 + self.start_castle: Optional[int] = 0 + # dict of item names -> list of speed deltas + self.dragon_speed_reducer_info: {} = {} + self.created_items: int = 0 + + @classmethod + def stage_assert_generate(cls, _multiworld: MultiWorld) -> None: + # don't need rom anymore + pass + + def place_random_dragon(self, dragon_index: int): + region_list = ["Overworld", "YellowCastle", "BlackCastle", "WhiteCastle"] + self.dragon_rooms[dragon_index] = get_random_room_in_regions(region_list, self.multiworld.random) + + def generate_early(self) -> None: + self.rom_name = \ + bytearray(f"ADVENTURE{__version__.replace('.', '')[:3]}_{self.player}_{self.multiworld.seed}", "utf8")[:21] + self.rom_name.extend([0] * (21 - len(self.rom_name))) + + self.dragon_rando_type = self.multiworld.dragon_rando_type[self.player].value + self.dragon_slay_check = self.multiworld.dragon_slay_check[self.player].value + self.connector_multi_slot = self.multiworld.connector_multi_slot[self.player].value + self.yorgle_speed = self.multiworld.yorgle_speed[self.player].value + self.yorgle_min_speed = self.multiworld.yorgle_min_speed[self.player].value + self.grundle_speed = self.multiworld.grundle_speed[self.player].value + self.grundle_min_speed = self.multiworld.grundle_min_speed[self.player].value + self.rhindle_speed = self.multiworld.rhindle_speed[self.player].value + self.rhindle_min_speed = self.multiworld.rhindle_min_speed[self.player].value + self.difficulty_switch_a = self.multiworld.difficulty_switch_a[self.player].value + self.difficulty_switch_b = self.multiworld.difficulty_switch_b[self.player].value + self.start_castle = self.multiworld.start_castle[self.player].value + self.created_items = 0 + + if self.dragon_slay_check == 0: + item_table["Sword"].classification = ItemClassification.useful + else: + item_table["Sword"].classification = ItemClassification.progression + if self.difficulty_switch_b == DifficultySwitchB.option_hard_with_unlock_item: + item_table["Right Difficulty Switch"].classification = ItemClassification.progression + + if self.dragon_rando_type == DragonRandoType.option_shuffle: + self.multiworld.random.shuffle(self.dragon_rooms) + elif self.dragon_rando_type == DragonRandoType.option_overworldplus: + dragon_indices = [0, 1, 2] + overworld_forced_index = self.multiworld.random.choice(dragon_indices) + dragon_indices.remove(overworld_forced_index) + region_list = ["Overworld"] + self.dragon_rooms[overworld_forced_index] = get_random_room_in_regions(region_list, self.multiworld.random) + self.place_random_dragon(dragon_indices[0]) + self.place_random_dragon(dragon_indices[1]) + elif self.dragon_rando_type == DragonRandoType.option_randomized: + self.place_random_dragon(0) + self.place_random_dragon(1) + self.place_random_dragon(2) + + def create_items(self) -> None: + for event in map(self.create_item, event_table): + self.multiworld.itempool.append(event) + exclude = [item for item in self.multiworld.precollected_items[self.player]] + self.created_items = 0 + for item in map(self.create_item, item_table): + if item.code == nothing_item_id: + continue + if item in exclude and item.code <= standard_item_max: + exclude.remove(item) # this is destructive. create unique list above + else: + if item.code <= standard_item_max: + self.multiworld.itempool.append(item) + self.created_items += 1 + num_locations = len(location_table) - 1 # subtract out the chalice location + if self.dragon_slay_check == 0: + num_locations -= 3 + + if self.difficulty_switch_a == DifficultySwitchA.option_hard_with_unlock_item: + self.multiworld.itempool.append(self.create_item("Left Difficulty Switch")) + self.created_items += 1 + if self.difficulty_switch_b == DifficultySwitchB.option_hard_with_unlock_item: + self.multiworld.itempool.append(self.create_item("Right Difficulty Switch")) + self.created_items += 1 + + extra_filler_count = num_locations - self.created_items + self.dragon_speed_reducer_info = {} + # make sure yorgle doesn't take 2 if there's not enough for the others to get at least one + if extra_filler_count <= 4: + extra_filler_count = 1 + self.create_dragon_slow_items(self.yorgle_min_speed, self.yorgle_speed, "Slow Yorgle", extra_filler_count) + extra_filler_count = num_locations - self.created_items + + if extra_filler_count <= 3: + extra_filler_count = 1 + self.create_dragon_slow_items(self.grundle_min_speed, self.grundle_speed, "Slow Grundle", extra_filler_count) + extra_filler_count = num_locations - self.created_items + + self.create_dragon_slow_items(self.rhindle_min_speed, self.rhindle_speed, "Slow Rhindle", extra_filler_count) + extra_filler_count = num_locations - self.created_items + + # traps would probably go here, if enabled + freeincarnate_max = self.multiworld.freeincarnate_max[self.player].value + actual_freeincarnates = min(extra_filler_count, freeincarnate_max) + self.multiworld.itempool += [self.create_item("Freeincarnate") for _ in range(actual_freeincarnates)] + self.created_items += actual_freeincarnates + + def create_dragon_slow_items(self, min_speed: int, speed: int, item_name: str, maximum_items: int): + if min_speed < speed: + delta = speed - min_speed + if delta > 2 and maximum_items >= 2: + self.multiworld.itempool.append(self.create_item(item_name)) + self.multiworld.itempool.append(self.create_item(item_name)) + speed_with_one = speed - math.floor(delta / 2) + self.dragon_speed_reducer_info[item_table[item_name].id] = [speed_with_one, min_speed] + self.created_items += 2 + elif maximum_items >= 1: + self.multiworld.itempool.append(self.create_item(item_name)) + self.dragon_speed_reducer_info[item_table[item_name].id] = [min_speed] + self.created_items += 1 + + def create_regions(self) -> None: + create_regions(self.multiworld, self.player, self.dragon_rooms) + + set_rules = set_rules + + def generate_basic(self) -> None: + self.multiworld.get_location("Chalice Home", self.player).place_locked_item( + self.create_event("Victory", ItemClassification.progression)) + self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) + + def pre_fill(self): + # Place empty items in filler locations here, to limit + # the number of exported empty items and the density of stuff in overworld. + max_location_count = len(location_table) - 1 + if self.dragon_slay_check == 0: + max_location_count -= 3 + + force_empty_item_count = (max_location_count - self.created_items) + if force_empty_item_count <= 0: + return + overworld = self.multiworld.get_region("Overworld", self.player) + overworld_locations_copy = overworld.locations.copy() + all_locations = self.multiworld.get_locations(self.player) + + locations_copy = all_locations.copy() + for loc in all_locations: + if loc.item is not None or loc.progress_type != LocationProgressType.DEFAULT: + locations_copy.remove(loc) + if loc in overworld_locations_copy: + overworld_locations_copy.remove(loc) + + # guarantee at least one overworld location, so we can for sure get a key somewhere + # if too much stuff is plando'd though, just let it go + if len(overworld_locations_copy) >= 3: + saved_overworld_loc = self.multiworld.random.choice(overworld_locations_copy) + locations_copy.remove(saved_overworld_loc) + overworld_locations_copy.remove(saved_overworld_loc) + + # if we have few items, enforce another overworld slot, fill a hard slot, and ensure we have + # at least one hard slot available + if self.created_items < 15: + hard_locations = [] + for loc in locations_copy: + if "Vault" in loc.name or "Credits" in loc.name: + hard_locations.append(loc) + force_empty_item_count -= 1 + loc = self.multiworld.random.choice(hard_locations) + loc.place_locked_item(self.create_item('nothing')) + hard_locations.remove(loc) + locations_copy.remove(loc) + + loc = self.multiworld.random.choice(hard_locations) + locations_copy.remove(loc) + hard_locations.remove(loc) + + saved_overworld_loc = self.multiworld.random.choice(overworld_locations_copy) + locations_copy.remove(saved_overworld_loc) + overworld_locations_copy.remove(saved_overworld_loc) + + # if we have very few items, fill another two difficult slots + if self.created_items < 10: + for i in range(2): + force_empty_item_count -= 1 + loc = self.multiworld.random.choice(hard_locations) + loc.place_locked_item(self.create_item('nothing')) + hard_locations.remove(loc) + locations_copy.remove(loc) + + # for the absolute minimum number of items, enforce a third overworld slot + if self.created_items <= 7: + saved_overworld_loc = self.multiworld.random.choice(overworld_locations_copy) + locations_copy.remove(saved_overworld_loc) + overworld_locations_copy.remove(saved_overworld_loc) + + # finally, place nothing items + while force_empty_item_count > 0 and locations_copy: + force_empty_item_count -= 1 + # prefer somewhat to thin out the overworld. + if len(overworld_locations_copy) > 0 and self.multiworld.random.randint(0, 10) < 4: + loc = self.multiworld.random.choice(overworld_locations_copy) + else: + loc = self.multiworld.random.choice(locations_copy) + loc.place_locked_item(self.create_item('nothing')) + locations_copy.remove(loc) + if loc in overworld_locations_copy: + overworld_locations_copy.remove(loc) + + def place_dragons(self, rom_deltas: {int, int}): + for i in range(3): + table_index = static_first_dragon_index + i + item_position_data_start = get_item_position_data_start(table_index) + rom_deltas[item_position_data_start] = self.dragon_rooms[i] + + def set_dragon_speeds(self, rom_deltas: {int, int}): + rom_deltas[yorgle_speed_data_location] = self.yorgle_speed + rom_deltas[grundle_speed_data_location] = self.grundle_speed + rom_deltas[rhindle_speed_data_location] = self.rhindle_speed + + def set_start_castle(self, rom_deltas): + start_castle_value = start_castle_values[self.start_castle] + rom_deltas[start_castle_offset] = start_castle_value + + def generate_output(self, output_directory: str) -> None: + rom_path: str = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.bin") + foreign_item_locations: [LocationData] = [] + auto_collect_locations: [AdventureAutoCollectLocation] = [] + local_item_to_location: {int, int} = {} + bat_no_touch_locs: [LocationData] = [] + bat_logic: int = self.multiworld.bat_logic[self.player].value + try: + rom_deltas: { int, int } = {} + self.place_dragons(rom_deltas) + self.set_dragon_speeds(rom_deltas) + self.set_start_castle(rom_deltas) + # start and stop indices are offsets in the ROM file, not Adventure ROM addresses (which start at f000) + + # This places the local items (I still need to make it easy to inject the offset data) + unplaced_local_items = dict(filter(lambda x: nothing_item_id < x[1].id <= standard_item_max, + item_table.items())) + for location in self.multiworld.get_locations(self.player): + # 'nothing' items, which are autocollected when the room is entered + if location.item.player == self.player and \ + location.item.name == "nothing": + location_data = location_table[location.name] + auto_collect_locations.append(AdventureAutoCollectLocation(location_data.short_location_id, + location_data.room_id)) + # standard Adventure items, which are placed in the rom + elif location.item.player == self.player and \ + location.item.name != "nothing" and \ + location.item.code is not None and \ + location.item.code <= standard_item_max: + # I need many of the intermediate values here. + item_table_offset = item_table[location.item.name].table_index * static_item_element_size + item_ram_address = item_ram_addresses[item_table[location.item.name].table_index] + item_position_data_start = item_position_table + item_ram_address - items_ram_start + location_data = location_table[location.name] + room_x, room_y = location_data.get_position(self.multiworld.per_slot_randoms[self.player]) + if location_data.needs_bat_logic and bat_logic == 0x0: + copied_location = copy.copy(location_data) + copied_location.local_item = item_ram_address + bat_no_touch_locs.append(copied_location) + del unplaced_local_items[location.item.name] + + rom_deltas[item_position_data_start] = location_data.room_id + rom_deltas[item_position_data_start + 1] = room_x + rom_deltas[item_position_data_start + 2] = room_y + local_item_to_location[item_table_offset] = self.location_name_to_id[location.name] \ + - base_location_id + # items from other worlds, and non-standard Adventure items handled by script, like difficulty switches + elif location.item.code is not None: + if location.item.code != nothing_item_id: + location_data = location_table[location.name] + foreign_item_locations.append(location_data) + if location_data.needs_bat_logic and bat_logic == 0x0: + bat_no_touch_locs.append(location_data) + else: + location_data = location_table[location.name] + auto_collect_locations.append(AdventureAutoCollectLocation(location_data.short_location_id, + location_data.room_id)) + # Adventure items that are in another world get put in an invalid room until needed + for unplaced_item_name, unplaced_item in unplaced_local_items.items(): + item_position_data_start = get_item_position_data_start(unplaced_item.table_index) + rom_deltas[item_position_data_start] = 0xff + + if self.multiworld.connector_multi_slot[self.player].value: + rom_deltas[connector_port_offset] = (self.player & 0xff) + else: + rom_deltas[connector_port_offset] = 0 + except Exception as e: + raise e + else: + patch = AdventureDeltaPatch(os.path.splitext(rom_path)[0] + AdventureDeltaPatch.patch_file_ending, + player=self.player, player_name=self.multiworld.player_name[self.player], + locations=foreign_item_locations, + autocollect=auto_collect_locations, local_item_locations=local_item_to_location, + dragon_speed_reducer_info=self.dragon_speed_reducer_info, + diff_a_mode=self.difficulty_switch_a, diff_b_mode=self.difficulty_switch_b, + bat_logic=bat_logic, bat_no_touch_locations=bat_no_touch_locs, + rom_deltas=rom_deltas, + seed_name=bytes(self.multiworld.seed_name, encoding="ascii")) + patch.write() + finally: + if os.path.exists(rom_path): + os.unlink(rom_path) + + # end of ordered Main.py calls + + def create_item(self, name: str) -> Item: + item_data: ItemData = item_table.get(name) + return AdventureItem(name, item_data.classification, item_data.id, self.player) + + def create_event(self, name: str, classification: ItemClassification) -> Item: + return AdventureItem(name, classification, None, self.player) diff --git a/worlds/adventure/docs/en_Adventure.md b/worlds/adventure/docs/en_Adventure.md new file mode 100644 index 0000000000..c39e0f7d91 --- /dev/null +++ b/worlds/adventure/docs/en_Adventure.md @@ -0,0 +1,62 @@ +# Adventure + +## Where is the settings page? +The [player settings page for Adventure](../player-settings) contains all the options you need to configure and export a config file. + +## What does randomization do to this game? +Adventure items may be distributed into additional locations not possible in the vanilla Adventure randomizer. All +Adventure items are added to the multiworld item pool. Depending on the settings, dragon locations may be randomized, +slaying dragons may award items, difficulty switches may require items to unlock, and limited use 'freeincarnates' +can allow reincarnation without resurrecting dragons. Dragon speeds may also be randomized, and items may exist +to reduce their speeds. + +## What is the goal of Adventure when randomized? +Same as vanilla; Find the Enchanted Chalice and return it to the Yellow Castle + +## Which items can be in another player's world? +All three keys, the chalice, the sword, the magnet, and the bridge can be found in another player's world. Depending on +settings, dragon slowdowns, difficulty switch unlocks, and freeincarnates may also be found. + +## What is considered a location check in Adventure? +Most areas in Adventure have one or more locations which can contain an Adventure item or an Archipelago item. +A few rooms have two potential locaions. If the location contains a 'nothing' Adventure item, it will send a check when +that is seen. If it contains an item from another Adventure or other game, it will show a rough approximation of the +Archipelago logo that can be touched for a check. Touching a local Adventure item also 'checks' it, allowing it to be +retrieved after a select-reset or hard reset. + +## Why isn't my item where the spoiler says it should be? +If something isn't where the spoiler says, most likely the bat carried it somewhere else. The bat's ability to shuffle +items around makes it somewhat unique in Archipelago. Touching the item, wherever it is, will award the location check +for wherever the item was originally placed. + +## Which notable items are not randomized? +The bat, dot, and map are not yet randomized. If the chalice is local, it is randomized, but is always in either a +castle or the credits screen. Forcing the chalice local in the yaml is recommended. + +## What does another world's item look like in Adventure? +It looks vaguely like a flashing Archipelago logo. + +## When the player receives an item, what happens? +A message is shown in the client log. While empty handed, the player can press the fire button to retrieve items in the +order they were received. Once an item is retrieved this way, it cannot be retrieved again until pressing select to +return to the 'GO' screen or doing a hard reset, either one of which will reset all items to their original positions. + +## What are recommended settings to tweak for beginners to the rando? +Setting difficulty_switch_a and lowering the dragons' speeds makes the dragons easier to avoid. Adding Chalice to +local_items guarantees you'll visit at least one of the interesting castles, as it can only be placed in a castle or +the credits room. + +## My yellow key is stuck in a wall! Am I softlocked? +Maybe! That's all part of Adventure. If you have access to the magnet, bridge, or bat, you might be able to retrieve +it. In general, since the bat always starts outside of castles, you should always be able to find it unless you lock +it in a castle yourself. This mod's inventory system allows you to quickly recover all the items +you've collected after a hard reset or select-reset (except for the dot), so usually it's not as bad as in vanilla. + +## How do I get into the credits room? There's a item I need in there. +Searching for 'Adventure dot map' should bring up an AtariAge map with a good walkthrough, but here's the basics. +Bring the bridge into the black castle. Find the small room in the dungeon that cannot be reached without the bridge, +enter it, and push yourself into the bottom right corner to pick up the dot. The dot color matches the background, +so you won't be able to see it if it isn't in a wall, so be careful not to drop it. Bring it to the room one south and +one east of the yellow castle and drop it there. Bring 2-3 more objects (the bat and dragons also count for this) until +it lets you walk through the right wall. +If the item is on the right side, you'll need the magnet to get it. \ No newline at end of file diff --git a/worlds/adventure/docs/setup_en.md b/worlds/adventure/docs/setup_en.md new file mode 100644 index 0000000000..658162d8c2 --- /dev/null +++ b/worlds/adventure/docs/setup_en.md @@ -0,0 +1,75 @@ +# Setup Guide for Adventure: Archipelago + +## Important + +As we are using Bizhawk, this guide is only applicable to Windows and Linux systems. + +## Required Software + +- Bizhawk: [Bizhawk Releases from TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory) + - Version 2.3.1 and later are supported. Version 2.7 is recommended for stability. + - Detailed installation instructions for Bizhawk can be found at the above link. + - Windows users must run the prereq installer first, which can also be found at the above link. +- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases) + (select `Adventure Client` during installation). +- An Adventure NTSC ROM file. The Archipelago community cannot provide these. + +## Configuring Bizhawk + +Once Bizhawk has been installed, open Bizhawk and change the following settings: + +- Go to Config > Customize. Switch to the Advanced tab, then switch the Lua Core from "NLua+KopiLua" to + "Lua+LuaInterface". Then restart Bizhawk. This is required for the Lua script to function correctly. + **NOTE: Even if "Lua+LuaInterface" is already selected, toggle between the two options and reselect it. Fresh installs** + **of newer versions of Bizhawk have a tendency to show "Lua+LuaInterface" as the default selected option but still load** + **"NLua+KopiLua" until this step is done.** +- Under Config > Customize, check the "Run in background" box. This will prevent disconnecting from the client while +BizHawk is running in the background. + +- It is recommended that you provide a path to BizHawk in your host.yaml for Adventure so the client can start it automatically +- At the same time, you can set an option to automatically load the adventure_connector.lua script when launching BizHawk +from AdventureClient. +Default Windows install example: +```rom_args: "--lua=C:/ProgramData/Archipelago/data/lua/ADVENTURE/adventure_connector.lua"``` + +## Configuring your YAML file + +### What is a YAML file and why do I need one? + +Your YAML file contains a set of configuration options which provide the generator with information about how it should +generate your game. Each player of a multiworld will provide their own YAML file. This setup allows each player to enjoy +an experience customized for their taste, and different players in the same multiworld can all have different options. + +### Where do I get a YAML file? + +You can generate a yaml or download a template by visiting the [Adventure Settings Page](/games/Adventure/player-settings) + +### What are recommended settings to tweak for beginners to the rando? +Setting difficulty_switch_a and lowering the dragons' speeds makes the dragons easier to avoid. Adding Chalice to +local_items guarantees you'll visit at least one of the interesting castles, as it can only be placed in a castle or +the credits room. + +## Joining a MultiWorld Game + +### Obtain your Adventure patch file + +When you join a multiworld game, you will be asked to provide your YAML file to whoever is hosting. Once that is done, +the host will provide you with either a link to download your data file, or with a zip file containing everyone's data +files. Your data file should have a `.apadvn` extension. + +Drag your patch file to the AdventureClient.exe to start your client and start the ROM patch process. Once the process +is finished (this can take a while), the client and the emulator will be started automatically (if you set the emulator +path as recommended). + +### Connect to the Multiserver + +Once both the client and the emulator are started, you must connect them. Within the emulator click on the "Tools" +menu and select "Lua Console". Click the folder button or press Ctrl+O to open a Lua script. + +Navigate to your Archipelago install folder and open `data/lua/ADVENTURE/adventure_connector.lua`, if it is not +configured to do this automatically. + +To connect the client to the multiserver simply put `
:` on the textfield on top and press enter (if the +server uses password, type in the bottom textfield `/connect
: [password]`) + +Press Reset and begin playing diff --git a/worlds/alttp/Bosses.py b/worlds/alttp/Bosses.py index 5f915a3342..51615ddc45 100644 --- a/worlds/alttp/Bosses.py +++ b/worlds/alttp/Bosses.py @@ -4,7 +4,7 @@ from typing import Optional, Union, List, Tuple, Callable, Dict from BaseClasses import Boss from Fill import FillError from .Options import LTTPBosses as Bosses - +from .StateHelpers import can_shoot_arrows, can_extend_magic, can_get_good_bee, has_sword, has_beam_sword, has_melee_weapon, has_fire_source def BossFactory(boss: str, player: int) -> Optional[Boss]: if boss in boss_table: @@ -16,33 +16,33 @@ def BossFactory(boss: str, player: int) -> Optional[Boss]: def ArmosKnightsDefeatRule(state, player: int) -> bool: # Magic amounts are probably a bit overkill return ( - state.has_melee_weapon(player) or - state.can_shoot_arrows(player) or - (state.has('Cane of Somaria', player) and state.can_extend_magic(player, 10)) or - (state.has('Cane of Byrna', player) and state.can_extend_magic(player, 16)) or - (state.has('Ice Rod', player) and state.can_extend_magic(player, 32)) or - (state.has('Fire Rod', player) and state.can_extend_magic(player, 32)) or + has_melee_weapon(state, player) or + can_shoot_arrows(state, player) or + (state.has('Cane of Somaria', player) and can_extend_magic(state, player, 10)) or + (state.has('Cane of Byrna', player) and can_extend_magic(state, player, 16)) or + (state.has('Ice Rod', player) and can_extend_magic(state, player, 32)) or + (state.has('Fire Rod', player) and can_extend_magic(state, player, 32)) or state.has('Blue Boomerang', player) or state.has('Red Boomerang', player)) def LanmolasDefeatRule(state, player: int) -> bool: return ( - state.has_melee_weapon(player) or + has_melee_weapon(state, player) or state.has('Fire Rod', player) or state.has('Ice Rod', player) or state.has('Cane of Somaria', player) or state.has('Cane of Byrna', player) or - state.can_shoot_arrows(player)) + can_shoot_arrows(state, player)) def MoldormDefeatRule(state, player: int) -> bool: - return state.has_melee_weapon(player) + return has_melee_weapon(state, player) def HelmasaurKingDefeatRule(state, player: int) -> bool: # TODO: technically possible with the hammer - return state.has_sword(player) or state.can_shoot_arrows(player) + return has_sword(state, player) or can_shoot_arrows(state, player) def ArrghusDefeatRule(state, player: int) -> bool: @@ -51,28 +51,28 @@ def ArrghusDefeatRule(state, player: int) -> bool: # TODO: ideally we would have a check for bow and silvers, which combined with the # hookshot is enough. This is not coded yet because the silvers that only work in pyramid feature # makes this complicated - if state.has_melee_weapon(player): + if has_melee_weapon(state, player): return True - return ((state.has('Fire Rod', player) and (state.can_shoot_arrows(player) or state.can_extend_magic(player, + return ((state.has('Fire Rod', player) and (can_shoot_arrows(state, player) or can_extend_magic(state, player, 12))) or # assuming mostly gitting two puff with one shot - (state.has('Ice Rod', player) and (state.can_shoot_arrows(player) or state.can_extend_magic(player, 16)))) + (state.has('Ice Rod', player) and (can_shoot_arrows(state, player) or can_extend_magic(state, player, 16)))) def MothulaDefeatRule(state, player: int) -> bool: return ( - state.has_melee_weapon(player) or - (state.has('Fire Rod', player) and state.can_extend_magic(player, 10)) or + has_melee_weapon(state, player) or + (state.has('Fire Rod', player) and can_extend_magic(state, player, 10)) or # TODO: Not sure how much (if any) extend magic is needed for these two, since they only apply # to non-vanilla locations, so are harder to test, so sticking with what VT has for now: - (state.has('Cane of Somaria', player) and state.can_extend_magic(player, 16)) or - (state.has('Cane of Byrna', player) and state.can_extend_magic(player, 16)) or - state.can_get_good_bee(player) + (state.has('Cane of Somaria', player) and can_extend_magic(state, player, 16)) or + (state.has('Cane of Byrna', player) and can_extend_magic(state, player, 16)) or + can_get_good_bee(state, player) ) def BlindDefeatRule(state, player: int) -> bool: - return state.has_melee_weapon(player) or state.has('Cane of Somaria', player) or state.has('Cane of Byrna', player) + return has_melee_weapon(state, player) or state.has('Cane of Somaria', player) or state.has('Cane of Byrna', player) def KholdstareDefeatRule(state, player: int) -> bool: @@ -81,56 +81,56 @@ def KholdstareDefeatRule(state, player: int) -> bool: state.has('Fire Rod', player) or ( state.has('Bombos', player) and - (state.has_sword(player) or state.multiworld.swordless[player]) + (has_sword(state, player) or state.multiworld.swordless[player]) ) ) and ( - state.has_melee_weapon(player) or - (state.has('Fire Rod', player) and state.can_extend_magic(player, 20)) or + has_melee_weapon(state, player) or + (state.has('Fire Rod', player) and can_extend_magic(state, player, 20)) or ( state.has('Fire Rod', player) and state.has('Bombos', player) and state.multiworld.swordless[player] and - state.can_extend_magic(player, 16) + can_extend_magic(state, player, 16) ) ) ) def VitreousDefeatRule(state, player: int) -> bool: - return state.can_shoot_arrows(player) or state.has_melee_weapon(player) + return can_shoot_arrows(state, player) or has_melee_weapon(state, player) def TrinexxDefeatRule(state, player: int) -> bool: if not (state.has('Fire Rod', player) and state.has('Ice Rod', player)): return False return state.has('Hammer', player) or state.has('Tempered Sword', player) or state.has('Golden Sword', player) or \ - (state.has('Master Sword', player) and state.can_extend_magic(player, 16)) or \ - (state.has_sword(player) and state.can_extend_magic(player, 32)) + (state.has('Master Sword', player) and can_extend_magic(state, player, 16)) or \ + (has_sword(state, player) and can_extend_magic(state, player, 32)) def AgahnimDefeatRule(state, player: int) -> bool: - return state.has_sword(player) or state.has('Hammer', player) or state.has('Bug Catching Net', player) + return has_sword(state, player) or state.has('Hammer', player) or state.has('Bug Catching Net', player) def GanonDefeatRule(state, player: int) -> bool: if state.multiworld.swordless[player]: return state.has('Hammer', player) and \ - state.has_fire_source(player) and \ + has_fire_source(state, player) and \ state.has('Silver Bow', player) and \ - state.can_shoot_arrows(player) + can_shoot_arrows(state, player) - can_hurt = state.has_beam_sword(player) - common = can_hurt and state.has_fire_source(player) + can_hurt = has_beam_sword(state, player) + common = can_hurt and has_fire_source(state, player) # silverless ganon may be needed in anything higher than no glitches if state.multiworld.logic[player] != 'noglitches': # need to light torch a sufficient amount of times return common and (state.has('Tempered Sword', player) or state.has('Golden Sword', player) or ( - state.has('Silver Bow', player) and state.can_shoot_arrows(player)) or - state.has('Lamp', player) or state.can_extend_magic(player, 12)) + state.has('Silver Bow', player) and can_shoot_arrows(state, player)) or + state.has('Lamp', player) or can_extend_magic(state, player, 12)) else: - return common and state.has('Silver Bow', player) and state.can_shoot_arrows(player) + return common and state.has('Silver Bow', player) and can_shoot_arrows(state, player) boss_table: Dict[str, Tuple[str, Optional[Callable]]] = { diff --git a/worlds/alttp/Dungeons.py b/worlds/alttp/Dungeons.py index a37ded8d10..ec6862b9d0 100644 --- a/worlds/alttp/Dungeons.py +++ b/worlds/alttp/Dungeons.py @@ -7,6 +7,8 @@ from worlds.alttp.Items import ItemFactory from worlds.alttp.Regions import lookup_boss_drops from worlds.alttp.Options import smallkey_shuffle +if typing.TYPE_CHECKING: + from .SubClasses import ALttPLocation def create_dungeons(world, player): def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dungeon_items): @@ -138,9 +140,10 @@ def fill_dungeons_restrictive(world): if in_dungeon_items: restricted_players = {player for player, restricted in world.restrict_dungeon_item_on_boss.items() if restricted} - locations = [location for location in get_unfilled_dungeon_locations(world) - # filter boss - if not (location.player in restricted_players and location.name in lookup_boss_drops)] + locations: typing.List["ALttPLocation"] = [ + location for location in get_unfilled_dungeon_locations(world) + # filter boss + if not (location.player in restricted_players and location.name in lookup_boss_drops)] if dungeon_specific: for location in locations: dungeon = location.parent_region.dungeon @@ -159,7 +162,7 @@ def fill_dungeons_restrictive(world): (5 if (item.player, item.name) in dungeon_specific else 0)) for item in in_dungeon_items: all_state_base.remove(item) - fill_restrictive(world, all_state_base, locations, in_dungeon_items, True, True) + fill_restrictive(world, all_state_base, locations, in_dungeon_items, True, True, allow_excluded=True) dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A], diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index f7326092ec..7fd93ab93e 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -9,7 +9,8 @@ from worlds.alttp.Dungeons import get_dungeon_item_pool_player from worlds.alttp.EntranceShuffle import connect_entrance from Fill import FillError from worlds.alttp.Items import ItemFactory, GetBeemizerItem -from worlds.alttp.Options import smallkey_shuffle, compass_shuffle, bigkey_shuffle, map_shuffle +from worlds.alttp.Options import smallkey_shuffle, compass_shuffle, bigkey_shuffle, map_shuffle, LTTPBosses +from .StateHelpers import has_triforce_pieces, has_melee_weapon # This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space. # Some basic items that various modes require are placed here, including pendants and crystals. Medallion requirements for the two relevant entrances are also decided. @@ -249,8 +250,10 @@ def generate_itempool(world): world.push_item(loc, ItemFactory('Triforce Piece', player), False) world.treasure_hunt_count[player] = 1 if world.boss_shuffle[player] != 'none': - if 'turtle rock-' not in world.boss_shuffle[player]: - world.boss_shuffle[player] = f'Turtle Rock-Trinexx;{world.boss_shuffle[player]}' + if isinstance(world.boss_shuffle[player].value, str) and 'turtle rock-' not in world.boss_shuffle[player].value: + world.boss_shuffle[player] = LTTPBosses.from_text(f'Turtle Rock-Trinexx;{world.boss_shuffle[player].current_key}') + elif isinstance(world.boss_shuffle[player].value, int): + world.boss_shuffle[player] = LTTPBosses.from_text(f'Turtle Rock-Trinexx;{world.boss_shuffle[player].current_key}') else: logging.warning(f'Cannot guarantee that Trinexx is the boss of Turtle Rock for player {player}') loc.event = True @@ -286,7 +289,7 @@ def generate_itempool(world): region = world.get_region('Light World', player) loc = ALttPLocation(player, "Murahdahla", parent=region) - loc.access_rule = lambda state: state.has_triforce_pieces(state.multiworld.treasure_hunt_count[player], player) + loc.access_rule = lambda state: has_triforce_pieces(state, player) region.locations.append(loc) world.clear_location_cache() @@ -327,7 +330,7 @@ def generate_itempool(world): for item in precollected_items: world.push_precollected(ItemFactory(item, player)) - if world.mode[player] == 'standard' and not world.state.has_melee_weapon(player): + if world.mode[player] == 'standard' and not has_melee_weapon(world.state, player): if "Link's Uncle" not in placed_items: found_sword = False found_bow = False diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index 90ae118771..992920b177 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -107,10 +107,14 @@ class Crystals(Range): class CrystalsTower(Crystals): + """Number of crystals needed to open Ganon's Tower""" + display_name = "Crystals for GT" default = 7 class CrystalsGanon(Crystals): + """Number of crystals needed to damage Ganon""" + display_name = "Crystals for Ganon" default = 7 @@ -121,12 +125,15 @@ class TriforcePieces(Range): class ShopItemSlots(Range): + """Number of slots in all shops available to have items from the multiworld""" + display_name = "Available Shop Slots" range_start = 0 range_end = 30 class ShopPriceModifier(Range): """Percentage modifier for shuffled item prices in shops""" + display_name = "Shop Price Cost Percent" range_start = 0 default = 100 range_end = 400 @@ -144,7 +151,7 @@ class LTTPBosses(PlandoBosses): Full chooses 3 bosses at random to be placed twice instead of Lanmolas, Moldorm, and Helmasaur. Chaos allows any boss to appear any number of times. Singularity places a single boss in as many places as possible, and a second boss in any remaining locations. - Supports plando placement. Formatting here: https://archipelago.gg/tutorial/A%20Link%20to%20the%20Past/plando/en""" + Supports plando placement.""" display_name = "Boss Shuffle" option_none = 0 option_basic = 1 @@ -202,6 +209,7 @@ class Enemies(Choice): class Progressive(Choice): + """How item types that have multiple tiers (armor, bows, gloves, shields, and swords) should be rewarded""" display_name = "Progressive Items" option_off = 0 option_grouped_random = 1 @@ -305,22 +313,27 @@ class Palette(Choice): class OWPalette(Palette): + """The type of palette shuffle to use for the overworld""" display_name = "Overworld Palette" class UWPalette(Palette): + """The type of palette shuffle to use for the underworld (caves, dungeons, etc.)""" display_name = "Underworld Palette" class HUDPalette(Palette): + """The type of palette shuffle to use for the HUD""" display_name = "Menu Palette" class SwordPalette(Palette): + """The type of palette shuffle to use for the sword""" display_name = "Sword Palette" class ShieldPalette(Palette): + """The type of palette shuffle to use for the shield""" display_name = "Shield Palette" @@ -329,6 +342,7 @@ class ShieldPalette(Palette): class HeartBeep(Choice): + """How quickly the heart beep sound effect will play""" display_name = "Heart Beep Rate" option_normal = 0 option_double = 1 @@ -338,6 +352,7 @@ class HeartBeep(Choice): class HeartColor(Choice): + """The color of hearts in the HUD""" display_name = "Heart Color" option_red = 0 option_blue = 1 @@ -346,10 +361,12 @@ class HeartColor(Choice): class QuickSwap(DefaultOnToggle): + """Allows you to quickly swap items while playing with L/R""" display_name = "L/R Quickswapping" class MenuSpeed(Choice): + """How quickly the menu appears/disappears""" display_name = "Menu Speed" option_normal = 0 option_instant = 1, @@ -360,14 +377,17 @@ class MenuSpeed(Choice): class Music(DefaultOnToggle): + """Whether background music will play in game""" display_name = "Play music" class ReduceFlashing(DefaultOnToggle): + """Reduces flashing for certain scenes such as the Misery Mire and Ganon's Tower opening cutscenes""" display_name = "Reduce Screen Flashes" class TriforceHud(Choice): + """When and how the triforce hunt HUD should display""" display_name = "Display Method for Triforce Hunt" option_normal = 0 option_hide_goal = 1 @@ -375,6 +395,11 @@ class TriforceHud(Choice): option_hide_both = 3 +class GlitchBoots(DefaultOnToggle): + """If this is enabled, the player will start with Pegasus Boots when playing with overworld glitches or harder logic.""" + display_name = "Glitched Starting Boots" + + class BeemizerRange(Range): value: int range_start = 0 @@ -438,7 +463,7 @@ alttp_options: typing.Dict[str, type(Option)] = { "music": Music, "reduceflashing": ReduceFlashing, "triforcehud": TriforceHud, - "glitch_boots": DefaultOnToggle, + "glitch_boots": GlitchBoots, "beemizer_total_chance": BeemizerTotalChance, "beemizer_trap_chance": BeemizerTrapChance, "death_link": DeathLink, diff --git a/worlds/alttp/OverworldGlitchRules.py b/worlds/alttp/OverworldGlitchRules.py index 705db7e7c0..f6c3ec8d14 100644 --- a/worlds/alttp/OverworldGlitchRules.py +++ b/worlds/alttp/OverworldGlitchRules.py @@ -4,6 +4,7 @@ Helper functions to deliver entrance/exit/region sets to OWG rules. from BaseClasses import Entrance +from .StateHelpers import can_lift_heavy_rocks, can_boots_clip_lw, can_boots_clip_dw, can_get_glitched_speed_dw def get_sword_required_superbunny_mirror_regions(): """ @@ -169,7 +170,7 @@ def get_boots_clip_exits_dw(inverted, player): yield ('Ganons Tower Ascent', 'Dark Death Mountain (West Bottom)', 'Dark Death Mountain (Top)') # This only gets you to the GT entrance yield ('Dark Death Mountain Glitched Bridge', 'Dark Death Mountain (West Bottom)', 'Dark Death Mountain (Top)') yield ('Turtle Rock (Top) Clip Spot', 'Dark Death Mountain (Top)', 'Turtle Rock (Top)') - yield ('Ice Palace Clip', 'South Dark World', 'Dark Lake Hylia Central Island', lambda state: state.can_boots_clip_dw(player) and state.has('Flippers', player)) + yield ('Ice Palace Clip', 'South Dark World', 'Dark Lake Hylia Central Island', lambda state: can_boots_clip_dw(state, player) and state.has('Flippers', player)) else: yield ('Dark Desert Teleporter Clip Spot', 'Dark Desert', 'Dark Desert Ledge') @@ -203,7 +204,7 @@ def get_mirror_offset_spots_lw(player): Mirror shenanigans placing a mirror portal with a broken camera """ yield ('Death Mountain Offset Mirror', 'Death Mountain', 'Light World') - yield ('Death Mountain Offset Mirror (Houlihan Exit)', 'Death Mountain', 'Hyrule Castle Ledge', lambda state: state.has('Magic Mirror', player) and state.can_boots_clip_dw(player) and state.has('Moon Pearl', player)) + yield ('Death Mountain Offset Mirror (Houlihan Exit)', 'Death Mountain', 'Hyrule Castle Ledge', lambda state: state.has('Magic Mirror', player) and can_boots_clip_dw(state, player) and state.has('Moon Pearl', player)) @@ -255,11 +256,11 @@ def overworld_glitch_connections(world, player): def overworld_glitches_rules(world, player): # Boots-accessible locations. - set_owg_connection_rules(player, world, get_boots_clip_exits_lw(world.mode[player] == 'inverted'), lambda state: state.can_boots_clip_lw(player)) - set_owg_connection_rules(player, world, get_boots_clip_exits_dw(world.mode[player] == 'inverted', player), lambda state: state.can_boots_clip_dw(player)) + set_owg_connection_rules(player, world, get_boots_clip_exits_lw(world.mode[player] == 'inverted'), lambda state: can_boots_clip_lw(state, player)) + set_owg_connection_rules(player, world, get_boots_clip_exits_dw(world.mode[player] == 'inverted', player), lambda state: can_boots_clip_dw(state, player)) # Glitched speed drops. - set_owg_connection_rules(player, world, get_glitched_speed_drops_dw(world.mode[player] == 'inverted'), lambda state: state.can_get_glitched_speed_dw(player)) + set_owg_connection_rules(player, world, get_glitched_speed_drops_dw(world.mode[player] == 'inverted'), lambda state: can_get_glitched_speed_dw(state, player)) # Dark Death Mountain Ledge Clip Spot also accessible with mirror. if world.mode[player] != 'inverted': add_alternate_rule(world.get_entrance('Dark Death Mountain Ledge Clip Spot', player), lambda state: state.has('Magic Mirror', player)) @@ -267,20 +268,20 @@ def overworld_glitches_rules(world, player): # Mirror clip spots. if world.mode[player] != 'inverted': set_owg_connection_rules(player, world, get_mirror_clip_spots_dw(), lambda state: state.has('Magic Mirror', player)) - set_owg_connection_rules(player, world, get_mirror_offset_spots_dw(), lambda state: state.has('Magic Mirror', player) and state.can_boots_clip_lw(player)) + set_owg_connection_rules(player, world, get_mirror_offset_spots_dw(), lambda state: state.has('Magic Mirror', player) and can_boots_clip_lw(state, player)) else: - set_owg_connection_rules(player, world, get_mirror_offset_spots_lw(player), lambda state: state.has('Magic Mirror', player) and state.can_boots_clip_dw(player)) + set_owg_connection_rules(player, world, get_mirror_offset_spots_lw(player), lambda state: state.has('Magic Mirror', player) and can_boots_clip_dw(state, player)) # Regions that require the boots and some other stuff. if world.mode[player] != 'inverted': - world.get_entrance('Turtle Rock Teleporter', player).access_rule = lambda state: (state.can_boots_clip_lw(player) or state.can_lift_heavy_rocks(player)) and state.has('Hammer', player) + world.get_entrance('Turtle Rock Teleporter', player).access_rule = lambda state: (can_boots_clip_lw(state, player) or can_lift_heavy_rocks(state, player)) and state.has('Hammer', player) add_alternate_rule(world.get_entrance('Waterfall of Wishing', player), lambda state: state.has('Moon Pearl', player) or state.has('Pegasus Boots', player)) else: add_alternate_rule(world.get_entrance('Waterfall of Wishing Cave', player), lambda state: state.has('Moon Pearl', player)) - world.get_entrance('Dark Desert Teleporter', player).access_rule = lambda state: (state.has('Flute', player) or state.has('Pegasus Boots', player)) and state.can_lift_heavy_rocks(player) - add_alternate_rule(world.get_entrance('Catfish Exit Rock', player), lambda state: state.can_boots_clip_dw(player)) - add_alternate_rule(world.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: state.can_boots_clip_dw(player)) + world.get_entrance('Dark Desert Teleporter', player).access_rule = lambda state: (state.has('Flute', player) or state.has('Pegasus Boots', player)) and can_lift_heavy_rocks(state, player) + add_alternate_rule(world.get_entrance('Catfish Exit Rock', player), lambda state: can_boots_clip_dw(state, player)) + add_alternate_rule(world.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: can_boots_clip_dw(state, player)) # Zora's Ledge via waterwalk setup. add_alternate_rule(world.get_location('Zora\'s Ledge', player), lambda state: state.has('Pegasus Boots', player)) diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index 7f863ec2a6..f47672ec59 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -3,17 +3,24 @@ import logging from typing import Iterator, Set from Options import ItemsAccessibility -from worlds.alttp import OverworldGlitchRules -from BaseClasses import MultiWorld, Entrance -from worlds.alttp.Items import ItemFactory, progression_items, item_name_groups, item_table -from worlds.alttp.OverworldGlitchRules import overworld_glitches_rules, no_logic_rules -from worlds.alttp.Regions import location_table -from worlds.alttp.UnderworldGlitchRules import underworld_glitches_rules -from worlds.alttp.Bosses import GanonDefeatRule -from worlds.generic.Rules import set_rule, add_rule, forbid_item, add_item_rule, item_in_locations, \ - item_name -from worlds.alttp.Options import smallkey_shuffle -from worlds.alttp.Regions import LTTPRegionType +from BaseClasses import Entrance, MultiWorld +from worlds.generic.Rules import (add_item_rule, add_rule, forbid_item, + item_in_locations, location_item_name, set_rule, allow_self_locking_items) + +from . import OverworldGlitchRules +from .Bosses import GanonDefeatRule +from .Items import ItemFactory, item_name_groups, item_table, progression_items +from .Options import smallkey_shuffle +from .OverworldGlitchRules import no_logic_rules, overworld_glitches_rules +from .Regions import LTTPRegionType, location_table +from .StateHelpers import (can_extend_magic, can_kill_most_things, + can_lift_heavy_rocks, can_lift_rocks, + can_melt_things, can_retrieve_tablet, + can_shoot_arrows, has_beam_sword, has_crystals, + has_fire_source, has_hearts, + has_misery_mire_medallion, has_sword, has_turtle_rock_medallion, + has_triforce_pieces) +from .UnderworldGlitchRules import underworld_glitches_rules def set_rules(world): @@ -77,7 +84,7 @@ def set_rules(world): if world.goal[player] == 'bosses': # require all bosses to beat ganon - add_rule(world.get_location('Ganon', player), lambda state: state.can_reach('Master Sword Pedestal', 'Location', player) and state.has('Beat Agahnim 1', player) and state.has('Beat Agahnim 2', player) and state.has_crystals(7, player)) + add_rule(world.get_location('Ganon', player), lambda state: state.can_reach('Master Sword Pedestal', 'Location', player) and state.has('Beat Agahnim 1', player) and state.has('Beat Agahnim 2', player) and has_crystals(state, 7, player)) elif world.goal[player] == 'ganon': # require aga2 to beat ganon add_rule(world.get_location('Ganon', player), lambda state: state.has('Beat Agahnim 2', player)) @@ -102,7 +109,7 @@ def set_rules(world): set_trock_key_rules(world, player) - set_rule(ganons_tower, lambda state: state.has_crystals(state.multiworld.crystals_needed_for_gt[player], player)) + set_rule(ganons_tower, lambda state: has_crystals(state, state.multiworld.crystals_needed_for_gt[player], player)) if world.mode[player] != 'inverted' and world.logic[player] in ['owglitches', 'hybridglitches', 'nologic']: add_rule(world.get_entrance('Ganons Tower', player), lambda state: state.multiworld.get_entrance('Ganons Tower Ascent', player).can_reach(state), 'or') @@ -200,7 +207,7 @@ def global_rules(world, player): set_rule(world.get_location('Dark Blacksmith Ruins', player), lambda state: state.has('Return Smith', player)) set_rule(world.get_location('Purple Chest', player), lambda state: state.has('Pick Up Purple Chest', player)) # Can S&Q with chest - set_rule(world.get_location('Ether Tablet', player), lambda state: state.can_retrieve_tablet(player)) + set_rule(world.get_location('Ether Tablet', player), lambda state: can_retrieve_tablet(state, player)) set_rule(world.get_location('Master Sword Pedestal', player), lambda state: state.has('Red Pendant', player) and state.has('Blue Pendant', player) and state.has('Green Pendant', player)) set_rule(world.get_location('Missing Smith', player), lambda state: state.has('Get Frog', player) and state.can_reach('Blacksmiths Hut', 'Region', player)) # Can't S&Q with smith @@ -213,11 +220,11 @@ def global_rules(world, player): set_rule(world.get_location('Spike Cave', player), lambda state: - state.has('Hammer', player) and state.can_lift_rocks(player) and - ((state.has('Cape', player) and state.can_extend_magic(player, 16, True)) or + state.has('Hammer', player) and can_lift_rocks(state, player) and + ((state.has('Cape', player) and can_extend_magic(state, player, 16, True)) or (state.has('Cane of Byrna', player) and - (state.can_extend_magic(player, 12, True) or - (state.multiworld.can_take_damage[player] and (state.has('Pegasus Boots', player) or state.has_hearts(player, 4)))))) + (can_extend_magic(state, player, 12, True) or + (state.multiworld.can_take_damage[player] and (state.has('Pegasus Boots', player) or has_hearts(state, player, 4)))))) ) set_rule(world.get_location('Hookshot Cave - Top Right', player), lambda state: state.has('Hookshot', player)) @@ -233,11 +240,11 @@ def global_rules(world, player): set_rule(world.get_entrance('Sewers Back Door', player), lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player)) set_rule(world.get_entrance('Agahnim 1', player), - lambda state: state.has_sword(player) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2)) + lambda state: has_sword(state, player) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2)) - set_rule(world.get_location('Castle Tower - Room 03', player), lambda state: state.can_kill_most_things(player, 8)) + set_rule(world.get_location('Castle Tower - Room 03', player), lambda state: can_kill_most_things(state, player, 8)) set_rule(world.get_location('Castle Tower - Dark Maze', player), - lambda state: state.can_kill_most_things(player, 8) and state._lttp_has_key('Small Key (Agahnims Tower)', + lambda state: can_kill_most_things(state, player, 8) and state._lttp_has_key('Small Key (Agahnims Tower)', player)) set_rule(world.get_location('Eastern Palace - Big Chest', player), @@ -249,62 +256,62 @@ def global_rules(world, player): set_rule(ep_prize, lambda state: state.has('Big Key (Eastern Palace)', player) and ep_prize.parent_region.dungeon.boss.can_defeat(state)) if not world.enemy_shuffle[player]: - add_rule(ep_boss, lambda state: state.can_shoot_arrows(player)) - add_rule(ep_prize, lambda state: state.can_shoot_arrows(player)) + add_rule(ep_boss, lambda state: can_shoot_arrows(state, player)) + add_rule(ep_prize, lambda state: can_shoot_arrows(state, player)) set_rule(world.get_location('Desert Palace - Big Chest', player), lambda state: state.has('Big Key (Desert Palace)', player)) set_rule(world.get_location('Desert Palace - Torch', player), lambda state: state.has('Pegasus Boots', player)) set_rule(world.get_entrance('Desert Palace East Wing', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player)) - set_rule(world.get_location('Desert Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and state.has_fire_source(player) and state.multiworld.get_location('Desert Palace - Prize', player).parent_region.dungeon.boss.can_defeat(state)) - set_rule(world.get_location('Desert Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and state.has_fire_source(player) and state.multiworld.get_location('Desert Palace - Boss', player).parent_region.dungeon.boss.can_defeat(state)) + set_rule(world.get_location('Desert Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and has_fire_source(state, player) and state.multiworld.get_location('Desert Palace - Prize', player).parent_region.dungeon.boss.can_defeat(state)) + set_rule(world.get_location('Desert Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and has_fire_source(state, player) and state.multiworld.get_location('Desert Palace - Boss', player).parent_region.dungeon.boss.can_defeat(state)) # logic patch to prevent placing a crystal in Desert that's required to reach the required keys if not (world.smallkey_shuffle[player] and world.bigkey_shuffle[player]): add_rule(world.get_location('Desert Palace - Prize', player), lambda state: state.multiworld.get_region('Desert Palace Main (Outer)', player).can_reach(state)) - set_rule(world.get_entrance('Tower of Hera Small Key Door', player), lambda state: state._lttp_has_key('Small Key (Tower of Hera)', player) or item_name(state, 'Tower of Hera - Big Key Chest', player) == ('Small Key (Tower of Hera)', player)) + set_rule(world.get_entrance('Tower of Hera Small Key Door', player), lambda state: state._lttp_has_key('Small Key (Tower of Hera)', player) or location_item_name(state, 'Tower of Hera - Big Key Chest', player) == ('Small Key (Tower of Hera)', player)) set_rule(world.get_entrance('Tower of Hera Big Key Door', player), lambda state: state.has('Big Key (Tower of Hera)', player)) set_rule(world.get_location('Tower of Hera - Big Chest', player), lambda state: state.has('Big Key (Tower of Hera)', player)) - set_rule(world.get_location('Tower of Hera - Big Key Chest', player), lambda state: state.has_fire_source(player)) + set_rule(world.get_location('Tower of Hera - Big Key Chest', player), lambda state: has_fire_source(state, player)) if world.accessibility[player] != 'full': set_always_allow(world.get_location('Tower of Hera - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Tower of Hera)' and item.player == player) set_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Flippers', player) and state.has('Open Floodgate', player)) set_rule(world.get_entrance('Swamp Palace Small Key Door', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player)) set_rule(world.get_entrance('Swamp Palace (Center)', player), lambda state: state.has('Hammer', player)) - set_rule(world.get_location('Swamp Palace - Big Chest', player), lambda state: state.has('Big Key (Swamp Palace)', player) or item_name(state, 'Swamp Palace - Big Chest', player) == ('Big Key (Swamp Palace)', player)) + set_rule(world.get_location('Swamp Palace - Big Chest', player), lambda state: state.has('Big Key (Swamp Palace)', player)) if world.accessibility[player] != 'full': - set_always_allow(world.get_location('Swamp Palace - Big Chest', player), lambda state, item: item.name == 'Big Key (Swamp Palace)' and item.player == player) + allow_self_locking_items(world.get_location('Swamp Palace - Big Chest', player), 'Big Key (Swamp Palace)') set_rule(world.get_entrance('Swamp Palace (North)', player), lambda state: state.has('Hookshot', player)) if not world.smallkey_shuffle[player] and world.logic[player] not in ['hybridglitches', 'nologic']: forbid_item(world.get_location('Swamp Palace - Entrance', player), 'Big Key (Swamp Palace)', player) set_rule(world.get_entrance('Thieves Town Big Key Door', player), lambda state: state.has('Big Key (Thieves Town)', player)) set_rule(world.get_entrance('Blind Fight', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player)) - set_rule(world.get_location('Thieves\' Town - Big Chest', player), lambda state: (state._lttp_has_key('Small Key (Thieves Town)', player) or item_name(state, 'Thieves\' Town - Big Chest', player) == ('Small Key (Thieves Town)', player)) and state.has('Hammer', player)) + set_rule(world.get_location('Thieves\' Town - Big Chest', player), lambda state: (state._lttp_has_key('Small Key (Thieves Town)', player)) and state.has('Hammer', player)) if world.accessibility[player] != 'full': - set_always_allow(world.get_location('Thieves\' Town - Big Chest', player), lambda state, item: item.name == 'Small Key (Thieves Town)' and item.player == player and state.has('Hammer', player)) + allow_self_locking_items(world.get_location('Thieves\' Town - Big Chest', player), 'Small Key (Thieves Town)') set_rule(world.get_location('Thieves\' Town - Attic', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player)) set_rule(world.get_entrance('Skull Woods First Section South Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player)) set_rule(world.get_entrance('Skull Woods First Section (Right) North Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player)) set_rule(world.get_entrance('Skull Woods First Section West Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 2)) # ideally would only be one key, but we may have spent thst key already on escaping the right section set_rule(world.get_entrance('Skull Woods First Section (Left) Door to Exit', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 2)) - set_rule(world.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player) or item_name(state, 'Skull Woods - Big Chest', player) == ('Big Key (Skull Woods)', player)) + set_rule(world.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player)) if world.accessibility[player] != 'full': - set_always_allow(world.get_location('Skull Woods - Big Chest', player), lambda state, item: item.name == 'Big Key (Skull Woods)' and item.player == player) - set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player) and state.has_sword(player)) # sword required for curtain + allow_self_locking_items(world.get_location('Skull Woods - Big Chest', player), 'Big Key (Skull Woods)') + set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player) and has_sword(state, player)) # sword required for curtain - set_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: state.can_melt_things(player)) + set_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: can_melt_things(state, player)) set_rule(world.get_location('Ice Palace - Big Chest', player), lambda state: state.has('Big Key (Ice Palace)', player)) - set_rule(world.get_entrance('Ice Palace (Kholdstare)', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player) and state.has('Big Key (Ice Palace)', player) and (state._lttp_has_key('Small Key (Ice Palace)', player, 2) or (state.has('Cane of Somaria', player) and state._lttp_has_key('Small Key (Ice Palace)', player, 1)))) + set_rule(world.get_entrance('Ice Palace (Kholdstare)', player), lambda state: can_lift_rocks(state, player) and state.has('Hammer', player) and state.has('Big Key (Ice Palace)', player) and (state._lttp_has_key('Small Key (Ice Palace)', player, 2) or (state.has('Cane of Somaria', player) and state._lttp_has_key('Small Key (Ice Palace)', player, 1)))) set_rule(world.get_entrance('Ice Palace (East)', player), lambda state: (state.has('Hookshot', player) or ( item_in_locations(state, 'Big Key (Ice Palace)', player, [('Ice Palace - Spike Room', player), ('Ice Palace - Big Key Chest', player), ('Ice Palace - Map Chest', player)]) and state._lttp_has_key('Small Key (Ice Palace)', player))) and (state.multiworld.can_take_damage[player] or state.has('Hookshot', player) or state.has('Cape', player) or state.has('Cane of Byrna', player))) - set_rule(world.get_entrance('Ice Palace (East Top)', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player)) + set_rule(world.get_entrance('Ice Palace (East Top)', player), lambda state: can_lift_rocks(state, player) and state.has('Hammer', player)) - set_rule(world.get_entrance('Misery Mire Entrance Gap', player), lambda state: (state.has('Pegasus Boots', player) or state.has('Hookshot', player)) and (state.has_sword(player) or state.has('Fire Rod', player) or state.has('Ice Rod', player) or state.has('Hammer', player) or state.has('Cane of Somaria', player) or state.can_shoot_arrows(player))) # need to defeat wizzrobes, bombs don't work ... + set_rule(world.get_entrance('Misery Mire Entrance Gap', player), lambda state: (state.has('Pegasus Boots', player) or state.has('Hookshot', player)) and (has_sword(state, player) or state.has('Fire Rod', player) or state.has('Ice Rod', player) or state.has('Hammer', player) or state.has('Cane of Somaria', player) or can_shoot_arrows(state, player))) # need to defeat wizzrobes, bombs don't work ... set_rule(world.get_location('Misery Mire - Big Chest', player), lambda state: state.has('Big Key (Misery Mire)', player)) - set_rule(world.get_location('Misery Mire - Spike Chest', player), lambda state: (state.multiworld.can_take_damage[player] and state.has_hearts(player, 4)) or state.has('Cane of Byrna', player) or state.has('Cape', player)) + set_rule(world.get_location('Misery Mire - Spike Chest', player), lambda state: (state.multiworld.can_take_damage[player] and has_hearts(state, player, 4)) or state.has('Cane of Byrna', player) or state.has('Cape', player)) set_rule(world.get_entrance('Misery Mire Big Key Door', player), lambda state: state.has('Big Key (Misery Mire)', player)) # you can squander the free small key from the pot by opening the south door to the north west switch room, locking you out of accessing a color switch ... # big key gives backdoor access to that from the teleporter in the north west @@ -312,11 +319,11 @@ def global_rules(world, player): set_rule(world.get_location('Misery Mire - Main Lobby', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 1) or state._lttp_has_key('Big Key (Misery Mire)', player)) # we can place a small key in the West wing iff it also contains/blocks the Big Key, as we cannot reach and softlock with the basement key door yet set_rule(world.get_entrance('Misery Mire (West)', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 2) if (( - item_name(state, 'Misery Mire - Compass Chest', player) in [('Big Key (Misery Mire)', player)]) or + location_item_name(state, 'Misery Mire - Compass Chest', player) in [('Big Key (Misery Mire)', player)]) or ( - item_name(state, 'Misery Mire - Big Key Chest', player) in [('Big Key (Misery Mire)', player)])) else state._lttp_has_key('Small Key (Misery Mire)', player, 3)) - set_rule(world.get_location('Misery Mire - Compass Chest', player), lambda state: state.has_fire_source(player)) - set_rule(world.get_location('Misery Mire - Big Key Chest', player), lambda state: state.has_fire_source(player)) + location_item_name(state, 'Misery Mire - Big Key Chest', player) in [('Big Key (Misery Mire)', player)])) else state._lttp_has_key('Small Key (Misery Mire)', player, 3)) + set_rule(world.get_location('Misery Mire - Compass Chest', player), lambda state: has_fire_source(state, player)) + set_rule(world.get_location('Misery Mire - Big Key Chest', player), lambda state: has_fire_source(state, player)) set_rule(world.get_entrance('Misery Mire (Vitreous)', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('Turtle Rock Entrance Gap', player), lambda state: state.has('Cane of Somaria', player)) @@ -336,20 +343,20 @@ def global_rules(world, player): set_rule(world.get_entrance('Turtle Rock (Trinexx)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 4) and state.has('Big Key (Turtle Rock)', player) and state.has('Cane of Somaria', player)) if not world.enemy_shuffle[player]: - set_rule(world.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: state.can_shoot_arrows(player)) + set_rule(world.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: can_shoot_arrows(state, player)) set_rule(world.get_entrance('Palace of Darkness Hammer Peg Drop', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('Palace of Darkness Bridge Room', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 1)) # If we can reach any other small key door, we already have back door access to this area - set_rule(world.get_entrance('Palace of Darkness Big Key Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) and state.has('Big Key (Palace of Darkness)', player) and state.can_shoot_arrows(player) and state.has('Hammer', player)) + set_rule(world.get_entrance('Palace of Darkness Big Key Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) and state.has('Big Key (Palace of Darkness)', player) and can_shoot_arrows(state, player) and state.has('Hammer', player)) set_rule(world.get_entrance('Palace of Darkness (North)', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 4)) set_rule(world.get_location('Palace of Darkness - Big Chest', player), lambda state: state.has('Big Key (Palace of Darkness)', player)) set_rule(world.get_entrance('Palace of Darkness Big Key Chest Staircase', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or ( - item_name(state, 'Palace of Darkness - Big Key Chest', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 3))) + location_item_name(state, 'Palace of Darkness - Big Key Chest', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 3))) if world.accessibility[player] != 'full': set_always_allow(world.get_location('Palace of Darkness - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5)) set_rule(world.get_entrance('Palace of Darkness Spike Statue Room Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or ( - item_name(state, 'Palace of Darkness - Harmless Hellway', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 4))) + location_item_name(state, 'Palace of Darkness - Harmless Hellway', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 4))) if world.accessibility[player] != 'full': set_always_allow(world.get_location('Palace of Darkness - Harmless Hellway', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5)) @@ -363,7 +370,7 @@ def global_rules(world, player): set_rule(world.get_entrance('Ganons Tower (Tile Room)', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('Ganons Tower (Hookshot Room)', player), lambda state: state.has('Hammer', player) and (state.has('Hookshot', player) or state.has('Pegasus Boots', player))) set_rule(world.get_entrance('Ganons Tower (Map Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 4) or ( - item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player), ('Small Key (Ganons Tower)', player)] and state._lttp_has_key('Small Key (Ganons Tower)', player, 3))) + location_item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player), ('Small Key (Ganons Tower)', player)] and state._lttp_has_key('Small Key (Ganons Tower)', player, 3))) if world.accessibility[player] != 'full': set_always_allow(world.get_location('Ganons Tower - Map Chest', player), lambda state, item: item.name == 'Small Key (Ganons Tower)' and item.player == player and state._lttp_has_key('Small Key (Ganons Tower)', player, 3) and state.can_reach('Ganons Tower (Hookshot Room)', 'region', player)) @@ -400,9 +407,9 @@ def global_rules(world, player): lambda state: state.has('Big Key (Ganons Tower)', player)) else: set_rule(world.get_entrance('Ganons Tower Big Key Door', player), - lambda state: state.has('Big Key (Ganons Tower)', player) and state.can_shoot_arrows(player)) + lambda state: state.has('Big Key (Ganons Tower)', player) and can_shoot_arrows(state, player)) set_rule(world.get_entrance('Ganons Tower Torch Rooms', player), - lambda state: state.has_fire_source(player) and state.multiworld.get_entrance('Ganons Tower Torch Rooms', player).parent_region.dungeon.bosses['middle'].can_defeat(state)) + lambda state: has_fire_source(state, player) and state.multiworld.get_entrance('Ganons Tower Torch Rooms', player).parent_region.dungeon.bosses['middle'].can_defeat(state)) set_rule(world.get_location('Ganons Tower - Pre-Moldorm Chest', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3)) set_rule(world.get_entrance('Ganons Tower Moldorm Door', player), @@ -413,12 +420,12 @@ def global_rules(world, player): ganon = world.get_location('Ganon', player) set_rule(ganon, lambda state: GanonDefeatRule(state, player)) if world.goal[player] in ['ganontriforcehunt', 'localganontriforcehunt']: - add_rule(ganon, lambda state: state.has_triforce_pieces(state.multiworld.treasure_hunt_count[player], player)) + add_rule(ganon, lambda state: has_triforce_pieces(state, player)) elif world.goal[player] == 'ganonpedestal': add_rule(world.get_location('Ganon', player), lambda state: state.can_reach('Master Sword Pedestal', 'Location', player)) else: - add_rule(ganon, lambda state: state.has_crystals(state.multiworld.crystals_needed_for_ganon[player], player)) - set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has_beam_sword(player)) # need to damage ganon to get tiles to drop + add_rule(ganon, lambda state: has_crystals(state, state.multiworld.crystals_needed_for_ganon[player], player)) + set_rule(world.get_entrance('Ganon Drop', player), lambda state: has_beam_sword(state, player)) # need to damage ganon to get tiles to drop set_rule(world.get_location('Flute Activation Spot', player), lambda state: state.has('Flute', player)) @@ -427,51 +434,51 @@ def default_rules(world, player): """Default world rules when world state is not inverted.""" # overworld requirements set_rule(world.get_entrance('Kings Grave', player), lambda state: state.has('Pegasus Boots', player)) - set_rule(world.get_entrance('Kings Grave Outer Rocks', player), lambda state: state.can_lift_heavy_rocks(player)) - set_rule(world.get_entrance('Kings Grave Inner Rocks', player), lambda state: state.can_lift_heavy_rocks(player)) + set_rule(world.get_entrance('Kings Grave Outer Rocks', player), lambda state: can_lift_heavy_rocks(state, player)) + set_rule(world.get_entrance('Kings Grave Inner Rocks', player), lambda state: can_lift_heavy_rocks(state, player)) set_rule(world.get_entrance('Kings Grave Mirror Spot', player), lambda state: state.has('Moon Pearl', player) and state.has('Magic Mirror', player)) # Caution: If king's grave is releaxed at all to account for reaching it via a two way cave's exit in insanity mode, then the bomb shop logic will need to be updated (that would involve create a small ledge-like Region for it) set_rule(world.get_entrance('Bonk Fairy (Light)', player), lambda state: state.has('Pegasus Boots', player)) set_rule(world.get_entrance('Lumberjack Tree Tree', player), lambda state: state.has('Pegasus Boots', player) and state.has('Beat Agahnim 1', player)) set_rule(world.get_entrance('Bonk Rock Cave', player), lambda state: state.has('Pegasus Boots', player)) set_rule(world.get_entrance('Desert Palace Stairs', player), lambda state: state.has('Book of Mudora', player)) - set_rule(world.get_entrance('Sanctuary Grave', player), lambda state: state.can_lift_rocks(player)) - set_rule(world.get_entrance('20 Rupee Cave', player), lambda state: state.can_lift_rocks(player)) - set_rule(world.get_entrance('50 Rupee Cave', player), lambda state: state.can_lift_rocks(player)) - set_rule(world.get_entrance('Death Mountain Entrance Rock', player), lambda state: state.can_lift_rocks(player)) + set_rule(world.get_entrance('Sanctuary Grave', player), lambda state: can_lift_rocks(state, player)) + set_rule(world.get_entrance('20 Rupee Cave', player), lambda state: can_lift_rocks(state, player)) + set_rule(world.get_entrance('50 Rupee Cave', player), lambda state: can_lift_rocks(state, player)) + set_rule(world.get_entrance('Death Mountain Entrance Rock', player), lambda state: can_lift_rocks(state, player)) set_rule(world.get_entrance('Bumper Cave Entrance Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Flute Spot 1', player), lambda state: state.has('Activated Flute', player)) - set_rule(world.get_entrance('Lake Hylia Central Island Teleporter', player), lambda state: state.can_lift_heavy_rocks(player)) - set_rule(world.get_entrance('Dark Desert Teleporter', player), lambda state: state.has('Activated Flute', player) and state.can_lift_heavy_rocks(player)) - set_rule(world.get_entrance('East Hyrule Teleporter', player), lambda state: state.has('Hammer', player) and state.can_lift_rocks(player) and state.has('Moon Pearl', player)) # bunny cannot use hammer - set_rule(world.get_entrance('South Hyrule Teleporter', player), lambda state: state.has('Hammer', player) and state.can_lift_rocks(player) and state.has('Moon Pearl', player)) # bunny cannot use hammer - set_rule(world.get_entrance('Kakariko Teleporter', player), lambda state: ((state.has('Hammer', player) and state.can_lift_rocks(player)) or state.can_lift_heavy_rocks(player)) and state.has('Moon Pearl', player)) # bunny cannot lift bushes + set_rule(world.get_entrance('Lake Hylia Central Island Teleporter', player), lambda state: can_lift_heavy_rocks(state, player)) + set_rule(world.get_entrance('Dark Desert Teleporter', player), lambda state: state.has('Activated Flute', player) and can_lift_heavy_rocks(state, player)) + set_rule(world.get_entrance('East Hyrule Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer + set_rule(world.get_entrance('South Hyrule Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer + set_rule(world.get_entrance('Kakariko Teleporter', player), lambda state: ((state.has('Hammer', player) and can_lift_rocks(state, player)) or can_lift_heavy_rocks(state, player)) and state.has('Moon Pearl', player)) # bunny cannot lift bushes set_rule(world.get_location('Flute Spot', player), lambda state: state.has('Shovel', player)) set_rule(world.get_entrance('Bat Cave Drop Ledge', player), lambda state: state.has('Hammer', player)) set_rule(world.get_location('Zora\'s Ledge', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('Waterfall of Wishing', player), lambda state: state.has('Flippers', player)) - set_rule(world.get_location('Frog', player), lambda state: state.can_lift_heavy_rocks(player)) # will get automatic moon pearl requirement + set_rule(world.get_location('Frog', player), lambda state: can_lift_heavy_rocks(state, player)) # will get automatic moon pearl requirement set_rule(world.get_location('Potion Shop', player), lambda state: state.has('Mushroom', player)) - set_rule(world.get_entrance('Desert Palace Entrance (North) Rocks', player), lambda state: state.can_lift_rocks(player)) - set_rule(world.get_entrance('Desert Ledge Return Rocks', player), lambda state: state.can_lift_rocks(player)) # should we decide to place something that is not a dungeon end up there at some point - set_rule(world.get_entrance('Checkerboard Cave', player), lambda state: state.can_lift_rocks(player)) - set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has_beam_sword(player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle + set_rule(world.get_entrance('Desert Palace Entrance (North) Rocks', player), lambda state: can_lift_rocks(state, player)) + set_rule(world.get_entrance('Desert Ledge Return Rocks', player), lambda state: can_lift_rocks(state, player)) # should we decide to place something that is not a dungeon end up there at some point + set_rule(world.get_entrance('Checkerboard Cave', player), lambda state: can_lift_rocks(state, player)) + set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or has_beam_sword(state, player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle set_rule(world.get_entrance('Top of Pyramid', player), lambda state: state.has('Beat Agahnim 1', player)) set_rule(world.get_entrance('Old Man Cave Exit (West)', player), lambda state: False) # drop cannot be climbed up set_rule(world.get_entrance('Broken Bridge (West)', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('Broken Bridge (East)', player), lambda state: state.has('Hookshot', player)) - set_rule(world.get_entrance('East Death Mountain Teleporter', player), lambda state: state.can_lift_heavy_rocks(player)) - set_rule(world.get_entrance('Fairy Ascension Rocks', player), lambda state: state.can_lift_heavy_rocks(player)) + set_rule(world.get_entrance('East Death Mountain Teleporter', player), lambda state: can_lift_heavy_rocks(state, player)) + set_rule(world.get_entrance('Fairy Ascension Rocks', player), lambda state: can_lift_heavy_rocks(state, player)) set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: state.has('Mirror', player)) # can erase block set_rule(world.get_entrance('Death Mountain (Top)', player), lambda state: state.has('Hammer', player)) - set_rule(world.get_entrance('Turtle Rock Teleporter', player), lambda state: state.can_lift_heavy_rocks(player) and state.has('Hammer', player)) + set_rule(world.get_entrance('Turtle Rock Teleporter', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) set_rule(world.get_entrance('East Death Mountain (Top)', player), lambda state: state.has('Hammer', player)) - set_rule(world.get_entrance('Catfish Exit Rock', player), lambda state: state.can_lift_rocks(player)) - set_rule(world.get_entrance('Catfish Entrance Rock', player), lambda state: state.can_lift_rocks(player)) - set_rule(world.get_entrance('Northeast Dark World Broken Bridge Pass', player), lambda state: state.has('Moon Pearl', player) and (state.can_lift_rocks(player) or state.has('Hammer', player) or state.has('Flippers', player))) - set_rule(world.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: state.has('Moon Pearl', player) and (state.can_lift_rocks(player) or state.has('Hammer', player))) + set_rule(world.get_entrance('Catfish Exit Rock', player), lambda state: can_lift_rocks(state, player)) + set_rule(world.get_entrance('Catfish Entrance Rock', player), lambda state: can_lift_rocks(state, player)) + set_rule(world.get_entrance('Northeast Dark World Broken Bridge Pass', player), lambda state: state.has('Moon Pearl', player) and (can_lift_rocks(state, player) or state.has('Hammer', player) or state.has('Flippers', player))) + set_rule(world.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: state.has('Moon Pearl', player) and (can_lift_rocks(state, player) or state.has('Hammer', player))) set_rule(world.get_entrance('South Dark World Bridge', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Bonk Fairy (Dark)', player), lambda state: state.has('Moon Pearl', player) and state.has('Pegasus Boots', player)) set_rule(world.get_entrance('West Dark World Gap', player), lambda state: state.has('Moon Pearl', player) and state.has('Hookshot', player)) @@ -479,12 +486,12 @@ def default_rules(world, player): set_rule(world.get_entrance('Hyrule Castle Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Hyrule Castle Main Gate', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: (state.has('Moon Pearl', player) and state.has('Flippers', player) or state.has('Magic Mirror', player))) # Overworld Bunny Revival - set_rule(world.get_location('Bombos Tablet', player), lambda state: state.can_retrieve_tablet(player)) + set_rule(world.get_location('Bombos Tablet', player), lambda state: can_retrieve_tablet(state, player)) set_rule(world.get_entrance('Dark Lake Hylia Drop (South)', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # ToDo any fake flipper set up? set_rule(world.get_entrance('Dark Lake Hylia Ledge Fairy', player), lambda state: state.has('Moon Pearl', player)) # bomb required - set_rule(world.get_entrance('Dark Lake Hylia Ledge Spike Cave', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player)) + set_rule(world.get_entrance('Dark Lake Hylia Ledge Spike Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Village of Outcasts Heavy Rock', player), lambda state: state.has('Moon Pearl', player) and state.can_lift_heavy_rocks(player)) + set_rule(world.get_entrance('Village of Outcasts Heavy Rock', player), lambda state: state.has('Moon Pearl', player) and can_lift_heavy_rocks(state, player)) set_rule(world.get_entrance('Hype Cave', player), lambda state: state.has('Moon Pearl', player)) # bomb required set_rule(world.get_entrance('Brewery', player), lambda state: state.has('Moon Pearl', player)) # bomb required set_rule(world.get_entrance('Thieves Town', player), lambda state: state.has('Moon Pearl', player)) # bunny cannot pull @@ -498,26 +505,26 @@ def default_rules(world, player): set_rule(world.get_entrance('Lake Hylia Central Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('East Dark World River Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) set_rule(world.get_entrance('Graveyard Ledge Mirror Spot', player), lambda state: state.has('Moon Pearl', player) and state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Bumper Cave Entrance Rock', player), lambda state: state.has('Moon Pearl', player) and state.can_lift_rocks(player)) + set_rule(world.get_entrance('Bumper Cave Entrance Rock', player), lambda state: state.has('Moon Pearl', player) and can_lift_rocks(state, player)) set_rule(world.get_entrance('Bumper Cave Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Bat Cave Drop Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Dark World Hammer Peg Cave', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player)) - set_rule(world.get_entrance('Village of Outcasts Eastern Rocks', player), lambda state: state.has('Moon Pearl', player) and state.can_lift_heavy_rocks(player)) - set_rule(world.get_entrance('Peg Area Rocks', player), lambda state: state.has('Moon Pearl', player) and state.can_lift_heavy_rocks(player)) + set_rule(world.get_entrance('Village of Outcasts Eastern Rocks', player), lambda state: state.has('Moon Pearl', player) and can_lift_heavy_rocks(state, player)) + set_rule(world.get_entrance('Peg Area Rocks', player), lambda state: state.has('Moon Pearl', player) and can_lift_heavy_rocks(state, player)) set_rule(world.get_entrance('Village of Outcasts Pegs', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player)) set_rule(world.get_entrance('Grassy Lawn Pegs', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player)) set_rule(world.get_entrance('Bumper Cave Exit (Top)', player), lambda state: state.has('Cape', player)) set_rule(world.get_entrance('Bumper Cave Exit (Bottom)', player), lambda state: state.has('Cape', player) or state.has('Hookshot', player)) set_rule(world.get_entrance('Skull Woods Final Section', player), lambda state: state.has('Fire Rod', player) and state.has('Moon Pearl', player)) # bunny cannot use fire rod - set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has('Moon Pearl', player) and state.has_sword(player) and state.has_misery_mire_medallion(player)) # sword required to cast magic (!) + set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has('Moon Pearl', player) and has_sword(state, player) and has_misery_mire_medallion(state, player)) # sword required to cast magic (!) set_rule(world.get_entrance('Desert Ledge (Northeast) Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Desert Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Desert Palace Stairs Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Desert Palace Entrance (North) Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Spectacle Rock Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Hookshot Cave', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player)) + set_rule(world.get_entrance('Hookshot Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('East Death Mountain (Top) Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Mimic Cave Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) @@ -526,7 +533,7 @@ def default_rules(world, player): set_rule(world.get_entrance('Isolated Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Superbunny Cave Exit (Bottom)', player), lambda state: False) # Cannot get to bottom exit from top. Just exists for shuffling set_rule(world.get_entrance('Floating Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has('Moon Pearl', player) and state.has_sword(player) and state.has_turtle_rock_medallion(player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword required to cast magic (!) + set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has('Moon Pearl', player) and has_sword(state, player) and has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword required to cast magic (!) set_rule(world.get_entrance('Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.open_pyramid[player].to_bool(world, player)) @@ -546,12 +553,12 @@ def inverted_rules(world, player): set_rule(world.get_entrance('Potion Shop Pier', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Light World Pier', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Kings Grave', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Kings Grave Outer Rocks', player), lambda state: state.can_lift_heavy_rocks(player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Kings Grave Inner Rocks', player), lambda state: state.can_lift_heavy_rocks(player) and state.has('Moon Pearl', player)) + set_rule(world.get_entrance('Kings Grave Outer Rocks', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(world.get_entrance('Kings Grave Inner Rocks', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Potion Shop Inner Bushes', player), lambda state: state.has('Moon Pearl', player)) set_rule(world.get_entrance('Potion Shop Outer Bushes', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Potion Shop Outer Rock', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Potion Shop Inner Rock', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player)) + set_rule(world.get_entrance('Potion Shop Outer Rock', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(world.get_entrance('Potion Shop Inner Rock', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Graveyard Cave Inner Bushes', player), lambda state: state.has('Moon Pearl', player)) set_rule(world.get_entrance('Graveyard Cave Outer Bushes', player), lambda state: state.has('Moon Pearl', player)) set_rule(world.get_entrance('Secret Passage Inner Bushes', player), lambda state: state.has('Moon Pearl', player)) @@ -561,23 +568,23 @@ def inverted_rules(world, player): set_rule(world.get_entrance('Lumberjack Tree Tree', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player) and state.has('Beat Agahnim 1', player)) set_rule(world.get_entrance('Bonk Rock Cave', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Desert Palace Stairs', player), lambda state: state.has('Book of Mudora', player)) # bunny can use book - set_rule(world.get_entrance('Sanctuary Grave', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('20 Rupee Cave', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('50 Rupee Cave', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Death Mountain Entrance Rock', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player)) + set_rule(world.get_entrance('Sanctuary Grave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(world.get_entrance('20 Rupee Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(world.get_entrance('50 Rupee Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(world.get_entrance('Death Mountain Entrance Rock', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Bumper Cave Entrance Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Lake Hylia Central Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Dark Lake Hylia Central Island Teleporter', player), lambda state: state.can_lift_heavy_rocks(player)) - set_rule(world.get_entrance('Dark Desert Teleporter', player), lambda state: state.has('Activated Flute', player) and state.can_lift_heavy_rocks(player)) - set_rule(world.get_entrance('East Dark World Teleporter', player), lambda state: state.has('Hammer', player) and state.can_lift_rocks(player) and state.has('Moon Pearl', player)) # bunny cannot use hammer - set_rule(world.get_entrance('South Dark World Teleporter', player), lambda state: state.has('Hammer', player) and state.can_lift_rocks(player) and state.has('Moon Pearl', player)) # bunny cannot use hammer - set_rule(world.get_entrance('West Dark World Teleporter', player), lambda state: ((state.has('Hammer', player) and state.can_lift_rocks(player)) or state.can_lift_heavy_rocks(player)) and state.has('Moon Pearl', player)) + set_rule(world.get_entrance('Dark Lake Hylia Central Island Teleporter', player), lambda state: can_lift_heavy_rocks(state, player)) + set_rule(world.get_entrance('Dark Desert Teleporter', player), lambda state: state.has('Activated Flute', player) and can_lift_heavy_rocks(state, player)) + set_rule(world.get_entrance('East Dark World Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer + set_rule(world.get_entrance('South Dark World Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer + set_rule(world.get_entrance('West Dark World Teleporter', player), lambda state: ((state.has('Hammer', player) and can_lift_rocks(state, player)) or can_lift_heavy_rocks(state, player)) and state.has('Moon Pearl', player)) set_rule(world.get_location('Flute Spot', player), lambda state: state.has('Shovel', player) and state.has('Moon Pearl', player)) set_rule(world.get_location('Zora\'s Ledge', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Waterfall of Wishing Cave', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Northeast Light World Return', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) - set_rule(world.get_location('Frog', player), lambda state: state.can_lift_heavy_rocks(player) and (state.has('Moon Pearl', player) or state.has('Beat Agahnim 1', player)) or (state.can_reach('Light World', 'Region', player) and state.has('Magic Mirror', player))) # Need LW access using Mirror or Portal + set_rule(world.get_location('Frog', player), lambda state: can_lift_heavy_rocks(state, player) and (state.has('Moon Pearl', player) or state.has('Beat Agahnim 1', player)) or (state.can_reach('Light World', 'Region', player) and state.has('Magic Mirror', player))) # Need LW access using Mirror or Portal set_rule(world.get_location('Missing Smith', player), lambda state: state.has('Get Frog', player) and state.can_reach('Blacksmiths Hut', 'Region', player)) # Can't S&Q with smith set_rule(world.get_location('Blacksmith', player), lambda state: state.has('Return Smith', player)) set_rule(world.get_location('Magic Bat', player), lambda state: state.has('Magic Powder', player) and state.has('Moon Pearl', player)) @@ -592,52 +599,52 @@ def inverted_rules(world, player): set_rule(world.get_entrance('North Fairy Cave Drop', player), lambda state: state.has('Moon Pearl', player)) set_rule(world.get_entrance('Lost Woods Hideout Drop', player), lambda state: state.has('Moon Pearl', player)) set_rule(world.get_location('Potion Shop', player), lambda state: state.has('Mushroom', player) and (state.can_reach('Potion Shop Area', 'Region', player))) # new inverted region, need pearl for bushes or access to potion shop door/waterfall fairy - set_rule(world.get_entrance('Desert Palace Entrance (North) Rocks', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Desert Ledge Return Rocks', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player)) # should we decide to place something that is not a dungeon end up there at some point - set_rule(world.get_entrance('Checkerboard Cave', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player)) + set_rule(world.get_entrance('Desert Palace Entrance (North) Rocks', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(world.get_entrance('Desert Ledge Return Rocks', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # should we decide to place something that is not a dungeon end up there at some point + set_rule(world.get_entrance('Checkerboard Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Hyrule Castle Secret Entrance Drop', player), lambda state: state.has('Moon Pearl', player)) set_rule(world.get_entrance('Old Man Cave Exit (West)', player), lambda state: False) # drop cannot be climbed up set_rule(world.get_entrance('Broken Bridge (West)', player), lambda state: state.has('Hookshot', player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Broken Bridge (East)', player), lambda state: state.has('Hookshot', player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Dark Death Mountain Teleporter (East Bottom)', player), lambda state: state.can_lift_heavy_rocks(player)) - set_rule(world.get_entrance('Fairy Ascension Rocks', player), lambda state: state.can_lift_heavy_rocks(player) and state.has('Moon Pearl', player)) + set_rule(world.get_entrance('Dark Death Mountain Teleporter (East Bottom)', player), lambda state: can_lift_heavy_rocks(state, player)) + set_rule(world.get_entrance('Fairy Ascension Rocks', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: state.has('Mirror', player)) # can erase block set_rule(world.get_entrance('Death Mountain (Top)', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Dark Death Mountain Teleporter (East)', player), lambda state: state.can_lift_heavy_rocks(player) and state.has('Hammer', player) and state.has('Moon Pearl', player)) # bunny cannot use hammer + set_rule(world.get_entrance('Dark Death Mountain Teleporter (East)', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Hammer', player) and state.has('Moon Pearl', player)) # bunny cannot use hammer set_rule(world.get_entrance('East Death Mountain (Top)', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player)) # bunny can not use hammer - set_rule(world.get_entrance('Catfish Entrance Rock', player), lambda state: state.can_lift_rocks(player)) - set_rule(world.get_entrance('Northeast Dark World Broken Bridge Pass', player), lambda state: ((state.can_lift_rocks(player) or state.has('Hammer', player)) or state.has('Flippers', player))) - set_rule(world.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: (state.can_lift_rocks(player) or state.has('Hammer', player))) + set_rule(world.get_entrance('Catfish Entrance Rock', player), lambda state: can_lift_rocks(state, player)) + set_rule(world.get_entrance('Northeast Dark World Broken Bridge Pass', player), lambda state: ((can_lift_rocks(state, player) or state.has('Hammer', player)) or state.has('Flippers', player))) + set_rule(world.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: (can_lift_rocks(state, player) or state.has('Hammer', player))) set_rule(world.get_entrance('South Dark World Bridge', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('Bonk Fairy (Dark)', player), lambda state: state.has('Pegasus Boots', player)) set_rule(world.get_entrance('West Dark World Gap', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Flippers', player)) - set_rule(world.get_location('Bombos Tablet', player), lambda state: state.can_retrieve_tablet(player)) + set_rule(world.get_location('Bombos Tablet', player), lambda state: can_retrieve_tablet(state, player)) set_rule(world.get_entrance('Dark Lake Hylia Drop (South)', player), lambda state: state.has('Flippers', player)) # ToDo any fake flipper set up? set_rule(world.get_entrance('Dark Lake Hylia Ledge Pier', player), lambda state: state.has('Flippers', player)) - set_rule(world.get_entrance('Dark Lake Hylia Ledge Spike Cave', player), lambda state: state.can_lift_rocks(player)) + set_rule(world.get_entrance('Dark Lake Hylia Ledge Spike Cave', player), lambda state: can_lift_rocks(state, player)) set_rule(world.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Flippers', player)) # Fake Flippers set_rule(world.get_entrance('Dark Lake Hylia Shallows', player), lambda state: state.has('Flippers', player)) - set_rule(world.get_entrance('Village of Outcasts Heavy Rock', player), lambda state: state.can_lift_heavy_rocks(player)) + set_rule(world.get_entrance('Village of Outcasts Heavy Rock', player), lambda state: can_lift_heavy_rocks(state, player)) set_rule(world.get_entrance('East Dark World Bridge', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('Lake Hylia Central Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('East Dark World River Pier', player), lambda state: state.has('Flippers', player)) - set_rule(world.get_entrance('Bumper Cave Entrance Rock', player), lambda state: state.can_lift_rocks(player)) + set_rule(world.get_entrance('Bumper Cave Entrance Rock', player), lambda state: can_lift_rocks(state, player)) set_rule(world.get_entrance('Bumper Cave Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Hammer Peg Area Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Dark World Hammer Peg Cave', player), lambda state: state.has('Hammer', player)) - set_rule(world.get_entrance('Village of Outcasts Eastern Rocks', player), lambda state: state.can_lift_heavy_rocks(player)) - set_rule(world.get_entrance('Peg Area Rocks', player), lambda state: state.can_lift_heavy_rocks(player)) + set_rule(world.get_entrance('Village of Outcasts Eastern Rocks', player), lambda state: can_lift_heavy_rocks(state, player)) + set_rule(world.get_entrance('Peg Area Rocks', player), lambda state: can_lift_heavy_rocks(state, player)) set_rule(world.get_entrance('Village of Outcasts Pegs', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('Grassy Lawn Pegs', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('Bumper Cave Exit (Top)', player), lambda state: state.has('Cape', player)) set_rule(world.get_entrance('Bumper Cave Exit (Bottom)', player), lambda state: state.has('Cape', player) or state.has('Hookshot', player)) set_rule(world.get_entrance('Skull Woods Final Section', player), lambda state: state.has('Fire Rod', player)) - set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has_sword(player) and state.has_misery_mire_medallion(player)) # sword required to cast magic (!) + set_rule(world.get_entrance('Misery Mire', player), lambda state: has_sword(state, player) and has_misery_mire_medallion(state, player)) # sword required to cast magic (!) - set_rule(world.get_entrance('Hookshot Cave', player), lambda state: state.can_lift_rocks(player)) + set_rule(world.get_entrance('Hookshot Cave', player), lambda state: can_lift_rocks(state, player)) set_rule(world.get_entrance('East Death Mountain Mirror Spot (Top)', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Death Mountain (Top) Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) @@ -647,7 +654,7 @@ def inverted_rules(world, player): set_rule(world.get_entrance('Dark Death Mountain Ledge Mirror Spot (West)', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Laser Bridge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Floating Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has_sword(player) and state.has_turtle_rock_medallion(player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword required to cast magic (!) + set_rule(world.get_entrance('Turtle Rock', player), lambda state: has_sword(state, player) and has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword required to cast magic (!) # new inverted spots set_rule(world.get_entrance('Post Aga Teleporter', player), lambda state: state.has('Beat Agahnim 1', player)) @@ -688,7 +695,7 @@ def inverted_rules(world, player): def no_glitches_rules(world, player): """""" if world.mode[player] == 'inverted': - set_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Moon Pearl', player) and (state.has('Flippers', player) or state.can_lift_rocks(player))) + set_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Moon Pearl', player) and (state.has('Flippers', player) or can_lift_rocks(state, player))) set_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to set_rule(world.get_entrance('Lake Hylia Island Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to set_rule(world.get_entrance('Lake Hylia Warp', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to @@ -699,7 +706,7 @@ def no_glitches_rules(world, player): set_rule(world.get_entrance('Dark Lake Hylia Ledge Drop', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('East Dark World Pier', player), lambda state: state.has('Flippers', player)) else: - set_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Flippers', player) or state.can_lift_rocks(player)) + set_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Flippers', player) or can_lift_rocks(state, player)) set_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Flippers', player)) # can be fake flippered to set_rule(world.get_entrance('Hobo Bridge', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) @@ -821,19 +828,19 @@ def open_rules(world, player): def swordless_rules(world, player): - set_rule(world.get_entrance('Agahnim 1', player), lambda state: (state.has('Hammer', player) or state.has('Fire Rod', player) or state.can_shoot_arrows(player) or state.has('Cane of Somaria', player)) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2)) + set_rule(world.get_entrance('Agahnim 1', player), lambda state: (state.has('Hammer', player) or state.has('Fire Rod', player) or can_shoot_arrows(state, player) or state.has('Cane of Somaria', player)) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2)) set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player)) # no curtain set_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player)) #in swordless mode bombos pads are present in the relevant parts of ice palace set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has('Hammer', player)) # need to damage ganon to get tiles to drop if world.mode[player] != 'inverted': set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has('Hammer', player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle - set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has('Moon Pearl', player) and state.has_turtle_rock_medallion(player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword not required to use medallion for opening in swordless (!) - set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has('Moon Pearl', player) and state.has_misery_mire_medallion(player)) # sword not required to use medallion for opening in swordless (!) + set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has('Moon Pearl', player) and has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword not required to use medallion for opening in swordless (!) + set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has('Moon Pearl', player) and has_misery_mire_medallion(state, player)) # sword not required to use medallion for opening in swordless (!) else: # only need ddm access for aga tower in inverted - set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has_turtle_rock_medallion(player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword not required to use medallion for opening in swordless (!) - set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has_misery_mire_medallion(player)) # sword not required to use medallion for opening in swordless (!) + set_rule(world.get_entrance('Turtle Rock', player), lambda state: has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword not required to use medallion for opening in swordless (!) + set_rule(world.get_entrance('Misery Mire', player), lambda state: has_misery_mire_medallion(state, player)) # sword not required to use medallion for opening in swordless (!) def add_connection(parent_name, target_name, entrance_name, world, player): @@ -905,7 +912,7 @@ def set_trock_key_rules(world, player): # Now we need to set rules based on which entrances we have access to. The most important point is whether we have back access. If we have back access, we # might open all the locked doors in any order so we need maximally restrictive rules. if can_reach_back: - set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: (state._lttp_has_key('Small Key (Turtle Rock)', player, 4) or item_name(state, 'Turtle Rock - Big Key Chest', player) == ('Small Key (Turtle Rock)', player))) + set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: (state._lttp_has_key('Small Key (Turtle Rock)', player, 4) or location_item_name(state, 'Turtle Rock - Big Key Chest', player) == ('Small Key (Turtle Rock)', player))) set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 4)) # Only consider wasting the key on the Trinexx door for going from the front entrance to middle section. If other key doors are accessible, then these doors can be avoided set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3)) @@ -925,7 +932,7 @@ def set_trock_key_rules(world, player): def tr_big_key_chest_keys_needed(state): # This function handles the key requirements for the TR Big Chest in the situations it having the Big Key should logically require 2 keys, small key # should logically require no keys, and anything else should logically require 4 keys. - item = item_name(state, 'Turtle Rock - Big Key Chest', player) + item = location_item_name(state, 'Turtle Rock - Big Key Chest', player) if item in [('Small Key (Turtle Rock)', player)]: return 0 if item in [('Big Key (Turtle Rock)', player)]: @@ -1084,7 +1091,7 @@ def set_big_bomb_rules(world, player): # returning via the eastern and southern teleporters needs the same items, so we use the southern teleporter for out routing. # crossing preg bridge already requires hammer so we just add the gloves to the requirement def southern_teleporter(state): - return state.can_lift_rocks(player) and cross_peg_bridge(state) + return can_lift_rocks(state, player) and cross_peg_bridge(state) # the basic routes assume you can reach eastern light world with the bomb. # you can then use the southern teleporter, or (if you have beaten Aga1) the hyrule castle gate warp @@ -1111,13 +1118,13 @@ def set_big_bomb_rules(world, player): #1. Mirror and basic routes #2. Go to south DW and then cross peg bridge: Need Mitts and hammer and moon pearl # -> (Mitts and CPB) or (M and BR) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.can_lift_heavy_rocks(player) and cross_peg_bridge(state)) or (state.has('Magic Mirror', player) and basic_routes(state))) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (can_lift_heavy_rocks(state, player) and cross_peg_bridge(state)) or (state.has('Magic Mirror', player) and basic_routes(state))) elif bombshop_entrance.name == 'Bumper Cave (Bottom)': #1. Mirror and Lift rock and basic_routes #2. Mirror and Flute and basic routes (can make difference if accessed via insanity or w/ mirror from connector, and then via hyrule castle gate, because no gloves are needed in that case) #3. Go to south DW and then cross peg bridge: Need Mitts and hammer and moon pearl # -> (Mitts and CPB) or (((G or Flute) and M) and BR)) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.can_lift_heavy_rocks(player) and cross_peg_bridge(state)) or (((state.can_lift_rocks(player) or state.has('Flute', player)) and state.has('Magic Mirror', player)) and basic_routes(state))) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (can_lift_heavy_rocks(state, player) and cross_peg_bridge(state)) or (((can_lift_rocks(state, player) or state.has('Flute', player)) and state.has('Magic Mirror', player)) and basic_routes(state))) elif bombshop_entrance.name in Southern_DW_entrances: #1. Mirror and enter via gate: Need mirror and Aga1 #2. cross peg bridge: Need hammer and moon pearl @@ -1145,7 +1152,7 @@ def set_big_bomb_rules(world, player): elif bombshop_entrance.name == 'Fairy Ascension Cave (Bottom)': # Same as East_LW_DM_entrances except navigation without BR requires Mitts # -> Flute and ((M and Hookshot and Mitts) or BR) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and ((state.has('Magic Mirror', player) and state.has('Hookshot', player) and state.can_lift_heavy_rocks(player)) or basic_routes(state))) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and ((state.has('Magic Mirror', player) and state.has('Hookshot', player) and can_lift_heavy_rocks(state, player)) or basic_routes(state))) elif bombshop_entrance.name in Castle_ledge_entrances: # 1. mirror on pyramid to castle ledge, grab bomb, return through mirror spot: Needs mirror # 2. flute then basic routes @@ -1161,7 +1168,7 @@ def set_big_bomb_rules(world, player): # 1. Lift rock then basic_routes # 2. flute then basic_routes # -> (Flute or G) and BR - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or state.can_lift_rocks(player)) and basic_routes(state)) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or can_lift_rocks(state, player)) and basic_routes(state)) elif bombshop_entrance.name == 'Graveyard Cave': # 1. flute then basic routes # 2. (has west dark world access) use existing mirror spot (required Pearl), mirror again off ledge @@ -1177,13 +1184,13 @@ def set_big_bomb_rules(world, player): # 2. walk down by hammering peg: needs hammer and pearl # 3. mirror and basic routes # -> (P and (H or Gloves)) or (M and BR) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Moon Pearl', player) and (state.has('Hammer', player) or state.can_lift_rocks(player))) or (state.has('Magic Mirror', player) and basic_routes(state))) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Moon Pearl', player) and (state.has('Hammer', player) or can_lift_rocks(state, player))) or (state.has('Magic Mirror', player) and basic_routes(state))) elif bombshop_entrance.name == 'Kings Grave': # same as the Normal_LW_entrances case except that the pre-existing mirror is only possible if you have mitts # (because otherwise mirror was used to reach the grave, so would cancel a pre-existing mirror spot) # to account for insanity, must consider a way to escape without a cave for basic_routes # -> (M and Mitts) or ((Mitts or Flute or (M and P and West Dark World access)) and BR) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.can_lift_heavy_rocks(player) and state.has('Magic Mirror', player)) or ((state.can_lift_heavy_rocks(player) or state.has('Activated Flute', player) or (state.can_reach('West Dark World', 'Region', player) and state.has('Moon Pearl', player) and state.has('Magic Mirror', player))) and basic_routes(state))) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (can_lift_heavy_rocks(state, player) and state.has('Magic Mirror', player)) or ((can_lift_heavy_rocks(state, player) or state.has('Activated Flute', player) or (state.can_reach('West Dark World', 'Region', player) and state.has('Moon Pearl', player) and state.has('Magic Mirror', player))) and basic_routes(state))) elif bombshop_entrance.name == 'Waterfall of Wishing': # same as the Normal_LW_entrances case except in insanity it's possible you could be here without Flippers which # means you need an escape route of either Flippers or Flute @@ -1330,7 +1337,7 @@ def set_inverted_big_bomb_rules(world, player): elif bombshop_entrance.name in Northern_DW_entrances: # You can just fly with the Flute, you can take a long walk with Mitts and Hammer, # or you can leave a Mirror portal nearby and then walk to the castle to Mirror again. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (state.can_lift_heavy_rocks(player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player))) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player))) elif bombshop_entrance.name in Southern_DW_entrances: # This is the same as north DW without the Mitts rock present. add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Hammer', player) or state.has('Activated Flute', player) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player))) @@ -1342,22 +1349,22 @@ def set_inverted_big_bomb_rules(world, player): add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player))) elif bombshop_entrance.name in LW_bush_entrances: # These entrances are behind bushes in LW so you need either Pearl or the tools to solve NDW bomb shop locations. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and (state.has('Activated Flute', player) or state.has('Moon Pearl', player) or (state.can_lift_heavy_rocks(player) and state.has('Hammer', player)))) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and (state.has('Activated Flute', player) or state.has('Moon Pearl', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player)))) elif bombshop_entrance.name == 'Village of Outcasts Shop': # This is mostly the same as NDW but the Mirror path requires the Pearl, or using the Hammer - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (state.can_lift_heavy_rocks(player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player) and (state.has('Moon Pearl', player) or state.has('Hammer', player)))) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player) and (state.has('Moon Pearl', player) or state.has('Hammer', player)))) elif bombshop_entrance.name == 'Bumper Cave (Bottom)': # This is mostly the same as NDW but the Mirror path requires being able to lift a rock. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (state.can_lift_heavy_rocks(player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and state.can_lift_rocks(player) and state.can_reach('Light World', 'Region', player))) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and can_lift_rocks(state, player) and state.can_reach('Light World', 'Region', player))) elif bombshop_entrance.name == 'Old Man Cave (West)': # The three paths back are Mirror and DW walk, Mirror and Flute, or LW walk and then Mirror. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and ((state.can_lift_heavy_rocks(player) and state.has('Hammer', player)) or (state.can_lift_rocks(player) and state.has('Moon Pearl', player)) or state.has('Activated Flute', player))) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and ((can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (can_lift_rocks(state, player) and state.has('Moon Pearl', player)) or state.has('Activated Flute', player))) elif bombshop_entrance.name == 'Dark World Potion Shop': # You either need to Flute to 5 or cross the rock/hammer choice pass to the south. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or state.has('Hammer', player) or state.can_lift_rocks(player)) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or state.has('Hammer', player) or can_lift_rocks(state, player)) elif bombshop_entrance.name == 'Kings Grave': # Either lift the rock and walk to the castle to Mirror or Mirror immediately and Flute. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or state.can_lift_heavy_rocks(player)) and state.has('Magic Mirror', player)) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or can_lift_heavy_rocks(state, player)) and state.has('Magic Mirror', player)) elif bombshop_entrance.name == 'Waterfall of Wishing': # You absolutely must be able to swim to return it from here. add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player) and state.has('Magic Mirror', player)) @@ -1422,7 +1429,7 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool): if region.name == 'Swamp Palace (Entrance)': # Need to 0hp revive - not in logic return lambda state: state.has('Moon Pearl', player) if region.name == 'Tower of Hera (Bottom)': # Need to hit the crystal switch - return lambda state: state.has('Magic Mirror', player) and state.has_sword(player) or state.has('Moon Pearl', player) + return lambda state: state.has('Magic Mirror', player) and has_sword(state, player) or state.has('Moon Pearl', player) if region.name in OverworldGlitchRules.get_invalid_bunny_revival_dungeons(): return lambda state: state.has('Magic Mirror', player) or state.has('Moon Pearl', player) if region.type == LTTPRegionType.Dungeon: @@ -1460,7 +1467,7 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool): # For glitch rulesets, establish superbunny and revival rules. if world.logic[player] in ['minorglitches', 'owglitches', 'hybridglitches', 'nologic'] and entrance.name not in OverworldGlitchRules.get_invalid_bunny_revival_dungeons(): if region.name in OverworldGlitchRules.get_sword_required_superbunny_mirror_regions(): - possible_options.append(lambda state: path_to_access_rule(new_path, entrance) and state.has('Magic Mirror', player) and state.has_sword(player)) + possible_options.append(lambda state: path_to_access_rule(new_path, entrance) and state.has('Magic Mirror', player) and has_sword(state, player)) elif (region.name in OverworldGlitchRules.get_boots_required_superbunny_mirror_regions() or location is not None and location.name in OverworldGlitchRules.get_boots_required_superbunny_mirror_locations()): possible_options.append(lambda state: path_to_access_rule(new_path, entrance) and state.has('Magic Mirror', player) and state.has('Pegasus Boots', player)) @@ -1508,4 +1515,4 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool): continue if location.name in bunny_accessible_locations: continue - add_rule(location, get_rule_to_add(entrance.connected_region, location)) \ No newline at end of file + add_rule(location, get_rule_to_add(entrance.connected_region, location)) diff --git a/worlds/alttp/StateHelpers.py b/worlds/alttp/StateHelpers.py new file mode 100644 index 0000000000..33cea8fbfb --- /dev/null +++ b/worlds/alttp/StateHelpers.py @@ -0,0 +1,137 @@ +from .SubClasses import LTTPRegion +from BaseClasses import CollectionState + +def is_not_bunny(state: CollectionState, region: LTTPRegion, player: int) -> bool: + if state.has('Moon Pearl', player): + return True + + return region.is_light_world if state.multiworld.mode[player] != 'inverted' else region.is_dark_world + +def can_bomb_clip(state: CollectionState, region: LTTPRegion, player: int) -> bool: + return is_not_bunny(state, region, player) and state.has('Pegasus Boots', player) + +def can_buy_unlimited(state: CollectionState, item: str, player: int) -> bool: + return any(shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(state) for + shop in state.multiworld.shops) + +def can_buy(state: CollectionState, item: str, player: int) -> bool: + return any(shop.region.player == player and shop.has(item) and shop.region.can_reach(state) for + shop in state.multiworld.shops) + +def can_shoot_arrows(state: CollectionState, player: int) -> bool: + if state.multiworld.retro_bow[player]: + return (state.has('Bow', player) or state.has('Silver Bow', player)) and can_buy(state, 'Single Arrow', player) + return state.has('Bow', player) or state.has('Silver Bow', player) + +def has_triforce_pieces(state: CollectionState, player: int) -> bool: + count = state.multiworld.treasure_hunt_count[player] + return state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= count + +def has_crystals(state: CollectionState, count: int, player: int) -> bool: + found = state.count_group("Crystals", player) + return found >= count + +def can_lift_rocks(state: CollectionState, player: int): + return state.has('Power Glove', player) or state.has('Titans Mitts', player) + +def can_lift_heavy_rocks(state: CollectionState, player: int) -> bool: + return state.has('Titans Mitts', player) + +def bottle_count(state: CollectionState, player: int) -> int: + return min(state.multiworld.difficulty_requirements[player].progressive_bottle_limit, + state.count_group("Bottles", player)) + +def has_hearts(state: CollectionState, player: int, count: int) -> int: + # Warning: This only considers items that are marked as advancement items + return heart_count(state, player) >= count + +def heart_count(state: CollectionState, player: int) -> int: + # Warning: This only considers items that are marked as advancement items + diff = state.multiworld.difficulty_requirements[player] + return min(state.item_count('Boss Heart Container', player), diff.boss_heart_container_limit) \ + + state.item_count('Sanctuary Heart Container', player) \ + + min(state.item_count('Piece of Heart', player), diff.heart_piece_limit) // 4 \ + + 3 # starting hearts + +def can_extend_magic(state: CollectionState, player: int, smallmagic: int = 16, + fullrefill: bool = False): # This reflects the total magic Link has, not the total extra he has. + basemagic = 8 + if state.has('Magic Upgrade (1/4)', player): + basemagic = 32 + elif state.has('Magic Upgrade (1/2)', player): + basemagic = 16 + if can_buy_unlimited(state, 'Green Potion', player) or can_buy_unlimited(state, 'Blue Potion', player): + if state.multiworld.item_functionality[player] == 'hard' and not fullrefill: + basemagic = basemagic + int(basemagic * 0.5 * bottle_count(state, player)) + elif state.multiworld.item_functionality[player] == 'expert' and not fullrefill: + basemagic = basemagic + int(basemagic * 0.25 * bottle_count(state, player)) + else: + basemagic = basemagic + basemagic * bottle_count(state, player) + return basemagic >= smallmagic + +def can_kill_most_things(state: CollectionState, player: int, enemies: int = 5) -> bool: + return (has_melee_weapon(state, player) + or state.has('Cane of Somaria', player) + or (state.has('Cane of Byrna', player) and (enemies < 6 or can_extend_magic(state, player))) + or can_shoot_arrows(state, player) + or state.has('Fire Rod', player) + or (state.has('Bombs (10)', player) and enemies < 6)) + +def can_get_good_bee(state: CollectionState, player: int) -> bool: + cave = state.multiworld.get_region('Good Bee Cave', player) + return ( + state.has_group("Bottles", player) and + state.has('Bug Catching Net', player) and + (state.has('Pegasus Boots', player) or (has_sword(state, player) and state.has('Quake', player))) and + cave.can_reach(state) and + is_not_bunny(state, cave, player) + ) + +def can_retrieve_tablet(state: CollectionState, player: int) -> bool: + return state.has('Book of Mudora', player) and (has_beam_sword(state, player) or + (state.multiworld.swordless[player] and + state.has("Hammer", player))) + +def has_sword(state: CollectionState, player: int) -> bool: + return state.has('Fighter Sword', player) \ + or state.has('Master Sword', player) \ + or state.has('Tempered Sword', player) \ + or state.has('Golden Sword', player) + +def has_beam_sword(state: CollectionState, player: int) -> bool: + return state.has('Master Sword', player) or state.has('Tempered Sword', player) or state.has('Golden Sword', + player) + +def has_melee_weapon(state: CollectionState, player: int) -> bool: + return has_sword(state, player) or state.has('Hammer', player) + +def has_fire_source(state: CollectionState, player: int) -> bool: + return state.has('Fire Rod', player) or state.has('Lamp', player) + +def can_melt_things(state: CollectionState, player: int) -> bool: + return state.has('Fire Rod', player) or \ + (state.has('Bombos', player) and + (state.multiworld.swordless[player] or + has_sword(state, player))) + +def has_misery_mire_medallion(state: CollectionState, player: int) -> bool: + return state.has(state.multiworld.required_medallions[player][0], player) + +def has_turtle_rock_medallion(state: CollectionState, player: int) -> bool: + return state.has(state.multiworld.required_medallions[player][1], player) + +def can_boots_clip_lw(state: CollectionState, player: int) -> bool: + if state.multiworld.mode[player] == 'inverted': + return state.has('Pegasus Boots', player) and state.has('Moon Pearl', player) + return state.has('Pegasus Boots', player) + +def can_boots_clip_dw(state: CollectionState, player: int) -> bool: + if state.multiworld.mode[player] != 'inverted': + return state.has('Pegasus Boots', player) and state.has('Moon Pearl', player) + return state.has('Pegasus Boots', player) + +def can_get_glitched_speed_dw(state: CollectionState, player: int) -> bool: + rules = [state.has('Pegasus Boots', player), any([state.has('Hookshot', player), has_sword(state, player)])] + if state.multiworld.mode[player] != 'inverted': + rules.append(state.has('Moon Pearl', player)) + return all(rules) \ No newline at end of file diff --git a/worlds/alttp/SubClasses.py b/worlds/alttp/SubClasses.py index 50f3ca47d9..5fc2aa0ba3 100644 --- a/worlds/alttp/SubClasses.py +++ b/worlds/alttp/SubClasses.py @@ -4,7 +4,6 @@ from enum import IntEnum from BaseClasses import Location, Item, ItemClassification, Region, MultiWorld - class ALttPLocation(Location): game: str = "A Link to the Past" crystal: bool @@ -81,6 +80,12 @@ class LTTPRegionType(IntEnum): class LTTPRegion(Region): type: LTTPRegionType + # will be set after making connections. + is_light_world: bool = False + is_dark_world: bool = False + + shop: Optional = None + def __init__(self, name: str, type_: LTTPRegionType, hint: str, player: int, multiworld: MultiWorld): super().__init__(name, player, multiworld, hint) self.type = type_ diff --git a/worlds/alttp/UnderworldGlitchRules.py b/worlds/alttp/UnderworldGlitchRules.py index f7e7736702..f3d78e365c 100644 --- a/worlds/alttp/UnderworldGlitchRules.py +++ b/worlds/alttp/UnderworldGlitchRules.py @@ -1,6 +1,8 @@ from BaseClasses import Entrance +from .SubClasses import LTTPRegion from worlds.generic.Rules import set_rule, add_rule +from .StateHelpers import can_bomb_clip, has_sword, has_beam_sword, has_fire_source, can_melt_things, has_misery_mire_medallion # We actually need the logic to properly "mark" these regions as Light or Dark world. # Therefore we need to make these connections during the normal link_entrances stage, rather than during set_rules. @@ -46,9 +48,9 @@ def dungeon_reentry_rules(world, player, clip: Entrance, dungeon_region: str, du if dungeon_entrance.name == 'Skull Woods Final Section': set_rule(clip, lambda state: False) # entrance doesn't exist until you fire rod it from the other side elif dungeon_entrance.name == 'Misery Mire': - add_rule(clip, lambda state: state.has_sword(player) and state.has_misery_mire_medallion(player)) # open the dungeon + add_rule(clip, lambda state: has_sword(state, player) and has_misery_mire_medallion(state, player)) # open the dungeon elif dungeon_entrance.name == 'Agahnims Tower': - add_rule(clip, lambda state: state.has('Cape', player) or state.has_beam_sword(player) or state.has('Beat Agahnim 1', player)) # kill/bypass barrier + add_rule(clip, lambda state: state.has('Cape', player) or has_beam_sword(state, player) or state.has('Beat Agahnim 1', player)) # kill/bypass barrier # Then we set a restriction on exiting the dungeon, so you can't leave unless you got in normally. add_rule(world.get_entrance(dungeon_exit, player), lambda state: dungeon_entrance.can_reach(state)) elif not fix_fake_worlds: # full, dungeonsfull; fixed dungeon exits, but no fake worlds fix @@ -66,21 +68,21 @@ def underworld_glitches_rules(world, player): # Ice Palace Entrance Clip # This is the easiest one since it's a simple internal clip. Just need to also add melting to freezor chest since it's otherwise assumed. - add_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: state.can_bomb_clip(world.get_region('Ice Palace (Entrance)', player), player), combine='or') - add_rule(world.get_location('Ice Palace - Freezor Chest', player), lambda state: state.can_melt_things(player)) + add_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: can_bomb_clip(state, world.get_region('Ice Palace (Entrance)', player), player), combine='or') + add_rule(world.get_location('Ice Palace - Freezor Chest', player), lambda state: can_melt_things(state, player)) # Kiki Skip kikiskip = world.get_entrance('Kiki Skip', player) - set_rule(kikiskip, lambda state: state.can_bomb_clip(kikiskip.parent_region, player)) + set_rule(kikiskip, lambda state: can_bomb_clip(state, kikiskip.parent_region, player)) dungeon_reentry_rules(world, player, kikiskip, 'Palace of Darkness (Entrance)', 'Palace of Darkness Exit') # Mire -> Hera -> Swamp # Using mire keys on other dungeon doors mire = world.get_region('Misery Mire (West)', player) - mire_clip = lambda state: state.can_reach('Misery Mire (West)', 'Region', player) and state.can_bomb_clip(mire, player) and state.has_fire_source(player) - hera_clip = lambda state: state.can_reach('Tower of Hera (Top)', 'Region', player) and state.can_bomb_clip(world.get_region('Tower of Hera (Top)', player), player) + mire_clip = lambda state: state.can_reach('Misery Mire (West)', 'Region', player) and can_bomb_clip(state, mire, player) and has_fire_source(state, player) + hera_clip = lambda state: state.can_reach('Tower of Hera (Top)', 'Region', player) and can_bomb_clip(state, world.get_region('Tower of Hera (Top)', player), player) add_rule(world.get_entrance('Tower of Hera Big Key Door', player), lambda state: mire_clip(state) and state.has('Big Key (Misery Mire)', player), combine='or') add_rule(world.get_entrance('Swamp Palace Small Key Door', player), lambda state: mire_clip(state), combine='or') add_rule(world.get_entrance('Swamp Palace (Center)', player), lambda state: mire_clip(state) or hera_clip(state), combine='or') diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 5f33b152b8..8ca82d43d5 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -20,9 +20,10 @@ from .Client import ALTTPSNIClient from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enemizer, apply_rom_settings, \ get_hash_string, get_base_rom_path, LttPDeltaPatch from .Rules import set_rules -from .Shops import create_shops, ShopSlotFill, ShopType, price_rate_display, price_type_display_name +from .Shops import create_shops, Shop, ShopSlotFill, ShopType, price_rate_display, price_type_display_name from .SubClasses import ALttPItem, LTTPRegionType from worlds.AutoWorld import World, WebWorld, LogicMixin +from .StateHelpers import can_buy_unlimited lttp_logger = logging.getLogger("A Link to the Past") @@ -116,6 +117,75 @@ class ALTTPWorld(World): option_definitions = alttp_options topology_present = True item_name_groups = item_name_groups + location_name_groups = { + "Blind's Hideout": {"Blind's Hideout - Top", "Blind's Hideout - Left", "Blind's Hideout - Right", + "Blind's Hideout - Far Left", "Blind's Hideout - Far Right"}, + "Kakariko Well": {"Kakariko Well - Top", "Kakariko Well - Left", "Kakariko Well - Middle", + "Kakariko Well - Right", "Kakariko Well - Bottom"}, + "Mini Moldorm Cave": {"Mini Moldorm Cave - Far Left", "Mini Moldorm Cave - Left", "Mini Moldorm Cave - Right", + "Mini Moldorm Cave - Far Right", "Mini Moldorm Cave - Generous Guy"}, + "Paradox Cave": {"Paradox Cave Lower - Far Left", "Paradox Cave Lower - Left", "Paradox Cave Lower - Right", + "Paradox Cave Lower - Far Right", "Paradox Cave Lower - Middle", "Paradox Cave Upper - Left", + "Paradox Cave Upper - Right"}, + "Hype Cave": {"Hype Cave - Top", "Hype Cave - Middle Right", "Hype Cave - Middle Left", + "Hype Cave - Bottom", "Hype Cave - Generous Guy"}, + "Hookshot Cave": {"Hookshot Cave - Top Right", "Hookshot Cave - Top Left", "Hookshot Cave - Bottom Right", + "Hookshot Cave - Bottom Left"}, + "Hyrule Castle": {"Hyrule Castle - Boomerang Chest", "Hyrule Castle - Map Chest", + "Hyrule Castle - Zelda's Chest", "Sewers - Dark Cross", "Sewers - Secret Room - Left", + "Sewers - Secret Room - Middle", "Sewers - Secret Room - Right"}, + "Eastern Palace": {"Eastern Palace - Compass Chest", "Eastern Palace - Big Chest", + "Eastern Palace - Cannonball Chest", "Eastern Palace - Big Key Chest", + "Eastern Palace - Map Chest", "Eastern Palace - Boss"}, + "Desert Palace": {"Desert Palace - Big Chest", "Desert Palace - Torch", "Desert Palace - Map Chest", + "Desert Palace - Compass Chest", "Desert Palace Big Key Chest", "Desert Palace - Boss"}, + "Tower of Hera": {"Tower of Hera - Basement Cage", "Tower of Hera - Map Chest", "Tower of Hera - Big Key Chest", + "Tower of Hera - Compass Chest", "Tower of Hera - Big Chest", "Tower of Hera - Boss"}, + "Palace of Darkness": {"Palace of Darkness - Shooter Room", "Palace of Darkness - The Arena - Bridge", + "Palace of Darkness - Stalfos Basement", "Palace of Darkness - Big Key Chest", + "Palace of Darkness - The Arena - Ledge", "Palace of Darkness - Map Chest", + "Palace of Darkness - Compass Chest", "Palace of Darkness - Dark Basement - Left", + "Palace of Darkness - Dark Basement - Right", "Palace of Darkness - Dark Maze - Top", + "Palace of Darkness - Dark Maze - Bottom", "Palace of Darkness - Big Chest", + "Palace of Darkness - Harmless Hellway", "Palace of Darkness - Boss"}, + "Swamp Palace": {"Swamp Palace - Entrance", "Swamp Palace - Swamp Palace - Map Chest", + "Swamp Palace - Big Chest", "Swamp Palace - Compass Chest", "Swamp Palace - Big Key Chest", + "Swamp Palace - West Chest", "Swamp Palace - Flooded Room - Left", + "Swamp Palace - Flooded Room - Right", "Swamp Palace - Waterfall Room", "Swamp Palace - Boss"}, + "Thieves' Town": {"Thieves' Town - Big Key Chest", "Thieves' Town - Map Chest", "Thieves' Town - Compass Chest", + "Thieves' Town - Ambush Chest", "Thieves' Town - Attic", "Thieves' Town - Big Chest", + "Thieves' Town - Blind's Cell", "Thieves' Town - Boss"}, + "Skull Woods": {"Skull Woods - Map Chest", "Skull Woods - Pinball Room", "Skull Woods - Compass Chest", + "Skull Woods - Pot Prison", "Skull Woods - Big Chest", "Skull Woods - Big Key Chest", + "Skull Woods - Bridge Room", "Skull Woods - Boss"}, + "Ice Palace": {"Ice Palace - Compass Chest", "Ice Palace - Freezor Chest", "Ice Palace - Big Chest", + "Ice Palace - Freezor Chest", "Ice Palace - Big Chest", "Ice Palace - Iced T Room", + "Ice Palace - Spike Room", "Ice Palace - Big Key Chest", "Ice Palace - Map Chest", + "Ice Palace - Boss"}, + "Misery Mire": {"Misery Mire - Big Chest", "Misery Mire - Map Chest", "Misery Mire - Main Lobby", + "Misery Mire - Bridge Chest", "Misery Mire - Spike Chest", "Misery Mire - Compass Chest", + "Misery Mire - Big Key Chest", "Misery Mire - Boss"}, + "Turtle Rock": {"Turtle Rock - Compass Chest", "Turtle Rock - Roller Room - Left", + "Turtle Rock - Roller Room - Right", "Turtle Room - Chain Chomps", "Turtle Rock - Big Key Chest", + "Turtle Rock - Big Chest", "Turtle Rock - Crystaroller Room", + "Turtle Rock - Eye Bridge - Bottom Left", "Turtle Rock - Eye Bridge - Bottom Right", + "Turtle Rock - Eye Bridge - Top Left", "Turtle Rock - Eye Bridge - Top Right", "Turtle Rock - Boss"}, + "Ganons Tower": {"Ganons Tower - Bob's Torch", "Ganon's Tower - Hope Room - Left", + "Ganons Tower - Hope Room - Right", "Ganons Tower - Tile Room", + "Ganons Tower - Compass Room - Top Left", "Ganons Tower - Compass Room - Top Right", + "Ganons Tower - Compass Room - Bottom Left", "Ganons Tower - Compass Room - Bottom Left", + "Ganons Tower - DMs Room - Top Left", "Ganons Tower - DMs Room - Top Right", + "Ganons Tower - DMs Room - Bottom Left", "Ganons Tower - DMs Room - Bottom Right", + "Ganons Tower - Map Chest", "Ganons Tower - Firesnake Room", + "Ganons Tower - Randomizer Room - Top Left", "Ganons Tower - Randomizer Room - Top Right", + "Ganons Tower - Randomizer Room - Bottom Left", "Ganons Tower - Randomizer Room - Bottom Right", + "Ganons Tower - Bob's Chest", "Ganons Tower - Big Chest", "Ganons Tower - Big Key Room - Left", + "Ganons Tower - Big Key Room - Right", "Ganons Tower - Big Key Chest", + "Ganons Tower - Mini Helmasaur Room - Left", "Ganons Tower - Mini Helmasaur Room - Right", + "Ganons Tower - Pre-Moldorm Room", "Ganons Tower - Validation Chest"}, + "Ganons Tower Climb": {"Ganons Tower - Mini Helmasaur Room - Left", "Ganons Tower - Mini Helmasaur Room - Right", + "Ganons Tower - Pre-Moldorm Room", "Ganons Tower - Validation Chest"}, + } hint_blacklist = {"Triforce"} item_name_to_id = {name: data.item_code for name, data in item_table.items() if type(data.item_code) == int} @@ -604,11 +674,7 @@ class ALTTPWorld(World): f'\n\nBosses{(f" ({self.multiworld.get_player_name(self.player)})" if self.multiworld.players > 1 else "")}:\n') spoiler_handle.write(' ' + '\n '.join([f'{x}: {y}' for x, y in bossmap.items()])) - def build_shop_info() -> typing.Dict: - shop = self.multiworld.shops[self.player] - if not shop.custom: - return None - + def build_shop_info(shop: Shop) -> typing.Dict[str, str]: shop_data = { "location": str(shop.region), "type": "Take Any" if shop.type == ShopType.TakeAny else "Shop" @@ -634,12 +700,12 @@ class ALTTPWorld(World): return shop_data - shop_data = build_shop_info() - if shop_data is not None: + if shop_info := [build_shop_info(shop) for shop in self.multiworld.shops if shop.custom]: spoiler_handle.write('\n\nShops:\n\n') - spoiler_handle.write(''.join("{} [{}]\n {}".format(shop_data['location'], shop_data['type'], "\n ".join( + for shop_data in shop_info: + spoiler_handle.write("{} [{}]\n {}\n".format(shop_data['location'], shop_data['type'], "\n ".join( item for item in [shop_data.get('item_0', None), shop_data.get('item_1', None), shop_data.get('item_2', None)] if - item)))) + item))) def get_filler_item_name(self) -> str: if self.multiworld.goal[self.player] == "icerodhunt": @@ -673,5 +739,5 @@ class ALttPLogic(LogicMixin): if self.multiworld.logic[player] == 'nologic': return True if self.multiworld.smallkey_shuffle[player] == smallkey_shuffle.option_universal: - return self.can_buy_unlimited('Small Key (Universal)', player) + return can_buy_unlimited(self, 'Small Key (Universal)', player) return self.prog_items[item, player] >= count diff --git a/worlds/alttp/test/options/TestPlandoBosses.py b/worlds/alttp/test/options/TestPlandoBosses.py index a6c3485f60..83c1510a3e 100644 --- a/worlds/alttp/test/options/TestPlandoBosses.py +++ b/worlds/alttp/test/options/TestPlandoBosses.py @@ -1,5 +1,5 @@ import unittest -import Generate +from BaseClasses import PlandoOptions from Options import PlandoBosses @@ -123,14 +123,14 @@ class TestPlandoBosses(unittest.TestCase): regular = MultiBosses.from_any(regular_string) # plando should work with boss plando - plandoed.verify(None, "Player", Generate.PlandoOptions.bosses) + plandoed.verify(None, "Player", PlandoOptions.bosses) self.assertTrue(plandoed.value.startswith(plandoed_string)) # plando should fall back to default without boss plando - plandoed.verify(None, "Player", Generate.PlandoOptions.items) + plandoed.verify(None, "Player", PlandoOptions.items) self.assertEqual(plandoed, MultiBosses.option_vanilla) # mixed should fall back to mode - mixed.verify(None, "Player", Generate.PlandoOptions.items) # should produce a warning and still work + mixed.verify(None, "Player", PlandoOptions.items) # should produce a warning and still work self.assertEqual(mixed, MultiBosses.option_shuffle) # mode stuff should just work - regular.verify(None, "Player", Generate.PlandoOptions.items) + regular.verify(None, "Player", PlandoOptions.items) self.assertEqual(regular, MultiBosses.option_shuffle) diff --git a/worlds/archipidle/Items.py b/worlds/archipidle/Items.py index 945d3aae60..2b5e6e9a81 100644 --- a/worlds/archipidle/Items.py +++ b/worlds/archipidle/Items.py @@ -300,4 +300,13 @@ item_table = ( 'Roomba with a Knife', 'Wet Cat', 'The missing moderator, Frostwares', + '1,793 Crossbows', + 'Holographic First Edition Charizard (Gen 1)', + 'VR Headset', + 'Archipelago 1.0 Release Date', + 'Strand of Galadriel\'s Hair', + 'Can of Meow-Mix', + 'Shake-Weight', + 'DVD Collection of Billy Mays Infomercials', + 'Old CD Key', ) diff --git a/worlds/archipidle/Rules.py b/worlds/archipidle/Rules.py index ddf906c21a..cdd48e7604 100644 --- a/worlds/archipidle/Rules.py +++ b/worlds/archipidle/Rules.py @@ -16,22 +16,28 @@ class ArchipIDLELogic(LogicMixin): def set_rules(world: MultiWorld, player: int): for i in range(16, 31): set_rule( - world.get_location(f"IDLE for at least {int(i / 2)} minutes {30 if (i % 2) else 0} seconds", player), + world.get_location(f"IDLE item number {i}", player), lambda state: state._archipidle_location_is_accessible(player, 4) ) for i in range(31, 51): set_rule( - world.get_location(f"IDLE for at least {int(i / 2)} minutes {30 if (i % 2) else 0} seconds", player), + world.get_location(f"IDLE item number {i}", player), lambda state: state._archipidle_location_is_accessible(player, 10) ) for i in range(51, 101): set_rule( - world.get_location(f"IDLE for at least {int(i / 2)} minutes {30 if (i % 2) else 0} seconds", player), + world.get_location(f"IDLE item number {i}", player), lambda state: state._archipidle_location_is_accessible(player, 20) ) + for i in range(101, 201): + set_rule( + world.get_location(f"IDLE item number {i}", player), + lambda state: state._archipidle_location_is_accessible(player, 40) + ) + world.completion_condition[player] =\ lambda state:\ - state.can_reach(world.get_location("IDLE for at least 50 minutes 0 seconds", player), "Location", player) + state.can_reach(world.get_location("IDLE item number 200", player), "Location", player) diff --git a/worlds/archipidle/__init__.py b/worlds/archipidle/__init__.py index 5054872dbe..768b7604e7 100644 --- a/worlds/archipidle/__init__.py +++ b/worlds/archipidle/__init__.py @@ -21,11 +21,11 @@ class ArchipIDLEWebWorld(WebWorld): class ArchipIDLEWorld(World): """ - An idle game which sends a check every thirty seconds, up to one hundred checks. + An idle game which sends a check every thirty seconds, up to two hundred checks. """ game = "ArchipIDLE" topology_present = False - data_version = 4 + data_version = 5 hidden = (datetime.now().month != 4) # ArchipIDLE is only visible during April web = ArchipIDLEWebWorld() @@ -37,32 +37,32 @@ class ArchipIDLEWorld(World): location_name_to_id = {} start_id = 9000 - for i in range(1, 101): - location_name_to_id[f"IDLE for at least {int(i / 2)} minutes {30 if (i % 2) else 0} seconds"] = start_id + for i in range(1, 201): + location_name_to_id[f"IDLE item number {i}"] = start_id start_id += 1 - def generate_basic(self): - item_table_copy = list(item_table) - self.multiworld.random.shuffle(item_table_copy) - - item_pool = [] - for i in range(100): - item = ArchipIDLEItem( - item_table_copy[i], - ItemClassification.progression if i < 20 else ItemClassification.filler, - self.item_name_to_id[item_table_copy[i]], - self.player - ) - item_pool.append(item) - - self.multiworld.itempool += item_pool - def set_rules(self): set_rules(self.multiworld, self.player) def create_item(self, name: str) -> Item: return Item(name, ItemClassification.progression, self.item_name_to_id[name], self.player) + def create_items(self): + item_table_copy = list(item_table) + self.multiworld.random.shuffle(item_table_copy) + + item_pool = [] + for i in range(200): + item = ArchipIDLEItem( + item_table_copy[i], + ItemClassification.progression if i < 40 else ItemClassification.filler, + self.item_name_to_id[item_table_copy[i]], + self.player + ) + item_pool.append(item) + + self.multiworld.itempool += item_pool + def create_regions(self): self.multiworld.regions += [ create_region(self.multiworld, self.player, 'Menu', None, ['Entrance to IDLE Zone']), diff --git a/worlds/archipidle/docs/en_ArchipIDLE.md b/worlds/archipidle/docs/en_ArchipIDLE.md index 066f3f05bf..3d57e3a055 100644 --- a/worlds/archipidle/docs/en_ArchipIDLE.md +++ b/worlds/archipidle/docs/en_ArchipIDLE.md @@ -2,8 +2,9 @@ ## What is this game? -ArchipIDLE is the 2022 Archipelago April Fools' Day joke. It is an idle game that sends a location check every -thirty seconds, up to one hundred checks. +ArchipIDLE was originally the 2022 Archipelago April Fools' Day joke. It is an idle game that sends a location check +on regular intervals. Updated annually with more items, gimmicks, and features, the game is visible +only during the month of April. ## Where is the settings page? diff --git a/worlds/bk_sudoku/__init__.py b/worlds/bk_sudoku/__init__.py index 4b0f0fb408..f914baf066 100644 --- a/worlds/bk_sudoku/__init__.py +++ b/worlds/bk_sudoku/__init__.py @@ -1,7 +1,8 @@ -from BaseClasses import Tutorial -from ..AutoWorld import World, WebWorld from typing import Dict +from BaseClasses import Tutorial +from ..AutoWorld import WebWorld, World + class Bk_SudokuWebWorld(WebWorld): settings_page = "games/Sudoku/info/en" @@ -24,6 +25,7 @@ class Bk_SudokuWorld(World): """ game = "Sudoku" web = Bk_SudokuWebWorld() + data_version = 1 item_name_to_id: Dict[str, int] = {} location_name_to_id: Dict[str, int] = {} diff --git a/worlds/blasphemous/Options.py b/worlds/blasphemous/Options.py index be43d8b7c8..b3bb3a6bf5 100644 --- a/worlds/blasphemous/Options.py +++ b/worlds/blasphemous/Options.py @@ -38,10 +38,12 @@ class ExpertLogic(Toggle): class Ending(Choice): - """Choose which ending is required to complete the game.""" + """Choose which ending is required to complete the game. + Ending A: Collect all thorn upgrades. + Ending C: Collect all thorn upgrades and the Holy Wound of Abnegation.""" display_name = "Ending" option_any_ending = 0 - option_ending_b = 1 + option_ending_a = 1 option_ending_c = 2 default = 0 diff --git a/worlds/blasphemous/Rules.py b/worlds/blasphemous/Rules.py index 6bf4a6858d..01d9643542 100644 --- a/worlds/blasphemous/Rules.py +++ b/worlds/blasphemous/Rules.py @@ -114,51 +114,66 @@ class BlasphemousLogic(LogicMixin): return self.has("Taranto to my Sister", player) def _blasphemous_tirana(self, player): - return self.has("Tirana of the Celestial Bastion", player) + return self.has("Tirana of the Celestial Bastion", player) and \ + self.has("Fervour Upgrade", player, 2) def _blasphemous_aubade(self, player): - return self.has("Aubade of the Nameless Guardian", player) + return self.has("Aubade of the Nameless Guardian", player) and \ + self.has("Fervour Upgrade", player, 2) def _blasphemous_cherub_6(self, player): return self.has_any({"Debla of the Lights", "Taranto to my Sister", "Verdiales of the Forsaken Hamlet", \ - "Tirana of the Celestial Bastion", "Cloistered Ruby"}, player) + "Cloistered Ruby"}, player) or \ + (self.has("Tirana of the Celestial Bastion", player) and \ + self.has("Fervour Upgrade", player, 2)) def _blasphemous_cherub_13(self, player): return self.has_any({"Ranged Skill", "Debla of the Lights", "Taranto to my Sister", \ - "Cante Jondo of the Three Sisters", "Aubade of the Nameless Guardian", "Tirana of the Celestial Bastion", \ - "Cloistered Ruby"}, player) + "Cante Jondo of the Three Sisters", "Cloistered Ruby"}, player) or \ + (self.has_any({"Aubade of the Nameless Guardian", "Tirana of the Celestial Bastion"}, player) and \ + self.has("Fervour Upgrade", player, 2)) def _blasphemous_cherub_20(self, player): return self.has_any({"Debla of the Lights", "Lorqiana", "Zarabanda of the Safe Haven", "Taranto to my Sister", \ - "Cante Jondo of the Three Sisters", "Aubade of the Nameless Guardian", "Tirana of the Celestial Bastion", \ - "Cloistered Ruby"}, player) + "Cante Jondo of the Three Sisters", "Cloistered Ruby"}, player) or \ + (self.has_any({"Aubade of the Nameless Guardian", "Tirana of the Celestial Bastion"}, player) and \ + self.has("Fervour Upgrade", player, 2)) def _blasphemous_cherub_21(self, player): return self.has_any({"Debla of the Lights", "Taranto to my Sister", "Cante Jondo of the Three Sisters", \ - "Verdiales of the Forsaken Hamlet", "Tirana of the Celestial Bastion", "Cloistered Ruby"}, player) + "Verdiales of the Forsaken Hamlet", "Cloistered Ruby"}, player) or \ + (self.has("Tirana of the Celestial Bastion", player) and \ + self.has("Fervour Upgrade"), player, 2) def _blasphemous_cherub_22_23_31_32(self, player): return self.has_any({"Debla of the Lights", "Taranto to my Sister", "Cloistered Ruby"}, player) def _blasphemous_cherub_24_33(self, player): return self.has_any({"Debla of the Lights", "Taranto to my Sister", "Cante Jondo of the Three Sisters", \ - "Tirana of the Celestial Bastion", "Cloistered Ruby"}, player) + "Cloistered Ruby"}, player) or \ + (self.has("Tirana of the Celestial Bastion", player) and \ + self.has("Fervour Upgrade", player, 2)) def _blasphemous_cherub_25(self, player): return self.has_any({"Debla of the Lights", "Lorquiana", "Taranto to my Sister", \ - "Cante Jondo of the Three Sisters", "Verdiales of the Forsaken Hamlet", "Aubade of the Nameless Guardian", \ - "Cantina of the Blue Rose", "Cloistered Ruby"}, player) + "Cante Jondo of the Three Sisters", "Verdiales of the Forsaken Hamlet", "Cantina of the Blue Rose", \ + "Cloistered Ruby"}, player) or \ + (self.has("Aubade of the Nameless Guardian", player) and \ + self.has("Fervour Upgrade", player, 2)) def _blasphemous_cherub_27(self, player): return self.has_any({"Ranged Skill", "Debla of the Lights", "Lorquiana", "Taranto to my Sister", \ - "Cante Jondo of the Three Sisters", "Aubade of the Nameless Guardian", "Cantina of the Blue Rose", \ - "Cloistered Ruby"}, player) + "Cante Jondo of the Three Sisters", "Cantina of the Blue Rose", "Cloistered Ruby"}, player) or \ + (self.has("Aubade of the Nameless Guardian", player) and \ + self.has("Fervour Upgrade", player, 2)) def _blasphemous_cherub_38(self, player): return self.has_any({"Ranged Skill", "Lorquiana", "Cante Jondo of the Three Sisters", \ - "Aubade of the Nameless Guardian", "Cantina of the Blue Rose", "Cloistered Ruby"}, player) or \ + "Cantina of the Blue Rose", "Cloistered Ruby"}, player) or \ (self.has("The Young Mason's Wheel", player) and \ - self.has("Brilliant Heart of Dawn", player)) + self.has("Brilliant Heart of Dawn", player)) or \ + (self.has("Aubade of the Nameless Guardian", player) and \ + self.has("Fervour Upgrade", player, 2)) def _blasphemous_wheel(self, player): return self.has("The Young Mason's Wheel", player) diff --git a/worlds/blasphemous/__init__.py b/worlds/blasphemous/__init__.py index 70aea1ef76..a7a86826c3 100644 --- a/worlds/blasphemous/__init__.py +++ b/worlds/blasphemous/__init__.py @@ -32,7 +32,7 @@ class BlasphemousWorld(World): game: str = "Blasphemous" web = BlasphemousWeb() - data_version: 1 + data_version = 1 item_name_to_id = {item["name"]: (base_id + index) for index, item in enumerate(item_table)} location_name_to_id = {loc["name"]: (base_id + index) for index, loc in enumerate(location_table)} diff --git a/worlds/clique/Options.py b/worlds/clique/Options.py new file mode 100644 index 0000000000..1d74d2c5a5 --- /dev/null +++ b/worlds/clique/Options.py @@ -0,0 +1,13 @@ +from typing import Dict + +from Options import Option, Toggle + + +class HardMode(Toggle): + """Only for masochists: requires 2 presses!""" + display_name = "Hard Mode" + + +clique_options: Dict[str, type(Option)] = { + "hard_mode": HardMode +} diff --git a/worlds/clique/__init__.py b/worlds/clique/__init__.py new file mode 100644 index 0000000000..c73d0437bf --- /dev/null +++ b/worlds/clique/__init__.py @@ -0,0 +1,109 @@ +from BaseClasses import Entrance, Item, ItemClassification, Location, MultiWorld, Region, Tutorial +from worlds.AutoWorld import WebWorld, World +from worlds.generic.Rules import set_rule +from .Options import clique_options + +item_table = { + "The feeling of satisfaction.": 69696969, + "Button Key": 69696968, +} + +location_table = { + "The Button": 69696969, + "The Desk": 69696968, +} + + +class CliqueWebWorld(WebWorld): + theme = "partyTime" + tutorials = [ + Tutorial( + tutorial_name="Start Guide", + description="A guide to playing Clique.", + language="English", + file_name="guide_en.md", + link="guide/en", + authors=["Phar"] + ) + ] + + +class CliqueWorld(World): + """The greatest game ever designed. Full of exciting gameplay!""" + + game = "Clique" + topology_present = False + data_version = 1 + web = CliqueWebWorld() + option_definitions = clique_options + + location_name_to_id = location_table + item_name_to_id = item_table + + def create_item(self, name: str) -> "Item": + return Item(name, ItemClassification.progression, self.item_name_to_id[name], self.player) + + def get_setting(self, name: str): + return getattr(self.multiworld, name)[self.player] + + def fill_slot_data(self) -> dict: + return {option_name: self.get_setting(option_name).value for option_name in self.option_definitions} + + def generate_basic(self) -> None: + self.multiworld.itempool.append(self.create_item("The feeling of satisfaction.")) + + if self.multiworld.hard_mode[self.player]: + self.multiworld.itempool.append(self.create_item("Button Key")) + + def create_regions(self) -> None: + if self.multiworld.hard_mode[self.player]: + self.multiworld.regions += [ + create_region(self.multiworld, self.player, "Menu", None, ["Entrance to THE BUTTON"]), + create_region(self.multiworld, self.player, "THE BUTTON", self.location_name_to_id) + ] + else: + self.multiworld.regions += [ + create_region(self.multiworld, self.player, "Menu", None, ["Entrance to THE BUTTON"]), + create_region(self.multiworld, self.player, "THE BUTTON", {"The Button": 69696969}) + ] + + self.multiworld.get_entrance("Entrance to THE BUTTON", self.player)\ + .connect(self.multiworld.get_region("THE BUTTON", self.player)) + + def get_filler_item_name(self) -> str: + return self.multiworld.random.choice(item_table) + + def set_rules(self) -> None: + if self.multiworld.hard_mode[self.player]: + set_rule( + self.multiworld.get_location("The Button", self.player), + lambda state: state.has("Button Key", self.player) + ) + + self.multiworld.completion_condition[self.player] = lambda state: \ + state.has("Button Key", self.player) + else: + self.multiworld.completion_condition[self.player] = lambda state: \ + state.has("The feeling of satisfaction.", self.player) + + +def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None): + region = Region(name, player, world) + if locations: + for location_name in locations.keys(): + location = CliqueLocation(player, location_name, locations[location_name], region) + region.locations.append(location) + + if exits: + for _exit in exits: + region.exits.append(Entrance(player, _exit, region)) + + return region + + +class CliqueItem(Item): + game = "Clique" + + +class CliqueLocation(Location): + game: str = "Clique" diff --git a/worlds/clique/docs/en_Clique.md b/worlds/clique/docs/en_Clique.md new file mode 100644 index 0000000000..bf0562e2ba --- /dev/null +++ b/worlds/clique/docs/en_Clique.md @@ -0,0 +1,11 @@ +# Clique + +## What is this game? + +Even I don't know. + +## Where is the settings page? + +The [player settings page for this game](../player-settings) contains all the options you need to configure +and export a config file. + diff --git a/worlds/clique/docs/guide_en.md b/worlds/clique/docs/guide_en.md new file mode 100644 index 0000000000..7b4b0f1c21 --- /dev/null +++ b/worlds/clique/docs/guide_en.md @@ -0,0 +1,6 @@ +# Clique Start Guide + +Go to the [Clique Game](http://clique.darkshare.site.nfoservers.com/) and enter the hostname:ip address, +then your slot name. + +Enjoy. \ No newline at end of file diff --git a/worlds/dark_souls_3/data/locations_data.py b/worlds/dark_souls_3/data/locations_data.py index 5a85916973..98eb8076c2 100644 --- a/worlds/dark_souls_3/data/locations_data.py +++ b/worlds/dark_souls_3/data/locations_data.py @@ -468,7 +468,6 @@ painted_world_table = { # DLC "PW: Vilhelm's Armor": 0x113130E8, "PW: Vilhelm's Gauntlets": 0x113134D0, "PW: Vilhelm's Leggings": 0x113138B8, - "PW: Vilhelm's Leggings": 0x113138B8, "PW: Valorheart": 0x00F646E0, # GRAVETENDER FIGHT "PW: Champions Bones": 0x40000869, # GRAVETENDER FIGHT "PW: Onyx Blade": 0x00222E00, # VILHELM FIGHT diff --git a/worlds/dkc3/Rom.py b/worlds/dkc3/Rom.py index 37b8ecf04c..c193e909eb 100644 --- a/worlds/dkc3/Rom.py +++ b/worlds/dkc3/Rom.py @@ -740,5 +740,5 @@ def get_base_rom_path(file_name: str = "") -> str: if not file_name: file_name = options["dkc3_options"]["rom_file"] if not os.path.exists(file_name): - file_name = Utils.local_path(file_name) + file_name = Utils.user_path(file_name) return file_name diff --git a/worlds/dkc3/docs/setup_en.md b/worlds/dkc3/docs/setup_en.md index 0ebe189a1e..f3ba472599 100644 --- a/worlds/dkc3/docs/setup_en.md +++ b/worlds/dkc3/docs/setup_en.md @@ -100,7 +100,7 @@ the lua you are using in your file explorer and copy the `socket.dll` to the bas Once you have changed the loaded core, you must restart BizHawk. 2. Load your ROM file if it hasn't already been loaded. 3. Click on the Tools menu and click on **Lua Console** -4. Click the button to open a new Lua script. +4. Click the Open Folder icon that says `Open Script` via the tooltip on mouse hover, or click the Script Menu then `Open Script...`, or press `Ctrl-O`. 5. Select the `Connector.lua` file included with your client - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the emulator is 64-bit or 32-bit. Please note the most recent versions of BizHawk are 64-bit only. diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index cda1ca1f66..bce4bb2d16 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -136,6 +136,7 @@ def generate_mod(world: "Factorio", output_directory: str): "goal": multiworld.goal[player].value, "energy_link": multiworld.energy_link[player].value, "useless_technologies": useless_technologies, + "chunk_shuffle": multiworld.chunk_shuffle[player].value if hasattr(multiworld, "chunk_shuffle") else 0, } for factorio_option in Options.factorio_options: diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py index eda5eec701..85394cae44 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -1,5 +1,6 @@ from __future__ import annotations import typing +import datetime from Options import Choice, OptionDict, OptionSet, ItemDict, Option, DefaultOnToggle, Range, DeathLink, Toggle from schema import Schema, Optional, And, Or @@ -197,6 +198,14 @@ class RecipeIngredients(Choice): option_science_pack = 1 +class RecipeIngredientsOffset(Range): + """When randomizing ingredients, remove or add this many "slots" of items. + For example, at -1 a randomized Automation Science Pack will only require 1 ingredient, instead of 2.""" + display_name = "Randomized Recipe Ingredients Offset" + range_start = -1 + range_end = 5 + + class FactorioStartItems(ItemDict): """Mapping of Factorio internal item-name to amount granted on start.""" display_name = "Starting Items" @@ -223,9 +232,36 @@ class AttackTrapCount(TrapCount): display_name = "Attack Traps" +class TeleportTrapCount(TrapCount): + """Trap items that when received trigger a random teleport.""" + display_name = "Teleport Traps" + + +class GrenadeTrapCount(TrapCount): + """Trap items that when received trigger a grenade explosion on each player.""" + display_name = "Grenade Traps" + + +class ClusterGrenadeTrapCount(TrapCount): + """Trap items that when received trigger a cluster grenade explosion on each player.""" + display_name = "Cluster Grenade Traps" + + +class ArtilleryTrapCount(TrapCount): + """Trap items that when received trigger an artillery shell on each player.""" + display_name = "Artillery Traps" + + +class AtomicRocketTrapCount(TrapCount): + """Trap items that when received trigger an atomic rocket explosion on each player. + Warning: there is no warning. The launch is instantaneous.""" + display_name = "Atomic Rocket Traps" + + class EvolutionTrapCount(TrapCount): """Trap items that when received increase the enemy evolution.""" display_name = "Evolution Traps" + range_end = 10 class EvolutionTrapIncrease(Range): @@ -404,12 +440,31 @@ factorio_options: typing.Dict[str, type(Option)] = { "free_sample_whitelist": FactorioFreeSampleWhitelist, "recipe_time": RecipeTime, "recipe_ingredients": RecipeIngredients, + "recipe_ingredients_offset": RecipeIngredientsOffset, "imported_blueprints": ImportedBlueprint, "world_gen": FactorioWorldGen, "progressive": Progressive, - "evolution_traps": EvolutionTrapCount, + "teleport_traps": TeleportTrapCount, + "grenade_traps": GrenadeTrapCount, + "cluster_grenade_traps": ClusterGrenadeTrapCount, + "artillery_traps": ArtilleryTrapCount, + "atomic_rocket_traps": AtomicRocketTrapCount, "attack_traps": AttackTrapCount, + "evolution_traps": EvolutionTrapCount, "evolution_trap_increase": EvolutionTrapIncrease, "death_link": DeathLink, - "energy_link": EnergyLink + "energy_link": EnergyLink, } + +# spoilers below. If you spoil it for yourself, please at least don't spoil it for anyone else. +if datetime.datetime.today().month == 4: + + class ChunkShuffle(Toggle): + """Entrance Randomizer.""" + display_name = "Chunk Shuffle" + + + if datetime.datetime.today().day > 1: + ChunkShuffle.__doc__ += """ + 2023 April Fool's option. Shuffles chunk border transitions.""" + factorio_options["chunk_shuffle"] = ChunkShuffle diff --git a/worlds/factorio/Technologies.py b/worlds/factorio/Technologies.py index 2db7f23dea..1c1939ee24 100644 --- a/worlds/factorio/Technologies.py +++ b/worlds/factorio/Technologies.py @@ -422,7 +422,7 @@ for root in sorted_rows: progressive = progressive_rows[root] assert all(tech in tech_table for tech in progressive), "declared a progressive technology without base technology" factorio_id += 1 - progressive_technology = Technology(root, technology_table[progressive_rows[root][0]].ingredients, factorio_id, + progressive_technology = Technology(root, technology_table[progressive[0]].ingredients, factorio_id, progressive, has_modifier=any(technology_table[tech].has_modifier for tech in progressive), unlocks=any(technology_table[tech].unlocks for tech in progressive)) diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index e691ac61c9..567ab0bbda 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -15,6 +15,9 @@ from .Technologies import base_tech_table, recipe_sources, base_technology_table get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies, \ fluids, stacking_items, valid_ingredients, progressive_rows from .Locations import location_pools, location_table +from worlds.LauncherComponents import Component, components + +components.append(Component("Factorio Client", "FactorioClient")) class FactorioWeb(WebWorld): @@ -35,6 +38,11 @@ class FactorioItem(Item): all_items = tech_table.copy() all_items["Attack Trap"] = factorio_base_id - 1 all_items["Evolution Trap"] = factorio_base_id - 2 +all_items["Teleport Trap"] = factorio_base_id - 3 +all_items["Grenade Trap"] = factorio_base_id - 4 +all_items["Cluster Grenade Trap"] = factorio_base_id - 5 +all_items["Artillery Trap"] = factorio_base_id - 6 +all_items["Atomic Rocket Trap"] = factorio_base_id - 7 class Factorio(World): @@ -43,7 +51,7 @@ class Factorio(World): Nauvis, an inhospitable world filled with dangerous creatures called biters. Build a factory, research new technologies, and become more efficient in your quest to build a rocket and return home. """ - game: str = "Factorio" + game = "Factorio" special_nodes = {"automation", "logistics", "rocket-silo"} custom_recipes: typing.Dict[str, Recipe] location_pool: typing.List[FactorioScienceLocation] @@ -52,12 +60,11 @@ class Factorio(World): web = FactorioWeb() item_name_to_id = all_items - # TODO: remove base_tech_table ~ 0.3.7 - location_name_to_id = {**base_tech_table, **location_table} + location_name_to_id = location_table item_name_groups = { "Progressive": set(progressive_tech_table.keys()), } - data_version = 6 + data_version = 7 required_client_version = (0, 3, 6) ordered_science_packs: typing.List[str] = MaxSciencePack.get_ordered_science_packs() @@ -73,8 +80,10 @@ class Factorio(World): generate_output = generate_mod def generate_early(self) -> None: - self.multiworld.max_tech_cost[self.player] = max(self.multiworld.max_tech_cost[self.player], - self.multiworld.min_tech_cost[self.player]) + # if max < min, then swap max and min + if self.multiworld.max_tech_cost[self.player] < self.multiworld.min_tech_cost[self.player]: + self.multiworld.min_tech_cost[self.player].value, self.multiworld.max_tech_cost[self.player].value = \ + self.multiworld.max_tech_cost[self.player].value, self.multiworld.min_tech_cost[self.player].value self.tech_mix = self.multiworld.tech_cost_mix[self.player] self.skip_silo = self.multiworld.silo[self.player].value == Silo.option_spawn @@ -87,14 +96,25 @@ class Factorio(World): nauvis = Region("Nauvis", player, self.multiworld) location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + \ - self.multiworld.evolution_traps[player].value + self.multiworld.attack_traps[player].value + self.multiworld.evolution_traps[player] + \ + self.multiworld.attack_traps[player] + \ + self.multiworld.teleport_traps[player] + \ + self.multiworld.grenade_traps[player] + \ + self.multiworld.cluster_grenade_traps[player] + \ + self.multiworld.atomic_rocket_traps[player] + \ + self.multiworld.artillery_traps[player] location_pool = [] for pack in sorted(self.multiworld.max_science_pack[self.player].get_allowed_packs()): location_pool.extend(location_pools[pack]) + try: + location_names = self.multiworld.random.sample(location_pool, location_count) + except ValueError as e: + # should be "ValueError: Sample larger than population or is negative" + raise Exception("Too many traps for too few locations. Either decrease the trap count, " + f"or increase the location count (higher max science pack). (Player {self.player})") from e - location_names = self.multiworld.random.sample(location_pool, location_count) self.locations = [FactorioScienceLocation(player, loc_name, self.location_name_to_id[loc_name], nauvis) for loc_name in location_names] distribution: TechCostDistribution = self.multiworld.tech_cost_distribution[self.player] @@ -132,6 +152,14 @@ class Factorio(World): crash.connect(nauvis) self.multiworld.regions += [menu, nauvis] + def create_items(self) -> None: + player = self.player + traps = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery", "Atomic Rocket") + for trap_name in traps: + self.multiworld.itempool.extend(self.create_item(f"{trap_name} Trap") for _ in + range(getattr(self.multiworld, + f"{trap_name.lower().replace(' ', '_')}_traps")[player])) + def set_rules(self): world = self.multiworld player = self.player @@ -184,10 +212,6 @@ class Factorio(World): player = self.player want_progressives = collections.defaultdict(lambda: self.multiworld.progressive[player]. want_progressives(self.multiworld.random)) - self.multiworld.itempool.extend(self.create_item("Evolution Trap") for _ in - range(self.multiworld.evolution_traps[player].value)) - self.multiworld.itempool.extend(self.create_item("Attack Trap") for _ in - range(self.multiworld.attack_traps[player].value)) cost_sorted_locations = sorted(self.locations, key=lambda location: location.name) special_index = {"automation": 0, @@ -265,10 +289,11 @@ class Factorio(World): 2: "chemistry"} return categories.get(liquids, category) - def make_quick_recipe(self, original: Recipe, pool: list, allow_liquids: int = 2) -> Recipe: + def make_quick_recipe(self, original: Recipe, pool: list, allow_liquids: int = 2, + ingredients_offset: int = 0) -> Recipe: new_ingredients = {} liquids_used = 0 - for _ in original.ingredients: + for _ in range(len(original.ingredients) + ingredients_offset): new_ingredient = pool.pop() if new_ingredient in fluids: while liquids_used == allow_liquids and new_ingredient in fluids: @@ -282,7 +307,7 @@ class Factorio(World): original.products, original.energy) def make_balanced_recipe(self, original: Recipe, pool: typing.Set[str], factor: float = 1, - allow_liquids: int = 2) -> Recipe: + allow_liquids: int = 2, ingredients_offset: int = 0) -> Recipe: """Generate a recipe from pool with time and cost similar to original * factor""" new_ingredients = {} # have to first sort for determinism, while filtering out non-stacking items @@ -291,7 +316,7 @@ class Factorio(World): self.multiworld.random.shuffle(pool) target_raw = int(sum((count for ingredient, count in original.base_cost.items())) * factor) target_energy = original.total_energy * factor - target_num_ingredients = len(original.ingredients) + target_num_ingredients = len(original.ingredients) + ingredients_offset remaining_raw = target_raw remaining_energy = target_energy remaining_num_ingredients = target_num_ingredients @@ -382,12 +407,13 @@ class Factorio(World): return custom_technologies def set_custom_recipes(self): + ingredients_offset = self.multiworld.recipe_ingredients_offset[self.player] original_rocket_part = recipes["rocket-part"] science_pack_pools = get_science_pack_pools() valid_pool = sorted(science_pack_pools[self.multiworld.max_science_pack[self.player].get_max_pack()] & valid_ingredients) self.multiworld.random.shuffle(valid_pool) self.custom_recipes = {"rocket-part": Recipe("rocket-part", original_rocket_part.category, - {valid_pool[x]: 10 for x in range(3)}, + {valid_pool[x]: 10 for x in range(3 + ingredients_offset)}, original_rocket_part.products, original_rocket_part.energy)} @@ -397,7 +423,8 @@ class Factorio(World): valid_pool += sorted(science_pack_pools[pack]) self.multiworld.random.shuffle(valid_pool) if pack in recipes: # skips over space science pack - new_recipe = self.make_quick_recipe(recipes[pack], valid_pool) + new_recipe = self.make_quick_recipe(recipes[pack], valid_pool, ingredients_offset= + ingredients_offset) self.custom_recipes[pack] = new_recipe if self.multiworld.silo[self.player].value == Silo.option_randomize_recipe \ @@ -407,21 +434,27 @@ class Factorio(World): valid_pool |= science_pack_pools[pack] if self.multiworld.silo[self.player].value == Silo.option_randomize_recipe: - new_recipe = self.make_balanced_recipe(recipes["rocket-silo"], valid_pool, - factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7) + new_recipe = self.make_balanced_recipe( + recipes["rocket-silo"], valid_pool, + factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7, + ingredients_offset=ingredients_offset) self.custom_recipes["rocket-silo"] = new_recipe if self.multiworld.satellite[self.player].value == Satellite.option_randomize_recipe: - new_recipe = self.make_balanced_recipe(recipes["satellite"], valid_pool, - factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7) + new_recipe = self.make_balanced_recipe( + recipes["satellite"], valid_pool, + factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7, + ingredients_offset=ingredients_offset) self.custom_recipes["satellite"] = new_recipe bridge = "ap-energy-bridge" new_recipe = self.make_quick_recipe( - Recipe(bridge, "crafting", {"replace_1": 1, "replace_2": 1, "replace_3": 1}, + Recipe(bridge, "crafting", {"replace_1": 1, "replace_2": 1, "replace_3": 1, + "replace_4": 1, "replace_5": 1, "replace_6": 1}, {bridge: 1}, 10), - sorted(science_pack_pools[self.multiworld.max_science_pack[self.player].get_ordered_science_packs()[0]])) + sorted(science_pack_pools[self.multiworld.max_science_pack[self.player].get_ordered_science_packs()[0]]), + ingredients_offset=ingredients_offset) for ingredient_name in new_recipe.ingredients: - new_recipe.ingredients[ingredient_name] = self.multiworld.random.randint(10, 100) + new_recipe.ingredients[ingredient_name] = self.multiworld.random.randint(50, 500) self.custom_recipes[bridge] = new_recipe needed_recipes = self.multiworld.max_science_pack[self.player].get_allowed_packs() | {"rocket-part"} @@ -452,7 +485,7 @@ class Factorio(World): tech_table[name], self.player) item = FactorioItem(name, - ItemClassification.trap if "Trap" in name else ItemClassification.filler, + ItemClassification.trap if name.endswith("Trap") else ItemClassification.filler, all_items[name], self.player) return item diff --git a/worlds/factorio/data/mod/LICENSE.md b/worlds/factorio/data/mod/LICENSE.md index 1299d90b46..5cf699c413 100644 --- a/worlds/factorio/data/mod/LICENSE.md +++ b/worlds/factorio/data/mod/LICENSE.md @@ -1,7 +1,8 @@ The MIT License (MIT) -Copyright (c) 2021 Berserker55 and Dewiniaid +Copyright (c) 2023 Berserker55 +Copyright (c) 2021 Dewiniaid Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/worlds/factorio/data/mod/lib.lua b/worlds/factorio/data/mod/lib.lua index 8bcd7325ee..2b18f119a4 100644 --- a/worlds/factorio/data/mod/lib.lua +++ b/worlds/factorio/data/mod/lib.lua @@ -1,32 +1,3 @@ -function filter_ingredients(ingredients, ingredient_filter) - local new_ingredient_list = {} - for _, ingredient_table in pairs(ingredients) do - if ingredient_filter[ingredient_table[1]] then -- name of ingredient_table - table.insert(new_ingredient_list, ingredient_table) - end - end - - return new_ingredient_list -end - -function add_ingredients(ingredients, added_ingredients) - local new_ingredient_list = table.deepcopy(ingredients) - for new_ingredient, count in pairs(added_ingredients) do - local found = false - for _, old_ingredient in pairs(ingredients) do - if old_ingredient[1] == new_ingredient then - found = true - break - end - end - if not found then - table.insert(new_ingredient_list, {new_ingredient, count}) - end - end - - return new_ingredient_list -end - function get_any_stack_size(name) local item = game.item_prototypes[name] if item ~= nil then @@ -50,4 +21,19 @@ function split(s, sep) string.gsub(s, pattern, function(c) fields[#fields + 1] = c end) return fields +end + +function random_offset_position(position, offset) + return {x=position.x+math.random(-offset, offset), y=position.y+math.random(-1024, 1024)} +end + +function fire_entity_at_players(entity_name, speed) + for _, player in ipairs(game.forces["player"].players) do + current_character = player.character + if current_character ~= nil then + current_character.surface.create_entity{name=entity_name, + position=random_offset_position(current_character.position, 128), + target=current_character, speed=speed} + end + end end \ No newline at end of file diff --git a/worlds/factorio/data/mod_template/control.lua b/worlds/factorio/data/mod_template/control.lua index 077c7b03a9..4ecfdb4630 100644 --- a/worlds/factorio/data/mod_template/control.lua +++ b/worlds/factorio/data/mod_template/control.lua @@ -11,7 +11,7 @@ TRAP_EVO_FACTOR = {{ evolution_trap_increase }} / 100 MAX_SCIENCE_PACK = {{ max_science_pack }} GOAL = {{ goal }} ARCHIPELAGO_DEATH_LINK_SETTING = "archipelago-death-link-{{ slot_player }}-{{ seed_name }}" -ENERGY_INCREMENT = {{ energy_link * 1000000 }} +ENERGY_INCREMENT = {{ energy_link * 10000000 }} ENERGY_LINK_EFFICIENCY = 0.75 if settings.global[ARCHIPELAGO_DEATH_LINK_SETTING].value then @@ -22,6 +22,119 @@ end CURRENTLY_DEATH_LOCK = 0 +{% if chunk_shuffle %} +LAST_POSITIONS = {} +GENERATOR = nil +NORTH = 1 +EAST = 2 +SOUTH = 3 +WEST = 4 +ER_COLOR = {1, 1, 1, 0.2} +ER_SEED = {{ random.randint(4294967295, 2*4294967295)}} +CURRENTLY_MOVING = false +ER_FRAMES = {} +CHUNK_OFFSET = { +[NORTH] = {0, 1}, +[EAST] = {1, 0}, +[SOUTH] = {0, -1}, +[WEST] = {-1, 0} +} + + +function on_player_changed_position(event) + if CURRENTLY_MOVING == true then + return + end + local player_id = event.player_index + local player = game.get_player(player_id) + local character = player.character -- can be nil, such as spectators + + if character == nil then + return + end + local last_position = LAST_POSITIONS[player_id] + if last_position == nil then + LAST_POSITIONS[player_id] = character.position + return + end + + last_x_chunk = math.floor(last_position.x / 32) + current_x_chunk = math.floor(character.position.x / 32) + last_y_chunk = math.floor(last_position.y / 32) + current_y_chunk = math.floor(character.position.y / 32) + if (ER_FRAMES[player_id] ~= nil and rendering.is_valid(ER_FRAMES[player_id])) then + rendering.destroy(ER_FRAMES[player_id]) + end + ER_FRAMES[player_id] = rendering.draw_rectangle{ + color=ER_COLOR, width=1, filled=false, left_top = {current_x_chunk*32, current_y_chunk*32}, + right_bottom={current_x_chunk*32+32, current_y_chunk*32+32}, players={player}, time_to_live=60, + draw_on_ground= true, only_in_alt_mode = true, surface=character.surface} + if current_x_chunk == last_x_chunk and current_y_chunk == last_y_chunk then -- nothing needs doing + return + end + if ((last_position.x - character.position.x) ^ 2 + (last_position.y - character.position.y) ^ 2) > 4000 then + -- distance too high, death or other teleport took place + LAST_POSITIONS[player_id] = character.position + return + end + -- we'll need a deterministic random state + if GENERATOR == nil or not GENERATOR.valid then + GENERATOR = game.create_random_generator() + end + + -- sufficiently random pattern + GENERATOR.re_seed((ER_SEED + (last_x_chunk * 1730000000) + (last_y_chunk * 97000)) % 4294967295) + -- we now need all 4 exit directions deterministically shuffled to the 4 outgoing directions. + local exit_table = { + [1] = 1, + [2] = 2, + [3] = 3, + [4] = 4 + } + exit_table = fisher_yates_shuffle(exit_table) + if current_x_chunk > last_x_chunk then -- going right/east + outbound_direction = EAST + elseif current_x_chunk < last_x_chunk then -- going left/west + outbound_direction = WEST + end + + if current_y_chunk > last_y_chunk then -- going down/south + outbound_direction = SOUTH + elseif current_y_chunk < last_y_chunk then -- going up/north + outbound_direction = NORTH + end + local target_direction = exit_table[outbound_direction] + + local target_position = {(CHUNK_OFFSET[target_direction][1] + last_x_chunk) * 32 + 16, + (CHUNK_OFFSET[target_direction][2] + last_y_chunk) * 32 + 16} + target_position = character.surface.find_non_colliding_position(character.prototype.name, + target_position, 32, 0.5) + if target_position ~= nil then + rendering.draw_circle{color = ER_COLOR, radius = 1, filled = true, + target = {character.position.x, character.position.y}, surface = character.surface, + time_to_live = 300, draw_on_ground = true} + rendering.draw_line{color = ER_COLOR, width = 3, gap_length = 0.5, dash_length = 0.5, + from = {character.position.x, character.position.y}, to = target_position, + surface = character.surface, + time_to_live = 300, draw_on_ground = true} + CURRENTLY_MOVING = true -- prevent recursive event + character.teleport(target_position) + CURRENTLY_MOVING = false + end + LAST_POSITIONS[player_id] = character.position +end + +function fisher_yates_shuffle(tbl) + for i = #tbl, 2, -1 do + local j = GENERATOR(i) + tbl[i], tbl[j] = tbl[j], tbl[i] + end + return tbl +end + +script.on_event(defines.events.on_player_changed_position, on_player_changed_position) +{% endif %} + function on_check_energy_link(event) --- assuming 1 MJ increment and 5MJ battery: --- first 2 MJ request fill, last 2 MJ push energy, middle 1 MJ does nothing @@ -180,8 +293,8 @@ script.on_event(defines.events.on_player_removed, on_player_removed) function on_rocket_launched(event) if event.rocket and event.rocket.valid and global.forcedata[event.rocket.force.name]['victory'] == 0 then - if event.rocket.get_item_count("satellite") > 0 or GOAL == 0 then - global.forcedata[event.rocket.force.name]['victory'] = 1 + if event.rocket.get_item_count("satellite") > 0 or GOAL == 0 then + global.forcedata[event.rocket.force.name]['victory'] = 1 dumpInfo(event.rocket.force) game.set_game_state { @@ -190,8 +303,8 @@ function on_rocket_launched(event) can_continue = true, victorious_force = event.rocket.force } - end - end + end + end end script.on_event(defines.events.on_rocket_launched, on_rocket_launched) @@ -236,7 +349,7 @@ function update_player(index) end else player.print("Unable to receive " .. count .. "x [item=" .. name .. "] as this item does not exist.") - samples[name] = nil + samples[name] = nil end end @@ -254,9 +367,9 @@ script.on_event(defines.events.on_player_main_inventory_changed, update_player_e function add_samples(force, name, count) local function add_to_table(t) if count <= 0 then - -- Fixes a bug with single craft, if a recipe gives 0 of a given item. - return - end + -- Fixes a bug with single craft, if a recipe gives 0 of a given item. + return + end t[name] = (t[name] or 0) + count end -- Add to global table of earned samples for future new players @@ -298,8 +411,8 @@ script.on_event(defines.events.on_research_finished, function(event) --Don't acknowledge AP research as an Editor Extensions test force --Also no need for free samples in the Editor extensions testing surfaces, as these testing surfaces --are worked on exclusively in editor mode. - return - end + return + end if technology.researched and string.find(technology.name, "ap%-") == 1 then -- check if it came from the server anyway, then we don't need to double send. dumpInfo(technology.force) --is sendable @@ -510,6 +623,37 @@ commands.add_command("ap-print", "Used by the Archipelago client to print messag game.print(call.parameter) end) +TRAP_TABLE = { +["Attack Trap"] = function () + game.surfaces["nauvis"].build_enemy_base(game.forces["player"].get_spawn_position(game.get_surface(1)), 25) +end, +["Evolution Trap"] = function () + game.forces["enemy"].evolution_factor = game.forces["enemy"].evolution_factor + (TRAP_EVO_FACTOR * (1 - game.forces["enemy"].evolution_factor)) + game.print({"", "New evolution factor:", game.forces["enemy"].evolution_factor}) +end, +["Teleport Trap"] = function () + for _, player in ipairs(game.forces["player"].players) do + current_character = player.character + if current_character ~= nil then + current_character.teleport(current_character.surface.find_non_colliding_position( + current_character.prototype.name, random_offset_position(current_character.position, 1024), 0, 1)) + end + end +end, +["Grenade Trap"] = function () + fire_entity_at_players("grenade", 0.1) +end, +["Cluster Grenade Trap"] = function () + fire_entity_at_players("cluster-grenade", 0.1) +end, +["Artillery Trap"] = function () + fire_entity_at_players("artillery-projectile", 1) +end, +["Atomic Rocket Trap"] = function () + fire_entity_at_players("atomic-rocket", 0.1) +end, +} + commands.add_command("ap-get-technology", "Grant a technology, used by the Archipelago Client.", function(call) if global.index_sync == nil then global.index_sync = {} @@ -552,18 +696,11 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi tech.researched = true end end - elseif item_name == "Attack Trap" then - if global.index_sync[index] == nil then -- not yet received trap - game.print({"", "Received Attack Trap from ", source}) - global.index_sync[index] = item_name - local spawn_position = force.get_spawn_position(game.get_surface(1)) - game.surfaces["nauvis"].build_enemy_base(spawn_position, 25) - end - elseif item_name == "Evolution Trap" then + elseif TRAP_TABLE[item_name] ~= nil then if global.index_sync[index] == nil then -- not yet received trap global.index_sync[index] = item_name - game.forces["enemy"].evolution_factor = game.forces["enemy"].evolution_factor + (TRAP_EVO_FACTOR * (1 - game.forces["enemy"].evolution_factor)) - game.print({"", "Received Evolution Trap from ", source, ". New factor:", game.forces["enemy"].evolution_factor}) + game.print({"", "Received ", item_name, " from ", source}) + TRAP_TABLE[item_name]() end else game.print("Unknown Item " .. item_name) diff --git a/worlds/factorio/data/mod_template/data.lua b/worlds/factorio/data/mod_template/data.lua index d790831478..82053453ea 100644 --- a/worlds/factorio/data/mod_template/data.lua +++ b/worlds/factorio/data/mod_template/data.lua @@ -14,9 +14,9 @@ local energy_bridge = table.deepcopy(data.raw["accumulator"]["accumulator"]) energy_bridge.name = "ap-energy-bridge" energy_bridge.minable.result = "ap-energy-bridge" energy_bridge.localised_name = "Archipelago EnergyLink Bridge" -energy_bridge.energy_source.buffer_capacity = "5MJ" -energy_bridge.energy_source.input_flow_limit = "1MW" -energy_bridge.energy_source.output_flow_limit = "1MW" +energy_bridge.energy_source.buffer_capacity = "50MJ" +energy_bridge.energy_source.input_flow_limit = "10MW" +energy_bridge.energy_source.output_flow_limit = "10MW" tint_icon(energy_bridge, energy_bridge_tint()) energy_bridge.picture.layers[1].tint = energy_bridge_tint() energy_bridge.picture.layers[1].hr_version.tint = energy_bridge_tint() diff --git a/worlds/factorio/docs/en_Factorio.md b/worlds/factorio/docs/en_Factorio.md index 8f2c3f69fd..61bceb3820 100644 --- a/worlds/factorio/docs/en_Factorio.md +++ b/worlds/factorio/docs/en_Factorio.md @@ -39,6 +39,6 @@ EnergyLink is an energy storage supported by certain games that is shared across In Factorio, if enabled in the player settings, EnergyLink Bridge buildings can be crafted and placed, which allow depositing excess energy and supplementing energy deficits, much like Accumulators. -Each placed EnergyLink Bridge provides 1 MW of throughput. The shared storage has unlimited capacity, but 25% of energy +Each placed EnergyLink Bridge provides 10 MW of throughput. The shared storage has unlimited capacity, but 25% of energy is lost during depositing. The amount of energy currently in the shared storage is displayed in the Archipelago client. It can also be queried by typing `/energy-link` in-game. diff --git a/worlds/ff1/Options.py b/worlds/ff1/Options.py index 2ab4b33622..0993d103d5 100644 --- a/worlds/ff1/Options.py +++ b/worlds/ff1/Options.py @@ -4,14 +4,17 @@ from Options import OptionDict class Locations(OptionDict): + """to roll settings go to https://finalfantasyrandomizer.com/""" display_name = "locations" class Items(OptionDict): + """to roll settings go to https://finalfantasyrandomizer.com/""" display_name = "items" class Rules(OptionDict): + """to roll settings go to https://finalfantasyrandomizer.com/""" display_name = "rules" diff --git a/worlds/generic/Rules.py b/worlds/generic/Rules.py index 98fb560aee..fb783edb67 100644 --- a/worlds/generic/Rules.py +++ b/worlds/generic/Rules.py @@ -1,7 +1,7 @@ import collections import typing -from BaseClasses import LocationProgressType, MultiWorld +from BaseClasses import LocationProgressType, MultiWorld, Location, Region, Entrance if typing.TYPE_CHECKING: import BaseClasses @@ -143,14 +143,41 @@ def add_item_rule(location: "BaseClasses.Location", rule: ItemRule, combine: str def item_in_locations(state: "BaseClasses.CollectionState", item: str, player: int, locations: typing.Sequence["BaseClasses.Location"]) -> bool: for location in locations: - if item_name(state, location[0], location[1]) == (item, player): + if location_item_name(state, location[0], location[1]) == (item, player): return True return False -def item_name(state: "BaseClasses.CollectionState", location: str, player: int) -> \ +def location_item_name(state: "BaseClasses.CollectionState", location: str, player: int) -> \ typing.Optional[typing.Tuple[str, int]]: location = state.multiworld.get_location(location, player) if location.item is None: return None return location.item.name, location.item.player + + +def allow_self_locking_items(spot: typing.Union[Location, Region], *item_names: str) -> None: + """ + This function sets rules on the supplied spot, such that the supplied item_name(s) can possibly be placed there. + + spot: Location or Region that the item(s) are allowed to be placed in + item_names: item name or names that are allowed to be placed in the Location or Region + """ + player = spot.player + + def add_allowed_rules(area: typing.Union[Location, Entrance], location: Location) -> None: + def set_always_allow(location: Location, rule: typing.Callable) -> None: + location.always_allow = rule + + for item_name in item_names: + add_rule(area, lambda state, item_name=item_name: + location_item_name(state, location.name, player) == (item_name, player), "or") + set_always_allow(location, lambda state, item: + item.player == player and item.name in [item_name for item_name in item_names]) + + if isinstance(spot, Region): + for entrance in spot.entrances: + for location in spot.locations: + add_allowed_rules(entrance, location) + else: + add_allowed_rules(spot, spot) diff --git a/worlds/generic/__init__.py b/worlds/generic/__init__.py index 4c7c14c48f..732dc51196 100644 --- a/worlds/generic/__init__.py +++ b/worlds/generic/__init__.py @@ -42,6 +42,7 @@ class GenericWorld(World): } hidden = True web = GenericWeb() + data_version = 1 def generate_early(self): self.multiworld.player_types[self.player] = SlotType.spectator # mark as spectator diff --git a/worlds/generic/docs/mac_en.md b/worlds/generic/docs/mac_en.md index 1e2d235c8c..dc7e32fa98 100644 --- a/worlds/generic/docs/mac_en.md +++ b/worlds/generic/docs/mac_en.md @@ -2,7 +2,8 @@ Archipelago does not have a compiled release on macOS. However, it is possible to run from source code on macOS. This guide expects you to have some experience with running software from the terminal. ## Prerequisite Software Here is a list of software to install and source code to download. -1. Python 3.8 or newer from the [macOS Python downloads page](https://www.python.org/downloads/macos/). +1. Python 3.9 "universal2" or newer from the [macOS Python downloads page](https://www.python.org/downloads/macos/). + **Python 3.11 is not supported yet.** 2. Xcode from the [macOS App Store](https://apps.apple.com/us/app/xcode/id497799835). 3. The source code from the [Archipelago releases page](https://github.com/ArchipelagoMW/Archipelago/releases). 4. The asset with darwin in the name from the [SNI Github releases page](https://github.com/alttpo/sni/releases). diff --git a/worlds/hylics2/__init__.py b/worlds/hylics2/__init__.py index b5fede32c3..b12beaaa3a 100644 --- a/worlds/hylics2/__init__.py +++ b/worlds/hylics2/__init__.py @@ -2,8 +2,8 @@ import random from typing import Dict, Any from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification from worlds.generic.Rules import set_rule -from ..AutoWorld import World, WebWorld -from . import Items, Locations, Options, Rules, Exits +from . import Exits, Items, Locations, Options, Rules +from ..AutoWorld import WebWorld, World class Hylics2Web(WebWorld): @@ -20,13 +20,13 @@ class Hylics2Web(WebWorld): class Hylics2World(World): """ - Hylics 2 is a surreal and unusual RPG, with a bizarre yet unique visual style. Play as Wayne, + Hylics 2 is a surreal and unusual RPG, with a bizarre yet unique visual style. Play as Wayne, travel the world, and gather your allies to defeat the nefarious Gibby in his Hylemxylem! """ game: str = "Hylics 2" web = Hylics2Web() - all_items = {**Items.item_table, **Items.gesture_item_table, **Items.party_item_table, + all_items = {**Items.item_table, **Items.gesture_item_table, **Items.party_item_table, **Items.medallion_item_table} all_locations = {**Locations.location_table, **Locations.tv_location_table, **Locations.party_location_table, **Locations.medallion_location_table} @@ -37,7 +37,7 @@ class Hylics2World(World): topology_present: bool = True - data_version: 1 + data_version = 1 start_location = "Waynehouse" @@ -59,7 +59,7 @@ class Hylics2World(World): def create_event(self, event: str): return Hylics2Item(event, ItemClassification.progression_skip_balancing, None, self.player) - + # set random starting location if option is enabled def generate_early(self): if self.multiworld.random_start[self.player]: @@ -76,7 +76,7 @@ class Hylics2World(World): def generate_basic(self): # create item pool pool = [] - + # add regular items for i, data in Items.item_table.items(): if data["count"] > 0: @@ -114,7 +114,7 @@ class Hylics2World(World): gestures = list(Items.gesture_item_table.items()) tvs = list(Locations.tv_location_table.items()) - # if Extra Items in Logic is enabled place CHARGE UP first and make sure it doesn't get + # if Extra Items in Logic is enabled place CHARGE UP first and make sure it doesn't get # placed at Sage Airship: TV if self.multiworld.extra_items_in_logic[self.player]: tv = self.multiworld.random.choice(tvs) @@ -122,7 +122,7 @@ class Hylics2World(World): while tv[1]["name"] == "Sage Airship: TV": tv = self.multiworld.random.choice(tvs) self.multiworld.get_location(tv[1]["name"], self.player)\ - .place_locked_item(self.add_item(gestures[gest][1]["name"], gestures[gest][1]["classification"], + .place_locked_item(self.add_item(gestures[gest][1]["name"], gestures[gest][1]["classification"], gestures[gest])) gestures.remove(gestures[gest]) tvs.remove(tv) @@ -182,7 +182,7 @@ class Hylics2World(World): 16: Region("Sage Airship", self.player, self.multiworld), 17: Region("Hylemxylem", self.player, self.multiworld) } - + # create regions from table for i, reg in region_table.items(): self.multiworld.regions.append(reg) @@ -214,7 +214,7 @@ class Hylics2World(World): for i, data in Locations.tv_location_table.items(): region_table[data["region"]].locations\ .append(Hylics2Location(self.player, data["name"], i, region_table[data["region"]])) - + # add party member locations if option is enabled if self.multiworld.party_shuffle[self.player]: for i, data in Locations.party_location_table.items(): @@ -241,4 +241,4 @@ class Hylics2Location(Location): class Hylics2Item(Item): - game: str = "Hylics 2" \ No newline at end of file + game: str = "Hylics 2" diff --git a/worlds/kh2/Items.py b/worlds/kh2/Items.py new file mode 100644 index 0000000000..aa0e326c3d --- /dev/null +++ b/worlds/kh2/Items.py @@ -0,0 +1,1009 @@ +import typing + +from BaseClasses import Item +from .Names import ItemName + + +class KH2Item(Item): + game: str = "Kingdom Hearts 2" + + +class ItemData(typing.NamedTuple): + code: typing.Optional[int] + quantity: int = 0 + kh2id: int = 0 + # Save+ mem addr + memaddr: int = 0 + # some items have bitmasks. if bitmask>0 bitor to give item else + bitmask: int = 0 + # if ability then + ability: bool = False + + +Reports_Table = { + ItemName.SecretAnsemsReport1: ItemData(0x130000, 1, 226, 0x36C4, 6), + ItemName.SecretAnsemsReport2: ItemData(0x130001, 1, 227, 0x36C4, 7), + ItemName.SecretAnsemsReport3: ItemData(0x130002, 1, 228, 0x36C5, 0), + ItemName.SecretAnsemsReport4: ItemData(0x130003, 1, 229, 0x36C5, 1), + ItemName.SecretAnsemsReport5: ItemData(0x130004, 1, 230, 0x36C5, 2), + ItemName.SecretAnsemsReport6: ItemData(0x130005, 1, 231, 0x36C5, 3), + ItemName.SecretAnsemsReport7: ItemData(0x130006, 1, 232, 0x36C5, 4), + ItemName.SecretAnsemsReport8: ItemData(0x130007, 1, 233, 0x36C5, 5), + ItemName.SecretAnsemsReport9: ItemData(0x130008, 1, 234, 0x36C5, 6), + ItemName.SecretAnsemsReport10: ItemData(0x130009, 1, 235, 0x36C5, 7), + ItemName.SecretAnsemsReport11: ItemData(0x13000A, 1, 236, 0x36C6, 0), + ItemName.SecretAnsemsReport12: ItemData(0x13000B, 1, 237, 0x36C6, 1), + ItemName.SecretAnsemsReport13: ItemData(0x13000C, 1, 238, 0x36C6, 2), +} + +Progression_Table = { + ItemName.ProofofConnection: ItemData(0x13000D, 1, 593, 0x36B2), + ItemName.ProofofNonexistence: ItemData(0x13000E, 1, 594, 0x36B3), + ItemName.ProofofPeace: ItemData(0x13000F, 1, 595, 0x36B4), + ItemName.PromiseCharm: ItemData(0x130010, 1, 524, 0x3694), + ItemName.NamineSketches: ItemData(0x130011, 1, 368, 0x3642), + ItemName.CastleKey: ItemData(0x130012, 2, 460, 0x365D), # dummy 13 + ItemName.BattlefieldsofWar: ItemData(0x130013, 2, 54, 0x35AE), + ItemName.SwordoftheAncestor: ItemData(0x130014, 2, 55, 0x35AF), + ItemName.BeastsClaw: ItemData(0x130015, 2, 59, 0x35B3), + ItemName.BoneFist: ItemData(0x130016, 2, 60, 0x35B4), + ItemName.ProudFang: ItemData(0x130017, 2, 61, 0x35B5), + ItemName.SkillandCrossbones: ItemData(0x130018, 2, 62, 0x35B6), + ItemName.Scimitar: ItemData(0x130019, 2, 72, 0x35C0), + ItemName.MembershipCard: ItemData(0x13001A, 2, 369, 0x3643), + ItemName.IceCream: ItemData(0x13001B, 3, 375, 0x3649), + # Changed to 3 instead of one poster, picture and ice cream respectively + ItemName.WaytotheDawn: ItemData(0x13001C, 1, 73, 0x35C1), + # currently first visit locking doesn't work for twtnw.When goa is updated should be 2 + ItemName.IdentityDisk: ItemData(0x13001D, 2, 74, 0x35C2), + ItemName.TornPages: ItemData(0x13001E, 5, 32, 0x3598), + +} +Forms_Table = { + ItemName.ValorForm: ItemData(0x13001F, 1, 26, 0x36C0, 1), + ItemName.WisdomForm: ItemData(0x130020, 1, 27, 0x36C0, 2), + ItemName.LimitForm: ItemData(0x130021, 1, 563, 0x36CA, 3), + ItemName.MasterForm: ItemData(0x130022, 1, 31, 0x36C0, 6), + ItemName.FinalForm: ItemData(0x130023, 1, 29, 0x36C0, 4), +} +Magic_Table = { + ItemName.FireElement: ItemData(0x130024, 3, 21, 0x3594), + ItemName.BlizzardElement: ItemData(0x130025, 3, 22, 0x3595), + ItemName.ThunderElement: ItemData(0x130026, 3, 23, 0x3596), + ItemName.CureElement: ItemData(0x130027, 3, 24, 0x3597), + ItemName.MagnetElement: ItemData(0x130028, 3, 87, 0x35CF), + ItemName.ReflectElement: ItemData(0x130029, 3, 88, 0x35D0), + ItemName.Genie: ItemData(0x13002A, 1, 159, 0x36C4, 4), + ItemName.PeterPan: ItemData(0x13002B, 1, 160, 0x36C4, 5), + ItemName.Stitch: ItemData(0x13002C, 1, 25, 0x36C0, 0), + ItemName.ChickenLittle: ItemData(0x13002D, 1, 383, 0x36C0, 3), +} + +Movement_Table = { + ItemName.HighJump: ItemData(0x13002E, 4, 94, 0x05E, 0, True), + ItemName.QuickRun: ItemData(0x13002F, 4, 98, 0x062, 0, True), + ItemName.DodgeRoll: ItemData(0x130030, 4, 564, 0x234, 0, True), + ItemName.AerialDodge: ItemData(0x130031, 4, 102, 0x066, 0, True), + ItemName.Glide: ItemData(0x130032, 4, 106, 0x06A, 0, True), +} + +Keyblade_Table = { + ItemName.Oathkeeper: ItemData(0x130033, 1, 42, 0x35A2), + ItemName.Oblivion: ItemData(0x130034, 1, 43, 0x35A3), + ItemName.StarSeeker: ItemData(0x130035, 1, 480, 0x367B), + ItemName.HiddenDragon: ItemData(0x130036, 1, 481, 0x367C), + ItemName.HerosCrest: ItemData(0x130037, 1, 484, 0x367F), + ItemName.Monochrome: ItemData(0x130038, 1, 485, 0x3680), + ItemName.FollowtheWind: ItemData(0x130039, 1, 486, 0x3681), + ItemName.CircleofLife: ItemData(0x13003A, 1, 487, 0x3682), + ItemName.PhotonDebugger: ItemData(0x13003B, 1, 488, 0x3683), + ItemName.GullWing: ItemData(0x13003C, 1, 489, 0x3684), + ItemName.RumblingRose: ItemData(0x13003D, 1, 490, 0x3685), + ItemName.GuardianSoul: ItemData(0x13003E, 1, 491, 0x3686), + ItemName.WishingLamp: ItemData(0x13003F, 1, 492, 0x3687), + ItemName.DecisivePumpkin: ItemData(0x130040, 1, 493, 0x3688), + ItemName.SleepingLion: ItemData(0x130041, 1, 494, 0x3689), + ItemName.SweetMemories: ItemData(0x130042, 1, 495, 0x368A), + ItemName.MysteriousAbyss: ItemData(0x130043, 1, 496, 0x368B), + ItemName.TwoBecomeOne: ItemData(0x130044, 1, 543, 0x3698), + ItemName.FatalCrest: ItemData(0x130045, 1, 497, 0x368C), + ItemName.BondofFlame: ItemData(0x130046, 1, 498, 0x368D), + ItemName.Fenrir: ItemData(0x130047, 1, 499, 0x368E), + ItemName.UltimaWeapon: ItemData(0x130048, 1, 500, 0x368F), + ItemName.WinnersProof: ItemData(0x130049, 1, 544, 0x3699), + ItemName.Pureblood: ItemData(0x13004A, 1, 71, 0x35BF), +} +Staffs_Table = { + ItemName.Centurion2: ItemData(0x13004B, 1, 546, 0x369B), + ItemName.MeteorStaff: ItemData(0x13004C, 1, 150, 0x35F1), + ItemName.NobodyLance: ItemData(0x13004D, 1, 155, 0x35F6), + ItemName.PreciousMushroom: ItemData(0x13004E, 1, 549, 0x369E), + ItemName.PreciousMushroom2: ItemData(0x13004F, 1, 550, 0x369F), + ItemName.PremiumMushroom: ItemData(0x130050, 1, 551, 0x36A0), + ItemName.RisingDragon: ItemData(0x130051, 1, 154, 0x35F5), + ItemName.SaveTheQueen2: ItemData(0x130052, 1, 503, 0x3692), + ItemName.ShamansRelic: ItemData(0x130053, 1, 156, 0x35F7), +} +Shields_Table = { + ItemName.AkashicRecord: ItemData(0x130054, 1, 146, 0x35ED), + ItemName.FrozenPride2: ItemData(0x130055, 1, 553, 0x36A2), + ItemName.GenjiShield: ItemData(0x130056, 1, 145, 0x35EC), + ItemName.MajesticMushroom: ItemData(0x130057, 1, 556, 0x36A5), + ItemName.MajesticMushroom2: ItemData(0x130058, 1, 557, 0x36A6), + ItemName.NobodyGuard: ItemData(0x130059, 1, 147, 0x35EE), + ItemName.OgreShield: ItemData(0x13005A, 1, 141, 0x35E8), + ItemName.SaveTheKing2: ItemData(0x13005B, 1, 504, 0x3693), + ItemName.UltimateMushroom: ItemData(0x13005C, 1, 558, 0x36A7), +} +Accessory_Table = { + ItemName.AbilityRing: ItemData(0x13005D, 1, 8, 0x3587), + ItemName.EngineersRing: ItemData(0x13005E, 1, 9, 0x3588), + ItemName.TechniciansRing: ItemData(0x13005F, 1, 10, 0x3589), + ItemName.SkillRing: ItemData(0x130060, 1, 38, 0x359F), + ItemName.SkillfulRing: ItemData(0x130061, 1, 39, 0x35A0), + ItemName.ExpertsRing: ItemData(0x130062, 1, 11, 0x358A), + ItemName.MastersRing: ItemData(0x130063, 1, 34, 0x359B), + ItemName.CosmicRing: ItemData(0x130064, 1, 52, 0x35AD), + ItemName.ExecutivesRing: ItemData(0x130065, 1, 599, 0x36B5), + ItemName.SardonyxRing: ItemData(0x130066, 1, 12, 0x358B), + ItemName.TourmalineRing: ItemData(0x130067, 1, 13, 0x358C), + ItemName.AquamarineRing: ItemData(0x130068, 1, 14, 0x358D), + ItemName.GarnetRing: ItemData(0x130069, 1, 15, 0x358E), + ItemName.DiamondRing: ItemData(0x13006A, 1, 16, 0x358F), + ItemName.SilverRing: ItemData(0x13006B, 1, 17, 0x3590), + ItemName.GoldRing: ItemData(0x13006C, 1, 18, 0x3591), + ItemName.PlatinumRing: ItemData(0x13006D, 1, 19, 0x3592), + ItemName.MythrilRing: ItemData(0x13006E, 1, 20, 0x3593), + ItemName.OrichalcumRing: ItemData(0x13006F, 1, 28, 0x359A), + ItemName.SoldierEarring: ItemData(0x130070, 1, 40, 0x35A6), + ItemName.FencerEarring: ItemData(0x130071, 1, 46, 0x35A7), + ItemName.MageEarring: ItemData(0x130072, 1, 47, 0x35A8), + ItemName.SlayerEarring: ItemData(0x130073, 1, 48, 0x35AC), + ItemName.Medal: ItemData(0x130074, 1, 53, 0x35B0), + ItemName.MoonAmulet: ItemData(0x130075, 1, 35, 0x359C), + ItemName.StarCharm: ItemData(0x130076, 1, 36, 0x359E), + ItemName.CosmicArts: ItemData(0x130077, 1, 56, 0x35B1), + ItemName.ShadowArchive: ItemData(0x130078, 1, 57, 0x35B2), + ItemName.ShadowArchive2: ItemData(0x130079, 1, 58, 0x35B7), + ItemName.FullBloom: ItemData(0x13007A, 1, 64, 0x35B9), + ItemName.FullBloom2: ItemData(0x13007B, 1, 66, 0x35BB), + ItemName.DrawRing: ItemData(0x13007C, 1, 65, 0x35BA), + ItemName.LuckyRing: ItemData(0x13007D, 1, 63, 0x35B8), +} +Armor_Table = { + ItemName.ElvenBandana: ItemData(0x13007E, 1, 67, 0x35BC), + ItemName.DivineBandana: ItemData(0x13007F, 1, 68, 0x35BD), + ItemName.ProtectBelt: ItemData(0x130080, 1, 78, 0x35C7), + ItemName.GaiaBelt: ItemData(0x130081, 1, 79, 0x35CA), + ItemName.PowerBand: ItemData(0x130082, 1, 69, 0x35BE), + ItemName.BusterBand: ItemData(0x130083, 1, 70, 0x35C6), + ItemName.CosmicBelt: ItemData(0x130084, 1, 111, 0x35D1), + ItemName.FireBangle: ItemData(0x130085, 1, 173, 0x35D7), + ItemName.FiraBangle: ItemData(0x130086, 1, 174, 0x35D8), + ItemName.FiragaBangle: ItemData(0x130087, 1, 197, 0x35D9), + ItemName.FiragunBangle: ItemData(0x130088, 1, 284, 0x35DA), + ItemName.BlizzardArmlet: ItemData(0x130089, 1, 286, 0x35DC), + ItemName.BlizzaraArmlet: ItemData(0x13008A, 1, 287, 0x35DD), + ItemName.BlizzagaArmlet: ItemData(0x13008B, 1, 288, 0x35DE), + ItemName.BlizzagunArmlet: ItemData(0x13008C, 1, 289, 0x35DF), + ItemName.ThunderTrinket: ItemData(0x13008D, 1, 291, 0x35E2), + ItemName.ThundaraTrinket: ItemData(0x13008E, 1, 292, 0x35E3), + ItemName.ThundagaTrinket: ItemData(0x13008F, 1, 293, 0x35E4), + ItemName.ThundagunTrinket: ItemData(0x130090, 1, 294, 0x35E5), + ItemName.ShockCharm: ItemData(0x130091, 1, 132, 0x35D2), + ItemName.ShockCharm2: ItemData(0x130092, 1, 133, 0x35D3), + ItemName.ShadowAnklet: ItemData(0x130093, 1, 296, 0x35F9), + ItemName.DarkAnklet: ItemData(0x130094, 1, 297, 0x35FB), + ItemName.MidnightAnklet: ItemData(0x130095, 1, 298, 0x35FC), + ItemName.ChaosAnklet: ItemData(0x130096, 1, 299, 0x35FD), + ItemName.ChampionBelt: ItemData(0x130097, 1, 305, 0x3603), + ItemName.AbasChain: ItemData(0x130098, 1, 301, 0x35FF), + ItemName.AegisChain: ItemData(0x130099, 1, 302, 0x3600), + ItemName.Acrisius: ItemData(0x13009A, 1, 303, 0x3601), + ItemName.Acrisius2: ItemData(0x13009B, 1, 307, 0x3605), + ItemName.CosmicChain: ItemData(0x13009C, 1, 308, 0x3606), + ItemName.PetiteRibbon: ItemData(0x13009D, 1, 306, 0x3604), + ItemName.Ribbon: ItemData(0x13009E, 1, 304, 0x3602), + ItemName.GrandRibbon: ItemData(0x13009F, 1, 157, 0x35D4), +} +Usefull_Table = { + ItemName.MickyMunnyPouch: ItemData(0x1300A0, 3, 535, 0x3695), # 5000 munny per + ItemName.OletteMunnyPouch: ItemData(0x1300A1, 6, 362, 0x363C), # 2500 munny per + ItemName.HadesCupTrophy: ItemData(0x1300A2, 1, 537, 0x3696), + ItemName.UnknownDisk: ItemData(0x1300A3, 1, 462, 0x365F), + ItemName.OlympusStone: ItemData(0x1300A4, 1, 370, 0x3644), + ItemName.MaxHPUp: ItemData(0x1300A5, 20, 470, 0x3671), + ItemName.MaxMPUp: ItemData(0x1300A6, 4, 471, 0x3672), + ItemName.DriveGaugeUp: ItemData(0x1300A7, 6, 472, 0x3673), + ItemName.ArmorSlotUp: ItemData(0x1300A8, 3, 473, 0x3674), + ItemName.AccessorySlotUp: ItemData(0x1300A9, 3, 474, 0x3675), + ItemName.ItemSlotUp: ItemData(0x1300AA, 5, 463, 0x3660), +} +SupportAbility_Table = { + ItemName.Scan: ItemData(0x1300AB, 2, 138, 0x08A, 0, True), + ItemName.AerialRecovery: ItemData(0x1300AC, 1, 158, 0x09E, 0, True), + ItemName.ComboMaster: ItemData(0x1300AD, 1, 539, 0x21B, 0, True), + ItemName.ComboPlus: ItemData(0x1300AE, 3, 162, 0x0A2, 0, True), + ItemName.AirComboPlus: ItemData(0x1300AF, 3, 163, 0x0A3, 0, True), + ItemName.ComboBoost: ItemData(0x1300B0, 2, 390, 0x186, 0, True), + ItemName.AirComboBoost: ItemData(0x1300B1, 2, 391, 0x187, 0, True), + ItemName.ReactionBoost: ItemData(0x1300B2, 3, 392, 0x188, 0, True), + ItemName.FinishingPlus: ItemData(0x1300B3, 3, 393, 0x189, 0, True), + ItemName.NegativeCombo: ItemData(0x1300B4, 2, 394, 0x18A, 0, True), + ItemName.BerserkCharge: ItemData(0x1300B5, 2, 395, 0x18B, 0, True), + ItemName.DamageDrive: ItemData(0x1300B6, 2, 396, 0x18C, 0, True), + ItemName.DriveBoost: ItemData(0x1300B7, 2, 397, 0x18D, 0, True), + ItemName.FormBoost: ItemData(0x1300B8, 3, 398, 0x18E, 0, True), + ItemName.SummonBoost: ItemData(0x1300B9, 1, 399, 0x18F, 0, True), + ItemName.ExperienceBoost: ItemData(0x1300BA, 2, 401, 0x191, 0, True), + ItemName.Draw: ItemData(0x1300BB, 4, 405, 0x195, 0, True), + ItemName.Jackpot: ItemData(0x1300BC, 2, 406, 0x196, 0, True), + ItemName.LuckyLucky: ItemData(0x1300BD, 3, 407, 0x197, 0, True), + ItemName.DriveConverter: ItemData(0x1300BE, 2, 540, 0x21C, 0, True), + ItemName.FireBoost: ItemData(0x1300BF, 2, 408, 0x198, 0, True), + ItemName.BlizzardBoost: ItemData(0x1300C0, 2, 409, 0x199, 0, True), + ItemName.ThunderBoost: ItemData(0x1300C1, 2, 410, 0x19A, 0, True), + ItemName.ItemBoost: ItemData(0x1300C2, 2, 411, 0x19B, 0, True), + ItemName.MPRage: ItemData(0x1300C3, 2, 412, 0x19C, 0, True), + ItemName.MPHaste: ItemData(0x1300C4, 2, 413, 0x19D, 0, True), + ItemName.MPHastera: ItemData(0x1300C5, 2, 421, 0x1A5, 0, True), + ItemName.MPHastega: ItemData(0x1300C6, 1, 422, 0x1A6, 0, True), + ItemName.Defender: ItemData(0x1300C7, 2, 414, 0x19E, 0, True), + ItemName.DamageControl: ItemData(0x1300C8, 2, 542, 0x21E, 0, True), + ItemName.NoExperience: ItemData(0x1300C9, 1, 404, 0x194, 0, True), + ItemName.LightDarkness: ItemData(0x1300CA, 1, 541, 0x21D, 0, True), + ItemName.MagicLock: ItemData(0x1300CB, 1, 403, 0x193, 0, True), + ItemName.LeafBracer: ItemData(0x1300CC, 1, 402, 0x192, 0, True), + ItemName.CombinationBoost: ItemData(0x1300CD, 1, 400, 0x190, 0, True), + ItemName.OnceMore: ItemData(0x1300CE, 1, 416, 0x1A0, 0, True), + ItemName.SecondChance: ItemData(0x1300CF, 1, 415, 0x19F, 0, True), +} +ActionAbility_Table = { + ItemName.Guard: ItemData(0x1300D0, 1, 82, 0x052, 0, True), + ItemName.UpperSlash: ItemData(0x1300D1, 1, 137, 0x089, 0, True), + ItemName.HorizontalSlash: ItemData(0x1300D2, 1, 271, 0x10F, 0, True), + ItemName.FinishingLeap: ItemData(0x1300D3, 1, 267, 0x10B, 0, True), + ItemName.RetaliatingSlash: ItemData(0x1300D4, 1, 273, 0x111, 0, True), + ItemName.Slapshot: ItemData(0x1300D5, 1, 262, 0x106, 0, True), + ItemName.DodgeSlash: ItemData(0x1300D6, 1, 263, 0x107, 0, True), + ItemName.FlashStep: ItemData(0x1300D7, 1, 559, 0x22F, 0, True), + ItemName.SlideDash: ItemData(0x1300D8, 1, 264, 0x108, 0, True), + ItemName.VicinityBreak: ItemData(0x1300D9, 1, 562, 0x232, 0, True), + ItemName.GuardBreak: ItemData(0x1300DA, 1, 265, 0x109, 0, True), + ItemName.Explosion: ItemData(0x1300DB, 1, 266, 0x10A, 0, True), + ItemName.AerialSweep: ItemData(0x1300DC, 1, 269, 0x10D, 0, True), + ItemName.AerialDive: ItemData(0x1300DD, 1, 560, 0x230, 0, True), + ItemName.AerialSpiral: ItemData(0x1300DE, 1, 270, 0x10E, 0, True), + ItemName.AerialFinish: ItemData(0x1300DF, 1, 272, 0x110, 0, True), + ItemName.MagnetBurst: ItemData(0x1300E0, 1, 561, 0x231, 0, True), + ItemName.Counterguard: ItemData(0x1300E1, 1, 268, 0x10C, 0, True), + ItemName.AutoValor: ItemData(0x1300E2, 1, 385, 0x181, 0, True), + ItemName.AutoWisdom: ItemData(0x1300E3, 1, 386, 0x182, 0, True), + ItemName.AutoLimit: ItemData(0x1300E4, 1, 568, 0x238, 0, True), + ItemName.AutoMaster: ItemData(0x1300E5, 1, 387, 0x183, 0, True), + ItemName.AutoFinal: ItemData(0x1300E6, 1, 388, 0x184, 0, True), + ItemName.AutoSummon: ItemData(0x1300E7, 1, 389, 0x185, 0, True), + ItemName.TrinityLimit: ItemData(0x1300E8, 1, 198, 0x0C6, 0, True), +} +Items_Table = { + ItemName.PowerBoost: ItemData(0x1300E9, 1, 276, 0x3666), + ItemName.MagicBoost: ItemData(0x1300EA, 1, 277, 0x3667), + ItemName.DefenseBoost: ItemData(0x1300EB, 1, 278, 0x3668), + ItemName.APBoost: ItemData(0x1300EC, 1, 279, 0x3669), +} + +# These items cannot be in other games so these are done locally in kh2 +DonaldAbility_Table = { + ItemName.DonaldFire: ItemData(0x1300ED, 1, 165, 0xA5, 0, True), + ItemName.DonaldBlizzard: ItemData(0x1300EE, 1, 166, 0xA6, 0, True), + ItemName.DonaldThunder: ItemData(0x1300EF, 1, 167, 0xA7, 0, True), + ItemName.DonaldCure: ItemData(0x1300F0, 1, 168, 0xA8, 0, True), + ItemName.Fantasia: ItemData(0x1300F1, 1, 199, 0xC7, 0, True), + ItemName.FlareForce: ItemData(0x1300F2, 1, 200, 0xC8, 0, True), + ItemName.DonaldMPRage: ItemData(0x1300F3, 3, 412, 0x19C, 0, True), + ItemName.DonaldJackpot: ItemData(0x1300F4, 1, 406, 0x196, 0, True), + ItemName.DonaldLuckyLucky: ItemData(0x1300F5, 3, 407, 0x197, 0, True), + ItemName.DonaldFireBoost: ItemData(0x1300F6, 2, 408, 0x198, 0, True), + ItemName.DonaldBlizzardBoost: ItemData(0x1300F7, 2, 409, 0x199, 0, True), + ItemName.DonaldThunderBoost: ItemData(0x1300F8, 2, 410, 0x19A, 0, True), + ItemName.DonaldMPHaste: ItemData(0x1300F9, 1, 413, 0x19D, 0, True), + ItemName.DonaldMPHastera: ItemData(0x1300FA, 2, 421, 0x1A5, 0, True), + ItemName.DonaldMPHastega: ItemData(0x1300FB, 2, 422, 0x1A6, 0, True), + ItemName.DonaldAutoLimit: ItemData(0x1300FC, 1, 417, 0x1A1, 0, True), + ItemName.DonaldHyperHealing: ItemData(0x1300FD, 2, 419, 0x1A3, 0, True), + ItemName.DonaldAutoHealing: ItemData(0x1300FE, 1, 420, 0x1A4, 0, True), + ItemName.DonaldItemBoost: ItemData(0x1300FF, 1, 411, 0x19B, 0, True), + ItemName.DonaldDamageControl: ItemData(0x130100, 2, 542, 0x21E, 0, True), + ItemName.DonaldDraw: ItemData(0x130101, 1, 405, 0x195, 0, True), +} +GoofyAbility_Table = { + ItemName.GoofyTornado: ItemData(0x130102, 1, 423, 0x1A7, 0, True), + ItemName.GoofyTurbo: ItemData(0x130103, 1, 425, 0x1A9, 0, True), + ItemName.GoofyBash: ItemData(0x130104, 1, 429, 0x1AD, 0, True), + ItemName.TornadoFusion: ItemData(0x130105, 1, 201, 0xC9, 0, True), + ItemName.Teamwork: ItemData(0x130106, 1, 202, 0xCA, 0, True), + ItemName.GoofyDraw: ItemData(0x130107, 1, 405, 0x195, 0, True), + ItemName.GoofyJackpot: ItemData(0x130108, 1, 406, 0x196, 0, True), + ItemName.GoofyLuckyLucky: ItemData(0x130109, 1, 407, 0x197, 0, True), + ItemName.GoofyItemBoost: ItemData(0x13010A, 2, 411, 0x19B, 0, True), + ItemName.GoofyMPRage: ItemData(0x13010B, 2, 412, 0x19C, 0, True), + ItemName.GoofyDefender: ItemData(0x13010C, 2, 414, 0x19E, 0, True), + ItemName.GoofyDamageControl: ItemData(0x13010D, 3, 542, 0x21E, 0, True), + ItemName.GoofyAutoLimit: ItemData(0x13010E, 1, 417, 0x1A1, 0, True), + ItemName.GoofySecondChance: ItemData(0x13010F, 1, 415, 0x19F, 0, True), + ItemName.GoofyOnceMore: ItemData(0x130110, 1, 416, 0x1A0, 0, True), + ItemName.GoofyAutoChange: ItemData(0x130111, 1, 418, 0x1A2, 0, True), + ItemName.GoofyHyperHealing: ItemData(0x130112, 2, 419, 0x1A3, 0, True), + ItemName.GoofyAutoHealing: ItemData(0x130113, 1, 420, 0x1A4, 0, True), + ItemName.GoofyMPHaste: ItemData(0x130114, 1, 413, 0x19D, 0, True), + ItemName.GoofyMPHastera: ItemData(0x130115, 1, 421, 0x1A5, 0, True), + ItemName.GoofyMPHastega: ItemData(0x130116, 1, 422, 0x1A6, 0, True), + ItemName.GoofyProtect: ItemData(0x130117, 2, 596, 0x254, 0, True), + ItemName.GoofyProtera: ItemData(0x130118, 2, 597, 0x255, 0, True), + ItemName.GoofyProtega: ItemData(0x130119, 2, 598, 0x256, 0, True), + +} + +Misc_Table = { + ItemName.LuckyEmblem: ItemData(0x13011A, 0, 367, 0x3641), # letter item + ItemName.Victory: ItemData(0x13011B, 0, 263, 0x111), + ItemName.Bounty: ItemData(0x13011C, 0, 461, 0, 0), # Dummy 14 + # ItemName.UniversalKey:ItemData(0x130129,0,365,0x363F,0)#Tournament Poster + +} +# Items that are prone to duping. +# anchors for checking form keyblade +# Save+32F4 Valor Form Save+339C Master Form Save+33D4 Final Form +# Have to use the kh2id for checking stuff that sora has equipped +# Equipped abilities have an offset of 0x8000 so check for if whatever || whatever+0x8000 +CheckDupingItems = { + "Items": { + ItemName.ProofofConnection, + ItemName.ProofofNonexistence, + ItemName.ProofofPeace, + ItemName.PromiseCharm, + ItemName.NamineSketches, + ItemName.CastleKey, + ItemName.BattlefieldsofWar, + ItemName.SwordoftheAncestor, + ItemName.BeastsClaw, + ItemName.BoneFist, + ItemName.ProudFang, + ItemName.SkillandCrossbones, + ItemName.Scimitar, + ItemName.MembershipCard, + ItemName.IceCream, + ItemName.WaytotheDawn, + ItemName.IdentityDisk, + ItemName.TornPages, + ItemName.LuckyEmblem, + ItemName.MickyMunnyPouch, + ItemName.OletteMunnyPouch, + ItemName.HadesCupTrophy, + ItemName.UnknownDisk, + ItemName.OlympusStone, + }, + "Magic": { + ItemName.FireElement, + ItemName.BlizzardElement, + ItemName.ThunderElement, + ItemName.CureElement, + ItemName.MagnetElement, + ItemName.ReflectElement, + }, + "Bitmask": { + ItemName.ValorForm, + ItemName.WisdomForm, + ItemName.LimitForm, + ItemName.MasterForm, + ItemName.FinalForm, + ItemName.Genie, + ItemName.PeterPan, + ItemName.Stitch, + ItemName.ChickenLittle, + ItemName.SecretAnsemsReport1, + ItemName.SecretAnsemsReport2, + ItemName.SecretAnsemsReport3, + ItemName.SecretAnsemsReport4, + ItemName.SecretAnsemsReport5, + ItemName.SecretAnsemsReport6, + ItemName.SecretAnsemsReport7, + ItemName.SecretAnsemsReport8, + ItemName.SecretAnsemsReport9, + ItemName.SecretAnsemsReport10, + ItemName.SecretAnsemsReport11, + ItemName.SecretAnsemsReport12, + ItemName.SecretAnsemsReport13, + + }, + "Weapons": { + "Keyblades": { + ItemName.Oathkeeper, + ItemName.Oblivion, + ItemName.StarSeeker, + ItemName.HiddenDragon, + ItemName.HerosCrest, + ItemName.Monochrome, + ItemName.FollowtheWind, + ItemName.CircleofLife, + ItemName.PhotonDebugger, + ItemName.GullWing, + ItemName.RumblingRose, + ItemName.GuardianSoul, + ItemName.WishingLamp, + ItemName.DecisivePumpkin, + ItemName.SleepingLion, + ItemName.SweetMemories, + ItemName.MysteriousAbyss, + ItemName.TwoBecomeOne, + ItemName.FatalCrest, + ItemName.BondofFlame, + ItemName.Fenrir, + ItemName.UltimaWeapon, + ItemName.WinnersProof, + ItemName.Pureblood, + }, + "Staffs": { + ItemName.Centurion2, + ItemName.MeteorStaff, + ItemName.NobodyLance, + ItemName.PreciousMushroom, + ItemName.PreciousMushroom2, + ItemName.PremiumMushroom, + ItemName.RisingDragon, + ItemName.SaveTheQueen2, + ItemName.ShamansRelic, + }, + "Shields": { + ItemName.AkashicRecord, + ItemName.FrozenPride2, + ItemName.GenjiShield, + ItemName.MajesticMushroom, + ItemName.MajesticMushroom2, + ItemName.NobodyGuard, + ItemName.OgreShield, + ItemName.SaveTheKing2, + ItemName.UltimateMushroom, + } + }, + "Equipment": { + "Accessories": { + ItemName.AbilityRing, + ItemName.EngineersRing, + ItemName.TechniciansRing, + ItemName.SkillRing, + ItemName.SkillfulRing, + ItemName.ExpertsRing, + ItemName.MastersRing, + ItemName.CosmicRing, + ItemName.ExecutivesRing, + ItemName.SardonyxRing, + ItemName.TourmalineRing, + ItemName.AquamarineRing, + ItemName.GarnetRing, + ItemName.DiamondRing, + ItemName.SilverRing, + ItemName.GoldRing, + ItemName.PlatinumRing, + ItemName.MythrilRing, + ItemName.OrichalcumRing, + ItemName.SoldierEarring, + ItemName.FencerEarring, + ItemName.MageEarring, + ItemName.SlayerEarring, + ItemName.Medal, + ItemName.MoonAmulet, + ItemName.StarCharm, + ItemName.CosmicArts, + ItemName.ShadowArchive, + ItemName.ShadowArchive2, + ItemName.FullBloom, + ItemName.FullBloom2, + ItemName.DrawRing, + ItemName.LuckyRing, + }, + "Armor": { + ItemName.ElvenBandana, + ItemName.DivineBandana, + ItemName.ProtectBelt, + ItemName.GaiaBelt, + ItemName.PowerBand, + ItemName.BusterBand, + ItemName.CosmicBelt, + ItemName.FireBangle, + ItemName.FiraBangle, + ItemName.FiragaBangle, + ItemName.FiragunBangle, + ItemName.BlizzardArmlet, + ItemName.BlizzaraArmlet, + ItemName.BlizzagaArmlet, + ItemName.BlizzagunArmlet, + ItemName.ThunderTrinket, + ItemName.ThundaraTrinket, + ItemName.ThundagaTrinket, + ItemName.ThundagunTrinket, + ItemName.ShockCharm, + ItemName.ShockCharm2, + ItemName.ShadowAnklet, + ItemName.DarkAnklet, + ItemName.MidnightAnklet, + ItemName.ChaosAnklet, + ItemName.ChampionBelt, + ItemName.AbasChain, + ItemName.AegisChain, + ItemName.Acrisius, + ItemName.Acrisius2, + ItemName.CosmicChain, + ItemName.PetiteRibbon, + ItemName.Ribbon, + ItemName.GrandRibbon, + } + }, + "Stat Increases": { + ItemName.MaxHPUp, + ItemName.MaxMPUp, + ItemName.DriveGaugeUp, + ItemName.ArmorSlotUp, + ItemName.AccessorySlotUp, + ItemName.ItemSlotUp, + }, + "Abilities": { + "Sora": { + ItemName.Scan, + ItemName.AerialRecovery, + ItemName.ComboMaster, + ItemName.ComboPlus, + ItemName.AirComboPlus, + ItemName.ComboBoost, + ItemName.AirComboBoost, + ItemName.ReactionBoost, + ItemName.FinishingPlus, + ItemName.NegativeCombo, + ItemName.BerserkCharge, + ItemName.DamageDrive, + ItemName.DriveBoost, + ItemName.FormBoost, + ItemName.SummonBoost, + ItemName.ExperienceBoost, + ItemName.Draw, + ItemName.Jackpot, + ItemName.LuckyLucky, + ItemName.DriveConverter, + ItemName.FireBoost, + ItemName.BlizzardBoost, + ItemName.ThunderBoost, + ItemName.ItemBoost, + ItemName.MPRage, + ItemName.MPHaste, + ItemName.MPHastera, + ItemName.MPHastega, + ItemName.Defender, + ItemName.DamageControl, + ItemName.NoExperience, + ItemName.LightDarkness, + ItemName.MagicLock, + ItemName.LeafBracer, + ItemName.CombinationBoost, + ItemName.OnceMore, + ItemName.SecondChance, + ItemName.Guard, + ItemName.UpperSlash, + ItemName.HorizontalSlash, + ItemName.FinishingLeap, + ItemName.RetaliatingSlash, + ItemName.Slapshot, + ItemName.DodgeSlash, + ItemName.FlashStep, + ItemName.SlideDash, + ItemName.VicinityBreak, + ItemName.GuardBreak, + ItemName.Explosion, + ItemName.AerialSweep, + ItemName.AerialDive, + ItemName.AerialSpiral, + ItemName.AerialFinish, + ItemName.MagnetBurst, + ItemName.Counterguard, + ItemName.AutoValor, + ItemName.AutoWisdom, + ItemName.AutoLimit, + ItemName.AutoMaster, + ItemName.AutoFinal, + ItemName.AutoSummon, + ItemName.TrinityLimit, + ItemName.HighJump, + ItemName.QuickRun, + ItemName.DodgeRoll, + ItemName.AerialDodge, + ItemName.Glide, + }, + "Donald": { + ItemName.DonaldFire, + ItemName.DonaldBlizzard, + ItemName.DonaldThunder, + ItemName.DonaldCure, + ItemName.Fantasia, + ItemName.FlareForce, + ItemName.DonaldMPRage, + ItemName.DonaldJackpot, + ItemName.DonaldLuckyLucky, + ItemName.DonaldFireBoost, + ItemName.DonaldBlizzardBoost, + ItemName.DonaldThunderBoost, + ItemName.DonaldMPHaste, + ItemName.DonaldMPHastera, + ItemName.DonaldMPHastega, + ItemName.DonaldAutoLimit, + ItemName.DonaldHyperHealing, + ItemName.DonaldAutoHealing, + ItemName.DonaldItemBoost, + ItemName.DonaldDamageControl, + ItemName.DonaldDraw, + }, + "Goofy": { + ItemName.GoofyTornado, + ItemName.GoofyTurbo, + ItemName.GoofyBash, + ItemName.TornadoFusion, + ItemName.Teamwork, + ItemName.GoofyDraw, + ItemName.GoofyJackpot, + ItemName.GoofyLuckyLucky, + ItemName.GoofyItemBoost, + ItemName.GoofyMPRage, + ItemName.GoofyDefender, + ItemName.GoofyDamageControl, + ItemName.GoofyAutoLimit, + ItemName.GoofySecondChance, + ItemName.GoofyOnceMore, + ItemName.GoofyAutoChange, + ItemName.GoofyHyperHealing, + ItemName.GoofyAutoHealing, + ItemName.GoofyMPHaste, + ItemName.GoofyMPHastera, + ItemName.GoofyMPHastega, + ItemName.GoofyProtect, + ItemName.GoofyProtera, + ItemName.GoofyProtega, + } + }, + "Boosts": { + ItemName.PowerBoost, + ItemName.MagicBoost, + ItemName.DefenseBoost, + ItemName.APBoost, + } +} + +Progression_Dicts = { + # Items that are classified as progression + "Progression": { + # Wincons + ItemName.Victory, + ItemName.LuckyEmblem, + ItemName.Bounty, + ItemName.ProofofConnection, + ItemName.ProofofNonexistence, + ItemName.ProofofPeace, + ItemName.PromiseCharm, + # visit locking + ItemName.NamineSketches, + # dummy 13 + ItemName.CastleKey, + ItemName.BattlefieldsofWar, + ItemName.SwordoftheAncestor, + ItemName.BeastsClaw, + ItemName.BoneFist, + ItemName.ProudFang, + ItemName.SkillandCrossbones, + ItemName.Scimitar, + ItemName.MembershipCard, + ItemName.IceCream, + ItemName.WaytotheDawn, + ItemName.IdentityDisk, + ItemName.TornPages, + # forms + ItemName.ValorForm, + ItemName.WisdomForm, + ItemName.LimitForm, + ItemName.MasterForm, + ItemName.FinalForm, + # magic + ItemName.FireElement, + ItemName.BlizzardElement, + ItemName.ThunderElement, + ItemName.CureElement, + ItemName.MagnetElement, + ItemName.ReflectElement, + ItemName.Genie, + ItemName.PeterPan, + ItemName.Stitch, + ItemName.ChickenLittle, + # movement + ItemName.HighJump, + ItemName.QuickRun, + ItemName.DodgeRoll, + ItemName.AerialDodge, + ItemName.Glide, + # abilities + ItemName.Scan, + ItemName.AerialRecovery, + ItemName.ComboMaster, + ItemName.ComboPlus, + ItemName.AirComboPlus, + ItemName.ComboBoost, + ItemName.AirComboBoost, + ItemName.ReactionBoost, + ItemName.FinishingPlus, + ItemName.NegativeCombo, + ItemName.BerserkCharge, + ItemName.DamageDrive, + ItemName.DriveBoost, + ItemName.FormBoost, + ItemName.SummonBoost, + ItemName.ExperienceBoost, + ItemName.Draw, + ItemName.Jackpot, + ItemName.LuckyLucky, + ItemName.DriveConverter, + ItemName.FireBoost, + ItemName.BlizzardBoost, + ItemName.ThunderBoost, + ItemName.ItemBoost, + ItemName.MPRage, + ItemName.MPHaste, + ItemName.MPHastera, + ItemName.MPHastega, + ItemName.Defender, + ItemName.DamageControl, + ItemName.NoExperience, + ItemName.LightDarkness, + ItemName.MagicLock, + ItemName.LeafBracer, + ItemName.CombinationBoost, + ItemName.OnceMore, + ItemName.SecondChance, + ItemName.Guard, + ItemName.UpperSlash, + ItemName.HorizontalSlash, + ItemName.FinishingLeap, + ItemName.RetaliatingSlash, + ItemName.Slapshot, + ItemName.DodgeSlash, + ItemName.FlashStep, + ItemName.SlideDash, + ItemName.VicinityBreak, + ItemName.GuardBreak, + ItemName.Explosion, + ItemName.AerialSweep, + ItemName.AerialDive, + ItemName.AerialSpiral, + ItemName.AerialFinish, + ItemName.MagnetBurst, + ItemName.Counterguard, + ItemName.AutoValor, + ItemName.AutoWisdom, + ItemName.AutoLimit, + ItemName.AutoMaster, + ItemName.AutoFinal, + ItemName.AutoSummon, + ItemName.TrinityLimit, + # keyblades + ItemName.Oathkeeper, + ItemName.Oblivion, + ItemName.StarSeeker, + ItemName.HiddenDragon, + ItemName.HerosCrest, + ItemName.Monochrome, + ItemName.FollowtheWind, + ItemName.CircleofLife, + ItemName.PhotonDebugger, + ItemName.GullWing, + ItemName.RumblingRose, + ItemName.GuardianSoul, + ItemName.WishingLamp, + ItemName.DecisivePumpkin, + ItemName.SleepingLion, + ItemName.SweetMemories, + ItemName.MysteriousAbyss, + ItemName.TwoBecomeOne, + ItemName.FatalCrest, + ItemName.BondofFlame, + ItemName.Fenrir, + ItemName.UltimaWeapon, + ItemName.WinnersProof, + ItemName.Pureblood, + # Staffs + ItemName.Centurion2, + ItemName.MeteorStaff, + ItemName.NobodyLance, + ItemName.PreciousMushroom, + ItemName.PreciousMushroom2, + ItemName.PremiumMushroom, + ItemName.RisingDragon, + ItemName.SaveTheQueen2, + ItemName.ShamansRelic, + # Shields + ItemName.AkashicRecord, + ItemName.FrozenPride2, + ItemName.GenjiShield, + ItemName.MajesticMushroom, + ItemName.MajesticMushroom2, + ItemName.NobodyGuard, + ItemName.OgreShield, + ItemName.SaveTheKing2, + ItemName.UltimateMushroom, + # Party Limits + ItemName.FlareForce, + ItemName.Fantasia, + ItemName.Teamwork, + ItemName.TornadoFusion + }, + "2VisitLocking": { + ItemName.CastleKey, + ItemName.BattlefieldsofWar, + ItemName.SwordoftheAncestor, + ItemName.BeastsClaw, + ItemName.BoneFist, + ItemName.ProudFang, + ItemName.SkillandCrossbones, + ItemName.Scimitar, + ItemName.MembershipCard, + ItemName.IceCream, + ItemName.WaytotheDawn, + ItemName.IdentityDisk, + ItemName.IceCream, + ItemName.NamineSketches + }, + "AllVisitLocking": { + ItemName.CastleKey: 2, + ItemName.BattlefieldsofWar: 2, + ItemName.SwordoftheAncestor: 2, + ItemName.BeastsClaw: 2, + ItemName.BoneFist: 2, + ItemName.ProudFang: 2, + ItemName.SkillandCrossbones: 2, + ItemName.Scimitar: 2, + ItemName.MembershipCard: 2, + ItemName.WaytotheDawn: 1, + ItemName.IdentityDisk: 2, + ItemName.IceCream: 3, + ItemName.NamineSketches: 1, + } +} + +exclusionItem_table = { + "Ability": { + ItemName.Scan, + ItemName.AerialRecovery, + ItemName.ComboMaster, + ItemName.ComboPlus, + ItemName.AirComboPlus, + ItemName.ComboBoost, + ItemName.AirComboBoost, + ItemName.ReactionBoost, + ItemName.FinishingPlus, + ItemName.NegativeCombo, + ItemName.BerserkCharge, + ItemName.DamageDrive, + ItemName.DriveBoost, + ItemName.FormBoost, + ItemName.SummonBoost, + ItemName.ExperienceBoost, + ItemName.Draw, + ItemName.Jackpot, + ItemName.LuckyLucky, + ItemName.DriveConverter, + ItemName.FireBoost, + ItemName.BlizzardBoost, + ItemName.ThunderBoost, + ItemName.ItemBoost, + ItemName.MPRage, + ItemName.MPHaste, + ItemName.MPHastera, + ItemName.MPHastega, + ItemName.Defender, + ItemName.DamageControl, + ItemName.NoExperience, + ItemName.LightDarkness, + ItemName.MagicLock, + ItemName.LeafBracer, + ItemName.CombinationBoost, + ItemName.DamageDrive, + ItemName.OnceMore, + ItemName.SecondChance, + ItemName.Guard, + ItemName.UpperSlash, + ItemName.HorizontalSlash, + ItemName.FinishingLeap, + ItemName.RetaliatingSlash, + ItemName.Slapshot, + ItemName.DodgeSlash, + ItemName.FlashStep, + ItemName.SlideDash, + ItemName.VicinityBreak, + ItemName.GuardBreak, + ItemName.Explosion, + ItemName.AerialSweep, + ItemName.AerialDive, + ItemName.AerialSpiral, + ItemName.AerialFinish, + ItemName.MagnetBurst, + ItemName.Counterguard, + ItemName.AutoValor, + ItemName.AutoWisdom, + ItemName.AutoLimit, + ItemName.AutoMaster, + ItemName.AutoFinal, + ItemName.AutoSummon, + ItemName.TrinityLimit, + ItemName.HighJump, + ItemName.QuickRun, + ItemName.DodgeRoll, + ItemName.AerialDodge, + ItemName.Glide, + }, + "StatUps": { + ItemName.MaxHPUp, + ItemName.MaxMPUp, + ItemName.DriveGaugeUp, + ItemName.ArmorSlotUp, + ItemName.AccessorySlotUp, + ItemName.ItemSlotUp, + }, +} + +item_dictionary_table = {**Reports_Table, + **Progression_Table, + **Forms_Table, + **Magic_Table, + **Armor_Table, + **Movement_Table, + **Staffs_Table, + **Shields_Table, + **Keyblade_Table, + **Accessory_Table, + **Usefull_Table, + **SupportAbility_Table, + **ActionAbility_Table, + **Items_Table, + **Misc_Table, + **Items_Table, + **DonaldAbility_Table, + **GoofyAbility_Table, + } + +lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_dictionary_table.items() if + data.code} + +item_groups: typing.Dict[str, list] = {"Drive Form": [item_name for item_name in Forms_Table.keys()], + "Growth": [item_name for item_name in Movement_Table.keys()], + "Donald Limit": [ItemName.FlareForce, ItemName.Fantasia], + "Goofy Limit": [ItemName.Teamwork, ItemName.TornadoFusion], + "Magic": [ItemName.FireElement, ItemName.BlizzardElement, + ItemName.ThunderElement, + ItemName.CureElement, ItemName.MagnetElement, + ItemName.ReflectElement], + "Summon": [ItemName.ChickenLittle, ItemName.Genie, ItemName.Stitch, + ItemName.PeterPan], + "Gap Closer": [ItemName.SlideDash, ItemName.FlashStep], + "Ground Finisher": [ItemName.GuardBreak, ItemName.Explosion, + ItemName.FinishingLeap], + "Visit Lock": [item_name for item_name in + Progression_Dicts["2VisitLocking"]], + "Keyblade": [item_name for item_name in Keyblade_Table.keys()], + "Fire": [ItemName.FireElement], + "Blizzard": [ItemName.BlizzardElement], + "Thunder": [ItemName.ThunderElement], + "Cure": [ItemName.CureElement], + "Magnet": [ItemName.MagnetElement], + "Reflect": [ItemName.ReflectElement], + "Proof": [ItemName.ProofofNonexistence, ItemName.ProofofPeace, + ItemName.ProofofConnection], + "Filler": [ + ItemName.PowerBoost, ItemName.MagicBoost, + ItemName.DefenseBoost, ItemName.APBoost] + } + +# lookup_kh2id_to_name: typing.Dict[int, str] = {data.kh2id: item_name for item_name, data in +# item_dictionary_table.items() if data.kh2id} diff --git a/worlds/kh2/Locations.py b/worlds/kh2/Locations.py new file mode 100644 index 0000000000..9b5cc55252 --- /dev/null +++ b/worlds/kh2/Locations.py @@ -0,0 +1,1779 @@ +import typing + +from BaseClasses import Location +from .Names import LocationName, RegionName, ItemName + + +class KH2Location(Location): + game: str = "Kingdom Hearts 2" + + +class LocationData(typing.NamedTuple): + code: typing.Optional[int] + locid: int + yml: str + charName: str = "Sora" + charNumber: int = 1 + + +# data's addrcheck sys3 addr obtained roomid bit index is eventid +LoD_Checks = { + LocationName.BambooGroveDarkShard: LocationData(0x130000, 245, "Chest"), + LocationName.BambooGroveEther: LocationData(0x130001, 497, "Chest"), + LocationName.BambooGroveMythrilShard: LocationData(0x130002, 498, "Chest"), + LocationName.EncampmentAreaMap: LocationData(0x130003, 350, "Chest"), + LocationName.Mission3: LocationData(0x130004, 417, "Chest"), + LocationName.CheckpointHiPotion: LocationData(0x130005, 21, "Chest"), + LocationName.CheckpointMythrilShard: LocationData(0x130006, 121, "Chest"), + LocationName.MountainTrailLightningShard: LocationData(0x130007, 22, "Chest"), + LocationName.MountainTrailRecoveryRecipe: LocationData(0x130008, 23, "Chest"), + LocationName.MountainTrailEther: LocationData(0x130009, 122, "Chest"), + LocationName.MountainTrailMythrilShard: LocationData(0x13000A, 123, "Chest"), + LocationName.VillageCaveAreaMap: LocationData(0x13000B, 495, "Chest"), + LocationName.VillageCaveDarkShard: LocationData(0x13000C, 125, "Chest"), + LocationName.VillageCaveAPBoost: LocationData(0x13000D, 124, "Chest"), + LocationName.VillageCaveBonus: LocationData(0x13000E, 43, "Get Bonus"), + LocationName.RidgeFrostShard: LocationData(0x13000F, 24, "Chest"), + LocationName.RidgeAPBoost: LocationData(0x130010, 126, "Chest"), + LocationName.ShanYu: LocationData(0x130011, 9, "Double Get Bonus"), + LocationName.ShanYuGetBonus: LocationData(0x130012, 9, "Second Get Bonus"), + LocationName.HiddenDragon: LocationData(0x130013, 257, "Chest"), + +} +LoD2_Checks = { + LocationName.ThroneRoomTornPages: LocationData(0x130014, 25, "Chest"), + LocationName.ThroneRoomPalaceMap: LocationData(0x130015, 127, "Chest"), + LocationName.ThroneRoomAPBoost: LocationData(0x130016, 26, "Chest"), + LocationName.ThroneRoomQueenRecipe: LocationData(0x130017, 27, "Chest"), + LocationName.ThroneRoomAPBoost2: LocationData(0x130018, 128, "Chest"), + LocationName.ThroneRoomOgreShield: LocationData(0x130019, 129, "Chest"), + LocationName.ThroneRoomMythrilCrystal: LocationData(0x13001A, 130, "Chest"), + LocationName.ThroneRoomOrichalcum: LocationData(0x13001B, 131, "Chest"), + LocationName.StormRider: LocationData(0x13001C, 10, "Get Bonus"), + LocationName.XigbarDataDefenseBoost: LocationData(0x13001D, 555, "Chest"), + +} +AG_Checks = { + LocationName.AgrabahMap: LocationData(0x13001E, 353, "Chest"), + LocationName.AgrabahDarkShard: LocationData(0x13001F, 28, "Chest"), + LocationName.AgrabahMythrilShard: LocationData(0x130020, 29, "Chest"), + LocationName.AgrabahHiPotion: LocationData(0x130021, 30, "Chest"), + LocationName.AgrabahAPBoost: LocationData(0x130022, 132, "Chest"), + LocationName.AgrabahMythrilStone: LocationData(0x130023, 133, "Chest"), + LocationName.AgrabahMythrilShard2: LocationData(0x130024, 249, "Chest"), + LocationName.AgrabahSerenityShard: LocationData(0x130025, 501, "Chest"), + LocationName.BazaarMythrilGem: LocationData(0x130026, 31, "Chest"), + LocationName.BazaarPowerShard: LocationData(0x130027, 32, "Chest"), + LocationName.BazaarHiPotion: LocationData(0x130028, 33, "Chest"), + LocationName.BazaarAPBoost: LocationData(0x130029, 134, "Chest"), + LocationName.BazaarMythrilShard: LocationData(0x13002A, 135, "Chest"), + LocationName.PalaceWallsSkillRing: LocationData(0x13002B, 136, "Chest"), + LocationName.PalaceWallsMythrilStone: LocationData(0x13002C, 520, "Chest"), + LocationName.CaveEntrancePowerStone: LocationData(0x13002D, 250, "Chest"), + LocationName.CaveEntranceMythrilShard: LocationData(0x13002E, 251, "Chest"), + LocationName.ValleyofStoneMythrilStone: LocationData(0x13002F, 35, "Chest"), + LocationName.ValleyofStoneAPBoost: LocationData(0x130030, 36, "Chest"), + LocationName.ValleyofStoneMythrilShard: LocationData(0x130031, 137, "Chest"), + LocationName.ValleyofStoneHiPotion: LocationData(0x130032, 138, "Chest"), + LocationName.AbuEscort: LocationData(0x130033, 42, "Get Bonus"), + LocationName.ChasmofChallengesCaveofWondersMap: LocationData(0x130034, 487, "Chest"), + LocationName.ChasmofChallengesAPBoost: LocationData(0x130035, 37, "Chest"), + LocationName.TreasureRoom: LocationData(0x130036, 46, "Get Bonus"), + LocationName.TreasureRoomAPBoost: LocationData(0x130037, 502, "Chest"), + LocationName.TreasureRoomSerenityGem: LocationData(0x130038, 503, "Chest"), + LocationName.ElementalLords: LocationData(0x130039, 37, "Get Bonus"), + LocationName.LampCharm: LocationData(0x13003A, 300, "Chest"), + +} +AG2_Checks = { + LocationName.RuinedChamberTornPages: LocationData(0x13003B, 34, "Chest"), + LocationName.RuinedChamberRuinsMap: LocationData(0x13003C, 486, "Chest"), + LocationName.GenieJafar: LocationData(0x13003D, 15, "Get Bonus"), + LocationName.WishingLamp: LocationData(0x13003E, 303, "Chest"), + LocationName.LexaeusBonus: LocationData(0x13003F, 65, "Get Bonus"), + LocationName.LexaeusASStrengthBeyondStrength: LocationData(0x130040, 545, "Chest"), + LocationName.LexaeusDataLostIllusion: LocationData(0x130041, 550, "Chest"), +} +DC_Checks = { + LocationName.DCCourtyardMythrilShard: LocationData(0x130042, 16, "Chest"), + LocationName.DCCourtyardStarRecipe: LocationData(0x130043, 17, "Chest"), + LocationName.DCCourtyardAPBoost: LocationData(0x130044, 18, "Chest"), + LocationName.DCCourtyardMythrilStone: LocationData(0x130045, 92, "Chest"), + LocationName.DCCourtyardBlazingStone: LocationData(0x130046, 93, "Chest"), + LocationName.DCCourtyardBlazingShard: LocationData(0x130047, 247, "Chest"), + LocationName.DCCourtyardMythrilShard2: LocationData(0x130048, 248, "Chest"), + LocationName.LibraryTornPages: LocationData(0x130049, 91, "Chest"), + LocationName.DisneyCastleMap: LocationData(0x13004A, 332, "Chest"), + LocationName.MinnieEscort: LocationData(0x13004B, 38, "Double Get Bonus"), + LocationName.MinnieEscortGetBonus: LocationData(0x13004C, 38, "Second Get Bonus"), + +} +TR_Checks = { + LocationName.CornerstoneHillMap: LocationData(0x13004D, 79, "Chest"), + LocationName.CornerstoneHillFrostShard: LocationData(0x13004E, 12, "Chest"), + LocationName.PierMythrilShard: LocationData(0x13004F, 81, "Chest"), + LocationName.PierHiPotion: LocationData(0x130050, 82, "Chest"), + LocationName.WaterwayMythrilStone: LocationData(0x130051, 83, "Chest"), + LocationName.WaterwayAPBoost: LocationData(0x130052, 84, "Chest"), + LocationName.WaterwayFrostStone: LocationData(0x130053, 85, "Chest"), + LocationName.WindowofTimeMap: LocationData(0x130054, 368, "Chest"), + LocationName.BoatPete: LocationData(0x130055, 16, "Get Bonus"), + LocationName.FuturePete: LocationData(0x130056, 17, "Double Get Bonus"), + LocationName.FuturePeteGetBonus: LocationData(0x130057, 17, "Second Get Bonus"), + LocationName.Monochrome: LocationData(0x130058, 261, "Chest"), + LocationName.WisdomForm: LocationData(0x130059, 262, "Chest"), + LocationName.MarluxiaGetBonus: LocationData(0x13005A, 67, "Get Bonus"), + LocationName.MarluxiaASEternalBlossom: LocationData(0x13005B, 548, "Chest"), + LocationName.MarluxiaDataLostIllusion: LocationData(0x13005C, 553, "Chest"), + LocationName.LingeringWillBonus: LocationData(0x13005D, 70, "Get Bonus"), + LocationName.LingeringWillProofofConnection: LocationData(0x13005E, 587, "Chest"), + LocationName.LingeringWillManifestIllusion: LocationData(0x13005F, 591, "Chest"), + +} +# the mismatch might be here +HundredAcre1_Checks = { + LocationName.PoohsHouse100AcreWoodMap: LocationData(0x130060, 313, "Chest"), + LocationName.PoohsHouseAPBoost: LocationData(0x130061, 97, "Chest"), + LocationName.PoohsHouseMythrilStone: LocationData(0x130062, 98, "Chest"), +} +HundredAcre2_Checks = { + LocationName.PigletsHouseDefenseBoost: LocationData(0x130063, 105, "Chest"), + LocationName.PigletsHouseAPBoost: LocationData(0x130064, 103, "Chest"), + LocationName.PigletsHouseMythrilGem: LocationData(0x130065, 104, "Chest"), +} +HundredAcre3_Checks = { + LocationName.RabbitsHouseDrawRing: LocationData(0x130066, 314, "Chest"), + LocationName.RabbitsHouseMythrilCrystal: LocationData(0x130067, 100, "Chest"), + LocationName.RabbitsHouseAPBoost: LocationData(0x130068, 101, "Chest"), +} +HundredAcre4_Checks = { + LocationName.KangasHouseMagicBoost: LocationData(0x130069, 108, "Chest"), + LocationName.KangasHouseAPBoost: LocationData(0x13006A, 106, "Chest"), + LocationName.KangasHouseOrichalcum: LocationData(0x13006B, 107, "Chest"), +} +HundredAcre5_Checks = { + LocationName.SpookyCaveMythrilGem: LocationData(0x13006C, 110, "Chest"), + LocationName.SpookyCaveAPBoost: LocationData(0x13006D, 111, "Chest"), + LocationName.SpookyCaveOrichalcum: LocationData(0x13006E, 112, "Chest"), + LocationName.SpookyCaveGuardRecipe: LocationData(0x13006F, 113, "Chest"), + LocationName.SpookyCaveMythrilCrystal: LocationData(0x130070, 115, "Chest"), + LocationName.SpookyCaveAPBoost2: LocationData(0x130071, 116, "Chest"), + LocationName.SweetMemories: LocationData(0x130072, 284, "Chest"), + LocationName.SpookyCaveMap: LocationData(0x130073, 485, "Chest"), +} +HundredAcre6_Checks = { + LocationName.StarryHillCosmicRing: LocationData(0x130074, 312, "Chest"), + LocationName.StarryHillStyleRecipe: LocationData(0x130075, 94, "Chest"), + LocationName.StarryHillCureElement: LocationData(0x130076, 285, "Chest"), + LocationName.StarryHillOrichalcumPlus: LocationData(0x130077, 539, "Chest"), +} +Oc_Checks = { + LocationName.PassageMythrilShard: LocationData(0x130078, 7, "Chest"), + LocationName.PassageMythrilStone: LocationData(0x130079, 8, "Chest"), + LocationName.PassageEther: LocationData(0x13007A, 144, "Chest"), + LocationName.PassageAPBoost: LocationData(0x13007B, 145, "Chest"), + LocationName.PassageHiPotion: LocationData(0x13007C, 146, "Chest"), + LocationName.InnerChamberUnderworldMap: LocationData(0x13007D, 2, "Chest"), + LocationName.InnerChamberMythrilShard: LocationData(0x13007E, 243, "Chest"), + LocationName.Cerberus: LocationData(0x13007F, 5, "Get Bonus"), + LocationName.ColiseumMap: LocationData(0x130080, 338, "Chest"), + LocationName.Urns: LocationData(0x130081, 57, "Get Bonus"), + LocationName.UnderworldEntrancePowerBoost: LocationData(0x130082, 242, "Chest"), + LocationName.CavernsEntranceLucidShard: LocationData(0x130083, 3, "Chest"), + LocationName.CavernsEntranceAPBoost: LocationData(0x130084, 11, "Chest"), + LocationName.CavernsEntranceMythrilShard: LocationData(0x130085, 504, "Chest"), + LocationName.TheLostRoadBrightShard: LocationData(0x130086, 9, "Chest"), + LocationName.TheLostRoadEther: LocationData(0x130087, 10, "Chest"), + LocationName.TheLostRoadMythrilShard: LocationData(0x130088, 148, "Chest"), + LocationName.TheLostRoadMythrilStone: LocationData(0x130089, 149, "Chest"), + LocationName.AtriumLucidStone: LocationData(0x13008A, 150, "Chest"), + LocationName.AtriumAPBoost: LocationData(0x13008B, 151, "Chest"), + LocationName.DemyxOC: LocationData(0x13008C, 58, "Get Bonus"), + LocationName.SecretAnsemReport5: LocationData(0x13008D, 529, "Chest"), + LocationName.OlympusStone: LocationData(0x13008E, 293, "Chest"), + LocationName.TheLockCavernsMap: LocationData(0x13008F, 244, "Chest"), + LocationName.TheLockMythrilShard: LocationData(0x130090, 5, "Chest"), + LocationName.TheLockAPBoost: LocationData(0x130091, 142, "Chest"), + LocationName.PeteOC: LocationData(0x130092, 6, "Get Bonus"), + LocationName.Hydra: LocationData(0x130093, 7, "Double Get Bonus"), + LocationName.HydraGetBonus: LocationData(0x130094, 7, "Second Get Bonus"), + LocationName.HerosCrest: LocationData(0x130095, 260, "Chest"), + +} +Oc2_Checks = { + LocationName.AuronsStatue: LocationData(0x130096, 295, "Chest"), + LocationName.Hades: LocationData(0x130097, 8, "Double Get Bonus"), + LocationName.HadesGetBonus: LocationData(0x130098, 8, "Second Get Bonus"), + LocationName.GuardianSoul: LocationData(0x130099, 272, "Chest"), + LocationName.ZexionBonus: LocationData(0x13009A, 66, "Get Bonus"), + LocationName.ZexionASBookofShadows: LocationData(0x13009B, 546, "Chest"), + LocationName.ZexionDataLostIllusion: LocationData(0x13009C, 551, "Chest"), +} +Oc2Cups = { + LocationName.ProtectBeltPainandPanicCup: LocationData(0x13009D, 513, "Chest"), + LocationName.SerenityGemPainandPanicCup: LocationData(0x13009E, 540, "Chest"), + LocationName.RisingDragonCerberusCup: LocationData(0x13009F, 515, "Chest"), + LocationName.SerenityCrystalCerberusCup: LocationData(0x1300A0, 542, "Chest"), + LocationName.GenjiShieldTitanCup: LocationData(0x1300A1, 514, "Chest"), + LocationName.SkillfulRingTitanCup: LocationData(0x1300A2, 541, "Chest"), + LocationName.FatalCrestGoddessofFateCup: LocationData(0x1300A3, 516, "Chest"), + LocationName.OrichalcumPlusGoddessofFateCup: LocationData(0x1300A4, 517, "Chest"), + LocationName.HadesCupTrophyParadoxCups: LocationData(0x1300A5, 518, "Chest"), +} + +BC_Checks = { + LocationName.BCCourtyardAPBoost: LocationData(0x1300A6, 39, "Chest"), + LocationName.BCCourtyardHiPotion: LocationData(0x1300A7, 40, "Chest"), + LocationName.BCCourtyardMythrilShard: LocationData(0x1300A8, 505, "Chest"), + LocationName.BellesRoomCastleMap: LocationData(0x1300A9, 46, "Chest"), + LocationName.BellesRoomMegaRecipe: LocationData(0x1300AA, 240, "Chest"), + LocationName.TheEastWingMythrilShard: LocationData(0x1300AB, 63, "Chest"), + LocationName.TheEastWingTent: LocationData(0x1300AC, 155, "Chest"), + LocationName.TheWestHallHiPotion: LocationData(0x1300AD, 41, "Chest"), + LocationName.TheWestHallPowerShard: LocationData(0x1300AE, 207, "Chest"), + LocationName.TheWestHallAPBoostPostDungeon: LocationData(0x1300AF, 158, "Chest"), + LocationName.TheWestHallBrightStone: LocationData(0x1300B0, 159, "Chest"), + LocationName.TheWestHallMythrilShard: LocationData(0x1300B1, 206, "Chest"), + LocationName.Thresholder: LocationData(0x1300B2, 2, "Get Bonus"), + LocationName.DungeonBasementMap: LocationData(0x1300B3, 239, "Chest"), + LocationName.DungeonAPBoost: LocationData(0x1300B4, 43, "Chest"), + LocationName.SecretPassageMythrilShard: LocationData(0x1300B5, 44, "Chest"), + LocationName.SecretPassageHiPotion: LocationData(0x1300B6, 168, "Chest"), + LocationName.SecretPassageLucidShard: LocationData(0x1300B7, 45, "Chest"), + LocationName.TheWestHallMythrilShard2: LocationData(0x1300B8, 208, "Chest"), + LocationName.TheWestWingMythrilShard: LocationData(0x1300B9, 42, "Chest"), + LocationName.TheWestWingTent: LocationData(0x1300BA, 164, "Chest"), + LocationName.Beast: LocationData(0x1300BB, 12, "Get Bonus"), + LocationName.TheBeastsRoomBlazingShard: LocationData(0x1300BC, 241, "Chest"), + LocationName.DarkThorn: LocationData(0x1300BD, 3, "Double Get Bonus"), + LocationName.DarkThornGetBonus: LocationData(0x1300BE, 3, "Second Get Bonus"), + LocationName.DarkThornCureElement: LocationData(0x1300BF, 299, "Chest"), + +} +BC2_Checks = { + LocationName.RumblingRose: LocationData(0x1300C0, 270, "Chest"), + LocationName.CastleWallsMap: LocationData(0x1300C1, 325, "Chest"), + LocationName.Xaldin: LocationData(0x1300C2, 4, "Double Get Bonus"), + LocationName.XaldinGetBonus: LocationData(0x1300C3, 4, "Second Get Bonus"), + LocationName.SecretAnsemReport4: LocationData(0x1300C4, 528, "Chest"), + LocationName.XaldinDataDefenseBoost: LocationData(0x1300C5, 559, "Chest"), +} +SP_Checks = { + LocationName.PitCellAreaMap: LocationData(0x1300C6, 316, "Chest"), + LocationName.PitCellMythrilCrystal: LocationData(0x1300C7, 64, "Chest"), + LocationName.CanyonDarkCrystal: LocationData(0x1300C8, 65, "Chest"), + LocationName.CanyonMythrilStone: LocationData(0x1300C9, 171, "Chest"), + LocationName.CanyonMythrilGem: LocationData(0x1300CA, 253, "Chest"), + LocationName.CanyonFrostCrystal: LocationData(0x1300CB, 521, "Chest"), + LocationName.Screens: LocationData(0x1300CC, 45, "Get Bonus"), + LocationName.HallwayPowerCrystal: LocationData(0x1300CD, 49, "Chest"), + LocationName.HallwayAPBoost: LocationData(0x1300CE, 50, "Chest"), + LocationName.CommunicationsRoomIOTowerMap: LocationData(0x1300CF, 255, "Chest"), + LocationName.CommunicationsRoomGaiaBelt: LocationData(0x1300D0, 499, "Chest"), + LocationName.HostileProgram: LocationData(0x1300D1, 31, "Double Get Bonus"), + LocationName.HostileProgramGetBonus: LocationData(0x1300D2, 31, "Second Get Bonus"), + LocationName.PhotonDebugger: LocationData(0x1300D3, 267, "Chest"), + +} +SP2_Checks = { + LocationName.SolarSailer: LocationData(0x1300D4, 61, "Get Bonus"), + LocationName.CentralComputerCoreAPBoost: LocationData(0x1300D5, 177, "Chest"), + LocationName.CentralComputerCoreOrichalcumPlus: LocationData(0x1300D6, 178, "Chest"), + LocationName.CentralComputerCoreCosmicArts: LocationData(0x1300D7, 51, "Chest"), + LocationName.CentralComputerCoreMap: LocationData(0x1300D8, 488, "Chest"), + LocationName.MCP: LocationData(0x1300D9, 32, "Double Get Bonus"), + LocationName.MCPGetBonus: LocationData(0x1300DA, 32, "Second Get Bonus"), + LocationName.LarxeneBonus: LocationData(0x1300DB, 68, "Get Bonus"), + LocationName.LarxeneASCloakedThunder: LocationData(0x1300DC, 547, "Chest"), + LocationName.LarxeneDataLostIllusion: LocationData(0x1300DD, 552, "Chest"), +} +HT_Checks = { + LocationName.GraveyardMythrilShard: LocationData(0x1300DE, 53, "Chest"), + LocationName.GraveyardSerenityGem: LocationData(0x1300DF, 212, "Chest"), + LocationName.FinklesteinsLabHalloweenTownMap: LocationData(0x1300E0, 211, "Chest"), + LocationName.TownSquareMythrilStone: LocationData(0x1300E1, 209, "Chest"), + LocationName.TownSquareEnergyShard: LocationData(0x1300E2, 210, "Chest"), + LocationName.HinterlandsLightningShard: LocationData(0x1300E3, 54, "Chest"), + LocationName.HinterlandsMythrilStone: LocationData(0x1300E4, 213, "Chest"), + LocationName.HinterlandsAPBoost: LocationData(0x1300E5, 214, "Chest"), + LocationName.CandyCaneLaneMegaPotion: LocationData(0x1300E6, 55, "Chest"), + LocationName.CandyCaneLaneMythrilGem: LocationData(0x1300E7, 56, "Chest"), + LocationName.CandyCaneLaneLightningStone: LocationData(0x1300E8, 216, "Chest"), + LocationName.CandyCaneLaneMythrilStone: LocationData(0x1300E9, 217, "Chest"), + LocationName.SantasHouseChristmasTownMap: LocationData(0x1300EA, 57, "Chest"), + LocationName.SantasHouseAPBoost: LocationData(0x1300EB, 58, "Chest"), + LocationName.PrisonKeeper: LocationData(0x1300EC, 18, "Get Bonus"), + LocationName.OogieBoogie: LocationData(0x1300ED, 19, "Get Bonus"), + LocationName.OogieBoogieMagnetElement: LocationData(0x1300EE, 301, "Chest"), +} +HT2_Checks = { + LocationName.Lock: LocationData(0x1300EF, 40, "Get Bonus"), + LocationName.Present: LocationData(0x1300F0, 297, "Chest"), + LocationName.DecoyPresents: LocationData(0x1300F1, 298, "Chest"), + LocationName.Experiment: LocationData(0x1300F2, 20, "Get Bonus"), + LocationName.DecisivePumpkin: LocationData(0x1300F3, 275, "Chest"), + LocationName.VexenBonus: LocationData(0x1300F4, 64, "Get Bonus"), + LocationName.VexenASRoadtoDiscovery: LocationData(0x1300F5, 544, "Chest"), + LocationName.VexenDataLostIllusion: LocationData(0x1300F6, 549, "Chest"), +} +PR_Checks = { + LocationName.RampartNavalMap: LocationData(0x1300F7, 70, "Chest"), + LocationName.RampartMythrilStone: LocationData(0x1300F8, 219, "Chest"), + LocationName.RampartDarkShard: LocationData(0x1300F9, 220, "Chest"), + LocationName.TownDarkStone: LocationData(0x1300FA, 71, "Chest"), + LocationName.TownAPBoost: LocationData(0x1300FB, 72, "Chest"), + LocationName.TownMythrilShard: LocationData(0x1300FC, 73, "Chest"), + LocationName.TownMythrilGem: LocationData(0x1300FD, 221, "Chest"), + LocationName.CaveMouthBrightShard: LocationData(0x1300FE, 74, "Chest"), + LocationName.CaveMouthMythrilShard: LocationData(0x1300FF, 223, "Chest"), + LocationName.IsladeMuertaMap: LocationData(0x130100, 329, "Chest"), + LocationName.BoatFight: LocationData(0x130101, 62, "Get Bonus"), + LocationName.InterceptorBarrels: LocationData(0x130102, 39, "Get Bonus"), + LocationName.PowderStoreAPBoost1: LocationData(0x130103, 369, "Chest"), + LocationName.PowderStoreAPBoost2: LocationData(0x130104, 370, "Chest"), + LocationName.MoonlightNookMythrilShard: LocationData(0x130105, 75, "Chest"), + LocationName.MoonlightNookSerenityGem: LocationData(0x130106, 224, "Chest"), + LocationName.MoonlightNookPowerStone: LocationData(0x130107, 371, "Chest"), + LocationName.Barbossa: LocationData(0x130108, 21, "Double Get Bonus"), + LocationName.BarbossaGetBonus: LocationData(0x130109, 21, "Second Get Bonus"), + LocationName.FollowtheWind: LocationData(0x13010A, 263, "Chest"), + +} +PR2_Checks = { + LocationName.GrimReaper1: LocationData(0x13010B, 59, "Get Bonus"), + LocationName.InterceptorsHoldFeatherCharm: LocationData(0x13010C, 252, "Chest"), + LocationName.SeadriftKeepAPBoost: LocationData(0x13010D, 76, "Chest"), + LocationName.SeadriftKeepOrichalcum: LocationData(0x13010E, 225, "Chest"), + LocationName.SeadriftKeepMeteorStaff: LocationData(0x13010F, 372, "Chest"), + LocationName.SeadriftRowSerenityGem: LocationData(0x130110, 77, "Chest"), + LocationName.SeadriftRowKingRecipe: LocationData(0x130111, 78, "Chest"), + LocationName.SeadriftRowMythrilCrystal: LocationData(0x130112, 373, "Chest"), + LocationName.SeadriftRowCursedMedallion: LocationData(0x130113, 296, "Chest"), + LocationName.SeadriftRowShipGraveyardMap: LocationData(0x130114, 331, "Chest"), + LocationName.GrimReaper2: LocationData(0x130115, 22, "Get Bonus"), + LocationName.SecretAnsemReport6: LocationData(0x130116, 530, "Chest"), + LocationName.LuxordDataAPBoost: LocationData(0x130117, 557, "Chest"), +} +HB_Checks = { + LocationName.MarketplaceMap: LocationData(0x130118, 362, "Chest"), + LocationName.BoroughDriveRecovery: LocationData(0x130119, 194, "Chest"), + LocationName.BoroughAPBoost: LocationData(0x13011A, 195, "Chest"), + LocationName.BoroughHiPotion: LocationData(0x13011B, 196, "Chest"), + LocationName.BoroughMythrilShard: LocationData(0x13011C, 305, "Chest"), + LocationName.BoroughDarkShard: LocationData(0x13011D, 506, "Chest"), + LocationName.MerlinsHouseMembershipCard: LocationData(0x13011E, 256, "Chest"), + LocationName.MerlinsHouseBlizzardElement: LocationData(0x13011F, 292, "Chest"), + LocationName.Bailey: LocationData(0x130120, 47, "Get Bonus"), + LocationName.BaileySecretAnsemReport7: LocationData(0x130121, 531, "Chest"), + LocationName.BaseballCharm: LocationData(0x130122, 258, "Chest"), +} +HB2_Checks = { + LocationName.PosternCastlePerimeterMap: LocationData(0x130123, 310, "Chest"), + LocationName.PosternMythrilGem: LocationData(0x130124, 189, "Chest"), + LocationName.PosternAPBoost: LocationData(0x130125, 190, "Chest"), + LocationName.CorridorsMythrilStone: LocationData(0x130126, 200, "Chest"), + LocationName.CorridorsMythrilCrystal: LocationData(0x130127, 201, "Chest"), + LocationName.CorridorsDarkCrystal: LocationData(0x130128, 202, "Chest"), + LocationName.CorridorsAPBoost: LocationData(0x130129, 307, "Chest"), + LocationName.AnsemsStudyMasterForm: LocationData(0x13012A, 276, "Chest"), + LocationName.AnsemsStudySleepingLion: LocationData(0x13012B, 266, "Chest"), + LocationName.AnsemsStudySkillRecipe: LocationData(0x13012C, 184, "Chest"), + LocationName.AnsemsStudyUkuleleCharm: LocationData(0x13012D, 183, "Chest"), + LocationName.RestorationSiteMoonRecipe: LocationData(0x13012E, 309, "Chest"), + LocationName.RestorationSiteAPBoost: LocationData(0x13012F, 507, "Chest"), + LocationName.DemyxHB: LocationData(0x130130, 28, "Double Get Bonus"), + LocationName.DemyxHBGetBonus: LocationData(0x130131, 28, "Second Get Bonus"), + LocationName.FFFightsCureElement: LocationData(0x130132, 361, "Chest"), + LocationName.CrystalFissureTornPages: LocationData(0x130133, 179, "Chest"), + LocationName.CrystalFissureTheGreatMawMap: LocationData(0x130134, 489, "Chest"), + LocationName.CrystalFissureEnergyCrystal: LocationData(0x130135, 180, "Chest"), + LocationName.CrystalFissureAPBoost: LocationData(0x130136, 181, "Chest"), + LocationName.ThousandHeartless: LocationData(0x130137, 60, "Get Bonus"), + LocationName.ThousandHeartlessSecretAnsemReport1: LocationData(0x130138, 525, "Chest"), + LocationName.ThousandHeartlessIceCream: LocationData(0x130139, 269, "Chest"), + LocationName.ThousandHeartlessPicture: LocationData(0x13013A, 511, "Chest"), + LocationName.PosternGullWing: LocationData(0x13013B, 491, "Chest"), + LocationName.HeartlessManufactoryCosmicChain: LocationData(0x13013C, 311, "Chest"), + LocationName.SephirothBonus: LocationData(0x13013D, 35, "Get Bonus"), + LocationName.SephirothFenrir: LocationData(0x13013E, 282, "Chest"), + LocationName.WinnersProof: LocationData(0x13013F, 588, "Chest"), + LocationName.ProofofPeace: LocationData(0x130140, 589, "Chest"), + LocationName.DemyxDataAPBoost: LocationData(0x130141, 560, "Chest"), + LocationName.CoRDepthsAPBoost: LocationData(0x130142, 562, "Chest"), + LocationName.CoRDepthsPowerCrystal: LocationData(0x130143, 563, "Chest"), + LocationName.CoRDepthsFrostCrystal: LocationData(0x130144, 564, "Chest"), + LocationName.CoRDepthsManifestIllusion: LocationData(0x130145, 565, "Chest"), + LocationName.CoRDepthsAPBoost2: LocationData(0x130146, 566, "Chest"), + LocationName.CoRMineshaftLowerLevelDepthsofRemembranceMap: LocationData(0x130147, 580, "Chest"), + LocationName.CoRMineshaftLowerLevelAPBoost: LocationData(0x130148, 578, "Chest"), + +} +CoR_Checks = { + LocationName.CoRDepthsUpperLevelRemembranceGem: LocationData(0x130149, 567, "Chest"), + LocationName.CoRMiningAreaSerenityGem: LocationData(0x13014A, 568, "Chest"), + LocationName.CoRMiningAreaAPBoost: LocationData(0x13014B, 569, "Chest"), + LocationName.CoRMiningAreaSerenityCrystal: LocationData(0x13014C, 570, "Chest"), + LocationName.CoRMiningAreaManifestIllusion: LocationData(0x13014D, 571, "Chest"), + LocationName.CoRMiningAreaSerenityGem2: LocationData(0x13014E, 572, "Chest"), + LocationName.CoRMiningAreaDarkRemembranceMap: LocationData(0x13014F, 573, "Chest"), + LocationName.CoRMineshaftMidLevelPowerBoost: LocationData(0x130150, 581, "Chest"), + LocationName.CoREngineChamberSerenityCrystal: LocationData(0x130151, 574, "Chest"), + LocationName.CoREngineChamberRemembranceCrystal: LocationData(0x130152, 575, "Chest"), + LocationName.CoREngineChamberAPBoost: LocationData(0x130153, 576, "Chest"), + LocationName.CoREngineChamberManifestIllusion: LocationData(0x130154, 577, "Chest"), + LocationName.CoRMineshaftUpperLevelMagicBoost: LocationData(0x130155, 582, "Chest"), + LocationName.CoRMineshaftUpperLevelAPBoost: LocationData(0x130156, 579, "Chest"), + LocationName.TransporttoRemembrance: LocationData(0x130157, 72, "Get Bonus"), +} +PL_Checks = { + LocationName.GorgeSavannahMap: LocationData(0x130158, 492, "Chest"), + LocationName.GorgeDarkGem: LocationData(0x130159, 404, "Chest"), + LocationName.GorgeMythrilStone: LocationData(0x13015A, 405, "Chest"), + LocationName.ElephantGraveyardFrostGem: LocationData(0x13015B, 401, "Chest"), + LocationName.ElephantGraveyardMythrilStone: LocationData(0x13015C, 402, "Chest"), + LocationName.ElephantGraveyardBrightStone: LocationData(0x13015D, 403, "Chest"), + LocationName.ElephantGraveyardAPBoost: LocationData(0x13015E, 508, "Chest"), + LocationName.ElephantGraveyardMythrilShard: LocationData(0x13015F, 509, "Chest"), + LocationName.PrideRockMap: LocationData(0x130160, 418, "Chest"), + LocationName.PrideRockMythrilStone: LocationData(0x130161, 392, "Chest"), + LocationName.PrideRockSerenityCrystal: LocationData(0x130162, 393, "Chest"), + LocationName.WildebeestValleyEnergyStone: LocationData(0x130163, 396, "Chest"), + LocationName.WildebeestValleyAPBoost: LocationData(0x130164, 397, "Chest"), + LocationName.WildebeestValleyMythrilGem: LocationData(0x130165, 398, "Chest"), + LocationName.WildebeestValleyMythrilStone: LocationData(0x130166, 399, "Chest"), + LocationName.WildebeestValleyLucidGem: LocationData(0x130167, 400, "Chest"), + LocationName.WastelandsMythrilShard: LocationData(0x130168, 406, "Chest"), + LocationName.WastelandsSerenityGem: LocationData(0x130169, 407, "Chest"), + LocationName.WastelandsMythrilStone: LocationData(0x13016A, 408, "Chest"), + LocationName.JungleSerenityGem: LocationData(0x13016B, 409, "Chest"), + LocationName.JungleMythrilStone: LocationData(0x13016C, 410, "Chest"), + LocationName.JungleSerenityCrystal: LocationData(0x13016D, 411, "Chest"), + LocationName.OasisMap: LocationData(0x13016E, 412, "Chest"), + LocationName.OasisTornPages: LocationData(0x13016F, 493, "Chest"), + LocationName.OasisAPBoost: LocationData(0x130170, 413, "Chest"), + LocationName.CircleofLife: LocationData(0x130171, 264, "Chest"), + LocationName.Hyenas1: LocationData(0x130172, 49, "Get Bonus"), + LocationName.Scar: LocationData(0x130173, 29, "Get Bonus"), + LocationName.ScarFireElement: LocationData(0x130174, 302, "Chest"), + +} +PL2_Checks = { + LocationName.Hyenas2: LocationData(0x130175, 50, "Get Bonus"), + LocationName.Groundshaker: LocationData(0x130176, 30, "Double Get Bonus"), + LocationName.GroundshakerGetBonus: LocationData(0x130177, 30, "Second Get Bonus"), + LocationName.SaixDataDefenseBoost: LocationData(0x130178, 556, "Chest"), +} +STT_Checks = { + LocationName.TwilightTownMap: LocationData(0x130179, 319, "Chest"), + LocationName.MunnyPouchOlette: LocationData(0x13017A, 288, "Chest"), + LocationName.StationDusks: LocationData(0x13017B, 54, "Get Bonus", "Roxas", 14), + LocationName.StationofSerenityPotion: LocationData(0x13017C, 315, "Chest"), + LocationName.StationofCallingPotion: LocationData(0x13017D, 472, "Chest"), + LocationName.TwilightThorn: LocationData(0x13017E, 33, "Get Bonus", "Roxas", 14), + LocationName.Axel1: LocationData(0x13017F, 73, "Get Bonus", "Roxas", 14), + LocationName.JunkChampionBelt: LocationData(0x130180, 389, "Chest"), + LocationName.JunkMedal: LocationData(0x130181, 390, "Chest"), + LocationName.TheStruggleTrophy: LocationData(0x130182, 519, "Chest"), + LocationName.CentralStationPotion1: LocationData(0x130183, 428, "Chest"), + LocationName.STTCentralStationHiPotion: LocationData(0x130184, 429, "Chest"), + LocationName.CentralStationPotion2: LocationData(0x130185, 430, "Chest"), + LocationName.SunsetTerraceAbilityRing: LocationData(0x130186, 434, "Chest"), + LocationName.SunsetTerraceHiPotion: LocationData(0x130187, 435, "Chest"), + LocationName.SunsetTerracePotion1: LocationData(0x130188, 436, "Chest"), + LocationName.SunsetTerracePotion2: LocationData(0x130189, 437, "Chest"), + LocationName.MansionFoyerHiPotion: LocationData(0x13018A, 449, "Chest"), + LocationName.MansionFoyerPotion1: LocationData(0x13018B, 450, "Chest"), + LocationName.MansionFoyerPotion2: LocationData(0x13018C, 451, "Chest"), + LocationName.MansionDiningRoomElvenBandanna: LocationData(0x13018D, 455, "Chest"), + LocationName.MansionDiningRoomPotion: LocationData(0x13018E, 456, "Chest"), + LocationName.NaminesSketches: LocationData(0x13018F, 289, "Chest"), + LocationName.MansionMap: LocationData(0x130190, 483, "Chest"), + LocationName.MansionLibraryHiPotion: LocationData(0x130191, 459, "Chest"), + LocationName.Axel2: LocationData(0x130192, 34, "Get Bonus", "Roxas", 14), + LocationName.MansionBasementCorridorHiPotion: LocationData(0x130193, 463, "Chest"), + LocationName.RoxasDataMagicBoost: LocationData(0x130194, 558, "Chest"), + +} +TT_Checks = { + LocationName.OldMansionPotion: LocationData(0x130195, 447, "Chest"), + LocationName.OldMansionMythrilShard: LocationData(0x130196, 448, "Chest"), + LocationName.TheWoodsPotion: LocationData(0x130197, 442, "Chest"), + LocationName.TheWoodsMythrilShard: LocationData(0x130198, 443, "Chest"), + LocationName.TheWoodsHiPotion: LocationData(0x130199, 444, "Chest"), + LocationName.TramCommonHiPotion: LocationData(0x13019A, 420, "Chest"), + LocationName.TramCommonAPBoost: LocationData(0x13019B, 421, "Chest"), + LocationName.TramCommonTent: LocationData(0x13019C, 422, "Chest"), + LocationName.TramCommonMythrilShard1: LocationData(0x13019D, 423, "Chest"), + LocationName.TramCommonPotion1: LocationData(0x13019E, 424, "Chest"), + LocationName.TramCommonMythrilShard2: LocationData(0x13019F, 425, "Chest"), + LocationName.TramCommonPotion2: LocationData(0x1301A0, 484, "Chest"), + LocationName.StationPlazaSecretAnsemReport2: LocationData(0x1301A1, 526, "Chest"), + LocationName.MunnyPouchMickey: LocationData(0x1301A2, 290, "Chest"), + LocationName.CrystalOrb: LocationData(0x1301A3, 291, "Chest"), + LocationName.CentralStationTent: LocationData(0x1301A4, 431, "Chest"), + LocationName.TTCentralStationHiPotion: LocationData(0x1301A5, 432, "Chest"), + LocationName.CentralStationMythrilShard: LocationData(0x1301A6, 433, "Chest"), + LocationName.TheTowerPotion: LocationData(0x1301A7, 465, "Chest"), + LocationName.TheTowerHiPotion: LocationData(0x1301A8, 466, "Chest"), + LocationName.TheTowerEther: LocationData(0x1301A9, 522, "Chest"), + LocationName.TowerEntrywayEther: LocationData(0x1301AA, 467, "Chest"), + LocationName.TowerEntrywayMythrilShard: LocationData(0x1301AB, 468, "Chest"), + LocationName.SorcerersLoftTowerMap: LocationData(0x1301AC, 469, "Chest"), + LocationName.TowerWardrobeMythrilStone: LocationData(0x1301AD, 470, "Chest"), + LocationName.StarSeeker: LocationData(0x1301AE, 304, "Chest"), + LocationName.ValorForm: LocationData(0x1301AF, 286, "Chest"), + +} +TT2_Checks = { + LocationName.SeifersTrophy: LocationData(0x1301B0, 294, "Chest"), + LocationName.Oathkeeper: LocationData(0x1301B1, 265, "Chest"), + LocationName.LimitForm: LocationData(0x1301B2, 543, "Chest"), +} +TT3_Checks = { + LocationName.UndergroundConcourseMythrilGem: LocationData(0x1301B3, 479, "Chest"), + LocationName.UndergroundConcourseAPBoost: LocationData(0x1301B4, 481, "Chest"), + LocationName.UndergroundConcourseOrichalcum: LocationData(0x1301B5, 480, "Chest"), + LocationName.UndergroundConcourseMythrilCrystal: LocationData(0x1301B6, 482, "Chest"), + LocationName.TunnelwayOrichalcum: LocationData(0x1301B7, 477, "Chest"), + LocationName.TunnelwayMythrilCrystal: LocationData(0x1301B8, 478, "Chest"), + LocationName.SunsetTerraceOrichalcumPlus: LocationData(0x1301B9, 438, "Chest"), + LocationName.SunsetTerraceMythrilShard: LocationData(0x1301BA, 439, "Chest"), + LocationName.SunsetTerraceMythrilCrystal: LocationData(0x1301BB, 440, "Chest"), + LocationName.SunsetTerraceAPBoost: LocationData(0x1301BC, 441, "Chest"), + LocationName.MansionNobodies: LocationData(0x1301BD, 56, "Get Bonus"), + LocationName.MansionFoyerMythrilCrystal: LocationData(0x1301BE, 452, "Chest"), + LocationName.MansionFoyerMythrilStone: LocationData(0x1301BF, 453, "Chest"), + LocationName.MansionFoyerSerenityCrystal: LocationData(0x1301C0, 454, "Chest"), + LocationName.MansionDiningRoomMythrilCrystal: LocationData(0x1301C1, 457, "Chest"), + LocationName.MansionDiningRoomMythrilStone: LocationData(0x1301C2, 458, "Chest"), + LocationName.MansionLibraryOrichalcum: LocationData(0x1301C3, 460, "Chest"), + LocationName.BeamSecretAnsemReport10: LocationData(0x1301C4, 534, "Chest"), + LocationName.MansionBasementCorridorUltimateRecipe: LocationData(0x1301C5, 464, "Chest"), + LocationName.BetwixtandBetween: LocationData(0x1301C6, 63, "Get Bonus"), + LocationName.BetwixtandBetweenBondofFlame: LocationData(0x1301C7, 317, "Chest"), + LocationName.AxelDataMagicBoost: LocationData(0x1301C8, 561, "Chest"), +} +TWTNW_Checks = { + LocationName.FragmentCrossingMythrilStone: LocationData(0x1301C9, 374, "Chest"), + LocationName.FragmentCrossingMythrilCrystal: LocationData(0x1301CA, 375, "Chest"), + LocationName.FragmentCrossingAPBoost: LocationData(0x1301CB, 376, "Chest"), + LocationName.FragmentCrossingOrichalcum: LocationData(0x1301CC, 377, "Chest"), + LocationName.Roxas: LocationData(0x1301CD, 69, "Double Get Bonus"), + LocationName.RoxasGetBonus: LocationData(0x1301CE, 69, "Second Get Bonus"), + LocationName.RoxasSecretAnsemReport8: LocationData(0x1301CF, 532, "Chest"), + LocationName.TwoBecomeOne: LocationData(0x1301D0, 277, "Chest"), + LocationName.MemorysSkyscaperMythrilCrystal: LocationData(0x1301D1, 391, "Chest"), + LocationName.MemorysSkyscaperAPBoost: LocationData(0x1301D2, 523, "Chest"), + LocationName.MemorysSkyscaperMythrilStone: LocationData(0x1301D3, 524, "Chest"), + LocationName.TheBrinkofDespairDarkCityMap: LocationData(0x1301D4, 335, "Chest"), + LocationName.TheBrinkofDespairOrichalcumPlus: LocationData(0x1301D5, 500, "Chest"), + LocationName.NothingsCallMythrilGem: LocationData(0x1301D6, 378, "Chest"), + LocationName.NothingsCallOrichalcum: LocationData(0x1301D7, 379, "Chest"), + LocationName.TwilightsViewCosmicBelt: LocationData(0x1301D8, 336, "Chest"), +} +TWTNW2_Checks = { + LocationName.XigbarBonus: LocationData(0x1301D9, 23, "Get Bonus"), + LocationName.XigbarSecretAnsemReport3: LocationData(0x1301DA, 527, "Chest"), + LocationName.NaughtsSkywayMythrilGem: LocationData(0x1301DB, 380, "Chest"), + LocationName.NaughtsSkywayOrichalcum: LocationData(0x1301DC, 381, "Chest"), + LocationName.NaughtsSkywayMythrilCrystal: LocationData(0x1301DD, 382, "Chest"), + LocationName.Oblivion: LocationData(0x1301DE, 278, "Chest"), + LocationName.CastleThatNeverWasMap: LocationData(0x1301DF, 496, "Chest"), + LocationName.Luxord: LocationData(0x1301E0, 24, "Double Get Bonus"), + LocationName.LuxordGetBonus: LocationData(0x1301E1, 24, "Second Get Bonus"), + LocationName.LuxordSecretAnsemReport9: LocationData(0x1301E2, 533, "Chest"), + LocationName.SaixBonus: LocationData(0x1301E3, 25, "Get Bonus"), + LocationName.SaixSecretAnsemReport12: LocationData(0x1301E4, 536, "Chest"), + LocationName.PreXemnas1SecretAnsemReport11: LocationData(0x1301E5, 535, "Chest"), + LocationName.RuinandCreationsPassageMythrilStone: LocationData(0x1301E6, 385, "Chest"), + LocationName.RuinandCreationsPassageAPBoost: LocationData(0x1301E7, 386, "Chest"), + LocationName.RuinandCreationsPassageMythrilCrystal: LocationData(0x1301E8, 387, "Chest"), + LocationName.RuinandCreationsPassageOrichalcum: LocationData(0x1301E9, 388, "Chest"), + LocationName.Xemnas1: LocationData(0x1301EA, 26, "Double Get Bonus"), + LocationName.Xemnas1GetBonus: LocationData(0x1301EB, 26, "Second Get Bonus"), + LocationName.Xemnas1SecretAnsemReport13: LocationData(0x1301EC, 537, "Chest"), + LocationName.FinalXemnas: LocationData(0x1301ED, 71, "Get Bonus"), + LocationName.XemnasDataPowerBoost: LocationData(0x1301EE, 554, "Chest"), +} + +SoraLevels = { + LocationName.Lvl1: LocationData(0x1301EF, 1, "Levels"), + LocationName.Lvl2: LocationData(0x1301F0, 2, "Levels"), + LocationName.Lvl3: LocationData(0x1301F1, 3, "Levels"), + LocationName.Lvl4: LocationData(0x1301F2, 4, "Levels"), + LocationName.Lvl5: LocationData(0x1301F3, 5, "Levels"), + LocationName.Lvl6: LocationData(0x1301F4, 6, "Levels"), + LocationName.Lvl7: LocationData(0x1301F5, 7, "Levels"), + LocationName.Lvl8: LocationData(0x1301F6, 8, "Levels"), + LocationName.Lvl9: LocationData(0x1301F7, 9, "Levels"), + LocationName.Lvl10: LocationData(0x1301F8, 10, "Levels"), + LocationName.Lvl11: LocationData(0x1301F9, 11, "Levels"), + LocationName.Lvl12: LocationData(0x1301FA, 12, "Levels"), + LocationName.Lvl13: LocationData(0x1301FB, 13, "Levels"), + LocationName.Lvl14: LocationData(0x1301FC, 14, "Levels"), + LocationName.Lvl15: LocationData(0x1301FD, 15, "Levels"), + LocationName.Lvl16: LocationData(0x1301FE, 16, "Levels"), + LocationName.Lvl17: LocationData(0x1301FF, 17, "Levels"), + LocationName.Lvl18: LocationData(0x130200, 18, "Levels"), + LocationName.Lvl19: LocationData(0x130201, 19, "Levels"), + LocationName.Lvl20: LocationData(0x130202, 20, "Levels"), + LocationName.Lvl21: LocationData(0x130203, 21, "Levels"), + LocationName.Lvl22: LocationData(0x130204, 22, "Levels"), + LocationName.Lvl23: LocationData(0x130205, 23, "Levels"), + LocationName.Lvl24: LocationData(0x130206, 24, "Levels"), + LocationName.Lvl25: LocationData(0x130207, 25, "Levels"), + LocationName.Lvl26: LocationData(0x130208, 26, "Levels"), + LocationName.Lvl27: LocationData(0x130209, 27, "Levels"), + LocationName.Lvl28: LocationData(0x13020A, 28, "Levels"), + LocationName.Lvl29: LocationData(0x13020B, 29, "Levels"), + LocationName.Lvl30: LocationData(0x13020C, 30, "Levels"), + LocationName.Lvl31: LocationData(0x13020D, 31, "Levels"), + LocationName.Lvl32: LocationData(0x13020E, 32, "Levels"), + LocationName.Lvl33: LocationData(0x13020F, 33, "Levels"), + LocationName.Lvl34: LocationData(0x130210, 34, "Levels"), + LocationName.Lvl35: LocationData(0x130211, 35, "Levels"), + LocationName.Lvl36: LocationData(0x130212, 36, "Levels"), + LocationName.Lvl37: LocationData(0x130213, 37, "Levels"), + LocationName.Lvl38: LocationData(0x130214, 38, "Levels"), + LocationName.Lvl39: LocationData(0x130215, 39, "Levels"), + LocationName.Lvl40: LocationData(0x130216, 40, "Levels"), + LocationName.Lvl41: LocationData(0x130217, 41, "Levels"), + LocationName.Lvl42: LocationData(0x130218, 42, "Levels"), + LocationName.Lvl43: LocationData(0x130219, 43, "Levels"), + LocationName.Lvl44: LocationData(0x13021A, 44, "Levels"), + LocationName.Lvl45: LocationData(0x13021B, 45, "Levels"), + LocationName.Lvl46: LocationData(0x13021C, 46, "Levels"), + LocationName.Lvl47: LocationData(0x13021D, 47, "Levels"), + LocationName.Lvl48: LocationData(0x13021E, 48, "Levels"), + LocationName.Lvl49: LocationData(0x13021F, 49, "Levels"), + LocationName.Lvl50: LocationData(0x130220, 50, "Levels"), + LocationName.Lvl51: LocationData(0x130221, 51, "Levels"), + LocationName.Lvl52: LocationData(0x130222, 52, "Levels"), + LocationName.Lvl53: LocationData(0x130223, 53, "Levels"), + LocationName.Lvl54: LocationData(0x130224, 54, "Levels"), + LocationName.Lvl55: LocationData(0x130225, 55, "Levels"), + LocationName.Lvl56: LocationData(0x130226, 56, "Levels"), + LocationName.Lvl57: LocationData(0x130227, 57, "Levels"), + LocationName.Lvl58: LocationData(0x130228, 58, "Levels"), + LocationName.Lvl59: LocationData(0x130229, 59, "Levels"), + LocationName.Lvl60: LocationData(0x13022A, 60, "Levels"), + LocationName.Lvl61: LocationData(0x13022B, 61, "Levels"), + LocationName.Lvl62: LocationData(0x13022C, 62, "Levels"), + LocationName.Lvl63: LocationData(0x13022D, 63, "Levels"), + LocationName.Lvl64: LocationData(0x13022E, 64, "Levels"), + LocationName.Lvl65: LocationData(0x13022F, 65, "Levels"), + LocationName.Lvl66: LocationData(0x130230, 66, "Levels"), + LocationName.Lvl67: LocationData(0x130231, 67, "Levels"), + LocationName.Lvl68: LocationData(0x130232, 68, "Levels"), + LocationName.Lvl69: LocationData(0x130233, 69, "Levels"), + LocationName.Lvl70: LocationData(0x130234, 70, "Levels"), + LocationName.Lvl71: LocationData(0x130235, 71, "Levels"), + LocationName.Lvl72: LocationData(0x130236, 72, "Levels"), + LocationName.Lvl73: LocationData(0x130237, 73, "Levels"), + LocationName.Lvl74: LocationData(0x130238, 74, "Levels"), + LocationName.Lvl75: LocationData(0x130239, 75, "Levels"), + LocationName.Lvl76: LocationData(0x13023A, 76, "Levels"), + LocationName.Lvl77: LocationData(0x13023B, 77, "Levels"), + LocationName.Lvl78: LocationData(0x13023C, 78, "Levels"), + LocationName.Lvl79: LocationData(0x13023D, 79, "Levels"), + LocationName.Lvl80: LocationData(0x13023E, 80, "Levels"), + LocationName.Lvl81: LocationData(0x13023F, 81, "Levels"), + LocationName.Lvl82: LocationData(0x130240, 82, "Levels"), + LocationName.Lvl83: LocationData(0x130241, 83, "Levels"), + LocationName.Lvl84: LocationData(0x130242, 84, "Levels"), + LocationName.Lvl85: LocationData(0x130243, 85, "Levels"), + LocationName.Lvl86: LocationData(0x130244, 86, "Levels"), + LocationName.Lvl87: LocationData(0x130245, 87, "Levels"), + LocationName.Lvl88: LocationData(0x130246, 88, "Levels"), + LocationName.Lvl89: LocationData(0x130247, 89, "Levels"), + LocationName.Lvl90: LocationData(0x130248, 90, "Levels"), + LocationName.Lvl91: LocationData(0x130249, 91, "Levels"), + LocationName.Lvl92: LocationData(0x13024A, 92, "Levels"), + LocationName.Lvl93: LocationData(0x13024B, 93, "Levels"), + LocationName.Lvl94: LocationData(0x13024C, 94, "Levels"), + LocationName.Lvl95: LocationData(0x13024D, 95, "Levels"), + LocationName.Lvl96: LocationData(0x13024E, 96, "Levels"), + LocationName.Lvl97: LocationData(0x13024F, 97, "Levels"), + LocationName.Lvl98: LocationData(0x130250, 98, "Levels"), + LocationName.Lvl99: LocationData(0x130251, 99, "Levels"), +} +Form_Checks = { + LocationName.Valorlvl2: LocationData(0x130253, 2, "Forms", 1), + LocationName.Valorlvl3: LocationData(0x130254, 3, "Forms", 1), + LocationName.Valorlvl4: LocationData(0x130255, 4, "Forms", 1), + LocationName.Valorlvl5: LocationData(0x130256, 5, "Forms", 1), + LocationName.Valorlvl6: LocationData(0x130257, 6, "Forms", 1), + LocationName.Valorlvl7: LocationData(0x130258, 7, "Forms", 1), + + LocationName.Wisdomlvl2: LocationData(0x13025A, 2, "Forms", 2), + LocationName.Wisdomlvl3: LocationData(0x13025B, 3, "Forms", 2), + LocationName.Wisdomlvl4: LocationData(0x13025C, 4, "Forms", 2), + LocationName.Wisdomlvl5: LocationData(0x13025D, 5, "Forms", 2), + LocationName.Wisdomlvl6: LocationData(0x13025E, 6, "Forms", 2), + LocationName.Wisdomlvl7: LocationData(0x13025F, 7, "Forms", 2), + + LocationName.Limitlvl2: LocationData(0x130261, 2, "Forms", 3), + LocationName.Limitlvl3: LocationData(0x130262, 3, "Forms", 3), + LocationName.Limitlvl4: LocationData(0x130263, 4, "Forms", 3), + LocationName.Limitlvl5: LocationData(0x130264, 5, "Forms", 3), + LocationName.Limitlvl6: LocationData(0x130265, 6, "Forms", 3), + LocationName.Limitlvl7: LocationData(0x130266, 7, "Forms", 3), + + LocationName.Masterlvl2: LocationData(0x130268, 2, "Forms", 4), + LocationName.Masterlvl3: LocationData(0x130269, 3, "Forms", 4), + LocationName.Masterlvl4: LocationData(0x13026A, 4, "Forms", 4), + LocationName.Masterlvl5: LocationData(0x13026B, 5, "Forms", 4), + LocationName.Masterlvl6: LocationData(0x13026C, 6, "Forms", 4), + LocationName.Masterlvl7: LocationData(0x13026D, 7, "Forms", 4), + + LocationName.Finallvl2: LocationData(0x13026F, 2, "Forms", 5), + LocationName.Finallvl3: LocationData(0x130270, 3, "Forms", 5), + LocationName.Finallvl4: LocationData(0x130271, 4, "Forms", 5), + LocationName.Finallvl5: LocationData(0x130272, 5, "Forms", 5), + LocationName.Finallvl6: LocationData(0x130273, 6, "Forms", 5), + LocationName.Finallvl7: LocationData(0x130274, 7, "Forms", 5), +} +GoA_Checks = { + LocationName.GardenofAssemblageMap: LocationData(0x130275, 585, "Chest"), + LocationName.GoALostIllusion: LocationData(0x130276, 586, "Chest"), + LocationName.ProofofNonexistence: LocationData(0x130277, 590, "Chest"), +} +Keyblade_Slots = { + LocationName.FAKESlot: LocationData(0x130278, 116, "Keyblade"), + LocationName.DetectionSaberSlot: LocationData(0x130279, 83, "Keyblade"), + LocationName.EdgeofUltimaSlot: LocationData(0x13027A, 84, "Keyblade"), + LocationName.KingdomKeySlot: LocationData(0x13027B, 80, "Keyblade"), + LocationName.OathkeeperSlot: LocationData(0x13027C, 81, "Keyblade"), + LocationName.OblivionSlot: LocationData(0x13027D, 82, "Keyblade"), + LocationName.StarSeekerSlot: LocationData(0x13027E, 123, "Keyblade"), + LocationName.HiddenDragonSlot: LocationData(0x13027F, 124, "Keyblade"), + LocationName.HerosCrestSlot: LocationData(0x130280, 127, "Keyblade"), + LocationName.MonochromeSlot: LocationData(0x130281, 128, "Keyblade"), + LocationName.FollowtheWindSlot: LocationData(0x130282, 129, "Keyblade"), + LocationName.CircleofLifeSlot: LocationData(0x130283, 130, "Keyblade"), + LocationName.PhotonDebuggerSlot: LocationData(0x130284, 131, "Keyblade"), + LocationName.GullWingSlot: LocationData(0x130285, 132, "Keyblade"), + LocationName.RumblingRoseSlot: LocationData(0x130286, 133, "Keyblade"), + LocationName.GuardianSoulSlot: LocationData(0x130287, 134, "Keyblade"), + LocationName.WishingLampSlot: LocationData(0x130288, 135, "Keyblade"), + LocationName.DecisivePumpkinSlot: LocationData(0x130289, 136, "Keyblade"), + LocationName.SweetMemoriesSlot: LocationData(0x13028A, 138, "Keyblade"), + LocationName.MysteriousAbyssSlot: LocationData(0x13028B, 139, "Keyblade"), + LocationName.SleepingLionSlot: LocationData(0x13028C, 137, "Keyblade"), + LocationName.BondofFlameSlot: LocationData(0x13028D, 141, "Keyblade"), + LocationName.TwoBecomeOneSlot: LocationData(0x13028E, 148, "Keyblade"), + LocationName.FatalCrestSlot: LocationData(0x13028F, 140, "Keyblade"), + LocationName.FenrirSlot: LocationData(0x130290, 142, "Keyblade"), + LocationName.UltimaWeaponSlot: LocationData(0x130291, 143, "Keyblade"), + LocationName.WinnersProofSlot: LocationData(0x130292, 149, "Keyblade"), + LocationName.PurebloodSlot: LocationData(0x1302DB, 85, "Keyblade"), +} +# checks are given when talking to the computer in the GoA +Critical_Checks = { + LocationName.Crit_1: LocationData(0x130293, 1, "Critical"), + LocationName.Crit_2: LocationData(0x130294, 1, "Critical"), + LocationName.Crit_3: LocationData(0x130295, 1, "Critical"), + LocationName.Crit_4: LocationData(0x130296, 1, "Critical"), + LocationName.Crit_5: LocationData(0x130297, 1, "Critical"), + LocationName.Crit_6: LocationData(0x130298, 1, "Critical"), + LocationName.Crit_7: LocationData(0x130299, 1, "Critical"), +} + +Donald_Checks = { + LocationName.DonaldScreens: LocationData(0x13029A, 45, "Get Bonus", "Donald", 2), + LocationName.DonaldDemyxHBGetBonus: LocationData(0x13029B, 28, "Get Bonus", "Donald", 2), + LocationName.DonaldDemyxOC: LocationData(0x13029C, 58, "Get Bonus", "Donald", 2), + LocationName.DonaldBoatPete: LocationData(0x13029D, 16, "Double Get Bonus", "Donald", 2), + LocationName.DonaldBoatPeteGetBonus: LocationData(0x13029E, 16, "Second Get Bonus", "Donald", 2), + LocationName.DonaldPrisonKeeper: LocationData(0x13029F, 18, "Get Bonus", "Donald", 2), + LocationName.DonaldScar: LocationData(0x1302A0, 29, "Get Bonus", "Donald", 2), + LocationName.DonaldSolarSailer: LocationData(0x1302A1, 61, "Get Bonus", "Donald", 2), + LocationName.DonaldExperiment: LocationData(0x1302A2, 20, "Get Bonus", "Donald", 2), + LocationName.DonaldBoatFight: LocationData(0x1302A3, 62, "Get Bonus", "Donald", 2), + LocationName.DonaldMansionNobodies: LocationData(0x1302A4, 56, "Get Bonus", "Donald", 2), + LocationName.DonaldThresholder: LocationData(0x1302A5, 2, "Get Bonus", "Donald", 2), + LocationName.DonaldXaldinGetBonus: LocationData(0x1302A6, 4, "Get Bonus", "Donald", 2), + LocationName.DonaladGrimReaper2: LocationData(0x1302A7, 22, "Get Bonus", "Donald", 2), + + LocationName.CometStaff: LocationData(0x1302A8, 90, "Keyblade", "Donald"), + LocationName.HammerStaff: LocationData(0x1302A9, 87, "Keyblade", "Donald"), + LocationName.LordsBroom: LocationData(0x1302AA, 91, "Keyblade", "Donald"), + LocationName.MagesStaff: LocationData(0x1302AB, 86, "Keyblade", "Donald"), + LocationName.MeteorStaff: LocationData(0x1302AC, 89, "Keyblade", "Donald"), + LocationName.NobodyLance: LocationData(0x1302AD, 94, "Keyblade", "Donald"), + LocationName.PreciousMushroom: LocationData(0x1302AE, 154, "Keyblade", "Donald"), + LocationName.PreciousMushroom2: LocationData(0x1302AF, 155, "Keyblade", "Donald"), + LocationName.PremiumMushroom: LocationData(0x1302B0, 156, "Keyblade", "Donald"), + LocationName.RisingDragon: LocationData(0x1302B1, 93, "Keyblade", "Donald"), + LocationName.SaveTheQueen2: LocationData(0x1302B2, 146, "Keyblade", "Donald"), + LocationName.ShamansRelic: LocationData(0x1302B3, 95, "Keyblade", "Donald"), + LocationName.VictoryBell: LocationData(0x1302B4, 88, "Keyblade", "Donald"), + LocationName.WisdomWand: LocationData(0x1302B5, 92, "Keyblade", "Donald"), + LocationName.Centurion2: LocationData(0x1302B6, 151, "Keyblade", "Donald"), + LocationName.DonaldAbuEscort: LocationData(0x1302B7, 42, "Get Bonus", "Donald", 2), + LocationName.DonaldStarting1: LocationData(0x1302B8, 2, "Critical", "Donald"), + LocationName.DonaldStarting2: LocationData(0x1302B9, 2, "Critical", "Donald"), +} + +Goofy_Checks = { + LocationName.GoofyBarbossa: LocationData(0x1302BA, 21, "Double Get Bonus", "Goofy", 3), + LocationName.GoofyBarbossaGetBonus: LocationData(0x1302BB, 21, "Second Get Bonus", "Goofy", 3), + LocationName.GoofyGrimReaper1: LocationData(0x1302BC, 59, "Get Bonus", "Goofy", 3), + LocationName.GoofyHostileProgram: LocationData(0x1302BD, 31, "Get Bonus", "Goofy", 3), + LocationName.GoofyHyenas1: LocationData(0x1302BE, 49, "Get Bonus", "Goofy", 3), + LocationName.GoofyHyenas2: LocationData(0x1302BF, 50, "Get Bonus", "Goofy", 3), + LocationName.GoofyLock: LocationData(0x1302C0, 40, "Get Bonus", "Goofy", 3), + LocationName.GoofyOogieBoogie: LocationData(0x1302C1, 19, "Get Bonus", "Goofy", 3), + LocationName.GoofyPeteOC: LocationData(0x1302C2, 6, "Get Bonus", "Goofy", 3), + LocationName.GoofyFuturePete: LocationData(0x1302C3, 17, "Get Bonus", "Goofy", 3), + LocationName.GoofyShanYu: LocationData(0x1302C4, 9, "Get Bonus", "Goofy", 3), + LocationName.GoofyStormRider: LocationData(0x1302C5, 10, "Get Bonus", "Goofy", 3), + LocationName.GoofyBeast: LocationData(0x1302C6, 12, "Get Bonus", "Goofy", 3), + LocationName.GoofyInterceptorBarrels: LocationData(0x1302C7, 39, "Get Bonus", "Goofy", 3), + LocationName.GoofyTreasureRoom: LocationData(0x1302C8, 46, "Get Bonus", "Goofy", 3), + LocationName.GoofyZexion: LocationData(0x1302C9, 66, "Get Bonus", "Goofy", 3), + + LocationName.AdamantShield: LocationData(0x1302CA, 100, "Keyblade", "Goofy"), + LocationName.AkashicRecord: LocationData(0x1302CB, 107, "Keyblade", "Goofy"), + LocationName.ChainGear: LocationData(0x1302CC, 101, "Keyblade", "Goofy"), + LocationName.DreamCloud: LocationData(0x1302CD, 104, "Keyblade", "Goofy"), + LocationName.FallingStar: LocationData(0x1302CE, 103, "Keyblade", "Goofy"), + LocationName.FrozenPride2: LocationData(0x1302CF, 158, "Keyblade", "Goofy"), + LocationName.GenjiShield: LocationData(0x1302D0, 106, "Keyblade", "Goofy"), + LocationName.KnightDefender: LocationData(0x1302D1, 105, "Keyblade", "Goofy"), + LocationName.KnightsShield: LocationData(0x1302D2, 99, "Keyblade", "Goofy"), + LocationName.MajesticMushroom: LocationData(0x1302D3, 161, "Keyblade", "Goofy"), + LocationName.MajesticMushroom2: LocationData(0x1302D4, 162, "Keyblade", "Goofy"), + LocationName.NobodyGuard: LocationData(0x1302D5, 108, "Keyblade", "Goofy"), + LocationName.OgreShield: LocationData(0x1302D6, 102, "Keyblade", "Goofy"), + LocationName.SaveTheKing2: LocationData(0x1302D7, 147, "Keyblade", "Goofy"), + LocationName.UltimateMushroom: LocationData(0x1302D8, 163, "Keyblade", "Goofy"), + LocationName.GoofyStarting1: LocationData(0x1302D9, 3, "Critical", "Goofy"), + LocationName.GoofyStarting2: LocationData(0x1302DA, 3, "Critical", "Goofy"), +} +exclusion_table = { + "Popups": { + LocationName.SweetMemories, + LocationName.SpookyCaveMap, + LocationName.StarryHillCureElement, + LocationName.StarryHillOrichalcumPlus, + LocationName.AgrabahMap, + LocationName.LampCharm, + LocationName.WishingLamp, + LocationName.DarkThornCureElement, + LocationName.RumblingRose, + LocationName.CastleWallsMap, + LocationName.SecretAnsemReport4, + LocationName.DisneyCastleMap, + LocationName.WindowofTimeMap, + LocationName.Monochrome, + LocationName.WisdomForm, + LocationName.LingeringWillProofofConnection, + LocationName.LingeringWillManifestIllusion, + LocationName.OogieBoogieMagnetElement, + LocationName.Present, + LocationName.DecoyPresents, + LocationName.DecisivePumpkin, + LocationName.MarketplaceMap, + LocationName.MerlinsHouseMembershipCard, + LocationName.MerlinsHouseBlizzardElement, + LocationName.BaileySecretAnsemReport7, + LocationName.BaseballCharm, + LocationName.AnsemsStudyMasterForm, + LocationName.AnsemsStudySkillRecipe, + LocationName.AnsemsStudySleepingLion, + LocationName.FFFightsCureElement, + LocationName.ThousandHeartlessSecretAnsemReport1, + LocationName.ThousandHeartlessIceCream, + LocationName.ThousandHeartlessPicture, + LocationName.WinnersProof, + LocationName.ProofofPeace, + LocationName.SephirothFenrir, + LocationName.EncampmentAreaMap, + LocationName.Mission3, + LocationName.VillageCaveAreaMap, + LocationName.HiddenDragon, + LocationName.ColiseumMap, + LocationName.SecretAnsemReport6, + LocationName.OlympusStone, + LocationName.HerosCrest, + LocationName.AuronsStatue, + LocationName.GuardianSoul, + LocationName.ProtectBeltPainandPanicCup, + LocationName.SerenityGemPainandPanicCup, + LocationName.RisingDragonCerberusCup, + LocationName.SerenityCrystalCerberusCup, + LocationName.GenjiShieldTitanCup, + LocationName.SkillfulRingTitanCup, + LocationName.FatalCrestGoddessofFateCup, + LocationName.OrichalcumPlusGoddessofFateCup, + LocationName.HadesCupTrophyParadoxCups, + LocationName.IsladeMuertaMap, + LocationName.FollowtheWind, + LocationName.SeadriftRowCursedMedallion, + LocationName.SeadriftRowShipGraveyardMap, + LocationName.SecretAnsemReport5, + LocationName.CircleofLife, + LocationName.ScarFireElement, + LocationName.TwilightTownMap, + LocationName.MunnyPouchOlette, + LocationName.JunkChampionBelt, + LocationName.JunkMedal, + LocationName.TheStruggleTrophy, + LocationName.NaminesSketches, + LocationName.MansionMap, + LocationName.PhotonDebugger, + LocationName.StationPlazaSecretAnsemReport2, + LocationName.MunnyPouchMickey, + LocationName.CrystalOrb, + LocationName.StarSeeker, + LocationName.ValorForm, + LocationName.SeifersTrophy, + LocationName.Oathkeeper, + LocationName.LimitForm, + LocationName.BeamSecretAnsemReport10, + LocationName.BetwixtandBetweenBondofFlame, + LocationName.TwoBecomeOne, + LocationName.RoxasSecretAnsemReport8, + LocationName.XigbarSecretAnsemReport3, + LocationName.Oblivion, + LocationName.CastleThatNeverWasMap, + LocationName.LuxordSecretAnsemReport9, + LocationName.SaixSecretAnsemReport12, + LocationName.PreXemnas1SecretAnsemReport11, + LocationName.Xemnas1SecretAnsemReport13, + LocationName.XemnasDataPowerBoost, + LocationName.AxelDataMagicBoost, + LocationName.RoxasDataMagicBoost, + LocationName.SaixDataDefenseBoost, + LocationName.DemyxDataAPBoost, + LocationName.LuxordDataAPBoost, + LocationName.VexenDataLostIllusion, + LocationName.LarxeneDataLostIllusion, + LocationName.XaldinDataDefenseBoost, + LocationName.MarluxiaDataLostIllusion, + LocationName.LexaeusDataLostIllusion, + LocationName.XigbarDataDefenseBoost, + LocationName.VexenASRoadtoDiscovery, + LocationName.LarxeneASCloakedThunder, + LocationName.ZexionASBookofShadows, + LocationName.ZexionDataLostIllusion, + LocationName.LexaeusASStrengthBeyondStrength, + LocationName.MarluxiaASEternalBlossom + }, + "Datas": { + LocationName.XemnasDataPowerBoost, + LocationName.AxelDataMagicBoost, + LocationName.RoxasDataMagicBoost, + LocationName.SaixDataDefenseBoost, + LocationName.DemyxDataAPBoost, + LocationName.LuxordDataAPBoost, + LocationName.VexenDataLostIllusion, + LocationName.VexenBonus, + LocationName.VexenASRoadtoDiscovery, + LocationName.LarxeneDataLostIllusion, + LocationName.LarxeneBonus, + LocationName.LarxeneASCloakedThunder, + LocationName.XaldinDataDefenseBoost, + LocationName.MarluxiaDataLostIllusion, + LocationName.MarluxiaASEternalBlossom, + LocationName.MarluxiaGetBonus, + LocationName.LexaeusDataLostIllusion, + LocationName.LexaeusBonus, + LocationName.LexaeusASStrengthBeyondStrength, + LocationName.XigbarDataDefenseBoost, + LocationName.ZexionDataLostIllusion, + LocationName.ZexionBonus, + LocationName.ZexionASBookofShadows, + }, + "SuperBosses": { + LocationName.LingeringWillBonus, + LocationName.LingeringWillProofofConnection, + LocationName.LingeringWillManifestIllusion, + LocationName.SephirothBonus, + LocationName.SephirothFenrir, + }, + + # 23 checks spread through 50 levels + "Level50": { + LocationName.Lvl2, + LocationName.Lvl4, + LocationName.Lvl7, + LocationName.Lvl9, + LocationName.Lvl10, + LocationName.Lvl12, + LocationName.Lvl14, + LocationName.Lvl15, + LocationName.Lvl17, + LocationName.Lvl20, + LocationName.Lvl23, + LocationName.Lvl25, + LocationName.Lvl28, + LocationName.Lvl30, + LocationName.Lvl32, + LocationName.Lvl34, + LocationName.Lvl36, + LocationName.Lvl39, + LocationName.Lvl41, + LocationName.Lvl44, + LocationName.Lvl46, + LocationName.Lvl48, + LocationName.Lvl50, + }, + # 23 checks spread through 99 levels + "Level99": { + LocationName.Lvl7, + LocationName.Lvl9, + LocationName.Lvl12, + LocationName.Lvl15, + LocationName.Lvl17, + LocationName.Lvl20, + LocationName.Lvl23, + LocationName.Lvl25, + LocationName.Lvl28, + LocationName.Lvl31, + LocationName.Lvl33, + LocationName.Lvl36, + LocationName.Lvl39, + LocationName.Lvl41, + LocationName.Lvl44, + LocationName.Lvl47, + LocationName.Lvl49, + LocationName.Lvl53, + LocationName.Lvl59, + LocationName.Lvl65, + LocationName.Lvl73, + LocationName.Lvl85, + LocationName.Lvl99, + }, + "Level50Sanity": { + LocationName.Lvl2, + LocationName.Lvl3, + LocationName.Lvl4, + LocationName.Lvl5, + LocationName.Lvl6, + LocationName.Lvl7, + LocationName.Lvl8, + LocationName.Lvl9, + LocationName.Lvl10, + LocationName.Lvl11, + LocationName.Lvl12, + LocationName.Lvl13, + LocationName.Lvl14, + LocationName.Lvl15, + LocationName.Lvl16, + LocationName.Lvl17, + LocationName.Lvl18, + LocationName.Lvl19, + LocationName.Lvl20, + LocationName.Lvl21, + LocationName.Lvl22, + LocationName.Lvl23, + LocationName.Lvl24, + LocationName.Lvl25, + LocationName.Lvl26, + LocationName.Lvl27, + LocationName.Lvl28, + LocationName.Lvl29, + LocationName.Lvl30, + LocationName.Lvl31, + LocationName.Lvl32, + LocationName.Lvl33, + LocationName.Lvl34, + LocationName.Lvl35, + LocationName.Lvl36, + LocationName.Lvl37, + LocationName.Lvl38, + LocationName.Lvl39, + LocationName.Lvl40, + LocationName.Lvl41, + LocationName.Lvl42, + LocationName.Lvl43, + LocationName.Lvl44, + LocationName.Lvl45, + LocationName.Lvl46, + LocationName.Lvl47, + LocationName.Lvl48, + LocationName.Lvl49, + LocationName.Lvl50, + }, + "Level99Sanity": { + LocationName.Lvl51, + LocationName.Lvl52, + LocationName.Lvl53, + LocationName.Lvl54, + LocationName.Lvl55, + LocationName.Lvl56, + LocationName.Lvl57, + LocationName.Lvl58, + LocationName.Lvl59, + LocationName.Lvl60, + LocationName.Lvl61, + LocationName.Lvl62, + LocationName.Lvl63, + LocationName.Lvl64, + LocationName.Lvl65, + LocationName.Lvl66, + LocationName.Lvl67, + LocationName.Lvl68, + LocationName.Lvl69, + LocationName.Lvl70, + LocationName.Lvl71, + LocationName.Lvl72, + LocationName.Lvl73, + LocationName.Lvl74, + LocationName.Lvl75, + LocationName.Lvl76, + LocationName.Lvl77, + LocationName.Lvl78, + LocationName.Lvl79, + LocationName.Lvl80, + LocationName.Lvl81, + LocationName.Lvl82, + LocationName.Lvl83, + LocationName.Lvl84, + LocationName.Lvl85, + LocationName.Lvl86, + LocationName.Lvl87, + LocationName.Lvl88, + LocationName.Lvl89, + LocationName.Lvl90, + LocationName.Lvl91, + LocationName.Lvl92, + LocationName.Lvl93, + LocationName.Lvl94, + LocationName.Lvl95, + LocationName.Lvl96, + LocationName.Lvl97, + LocationName.Lvl98, + LocationName.Lvl99, + }, + "Critical": { + LocationName.Crit_1, + LocationName.Crit_2, + LocationName.Crit_3, + LocationName.Crit_4, + LocationName.Crit_5, + LocationName.Crit_6, + LocationName.Crit_7, + }, + "Hitlist": { + LocationName.XemnasDataPowerBoost, + LocationName.AxelDataMagicBoost, + LocationName.RoxasDataMagicBoost, + LocationName.SaixDataDefenseBoost, + LocationName.DemyxDataAPBoost, + LocationName.LuxordDataAPBoost, + LocationName.VexenDataLostIllusion, + LocationName.LarxeneDataLostIllusion, + LocationName.XaldinDataDefenseBoost, + LocationName.MarluxiaDataLostIllusion, + LocationName.LexaeusDataLostIllusion, + LocationName.XigbarDataDefenseBoost, + LocationName.ZexionDataLostIllusion, + LocationName.SephirothFenrir, + LocationName.LingeringWillProofofConnection, + LocationName.StarryHillOrichalcumPlus, + LocationName.Valorlvl7, + LocationName.Wisdomlvl7, + LocationName.Limitlvl7, + LocationName.Masterlvl7, + LocationName.Finallvl7, + LocationName.TransporttoRemembrance, + LocationName.OrichalcumPlusGoddessofFateCup, + LocationName.HadesCupTrophyParadoxCups, + }, + "Cups": { + LocationName.ProtectBeltPainandPanicCup, + LocationName.SerenityGemPainandPanicCup, + LocationName.RisingDragonCerberusCup, + LocationName.SerenityCrystalCerberusCup, + LocationName.GenjiShieldTitanCup, + LocationName.SkillfulRingTitanCup, + LocationName.FatalCrestGoddessofFateCup, + LocationName.OrichalcumPlusGoddessofFateCup, + LocationName.HadesCupTrophyParadoxCups, + }, + "WeaponSlots": { + LocationName.FAKESlot: ItemName.ValorForm, + LocationName.DetectionSaberSlot: ItemName.MasterForm, + LocationName.EdgeofUltimaSlot: ItemName.FinalForm, + LocationName.OathkeeperSlot: ItemName.Oathkeeper, + LocationName.OblivionSlot: ItemName.Oblivion, + LocationName.StarSeekerSlot: ItemName.StarSeeker, + LocationName.HiddenDragonSlot: ItemName.HiddenDragon, + LocationName.HerosCrestSlot: ItemName.HerosCrest, + LocationName.MonochromeSlot: ItemName.Monochrome, + LocationName.FollowtheWindSlot: ItemName.FollowtheWind, + LocationName.CircleofLifeSlot: ItemName.CircleofLife, + LocationName.PhotonDebuggerSlot: ItemName.PhotonDebugger, + LocationName.GullWingSlot: ItemName.GullWing, + LocationName.RumblingRoseSlot: ItemName.RumblingRose, + LocationName.GuardianSoulSlot: ItemName.GuardianSoul, + LocationName.WishingLampSlot: ItemName.WishingLamp, + LocationName.DecisivePumpkinSlot: ItemName.DecisivePumpkin, + LocationName.SweetMemoriesSlot: ItemName.SweetMemories, + LocationName.MysteriousAbyssSlot: ItemName.MysteriousAbyss, + LocationName.SleepingLionSlot: ItemName.SleepingLion, + LocationName.BondofFlameSlot: ItemName.BondofFlame, + LocationName.TwoBecomeOneSlot: ItemName.TwoBecomeOne, + LocationName.FatalCrestSlot: ItemName.FatalCrest, + LocationName.FenrirSlot: ItemName.Fenrir, + LocationName.UltimaWeaponSlot: ItemName.UltimaWeapon, + LocationName.WinnersProofSlot: ItemName.WinnersProof, + LocationName.PurebloodSlot: ItemName.Pureblood, + # goofy + LocationName.AkashicRecord: ItemName.AkashicRecord, + LocationName.FrozenPride2: ItemName.FrozenPride2, + LocationName.GenjiShield: ItemName.GenjiShield, + LocationName.MajesticMushroom: ItemName.MajesticMushroom, + LocationName.MajesticMushroom2: ItemName.MajesticMushroom2, + LocationName.NobodyGuard: ItemName.NobodyGuard, + LocationName.OgreShield: ItemName.OgreShield, + LocationName.SaveTheKing2: ItemName.SaveTheKing2, + LocationName.UltimateMushroom: ItemName.UltimateMushroom, + # donald + LocationName.MeteorStaff: ItemName.MeteorStaff, + LocationName.NobodyLance: ItemName.NobodyLance, + LocationName.PreciousMushroom: ItemName.PreciousMushroom, + LocationName.PreciousMushroom2: ItemName.PreciousMushroom2, + LocationName.PremiumMushroom: ItemName.PremiumMushroom, + LocationName.RisingDragon: ItemName.RisingDragon, + LocationName.SaveTheQueen2: ItemName.SaveTheQueen2, + LocationName.ShamansRelic: ItemName.ShamansRelic, + LocationName.Centurion2: ItemName.Centurion2, + }, + "Chests": { + LocationName.BambooGroveDarkShard, + LocationName.BambooGroveEther, + LocationName.BambooGroveMythrilShard, + LocationName.CheckpointHiPotion, + LocationName.CheckpointMythrilShard, + LocationName.MountainTrailLightningShard, + LocationName.MountainTrailRecoveryRecipe, + LocationName.MountainTrailEther, + LocationName.MountainTrailMythrilShard, + LocationName.VillageCaveAPBoost, + LocationName.VillageCaveDarkShard, + LocationName.RidgeFrostShard, + LocationName.RidgeAPBoost, + LocationName.ThroneRoomTornPages, + LocationName.ThroneRoomPalaceMap, + LocationName.ThroneRoomAPBoost, + LocationName.ThroneRoomQueenRecipe, + LocationName.ThroneRoomAPBoost2, + LocationName.ThroneRoomOgreShield, + LocationName.ThroneRoomMythrilCrystal, + LocationName.ThroneRoomOrichalcum, + LocationName.AgrabahDarkShard, + LocationName.AgrabahMythrilShard, + LocationName.AgrabahHiPotion, + LocationName.AgrabahAPBoost, + LocationName.AgrabahMythrilStone, + LocationName.AgrabahMythrilShard2, + LocationName.AgrabahSerenityShard, + LocationName.BazaarMythrilGem, + LocationName.BazaarPowerShard, + LocationName.BazaarHiPotion, + LocationName.BazaarAPBoost, + LocationName.BazaarMythrilShard, + LocationName.PalaceWallsSkillRing, + LocationName.PalaceWallsMythrilStone, + LocationName.CaveEntrancePowerStone, + LocationName.CaveEntranceMythrilShard, + LocationName.ValleyofStoneMythrilStone, + LocationName.ValleyofStoneAPBoost, + LocationName.ValleyofStoneMythrilShard, + LocationName.ValleyofStoneHiPotion, + LocationName.ChasmofChallengesCaveofWondersMap, + LocationName.ChasmofChallengesAPBoost, + LocationName.TreasureRoomAPBoost, + LocationName.TreasureRoomSerenityGem, + LocationName.RuinedChamberTornPages, + LocationName.RuinedChamberRuinsMap, + LocationName.DCCourtyardMythrilShard, + LocationName.DCCourtyardStarRecipe, + LocationName.DCCourtyardAPBoost, + LocationName.DCCourtyardMythrilStone, + LocationName.DCCourtyardBlazingStone, + LocationName.DCCourtyardBlazingShard, + LocationName.DCCourtyardMythrilShard2, + LocationName.LibraryTornPages, + LocationName.CornerstoneHillMap, + LocationName.CornerstoneHillFrostShard, + LocationName.PierMythrilShard, + LocationName.PierHiPotion, + LocationName.WaterwayMythrilStone, + LocationName.WaterwayAPBoost, + LocationName.WaterwayFrostStone, + LocationName.PoohsHouse100AcreWoodMap, + LocationName.PoohsHouseAPBoost, + LocationName.PoohsHouseMythrilStone, + LocationName.PigletsHouseDefenseBoost, + LocationName.PigletsHouseAPBoost, + LocationName.PigletsHouseMythrilGem, + LocationName.RabbitsHouseDrawRing, + LocationName.RabbitsHouseMythrilCrystal, + LocationName.RabbitsHouseAPBoost, + LocationName.KangasHouseMagicBoost, + LocationName.KangasHouseAPBoost, + LocationName.KangasHouseOrichalcum, + LocationName.SpookyCaveMythrilGem, + LocationName.SpookyCaveAPBoost, + LocationName.SpookyCaveOrichalcum, + LocationName.SpookyCaveGuardRecipe, + LocationName.SpookyCaveMythrilCrystal, + LocationName.SpookyCaveAPBoost2, + LocationName.StarryHillCosmicRing, + LocationName.StarryHillStyleRecipe, + LocationName.RampartNavalMap, + LocationName.RampartMythrilStone, + LocationName.RampartDarkShard, + LocationName.TownDarkStone, + LocationName.TownAPBoost, + LocationName.TownMythrilShard, + LocationName.TownMythrilGem, + LocationName.CaveMouthBrightShard, + LocationName.CaveMouthMythrilShard, + LocationName.PowderStoreAPBoost1, + LocationName.PowderStoreAPBoost2, + LocationName.MoonlightNookMythrilShard, + LocationName.MoonlightNookSerenityGem, + LocationName.MoonlightNookPowerStone, + LocationName.InterceptorsHoldFeatherCharm, + LocationName.SeadriftKeepAPBoost, + LocationName.SeadriftKeepOrichalcum, + LocationName.SeadriftKeepMeteorStaff, + LocationName.SeadriftRowSerenityGem, + LocationName.SeadriftRowKingRecipe, + LocationName.SeadriftRowMythrilCrystal, + LocationName.PassageMythrilShard, + LocationName.PassageMythrilStone, + LocationName.PassageEther, + LocationName.PassageAPBoost, + LocationName.PassageHiPotion, + LocationName.InnerChamberUnderworldMap, + LocationName.InnerChamberMythrilShard, + LocationName.UnderworldEntrancePowerBoost, + LocationName.CavernsEntranceLucidShard, + LocationName.CavernsEntranceAPBoost, + LocationName.CavernsEntranceMythrilShard, + LocationName.TheLostRoadBrightShard, + LocationName.TheLostRoadEther, + LocationName.TheLostRoadMythrilShard, + LocationName.TheLostRoadMythrilStone, + LocationName.AtriumLucidStone, + LocationName.AtriumAPBoost, + LocationName.TheLockCavernsMap, + LocationName.TheLockMythrilShard, + LocationName.TheLockAPBoost, + LocationName.BCCourtyardAPBoost, + LocationName.BCCourtyardHiPotion, + LocationName.BCCourtyardMythrilShard, + LocationName.BellesRoomCastleMap, + LocationName.BellesRoomMegaRecipe, + LocationName.TheEastWingMythrilShard, + LocationName.TheEastWingTent, + LocationName.TheWestHallHiPotion, + LocationName.TheWestHallPowerShard, + LocationName.TheWestHallMythrilShard2, + LocationName.TheWestHallBrightStone, + LocationName.TheWestHallMythrilShard, + LocationName.DungeonBasementMap, + LocationName.DungeonAPBoost, + LocationName.SecretPassageMythrilShard, + LocationName.SecretPassageHiPotion, + LocationName.SecretPassageLucidShard, + LocationName.TheWestHallAPBoostPostDungeon, + LocationName.TheWestWingMythrilShard, + LocationName.TheWestWingTent, + LocationName.TheBeastsRoomBlazingShard, + LocationName.PitCellAreaMap, + LocationName.PitCellMythrilCrystal, + LocationName.CanyonDarkCrystal, + LocationName.CanyonMythrilStone, + LocationName.CanyonMythrilGem, + LocationName.CanyonFrostCrystal, + LocationName.HallwayPowerCrystal, + LocationName.HallwayAPBoost, + LocationName.CommunicationsRoomIOTowerMap, + LocationName.CommunicationsRoomGaiaBelt, + LocationName.CentralComputerCoreAPBoost, + LocationName.CentralComputerCoreOrichalcumPlus, + LocationName.CentralComputerCoreCosmicArts, + LocationName.CentralComputerCoreMap, + LocationName.GraveyardMythrilShard, + LocationName.GraveyardSerenityGem, + LocationName.FinklesteinsLabHalloweenTownMap, + LocationName.TownSquareMythrilStone, + LocationName.TownSquareEnergyShard, + LocationName.HinterlandsLightningShard, + LocationName.HinterlandsMythrilStone, + LocationName.HinterlandsAPBoost, + LocationName.CandyCaneLaneMegaPotion, + LocationName.CandyCaneLaneMythrilGem, + LocationName.CandyCaneLaneLightningStone, + LocationName.CandyCaneLaneMythrilStone, + LocationName.SantasHouseChristmasTownMap, + LocationName.SantasHouseAPBoost, + LocationName.BoroughDriveRecovery, + LocationName.BoroughAPBoost, + LocationName.BoroughHiPotion, + LocationName.BoroughMythrilShard, + LocationName.BoroughDarkShard, + LocationName.PosternCastlePerimeterMap, + LocationName.PosternMythrilGem, + LocationName.PosternAPBoost, + LocationName.CorridorsMythrilStone, + LocationName.CorridorsMythrilCrystal, + LocationName.CorridorsDarkCrystal, + LocationName.CorridorsAPBoost, + LocationName.AnsemsStudyUkuleleCharm, + LocationName.RestorationSiteMoonRecipe, + LocationName.RestorationSiteAPBoost, + LocationName.CoRDepthsAPBoost, + LocationName.CoRDepthsPowerCrystal, + LocationName.CoRDepthsFrostCrystal, + LocationName.CoRDepthsManifestIllusion, + LocationName.CoRDepthsAPBoost2, + LocationName.CoRMineshaftLowerLevelDepthsofRemembranceMap, + LocationName.CoRMineshaftLowerLevelAPBoost, + LocationName.CrystalFissureTornPages, + LocationName.CrystalFissureTheGreatMawMap, + LocationName.CrystalFissureEnergyCrystal, + LocationName.CrystalFissureAPBoost, + LocationName.PosternGullWing, + LocationName.HeartlessManufactoryCosmicChain, + LocationName.CoRDepthsUpperLevelRemembranceGem, + LocationName.CoRMiningAreaSerenityGem, + LocationName.CoRMiningAreaAPBoost, + LocationName.CoRMiningAreaSerenityCrystal, + LocationName.CoRMiningAreaManifestIllusion, + LocationName.CoRMiningAreaSerenityGem2, + LocationName.CoRMiningAreaDarkRemembranceMap, + LocationName.CoRMineshaftMidLevelPowerBoost, + LocationName.CoREngineChamberSerenityCrystal, + LocationName.CoREngineChamberRemembranceCrystal, + LocationName.CoREngineChamberAPBoost, + LocationName.CoREngineChamberManifestIllusion, + LocationName.CoRMineshaftUpperLevelMagicBoost, + LocationName.CoRMineshaftUpperLevelAPBoost, + LocationName.GorgeSavannahMap, + LocationName.GorgeDarkGem, + LocationName.GorgeMythrilStone, + LocationName.ElephantGraveyardFrostGem, + LocationName.ElephantGraveyardMythrilStone, + LocationName.ElephantGraveyardBrightStone, + LocationName.ElephantGraveyardAPBoost, + LocationName.ElephantGraveyardMythrilShard, + LocationName.PrideRockMap, + LocationName.PrideRockMythrilStone, + LocationName.PrideRockSerenityCrystal, + LocationName.WildebeestValleyEnergyStone, + LocationName.WildebeestValleyAPBoost, + LocationName.WildebeestValleyMythrilGem, + LocationName.WildebeestValleyMythrilStone, + LocationName.WildebeestValleyLucidGem, + LocationName.WastelandsMythrilShard, + LocationName.WastelandsSerenityGem, + LocationName.WastelandsMythrilStone, + LocationName.JungleSerenityGem, + LocationName.JungleMythrilStone, + LocationName.JungleSerenityCrystal, + LocationName.OasisMap, + LocationName.OasisTornPages, + LocationName.OasisAPBoost, + LocationName.StationofCallingPotion, + LocationName.CentralStationPotion1, + LocationName.STTCentralStationHiPotion, + LocationName.CentralStationPotion2, + LocationName.SunsetTerraceAbilityRing, + LocationName.SunsetTerraceHiPotion, + LocationName.SunsetTerracePotion1, + LocationName.SunsetTerracePotion2, + LocationName.MansionFoyerHiPotion, + LocationName.MansionFoyerPotion1, + LocationName.MansionFoyerPotion2, + LocationName.MansionDiningRoomElvenBandanna, + LocationName.MansionDiningRoomPotion, + LocationName.MansionLibraryHiPotion, + LocationName.MansionBasementCorridorHiPotion, + LocationName.OldMansionPotion, + LocationName.OldMansionMythrilShard, + LocationName.TheWoodsPotion, + LocationName.TheWoodsMythrilShard, + LocationName.TheWoodsHiPotion, + LocationName.TramCommonHiPotion, + LocationName.TramCommonAPBoost, + LocationName.TramCommonTent, + LocationName.TramCommonMythrilShard1, + LocationName.TramCommonPotion1, + LocationName.TramCommonMythrilShard2, + LocationName.TramCommonPotion2, + LocationName.CentralStationTent, + LocationName.TTCentralStationHiPotion, + LocationName.CentralStationMythrilShard, + LocationName.TheTowerPotion, + LocationName.TheTowerHiPotion, + LocationName.TheTowerEther, + LocationName.TowerEntrywayEther, + LocationName.TowerEntrywayMythrilShard, + LocationName.SorcerersLoftTowerMap, + LocationName.TowerWardrobeMythrilStone, + LocationName.UndergroundConcourseMythrilGem, + LocationName.UndergroundConcourseAPBoost, + LocationName.UndergroundConcourseMythrilCrystal, + LocationName.UndergroundConcourseOrichalcum, + LocationName.TunnelwayOrichalcum, + LocationName.TunnelwayMythrilCrystal, + LocationName.SunsetTerraceOrichalcumPlus, + LocationName.SunsetTerraceMythrilShard, + LocationName.SunsetTerraceMythrilCrystal, + LocationName.SunsetTerraceAPBoost, + LocationName.MansionFoyerMythrilCrystal, + LocationName.MansionFoyerMythrilStone, + LocationName.MansionFoyerSerenityCrystal, + LocationName.MansionDiningRoomMythrilCrystal, + LocationName.MansionDiningRoomMythrilStone, + LocationName.MansionLibraryOrichalcum, + LocationName.MansionBasementCorridorUltimateRecipe, + LocationName.FragmentCrossingMythrilStone, + LocationName.FragmentCrossingMythrilCrystal, + LocationName.FragmentCrossingAPBoost, + LocationName.FragmentCrossingOrichalcum, + LocationName.MemorysSkyscaperMythrilCrystal, + LocationName.MemorysSkyscaperAPBoost, + LocationName.MemorysSkyscaperMythrilStone, + LocationName.TheBrinkofDespairDarkCityMap, + LocationName.TheBrinkofDespairOrichalcumPlus, + LocationName.NothingsCallMythrilGem, + LocationName.NothingsCallOrichalcum, + LocationName.TwilightsViewCosmicBelt, + LocationName.NaughtsSkywayMythrilGem, + LocationName.NaughtsSkywayOrichalcum, + LocationName.NaughtsSkywayMythrilCrystal, + LocationName.RuinandCreationsPassageMythrilStone, + LocationName.RuinandCreationsPassageAPBoost, + LocationName.RuinandCreationsPassageMythrilCrystal, + LocationName.RuinandCreationsPassageOrichalcum, + LocationName.GardenofAssemblageMap, + LocationName.GoALostIllusion, + LocationName.ProofofNonexistence, + } +} + +AllWeaponSlot = { + LocationName.FAKESlot, + LocationName.DetectionSaberSlot, + LocationName.EdgeofUltimaSlot, + LocationName.KingdomKeySlot, + LocationName.OathkeeperSlot, + LocationName.OblivionSlot, + LocationName.StarSeekerSlot, + LocationName.HiddenDragonSlot, + LocationName.HerosCrestSlot, + LocationName.MonochromeSlot, + LocationName.FollowtheWindSlot, + LocationName.CircleofLifeSlot, + LocationName.PhotonDebuggerSlot, + LocationName.GullWingSlot, + LocationName.RumblingRoseSlot, + LocationName.GuardianSoulSlot, + LocationName.WishingLampSlot, + LocationName.DecisivePumpkinSlot, + LocationName.SweetMemoriesSlot, + LocationName.MysteriousAbyssSlot, + LocationName.SleepingLionSlot, + LocationName.BondofFlameSlot, + LocationName.TwoBecomeOneSlot, + LocationName.FatalCrestSlot, + LocationName.FenrirSlot, + LocationName.UltimaWeaponSlot, + LocationName.WinnersProofSlot, + LocationName.PurebloodSlot, + LocationName.Centurion2, + LocationName.CometStaff, + LocationName.HammerStaff, + LocationName.LordsBroom, + LocationName.MagesStaff, + LocationName.MeteorStaff, + LocationName.NobodyLance, + LocationName.PreciousMushroom, + LocationName.PreciousMushroom2, + LocationName.PremiumMushroom, + LocationName.RisingDragon, + LocationName.SaveTheQueen2, + LocationName.ShamansRelic, + LocationName.VictoryBell, + LocationName.WisdomWand, + + LocationName.AdamantShield, + LocationName.AkashicRecord, + LocationName.ChainGear, + LocationName.DreamCloud, + LocationName.FallingStar, + LocationName.FrozenPride2, + LocationName.GenjiShield, + LocationName.KnightDefender, + LocationName.KnightsShield, + LocationName.MajesticMushroom, + LocationName.MajesticMushroom2, + LocationName.NobodyGuard, + LocationName.OgreShield, + LocationName.SaveTheKing2, + LocationName.UltimateMushroom, } +RegionTable = { + "FirstVisits": { + RegionName.LoD_Region, + RegionName.Ag_Region, + RegionName.Dc_Region, + RegionName.Pr_Region, + RegionName.Oc_Region, + RegionName.Bc_Region, + RegionName.Sp_Region, + RegionName.Ht_Region, + RegionName.Hb_Region, + RegionName.Pl_Region, + RegionName.STT_Region, + RegionName.TT_Region, + RegionName.Twtnw_Region, + }, + "SecondVisits": { + RegionName.LoD2_Region, + RegionName.Ag2_Region, + RegionName.Tr_Region, + RegionName.Pr2_Region, + RegionName.Oc2_Region, + RegionName.Bc2_Region, + RegionName.Sp2_Region, + RegionName.Ht2_Region, + RegionName.Hb2_Region, + RegionName.Pl2_Region, + RegionName.STT_Region, + RegionName.Twtnw2_Region, + }, + "ValorRegion": { + RegionName.LoD_Region, + RegionName.Ag_Region, + RegionName.Dc_Region, + RegionName.Pr_Region, + RegionName.Oc_Region, + RegionName.Bc_Region, + RegionName.Sp_Region, + RegionName.Ht_Region, + RegionName.Hb_Region, + RegionName.TT_Region, + RegionName.Twtnw_Region, + }, + "WisdomRegion": { + RegionName.LoD_Region, + RegionName.Ag_Region, + RegionName.Dc_Region, + RegionName.Pr_Region, + RegionName.Oc_Region, + RegionName.Bc_Region, + RegionName.Sp_Region, + RegionName.Ht_Region, + RegionName.Hb_Region, + RegionName.TT_Region, + RegionName.Twtnw_Region, + }, + "LimitRegion": { + RegionName.LoD_Region, + RegionName.Ag_Region, + RegionName.Dc_Region, + RegionName.Pr_Region, + RegionName.Oc_Region, + RegionName.Bc_Region, + RegionName.Sp_Region, + RegionName.Ht_Region, + RegionName.Hb_Region, + RegionName.TT_Region, + RegionName.Twtnw_Region, + RegionName.STT_Region, + }, + "MasterRegion": { + RegionName.LoD_Region, + RegionName.Ag_Region, + RegionName.Dc_Region, + RegionName.Pr_Region, + RegionName.Oc_Region, + RegionName.Bc_Region, + RegionName.Sp_Region, + RegionName.Ht_Region, + RegionName.Hb_Region, + RegionName.TT_Region, + RegionName.Twtnw_Region, + }, # could add lod2 and bc2 as an option since those spawns are rng + "FinalRegion": { + RegionName.TT3_Region, + RegionName.Twtnw_PostRoxas, + RegionName.Twtnw2_Region, + } +} + +all_locations = { + **TWTNW_Checks, + **TWTNW2_Checks, + **TT_Checks, + **TT2_Checks, + **TT3_Checks, + **STT_Checks, + **PL_Checks, + **PL2_Checks, + **CoR_Checks, + **HB_Checks, + **HB2_Checks, + **HT_Checks, + **HT2_Checks, + **PR_Checks, + **PR2_Checks, + **PR_Checks, + **PR2_Checks, + **SP_Checks, + **SP2_Checks, + **BC_Checks, + **BC2_Checks, + **Oc_Checks, + **Oc2_Checks, + **Oc2Cups, + **HundredAcre1_Checks, + **HundredAcre2_Checks, + **HundredAcre3_Checks, + **HundredAcre4_Checks, + **HundredAcre5_Checks, + **HundredAcre6_Checks, + **DC_Checks, + **TR_Checks, + **AG_Checks, + **AG2_Checks, + **LoD_Checks, + **LoD2_Checks, + **SoraLevels, + **Form_Checks, + **GoA_Checks, + **Keyblade_Slots, + **Critical_Checks, + **Donald_Checks, + **Goofy_Checks, +} + +location_table = {} + + +def setup_locations(): + totallocation_table = {**TWTNW_Checks, **TWTNW2_Checks, **TT_Checks, **TT2_Checks, **TT3_Checks, **STT_Checks, + **PL_Checks, **PL2_Checks, **CoR_Checks, **HB_Checks, **HB2_Checks, + **PR_Checks, **PR2_Checks, **PR_Checks, **PR2_Checks, **SP_Checks, **SP2_Checks, **BC_Checks, + **BC2_Checks, **HT_Checks, **HT2_Checks, + **Oc_Checks, **Oc2_Checks, **Oc2Cups, **Critical_Checks, **Donald_Checks, **Goofy_Checks, + **HundredAcre1_Checks, **HundredAcre2_Checks, **HundredAcre3_Checks, **HundredAcre4_Checks, + **HundredAcre5_Checks, **HundredAcre6_Checks, + **DC_Checks, **TR_Checks, **AG_Checks, **AG2_Checks, **LoD_Checks, **LoD2_Checks, + **SoraLevels, + **Form_Checks, **GoA_Checks, **Keyblade_Slots} + return totallocation_table + + +lookup_id_to_Location: typing.Dict[int, str] = {data.code: item_name for item_name, data in location_table.items() if + data.code} diff --git a/worlds/kh2/Names/ItemName.py b/worlds/kh2/Names/ItemName.py new file mode 100644 index 0000000000..57cfcbe060 --- /dev/null +++ b/worlds/kh2/Names/ItemName.py @@ -0,0 +1,404 @@ +# reports +SecretAnsemsReport1 = "Secret Ansem's Report 1" +SecretAnsemsReport2 = "Secret Ansem's Report 2" +SecretAnsemsReport3 = "Secret Ansem's Report 3" +SecretAnsemsReport4 = "Secret Ansem's Report 4" +SecretAnsemsReport5 = "Secret Ansem's Report 5" +SecretAnsemsReport6 = "Secret Ansem's Report 6" +SecretAnsemsReport7 = "Secret Ansem's Report 7" +SecretAnsemsReport8 = "Secret Ansem's Report 8" +SecretAnsemsReport9 = "Secret Ansem's Report 9" +SecretAnsemsReport10 = "Secret Ansem's Report 10" +SecretAnsemsReport11 = "Secret Ansem's Report 11" +SecretAnsemsReport12 = "Secret Ansem's Report 12" +SecretAnsemsReport13 = "Secret Ansem's Report 13" + +# progression +ProofofConnection = "Proof of Connection" +ProofofNonexistence = "Proof of Nonexistence" +ProofofPeace = "Proof of Peace" +PromiseCharm = "Promise Charm" +BattlefieldsofWar = "Battlefields of War" +SwordoftheAncestor = "Sword of the Ancestor" +BeastsClaw = "Beast's Claw" +BoneFist = "Bone Fist" +ProudFang = "Proud Fang" +SkillandCrossbones = "Skill and Crossbones" +Scimitar = "Scimitar" +MembershipCard = "Membership Card" +IceCream = "Ice Cream" +WaytotheDawn = "Way to the Dawn" +IdentityDisk = "Identity Disk" +NamineSketches = "Namine Sketches" +CastleKey = "Disney Castle Key" +TornPages = "Torn Page" +TornPages = "Torn Page" +TornPages = "Torn Page" +TornPages = "Torn Page" +TornPages = "Torn Page" +ValorForm = "Valor Form" +WisdomForm = "Wisdom Form" +LimitForm = "Limit Form" +MasterForm = "Master Form" +FinalForm = "Final Form" + +# magic and summons +FireElement = "Fire Element" + +BlizzardElement = "Blizzard Element" + +ThunderElement = "Thunder Element" + +CureElement = "Cure Element" + +MagnetElement = "Magnet Element" + +ReflectElement = "Reflect Element" + +Genie = "Genie" +PeterPan = "Peter Pan" +Stitch = "Stitch" +ChickenLittle = "Chicken Little" + +#movement +HighJump = "High Jump" + + +QuickRun = "Quick Run" + + +AerialDodge = "Aerial Dodge" + + +Glide = "Glide" + + +DodgeRoll = "Dodge Roll" + + +#keyblades +Oathkeeper = "Oathkeeper" +Oblivion = "Oblivion" +StarSeeker = "Star Seeker" +HiddenDragon = "Hidden Dragon" +HerosCrest = "Hero's Crest" +Monochrome = "Monochrome" +FollowtheWind = "Follow the Wind" +CircleofLife = "Circle of Life" +PhotonDebugger = "Photon Debugger" +GullWing = "Gull Wing" +RumblingRose = "Rumbling Rose" +GuardianSoul = "Guardian Soul" +WishingLamp = "Wishing Lamp" +DecisivePumpkin = "Decisive Pumpkin" +SleepingLion = "Sleeping Lion" +SweetMemories = "Sweet Memories" +MysteriousAbyss = "Mysterious Abyss" +TwoBecomeOne = "Two Become One" +FatalCrest = "Fatal Crest" +BondofFlame = "Bond of Flame" +Fenrir = "Fenrir" +UltimaWeapon = "Ultima Weapon" +WinnersProof = "Winner's Proof" +Pureblood = "Pureblood" + +# stafs +HammerStaff = "Hammer Staff" +LordsBroom = "Lord's Broom" +MagesStaff = "Mages Staff" +MeteorStaff = "Meteor Staff" +CometStaff = "Comet Staff" +Centurion2 = "Centurion+" +MeteorStaff = "Meteor Staff" +NobodyLance = "Nobody Lance" +PreciousMushroom = "Precious Mushroom" +PreciousMushroom2 = "Precious Mushroom+" +PremiumMushroom = "Premium Mushroom" +RisingDragon = "Rising Dragon" +SaveTheQueen2 = "Save The Queen+" +ShamansRelic = "Shaman's Relic" +VictoryBell = "Victory Bell" +WisdomWand = "Wisdom Wand" +# shelds + +AkashicRecord = "Akashic Record" +FrozenPride2 = "Frozen Pride+" +GenjiShield = "Genji Shield" +MajesticMushroom = "Majestic Mushroom" +MajesticMushroom2 = "Majestic Mushroom+" +NobodyGuard = "Nobody Guard" +OgreShield = "Ogre Shield" +SaveTheKing2 = "Save The King+" +UltimateMushroom = "Ultimate Mushroom" + +# accesrosies +AbilityRing = "Ability Ring" +EngineersRing = "Engineer's Ring" +TechniciansRing = "Technician's Ring" +SkillRing = "Skill Ring" +SkillfulRing = "Skillful Ring" +ExpertsRing = "Expert's Ring" +MastersRing = "Master's Ring" +CosmicRing = "Cosmic Ring" +ExecutivesRing = "Executive's Ring" +SardonyxRing = "Sardonyx Ring" +TourmalineRing = "Tourmaline Ring" +AquamarineRing = "Aquamarine Ring" +GarnetRing = "Garnet Ring" +DiamondRing = "Diamond Ring" +SilverRing = "Silver Ring" +GoldRing = "Gold Ring" +PlatinumRing = "Platinum Ring" +MythrilRing = "Mythril Ring" +OrichalcumRing = "Orichalcum Ring" +SoldierEarring = "Soldier Earring" +FencerEarring = "Fencer Earring" +MageEarring = "Mage Earring" +SlayerEarring = "Slayer Earring" +Medal = "Medal" +MoonAmulet = "Moon Amulet" +StarCharm = "Star Charm" +CosmicArts = "Cosmic Arts" +ShadowArchive = "Shadow Archive" +ShadowArchive2 = "Shadow Archive+" +FullBloom = "Full Bloom" +FullBloom2 = "Full Bloom+" +DrawRing = "Draw Ring" +LuckyRing = "Lucky Ring" + +# armorers +ElvenBandana = "Elven Bandana" +DivineBandana = "Divine Bandana" +ProtectBelt = "Protect Belt" +GaiaBelt = "Gaia Belt" +PowerBand = "Power Band" +BusterBand = "Buster Band" +CosmicBelt = "Cosmic Belt" +FireBangle = "Fire Bangle" +FiraBangle = "Fira Bangle" +FiragaBangle = "Firaga Bangle" +FiragunBangle = "Firagun Bangle" +BlizzardArmlet = "Blizzard Armlet" +BlizzaraArmlet = "Blizzara Armlet" +BlizzagaArmlet = "Blizzaga Armlet" +BlizzagunArmlet = "Blizzagun Armlet" +ThunderTrinket = "Thunder Trinket" +ThundaraTrinket = "Thundara Trinket" +ThundagaTrinket = "Thundaga Trinket" +ThundagunTrinket = "Thundagun Trinket" +ShockCharm = "Shock Charm" +ShockCharm2 = "Shock Charm+" +ShadowAnklet = "Shadow Anklet" +DarkAnklet = "Dark Anklet" +MidnightAnklet = "Midnight Anklet" +ChaosAnklet = "Chaos Anklet" +ChampionBelt = "Champion Belt" +AbasChain = "Abas Chain" +AegisChain = "Aegis Chain" +Acrisius = "Acrisius" +Acrisius2 = "Acrisius+" +CosmicChain = "Cosmic Chain" +PetiteRibbon = "Petite Ribbon" +Ribbon = "Ribbon" +GrandRibbon = "Grand Ribbon" + +# usefull and stat incre +MickyMunnyPouch = "Mickey Munny Pouch" +OletteMunnyPouch = "Olette Munny Pouch" +HadesCupTrophy = "Hades Cup Trophy" +UnknownDisk = "Unknown Disk" +OlympusStone = "Olympus Stone" +MaxHPUp = "Max HP Up" +MaxMPUp = "Max MP Up" +DriveGaugeUp = "Drive Gauge Up" +ArmorSlotUp = "Armor Slot Up" +AccessorySlotUp = "Accessory Slot Up" +ItemSlotUp = "Item Slot Up" + +# support abilitys +Scan = "Scan" +AerialRecovery = "Aerial Recovery" +ComboMaster = "Combo Master" +ComboPlus = "Combo Plus" +AirComboPlus = "Air Combo Plus" +ComboBoost = "Combo Boost" +AirComboBoost = "Air Combo Boost" +ReactionBoost = "Reaction Boost" +FinishingPlus = "Finishing Plus" +NegativeCombo = "Negative Combo" +BerserkCharge = "Berserk Charge" +DamageDrive = "Damage Drive" +DriveBoost = "Drive Boost" +FormBoost = "Form Boost" +SummonBoost = "Summon Boost" +ExperienceBoost = "Experience Boost" +Draw = "Draw" +Jackpot = "Jackpot" +LuckyLucky = "Lucky Lucky" +DriveConverter = "Drive Converter" +FireBoost = "Fire Boost" +BlizzardBoost = "Blizzard Boost" +ThunderBoost = "Thunder Boost" +ItemBoost = "Item Boost" +MPRage = "MP Rage" +MPHaste = "MP Haste" +MPHastera = "MP Hastera" +MPHastega = "MP Hastega" +Defender = "Defender" +DamageControl = "Damage Control" +NoExperience = "No Experience" +LightDarkness = "Light & Darkness" + +# level ability +MagicLock = "Magic Lock-On" +LeafBracer = "Leaf Bracer" +CombinationBoost = "Combination Boost" +DamageDrive = "Damage Drive" +OnceMore = "Once More" +SecondChance = "Second Chance" + +# action abilitys +Guard = "Guard" +UpperSlash = "Upper Slash" +HorizontalSlash = "Horizontal Slash" +FinishingLeap = "Finishing Leap" +RetaliatingSlash = "Retaliating Slash" +Slapshot = "Slapshot" +DodgeSlash = "Dodge Slash" +FlashStep = "Flash Step" +SlideDash = "Slide Dash" +VicinityBreak = "Vicinity Break" +GuardBreak = "Guard Break" +Explosion = "Explosion" +AerialSweep = "Aerial Sweep" +AerialDive = "Aerial Dive" +AerialSpiral = "Aerial Spiral" +AerialFinish = "Aerial Finish" +MagnetBurst = "Magnet Burst" +Counterguard = "Counterguard" +AutoValor = "Auto Valor" +AutoWisdom = "Auto Wisdom" +AutoLimit = "Auto Limit" +AutoMaster = "Auto Master" +AutoFinal = "Auto Final" +AutoSummon = "Auto Summon" +TrinityLimit = "Trinity Limit" + +# items +Potion = "Potion" +HiPotion = "Hi-Potion" +Ether = "Ether" +Elixir = "Elixir" +MegaPotion = "Mega-Potion" +MegaEther = "Mega-Ether" +Megalixir = "Megalixir" +Tent = "Tent" +DriveRecovery = "Drive Recovery" +HighDriveRecovery = "High Drive Recovery" +PowerBoost = "Power Boost" +MagicBoost = "Magic Boost" +DefenseBoost = "Defense Boost" +APBoost = "AP Boost" + +# donald abilitys +DonaldFire = "Donald Fire" +DonaldBlizzard = "Donald Blizzard" +DonaldThunder = "Donald Thunder" +DonaldCure = "Donald Cure" +Fantasia = "Donald Fantasia" +FlareForce = "Donald Flare Force" +DonaldMPRage = "Donald MP Rage" +DonaldJackpot = "Donald Jackpot" +DonaldLuckyLucky = "Donald Lucky Lucky" +DonaldFireBoost = "Donald Fire Boost" +DonaldBlizzardBoost = "Donald Blizzard Boost" +DonaldThunderBoost = "Donald Thunder Boost" +DonaldFireBoost = "Donald Fire Boost" +DonaldBlizzardBoost = "Donald Blizzard Boost" +DonaldThunderBoost = "Donald Thunder Boost" +DonaldMPRage = "Donald MP Rage" +DonaldMPHastera = "Donald MP Hastera" +DonaldAutoLimit = "Donald Auto Limit" +DonaldHyperHealing = "Donald Hyper Healing" +DonaldAutoHealing = "Donald Auto Healing" +DonaldMPHastega = "Donald MP Hastega" +DonaldItemBoost = "Donald Item Boost" +DonaldDamageControl = "Donald Damage Control" +DonaldHyperHealing = "Donald Hyper Healing" +DonaldMPRage = "Donald MP Rage" +DonaldMPHaste = "Donald MP Haste" +DonaldMPHastera = "Donald MP Hastera" +DonaldMPHastega = "Donald MP Hastega" +DonaldMPHaste = "Donald MP Haste" +DonaldDamageControl = "Donald Damage Control" +DonaldMPHastera = "Donald MP Hastera" +DonaldDraw = "Donald Draw" + +# goofy abili +GoofyTornado = "Goofy Tornado" +GoofyTurbo = "Goofy Turbo" +GoofyBash = "Goofy Bash" +TornadoFusion = "Tornado Fusion" +Teamwork = "Teamwork" +GoofyDraw = "Goofy Draw" +GoofyJackpot = "Goofy Jackpot" +GoofyLuckyLucky = "Goofy Lucky Lucky" +GoofyItemBoost = "Goofy Item Boost" +GoofyMPRage = "Goofy MP Rage" +GoofyDefender = "Goofy Defender" +GoofyDamageControl = "Goofy Damage Control" +GoofyAutoLimit = "Goofy Auto Limit" +GoofySecondChance = "Goofy Second Chance" +GoofyOnceMore = "Goofy Once More" +GoofyAutoChange = "Goofy Auto Change" +GoofyHyperHealing = "Goofy Hyper Healing" +GoofyAutoHealing = "Goofy Auto Healing" +GoofyDefender = "Goofy Defender" +GoofyHyperHealing = "Goofy Hyper Healing" +GoofyMPHaste = "Goofy MP Haste" +GoofyMPHastera = "Goofy MP Hastera" +GoofyMPRage = "Goofy MP Rage" +GoofyMPHastega = "Goofy MP Hastega" +GoofyItemBoost = "Goofy Item Boost" +GoofyDamageControl = "Goofy Damage Control" +GoofyProtect = "Goofy Protect" +GoofyProtera = "Goofy Protera" +GoofyProtega = "Goofy Protega" +GoofyDamageControl = "Goofy Damage Control" +GoofyProtect = "Goofy Protect" +GoofyProtera = "Goofy Protera" +GoofyProtega = "Goofy Protega" + +Victory = "Victory" +LuckyEmblem = "Lucky Emblem" +Bounty="Bounty" + +UniversalKey="Universal Key" +# Keyblade Slots +FAKESlot = "FAKE (Slot)" +DetectionSaberSlot = "Detection Saber (Slot)" +EdgeofUltimaSlot = "Edge of Ultima (Slot)" +KingdomKeySlot = "Kingdom Key (Slot)" +OathkeeperSlot = "Oathkeeper (Slot)" +OblivionSlot = "Oblivion (Slot)" +StarSeekerSlot = "Star Seeker (Slot)" +HiddenDragonSlot = "Hidden Dragon (Slot)" +HerosCrestSlot = "Hero's Crest (Slot)" +MonochromeSlot = "Monochrome (Slot)" +FollowtheWindSlot = "Follow the Wind (Slot)" +CircleofLifeSlot = "Circle of Life (Slot)" +PhotonDebuggerSlot = "Photon Debugger (Slot)" +GullWingSlot = "Gull Wing (Slot)" +RumblingRoseSlot = "Rumbling Rose (Slot)" +GuardianSoulSlot = "Guardian Soul (Slot)" +WishingLampSlot = "Wishing Lamp (Slot)" +DecisivePumpkinSlot = "Decisive Pumpkin (Slot)" +SweetMemoriesSlot = "Sweet Memories (Slot)" +MysteriousAbyssSlot = "Mysterious Abyss (Slot)" +SleepingLionSlot = "Sleeping Lion (Slot)" +BondofFlameSlot = "Bond of Flame (Slot)" +TwoBecomeOneSlot = "Two Become One (Slot)" +FatalCrestSlot = "Fatal Crest (Slot)" +FenrirSlot = "Fenrir (Slot)" +UltimaWeaponSlot = "Ultima Weapon (Slot)" +WinnersProofSlot = "Winner's Proof (Slot)" diff --git a/worlds/kh2/Names/LocationName.py b/worlds/kh2/Names/LocationName.py new file mode 100644 index 0000000000..1a6c4d07fb --- /dev/null +++ b/worlds/kh2/Names/LocationName.py @@ -0,0 +1,763 @@ +BambooGroveDarkShard = "(LoD) Bamboo Grove Dark Shard" +BambooGroveEther = "(LoD) Bamboo Grove Ether" +BambooGroveMythrilShard = "(LoD) Bamboo Grove Mythril Shard" +EncampmentAreaMap = "(LoD) Bamboo Grove Encampment Area Map" +Mission3 = "(LoD) Mission 3" +CheckpointHiPotion = "(LoD) Checkpoint Hi-Potion" +CheckpointMythrilShard = "(LoD) Checkpoint Mythril Shard" +MountainTrailLightningShard = "(LoD) Mountain Trail Lightning Shard" +MountainTrailRecoveryRecipe = "(LoD) Mountain Trail Recovery Recipe" +MountainTrailEther = "(LoD) Mountain Trail Ether" +MountainTrailMythrilShard = "(LoD) Mountain Trail Mythril Shard" +VillageCaveAreaMap = "(LoD) Village Cave Area Map" +VillageCaveAPBoost = "(LoD) Village Cave AP Boost" +VillageCaveDarkShard = "(LoD) Village Cave Dark Shard" +VillageCaveBonus = "(LoD) Village Cave Bonus: Sora Slot 1" +RidgeFrostShard = "(LoD) Ridge Frost Shard" +RidgeAPBoost = "(LoD) Ridge AP Boost" +ShanYu = "(LoD) Shan-Yu Bonus: Sora Slot 1" +ShanYuGetBonus = "(LoD) Shan-Yu Bonus: Sora Slot 2" +HiddenDragon = "(LoD) Hidden Dragon" +ThroneRoomTornPages = "(LoD2) Throne Room Torn Pages" +ThroneRoomPalaceMap = "(LoD2) Throne Room Palace Map" +ThroneRoomAPBoost = "(LoD2) Throne Room AP Boost" +ThroneRoomQueenRecipe = "(LoD2) Throne Room Queen Recipe" +ThroneRoomAPBoost2 = "(LoD2) Throne Room AP Boost 2" +ThroneRoomOgreShield = "(LoD2) Throne Room Ogre Shield" +ThroneRoomMythrilCrystal = "(LoD2) Throne Room Mythril Crystal" +ThroneRoomOrichalcum = "(LoD2) Throne Room Orichalcum" +StormRider = "(LoD2) Storm Rider Bonus: Sora Slot 1" +XigbarDataDefenseBoost = "Data Xigbar" + +AgrabahMap = "(AG) Agrabah Map" +AgrabahDarkShard = "(AG) Agrabah Dark Shard" +AgrabahMythrilShard = "(AG) Agrabah Mythril Shard" +AgrabahHiPotion = "(AG) Agrabah Hi-Potion" +AgrabahAPBoost = "(AG) Agrabah AP Boost" +AgrabahMythrilStone = "(AG) Agrabah Mythril Stone" +AgrabahMythrilShard2 = "(AG) Agrabah Mythril Shard 2" +AgrabahSerenityShard = "(AG) Agrabah Serenity Shard" +BazaarMythrilGem = "(AG) Bazaar Mythril Gem" +BazaarPowerShard = "(AG) Bazaar Power Shard" +BazaarHiPotion = "(AG) Bazaar Hi-Potion" +BazaarAPBoost = "(AG) Bazaar AP Boost" +BazaarMythrilShard = "(AG) Bazaar Mythril Shard" +PalaceWallsSkillRing = "(AG) Palace Walls Skill Ring" +PalaceWallsMythrilStone = "(AG) Palace Walls Mythril Stone" +CaveEntrancePowerStone = "(AG) Cave Entrance Power Stone" +CaveEntranceMythrilShard = "(AG) Cave Entrance Mythril Shard" +ValleyofStoneMythrilStone = "(AG) Valley of Stone Mythril Stone" +ValleyofStoneAPBoost = "(AG) Valley of Stone AP Boost" +ValleyofStoneMythrilShard = "(AG) Valley of Stone Mythril Shard" +ValleyofStoneHiPotion = "(AG) Valley of Stone Hi-Potion" +AbuEscort = "(AG) Abu Escort Bonus: Sora Slot 1" +ChasmofChallengesCaveofWondersMap = "(AG) Chasm of Challenges Cave of Wonders Map" +ChasmofChallengesAPBoost = "(AG) Chasm of Challenges AP Boost" +TreasureRoom = "(AG) Treasure Room" +TreasureRoomAPBoost = "(AG) Treasure Room AP Boost" +TreasureRoomSerenityGem = "(AG) Treasure Room Serenity Gem" +ElementalLords = "(AG) Elemental Lords Bonus: Sora Slot 1" +LampCharm = "(AG) Lamp Charm" +RuinedChamberTornPages = "(AG2) Ruined Chamber Torn Pages" +RuinedChamberRuinsMap = "(AG2) Ruined Chamber Ruins Map" +GenieJafar = "(AG2) Genie Jafar" +WishingLamp = "(AG2) Wishing Lamp" +LexaeusBonus = "Lexaeus Bonus: Sora Slot 1" +LexaeusASStrengthBeyondStrength = "AS Lexaeus" +LexaeusDataLostIllusion = "Data Lexaeus" +DCCourtyardMythrilShard = "(DC) Courtyard Mythril Shard" +DCCourtyardStarRecipe = "(DC) Courtyard Star Recipe" +DCCourtyardAPBoost = "(DC) Courtyard AP Boost" +DCCourtyardMythrilStone = "(DC) Courtyard Mythril Stone" +DCCourtyardBlazingStone = "(DC) Courtyard Blazing Stone" +DCCourtyardBlazingShard = "(DC) Courtyard Blazing Shard" +DCCourtyardMythrilShard2 = "(DC) Courtyard Mythril Shard 2" +LibraryTornPages = "(DC) Library Torn Pages" +DisneyCastleMap = "(DC) Disney Castle Map" +MinnieEscort = "(DC) Minnie Escort Bonus: Sora Slot 1" +MinnieEscortGetBonus = "(DC) Minnie Escort Bonus: Sora Slot 2" +CornerstoneHillMap = "(TR) Cornerstone Hill Map" +CornerstoneHillFrostShard = "(TR) Cornerstone Hill Frost Shard" +PierMythrilShard = "(TR) Pier Mythril Shard" +PierHiPotion = "(TR) Pier Hi-Potion" +WaterwayMythrilStone = "(TR) Waterway Mythril Stone" +WaterwayAPBoost = "(TR) Waterway AP Boost" +WaterwayFrostStone = "(TR) Waterway Frost Stone" +WindowofTimeMap = "(TR) Window of Time Map" +BoatPete = "(TR) Boat Pete Bonus: Sora Slot 1" +FuturePete = "(TR) Future Pete Bonus: Sora Slot 1" +FuturePeteGetBonus = "(TR) Future Pete Bonus: Sora Slot 2" +Monochrome = "(TR) Monochrome" +WisdomForm = "(TR) Wisdom Form" +MarluxiaGetBonus = "Marluxia Bonus: Sora Slot 1" +MarluxiaASEternalBlossom = "AS Marluxia" +MarluxiaDataLostIllusion = "Data Marluxia" +LingeringWillBonus = "Lingering Will Bonus: Sora Slot 1" +LingeringWillProofofConnection = "Lingering Will Proof of Connection" +LingeringWillManifestIllusion = "Lingering Will Manifest Illusion" +PoohsHouse100AcreWoodMap = "(100Acre) Pooh's House 100 Acre Wood Map" +PoohsHouseAPBoost = "(100Acre) Pooh's House AP Boost" +PoohsHouseMythrilStone = "(100Acre) Pooh's House Mythril Stone" +PigletsHouseDefenseBoost = "(100Acre) Piglet's House Defense Boost" +PigletsHouseAPBoost = "(100Acre) Piglet's House AP Boost" +PigletsHouseMythrilGem = "(100Acre) Piglet's House Mythril Gem" +RabbitsHouseDrawRing = "(100Acre) Rabbit's House Draw Ring" +RabbitsHouseMythrilCrystal = "(100Acre) Rabbit's House Mythril Crystal" +RabbitsHouseAPBoost = "(100Acre) Rabbit's House AP Boost" +KangasHouseMagicBoost = "(100Acre) Kanga's House Magic Boost" +KangasHouseAPBoost = "(100Acre) Kanga's House AP Boost" +KangasHouseOrichalcum = "(100Acre) Kanga's House Orichalcum" +SpookyCaveMythrilGem = "(100Acre) Spooky Cave Mythril Gem" +SpookyCaveAPBoost = "(100Acre) Spooky Cave AP Boost" +SpookyCaveOrichalcum = "(100Acre) Spooky Cave Orichalcum" +SpookyCaveGuardRecipe = "(100Acre) Spooky Cave Guard Recipe" +SpookyCaveMythrilCrystal = "(100Acre) Spooky Cave Mythril Crystal" +SpookyCaveAPBoost2 = "(100Acre) Spooky Cave AP Boost 2" +SweetMemories = "(100Acre) Sweet Memories" +SpookyCaveMap = "(100Acre) Spooky Cave Map" +StarryHillCosmicRing = "(100Acre) Starry Hill Cosmic Ring" +StarryHillStyleRecipe = "(100Acre) Starry Hill Style Recipe" +StarryHillCureElement = "(100Acre) Starry Hill Cure Element" +StarryHillOrichalcumPlus = "(100Acre) Starry Hill Orichalcum+" +PassageMythrilShard = "(OC) Passage Mythril Shard" +PassageMythrilStone = "(OC) Passage Mythril Stone" +PassageEther = "(OC) Passage Ether" +PassageAPBoost = "(OC) Passage AP Boost" +PassageHiPotion = "(OC) Passage Hi-Potion" +InnerChamberUnderworldMap = "(OC) Inner Chamber Underworld Map" +InnerChamberMythrilShard = "(OC) Inner Chamber Mythril Shard" +Cerberus = "(OC) Cerberus Bonus: Sora Slot 1" +ColiseumMap = "(OC) Coliseum Map" +Urns = "(OC) Urns Bonus: Sora Slot 1" +UnderworldEntrancePowerBoost = "(OC) Underworld Entrance Power Boost" +CavernsEntranceLucidShard = "(OC) Caverns Entrance Lucid Shard" +CavernsEntranceAPBoost = "(OC) Caverns Entrance AP Boost" +CavernsEntranceMythrilShard = "(OC) Caverns Entrance Mythril Shard" +TheLostRoadBrightShard = "(OC) The Lost Road Bright Shard" +TheLostRoadEther = "(OC) The Lost Road Ether" +TheLostRoadMythrilShard = "(OC) The Lost Road Mythril Shard" +TheLostRoadMythrilStone = "(OC) The Lost Road Mythril Stone" +AtriumLucidStone = "(OC) Atrium Lucid Stone" +AtriumAPBoost = "(OC) Atrium AP Boost" +DemyxOC = "(OC) Demyx Bonus: Sora Slot 1" +SecretAnsemReport5 = "(OC) Secret Ansem Report 5 (Demyx)" +OlympusStone = "(OC) Olympus Stone" +TheLockCavernsMap = "(OC) The Lock Caverns Map" +TheLockMythrilShard = "(OC) The Lock Mythril Shard" +TheLockAPBoost = "(OC) The Lock AP Boost" +PeteOC = "(OC) Pete OC Bonus: Sora Slot 1" +Hydra = "(OC) Hydra Bonus: Sora Slot 1" +HydraGetBonus = "(OC) Hydra Bonus: Sora Slot 2" +HerosCrest = "(OC) Hero's Crest" +AuronsStatue = "(OC2) Auron's Statue" +Hades = "(OC2) Hades Bonus: Sora Slot 1" +HadesGetBonus = "(OC2) Hades Bonus: Sora Slot 2" +GuardianSoul = "(OC2) Guardian Soul" +ProtectBeltPainandPanicCup = "Protect Belt Pain and Panic Cup" +SerenityGemPainandPanicCup = "Serenity Gem Pain and Panic Cup" +RisingDragonCerberusCup = "Rising Dragon Cerberus Cup" +SerenityCrystalCerberusCup = "Serenity Crystal Cerberus Cup" +GenjiShieldTitanCup = "Genji Shield Titan Cup" +SkillfulRingTitanCup = "Skillful Ring Titan Cup" +FatalCrestGoddessofFateCup = "Fatal Crest Goddess of Fate Cup" +OrichalcumPlusGoddessofFateCup = "Orichalcum+ Goddess of Fate Cup" +HadesCupTrophyParadoxCups = "Hades Cup Trophy Paradox Cups" +ZexionBonus = "Zexion Bonus: Sora Slot 1" +ZexionASBookofShadows = "AS Zexion" +ZexionDataLostIllusion = "Data Zexion" + + +BCCourtyardAPBoost = "(BC) Courtyard AP Boost" +BCCourtyardHiPotion = "(BC) Courtyard Hi-Potion" +BCCourtyardMythrilShard = "(BC) Courtyard Mythril Shard" +BellesRoomCastleMap = "(BC) Belle's Room Castle Map" +BellesRoomMegaRecipe = "(BC) Belle's Room Mega-Recipe" +TheEastWingMythrilShard = "(BC) The East Wing Mythril Shard" +TheEastWingTent = "(BC) The East Wing Tent" +TheWestHallHiPotion = "(BC) The West Hall Hi-Potion" +TheWestHallPowerShard = "(BC) The West Hall Power Shard" +TheWestHallMythrilShard2 = "(BC) The West Hall Mythril Shard 2" +TheWestHallBrightStone = "(BC) The West Hall Bright Stone" +TheWestHallMythrilShard = "(BC) The West Hall Mythril Shard" +Thresholder = "(BC) Thresholder Bonus: Sora Slot 1" +DungeonBasementMap = "(BC) Dungeon Basement Map" +DungeonAPBoost = "(BC) Dungeon AP Boost" +SecretPassageMythrilShard = "(BC) Secret Passage Mythril Shard" +SecretPassageHiPotion = "(BC) Secret Passage Hi-Potion" +SecretPassageLucidShard = "(BC) Secret Passage Lucid Shard" +TheWestHallAPBoostPostDungeon = "(BC) The West Hall AP Boost Post Dungeon" +TheWestWingMythrilShard = "(BC) The West Wing Mythril Shard" +TheWestWingTent = "(BC) The West Wing Tent" +Beast = "(BC) Beast Bonus: Sora Slot 1" +TheBeastsRoomBlazingShard = "(BC) The Beast's Room Blazing Shard" +DarkThorn = "(BC) Dark Thorn Bonus: Sora Slot 1" +DarkThornGetBonus = "(BC) Dark Thorn Bonus: Sora Slot 2" +DarkThornCureElement = "(BC) Dark Thorn Cure Element" +RumblingRose = "(BC2) Rumbling Rose" +CastleWallsMap = "(BC2) Castle Walls Map" +Xaldin = "(BC2) Xaldin Bonus: Sora Slot 1" +XaldinGetBonus = "(BC2) Xaldin Bonus: Sora Slot 2" +SecretAnsemReport4 = "(BC2) Secret Ansem Report 4 (Xaldin)" +XaldinDataDefenseBoost = "Data Xaldin" + + + +PitCellAreaMap = "(SP) Pit Cell Area Map" +PitCellMythrilCrystal = "(SP) Pit Cell Mythril Crystal" +CanyonDarkCrystal = "(SP) Canyon Dark Crystal" +CanyonMythrilStone = "(SP) Canyon Mythril Stone" +CanyonMythrilGem = "(SP) Canyon Mythril Gem" +CanyonFrostCrystal = "(SP) Canyon Frost Crystal" +Screens = "(SP) Screens Bonus: Sora Slot 1" +HallwayPowerCrystal = "(SP) Hallway Power Crystal" +HallwayAPBoost = "(SP) Hallway AP Boost" +CommunicationsRoomIOTowerMap = "(SP) Communications Room I/O Tower Map" +CommunicationsRoomGaiaBelt = "(SP) Communications Room Gaia Belt" +HostileProgram = "(SP) Hostile Program Bonus: Sora Slot 1" +HostileProgramGetBonus = "(SP) Hostile Program Bonus: Sora Slot 2" +PhotonDebugger = "(SP) Photon Debugger" +SolarSailer = "(SP2) Solar Sailer" +CentralComputerCoreAPBoost = "(SP2) Central Computer Core AP Boost" +CentralComputerCoreOrichalcumPlus = "(SP2) Central Computer Core Orichalcum+" +CentralComputerCoreCosmicArts = "(SP2) Central Computer Core Cosmic Arts" +CentralComputerCoreMap = "(SP2) Central Computer Core Map" +MCP = "(SP2) MCP Bonus: Sora Slot 1" +MCPGetBonus = "(SP2) MCP Bonus: Sora Slot 2" +LarxeneBonus = "Larxene Bonus: Sora Slot 1" +LarxeneASCloakedThunder = "AS Larxene" +LarxeneDataLostIllusion = "Data Larxene" + +GraveyardMythrilShard = "(HT) Graveyard Mythril Shard" +GraveyardSerenityGem = "(HT) Graveyard Serenity Gem" +FinklesteinsLabHalloweenTownMap = "(HT) Finklestein's Lab Halloween Town Map" +TownSquareMythrilStone = "(HT) Town Square Mythril Stone" +TownSquareEnergyShard = "(HT) Town Square Energy Shard" +HinterlandsLightningShard = "(HT) Hinterlands Lightning Shard" +HinterlandsMythrilStone = "(HT) Hinterlands Mythril Stone" +HinterlandsAPBoost = "(HT) Hinterlands AP Boost" +CandyCaneLaneMegaPotion = "(HT) Candy Cane Lane Mega-Potion" +CandyCaneLaneMythrilGem = "(HT) Candy Cane Lane Mythril Gem" +CandyCaneLaneLightningStone = "(HT) Candy Cane Lane Lightning Stone" +CandyCaneLaneMythrilStone = "(HT) Candy Cane Lane Mythril Stone" +SantasHouseChristmasTownMap = "(HT) Santa's House Christmas Town Map" +SantasHouseAPBoost = "(HT) Santa's House AP Boost" +PrisonKeeper = "(HT) Prison Keeper Bonus: Sora Slot 1" +OogieBoogie = "(HT) Oogie Boogie Bonus: Sora Slot 1" +OogieBoogieMagnetElement = "(HT) Oogie Boogie Magnet Element" +Lock = "(HT2) Lock, Shock and Barrel Bonus: Sora Slot 1" +Present = "(HT2) Present" +DecoyPresents = "(HT2) Decoy Presents" +Experiment = "(HT2) Experiment Bonus: Sora Slot 1" +DecisivePumpkin = "(HT2) Decisive Pumpkin" +VexenBonus = "Vexen Bonus: Sora Slot 1" +VexenASRoadtoDiscovery = "AS Vexen" +VexenDataLostIllusion = "Data Vexen" + +RampartNavalMap = "(PR) Rampart Naval Map" +RampartMythrilStone = "(PR) Rampart Mythril Stone" +RampartDarkShard = "(PR) Rampart Dark Shard" +TownDarkStone = "(PR) Town Dark Stone" +TownAPBoost = "(PR) Town AP Boost" +TownMythrilShard = "(PR) Town Mythril Shard" +TownMythrilGem = "(PR) Town Mythril Gem" +CaveMouthBrightShard = "(PR) Cave Mouth Bright Shard" +CaveMouthMythrilShard = "(PR) Cave Mouth Mythril Shard" +IsladeMuertaMap = "(PR) Isla de Muerta Map" +BoatFight = "(PR) Boat Fight Bonus: Sora Slot 1" +InterceptorBarrels = "(PR) Interceptor Barrels Bonus: Sora Slot 1" +PowderStoreAPBoost1 = "(PR) Powder Store AP Boost 1" +PowderStoreAPBoost2 = "(PR) Powder Store AP Boost 2" +MoonlightNookMythrilShard = "(PR) Moonlight Nook Mythril Shard" +MoonlightNookSerenityGem = "(PR) Moonlight Nook Serenity Gem" +MoonlightNookPowerStone = "(PR) Moonlight Nook Power Stone" +Barbossa = "(PR) Barbossa Bonus: Sora Slot 1" +BarbossaGetBonus = "(PR) Barbossa Bonus: Sora Slot 2" +FollowtheWind = "(PR) Follow the Wind" +GrimReaper1 = "(PR2) Grim Reaper 1 Bonus: Sora Slot 1" +InterceptorsHoldFeatherCharm = "(PR2) Interceptor's Hold Feather Charm" +SeadriftKeepAPBoost = "(PR2) Seadrift Keep AP Boost" +SeadriftKeepOrichalcum = "(PR2) Seadrift Keep Orichalcum" +SeadriftKeepMeteorStaff = "(PR2) Seadrift Keep Meteor Staff" +SeadriftRowSerenityGem = "(PR2) Seadrift Row Serenity Gem" +SeadriftRowKingRecipe = "(PR2) Seadrift Row King Recipe" +SeadriftRowMythrilCrystal = "(PR2) Seadrift Row Mythril Crystal" +SeadriftRowCursedMedallion = "(PR2) Seadrift Row Cursed Medallion" +SeadriftRowShipGraveyardMap = "(PR2) Seadrift Row Ship Graveyard Map" +GrimReaper2 = "(PR2) Grim Reaper 2 Bonus: Sora Slot 1" +SecretAnsemReport6 = "(PR2) Secret Ansem Report 6 (Grim Reaper 2)" + +LuxordDataAPBoost = "Data Luxord" + +MarketplaceMap = "(HB) Marketplace Map" +BoroughDriveRecovery = "(HB) Borough Drive Recovery" +BoroughAPBoost = "(HB) Borough AP Boost" +BoroughHiPotion = "(HB) Borough Hi-Potion" +BoroughMythrilShard = "(HB) Borough Mythril Shard" +BoroughDarkShard = "(HB) Borough Dark Shard" +MerlinsHouseMembershipCard = "(HB) Merlin's House Membership Card" +MerlinsHouseBlizzardElement = "(HB) Merlin's House Blizzard Element" +Bailey = "(HB) Bailey Bonus: Sora Slot 1" +BaileySecretAnsemReport7 = "(HB) Bailey Secret Ansem Report 7" +BaseballCharm = "(HB) Baseball Charm" +PosternCastlePerimeterMap = "(HB2) Postern Castle Perimeter Map" +PosternMythrilGem = "(HB2) Postern Mythril Gem" +PosternAPBoost = "(HB2) Postern AP Boost" +CorridorsMythrilStone = "(HB2) Corridors Mythril Stone" +CorridorsMythrilCrystal = "(HB2) Corridors Mythril Crystal" +CorridorsDarkCrystal = "(HB2) Corridors Dark Crystal" +CorridorsAPBoost = "(HB2) Corridors AP Boost" +AnsemsStudyMasterForm = "(HB2) Ansem's Study Master Form" +AnsemsStudySleepingLion = "(HB2) Ansem's Study Sleeping Lion" +AnsemsStudySkillRecipe = "(HB2) Ansem's Study Skill Recipe" +AnsemsStudyUkuleleCharm = "(HB2) Ansem's Study Ukulele Charm" +RestorationSiteMoonRecipe = "(HB2) Restoration Site Moon Recipe" +RestorationSiteAPBoost = "(HB2) Restoration Site AP Boost" +DemyxHB = "(HB2) Demyx Bonus: Sora Slot 1" +DemyxHBGetBonus = "(HB2) Demyx Bonus: Sora Slot 2" +FFFightsCureElement = "(HB2) FF Fights Cure Element" +CrystalFissureTornPages = "(HB2) Crystal Fissure Torn Pages" +CrystalFissureTheGreatMawMap = "(HB2) Crystal Fissure The Great Maw Map" +CrystalFissureEnergyCrystal = "(HB2) Crystal Fissure Energy Crystal" +CrystalFissureAPBoost = "(HB2) Crystal Fissure AP Boost" +ThousandHeartless = "(HB2) 1000 Heartless" +ThousandHeartlessSecretAnsemReport1 = "(HB2) 1000 Heartless Secret Ansem Report 1" +ThousandHeartlessIceCream = "(HB2) 1000 Heartless Ice Cream" +ThousandHeartlessPicture = "(HB2) 1000 Heartless Picture" +PosternGullWing = "(HB2) Postern Gull Wing" +HeartlessManufactoryCosmicChain = "(HB2) Heartless Manufactory Cosmic Chain" +SephirothBonus = "Sephiroth Bonus: Sora Slot 1" +SephirothFenrir = "Sephiroth Fenrir" +WinnersProof = "(HB2) Winner's Proof" +ProofofPeace = "(HB2) Proof of Peace" +DemyxDataAPBoost = "Data Demyx" + +CoRDepthsAPBoost = "(CoR) Depths AP Boost" +CoRDepthsPowerCrystal = "(CoR) Depths Power Crystal" +CoRDepthsFrostCrystal = "(CoR) Depths Frost Crystal" +CoRDepthsManifestIllusion = "(CoR) Depths Manifest Illusion" +CoRDepthsAPBoost2 = "(CoR) Depths AP Boost 2" +CoRMineshaftLowerLevelDepthsofRemembranceMap = "(CoR) Mineshaft Lower Level Depths of Remembrance Map" +CoRMineshaftLowerLevelAPBoost = "(CoR) Mineshaft Lower Level AP Boost" +CoRDepthsUpperLevelRemembranceGem = "(CoR) Depths Upper Level Remembrance Gem" +CoRMiningAreaSerenityGem = "(CoR) Mining Area Serenity Gem" +CoRMiningAreaAPBoost = "(CoR) Mining Area AP Boost" +CoRMiningAreaSerenityCrystal = "(CoR) Mining Area Serenity Crystal" +CoRMiningAreaManifestIllusion = "(CoR) Mining Area Manifest Illusion" +CoRMiningAreaSerenityGem2 = "(CoR) Mining Area Serenity Gem 2" +CoRMiningAreaDarkRemembranceMap = "(CoR) Mining Area Dark Remembrance Map" +CoRMineshaftMidLevelPowerBoost = "(CoR) Mineshaft Mid Level Power Boost" +CoREngineChamberSerenityCrystal = "(CoR) Engine Chamber Serenity Crystal" +CoREngineChamberRemembranceCrystal = "(CoR) Engine Chamber Remembrance Crystal" +CoREngineChamberAPBoost = "(CoR) Engine Chamber AP Boost" +CoREngineChamberManifestIllusion = "(CoR) Engine Chamber Manifest Illusion" +CoRMineshaftUpperLevelMagicBoost = "(CoR) Mineshaft Upper Level Magic Boost" +CoRMineshaftUpperLevelAPBoost = "(CoR) Mineshaft Upper Level AP Boost" +TransporttoRemembrance = "Transport to Remembrance" + +GorgeSavannahMap = "(PL) Gorge Savannah Map" +GorgeDarkGem = "(PL) Gorge Dark Gem" +GorgeMythrilStone = "(PL) Gorge Mythril Stone" +ElephantGraveyardFrostGem = "(PL) Elephant Graveyard Frost Gem" +ElephantGraveyardMythrilStone = "(PL) Elephant Graveyard Mythril Stone" +ElephantGraveyardBrightStone = "(PL) Elephant Graveyard Bright Stone" +ElephantGraveyardAPBoost = "(PL) Elephant Graveyard AP Boost" +ElephantGraveyardMythrilShard = "(PL) Elephant Graveyard Mythril Shard" +PrideRockMap = "(PL) Pride Rock Map" +PrideRockMythrilStone = "(PL) Pride Rock Mythril Stone" +PrideRockSerenityCrystal = "(PL) Pride Rock Serenity Crystal" +WildebeestValleyEnergyStone = "(PL) Wildebeest Valley Energy Stone" +WildebeestValleyAPBoost = "(PL) Wildebeest Valley AP Boost" +WildebeestValleyMythrilGem = "(PL) Wildebeest Valley Mythril Gem" +WildebeestValleyMythrilStone = "(PL) Wildebeest Valley Mythril Stone" +WildebeestValleyLucidGem = "(PL) Wildebeest Valley Lucid Gem" +WastelandsMythrilShard = "(PL) Wastelands Mythril Shard" +WastelandsSerenityGem = "(PL) Wastelands Serenity Gem" +WastelandsMythrilStone = "(PL) Wastelands Mythril Stone" +JungleSerenityGem = "(PL) Jungle Serenity Gem" +JungleMythrilStone = "(PL) Jungle Mythril Stone" +JungleSerenityCrystal = "(PL) Jungle Serenity Crystal" +OasisMap = "(PL) Oasis Map" +OasisTornPages = "(PL) Oasis Torn Pages" +OasisAPBoost = "(PL) Oasis AP Boost" +CircleofLife = "(PL) Circle of Life" +Hyenas1 = "(PL) Hyenas 1 Bonus: Sora Slot 1" +Scar = "(PL) Scar Bonus: Sora Slot 1" +ScarFireElement = "(PL) Scar Fire Element" +Hyenas2 = "(PL2) Hyenas 2 Bonus: Sora Slot 1" +Groundshaker = "(PL2) Groundshaker Bonus: Sora Slot 1" +GroundshakerGetBonus = "(PL2) Groundshaker Bonus: Sora Slot 2" +SaixDataDefenseBoost = "Data Saix" + +TwilightTownMap = "(STT) Twilight Town Map" +MunnyPouchOlette = "(STT) Munny Pouch Olette" +StationDusks = "(STT) Station Dusks" +StationofSerenityPotion = "(STT) Station of Serenity Potion" +StationofCallingPotion = "(STT) Station of Calling Potion" +TwilightThorn = "(STT) Twilight Thorn" +Axel1 = "(STT) Axel 1" +JunkChampionBelt = "(STT) Junk Champion Belt" +JunkMedal = "(STT) Junk Medal" +TheStruggleTrophy = "(STT) The Struggle Trophy" +CentralStationPotion1 = "(STT) Central Station Potion 1" +STTCentralStationHiPotion = "(STT) Central Station Hi-Potion" +CentralStationPotion2 = "(STT) Central Station Potion 2" +SunsetTerraceAbilityRing = "(STT) Sunset Terrace Ability Ring" +SunsetTerraceHiPotion = "(STT) Sunset Terrace Hi-Potion" +SunsetTerracePotion1 = "(STT) Sunset Terrace Potion 1" +SunsetTerracePotion2 = "(STT) Sunset Terrace Potion 2" +MansionFoyerHiPotion = "(STT) Mansion Foyer Hi-Potion" +MansionFoyerPotion1 = "(STT) Mansion Foyer Potion 1" +MansionFoyerPotion2 = "(STT) Mansion Foyer Potion 2" +MansionDiningRoomElvenBandanna = "(STT) Mansion Dining Room Elven Bandanna" +MansionDiningRoomPotion = "(STT) Mansion Dining Room Potion" +NaminesSketches = "(STT) Namine's Sketches" +MansionMap = "(STT) Mansion Map" +MansionLibraryHiPotion = "(STT) Mansion Library Hi-Potion" +Axel2 = "(STT) Axel 2" +MansionBasementCorridorHiPotion = "(STT) Mansion Basement Corridor Hi-Potion" +RoxasDataMagicBoost = "Data Roxas" + +OldMansionPotion = "(TT) Old Mansion Potion" +OldMansionMythrilShard = "(TT) Old Mansion Mythril Shard" +TheWoodsPotion = "(TT) The Woods Potion" +TheWoodsMythrilShard = "(TT) The Woods Mythril Shard" +TheWoodsHiPotion = "(TT) The Woods Hi-Potion" +TramCommonHiPotion = "(TT) Tram Common Hi-Potion" +TramCommonAPBoost = "(TT) Tram Common AP Boost" +TramCommonTent = "(TT) Tram Common Tent" +TramCommonMythrilShard1 = "(TT) Tram Common Mythril Shard 1" +TramCommonPotion1 = "(TT) Tram Common Potion 1" +TramCommonMythrilShard2 = "(TT) Tram Common Mythril Shard 2" +TramCommonPotion2 = "(TT) Tram Common Potion 2" +StationPlazaSecretAnsemReport2 = "(TT) Station Plaza Secret Ansem Report 2" +MunnyPouchMickey = "(TT) Munny Pouch Mickey" +CrystalOrb = "(TT) Crystal Orb" +CentralStationTent = "(TT) Central Station Tent" +TTCentralStationHiPotion = "(TT) Central Station Hi-Potion" +CentralStationMythrilShard = "(TT) Central Station Mythril Shard" +TheTowerPotion = "(TT) The Tower Potion" +TheTowerHiPotion = "(TT) The Tower Hi-Potion" +TheTowerEther = "(TT) The Tower Ether" +TowerEntrywayEther = "(TT) Tower Entryway Ether" +TowerEntrywayMythrilShard = "(TT) Tower Entryway Mythril Shard" +SorcerersLoftTowerMap = "(TT) Sorcerer's Loft Tower Map" +TowerWardrobeMythrilStone = "(TT) Tower Wardrobe Mythril Stone" +StarSeeker = "(TT) Star Seeker" +ValorForm = "(TT) Valor Form" +SeifersTrophy = "(TT2) Seifer's Trophy" +Oathkeeper = "(TT2) Oathkeeper" +LimitForm = "(TT2) Limit Form" +UndergroundConcourseMythrilGem = "(TT3) Underground Concourse Mythril Gem" +UndergroundConcourseOrichalcum = "(TT3) Underground Concourse Orichalcum" +UndergroundConcourseAPBoost = "(TT3) Underground Concourse AP Boost" +UndergroundConcourseMythrilCrystal = "(TT3) Underground Concourse Mythril Crystal" +TunnelwayOrichalcum = "(TT3) Tunnelway Orichalcum" +TunnelwayMythrilCrystal = "(TT3) Tunnelway Mythril Crystal" +SunsetTerraceOrichalcumPlus = "(TT3) Sunset Terrace Orichalcum+" +SunsetTerraceMythrilShard = "(TT3) Sunset Terrace Mythril Shard" +SunsetTerraceMythrilCrystal = "(TT3) Sunset Terrace Mythril Crystal" +SunsetTerraceAPBoost = "(TT3) Sunset Terrace AP Boost" +MansionNobodies = "(TT3) Mansion Nobodies Bonus: Sora Slot 1" +MansionFoyerMythrilCrystal = "(TT3) Mansion Foyer Mythril Crystal" +MansionFoyerMythrilStone = "(TT3) Mansion Foyer Mythril Stone" +MansionFoyerSerenityCrystal = "(TT3) Mansion Foyer Serenity Crystal" +MansionDiningRoomMythrilCrystal = "(TT3) Mansion Dining Room Mythril Crystal" +MansionDiningRoomMythrilStone = "(TT3) Mansion Dining Room Mythril Stone" +MansionLibraryOrichalcum = "(TT3) Mansion Library Orichalcum" +BeamSecretAnsemReport10 = "(TT3) Beam Secret Ansem Report 10" +MansionBasementCorridorUltimateRecipe = "(TT3) Mansion Basement Corridor Ultimate Recipe" +BetwixtandBetween = "(TT3) Betwixt and Between" +BetwixtandBetweenBondofFlame = "(TT3) Betwixt and Between Bond of Flame" +AxelDataMagicBoost = "Data Axel" + +FragmentCrossingMythrilStone = "(TWTNW) Fragment Crossing Mythril Stone" +FragmentCrossingMythrilCrystal = "(TWTNW) Fragment Crossing Mythril Crystal" +FragmentCrossingAPBoost = "(TWTNW) Fragment Crossing AP Boost" +FragmentCrossingOrichalcum = "(TWTNW) Fragment Crossing Orichalcum" +Roxas = "(TWTNW) Roxas Bonus: Sora Slot 1" +RoxasGetBonus = "(TWTNW) Roxas Bonus: Sora Slot 2" +RoxasSecretAnsemReport8 = "(TWTNW) Roxas Secret Ansem Report 8" +TwoBecomeOne = "(TWTNW) Two Become One" +MemorysSkyscaperMythrilCrystal = "(TWTNW) Memory's Skyscaper Mythril Crystal" +MemorysSkyscaperAPBoost = "(TWTNW) Memory's Skyscaper AP Boost" +MemorysSkyscaperMythrilStone = "(TWTNW) Memory's Skyscaper Mythril Stone" +TheBrinkofDespairDarkCityMap = "(TWTNW) The Brink of Despair Dark City Map" +TheBrinkofDespairOrichalcumPlus = "(TWTNW) The Brink of Despair Orichalcum+" +NothingsCallMythrilGem = "(TWTNW) Nothing's Call Mythril Gem" +NothingsCallOrichalcum = "(TWTNW) Nothing's Call Orichalcum" +TwilightsViewCosmicBelt = "(TWTNW) Twilight's View Cosmic Belt" +XigbarBonus = "(TWTNW) Xigbar Bonus: Sora Slot 1" +XigbarSecretAnsemReport3 = "(TWTNW) Xigbar Secret Ansem Report 3" +NaughtsSkywayMythrilGem = "(TWTNW) Naught's Skyway Mythril Gem" +NaughtsSkywayOrichalcum = "(TWTNW) Naught's Skyway Orichalcum" +NaughtsSkywayMythrilCrystal = "(TWTNW) Naught's Skyway Mythril Crystal" +Oblivion = "(TWTNW) Oblivion" +CastleThatNeverWasMap = "(TWTNW) Castle That Never Was Map" +Luxord = "(TWTNW) Luxord" +LuxordGetBonus = "(TWTNW) Luxord Bonus: Sora Slot 1" +LuxordSecretAnsemReport9 = "(TWTNW) Luxord Secret Ansem Report 9" +SaixBonus = "(TWTNW) Saix Bonus: Sora Slot 1" +SaixSecretAnsemReport12 = "(TWTNW) Saix Secret Ansem Report 12" +PreXemnas1SecretAnsemReport11 = "(TWTNW) Secret Ansem Report 11 (Pre-Xemnas 1)" +RuinandCreationsPassageMythrilStone = "(TWTNW) Ruin and Creation's Passage Mythril Stone" +RuinandCreationsPassageAPBoost = "(TWTNW) Ruin and Creation's Passage AP Boost" +RuinandCreationsPassageMythrilCrystal = "(TWTNW) Ruin and Creation's Passage Mythril Crystal" +RuinandCreationsPassageOrichalcum = "(TWTNW) Ruin and Creation's Passage Orichalcum" +Xemnas1 = "(TWTNW) Xemnas 1 Bonus: Sora Slot 1" +Xemnas1GetBonus = "(TWTNW) Xemnas 1 Bonus: Sora Slot 2" +Xemnas1SecretAnsemReport13 = "(TWTNW) Xemnas 1 Secret Ansem Report 13" +FinalXemnas = "Final Xemnas" +XemnasDataPowerBoost = "Data Xemnas" +Lvl1 ="Level 01" +Lvl2 ="Level 02" +Lvl3 ="Level 03" +Lvl4 ="Level 04" +Lvl5 ="Level 05" +Lvl6 ="Level 06" +Lvl7 ="Level 07" +Lvl8 ="Level 08" +Lvl9 ="Level 09" +Lvl10 ="Level 10" +Lvl11 ="Level 11" +Lvl12 ="Level 12" +Lvl13 ="Level 13" +Lvl14 ="Level 14" +Lvl15 ="Level 15" +Lvl16 ="Level 16" +Lvl17 ="Level 17" +Lvl18 ="Level 18" +Lvl19 ="Level 19" +Lvl20 ="Level 20" +Lvl21 ="Level 21" +Lvl22 ="Level 22" +Lvl23 ="Level 23" +Lvl24 ="Level 24" +Lvl25 ="Level 25" +Lvl26 ="Level 26" +Lvl27 ="Level 27" +Lvl28 ="Level 28" +Lvl29 ="Level 29" +Lvl30 ="Level 30" +Lvl31 ="Level 31" +Lvl32 ="Level 32" +Lvl33 ="Level 33" +Lvl34 ="Level 34" +Lvl35 ="Level 35" +Lvl36 ="Level 36" +Lvl37 ="Level 37" +Lvl38 ="Level 38" +Lvl39 ="Level 39" +Lvl40 ="Level 40" +Lvl41 ="Level 41" +Lvl42 ="Level 42" +Lvl43 ="Level 43" +Lvl44 ="Level 44" +Lvl45 ="Level 45" +Lvl46 ="Level 46" +Lvl47 ="Level 47" +Lvl48 ="Level 48" +Lvl49 ="Level 49" +Lvl50 ="Level 50" +Lvl51 ="Level 51" +Lvl52 ="Level 52" +Lvl53 ="Level 53" +Lvl54 ="Level 54" +Lvl55 ="Level 55" +Lvl56 ="Level 56" +Lvl57 ="Level 57" +Lvl58 ="Level 58" +Lvl59 ="Level 59" +Lvl60 ="Level 60" +Lvl61 ="Level 61" +Lvl62 ="Level 62" +Lvl63 ="Level 63" +Lvl64 ="Level 64" +Lvl65 ="Level 65" +Lvl66 ="Level 66" +Lvl67 ="Level 67" +Lvl68 ="Level 68" +Lvl69 ="Level 69" +Lvl70 ="Level 70" +Lvl71 ="Level 71" +Lvl72 ="Level 72" +Lvl73 ="Level 73" +Lvl74 ="Level 74" +Lvl75 ="Level 75" +Lvl76 ="Level 76" +Lvl77 ="Level 77" +Lvl78 ="Level 78" +Lvl79 ="Level 79" +Lvl80 ="Level 80" +Lvl81 ="Level 81" +Lvl82 ="Level 82" +Lvl83 ="Level 83" +Lvl84 ="Level 84" +Lvl85 ="Level 85" +Lvl86 ="Level 86" +Lvl87 ="Level 87" +Lvl88 ="Level 88" +Lvl89 ="Level 89" +Lvl90 ="Level 90" +Lvl91 ="Level 91" +Lvl92 ="Level 92" +Lvl93 ="Level 93" +Lvl94 ="Level 94" +Lvl95 ="Level 95" +Lvl96 ="Level 96" +Lvl97 ="Level 97" +Lvl98 ="Level 98" +Lvl99 ="Level 99" +Valorlvl1 ="Valor level 1" +Valorlvl2 ="Valor level 2" +Valorlvl3 ="Valor level 3" +Valorlvl4 ="Valor level 4" +Valorlvl5 ="Valor level 5" +Valorlvl6 ="Valor level 6" +Valorlvl7 ="Valor level 7" +Wisdomlvl1 ="Wisdom level 1" +Wisdomlvl2 ="Wisdom level 2" +Wisdomlvl3 ="Wisdom level 3" +Wisdomlvl4 ="Wisdom level 4" +Wisdomlvl5 ="Wisdom level 5" +Wisdomlvl6 ="Wisdom level 6" +Wisdomlvl7 ="Wisdom level 7" +Limitlvl1 ="Limit level 1" +Limitlvl2 ="Limit level 2" +Limitlvl3 ="Limit level 3" +Limitlvl4 ="Limit level 4" +Limitlvl5 ="Limit level 5" +Limitlvl6 ="Limit level 6" +Limitlvl7 ="Limit level 7" +Masterlvl1 ="Master level 1" +Masterlvl2 ="Master level 2" +Masterlvl3 ="Master level 3" +Masterlvl4 ="Master level 4" +Masterlvl5 ="Master level 5" +Masterlvl6 ="Master level 6" +Masterlvl7 ="Master level 7" +Finallvl1 ="Final level 1" +Finallvl2 ="Final level 2" +Finallvl3 ="Final level 3" +Finallvl4 ="Final level 4" +Finallvl5 ="Final level 5" +Finallvl6 ="Final level 6" +Finallvl7 ="Final level 7" + +GardenofAssemblageMap ="Garden of Assemblage Map" +GoALostIllusion ="GoA Lost Illusion" +ProofofNonexistence ="Proof of Nonexistence Location" + +test= "test" + + +Crit_1 ="Critical Starting Ability 1" +Crit_2 ="Critical Starting Ability 2" +Crit_3 ="Critical Starting Ability 3" +Crit_4 ="Critical Starting Ability 4" +Crit_5 ="Critical Starting Ability 5" +Crit_6 ="Critical Starting Ability 6" +Crit_7 ="Critical Starting Ability 7" +DonaldStarting1 ="Donald Starting Item 1" +DonaldStarting2 ="Donald Starting Item 2" +GoofyStarting1 ="Goofy Starting Item 1" +GoofyStarting2 ="Goofy Starting Item 2" + + +DonaldScreens ="(SP) Screens Bonus: Donald Slot 1" +DonaldDemyxHBGetBonus ="(HB) Demyx Bonus: Donald Slot 1" +DonaldDemyxOC ="(OC) Demyx Bonus: Donald Slot 1" +DonaldBoatPete ="(TR) Boat Pete Bonus: Donald Slot 1" +DonaldBoatPeteGetBonus ="(TR) Boat Pete Bonus: Donald Slot 2" +DonaldPrisonKeeper ="(HT) Prison Keeper Bonus: Donald Slot 1" +DonaldScar ="(PL) Scar Bonus: Donald Slot 1" +DonaldSolarSailer ="(SP2) Solar Sailer Bonus: Donald Slot 1" +DonaldExperiment ="(HT2) Experiment Bonus: Donald Slot 1" +DonaldBoatFight ="(PR) Boat Fight Bonus: Donald Slot 1" +DonaldMansionNobodies ="(TT3) Mansion Nobodies Bonus: Donald Slot 1" +DonaldThresholder ="(BC) Thresholder Bonus: Donald Slot 1" +DonaldXaldinGetBonus ="(BC2) Xaldin Bonus: Donald Slot 1" +DonaladGrimReaper2 ="(PR2) Grim Reaper 2 Bonus: Donald Slot 1" +DonaldAbuEscort ="(AG) Abu Escort Bonus: Donald Slot 1" + +GoofyBarbossa ="(PR) Barbossa Bonus: Goofy Slot 1" +GoofyBarbossaGetBonus ="(PR) Barbossa Bonus: Goofy Slot 2" +GoofyGrimReaper1 ="(PR2) Grim Reaper 1 Bonus: Goofy Slot 1" +GoofyHostileProgram ="(SP) Hostile Program Bonus: Goofy Slot 1" +GoofyHyenas1 ="(PL) Hyenas 1 Bonus: Goofy Slot 1" +GoofyHyenas2 ="(PL2) Hyenas 2 Bonus: Goofy Slot 1" +GoofyLock ="(HT2) Lock, Shock and Barrel Bonus: Goofy Slot 1" +GoofyOogieBoogie ="(HT) Oogie Boogie Bonus: Goofy Slot 1" +GoofyPeteOC ="(OC) Pete Bonus: Goofy Slot 1" +GoofyFuturePete ="(TR) Future Pete Bonus: Goofy Slot 1" +GoofyShanYu ="(LoD) Shan-Yu Bonus: Goofy Slot 1" +GoofyStormRider ="(LoD2) Storm Rider Bonus: Goofy Slot 1" +GoofyBeast ="(BC) Beast Bonus: Goofy Slot 1" +GoofyInterceptorBarrels ="(PR) Interceptor Barrels Bonus: Goofy Slot 1" +GoofyTreasureRoom ="(AG) Treasure Room Heartless Bonus: Goofy Slot 1" +GoofyZexion ="Zexion Bonus: Goofy Slot 1" + + +AdamantShield ="Adamant Shield Slot" +AkashicRecord ="Akashic Record Slot" +ChainGear ="Chain Gear Slot" +DreamCloud ="Dream Cloud Slot" +FallingStar ="Falling Star Slot" +FrozenPride2 ="Frozen Pride+ Slot" +GenjiShield ="Genji Shield Slot" +KnightDefender ="Knight Defender Slot" +KnightsShield ="Knight's Shield Slot" +MajesticMushroom ="Majestic Mushroom Slot" +MajesticMushroom2 ="Majestic Mushroom+ Slot" +NobodyGuard ="Nobody Guard Slot" +OgreShield ="Ogre Shield Slot" +SaveTheKing2 ="Save The King+ Slot" +UltimateMushroom ="Ultimate Mushroom Slot" + +HammerStaff ="Hammer Staff Slot" +LordsBroom ="Lord's Broom Slot" +MagesStaff ="Mages Staff Slot" +MeteorStaff ="Meteor Staff Slot" +CometStaff ="Comet Staff Slot" +Centurion2 ="Centurion+ Slot" +MeteorStaff ="Meteor Staff Slot" +NobodyLance ="Nobody Lance Slot" +PreciousMushroom ="Precious Mushroom Slot" +PreciousMushroom2 ="Precious Mushroom+ Slot" +PremiumMushroom ="Premium Mushroom Slot" +RisingDragon ="Rising Dragon Slot" +SaveTheQueen2 ="Save The Queen+ Slot" +ShamansRelic ="Shaman's Relic Slot" +VictoryBell ="Victory Bell Slot" +WisdomWand ="Wisdom Wand Slot" + +#Keyblade Slots +FAKESlot ="FAKE Slot" +DetectionSaberSlot ="Detection Saber Slot" +EdgeofUltimaSlot ="Edge of Ultima Slot" +KingdomKeySlot ="Kingdom Key Slot" +OathkeeperSlot ="Oathkeeper Slot" +OblivionSlot ="Oblivion Slot" +StarSeekerSlot ="Star Seeker Slot" +HiddenDragonSlot ="Hidden Dragon Slot" +HerosCrestSlot ="Hero's Crest Slot" +MonochromeSlot ="Monochrome Slot" +FollowtheWindSlot ="Follow the Wind Slot" +CircleofLifeSlot ="Circle of Life Slot" +PhotonDebuggerSlot ="Photon Debugger Slot" +GullWingSlot ="Gull Wing Slot" +RumblingRoseSlot ="Rumbling Rose Slot" +GuardianSoulSlot ="Guardian Soul Slot" +WishingLampSlot ="Wishing Lamp Slot" +DecisivePumpkinSlot ="Decisive Pumpkin Slot" +SweetMemoriesSlot ="Sweet Memories Slot" +MysteriousAbyssSlot ="Mysterious Abyss Slot" +SleepingLionSlot ="Sleeping Lion Slot" +BondofFlameSlot ="Bond of Flame Slot" +TwoBecomeOneSlot ="Two Become One Slot" +FatalCrestSlot ="Fatal Crest Slot" +FenrirSlot ="Fenrir Slot" +UltimaWeaponSlot ="Ultima Weapon Slot" +WinnersProofSlot ="Winner's Proof Slot" +PurebloodSlot ="Pureblood Slot" + +#Final_Region ="Final Form" diff --git a/worlds/kh2/Names/RegionName.py b/worlds/kh2/Names/RegionName.py new file mode 100644 index 0000000000..d07b5d3de3 --- /dev/null +++ b/worlds/kh2/Names/RegionName.py @@ -0,0 +1,90 @@ +LoD_Region ="Land of Dragons" +LoD2_Region ="Land of Dragons 2" + +Ag_Region ="Agrabah" +Ag2_Region ="Agrabah 2" + +Dc_Region ="Disney Castle" +Tr_Region ="Timeless River" + +HundredAcre1_Region ="Pooh's House" +HundredAcre2_Region ="Piglet's House" +HundredAcre3_Region ="Rabbit's House" +HundredAcre4_Region ="Roo's House" +HundredAcre5_Region ="Spookey Cave" +HundredAcre6_Region ="Starry Hill" + +Pr_Region ="Port Royal" +Pr2_Region ="Port Royal 2" +Gr2_Region ="Grim Reaper 2" + +Oc_Region ="Olympus Coliseum" +Oc2_Region ="Olympus Coliseum 2" +Oc2_pain_and_panic_Region ="Pain and Panic Cup" +Oc2_titan_Region ="Titan Cup" +Oc2_cerberus_Region ="Cerberus Cup" +Oc2_gof_Region ="Goddest of Fate Cup" +Oc2Cups_Region ="Olympus Coliseum Cups" +HadesCups_Region ="Olympus Coliseum Hade's Paradox" + +Bc_Region ="Beast's Castle" +Bc2_Region ="Beast's Castle 2" +Xaldin_Region ="Xaldin" + +Sp_Region ="Space Paranoids" +Sp2_Region ="Space Paranoids 2" +Mcp_Region ="Master Control Program" + +Ht_Region ="Holloween Town" +Ht2_Region ="Holloween Town 2" + +Hb_Region ="Hollow Bastion" +Hb2_Region ="Hollow Bastion 2" +ThousandHeartless_Region ="Thousand Hearless" +Mushroom13_Region ="Mushroom 13" +CoR_Region ="Cavern of Rememberance" +Transport_Region ="Transport to Rememberance" + +Pl_Region ="Pride Lands" +Pl2_Region ="Pride Lands 2" + +STT_Region ="Simulated Twilight Town" + +TT_Region ="Twlight Town" +TT2_Region ="Twlight Town 2" +TT3_Region ="Twlight Town 3" + +Twtnw_Region ="The World That Never Was (First Visit)" +Twtnw_PostRoxas ="The World That Never Was (Post Roxas)" +Twtnw_PostXigbar ="The World That Never Was (Post Xigbar)" +Twtnw2_Region ="The World That Never Was (Second Visit)" #before riku transformation + +SoraLevels_Region ="Sora's Levels" +GoA_Region ="Garden Of Assemblage" +Keyblade_Region ="Keyblade Slots" + +Valor_Region ="Valor Form" +Wisdom_Region ="Wisdom Form" +Limit_Region ="Limit Form" +Master_Region ="Master Form" +Final_Region ="Final Form" + +Terra_Region ="Lingering Will" +Sephi_Region ="Sephiroth" +Marluxia_Region ="Marluxia" +Larxene_Region ="Larxene" +Vexen_Region ="Vexen" +Lexaeus_Region ="Lexaeus" +Zexion_Region ="Zexion" + +LevelsVS1 ="Levels Region (1 Visit Locking Item)" +LevelsVS3 ="Levels Region (3 Visit Locking Items)" +LevelsVS6 ="Levels Region (6 Visit Locking Items)" +LevelsVS9 ="Levels Region (9 Visit Locking Items)" +LevelsVS12 ="Levels Region (12 Visit Locking Items)" +LevelsVS15 ="Levels Region (15 Visit Locking Items)" +LevelsVS18 ="Levels Region (18 Visit Locking Items)" +LevelsVS21 ="Levels Region (21 Visit Locking Items)" +LevelsVS24 ="Levels Region (24 Visit Locking Items)" +LevelsVS26 ="Levels Region (26 Visit Locking Items)" + diff --git a/worlds/kh2/OpenKH.py b/worlds/kh2/OpenKH.py new file mode 100644 index 0000000000..eb1a846e42 --- /dev/null +++ b/worlds/kh2/OpenKH.py @@ -0,0 +1,248 @@ +import logging + +import yaml +import os +import Utils +import zipfile + +from .Items import item_dictionary_table, CheckDupingItems +from .Locations import all_locations, SoraLevels, exclusion_table, AllWeaponSlot +from .Names import LocationName +from .XPValues import lvlStats, formExp, soraExp +from worlds.Files import APContainer + + +class KH2Container(APContainer): + game: str = 'Kingdom Hearts 2' + + def __init__(self, patch_data: dict, base_path: str, output_directory: str, + player=None, player_name: str = "", server: str = ""): + self.patch_data = patch_data + self.file_path = base_path + container_path = os.path.join(output_directory, base_path + ".zip") + super().__init__(container_path, player, player_name, server) + + def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None: + for filename, yml in self.patch_data.items(): + opened_zipfile.writestr(filename, yml) + for root, dirs, files in os.walk(os.path.join(os.path.dirname(__file__), "mod_template")): + for file in files: + opened_zipfile.write(os.path.join(root, file), + os.path.relpath(os.path.join(root, file), + os.path.join(os.path.dirname(__file__), "mod_template"))) + # opened_zipfile.writestr(self.zpf_path, self.patch_data) + super().write_contents(opened_zipfile) + + +def patch_kh2(self, output_directory): + def increaseStat(i): + if lvlStats[i] == "str": + self.strength += 2 + elif lvlStats[i] == "mag": + self.magic += 2 + elif lvlStats[i] == "def": + self.defense += 1 + elif lvlStats[i] == "ap": + self.ap += 3 + + self.formattedTrsr = {} + self.formattedBons = [] + self.formattedLvup = {"Sora": {}} + self.formattedBons = {} + self.formattedFmlv = {} + self.formattedItem = {"Stats": []} + self.formattedPlrp = [] + self.strength = 2 + self.magic = 6 + self.defense = 2 + self.ap = 0 + self.dblbonus = 0 + formexp = None + formName = None + levelsetting = list() + slotDataDuping = set() + for values in CheckDupingItems.values(): + if isinstance(values, set): + slotDataDuping = slotDataDuping.union(values) + else: + for inner_values in values.values(): + slotDataDuping = slotDataDuping.union(inner_values) + + if self.multiworld.Keyblade_Minimum[self.player].value > self.multiworld.Keyblade_Maximum[self.player].value: + logging.info( + f"{self.multiworld.get_file_safe_player_name(self.player)} has Keyblade Minimum greater than Keyblade Maximum") + keyblademin = self.multiworld.Keyblade_Maximum[self.player].value + keyblademax = self.multiworld.Keyblade_Minimum[self.player].value + else: + keyblademin = self.multiworld.Keyblade_Minimum[self.player].value + keyblademax = self.multiworld.Keyblade_Maximum[self.player].value + + if self.multiworld.LevelDepth[self.player] == "level_50": + levelsetting.extend(exclusion_table["Level50"]) + + elif self.multiworld.LevelDepth[self.player] == "level_99": + levelsetting.extend(exclusion_table["Level99"]) + + elif self.multiworld.LevelDepth[self.player] in ["level_50_sanity", "level_99_sanity"]: + levelsetting.extend(exclusion_table["Level50Sanity"]) + + if self.multiworld.LevelDepth[self.player] == "level_99_sanity": + levelsetting.extend(exclusion_table["Level99Sanity"]) + + mod_name = f"AP-{self.multiworld.seed_name}-P{self.player}-{self.multiworld.get_file_safe_player_name(self.player)}" + + for location in self.multiworld.get_filled_locations(self.player): + + data = all_locations[location.name] + if location.item.player == self.player: + itemcode = item_dictionary_table[location.item.name].kh2id + if location.item.name in slotDataDuping and \ + location.name not in AllWeaponSlot: + self.LocalItems[location.address] = item_dictionary_table[location.item.name].code + else: + itemcode = 90 # castle map + + if data.yml == "Chest": + self.formattedTrsr[data.locid] = {"ItemId": itemcode} + + elif data.yml in ["Get Bonus", "Double Get Bonus", "Second Get Bonus"]: + if data.yml == "Get Bonus": + self.dblbonus = 0 + # if double bonus then addresses dbl bonus so the next check gets 2 items on it + if data.yml == "Double Get Bonus": + self.dblbonus = itemcode + continue + if data.locid not in self.formattedBons.keys(): + self.formattedBons[data.locid] = {} + self.formattedBons[data.locid][data.charName] = { + "RewardId": data.locid, + "CharacterId": data.charNumber, + "HpIncrease": 0, + "MpIncrease": 0, + "DriveGaugeUpgrade": 0, + "ItemSlotUpgrade": 0, + "AccessorySlotUpgrade": 0, + "ArmorSlotUpgrade": 0, + "BonusItem1": itemcode, + "BonusItem2": self.dblbonus, + "Padding": 0 + + } + # putting dbl bonus at 0 again, so we don't have the same item placed multiple time + self.dblbonus = 0 + elif data.yml == "Keyblade": + self.formattedItem["Stats"].append({ + "Id": data.locid, + "Attack": self.multiworld.per_slot_randoms[self.player].randint(keyblademin, keyblademax), + "Magic": self.multiworld.per_slot_randoms[self.player].randint(keyblademin, keyblademax), + "Defense": 0, + "Ability": itemcode, + "AbilityPoints": 0, + "Unknown08": 100, + "FireResistance": 100, + "IceResistance": 100, + "LightningResistance": 100, + "DarkResistance": 100, + "Unknown0d": 100, + "GeneralResistance": 100, + "Unknown": 0 + }) + + elif data.yml == "Forms": + # loc id is form lvl + # char name is the form name number :) + if data.locid == 2: + formDict = {1: "Valor", 2: "Wisdom", 3: "Limit", 4: "Master", 5: "Final"} + formDictExp = { + 1: self.multiworld.Valor_Form_EXP[self.player].value, + 2: self.multiworld.Wisdom_Form_EXP[self.player].value, + 3: self.multiworld.Limit_Form_EXP[self.player].value, + 4: self.multiworld.Master_Form_EXP[self.player].value, + 5: self.multiworld.Final_Form_EXP[self.player].value} + formexp = formDictExp[data.charName] + formName = formDict[data.charName] + self.formattedFmlv[formName] = [] + self.formattedFmlv[formName].append({ + "Ability": 1, + "Experience": int(formExp[data.charName][data.locid] / formexp), + "FormId": data.charName, + "FormLevel": 1, + "GrowthAbilityLevel": 0, + }) + # row is form column is lvl + self.formattedFmlv[formName].append({ + "Ability": itemcode, + "Experience": int(formExp[data.charName][data.locid] / formexp), + "FormId": data.charName, + "FormLevel": data.locid, + "GrowthAbilityLevel": 0, + }) + + # Summons have no checks on them so done fully locally + self.formattedFmlv["Summon"] = [] + for x in range(1, 7): + self.formattedFmlv["Summon"].append({ + "Ability": 123, + "Experience": int(formExp[0][x] / self.multiworld.Summon_EXP[self.player].value), + "FormId": 0, + "FormLevel": x, + "GrowthAbilityLevel": 0, + }) + # levels done down here because of optional settings that can take locations out of the pool. + self.i = 1 + for location in SoraLevels: + increaseStat(self.multiworld.per_slot_randoms[self.player].randint(0, 3)) + if location in levelsetting: + data = self.multiworld.get_location(location, self.player) + if data.item.player == self.player: + itemcode = item_dictionary_table[data.item.name].kh2id + else: + itemcode = 90 # castle map + else: + increaseStat(self.multiworld.per_slot_randoms[self.player].randint(0, 3)) + itemcode = 0 + self.formattedLvup["Sora"][self.i] = { + "Exp": int(soraExp[self.i] / self.multiworld.Sora_Level_EXP[self.player].value), + "Strength": self.strength, + "Magic": self.magic, + "Defense": self.defense, + "Ap": self.ap, + "SwordAbility": itemcode, + "ShieldAbility": itemcode, + "StaffAbility": itemcode, + "Padding": 0, + "Character": "Sora", + "Level": self.i + } + self.i += 1 + # averaging stats for the struggle bats + for x in {122, 144, 145}: + self.formattedItem["Stats"].append({ + "Id": x, + "Attack": int((keyblademin + keyblademax) / 2), + "Magic": int((keyblademin + keyblademax) / 2), + "Defense": 0, + "Ability": 405, + "AbilityPoints": 0, + "Unknown08": 100, + "FireResistance": 100, + "IceResistance": 100, + "LightningResistance": 100, + "DarkResistance": 100, + "Unknown0d": 100, + "GeneralResistance": 100, + "Unknown": 0 + }) + mod_dir = os.path.join(output_directory, mod_name + "_" + Utils.__version__) + + openkhmod = { + "TrsrList.yml": yaml.dump(self.formattedTrsr, line_break="\n"), + "LvupList.yml": yaml.dump(self.formattedLvup, line_break="\n"), + "BonsList.yml": yaml.dump(self.formattedBons, line_break="\n"), + "ItemList.yml": yaml.dump(self.formattedItem, line_break="\n"), + "FmlvList.yml": yaml.dump(self.formattedFmlv, line_break="\n"), + } + + mod = KH2Container(openkhmod, mod_dir, output_directory, self.player, + self.multiworld.get_file_safe_player_name(self.player)) + mod.write() diff --git a/worlds/kh2/Options.py b/worlds/kh2/Options.py new file mode 100644 index 0000000000..31708218e2 --- /dev/null +++ b/worlds/kh2/Options.py @@ -0,0 +1,274 @@ +from Options import Choice, Option, Range, Toggle, OptionSet +import typing + +from worlds.kh2 import SupportAbility_Table, ActionAbility_Table + + +class SoraEXP(Range): + """Sora Level Exp Multiplier""" + display_name = "Sora Level EXP" + range_start = 1 + range_end = 10 + default = 5 + + +class FinalEXP(Range): + """Final Form Exp Multiplier""" + display_name = "Final Form EXP" + range_start = 1 + range_end = 10 + default = 3 + + +class MasterEXP(Range): + """Master Form Exp Multiplier""" + display_name = "Master Form EXP" + range_start = 1 + range_end = 10 + default = 3 + + +class LimitEXP(Range): + """Limit Form Exp Multiplier""" + display_name = "Limit Form EXP" + range_start = 1 + range_end = 10 + default = 3 + + +class WisdomEXP(Range): + """Wisdom Form Exp Multiplier""" + display_name = "Wisdom Form EXP" + range_start = 1 + range_end = 10 + default = 3 + + +class ValorEXP(Range): + """Valor Form Exp Multiplier""" + display_name = "Valor Form EXP" + range_start = 1 + range_end = 10 + default = 3 + + +class SummonEXP(Range): + """Summon Exp Multiplier""" + display_name = "Summon level EXP" + range_start = 1 + range_end = 10 + default = 5 + + +class Schmovement(Choice): + """Level of Progressive Movement Abilities You Start With""" + display_name = "Schmovement" + option_level_0 = 0 + option_level_1 = 1 + option_level_2 = 2 + option_level_3 = 3 + option_level_4 = 4 + default = 1 + + +class RandomGrowth(Range): + """Amount of Random Progressive Movement Abilities You Start With""" + display_name = "Random Starting Growth" + range_start = 0 + range_end = 20 + default = 0 + + +class KeybladeMin(Range): + """Minimum Stats for Keyblades""" + display_name = "Keyblade Minimum Stats" + range_start = 0 + range_end = 20 + default = 3 + + +class KeybladeMax(Range): + """Maximum Stats for Keyblades""" + display_name = "Keyblade Max Stats" + range_start = 0 + range_end = 20 + default = 7 + + +class Visitlocking(Choice): + """Determines the level of visit locking + + No Visit Locking: Start with all 25 visit locking items. + + + Second Visit Locking: Start with 13 visit locking items for every first visit. + + + First and Second Visit Locking: One item for First Visit Two For Second Visit""" + display_name = "Visit locking" + option_no_visit_locking = 0 # starts with 25 visit locking + option_second_visit_locking = 1 # starts with 13 (no icecream/picture) + option_first_and_second_visit_locking = 2 # starts with nothing + default = 2 + + +class RandomVisitLockingItem(Range): + """Start with random amount of visit locking items.""" + display_name = "Random Visit Locking Item" + range_start = 0 + range_end = 25 + default = 3 + + +class SuperBosses(Toggle): + """Terra, Sephiroth and Data Fights Toggle.""" + display_name = "Super Bosses" + default = False + + +class Cups(Choice): + """Olympus Cups Toggles + No Cups: All Cups are placed into Excluded Locations. + Cups: Hades Paradox Cup is placed into Excluded Locations + Cups and Hades Paradox: Has Every Cup On.""" + display_name = "Olympus Cups" + option_no_cups = 0 + option_cups = 1 + option_cups_and_hades_paradox = 2 + default = 1 + + +class LevelDepth(Choice): + """Determines How many locations you want on levels + + Level 50:23 checks spread through 50 levels. + Level 99:23 checks spread through 99 levels. + + Level 50 sanity: 49 checks spread through 50 levels. + Level 99 sanity: 98 checks spread through 99 levels. + + Level 1: no checks on levels(checks are replaced with stats)""" + display_name = "Level Depth" + option_level_50 = 0 + option_level_99 = 1 + option_level_50_sanity = 2 + option_level_99_sanity = 3 + option_level_1 = 4 + default = 0 + + +class PromiseCharm(Toggle): + """Add Promise Charm to the Pool""" + display_name = "Promise Charm" + default = False + + +class KeybladeAbilities(Choice): + """ + Action: Action Abilities in the Keyblade Slot Pool. + + Support: Support Abilities in the Keyblade Slot Pool. + + Both: Action and Support Abilities in the Keyblade Slot Pool.""" + display_name = "Keyblade Abilities" + option_support = 0 + option_action = 1 + option_both = 2 + default = 0 + + +class BlacklistKeyblade(OptionSet): + """Black List these Abilities on Keyblades""" + display_name = "Blacklist Keyblade Abilities" + valid_keys = set(SupportAbility_Table.keys()).union(ActionAbility_Table.keys()) + + +class Goal(Choice): + """Win Condition + Three Proofs: Get a Gold Crown on Sora's Head. + + Lucky Emblem Hunt: Find Required Amount of Lucky Emblems . + + Hitlist (Bounty Hunt): Find Required Amount of Bounties""" + display_name = "Goal" + option_three_proofs = 0 + option_lucky_emblem_hunt = 1 + option_hitlist = 2 + default = 0 + + +class FinalXemnas(Toggle): + """Kill Final Xemnas to Beat the Game. + This is in addition to your Goal. I.E. get three proofs+kill final Xemnas""" + display_name = "Final Xemnas" + default = True + + +class LuckyEmblemsRequired(Range): + """Number of Lucky Emblems to collect to Win/Unlock Final Xemnas Door. + + If Goal is not Lucky Emblem Hunt this does nothing.""" + display_name = "Lucky Emblems Required" + range_start = 1 + range_end = 60 + default = 30 + + +class LuckyEmblemsAmount(Range): + """Number of Lucky Emblems that are in the pool. + + If Goal is not Lucky Emblem Hunt this does nothing.""" + display_name = "Lucky Emblems Available" + range_start = 1 + range_end = 60 + default = 40 + + +class BountyRequired(Range): + """Number of Bounties to collect to Win/Unlock Final Xemnas Door. + + If Goal is not Hitlist this does nothing.""" + display_name = "Bounties Required" + range_start = 1 + range_end = 24 + default = 7 + + +class BountyAmount(Range): + """Number of Bounties that are in the pool. + + If Goal is not Hitlist this does nothing.""" + display_name = "Bounties Available" + range_start = 1 + range_end = 24 + default = 13 + + +KH2_Options: typing.Dict[str, type(Option)] = { + "LevelDepth": LevelDepth, + "Sora_Level_EXP": SoraEXP, + "Valor_Form_EXP": ValorEXP, + "Wisdom_Form_EXP": WisdomEXP, + "Limit_Form_EXP": LimitEXP, + "Master_Form_EXP": MasterEXP, + "Final_Form_EXP": FinalEXP, + "Summon_EXP": SummonEXP, + "Schmovement": Schmovement, + "RandomGrowth": RandomGrowth, + "Promise_Charm": PromiseCharm, + "Goal": Goal, + "FinalXemnas": FinalXemnas, + "LuckyEmblemsAmount": LuckyEmblemsAmount, + "LuckyEmblemsRequired": LuckyEmblemsRequired, + "BountyAmount": BountyAmount, + "BountyRequired": BountyRequired, + "Keyblade_Minimum": KeybladeMin, + "Keyblade_Maximum": KeybladeMax, + "Visitlocking": Visitlocking, + "RandomVisitLockingItem": RandomVisitLockingItem, + "SuperBosses": SuperBosses, + "KeybladeAbilities": KeybladeAbilities, + "BlacklistKeyblade": BlacklistKeyblade, + "Cups": Cups, + +} diff --git a/worlds/kh2/Regions.py b/worlds/kh2/Regions.py new file mode 100644 index 0000000000..36fc0c046b --- /dev/null +++ b/worlds/kh2/Regions.py @@ -0,0 +1,1242 @@ +import typing + +from BaseClasses import MultiWorld, Region, Entrance + +from .Locations import KH2Location, RegionTable +from .Names import LocationName, ItemName, RegionName + + +def create_regions(world, player: int, active_locations): + menu_region = create_region(world, player, active_locations, 'Menu', None) + + goa_region_locations = [ + LocationName.Crit_1, + LocationName.Crit_2, + LocationName.Crit_3, + LocationName.Crit_4, + LocationName.Crit_5, + LocationName.Crit_6, + LocationName.Crit_7, + LocationName.GardenofAssemblageMap, + LocationName.GoALostIllusion, + LocationName.ProofofNonexistence, + LocationName.DonaldStarting1, + LocationName.DonaldStarting2, + LocationName.GoofyStarting1, + LocationName.GoofyStarting2, + ] + + goa_region = create_region(world, player, active_locations, RegionName.GoA_Region, + goa_region_locations) + + lod_Region_locations = [ + LocationName.BambooGroveDarkShard, + LocationName.BambooGroveEther, + LocationName.BambooGroveMythrilShard, + LocationName.EncampmentAreaMap, + LocationName.Mission3, + LocationName.CheckpointHiPotion, + LocationName.CheckpointMythrilShard, + LocationName.MountainTrailLightningShard, + LocationName.MountainTrailRecoveryRecipe, + LocationName.MountainTrailEther, + LocationName.MountainTrailMythrilShard, + LocationName.VillageCaveAreaMap, + LocationName.VillageCaveAPBoost, + LocationName.VillageCaveDarkShard, + LocationName.VillageCaveBonus, + LocationName.RidgeFrostShard, + LocationName.RidgeAPBoost, + LocationName.ShanYu, + LocationName.ShanYuGetBonus, + LocationName.HiddenDragon, + LocationName.GoofyShanYu, + ] + lod_Region = create_region(world, player, active_locations, RegionName.LoD_Region, + lod_Region_locations) + lod2_Region_locations = [ + LocationName.ThroneRoomTornPages, + LocationName.ThroneRoomPalaceMap, + LocationName.ThroneRoomAPBoost, + LocationName.ThroneRoomQueenRecipe, + LocationName.ThroneRoomAPBoost2, + LocationName.ThroneRoomOgreShield, + LocationName.ThroneRoomMythrilCrystal, + LocationName.ThroneRoomOrichalcum, + LocationName.StormRider, + LocationName.XigbarDataDefenseBoost, + LocationName.GoofyStormRider, + ] + lod2_Region = create_region(world, player, active_locations, RegionName.LoD2_Region, + lod2_Region_locations) + ag_region_locations = [ + LocationName.AgrabahMap, + LocationName.AgrabahDarkShard, + LocationName.AgrabahMythrilShard, + LocationName.AgrabahHiPotion, + LocationName.AgrabahAPBoost, + LocationName.AgrabahMythrilStone, + LocationName.AgrabahMythrilShard2, + LocationName.AgrabahSerenityShard, + LocationName.BazaarMythrilGem, + LocationName.BazaarPowerShard, + LocationName.BazaarHiPotion, + LocationName.BazaarAPBoost, + LocationName.BazaarMythrilShard, + LocationName.PalaceWallsSkillRing, + LocationName.PalaceWallsMythrilStone, + LocationName.CaveEntrancePowerStone, + LocationName.CaveEntranceMythrilShard, + LocationName.ValleyofStoneMythrilStone, + LocationName.ValleyofStoneAPBoost, + LocationName.ValleyofStoneMythrilShard, + LocationName.ValleyofStoneHiPotion, + LocationName.AbuEscort, + LocationName.ChasmofChallengesCaveofWondersMap, + LocationName.ChasmofChallengesAPBoost, + LocationName.TreasureRoom, + LocationName.TreasureRoomAPBoost, + LocationName.TreasureRoomSerenityGem, + LocationName.ElementalLords, + LocationName.LampCharm, + LocationName.GoofyTreasureRoom, + LocationName.DonaldAbuEscort, + ] + ag_region = create_region(world, player, active_locations, RegionName.Ag_Region, + ag_region_locations) + ag2_region_locations = [ + LocationName.RuinedChamberTornPages, + LocationName.RuinedChamberRuinsMap, + LocationName.GenieJafar, + LocationName.WishingLamp, + ] + ag2_region = create_region(world, player, active_locations, RegionName.Ag2_Region, + ag2_region_locations) + lexaeus_region_locations = [ + LocationName.LexaeusBonus, + LocationName.LexaeusASStrengthBeyondStrength, + LocationName.LexaeusDataLostIllusion, + ] + lexaeus_region = create_region(world, player, active_locations, RegionName.Lexaeus_Region, + lexaeus_region_locations) + + dc_region_locations = [ + LocationName.DCCourtyardMythrilShard, + LocationName.DCCourtyardStarRecipe, + LocationName.DCCourtyardAPBoost, + LocationName.DCCourtyardMythrilStone, + LocationName.DCCourtyardBlazingStone, + LocationName.DCCourtyardBlazingShard, + LocationName.DCCourtyardMythrilShard2, + LocationName.LibraryTornPages, + LocationName.DisneyCastleMap, + LocationName.MinnieEscort, + LocationName.MinnieEscortGetBonus, + ] + dc_region = create_region(world, player, active_locations, RegionName.Dc_Region, + dc_region_locations) + tr_region_locations = [ + LocationName.CornerstoneHillMap, + LocationName.CornerstoneHillFrostShard, + LocationName.PierMythrilShard, + LocationName.PierHiPotion, + LocationName.WaterwayMythrilStone, + LocationName.WaterwayAPBoost, + LocationName.WaterwayFrostStone, + LocationName.WindowofTimeMap, + LocationName.BoatPete, + LocationName.FuturePete, + LocationName.FuturePeteGetBonus, + LocationName.Monochrome, + LocationName.WisdomForm, + LocationName.DonaldBoatPete, + LocationName.DonaldBoatPeteGetBonus, + LocationName.GoofyFuturePete, + ] + tr_region = create_region(world, player, active_locations, RegionName.Tr_Region, + tr_region_locations) + marluxia_region_locations = [ + LocationName.MarluxiaGetBonus, + LocationName.MarluxiaASEternalBlossom, + LocationName.MarluxiaDataLostIllusion, + ] + marluxia_region = create_region(world, player, active_locations, RegionName.Marluxia_Region, + marluxia_region_locations) + terra_region_locations = [ + LocationName.LingeringWillBonus, + LocationName.LingeringWillProofofConnection, + LocationName.LingeringWillManifestIllusion, + ] + terra_region = create_region(world, player, active_locations, RegionName.Terra_Region, + terra_region_locations) + + hundred_acre1_region_locations = [ + LocationName.PoohsHouse100AcreWoodMap, + LocationName.PoohsHouseAPBoost, + LocationName.PoohsHouseMythrilStone, + ] + hundred_acre1_region = create_region(world, player, active_locations, RegionName.HundredAcre1_Region, + hundred_acre1_region_locations) + hundred_acre2_region_locations = [ + LocationName.PigletsHouseDefenseBoost, + LocationName.PigletsHouseAPBoost, + LocationName.PigletsHouseMythrilGem, + ] + hundred_acre2_region = create_region(world, player, active_locations, RegionName.HundredAcre2_Region, + hundred_acre2_region_locations) + hundred_acre3_region_locations = [ + LocationName.RabbitsHouseDrawRing, + LocationName.RabbitsHouseMythrilCrystal, + LocationName.RabbitsHouseAPBoost, + ] + hundred_acre3_region = create_region(world, player, active_locations, RegionName.HundredAcre3_Region, + hundred_acre3_region_locations) + hundred_acre4_region_locations = [ + LocationName.KangasHouseMagicBoost, + LocationName.KangasHouseAPBoost, + LocationName.KangasHouseOrichalcum, + ] + hundred_acre4_region = create_region(world, player, active_locations, RegionName.HundredAcre4_Region, + hundred_acre4_region_locations) + hundred_acre5_region_locations = [ + LocationName.SpookyCaveMythrilGem, + LocationName.SpookyCaveAPBoost, + LocationName.SpookyCaveOrichalcum, + LocationName.SpookyCaveGuardRecipe, + LocationName.SpookyCaveMythrilCrystal, + LocationName.SpookyCaveAPBoost2, + LocationName.SweetMemories, + LocationName.SpookyCaveMap, + ] + hundred_acre5_region = create_region(world, player, active_locations, RegionName.HundredAcre5_Region, + hundred_acre5_region_locations) + hundred_acre6_region_locations = [ + LocationName.StarryHillCosmicRing, + LocationName.StarryHillStyleRecipe, + LocationName.StarryHillCureElement, + LocationName.StarryHillOrichalcumPlus, + ] + hundred_acre6_region = create_region(world, player, active_locations, RegionName.HundredAcre6_Region, + hundred_acre6_region_locations) + pr_region_locations = [ + LocationName.RampartNavalMap, + LocationName.RampartMythrilStone, + LocationName.RampartDarkShard, + LocationName.TownDarkStone, + LocationName.TownAPBoost, + LocationName.TownMythrilShard, + LocationName.TownMythrilGem, + LocationName.CaveMouthBrightShard, + LocationName.CaveMouthMythrilShard, + LocationName.IsladeMuertaMap, + LocationName.BoatFight, + LocationName.InterceptorBarrels, + LocationName.PowderStoreAPBoost1, + LocationName.PowderStoreAPBoost2, + LocationName.MoonlightNookMythrilShard, + LocationName.MoonlightNookSerenityGem, + LocationName.MoonlightNookPowerStone, + LocationName.Barbossa, + LocationName.BarbossaGetBonus, + LocationName.FollowtheWind, + LocationName.DonaldBoatFight, + LocationName.GoofyBarbossa, + LocationName.GoofyBarbossaGetBonus, + LocationName.GoofyInterceptorBarrels, + ] + pr_region = create_region(world, player, active_locations, RegionName.Pr_Region, + pr_region_locations) + pr2_region_locations = [ + LocationName.GrimReaper1, + LocationName.InterceptorsHoldFeatherCharm, + LocationName.SeadriftKeepAPBoost, + LocationName.SeadriftKeepOrichalcum, + LocationName.SeadriftKeepMeteorStaff, + LocationName.SeadriftRowSerenityGem, + LocationName.SeadriftRowKingRecipe, + LocationName.SeadriftRowMythrilCrystal, + LocationName.SeadriftRowCursedMedallion, + LocationName.SeadriftRowShipGraveyardMap, + LocationName.GoofyGrimReaper1, + + ] + pr2_region = create_region(world, player, active_locations, RegionName.Pr2_Region, + pr2_region_locations) + gr2_region_locations = [ + LocationName.DonaladGrimReaper2, + LocationName.GrimReaper2, + LocationName.SecretAnsemReport6, + LocationName.LuxordDataAPBoost, + ] + gr2_region = create_region(world, player, active_locations, RegionName.Gr2_Region, + gr2_region_locations) + oc_region_locations = [ + LocationName.PassageMythrilShard, + LocationName.PassageMythrilStone, + LocationName.PassageEther, + LocationName.PassageAPBoost, + LocationName.PassageHiPotion, + LocationName.InnerChamberUnderworldMap, + LocationName.InnerChamberMythrilShard, + LocationName.Cerberus, + LocationName.ColiseumMap, + LocationName.Urns, + LocationName.UnderworldEntrancePowerBoost, + LocationName.CavernsEntranceLucidShard, + LocationName.CavernsEntranceAPBoost, + LocationName.CavernsEntranceMythrilShard, + LocationName.TheLostRoadBrightShard, + LocationName.TheLostRoadEther, + LocationName.TheLostRoadMythrilShard, + LocationName.TheLostRoadMythrilStone, + LocationName.AtriumLucidStone, + LocationName.AtriumAPBoost, + LocationName.DemyxOC, + LocationName.SecretAnsemReport5, + LocationName.OlympusStone, + LocationName.TheLockCavernsMap, + LocationName.TheLockMythrilShard, + LocationName.TheLockAPBoost, + LocationName.PeteOC, + LocationName.Hydra, + LocationName.HydraGetBonus, + LocationName.HerosCrest, + LocationName.DonaldDemyxOC, + LocationName.GoofyPeteOC, + ] + oc_region = create_region(world, player, active_locations, RegionName.Oc_Region, + oc_region_locations) + oc2_region_locations = [ + LocationName.AuronsStatue, + LocationName.Hades, + LocationName.HadesGetBonus, + LocationName.GuardianSoul, + + ] + oc2_region = create_region(world, player, active_locations, RegionName.Oc2_Region, + oc2_region_locations) + oc2_pain_and_panic_locations = [ + LocationName.ProtectBeltPainandPanicCup, + LocationName.SerenityGemPainandPanicCup, + ] + oc2_titan_locations = [ + LocationName.GenjiShieldTitanCup, + LocationName.SkillfulRingTitanCup, + ] + oc2_cerberus_locations = [ + LocationName.RisingDragonCerberusCup, + LocationName.SerenityCrystalCerberusCup, + ] + oc2_gof_cup_locations = [ + LocationName.FatalCrestGoddessofFateCup, + LocationName.OrichalcumPlusGoddessofFateCup, + LocationName.HadesCupTrophyParadoxCups, + ] + zexion_region_locations = [ + LocationName.ZexionBonus, + LocationName.ZexionASBookofShadows, + LocationName.ZexionDataLostIllusion, + LocationName.GoofyZexion, + ] + oc2_pain_and_panic_cup = create_region(world, player, active_locations, RegionName.Oc2_pain_and_panic_Region, + oc2_pain_and_panic_locations) + oc2_titan_cup = create_region(world, player, active_locations, RegionName.Oc2_titan_Region, oc2_titan_locations) + oc2_cerberus_cup = create_region(world, player, active_locations, RegionName.Oc2_cerberus_Region, + oc2_cerberus_locations) + oc2_gof_cup = create_region(world, player, active_locations, RegionName.Oc2_gof_Region, oc2_gof_cup_locations) + zexion_region = create_region(world, player, active_locations, RegionName.Zexion_Region, zexion_region_locations) + + bc_region_locations = [ + LocationName.BCCourtyardAPBoost, + LocationName.BCCourtyardHiPotion, + LocationName.BCCourtyardMythrilShard, + LocationName.BellesRoomCastleMap, + LocationName.BellesRoomMegaRecipe, + LocationName.TheEastWingMythrilShard, + LocationName.TheEastWingTent, + LocationName.TheWestHallHiPotion, + LocationName.TheWestHallPowerShard, + LocationName.TheWestHallMythrilShard2, + LocationName.TheWestHallBrightStone, + LocationName.TheWestHallMythrilShard, + LocationName.Thresholder, + LocationName.DungeonBasementMap, + LocationName.DungeonAPBoost, + LocationName.SecretPassageMythrilShard, + LocationName.SecretPassageHiPotion, + LocationName.SecretPassageLucidShard, + LocationName.TheWestHallAPBoostPostDungeon, + LocationName.TheWestWingMythrilShard, + LocationName.TheWestWingTent, + LocationName.Beast, + LocationName.TheBeastsRoomBlazingShard, + LocationName.DarkThorn, + LocationName.DarkThornGetBonus, + LocationName.DarkThornCureElement, + LocationName.DonaldThresholder, + LocationName.GoofyBeast, + ] + bc_region = create_region(world, player, active_locations, RegionName.Bc_Region, + bc_region_locations) + bc2_region_locations = [ + LocationName.RumblingRose, + LocationName.CastleWallsMap, + + ] + bc2_region = create_region(world, player, active_locations, RegionName.Bc2_Region, + bc2_region_locations) + xaldin_region_locations = [ + LocationName.Xaldin, + LocationName.XaldinGetBonus, + LocationName.DonaldXaldinGetBonus, + LocationName.SecretAnsemReport4, + LocationName.XaldinDataDefenseBoost, + ] + xaldin_region = create_region(world, player, active_locations, RegionName.Xaldin_Region, + xaldin_region_locations) + sp_region_locations = [ + LocationName.PitCellAreaMap, + LocationName.PitCellMythrilCrystal, + LocationName.CanyonDarkCrystal, + LocationName.CanyonMythrilStone, + LocationName.CanyonMythrilGem, + LocationName.CanyonFrostCrystal, + LocationName.Screens, + LocationName.HallwayPowerCrystal, + LocationName.HallwayAPBoost, + LocationName.CommunicationsRoomIOTowerMap, + LocationName.CommunicationsRoomGaiaBelt, + LocationName.HostileProgram, + LocationName.HostileProgramGetBonus, + LocationName.PhotonDebugger, + LocationName.DonaldScreens, + LocationName.GoofyHostileProgram, + + ] + sp_region = create_region(world, player, active_locations, RegionName.Sp_Region, + sp_region_locations) + sp2_region_locations = [ + LocationName.SolarSailer, + LocationName.CentralComputerCoreAPBoost, + LocationName.CentralComputerCoreOrichalcumPlus, + LocationName.CentralComputerCoreCosmicArts, + LocationName.CentralComputerCoreMap, + + LocationName.DonaldSolarSailer, + ] + + sp2_region = create_region(world, player, active_locations, RegionName.Sp2_Region, + sp2_region_locations) + mcp_region_locations = [ + LocationName.MCP, + LocationName.MCPGetBonus, + ] + mcp_region = create_region(world, player, active_locations, RegionName.Mcp_Region, + mcp_region_locations) + larxene_region_locations = [ + LocationName.LarxeneBonus, + LocationName.LarxeneASCloakedThunder, + LocationName.LarxeneDataLostIllusion, + ] + larxene_region = create_region(world, player, active_locations, RegionName.Larxene_Region, + larxene_region_locations) + ht_region_locations = [ + LocationName.GraveyardMythrilShard, + LocationName.GraveyardSerenityGem, + LocationName.FinklesteinsLabHalloweenTownMap, + LocationName.TownSquareMythrilStone, + LocationName.TownSquareEnergyShard, + LocationName.HinterlandsLightningShard, + LocationName.HinterlandsMythrilStone, + LocationName.HinterlandsAPBoost, + LocationName.CandyCaneLaneMegaPotion, + LocationName.CandyCaneLaneMythrilGem, + LocationName.CandyCaneLaneLightningStone, + LocationName.CandyCaneLaneMythrilStone, + LocationName.SantasHouseChristmasTownMap, + LocationName.SantasHouseAPBoost, + LocationName.PrisonKeeper, + LocationName.OogieBoogie, + LocationName.OogieBoogieMagnetElement, + LocationName.DonaldPrisonKeeper, + LocationName.GoofyOogieBoogie, + ] + ht_region = create_region(world, player, active_locations, RegionName.Ht_Region, + ht_region_locations) + ht2_region_locations = [ + LocationName.Lock, + LocationName.Present, + LocationName.DecoyPresents, + LocationName.Experiment, + LocationName.DecisivePumpkin, + + LocationName.DonaldExperiment, + LocationName.GoofyLock, + ] + ht2_region = create_region(world, player, active_locations, RegionName.Ht2_Region, + ht2_region_locations) + vexen_region_locations = [ + LocationName.VexenBonus, + LocationName.VexenASRoadtoDiscovery, + LocationName.VexenDataLostIllusion, + ] + vexen_region = create_region(world, player, active_locations, RegionName.Vexen_Region, + vexen_region_locations) + hb_region_locations = [ + LocationName.MarketplaceMap, + LocationName.BoroughDriveRecovery, + LocationName.BoroughAPBoost, + LocationName.BoroughHiPotion, + LocationName.BoroughMythrilShard, + LocationName.BoroughDarkShard, + LocationName.MerlinsHouseMembershipCard, + LocationName.MerlinsHouseBlizzardElement, + LocationName.Bailey, + LocationName.BaileySecretAnsemReport7, + LocationName.BaseballCharm, + ] + hb_region = create_region(world, player, active_locations, RegionName.Hb_Region, + hb_region_locations) + hb2_region_locations = [ + LocationName.PosternCastlePerimeterMap, + LocationName.PosternMythrilGem, + LocationName.PosternAPBoost, + LocationName.CorridorsMythrilStone, + LocationName.CorridorsMythrilCrystal, + LocationName.CorridorsDarkCrystal, + LocationName.CorridorsAPBoost, + LocationName.AnsemsStudyMasterForm, + LocationName.AnsemsStudySleepingLion, + LocationName.AnsemsStudySkillRecipe, + LocationName.AnsemsStudyUkuleleCharm, + LocationName.RestorationSiteMoonRecipe, + LocationName.RestorationSiteAPBoost, + LocationName.CoRDepthsAPBoost, + LocationName.CoRDepthsPowerCrystal, + LocationName.CoRDepthsFrostCrystal, + LocationName.CoRDepthsManifestIllusion, + LocationName.CoRDepthsAPBoost2, + LocationName.CoRMineshaftLowerLevelDepthsofRemembranceMap, + LocationName.CoRMineshaftLowerLevelAPBoost, + LocationName.DonaldDemyxHBGetBonus, + ] + hb2_region = create_region(world, player, active_locations, RegionName.Hb2_Region, + hb2_region_locations) + onek_region_locations = [ + LocationName.DemyxHB, + LocationName.DemyxHBGetBonus, + LocationName.FFFightsCureElement, + LocationName.CrystalFissureTornPages, + LocationName.CrystalFissureTheGreatMawMap, + LocationName.CrystalFissureEnergyCrystal, + LocationName.CrystalFissureAPBoost, + LocationName.ThousandHeartless, + LocationName.ThousandHeartlessSecretAnsemReport1, + LocationName.ThousandHeartlessIceCream, + LocationName.ThousandHeartlessPicture, + LocationName.PosternGullWing, + LocationName.HeartlessManufactoryCosmicChain, + LocationName.DemyxDataAPBoost, + ] + onek_region = create_region(world, player, active_locations, RegionName.ThousandHeartless_Region, + onek_region_locations) + mushroom_region_locations = [ + LocationName.WinnersProof, + LocationName.ProofofPeace, + ] + mushroom_region = create_region(world, player, active_locations, RegionName.Mushroom13_Region, + mushroom_region_locations) + sephi_region_locations = [ + LocationName.SephirothBonus, + LocationName.SephirothFenrir, + ] + sephi_region = create_region(world, player, active_locations, RegionName.Sephi_Region, + sephi_region_locations) + + cor_region_locations = [ + LocationName.CoRDepthsUpperLevelRemembranceGem, + LocationName.CoRMiningAreaSerenityGem, + LocationName.CoRMiningAreaAPBoost, + LocationName.CoRMiningAreaSerenityCrystal, + LocationName.CoRMiningAreaManifestIllusion, + LocationName.CoRMiningAreaSerenityGem2, + LocationName.CoRMiningAreaDarkRemembranceMap, + LocationName.CoRMineshaftMidLevelPowerBoost, + LocationName.CoREngineChamberSerenityCrystal, + LocationName.CoREngineChamberRemembranceCrystal, + LocationName.CoREngineChamberAPBoost, + LocationName.CoREngineChamberManifestIllusion, + LocationName.CoRMineshaftUpperLevelMagicBoost, + ] + cor_region = create_region(world, player, active_locations, RegionName.CoR_Region, + cor_region_locations) + transport_region_locations = [ + LocationName.CoRMineshaftUpperLevelAPBoost, + LocationName.TransporttoRemembrance, + ] + transport_region = create_region(world, player, active_locations, RegionName.Transport_Region, + transport_region_locations) + pl_region_locations = [ + LocationName.GorgeSavannahMap, + LocationName.GorgeDarkGem, + LocationName.GorgeMythrilStone, + LocationName.ElephantGraveyardFrostGem, + LocationName.ElephantGraveyardMythrilStone, + LocationName.ElephantGraveyardBrightStone, + LocationName.ElephantGraveyardAPBoost, + LocationName.ElephantGraveyardMythrilShard, + LocationName.PrideRockMap, + LocationName.PrideRockMythrilStone, + LocationName.PrideRockSerenityCrystal, + LocationName.WildebeestValleyEnergyStone, + LocationName.WildebeestValleyAPBoost, + LocationName.WildebeestValleyMythrilGem, + LocationName.WildebeestValleyMythrilStone, + LocationName.WildebeestValleyLucidGem, + LocationName.WastelandsMythrilShard, + LocationName.WastelandsSerenityGem, + LocationName.WastelandsMythrilStone, + LocationName.JungleSerenityGem, + LocationName.JungleMythrilStone, + LocationName.JungleSerenityCrystal, + LocationName.OasisMap, + LocationName.OasisTornPages, + LocationName.OasisAPBoost, + LocationName.CircleofLife, + LocationName.Hyenas1, + LocationName.Scar, + LocationName.ScarFireElement, + LocationName.DonaldScar, + LocationName.GoofyHyenas1, + + ] + pl_region = create_region(world, player, active_locations, RegionName.Pl_Region, + pl_region_locations) + pl2_region_locations = [ + LocationName.Hyenas2, + LocationName.Groundshaker, + LocationName.GroundshakerGetBonus, + LocationName.SaixDataDefenseBoost, + LocationName.GoofyHyenas2, + ] + pl2_region = create_region(world, player, active_locations, RegionName.Pl2_Region, + pl2_region_locations) + + stt_region_locations = [ + LocationName.TwilightTownMap, + LocationName.MunnyPouchOlette, + LocationName.StationDusks, + LocationName.StationofSerenityPotion, + LocationName.StationofCallingPotion, + LocationName.TwilightThorn, + LocationName.Axel1, + LocationName.JunkChampionBelt, + LocationName.JunkMedal, + LocationName.TheStruggleTrophy, + LocationName.CentralStationPotion1, + LocationName.STTCentralStationHiPotion, + LocationName.CentralStationPotion2, + LocationName.SunsetTerraceAbilityRing, + LocationName.SunsetTerraceHiPotion, + LocationName.SunsetTerracePotion1, + LocationName.SunsetTerracePotion2, + LocationName.MansionFoyerHiPotion, + LocationName.MansionFoyerPotion1, + LocationName.MansionFoyerPotion2, + LocationName.MansionDiningRoomElvenBandanna, + LocationName.MansionDiningRoomPotion, + LocationName.NaminesSketches, + LocationName.MansionMap, + LocationName.MansionLibraryHiPotion, + LocationName.Axel2, + LocationName.MansionBasementCorridorHiPotion, + LocationName.RoxasDataMagicBoost, + ] + stt_region = create_region(world, player, active_locations, RegionName.STT_Region, + stt_region_locations) + + tt_region_locations = [ + LocationName.OldMansionPotion, + LocationName.OldMansionMythrilShard, + LocationName.TheWoodsPotion, + LocationName.TheWoodsMythrilShard, + LocationName.TheWoodsHiPotion, + LocationName.TramCommonHiPotion, + LocationName.TramCommonAPBoost, + LocationName.TramCommonTent, + LocationName.TramCommonMythrilShard1, + LocationName.TramCommonPotion1, + LocationName.TramCommonMythrilShard2, + LocationName.TramCommonPotion2, + LocationName.StationPlazaSecretAnsemReport2, + LocationName.MunnyPouchMickey, + LocationName.CrystalOrb, + LocationName.CentralStationTent, + LocationName.TTCentralStationHiPotion, + LocationName.CentralStationMythrilShard, + LocationName.TheTowerPotion, + LocationName.TheTowerHiPotion, + LocationName.TheTowerEther, + LocationName.TowerEntrywayEther, + LocationName.TowerEntrywayMythrilShard, + LocationName.SorcerersLoftTowerMap, + LocationName.TowerWardrobeMythrilStone, + LocationName.StarSeeker, + LocationName.ValorForm, + ] + tt_region = create_region(world, player, active_locations, RegionName.TT_Region, + tt_region_locations) + tt2_region_locations = [ + LocationName.SeifersTrophy, + LocationName.Oathkeeper, + LocationName.LimitForm, + ] + tt2_region = create_region(world, player, active_locations, RegionName.TT2_Region, + tt2_region_locations) + tt3_region_locations = [ + LocationName.UndergroundConcourseMythrilGem, + LocationName.UndergroundConcourseAPBoost, + LocationName.UndergroundConcourseMythrilCrystal, + LocationName.UndergroundConcourseOrichalcum, + LocationName.TunnelwayOrichalcum, + LocationName.TunnelwayMythrilCrystal, + LocationName.SunsetTerraceOrichalcumPlus, + LocationName.SunsetTerraceMythrilShard, + LocationName.SunsetTerraceMythrilCrystal, + LocationName.SunsetTerraceAPBoost, + LocationName.MansionNobodies, + LocationName.MansionFoyerMythrilCrystal, + LocationName.MansionFoyerMythrilStone, + LocationName.MansionFoyerSerenityCrystal, + LocationName.MansionDiningRoomMythrilCrystal, + LocationName.MansionDiningRoomMythrilStone, + LocationName.MansionLibraryOrichalcum, + LocationName.BeamSecretAnsemReport10, + LocationName.MansionBasementCorridorUltimateRecipe, + LocationName.BetwixtandBetween, + LocationName.BetwixtandBetweenBondofFlame, + LocationName.AxelDataMagicBoost, + LocationName.DonaldMansionNobodies, + ] + tt3_region = create_region(world, player, active_locations, RegionName.TT3_Region, + tt3_region_locations) + + twtnw_region_locations = [ + LocationName.FragmentCrossingMythrilStone, + LocationName.FragmentCrossingMythrilCrystal, + LocationName.FragmentCrossingAPBoost, + LocationName.FragmentCrossingOrichalcum, + ] + + twtnw_region = create_region(world, player, active_locations, RegionName.Twtnw_Region, + twtnw_region_locations) + twtnw_postroxas_region_locations = [ + LocationName.Roxas, + LocationName.RoxasGetBonus, + LocationName.RoxasSecretAnsemReport8, + LocationName.TwoBecomeOne, + LocationName.MemorysSkyscaperMythrilCrystal, + LocationName.MemorysSkyscaperAPBoost, + LocationName.MemorysSkyscaperMythrilStone, + LocationName.TheBrinkofDespairDarkCityMap, + LocationName.TheBrinkofDespairOrichalcumPlus, + LocationName.NothingsCallMythrilGem, + LocationName.NothingsCallOrichalcum, + LocationName.TwilightsViewCosmicBelt, + + ] + twtnw_postroxas_region = create_region(world, player, active_locations, RegionName.Twtnw_PostRoxas, + twtnw_postroxas_region_locations) + twtnw_postxigbar_region_locations = [ + LocationName.XigbarBonus, + LocationName.XigbarSecretAnsemReport3, + LocationName.NaughtsSkywayMythrilGem, + LocationName.NaughtsSkywayOrichalcum, + LocationName.NaughtsSkywayMythrilCrystal, + LocationName.Oblivion, + LocationName.CastleThatNeverWasMap, + LocationName.Luxord, + LocationName.LuxordGetBonus, + LocationName.LuxordSecretAnsemReport9, + ] + twtnw_postxigbar_region = create_region(world, player, active_locations, RegionName.Twtnw_PostXigbar, + twtnw_postxigbar_region_locations) + twtnw2_region_locations = [ + LocationName.SaixBonus, + LocationName.SaixSecretAnsemReport12, + LocationName.PreXemnas1SecretAnsemReport11, + LocationName.RuinandCreationsPassageMythrilStone, + LocationName.RuinandCreationsPassageAPBoost, + LocationName.RuinandCreationsPassageMythrilCrystal, + LocationName.RuinandCreationsPassageOrichalcum, + LocationName.Xemnas1, + LocationName.Xemnas1GetBonus, + LocationName.Xemnas1SecretAnsemReport13, + LocationName.FinalXemnas, + LocationName.XemnasDataPowerBoost, + ] + twtnw2_region = create_region(world, player, active_locations, RegionName.Twtnw2_Region, + twtnw2_region_locations) + + valor_region_locations = [ + LocationName.Valorlvl2, + LocationName.Valorlvl3, + LocationName.Valorlvl4, + LocationName.Valorlvl5, + LocationName.Valorlvl6, + LocationName.Valorlvl7, + ] + valor_region = create_region(world, player, active_locations, RegionName.Valor_Region, + valor_region_locations) + wisdom_region_locations = [ + LocationName.Wisdomlvl2, + LocationName.Wisdomlvl3, + LocationName.Wisdomlvl4, + LocationName.Wisdomlvl5, + LocationName.Wisdomlvl6, + LocationName.Wisdomlvl7, + ] + wisdom_region = create_region(world, player, active_locations, RegionName.Wisdom_Region, + wisdom_region_locations) + limit_region_locations = [ + LocationName.Limitlvl2, + LocationName.Limitlvl3, + LocationName.Limitlvl4, + LocationName.Limitlvl5, + LocationName.Limitlvl6, + LocationName.Limitlvl7, + ] + limit_region = create_region(world, player, active_locations, RegionName.Limit_Region, + limit_region_locations) + master_region_locations = [ + LocationName.Masterlvl2, + LocationName.Masterlvl3, + LocationName.Masterlvl4, + LocationName.Masterlvl5, + LocationName.Masterlvl6, + LocationName.Masterlvl7, + ] + master_region = create_region(world, player, active_locations, RegionName.Master_Region, + master_region_locations) + final_region_locations = [ + LocationName.Finallvl2, + LocationName.Finallvl3, + LocationName.Finallvl4, + LocationName.Finallvl5, + LocationName.Finallvl6, + LocationName.Finallvl7, + ] + final_region = create_region(world, player, active_locations, RegionName.Final_Region, + final_region_locations) + keyblade_region_locations = [ + LocationName.FAKESlot, + LocationName.DetectionSaberSlot, + LocationName.EdgeofUltimaSlot, + LocationName.KingdomKeySlot, + LocationName.OathkeeperSlot, + LocationName.OblivionSlot, + LocationName.StarSeekerSlot, + LocationName.HiddenDragonSlot, + LocationName.HerosCrestSlot, + LocationName.MonochromeSlot, + LocationName.FollowtheWindSlot, + LocationName.CircleofLifeSlot, + LocationName.PhotonDebuggerSlot, + LocationName.GullWingSlot, + LocationName.RumblingRoseSlot, + LocationName.GuardianSoulSlot, + LocationName.WishingLampSlot, + LocationName.DecisivePumpkinSlot, + LocationName.SweetMemoriesSlot, + LocationName.MysteriousAbyssSlot, + LocationName.SleepingLionSlot, + LocationName.BondofFlameSlot, + LocationName.TwoBecomeOneSlot, + LocationName.FatalCrestSlot, + LocationName.FenrirSlot, + LocationName.UltimaWeaponSlot, + LocationName.WinnersProofSlot, + LocationName.PurebloodSlot, + LocationName.Centurion2, + LocationName.CometStaff, + LocationName.HammerStaff, + LocationName.LordsBroom, + LocationName.MagesStaff, + LocationName.MeteorStaff, + LocationName.NobodyLance, + LocationName.PreciousMushroom, + LocationName.PreciousMushroom2, + LocationName.PremiumMushroom, + LocationName.RisingDragon, + LocationName.SaveTheQueen2, + LocationName.ShamansRelic, + LocationName.VictoryBell, + LocationName.WisdomWand, + + LocationName.AdamantShield, + LocationName.AkashicRecord, + LocationName.ChainGear, + LocationName.DreamCloud, + LocationName.FallingStar, + LocationName.FrozenPride2, + LocationName.GenjiShield, + LocationName.KnightDefender, + LocationName.KnightsShield, + LocationName.MajesticMushroom, + LocationName.MajesticMushroom2, + LocationName.NobodyGuard, + LocationName.OgreShield, + LocationName.SaveTheKing2, + LocationName.UltimateMushroom, + ] + keyblade_region = create_region(world, player, active_locations, RegionName.Keyblade_Region, + keyblade_region_locations) + + world.regions += [ + lod_Region, + lod2_Region, + ag_region, + ag2_region, + lexaeus_region, + dc_region, + tr_region, + terra_region, + marluxia_region, + hundred_acre1_region, + hundred_acre2_region, + hundred_acre3_region, + hundred_acre4_region, + hundred_acre5_region, + hundred_acre6_region, + pr_region, + pr2_region, + gr2_region, + oc_region, + oc2_region, + oc2_pain_and_panic_cup, + oc2_titan_cup, + oc2_cerberus_cup, + oc2_gof_cup, + zexion_region, + bc_region, + bc2_region, + xaldin_region, + sp_region, + sp2_region, + mcp_region, + larxene_region, + ht_region, + ht2_region, + vexen_region, + hb_region, + hb2_region, + onek_region, + mushroom_region, + sephi_region, + cor_region, + transport_region, + pl_region, + pl2_region, + stt_region, + tt_region, + tt2_region, + tt3_region, + twtnw_region, + twtnw_postroxas_region, + twtnw_postxigbar_region, + twtnw2_region, + goa_region, + menu_region, + valor_region, + wisdom_region, + limit_region, + master_region, + final_region, + keyblade_region, + ] + # Level region depends on level depth. + # for every 5 levels there should be +3 visit locking + levelVL1 = [] + levelVL3 = [] + levelVL6 = [] + levelVL9 = [] + levelVL12 = [] + levelVL15 = [] + levelVL18 = [] + levelVL21 = [] + levelVL24 = [] + levelVL26 = [] + # level 50 + if world.LevelDepth[player] == "level_50": + levelVL1 = [LocationName.Lvl2, LocationName.Lvl4, LocationName.Lvl7, LocationName.Lvl9, LocationName.Lvl10] + levelVL3 = [LocationName.Lvl12, LocationName.Lvl14, LocationName.Lvl15, LocationName.Lvl17, + LocationName.Lvl20, ] + levelVL6 = [LocationName.Lvl23, LocationName.Lvl25, LocationName.Lvl28, LocationName.Lvl30] + levelVL9 = [LocationName.Lvl32, LocationName.Lvl34, LocationName.Lvl36, LocationName.Lvl39, LocationName.Lvl41] + levelVL12 = [LocationName.Lvl44, LocationName.Lvl46, LocationName.Lvl48] + levelVL15 = [LocationName.Lvl50] + # level 99 + elif world.LevelDepth[player] == "level_99": + levelVL1 = [LocationName.Lvl7, LocationName.Lvl9, ] + levelVL3 = [LocationName.Lvl12, LocationName.Lvl15, LocationName.Lvl17, LocationName.Lvl20] + levelVL6 = [LocationName.Lvl23, LocationName.Lvl25, LocationName.Lvl28] + levelVL9 = [LocationName.Lvl31, LocationName.Lvl33, LocationName.Lvl36, LocationName.Lvl39] + levelVL12 = [LocationName.Lvl41, LocationName.Lvl44, LocationName.Lvl47, LocationName.Lvl49] + levelVL15 = [LocationName.Lvl53, LocationName.Lvl59] + levelVL18 = [LocationName.Lvl65] + levelVL21 = [LocationName.Lvl73] + levelVL24 = [LocationName.Lvl85] + levelVL26 = [LocationName.Lvl99] + # level sanity + # has to be [] instead of {} for in + elif world.LevelDepth[player] in ["level_50_sanity", "level_99_sanity"]: + levelVL1 = [LocationName.Lvl2, LocationName.Lvl3, LocationName.Lvl4, LocationName.Lvl5, LocationName.Lvl6, + LocationName.Lvl7, LocationName.Lvl8, LocationName.Lvl9, LocationName.Lvl10] + levelVL3 = [LocationName.Lvl11, LocationName.Lvl12, LocationName.Lvl13, LocationName.Lvl14, LocationName.Lvl15, + LocationName.Lvl16, LocationName.Lvl17, LocationName.Lvl18, LocationName.Lvl19, LocationName.Lvl20] + levelVL6 = [LocationName.Lvl21, LocationName.Lvl22, LocationName.Lvl23, LocationName.Lvl24, LocationName.Lvl25, + LocationName.Lvl26, LocationName.Lvl27, LocationName.Lvl28, LocationName.Lvl29, LocationName.Lvl30] + levelVL9 = [LocationName.Lvl31, LocationName.Lvl32, LocationName.Lvl33, LocationName.Lvl34, LocationName.Lvl35, + LocationName.Lvl36, LocationName.Lvl37, LocationName.Lvl38, LocationName.Lvl39, LocationName.Lvl40] + levelVL12 = [LocationName.Lvl41, LocationName.Lvl42, LocationName.Lvl43, LocationName.Lvl44, LocationName.Lvl45, + LocationName.Lvl46, LocationName.Lvl47, LocationName.Lvl48, LocationName.Lvl49, LocationName.Lvl50] + # level 99 sanity + if world.LevelDepth[player] == "level_99_sanity": + levelVL15 = [LocationName.Lvl51, LocationName.Lvl52, LocationName.Lvl53, LocationName.Lvl54, + LocationName.Lvl55, LocationName.Lvl56, LocationName.Lvl57, LocationName.Lvl58, + LocationName.Lvl59, LocationName.Lvl60] + levelVL18 = [LocationName.Lvl61, LocationName.Lvl62, LocationName.Lvl63, LocationName.Lvl64, + LocationName.Lvl65, LocationName.Lvl66, LocationName.Lvl67, LocationName.Lvl68, + LocationName.Lvl69, LocationName.Lvl70] + levelVL21 = [LocationName.Lvl71, LocationName.Lvl72, LocationName.Lvl73, LocationName.Lvl74, + LocationName.Lvl75, LocationName.Lvl76, LocationName.Lvl77, LocationName.Lvl78, + LocationName.Lvl79, LocationName.Lvl80] + levelVL24 = [LocationName.Lvl81, LocationName.Lvl82, LocationName.Lvl83, LocationName.Lvl84, + LocationName.Lvl85, LocationName.Lvl86, LocationName.Lvl87, LocationName.Lvl88, + LocationName.Lvl89, LocationName.Lvl90] + levelVL26 = [LocationName.Lvl91, LocationName.Lvl92, LocationName.Lvl93, LocationName.Lvl94, + LocationName.Lvl95, LocationName.Lvl96, LocationName.Lvl97, LocationName.Lvl98, + LocationName.Lvl99] + + level_regionVL1 = create_region(world, player, active_locations, RegionName.LevelsVS1, + levelVL1) + level_regionVL3 = create_region(world, player, active_locations, RegionName.LevelsVS3, + levelVL3) + level_regionVL6 = create_region(world, player, active_locations, RegionName.LevelsVS6, + levelVL6) + level_regionVL9 = create_region(world, player, active_locations, RegionName.LevelsVS9, + levelVL9) + level_regionVL12 = create_region(world, player, active_locations, RegionName.LevelsVS12, + levelVL12) + level_regionVL15 = create_region(world, player, active_locations, RegionName.LevelsVS15, + levelVL15) + level_regionVL18 = create_region(world, player, active_locations, RegionName.LevelsVS18, + levelVL18) + level_regionVL21 = create_region(world, player, active_locations, RegionName.LevelsVS21, + levelVL21) + level_regionVL24 = create_region(world, player, active_locations, RegionName.LevelsVS24, + levelVL24) + level_regionVL26 = create_region(world, player, active_locations, RegionName.LevelsVS26, + levelVL26) + world.regions += [level_regionVL1, level_regionVL3, level_regionVL6, level_regionVL9, level_regionVL12, + level_regionVL15, level_regionVL18, level_regionVL21, level_regionVL24, level_regionVL26] + + +def connect_regions(world: MultiWorld, player: int): + # connecting every first visit to the GoA + + names: typing.Dict[str, int] = {} + + connect(world, player, names, "Menu", RegionName.Keyblade_Region) + connect(world, player, names, "Menu", RegionName.GoA_Region) + + connect(world, player, names, RegionName.GoA_Region, RegionName.LoD_Region, + lambda state: state.kh_lod_unlocked(player, 1)) + connect(world, player, names, RegionName.LoD_Region, RegionName.LoD2_Region, + lambda state: state.kh_lod_unlocked(player, 2)) + + connect(world, player, names, RegionName.GoA_Region, RegionName.Oc_Region, + lambda state: state.kh_oc_unlocked(player, 1)) + connect(world, player, names, RegionName.Oc_Region, RegionName.Oc2_Region, + lambda state: state.kh_oc_unlocked(player, 2)) + connect(world, player, names, RegionName.Oc2_Region, RegionName.Zexion_Region, + lambda state: state.kh_datazexion(player)) + + connect(world, player, names, RegionName.Oc2_Region, RegionName.Oc2_pain_and_panic_Region, + lambda state: state.kh_painandpanic(player)) + connect(world, player, names, RegionName.Oc2_Region, RegionName.Oc2_cerberus_Region, + lambda state: state.kh_cerberuscup(player)) + connect(world, player, names, RegionName.Oc2_Region, RegionName.Oc2_titan_Region, + lambda state: state.kh_titan(player)) + connect(world, player, names, RegionName.Oc2_Region, RegionName.Oc2_gof_Region, + lambda state: state.kh_gof(player)) + + connect(world, player, names, RegionName.GoA_Region, RegionName.Ag_Region, + lambda state: state.kh_ag_unlocked(player, 1)) + connect(world, player, names, RegionName.Ag_Region, RegionName.Ag2_Region, + lambda state: state.kh_ag_unlocked(player, 2) + and (state.has(ItemName.FireElement, player) + and state.has(ItemName.BlizzardElement, player) + and state.has(ItemName.ThunderElement, player))) + connect(world, player, names, RegionName.Ag2_Region, RegionName.Lexaeus_Region, + lambda state: state.kh_datalexaeus(player)) + + connect(world, player, names, RegionName.GoA_Region, RegionName.Dc_Region, + lambda state: state.kh_dc_unlocked(player, 1)) + connect(world, player, names, RegionName.Dc_Region, RegionName.Tr_Region, + lambda state: state.kh_dc_unlocked(player, 2)) + connect(world, player, names, RegionName.Tr_Region, RegionName.Marluxia_Region, + lambda state: state.kh_datamarluxia(player)) + connect(world, player, names, RegionName.Tr_Region, RegionName.Terra_Region, lambda state: state.kh_terra(player)) + + connect(world, player, names, RegionName.GoA_Region, RegionName.Pr_Region, + lambda state: state.kh_pr_unlocked(player, 1)) + connect(world, player, names, RegionName.Pr_Region, RegionName.Pr2_Region, + lambda state: state.kh_pr_unlocked(player, 2)) + connect(world, player, names, RegionName.Pr2_Region, RegionName.Gr2_Region, + lambda state: state.kh_gr2(player)) + + connect(world, player, names, RegionName.GoA_Region, RegionName.Bc_Region, + lambda state: state.kh_bc_unlocked(player, 1)) + connect(world, player, names, RegionName.Bc_Region, RegionName.Bc2_Region, + lambda state: state.kh_bc_unlocked(player, 2)) + connect(world, player, names, RegionName.Bc2_Region, RegionName.Xaldin_Region, + lambda state: state.kh_xaldin(player)) + + connect(world, player, names, RegionName.GoA_Region, RegionName.Sp_Region, + lambda state: state.kh_sp_unlocked(player, 1)) + connect(world, player, names, RegionName.Sp_Region, RegionName.Sp2_Region, + lambda state: state.kh_sp_unlocked(player, 2)) + connect(world, player, names, RegionName.Sp2_Region, RegionName.Mcp_Region, + lambda state: state.kh_mcp(player)) + connect(world, player, names, RegionName.Mcp_Region, RegionName.Larxene_Region, + lambda state: state.kh_datalarxene(player)) + + connect(world, player, names, RegionName.GoA_Region, RegionName.Ht_Region, + lambda state: state.kh_ht_unlocked(player, 1)) + connect(world, player, names, RegionName.Ht_Region, RegionName.Ht2_Region, + lambda state: state.kh_ht_unlocked(player, 2)) + connect(world, player, names, RegionName.Ht2_Region, RegionName.Vexen_Region, + lambda state: state.kh_datavexen(player)) + + connect(world, player, names, RegionName.GoA_Region, RegionName.Hb_Region, + lambda state: state.kh_hb_unlocked(player, 1)) + connect(world, player, names, RegionName.Hb_Region, RegionName.Hb2_Region, + lambda state: state.kh_hb_unlocked(player, 2)) + connect(world, player, names, RegionName.Hb2_Region, RegionName.ThousandHeartless_Region, + lambda state: state.kh_onek(player)) + connect(world, player, names, RegionName.ThousandHeartless_Region, RegionName.Mushroom13_Region, + lambda state: state.has(ItemName.ProofofPeace, player)) + connect(world, player, names, RegionName.ThousandHeartless_Region, RegionName.Sephi_Region, + lambda state: state.kh_sephi(player)) + + connect(world, player, names, RegionName.Hb2_Region, RegionName.CoR_Region, lambda state: state.kh_cor(player)) + connect(world, player, names, RegionName.CoR_Region, RegionName.Transport_Region, lambda state: + state.has(ItemName.HighJump, player, 3) + and state.has(ItemName.AerialDodge, player, 3) + and state.has(ItemName.Glide, player, 3)) + + connect(world, player, names, RegionName.GoA_Region, RegionName.Pl_Region, + lambda state: state.kh_pl_unlocked(player, 1)) + connect(world, player, names, RegionName.Pl_Region, RegionName.Pl2_Region, + lambda state: state.kh_pl_unlocked(player, 2) and ( + state.has(ItemName.BerserkCharge, player) or state.kh_reflect(player))) + + connect(world, player, names, RegionName.GoA_Region, RegionName.STT_Region, + lambda state: state.kh_stt_unlocked(player, 1)) + + connect(world, player, names, RegionName.GoA_Region, RegionName.TT_Region, + lambda state: state.kh_tt_unlocked(player, 1)) + connect(world, player, names, RegionName.TT_Region, RegionName.TT2_Region, + lambda state: state.kh_tt_unlocked(player, 2)) + connect(world, player, names, RegionName.TT2_Region, RegionName.TT3_Region, + lambda state: state.kh_tt_unlocked(player, 3)) + + connect(world, player, names, RegionName.GoA_Region, RegionName.Twtnw_Region, + lambda state: state.kh_twtnw_unlocked(player, 0)) + connect(world, player, names, RegionName.Twtnw_Region, RegionName.Twtnw_PostRoxas, + lambda state: state.kh_roxastools(player)) + connect(world, player, names, RegionName.Twtnw_PostRoxas, RegionName.Twtnw_PostXigbar, + lambda state: state.kh_basetools(player) and (state.kh_donaldlimit(player) or ( + state.has(ItemName.FinalForm, player) and state.has(ItemName.FireElement, player)))) + connect(world, player, names, RegionName.Twtnw_PostRoxas, RegionName.Twtnw2_Region, + lambda state: state.kh_twtnw_unlocked(player, 1)) + + hundredacrevisits = {RegionName.HundredAcre1_Region: 0, RegionName.HundredAcre2_Region: 1, + RegionName.HundredAcre3_Region: 2, + RegionName.HundredAcre4_Region: 3, RegionName.HundredAcre5_Region: 4, + RegionName.HundredAcre6_Region: 5} + for visit, tornpage in hundredacrevisits.items(): + connect(world, player, names, RegionName.GoA_Region, visit, + lambda state: (state.has(ItemName.TornPages, player, tornpage))) + + connect(world, player, names, RegionName.GoA_Region, RegionName.LevelsVS1, + lambda state: state.kh_visit_locking_amount(player, 1)) + connect(world, player, names, RegionName.LevelsVS1, RegionName.LevelsVS3, + lambda state: state.kh_visit_locking_amount(player, 3)) + connect(world, player, names, RegionName.LevelsVS3, RegionName.LevelsVS6, + lambda state: state.kh_visit_locking_amount(player, 6)) + connect(world, player, names, RegionName.LevelsVS6, RegionName.LevelsVS9, + lambda state: state.kh_visit_locking_amount(player, 9)) + connect(world, player, names, RegionName.LevelsVS9, RegionName.LevelsVS12, + lambda state: state.kh_visit_locking_amount(player, 12)) + connect(world, player, names, RegionName.LevelsVS12, RegionName.LevelsVS15, + lambda state: state.kh_visit_locking_amount(player, 15)) + connect(world, player, names, RegionName.LevelsVS15, RegionName.LevelsVS18, + lambda state: state.kh_visit_locking_amount(player, 18)) + connect(world, player, names, RegionName.LevelsVS18, RegionName.LevelsVS21, + lambda state: state.kh_visit_locking_amount(player, 21)) + connect(world, player, names, RegionName.LevelsVS21, RegionName.LevelsVS24, + lambda state: state.kh_visit_locking_amount(player, 24)) + connect(world, player, names, RegionName.LevelsVS24, RegionName.LevelsVS26, + lambda state: state.kh_visit_locking_amount(player, 25)) # 25 because of goa twtnw bugs with visit locking. + + for region in RegionTable["ValorRegion"]: + connect(world, player, names, region, RegionName.Valor_Region, + lambda state: state.has(ItemName.ValorForm, player)) + for region in RegionTable["WisdomRegion"]: + connect(world, player, names, region, RegionName.Wisdom_Region, + lambda state: state.has(ItemName.WisdomForm, player)) + for region in RegionTable["LimitRegion"]: + connect(world, player, names, region, RegionName.Limit_Region, + lambda state: state.has(ItemName.LimitForm, player)) + for region in RegionTable["MasterRegion"]: + connect(world, player, names, region, RegionName.Master_Region, + lambda state: state.has(ItemName.MasterForm, player) and state.has(ItemName.DriveConverter, player)) + for region in RegionTable["FinalRegion"]: + connect(world, player, names, region, RegionName.Final_Region, + lambda state: state.has(ItemName.FinalForm, player)) + + +# shamelessly stolen from the sa2b +def connect(world: MultiWorld, player: int, used_names: typing.Dict[str, int], source: str, target: str, + rule: typing.Optional[typing.Callable] = None): + source_region = world.get_region(source, player) + target_region = world.get_region(target, player) + + if target not in used_names: + used_names[target] = 1 + name = target + else: + used_names[target] += 1 + name = target + (' ' * used_names[target]) + + connection = Entrance(player, name, source_region) + + if rule: + connection.access_rule = rule + + source_region.exits.append(connection) + connection.connect(target_region) + + +def create_region(world: MultiWorld, player: int, active_locations, name: str, locations=None): + ret = Region(name, player, world) + if locations: + for location in locations: + loc_id = active_locations.get(location, 0) + if loc_id: + location = KH2Location(player, location, loc_id.code, ret) + ret.locations.append(location) + + return ret diff --git a/worlds/kh2/Rules.py b/worlds/kh2/Rules.py new file mode 100644 index 0000000000..b86ae4a2db --- /dev/null +++ b/worlds/kh2/Rules.py @@ -0,0 +1,96 @@ + +from BaseClasses import MultiWorld + +from .Items import exclusionItem_table +from .Locations import STT_Checks, exclusion_table +from .Names import LocationName, ItemName +from ..generic.Rules import add_rule, forbid_items, set_rule + + +def set_rules(world: MultiWorld, player: int): + + add_rule(world.get_location(LocationName.RoxasDataMagicBoost, player), + lambda state: state.kh_dataroxas(player)) + add_rule(world.get_location(LocationName.DemyxDataAPBoost, player), + lambda state: state.kh_datademyx(player)) + add_rule(world.get_location(LocationName.SaixDataDefenseBoost, player), + lambda state: state.kh_datasaix(player)) + add_rule(world.get_location(LocationName.XaldinDataDefenseBoost, player), + lambda state: state.kh_dataxaldin(player)) + add_rule(world.get_location(LocationName.XemnasDataPowerBoost, player), + lambda state: state.kh_dataxemnas(player)) + add_rule(world.get_location(LocationName.XigbarDataDefenseBoost, player), + lambda state: state.kh_dataxigbar(player)) + add_rule(world.get_location(LocationName.VexenDataLostIllusion, player), + lambda state: state.kh_dataaxel(player)) + add_rule(world.get_location(LocationName.LuxordDataAPBoost, player), + lambda state: state.kh_dataluxord(player)) + + for slot, weapon in exclusion_table["WeaponSlots"].items(): + add_rule(world.get_location(slot, player), lambda state: state.has(weapon, player)) + formLogicTable = { + ItemName.ValorForm: [LocationName.Valorlvl4, + LocationName.Valorlvl5, + LocationName.Valorlvl6, + LocationName.Valorlvl7], + ItemName.WisdomForm: [LocationName.Wisdomlvl4, + LocationName.Wisdomlvl5, + LocationName.Wisdomlvl6, + LocationName.Wisdomlvl7], + ItemName.LimitForm: [LocationName.Limitlvl4, + LocationName.Limitlvl5, + LocationName.Limitlvl6, + LocationName.Limitlvl7], + ItemName.MasterForm: [LocationName.Masterlvl4, + LocationName.Masterlvl5, + LocationName.Masterlvl6, + LocationName.Masterlvl7], + ItemName.FinalForm: [LocationName.Finallvl4, + LocationName.Finallvl5, + LocationName.Finallvl6, + LocationName.Finallvl7] + } + + for form in formLogicTable: + for i in range(4): + location = world.get_location(formLogicTable[form][i], player) + set_rule(location, lambda state, i=i + 1, form=form: state.kh_amount_of_forms(player, i, form)) + + if world.Goal[player] == "three_proofs": + add_rule(world.get_location(LocationName.FinalXemnas, player), + lambda state: state.kh_three_proof_unlocked(player)) + if world.FinalXemnas[player]: + world.completion_condition[player] = lambda state: state.kh_victory(player) + else: + world.completion_condition[player] = lambda state: state.kh_three_proof_unlocked(player) + # lucky emblem hunt + elif world.Goal[player] == "lucky_emblem_hunt": + add_rule(world.get_location(LocationName.FinalXemnas, player), + lambda state: state.kh_lucky_emblem_unlocked(player, world.LuckyEmblemsRequired[player].value)) + if world.FinalXemnas[player]: + world.completion_condition[player] = lambda state: state.kh_victory(player) + else: + world.completion_condition[player] = lambda state: state.kh_lucky_emblem_unlocked(player, world.LuckyEmblemsRequired[player].value) + # hitlist if == 2 + else: + add_rule(world.get_location(LocationName.FinalXemnas, player), + lambda state: state.kh_hitlist(player, world.BountyRequired[player].value)) + if world.FinalXemnas[player]: + world.completion_condition[player] = lambda state: state.kh_victory(player) + else: + world.completion_condition[player] = lambda state: state.kh_hitlist(player, world.BountyRequired[player].value) + + # Forbid Abilities on popups due to game limitations + for location in exclusion_table["Popups"]: + forbid_items(world.get_location(location, player), exclusionItem_table["Ability"]) + forbid_items(world.get_location(location, player), exclusionItem_table["StatUps"]) + + for location in STT_Checks: + forbid_items(world.get_location(location, player), exclusionItem_table["StatUps"]) + + # Santa's house also breaks with stat ups + for location in {LocationName.SantasHouseChristmasTownMap, LocationName.SantasHouseAPBoost}: + forbid_items(world.get_location(location, player), exclusionItem_table["StatUps"]) + + add_rule(world.get_location(LocationName.TransporttoRemembrance, player), + lambda state: state.kh_transport(player)) diff --git a/worlds/kh2/WorldLocations.py b/worlds/kh2/WorldLocations.py new file mode 100644 index 0000000000..172874c2b7 --- /dev/null +++ b/worlds/kh2/WorldLocations.py @@ -0,0 +1,845 @@ +import typing +from .Names import LocationName + + +class WorldLocationData(typing.NamedTuple): + # save+ + addrObtained: int + # bitmask + bitIndex: int + + +LoD_Checks = { + LocationName.BambooGroveDarkShard: WorldLocationData(0x23AC, 1), + LocationName.BambooGroveEther: WorldLocationData(0x23D9, 7), + LocationName.BambooGroveMythrilShard: WorldLocationData(0x23DA, 0), + LocationName.EncampmentAreaMap: WorldLocationData(0x1D94, 6), + LocationName.Mission3: WorldLocationData(0x1D96, 0), + LocationName.CheckpointHiPotion: WorldLocationData(0x23AD, 1), + LocationName.CheckpointMythrilShard: WorldLocationData(0x23AD, 2), + LocationName.MountainTrailLightningShard: WorldLocationData(0x23AD, 3), + LocationName.MountainTrailRecoveryRecipe: WorldLocationData(0x23AD, 4), + LocationName.MountainTrailEther: WorldLocationData(0x23AD, 5), + LocationName.MountainTrailMythrilShard: WorldLocationData(0x23AD, 6), + LocationName.VillageCaveAreaMap: WorldLocationData(0x1D96, 6), + LocationName.VillageCaveDarkShard: WorldLocationData(0x23AE, 0), + LocationName.VillageCaveAPBoost: WorldLocationData(0x23AD, 7), + LocationName.VillageCaveBonus: WorldLocationData(0x3709, 3), + LocationName.RidgeFrostShard: WorldLocationData(0x23AE, 1), + LocationName.RidgeAPBoost: WorldLocationData(0x23AE, 2), + LocationName.ShanYu: WorldLocationData(0x3705, 1), + LocationName.ShanYuGetBonus: WorldLocationData(0x3705, 1), + LocationName.GoofyShanYu: WorldLocationData(0x3705, 1), + LocationName.HiddenDragon: WorldLocationData(0x1D92, 2), + LocationName.ThroneRoomTornPages: WorldLocationData(0x23AE, 3), + LocationName.ThroneRoomPalaceMap: WorldLocationData(0x23AE, 4), + LocationName.ThroneRoomAPBoost: WorldLocationData(0x23AE, 5), + LocationName.ThroneRoomQueenRecipe: WorldLocationData(0x23AE, 6), + LocationName.ThroneRoomAPBoost2: WorldLocationData(0x23AE, 7), + LocationName.ThroneRoomOgreShield: WorldLocationData(0x23AF, 0), + LocationName.ThroneRoomMythrilCrystal: WorldLocationData(0x23AF, 1), + LocationName.ThroneRoomOrichalcum: WorldLocationData(0x23AF, 2), + LocationName.StormRider: WorldLocationData(0x3705, 2), + LocationName.GoofyStormRider: WorldLocationData(0x3705, 2), + +} +AG_Checks = { + LocationName.AgrabahMap: WorldLocationData(0x1D74, 4), + LocationName.AgrabahDarkShard: WorldLocationData(0x23AF, 3), + LocationName.AgrabahMythrilShard: WorldLocationData(0x23AF, 4), + LocationName.AgrabahHiPotion: WorldLocationData(0x23AF, 5), + LocationName.AgrabahAPBoost: WorldLocationData(0x23AF, 6), + LocationName.AgrabahMythrilStone: WorldLocationData(0x23AF, 7), + LocationName.AgrabahMythrilShard2: WorldLocationData(0x23B0, 0), + LocationName.AgrabahSerenityShard: WorldLocationData(0x23DA, 3), + LocationName.BazaarMythrilGem: WorldLocationData(0x23B0, 1), + LocationName.BazaarPowerShard: WorldLocationData(0x23B0, 2), + LocationName.BazaarHiPotion: WorldLocationData(0x23B0, 3), + LocationName.BazaarAPBoost: WorldLocationData(0x23B0, 4), + LocationName.BazaarMythrilShard: WorldLocationData(0x23B0, 5), + LocationName.PalaceWallsSkillRing: WorldLocationData(0x23B0, 6), + LocationName.PalaceWallsMythrilStone: WorldLocationData(0x23DB, 5), + LocationName.CaveEntrancePowerStone: WorldLocationData(0x23B0, 7), + LocationName.CaveEntranceMythrilShard: WorldLocationData(0x23B1, 0), + LocationName.ValleyofStoneMythrilStone: WorldLocationData(0x23B1, 2), + LocationName.ValleyofStoneAPBoost: WorldLocationData(0x23B1, 3), + LocationName.ValleyofStoneMythrilShard: WorldLocationData(0x23B1, 4), + LocationName.ValleyofStoneHiPotion: WorldLocationData(0x23B1, 5), + LocationName.AbuEscort: WorldLocationData(0x3709, 2), + LocationName.DonaldAbuEscort: WorldLocationData(0x3709, 2), + LocationName.ChasmofChallengesCaveofWondersMap: WorldLocationData(0x23D8, 7), + LocationName.ChasmofChallengesAPBoost: WorldLocationData(0x23B1, 6), + LocationName.TreasureRoom: WorldLocationData(0x3709, 6), + LocationName.GoofyTreasureRoom: WorldLocationData(0x3709, 6), + LocationName.TreasureRoomAPBoost: WorldLocationData(0x23DA, 4), + LocationName.TreasureRoomSerenityGem: WorldLocationData(0x23DA, 5), + LocationName.ElementalLords: WorldLocationData(0x3708, 5), + LocationName.LampCharm: WorldLocationData(0x1D72, 4), + LocationName.RuinedChamberTornPages: WorldLocationData(0x23B1, 1), + LocationName.RuinedChamberRuinsMap: WorldLocationData(0x23D8, 6), + LocationName.GenieJafar: WorldLocationData(0x3705, 7), + LocationName.WishingLamp: WorldLocationData(0x1D77, 3), + +} +DC_Checks = { + LocationName.DCCourtyardMythrilShard: WorldLocationData(0x23B4, 1), + LocationName.DCCourtyardStarRecipe: WorldLocationData(0x23B4, 2), + LocationName.DCCourtyardAPBoost: WorldLocationData(0x23B4, 3), + LocationName.DCCourtyardMythrilStone: WorldLocationData(0x23B4, 4), + LocationName.DCCourtyardBlazingStone: WorldLocationData(0x23B4, 5), + LocationName.DCCourtyardBlazingShard: WorldLocationData(0x23B4, 6), + LocationName.DCCourtyardMythrilShard2: WorldLocationData(0x23B4, 7), + LocationName.LibraryTornPages: WorldLocationData(0x23B4, 0), + LocationName.DisneyCastleMap: WorldLocationData(0x1E10, 4), + LocationName.MinnieEscort: WorldLocationData(0x3708, 6), + LocationName.MinnieEscortGetBonus: WorldLocationData(0x3708, 6), + LocationName.LingeringWillBonus: WorldLocationData(0x370C, 6), + LocationName.LingeringWillProofofConnection: WorldLocationData(0x370C, 6), + LocationName.LingeringWillManifestIllusion: WorldLocationData(0x370C, 6), +} +TR_Checks = { + LocationName.CornerstoneHillMap: WorldLocationData(0x23B2, 0), + LocationName.CornerstoneHillFrostShard: WorldLocationData(0x23B2, 1), + LocationName.PierMythrilShard: WorldLocationData(0x23B2, 3), + LocationName.PierHiPotion: WorldLocationData(0x23B2, 4), + LocationName.WaterwayMythrilStone: WorldLocationData(0x23B2, 5), + LocationName.WaterwayAPBoost: WorldLocationData(0x23B2, 6), + LocationName.WaterwayFrostStone: WorldLocationData(0x23B2, 7), + LocationName.WindowofTimeMap: WorldLocationData(0x1E32, 4), + LocationName.BoatPete: WorldLocationData(0x3706, 0), + LocationName.DonaldBoatPete: WorldLocationData(0x3706, 0), + LocationName.DonaldBoatPeteGetBonus: WorldLocationData(0x3706, 0), + LocationName.FuturePete: WorldLocationData(0x3706, 1), + LocationName.FuturePeteGetBonus: WorldLocationData(0x3706, 1), + LocationName.GoofyFuturePete: WorldLocationData(0x3706, 1), + LocationName.Monochrome: WorldLocationData(0x1E33, 2), + LocationName.WisdomForm: WorldLocationData(0x1E33, 2), +} + +HundredAcreChecks = { + LocationName.PoohsHouse100AcreWoodMap: WorldLocationData(0x23C9, 7), + LocationName.PoohsHouseAPBoost: WorldLocationData(0x23B5, 4), + LocationName.PoohsHouseMythrilStone: WorldLocationData(0x23B5, 5), + LocationName.PigletsHouseDefenseBoost: WorldLocationData(0x23B6, 4), + LocationName.PigletsHouseAPBoost: WorldLocationData(0x23B6, 2), + LocationName.PigletsHouseMythrilGem: WorldLocationData(0x23B6, 3), + LocationName.RabbitsHouseDrawRing: WorldLocationData(0x23CA, 0), + LocationName.RabbitsHouseMythrilCrystal: WorldLocationData(0x23B5, 7), + LocationName.RabbitsHouseAPBoost: WorldLocationData(0x23B6, 0), + LocationName.KangasHouseMagicBoost: WorldLocationData(0x23B6, 7), + LocationName.KangasHouseAPBoost: WorldLocationData(0x23B6, 5), + LocationName.KangasHouseOrichalcum: WorldLocationData(0x23B6, 6), + LocationName.SpookyCaveMythrilGem: WorldLocationData(0x23B7, 1), + LocationName.SpookyCaveAPBoost: WorldLocationData(0x23B7, 2), + LocationName.SpookyCaveOrichalcum: WorldLocationData(0x23B7, 3), + LocationName.SpookyCaveGuardRecipe: WorldLocationData(0x23B7, 4), + LocationName.SpookyCaveMythrilCrystal: WorldLocationData(0x23B7, 6), + LocationName.SpookyCaveAPBoost2: WorldLocationData(0x23B7, 7), + LocationName.SweetMemories: WorldLocationData(0x1DB4, 6), + LocationName.SpookyCaveMap: WorldLocationData(0x1DB4, 6), + LocationName.StarryHillCosmicRing: WorldLocationData(0x23C9, 6), + LocationName.StarryHillStyleRecipe: WorldLocationData(0x23B5, 1), + LocationName.StarryHillCureElement: WorldLocationData(0x1DB5, 5), + LocationName.StarryHillOrichalcumPlus: WorldLocationData(0x1DB5, 5), +} +Oc_Checks = { + LocationName.PassageMythrilShard: WorldLocationData(0x23B9, 6), + LocationName.PassageMythrilStone: WorldLocationData(0x23B9, 7), + LocationName.PassageEther: WorldLocationData(0x23BA, 0), + LocationName.PassageAPBoost: WorldLocationData(0x23BA, 1), + LocationName.PassageHiPotion: WorldLocationData(0x23BA, 2), + LocationName.InnerChamberUnderworldMap: WorldLocationData(0x23B8, 4), + LocationName.InnerChamberMythrilShard: WorldLocationData(0x23B8, 3), + LocationName.Cerberus: WorldLocationData(0x3704, 5), + LocationName.ColiseumMap: WorldLocationData(0x1D5A, 4), + LocationName.Urns: WorldLocationData(0x370B, 1), + LocationName.UnderworldEntrancePowerBoost: WorldLocationData(0x23B8, 0), + LocationName.CavernsEntranceLucidShard: WorldLocationData(0x23B8, 5), + LocationName.CavernsEntranceAPBoost: WorldLocationData(0x23B8, 6), + LocationName.CavernsEntranceMythrilShard: WorldLocationData(0x23DA, 6), + LocationName.TheLostRoadBrightShard: WorldLocationData(0x23BA, 3), + LocationName.TheLostRoadEther: WorldLocationData(0x23BA, 4), + LocationName.TheLostRoadMythrilShard: WorldLocationData(0x23BA, 5), + LocationName.TheLostRoadMythrilStone: WorldLocationData(0x23BA, 6), + LocationName.AtriumLucidStone: WorldLocationData(0x23BA, 7), + LocationName.AtriumAPBoost: WorldLocationData(0x23BB, 0), + LocationName.DemyxOC: WorldLocationData(0x370B, 2), + LocationName.DonaldDemyxOC: WorldLocationData(0x370B, 2), + LocationName.SecretAnsemReport5: WorldLocationData(0x1D5B, 3), + LocationName.OlympusStone: WorldLocationData(0x1D5B, 3), + LocationName.TheLockCavernsMap: WorldLocationData(0x23B9, 4), + LocationName.TheLockMythrilShard: WorldLocationData(0x23B9, 0), + LocationName.TheLockAPBoost: WorldLocationData(0x23B9, 2), + LocationName.PeteOC: WorldLocationData(0x3704, 6), + LocationName.GoofyPeteOC: WorldLocationData(0x3704, 6), + LocationName.Hydra: WorldLocationData(0x3704, 7), + LocationName.HydraGetBonus: WorldLocationData(0x3704, 7), + LocationName.HerosCrest: WorldLocationData(0x1D55, 7), + LocationName.AuronsStatue: WorldLocationData(0x1D5F, 2), + LocationName.Hades: WorldLocationData(0x3705, 0), + LocationName.HadesGetBonus: WorldLocationData(0x3705, 0), + LocationName.GuardianSoul: WorldLocationData(0x1D56, 5), + LocationName.ProtectBeltPainandPanicCup: WorldLocationData(0x1D57, 6), + LocationName.SerenityGemPainandPanicCup: WorldLocationData(0x1D57, 6), + LocationName.RisingDragonCerberusCup: WorldLocationData(0x1D58, 0), + LocationName.SerenityCrystalCerberusCup: WorldLocationData(0x1D58, 0), + LocationName.GenjiShieldTitanCup: WorldLocationData(0x1D58, 1), + LocationName.SkillfulRingTitanCup: WorldLocationData(0x1D58, 1), + LocationName.FatalCrestGoddessofFateCup: WorldLocationData(0x1D58, 4), + LocationName.OrichalcumPlusGoddessofFateCup: WorldLocationData(0x1D58, 4), + LocationName.HadesCupTrophyParadoxCups: WorldLocationData(0x1D5A, 1), +} + +BC_Checks = { + LocationName.BCCourtyardAPBoost: WorldLocationData(0x23BB, 5), + LocationName.BCCourtyardHiPotion: WorldLocationData(0x23BB, 6), + LocationName.BCCourtyardMythrilShard: WorldLocationData(0x23DA, 7), + LocationName.BellesRoomCastleMap: WorldLocationData(0x23BB, 2), + LocationName.BellesRoomMegaRecipe: WorldLocationData(0x23BB, 3), + LocationName.TheEastWingMythrilShard: WorldLocationData(0x23BB, 7), + LocationName.TheEastWingTent: WorldLocationData(0x23BC, 0), + LocationName.TheWestHallHiPotion: WorldLocationData(0x23BC, 1), + LocationName.TheWestHallPowerShard: WorldLocationData(0x23BC, 3), + LocationName.TheWestHallAPBoostPostDungeon: WorldLocationData(0x23BC, 5), + LocationName.TheWestHallBrightStone: WorldLocationData(0x23DB, 0), + LocationName.TheWestHallMythrilShard: WorldLocationData(0x23BC, 2), + LocationName.TheWestHallMythrilShard2: WorldLocationData(0x23BC, 4), + LocationName.Thresholder: WorldLocationData(0x3704, 2), + LocationName.DonaldThresholder: WorldLocationData(0x3704, 2), + LocationName.DungeonBasementMap: WorldLocationData(0x23BD, 0), + LocationName.DungeonAPBoost: WorldLocationData(0x23BD, 1), + LocationName.SecretPassageMythrilShard: WorldLocationData(0x23BD, 2), + LocationName.SecretPassageHiPotion: WorldLocationData(0x23BD, 5), + LocationName.SecretPassageLucidShard: WorldLocationData(0x23BD, 3), + LocationName.TheWestWingMythrilShard: WorldLocationData(0x23BC, 6), + LocationName.TheWestWingTent: WorldLocationData(0x23BC, 7), + LocationName.Beast: WorldLocationData(0x3705, 4), + LocationName.GoofyBeast: WorldLocationData(0x3705, 4), + LocationName.TheBeastsRoomBlazingShard: WorldLocationData(0x23BB, 4), + LocationName.DarkThorn: WorldLocationData(0x3704, 3), + LocationName.DarkThornGetBonus: WorldLocationData(0x3704, 3), + LocationName.DarkThornCureElement: WorldLocationData(0x1D32, 5), + LocationName.RumblingRose: WorldLocationData(0x1D39, 0), + LocationName.CastleWallsMap: WorldLocationData(0x1D39, 0), + LocationName.Xaldin: WorldLocationData(0x3704, 4), + LocationName.XaldinGetBonus: WorldLocationData(0x3704, 4), + LocationName.DonaldXaldinGetBonus: WorldLocationData(0x3704, 4), + LocationName.SecretAnsemReport4: WorldLocationData(0x1D31, 2), + LocationName.XaldinDataDefenseBoost: WorldLocationData(0x1D34, 7), +} +SP_Checks = { + LocationName.PitCellAreaMap: WorldLocationData(0x23CA, 2), + LocationName.PitCellMythrilCrystal: WorldLocationData(0x23BD, 6), + LocationName.CanyonDarkCrystal: WorldLocationData(0x23BE, 1), + LocationName.CanyonMythrilStone: WorldLocationData(0x23BE, 2), + LocationName.CanyonMythrilGem: WorldLocationData(0x23BE, 3), + LocationName.CanyonFrostCrystal: WorldLocationData(0x23DB, 6), + LocationName.Screens: WorldLocationData(0x3709, 5), + LocationName.DonaldScreens: WorldLocationData(0x3709, 5), + LocationName.HallwayPowerCrystal: WorldLocationData(0x23BE, 4), + LocationName.HallwayAPBoost: WorldLocationData(0x23BE, 5), + LocationName.CommunicationsRoomIOTowerMap: WorldLocationData(0x23BF, 1), + LocationName.CommunicationsRoomGaiaBelt: WorldLocationData(0x23DA, 1), + LocationName.HostileProgram: WorldLocationData(0x3707, 7), + LocationName.HostileProgramGetBonus: WorldLocationData(0x3707, 7), + LocationName.GoofyHostileProgram: WorldLocationData(0x3707, 7), + LocationName.PhotonDebugger: WorldLocationData(0x1EB2, 3), + LocationName.SolarSailer: WorldLocationData(0x370B, 5), + LocationName.DonaldSolarSailer: WorldLocationData(0x370B, 5), + LocationName.CentralComputerCoreAPBoost: WorldLocationData(0x23BF, 4), + LocationName.CentralComputerCoreOrichalcumPlus: WorldLocationData(0x23BF, 5), + LocationName.CentralComputerCoreCosmicArts: WorldLocationData(0x23BF, 6), + LocationName.CentralComputerCoreMap: WorldLocationData(0x23D9, 0), + LocationName.MCP: WorldLocationData(0x3708, 0), + LocationName.MCPGetBonus: WorldLocationData(0x3708, 0), +} +HT_Checks = { + LocationName.GraveyardMythrilShard: WorldLocationData(0x23C0, 2), + LocationName.GraveyardSerenityGem: WorldLocationData(0x23C0, 3), + LocationName.FinklesteinsLabHalloweenTownMap: WorldLocationData(0x23C0, 1), + LocationName.TownSquareMythrilStone: WorldLocationData(0x23BF, 7), + LocationName.TownSquareEnergyShard: WorldLocationData(0x23C0, 0), + LocationName.HinterlandsLightningShard: WorldLocationData(0x23C0, 4), + LocationName.HinterlandsMythrilStone: WorldLocationData(0x23C0, 5), + LocationName.HinterlandsAPBoost: WorldLocationData(0x23C0, 6), + LocationName.CandyCaneLaneMegaPotion: WorldLocationData(0x23C1, 0), + LocationName.CandyCaneLaneMythrilGem: WorldLocationData(0x23C1, 1), + LocationName.CandyCaneLaneLightningStone: WorldLocationData(0x23C1, 2), + LocationName.CandyCaneLaneMythrilStone: WorldLocationData(0x23C1, 3), + LocationName.SantasHouseChristmasTownMap: WorldLocationData(0x23C1, 6), + LocationName.SantasHouseAPBoost: WorldLocationData(0x23C1, 4), + LocationName.PrisonKeeper: WorldLocationData(0x3706, 2), + LocationName.DonaldPrisonKeeper: WorldLocationData(0x3706, 2), + LocationName.OogieBoogie: WorldLocationData(0x3706, 3), + LocationName.GoofyOogieBoogie: WorldLocationData(0x3706, 3), + LocationName.OogieBoogieMagnetElement: WorldLocationData(0x1E53, 2), + LocationName.Lock: WorldLocationData(0x3709, 0), + LocationName.GoofyLock: WorldLocationData(0x3709, 0), + LocationName.Present: WorldLocationData(0x1E55, 1), + LocationName.DecoyPresents: WorldLocationData(0x1E55, 4), + LocationName.Experiment: WorldLocationData(0x3706, 4), + LocationName.DonaldExperiment: WorldLocationData(0x3706, 4), + LocationName.DecisivePumpkin: WorldLocationData(0x1E56, 0), + +} +PR_Checks = { + LocationName.RampartNavalMap: WorldLocationData(0x23C2, 1), + LocationName.RampartMythrilStone: WorldLocationData(0x23C2, 2), + LocationName.RampartDarkShard: WorldLocationData(0x23C2, 3), + LocationName.TownDarkStone: WorldLocationData(0x23C2, 4), + LocationName.TownAPBoost: WorldLocationData(0x23C2, 5), + LocationName.TownMythrilShard: WorldLocationData(0x23C2, 6), + LocationName.TownMythrilGem: WorldLocationData(0x23C2, 7), + LocationName.CaveMouthBrightShard: WorldLocationData(0x23C3, 1), + LocationName.CaveMouthMythrilShard: WorldLocationData(0x23C3, 2), + LocationName.IsladeMuertaMap: WorldLocationData(0x1E92, 4), + LocationName.BoatFight: WorldLocationData(0x370B, 6), + LocationName.DonaldBoatFight: WorldLocationData(0x370B, 6), + LocationName.InterceptorBarrels: WorldLocationData(0x3708, 7), + LocationName.GoofyInterceptorBarrels: WorldLocationData(0x3708, 7), + LocationName.PowderStoreAPBoost1: WorldLocationData(0x23CA, 7), + LocationName.PowderStoreAPBoost2: WorldLocationData(0x23CB, 0), + LocationName.MoonlightNookMythrilShard: WorldLocationData(0x23C3, 4), + LocationName.MoonlightNookSerenityGem: WorldLocationData(0x23C3, 5), + LocationName.MoonlightNookPowerStone: WorldLocationData(0x23CB, 1), + LocationName.Barbossa: WorldLocationData(0x3706, 5), + LocationName.BarbossaGetBonus: WorldLocationData(0x3706, 5), + LocationName.GoofyBarbossa: WorldLocationData(0x3706, 5), + LocationName.GoofyBarbossaGetBonus: WorldLocationData(0x3706, 5), + LocationName.FollowtheWind: WorldLocationData(0x1E93, 6), + LocationName.GrimReaper1: WorldLocationData(0x370B, 3), + LocationName.GoofyGrimReaper1: WorldLocationData(0x370B, 3), + LocationName.InterceptorsHoldFeatherCharm: WorldLocationData(0x23C3, 3), + LocationName.SeadriftKeepAPBoost: WorldLocationData(0x23C3, 6), + LocationName.SeadriftKeepOrichalcum: WorldLocationData(0x23C3, 7), + LocationName.SeadriftKeepMeteorStaff: WorldLocationData(0x23CB, 2), + LocationName.SeadriftRowSerenityGem: WorldLocationData(0x23C4, 0), + LocationName.SeadriftRowKingRecipe: WorldLocationData(0x23C4, 1), + LocationName.SeadriftRowMythrilCrystal: WorldLocationData(0x23CB, 3), + LocationName.SeadriftRowCursedMedallion: WorldLocationData(0x1E95, 2), + LocationName.SeadriftRowShipGraveyardMap: WorldLocationData(0x1E95, 2), + LocationName.GrimReaper2: WorldLocationData(0x3706, 6), + LocationName.DonaladGrimReaper2: WorldLocationData(0x3706, 6), + LocationName.SecretAnsemReport6: WorldLocationData(0x1E95, 7), + +} +HB_Checks = { + LocationName.MarketplaceMap: WorldLocationData(0x1D17, 4), + LocationName.BoroughDriveRecovery: WorldLocationData(0x23C6, 1), + LocationName.BoroughAPBoost: WorldLocationData(0x23C6, 2), + LocationName.BoroughHiPotion: WorldLocationData(0x23C6, 3), + LocationName.BoroughMythrilShard: WorldLocationData(0x23C8, 7), + LocationName.BoroughDarkShard: WorldLocationData(0x23DB, 1), + LocationName.MerlinsHouseMembershipCard: WorldLocationData(0x1D10, 6), + LocationName.MerlinsHouseBlizzardElement: WorldLocationData(0x1D10, 6), + # you cannot get these checks without baily so they are all on the same memory value. + LocationName.Bailey: WorldLocationData(0x3709, 7), + LocationName.BaileySecretAnsemReport7: WorldLocationData(0x3709, 7), + LocationName.BaseballCharm: WorldLocationData(0x3709, 7), + LocationName.PosternCastlePerimeterMap: WorldLocationData(0x23C9, 4), + LocationName.PosternMythrilGem: WorldLocationData(0x23C5, 4), + LocationName.PosternAPBoost: WorldLocationData(0x23C5, 5), + LocationName.CorridorsMythrilStone: WorldLocationData(0x23C6, 7), + LocationName.CorridorsMythrilCrystal: WorldLocationData(0x23C7, 0), + LocationName.CorridorsDarkCrystal: WorldLocationData(0x23C7, 1), + LocationName.CorridorsAPBoost: WorldLocationData(0x23C9, 1), + # this is probably gonna be wrong + LocationName.AnsemsStudyMasterForm: WorldLocationData(0x1D12, 6), + LocationName.AnsemsStudySleepingLion: WorldLocationData(0x1D12, 6), + LocationName.AnsemsStudySkillRecipe: WorldLocationData(0x23C4, 7), + LocationName.AnsemsStudyUkuleleCharm: WorldLocationData(0x23C4, 6), + LocationName.RestorationSiteMoonRecipe: WorldLocationData(0x23C9, 3), + LocationName.RestorationSiteAPBoost: WorldLocationData(0x23DB, 2), + LocationName.DemyxHB: WorldLocationData(0x3707, 4), + LocationName.DemyxHBGetBonus: WorldLocationData(0x3707, 4), + LocationName.DonaldDemyxHBGetBonus: WorldLocationData(0x3707, 4), + LocationName.FFFightsCureElement: WorldLocationData(0x1D14, 6), + LocationName.CrystalFissureTornPages: WorldLocationData(0x23C4, 2), + LocationName.CrystalFissureTheGreatMawMap: WorldLocationData(0x23D9, 1), + LocationName.CrystalFissureEnergyCrystal: WorldLocationData(0x23C4, 3), + LocationName.CrystalFissureAPBoost: WorldLocationData(0x23C4, 4), + LocationName.ThousandHeartless: WorldLocationData(0x370B, 4), + LocationName.ThousandHeartlessSecretAnsemReport1: WorldLocationData(0x1D19, 3), + LocationName.ThousandHeartlessIceCream: WorldLocationData(0x1D23, 0), + LocationName.ThousandHeartlessPicture: WorldLocationData(0x1D23, 0), + LocationName.PosternGullWing: WorldLocationData(0x23D9, 3), + LocationName.HeartlessManufactoryCosmicChain: WorldLocationData(0x23C9, 5), + LocationName.SephirothBonus: WorldLocationData(0x3708, 3), + LocationName.SephirothFenrir: WorldLocationData(0x1D1F, 7), + LocationName.WinnersProof: WorldLocationData(0x1D27, 5), + LocationName.ProofofPeace: WorldLocationData(0x1D27, 5), + + LocationName.CoRDepthsAPBoost: WorldLocationData(0x23DC, 2), + LocationName.CoRDepthsPowerCrystal: WorldLocationData(0x23DC, 3), + LocationName.CoRDepthsFrostCrystal: WorldLocationData(0x23DC, 4), + LocationName.CoRDepthsManifestIllusion: WorldLocationData(0x23DC, 5), + LocationName.CoRDepthsAPBoost2: WorldLocationData(0x23DC, 6), + LocationName.CoRMineshaftLowerLevelDepthsofRemembranceMap: WorldLocationData(0x23DE, 4), + LocationName.CoRMineshaftLowerLevelAPBoost: WorldLocationData(0x23DE, 2), + LocationName.CoRDepthsUpperLevelRemembranceGem: WorldLocationData(0x23DC, 7), + LocationName.CoRMiningAreaSerenityGem: WorldLocationData(0x23DD, 0), + LocationName.CoRMiningAreaAPBoost: WorldLocationData(0x23DD, 1), + LocationName.CoRMiningAreaSerenityCrystal: WorldLocationData(0x23DD, 2), + LocationName.CoRMiningAreaManifestIllusion: WorldLocationData(0x23DD, 3), + LocationName.CoRMiningAreaSerenityGem2: WorldLocationData(0x23DD, 4), + LocationName.CoRMiningAreaDarkRemembranceMap: WorldLocationData(0x23DD, 5), + LocationName.CoRMineshaftMidLevelPowerBoost: WorldLocationData(0x23DE, 5), + LocationName.CoREngineChamberSerenityCrystal: WorldLocationData(0x23DD, 6), + LocationName.CoREngineChamberRemembranceCrystal: WorldLocationData(0x23DD, 7), + LocationName.CoREngineChamberAPBoost: WorldLocationData(0x23DE, 0), + LocationName.CoREngineChamberManifestIllusion: WorldLocationData(0x23DE, 1), + LocationName.CoRMineshaftUpperLevelMagicBoost: WorldLocationData(0x23DE, 6), + LocationName.CoRMineshaftUpperLevelAPBoost: WorldLocationData(0x23DE, 3), + LocationName.TransporttoRemembrance: WorldLocationData(0x370D, 0), + + LocationName.LexaeusBonus: WorldLocationData(0x370C, 1), + LocationName.LexaeusASStrengthBeyondStrength: WorldLocationData(0x370C, 1), + LocationName.LexaeusDataLostIllusion: WorldLocationData(0x370C, 1), # + LocationName.MarluxiaGetBonus: WorldLocationData(0x370C, 3), + LocationName.MarluxiaASEternalBlossom: WorldLocationData(0x370C, 3), + LocationName.MarluxiaDataLostIllusion: WorldLocationData(0x370C, 3), # + LocationName.ZexionBonus: WorldLocationData(0x370C, 2), + LocationName.GoofyZexion: WorldLocationData(0x370C, 2), + LocationName.ZexionASBookofShadows: WorldLocationData(0x370C, 2), + LocationName.ZexionDataLostIllusion: WorldLocationData(0x370C, 2), # + LocationName.LarxeneBonus: WorldLocationData(0x370C, 4), + LocationName.LarxeneASCloakedThunder: WorldLocationData(0x370C, 4), + LocationName.LarxeneDataLostIllusion: WorldLocationData(0x370C, 4), # + LocationName.VexenBonus: WorldLocationData(0x370C, 0), + LocationName.VexenASRoadtoDiscovery: WorldLocationData(0x370C, 0), + LocationName.VexenDataLostIllusion: WorldLocationData(0x370C, 0), # + LocationName.DemyxDataAPBoost: WorldLocationData(0x1D26, 5), + LocationName.GardenofAssemblageMap: WorldLocationData(0x23DF, 1), + LocationName.GoALostIllusion: WorldLocationData(0x23DF, 2), + LocationName.ProofofNonexistence: WorldLocationData(0x23DF, 3), + # given when you talk to the computer + LocationName.KingdomKeySlot: WorldLocationData(0x1D27, 3), + LocationName.MagesStaff: WorldLocationData(0x1D27, 3), + LocationName.KnightsShield: WorldLocationData(0x1D27, 3), + LocationName.DonaldStarting1: WorldLocationData(0x1D27, 3), + LocationName.DonaldStarting2: WorldLocationData(0x1D27, 3), + LocationName.GoofyStarting1: WorldLocationData(0x1D27, 3), + LocationName.GoofyStarting2: WorldLocationData(0x1D27, 3), + LocationName.Crit_1: WorldLocationData(0x1D27, 3), + LocationName.Crit_2: WorldLocationData(0x1D27, 3), + LocationName.Crit_3: WorldLocationData(0x1D27, 3), + LocationName.Crit_4: WorldLocationData(0x1D27, 3), + LocationName.Crit_5: WorldLocationData(0x1D27, 3), + LocationName.Crit_6: WorldLocationData(0x1D27, 3), + LocationName.Crit_7: WorldLocationData(0x1D27, 3), + +} +PL_Checks = { + LocationName.GorgeSavannahMap: WorldLocationData(0x23D9, 4), + LocationName.GorgeDarkGem: WorldLocationData(0x23CF, 0), + LocationName.GorgeMythrilStone: WorldLocationData(0x23CF, 1), + LocationName.ElephantGraveyardFrostGem: WorldLocationData(0x23CE, 5), + LocationName.ElephantGraveyardMythrilStone: WorldLocationData(0x23CE, 6), + LocationName.ElephantGraveyardBrightStone: WorldLocationData(0x23CE, 7), + LocationName.ElephantGraveyardAPBoost: WorldLocationData(0x23DB, 3), + LocationName.ElephantGraveyardMythrilShard: WorldLocationData(0x23DB, 4), + LocationName.PrideRockMap: WorldLocationData(0x23D0, 3), + LocationName.PrideRockMythrilStone: WorldLocationData(0x23CD, 4), + LocationName.PrideRockSerenityCrystal: WorldLocationData(0x23CD, 5), + LocationName.WildebeestValleyEnergyStone: WorldLocationData(0x23CE, 0), + LocationName.WildebeestValleyAPBoost: WorldLocationData(0x23CE, 1), + LocationName.WildebeestValleyMythrilGem: WorldLocationData(0x23CE, 2), + LocationName.WildebeestValleyMythrilStone: WorldLocationData(0x23CE, 3), + LocationName.WildebeestValleyLucidGem: WorldLocationData(0x23CE, 4), + LocationName.WastelandsMythrilShard: WorldLocationData(0x23CF, 2), + LocationName.WastelandsSerenityGem: WorldLocationData(0x23CF, 3), + LocationName.WastelandsMythrilStone: WorldLocationData(0x23CF, 4), + LocationName.JungleSerenityGem: WorldLocationData(0x23CF, 5), + LocationName.JungleMythrilStone: WorldLocationData(0x23CF, 6), + LocationName.JungleSerenityCrystal: WorldLocationData(0x23CF, 7), + LocationName.OasisMap: WorldLocationData(0x23D0, 0), + LocationName.OasisTornPages: WorldLocationData(0x23D9, 5), + LocationName.OasisAPBoost: WorldLocationData(0x23D0, 1), + LocationName.CircleofLife: WorldLocationData(0x1DD2, 1), + LocationName.Hyenas1: WorldLocationData(0x370A, 1), + LocationName.GoofyHyenas1: WorldLocationData(0x370A, 1), + LocationName.Scar: WorldLocationData(0x3707, 5), + LocationName.DonaldScar: WorldLocationData(0x3707, 5), + LocationName.ScarFireElement: WorldLocationData(0x1DD4, 7), + LocationName.Hyenas2: WorldLocationData(0x370A, 2), + LocationName.GoofyHyenas2: WorldLocationData(0x370A, 2), + LocationName.Groundshaker: WorldLocationData(0x3707, 6), + LocationName.GroundshakerGetBonus: WorldLocationData(0x3707, 6), + +} +TT_Checks = { + LocationName.TwilightTownMap: WorldLocationData(0x1CD6, 3), + LocationName.MunnyPouchOlette: WorldLocationData(0x1CD6, 5), + LocationName.StationDusks: WorldLocationData(0x370A, 6), + LocationName.StationofSerenityPotion: WorldLocationData(0x23CA, 1), + LocationName.StationofCallingPotion: WorldLocationData(0x23D7, 1), + LocationName.TwilightThorn: WorldLocationData(0x3708, 1), + LocationName.Axel1: WorldLocationData(0x370D, 1), + LocationName.JunkChampionBelt: WorldLocationData(0x1CDC, 2), + LocationName.JunkMedal: WorldLocationData(0x1CDC, 2), + LocationName.TheStruggleTrophy: WorldLocationData(0x1CDC, 2), + LocationName.CentralStationPotion1: WorldLocationData(0x23D1, 5), + LocationName.STTCentralStationHiPotion: WorldLocationData(0x23D1, 6), + LocationName.CentralStationPotion2: WorldLocationData(0x23D1, 7), + LocationName.SunsetTerraceAbilityRing: WorldLocationData(0x23D2, 3), + LocationName.SunsetTerraceHiPotion: WorldLocationData(0x23D2, 4), + LocationName.SunsetTerracePotion1: WorldLocationData(0x23D2, 5), + LocationName.SunsetTerracePotion2: WorldLocationData(0x23D2, 6), + LocationName.MansionFoyerHiPotion: WorldLocationData(0x23D4, 2), + LocationName.MansionFoyerPotion1: WorldLocationData(0x23D4, 3), + LocationName.MansionFoyerPotion2: WorldLocationData(0x23D4, 4), + LocationName.MansionDiningRoomElvenBandanna: WorldLocationData(0x23D5, 0), + LocationName.MansionDiningRoomPotion: WorldLocationData(0x23D5, 1), + LocationName.NaminesSketches: WorldLocationData(0x1CE0, 6), + LocationName.MansionMap: WorldLocationData(0x1CE0, 6), + LocationName.MansionLibraryHiPotion: WorldLocationData(0x23D5, 4), + LocationName.Axel2: WorldLocationData(0x3708, 2), + LocationName.MansionBasementCorridorHiPotion: WorldLocationData(0x23D6, 0), + # stt and tt share the same world id + LocationName.OldMansionPotion: WorldLocationData(0x23D4, 0), + LocationName.OldMansionMythrilShard: WorldLocationData(0x23D4, 1), + LocationName.TheWoodsPotion: WorldLocationData(0x23D3, 3), + LocationName.TheWoodsMythrilShard: WorldLocationData(0x23D3, 4), + LocationName.TheWoodsHiPotion: WorldLocationData(0x23D3, 5), + LocationName.TramCommonHiPotion: WorldLocationData(0x23D0, 5), + LocationName.TramCommonAPBoost: WorldLocationData(0x23D0, 6), + LocationName.TramCommonTent: WorldLocationData(0x23D0, 7), + LocationName.TramCommonMythrilShard1: WorldLocationData(0x23D1, 0), + LocationName.TramCommonPotion1: WorldLocationData(0x23D1, 1), + LocationName.TramCommonMythrilShard2: WorldLocationData(0x23D1, 2), + LocationName.TramCommonPotion2: WorldLocationData(0x23D8, 5), + LocationName.StationPlazaSecretAnsemReport2: WorldLocationData(0x1CE3, 3), + LocationName.MunnyPouchMickey: WorldLocationData(0x1CE3, 3), + LocationName.CrystalOrb: WorldLocationData(0x1CE3, 3), + LocationName.CentralStationTent: WorldLocationData(0x23D2, 0), + LocationName.TTCentralStationHiPotion: WorldLocationData(0x23D2, 1), + LocationName.CentralStationMythrilShard: WorldLocationData(0x23D2, 2), + LocationName.TheTowerPotion: WorldLocationData(0x23D6, 2), + LocationName.TheTowerHiPotion: WorldLocationData(0x23D6, 3), + LocationName.TheTowerEther: WorldLocationData(0x23DB, 7), + LocationName.TowerEntrywayEther: WorldLocationData(0x23D6, 4), + LocationName.TowerEntrywayMythrilShard: WorldLocationData(0x23D6, 5), + LocationName.SorcerersLoftTowerMap: WorldLocationData(0x23D6, 6), + LocationName.TowerWardrobeMythrilStone: WorldLocationData(0x23D6, 7), + LocationName.StarSeeker: WorldLocationData(0x1CE5, 2), + LocationName.ValorForm: WorldLocationData(0x1CE5, 2), + LocationName.SeifersTrophy: WorldLocationData(0x1CE6, 4), + LocationName.Oathkeeper: WorldLocationData(0x1CE6, 7), + LocationName.LimitForm: WorldLocationData(0x1CE6, 7), + LocationName.UndergroundConcourseMythrilGem: WorldLocationData(0x23D8, 0), + LocationName.UndergroundConcourseAPBoost: WorldLocationData(0x23D8, 2), + LocationName.UndergroundConcourseOrichalcum: WorldLocationData(0x23D8, 1), + LocationName.UndergroundConcourseMythrilCrystal: WorldLocationData(0x23D8, 3), + LocationName.TunnelwayOrichalcum: WorldLocationData(0x23D7, 6), + LocationName.TunnelwayMythrilCrystal: WorldLocationData(0x23D7, 7), + LocationName.SunsetTerraceOrichalcumPlus: WorldLocationData(0x23D2, 7), + LocationName.SunsetTerraceMythrilShard: WorldLocationData(0x23D3, 0), + LocationName.SunsetTerraceMythrilCrystal: WorldLocationData(0x23D3, 1), + LocationName.SunsetTerraceAPBoost: WorldLocationData(0x23D3, 2), + LocationName.MansionNobodies: WorldLocationData(0x370B, 0), + LocationName.DonaldMansionNobodies: WorldLocationData(0x370B, 0), + LocationName.MansionFoyerMythrilCrystal: WorldLocationData(0x23D4, 5), + LocationName.MansionFoyerMythrilStone: WorldLocationData(0x23D4, 6), + LocationName.MansionFoyerSerenityCrystal: WorldLocationData(0x23D4, 7), + LocationName.MansionDiningRoomMythrilCrystal: WorldLocationData(0x23D5, 2), + LocationName.MansionDiningRoomMythrilStone: WorldLocationData(0x23D5, 3), + LocationName.MansionLibraryOrichalcum: WorldLocationData(0x23D5, 5), + LocationName.BeamSecretAnsemReport10: WorldLocationData(0x1CE8, 3), + LocationName.MansionBasementCorridorUltimateRecipe: WorldLocationData(0x23D6, 1), + LocationName.BetwixtandBetween: WorldLocationData(0x370B, 7), + LocationName.BetwixtandBetweenBondofFlame: WorldLocationData(0x1CE9, 1), + LocationName.AxelDataMagicBoost: WorldLocationData(0x1CEB, 4), +} +TWTNW_Checks = { + LocationName.FragmentCrossingMythrilStone: WorldLocationData(0x23CB, 4), + LocationName.FragmentCrossingMythrilCrystal: WorldLocationData(0x23CB, 5), + LocationName.FragmentCrossingAPBoost: WorldLocationData(0x23CB, 6), + LocationName.FragmentCrossingOrichalcum: WorldLocationData(0x23CB, 7), + LocationName.Roxas: WorldLocationData(0x370C, 5), + LocationName.RoxasGetBonus: WorldLocationData(0x370C, 5), + LocationName.RoxasSecretAnsemReport8: WorldLocationData(0x1ED1, 1), + LocationName.TwoBecomeOne: WorldLocationData(0x1ED1, 1), + LocationName.MemorysSkyscaperMythrilCrystal: WorldLocationData(0x23CD, 3), + LocationName.MemorysSkyscaperAPBoost: WorldLocationData(0x23DC, 0), + LocationName.MemorysSkyscaperMythrilStone: WorldLocationData(0x23DC, 1), + LocationName.TheBrinkofDespairDarkCityMap: WorldLocationData(0x23CA, 5), + LocationName.TheBrinkofDespairOrichalcumPlus: WorldLocationData(0x23DA, 2), + LocationName.NothingsCallMythrilGem: WorldLocationData(0x23CC, 0), + LocationName.NothingsCallOrichalcum: WorldLocationData(0x23CC, 1), + LocationName.TwilightsViewCosmicBelt: WorldLocationData(0x23CA, 6), + LocationName.XigbarBonus: WorldLocationData(0x3706, 7), + LocationName.XigbarSecretAnsemReport3: WorldLocationData(0x1ED2, 2), + LocationName.NaughtsSkywayMythrilGem: WorldLocationData(0x23CC, 2), + LocationName.NaughtsSkywayOrichalcum: WorldLocationData(0x23CC, 3), + LocationName.NaughtsSkywayMythrilCrystal: WorldLocationData(0x23CC, 4), + LocationName.Oblivion: WorldLocationData(0x1ED2, 4), + LocationName.CastleThatNeverWasMap: WorldLocationData(0x1ED2, 4), + LocationName.Luxord: WorldLocationData(0x3707, 0), + LocationName.LuxordGetBonus: WorldLocationData(0x3707, 0), + LocationName.LuxordSecretAnsemReport9: WorldLocationData(0x1ED2, 7), + LocationName.SaixBonus: WorldLocationData(0x3707, 1), + LocationName.SaixSecretAnsemReport12: WorldLocationData(0x1ED3, 2), + LocationName.PreXemnas1SecretAnsemReport11: WorldLocationData(0x1ED3, 6), + LocationName.RuinandCreationsPassageMythrilStone: WorldLocationData(0x23CC, 7), + LocationName.RuinandCreationsPassageAPBoost: WorldLocationData(0x23CD, 0), + LocationName.RuinandCreationsPassageMythrilCrystal: WorldLocationData(0x23CD, 1), + LocationName.RuinandCreationsPassageOrichalcum: WorldLocationData(0x23CD, 2), + LocationName.Xemnas1: WorldLocationData(0x3707, 2), + LocationName.Xemnas1GetBonus: WorldLocationData(0x3707, 2), + LocationName.Xemnas1SecretAnsemReport13: WorldLocationData(0x1ED4, 5), + LocationName.FinalXemnas: WorldLocationData(0x1ED8, 1), + LocationName.XemnasDataPowerBoost: WorldLocationData(0x1EDA, 2), + LocationName.XigbarDataDefenseBoost: WorldLocationData(0x1ED9, 7), + LocationName.SaixDataDefenseBoost: WorldLocationData(0x1EDA, 0), + LocationName.LuxordDataAPBoost: WorldLocationData(0x1EDA, 1), + LocationName.RoxasDataMagicBoost: WorldLocationData(0x1ED9, 6), +} +SoraLevels = { + # LocationName.Lvl1: WorldLocationData(0xFFFF,1), + LocationName.Lvl2: WorldLocationData(0xFFFF, 2), + LocationName.Lvl3: WorldLocationData(0xFFFF, 3), + LocationName.Lvl4: WorldLocationData(0xFFFF, 4), + LocationName.Lvl5: WorldLocationData(0xFFFF, 5), + LocationName.Lvl6: WorldLocationData(0xFFFF, 6), + LocationName.Lvl7: WorldLocationData(0xFFFF, 7), + LocationName.Lvl8: WorldLocationData(0xFFFF, 8), + LocationName.Lvl9: WorldLocationData(0xFFFF, 9), + LocationName.Lvl10: WorldLocationData(0xFFFF, 10), + LocationName.Lvl11: WorldLocationData(0xFFFF, 11), + LocationName.Lvl12: WorldLocationData(0xFFFF, 12), + LocationName.Lvl13: WorldLocationData(0xFFFF, 13), + LocationName.Lvl14: WorldLocationData(0xFFFF, 14), + LocationName.Lvl15: WorldLocationData(0xFFFF, 15), + LocationName.Lvl16: WorldLocationData(0xFFFF, 16), + LocationName.Lvl17: WorldLocationData(0xFFFF, 17), + LocationName.Lvl18: WorldLocationData(0xFFFF, 18), + LocationName.Lvl19: WorldLocationData(0xFFFF, 19), + LocationName.Lvl20: WorldLocationData(0xFFFF, 20), + LocationName.Lvl21: WorldLocationData(0xFFFF, 21), + LocationName.Lvl22: WorldLocationData(0xFFFF, 22), + LocationName.Lvl23: WorldLocationData(0xFFFF, 23), + LocationName.Lvl24: WorldLocationData(0xFFFF, 24), + LocationName.Lvl25: WorldLocationData(0xFFFF, 25), + LocationName.Lvl26: WorldLocationData(0xFFFF, 26), + LocationName.Lvl27: WorldLocationData(0xFFFF, 27), + LocationName.Lvl28: WorldLocationData(0xFFFF, 28), + LocationName.Lvl29: WorldLocationData(0xFFFF, 29), + LocationName.Lvl30: WorldLocationData(0xFFFF, 30), + LocationName.Lvl31: WorldLocationData(0xFFFF, 31), + LocationName.Lvl32: WorldLocationData(0xFFFF, 32), + LocationName.Lvl33: WorldLocationData(0xFFFF, 33), + LocationName.Lvl34: WorldLocationData(0xFFFF, 34), + LocationName.Lvl35: WorldLocationData(0xFFFF, 35), + LocationName.Lvl36: WorldLocationData(0xFFFF, 36), + LocationName.Lvl37: WorldLocationData(0xFFFF, 37), + LocationName.Lvl38: WorldLocationData(0xFFFF, 38), + LocationName.Lvl39: WorldLocationData(0xFFFF, 39), + LocationName.Lvl40: WorldLocationData(0xFFFF, 40), + LocationName.Lvl41: WorldLocationData(0xFFFF, 41), + LocationName.Lvl42: WorldLocationData(0xFFFF, 42), + LocationName.Lvl43: WorldLocationData(0xFFFF, 43), + LocationName.Lvl44: WorldLocationData(0xFFFF, 44), + LocationName.Lvl45: WorldLocationData(0xFFFF, 45), + LocationName.Lvl46: WorldLocationData(0xFFFF, 46), + LocationName.Lvl47: WorldLocationData(0xFFFF, 47), + LocationName.Lvl48: WorldLocationData(0xFFFF, 48), + LocationName.Lvl49: WorldLocationData(0xFFFF, 49), + LocationName.Lvl50: WorldLocationData(0xFFFF, 50), + LocationName.Lvl51: WorldLocationData(0xFFFF, 51), + LocationName.Lvl52: WorldLocationData(0xFFFF, 52), + LocationName.Lvl53: WorldLocationData(0xFFFF, 53), + LocationName.Lvl54: WorldLocationData(0xFFFF, 54), + LocationName.Lvl55: WorldLocationData(0xFFFF, 55), + LocationName.Lvl56: WorldLocationData(0xFFFF, 56), + LocationName.Lvl57: WorldLocationData(0xFFFF, 57), + LocationName.Lvl58: WorldLocationData(0xFFFF, 58), + LocationName.Lvl59: WorldLocationData(0xFFFF, 59), + LocationName.Lvl60: WorldLocationData(0xFFFF, 60), + LocationName.Lvl61: WorldLocationData(0xFFFF, 61), + LocationName.Lvl62: WorldLocationData(0xFFFF, 62), + LocationName.Lvl63: WorldLocationData(0xFFFF, 63), + LocationName.Lvl64: WorldLocationData(0xFFFF, 64), + LocationName.Lvl65: WorldLocationData(0xFFFF, 65), + LocationName.Lvl66: WorldLocationData(0xFFFF, 66), + LocationName.Lvl67: WorldLocationData(0xFFFF, 67), + LocationName.Lvl68: WorldLocationData(0xFFFF, 68), + LocationName.Lvl69: WorldLocationData(0xFFFF, 69), + LocationName.Lvl70: WorldLocationData(0xFFFF, 70), + LocationName.Lvl71: WorldLocationData(0xFFFF, 71), + LocationName.Lvl72: WorldLocationData(0xFFFF, 72), + LocationName.Lvl73: WorldLocationData(0xFFFF, 73), + LocationName.Lvl74: WorldLocationData(0xFFFF, 74), + LocationName.Lvl75: WorldLocationData(0xFFFF, 75), + LocationName.Lvl76: WorldLocationData(0xFFFF, 76), + LocationName.Lvl77: WorldLocationData(0xFFFF, 77), + LocationName.Lvl78: WorldLocationData(0xFFFF, 78), + LocationName.Lvl79: WorldLocationData(0xFFFF, 79), + LocationName.Lvl80: WorldLocationData(0xFFFF, 80), + LocationName.Lvl81: WorldLocationData(0xFFFF, 81), + LocationName.Lvl82: WorldLocationData(0xFFFF, 82), + LocationName.Lvl83: WorldLocationData(0xFFFF, 83), + LocationName.Lvl84: WorldLocationData(0xFFFF, 84), + LocationName.Lvl85: WorldLocationData(0xFFFF, 85), + LocationName.Lvl86: WorldLocationData(0xFFFF, 86), + LocationName.Lvl87: WorldLocationData(0xFFFF, 87), + LocationName.Lvl88: WorldLocationData(0xFFFF, 88), + LocationName.Lvl89: WorldLocationData(0xFFFF, 89), + LocationName.Lvl90: WorldLocationData(0xFFFF, 90), + LocationName.Lvl91: WorldLocationData(0xFFFF, 91), + LocationName.Lvl92: WorldLocationData(0xFFFF, 92), + LocationName.Lvl93: WorldLocationData(0xFFFF, 93), + LocationName.Lvl94: WorldLocationData(0xFFFF, 94), + LocationName.Lvl95: WorldLocationData(0xFFFF, 95), + LocationName.Lvl96: WorldLocationData(0xFFFF, 96), + LocationName.Lvl97: WorldLocationData(0xFFFF, 97), + LocationName.Lvl98: WorldLocationData(0xFFFF, 98), + LocationName.Lvl99: WorldLocationData(0xFFFF, 99), +} + +ValorLevels = { + # LocationName.Valorlvl1: WorldLocationData(0x32F6, 1), + LocationName.Valorlvl2: WorldLocationData(0x32F6, 2), + LocationName.Valorlvl3: WorldLocationData(0x32F6, 3), + LocationName.Valorlvl4: WorldLocationData(0x32F6, 4), + LocationName.Valorlvl5: WorldLocationData(0x32F6, 5), + LocationName.Valorlvl6: WorldLocationData(0x32F6, 6), + LocationName.Valorlvl7: WorldLocationData(0x32F6, 7), +} + +WisdomLevels = { + # LocationName.Wisdomlvl1: WorldLocationData(0x332E, 1), + LocationName.Wisdomlvl2: WorldLocationData(0x332E, 2), + LocationName.Wisdomlvl3: WorldLocationData(0x332E, 3), + LocationName.Wisdomlvl4: WorldLocationData(0x332E, 4), + LocationName.Wisdomlvl5: WorldLocationData(0x332E, 5), + LocationName.Wisdomlvl6: WorldLocationData(0x332E, 6), + LocationName.Wisdomlvl7: WorldLocationData(0x332E, 7), +} + +LimitLevels = { + # LocationName.Limitlvl1: WorldLocationData(0x3366, 1), + LocationName.Limitlvl2: WorldLocationData(0x3366, 2), + LocationName.Limitlvl3: WorldLocationData(0x3366, 3), + LocationName.Limitlvl4: WorldLocationData(0x3366, 4), + LocationName.Limitlvl5: WorldLocationData(0x3366, 5), + LocationName.Limitlvl6: WorldLocationData(0x3366, 6), + LocationName.Limitlvl7: WorldLocationData(0x3366, 7), +} +MasterLevels = { + # LocationName.Masterlvl1: WorldLocationData(0x339E, 1), + LocationName.Masterlvl2: WorldLocationData(0x339E, 2), + LocationName.Masterlvl3: WorldLocationData(0x339E, 3), + LocationName.Masterlvl4: WorldLocationData(0x339E, 4), + LocationName.Masterlvl5: WorldLocationData(0x339E, 5), + LocationName.Masterlvl6: WorldLocationData(0x339E, 6), + LocationName.Masterlvl7: WorldLocationData(0x339E, 7), +} +FinalLevels = { + # LocationName.Finallvl1: WorldLocationData(0x33D6,1), + LocationName.Finallvl2: WorldLocationData(0x33D6, 2), + LocationName.Finallvl3: WorldLocationData(0x33D6, 3), + LocationName.Finallvl4: WorldLocationData(0x33D6, 4), + LocationName.Finallvl5: WorldLocationData(0x33D6, 5), + LocationName.Finallvl6: WorldLocationData(0x33D6, 6), + LocationName.Finallvl7: WorldLocationData(0x33D6, 7), + +} +weaponSlots = { + LocationName.AdamantShield: WorldLocationData(0x35E6, 1), + LocationName.AkashicRecord: WorldLocationData(0x35ED, 1), + LocationName.ChainGear: WorldLocationData(0x35E7, 1), + LocationName.DreamCloud: WorldLocationData(0x35EA, 1), + LocationName.FallingStar: WorldLocationData(0x35E9, 1), + LocationName.FrozenPride2: WorldLocationData(0x36A2, 1), + LocationName.GenjiShield: WorldLocationData(0x35EC, 1), + LocationName.KnightDefender: WorldLocationData(0x35EB, 1), + LocationName.MajesticMushroom: WorldLocationData(0x36A5, 1), + LocationName.MajesticMushroom2: WorldLocationData(0x36A6, 1), + LocationName.NobodyGuard: WorldLocationData(0x35EE, 1), + LocationName.OgreShield: WorldLocationData(0x35E8, 1), + LocationName.SaveTheKing2: WorldLocationData(0x3693, 1), + LocationName.UltimateMushroom: WorldLocationData(0x36A7, 1), + + LocationName.CometStaff: WorldLocationData(0x35F2, 1), + LocationName.HammerStaff: WorldLocationData(0x35EF, 1), + LocationName.LordsBroom: WorldLocationData(0x35F3, 1), + LocationName.MeteorStaff: WorldLocationData(0x35F1, 1), + LocationName.NobodyLance: WorldLocationData(0x35F6, 1), + LocationName.PreciousMushroom: WorldLocationData(0x369E, 1), + LocationName.PreciousMushroom2: WorldLocationData(0x369F, 1), + LocationName.PremiumMushroom: WorldLocationData(0x36A0, 1), + LocationName.RisingDragon: WorldLocationData(0x35F5, 1), + LocationName.SaveTheQueen2: WorldLocationData(0x3692, 1), + LocationName.ShamansRelic: WorldLocationData(0x35F7, 1), + LocationName.VictoryBell: WorldLocationData(0x35F0, 1), + LocationName.WisdomWand: WorldLocationData(0x35F4, 1), + LocationName.Centurion2: WorldLocationData(0x369B, 1), + + LocationName.OathkeeperSlot: WorldLocationData(0x35A2, 1), + LocationName.OblivionSlot: WorldLocationData(0x35A3, 1), + LocationName.StarSeekerSlot: WorldLocationData(0x367B, 1), + LocationName.HiddenDragonSlot: WorldLocationData(0x367C, 1), + LocationName.HerosCrestSlot: WorldLocationData(0x367F, 1), + LocationName.MonochromeSlot: WorldLocationData(0x3680, 1), + LocationName.FollowtheWindSlot: WorldLocationData(0x3681, 1), + LocationName.CircleofLifeSlot: WorldLocationData(0x3682, 1), + LocationName.PhotonDebuggerSlot: WorldLocationData(0x3683, 1), + LocationName.GullWingSlot: WorldLocationData(0x3684, 1), + LocationName.RumblingRoseSlot: WorldLocationData(0x3685, 1), + LocationName.GuardianSoulSlot: WorldLocationData(0x3686, 1), + LocationName.WishingLampSlot: WorldLocationData(0x3687, 1), + LocationName.DecisivePumpkinSlot: WorldLocationData(0x3688, 1), + LocationName.SleepingLionSlot: WorldLocationData(0x3689, 1), + LocationName.SweetMemoriesSlot: WorldLocationData(0x368A, 1), + LocationName.MysteriousAbyssSlot: WorldLocationData(0x368B, 1), + LocationName.TwoBecomeOneSlot: WorldLocationData(0x3698, 1), + LocationName.FatalCrestSlot: WorldLocationData(0x368C, 1), + LocationName.BondofFlameSlot: WorldLocationData(0x368D, 1), + LocationName.FenrirSlot: WorldLocationData(0x368E, 1), + LocationName.UltimaWeaponSlot: WorldLocationData(0x368F, 1), + LocationName.WinnersProofSlot: WorldLocationData(0x3699, 1), + LocationName.PurebloodSlot: WorldLocationData(0x35BF, 1), +} + +formSlots = { + LocationName.FAKESlot: WorldLocationData(0x36C0, 1), + LocationName.DetectionSaberSlot: WorldLocationData(0x36C0, 6), + LocationName.EdgeofUltimaSlot: WorldLocationData(0x36C0, 4), +} + +tornPageLocks = { + "TornPage1": WorldLocationData(0x1DB7, 4), # --Scenario_1_start + "TornPage2": WorldLocationData(0x1DB7, 7), # --Scenario_2_start + "TornPage3": WorldLocationData(0x1DB8, 2), # --Scenario_3_start + "TornPage4": WorldLocationData(0x1DB8, 4), # --Scenario_4_start + "TornPage5": WorldLocationData(0x1DB8, 7), # --Scenario_5_start +} +all_world_locations = { + **TWTNW_Checks, + **TT_Checks, + **TT_Checks, + **HB_Checks, + **BC_Checks, + **Oc_Checks, + **AG_Checks, + **LoD_Checks, + **HundredAcreChecks, + **PL_Checks, + **DC_Checks, + **TR_Checks, + **HT_Checks, + **HB_Checks, + **PR_Checks, + **SP_Checks, + **TWTNW_Checks, + **HB_Checks, +} + +levels_locations = { + **SoraLevels, + **ValorLevels, + **WisdomLevels, + **LimitLevels, + **MasterLevels, + **FinalLevels, +} diff --git a/worlds/kh2/XPValues.py b/worlds/kh2/XPValues.py new file mode 100644 index 0000000000..46e59acaca --- /dev/null +++ b/worlds/kh2/XPValues.py @@ -0,0 +1,119 @@ +lvlStats = [ + "str", + "mag", + "def", + "ap", + "", +] + +formExp = { + 0: {1: 6, 2: 16, 3: 25, 4: 42, 5: 63, 6: 98, 7: 0}, + 1: {1: 80, 2: 160, 3: 280, 4: 448, 5: 560, 6: 672, 7: 0}, + 2: {1: 12, 2: 24, 3: 48, 4: 76, 5: 133, 6: 157, 7: 0}, + 3: {1: 3, 2: 6, 3: 12, 4: 19, 5: 23, 6: 27, 7: 0}, + 4: {1: 40, 2: 80, 3: 140, 4: 224, 5: 448, 6: 668, 7: 0}, + 5: {1: 12, 2: 24, 3: 48, 4: 76, 5: 133, 6: 157, 7: 0} +} + +soraExp = [ + 0, + 40, + 100, + 184, + 296, + 440, + 620, + 840, + 1128, + 1492, + 1940, + 2480, + 3120, + 3902, + 4838, + 5940, + 7260, + 8814, + 10618, + 12688, + 15088, + 17838, + 20949, + 24433, + 28302, + 32622, + 37407, + 42671, + 48485, + 54865, + 61886, + 69566, + 77984, + 87160, + 97177, + 108057, + 119887, + 132691, + 146560, + 161520, + 177666, + 195026, + 213699, + 233715, + 255177, + 278117, + 302642, + 328786, + 356660, + 386378, + 417978, + 450378, + 483578, + 517578, + 552378, + 587978, + 624378, + 661578, + 699578, + 738378, + 777978, + 818378, + 859578, + 901578, + 944378, + 987978, + 1032378, + 1077578, + 1123578, + 1170378, + 1217978, + 1266378, + 1315578, + 1365578, + 1416378, + 1467978, + 1520378, + 1573578, + 1627578, + 1682378, + 1737978, + 1794378, + 1851578, + 1909578, + 1968378, + 2027978, + 2088378, + 2149578, + 2211578, + 2274378, + 2337978, + 2402378, + 2467578, + 2533578, + 2600378, + 2667978, + 2736378, + 2805578, + 2875578, + 2875578 +] diff --git a/worlds/kh2/__init__.py b/worlds/kh2/__init__.py new file mode 100644 index 0000000000..e36c81e806 --- /dev/null +++ b/worlds/kh2/__init__.py @@ -0,0 +1,340 @@ + +from BaseClasses import Tutorial, ItemClassification +import logging + +from .Items import * +from .Locations import all_locations, setup_locations, exclusion_table +from .Names import ItemName, LocationName +from .OpenKH import patch_kh2 +from .Options import KH2_Options +from .Regions import create_regions, connect_regions +from .Rules import set_rules +from ..AutoWorld import World, WebWorld +from .logic import KH2Logic + + +class KingdomHearts2Web(WebWorld): + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to playing Kingdom Hearts 2 Final Mix with Archipelago.", + "English", + "setup_en.md", + "setup/en", + ["JaredWeakStrike"] + )] + + +# noinspection PyUnresolvedReferences +class KH2World(World): + """ + Kingdom Hearts II is an action role-playing game developed and published by Square Enix and released in 2005. + It is the sequel to Kingdom Hearts and Kingdom Hearts: Chain of Memories, and like the two previous games, + focuses on Sora and his friends' continued battle against the Darkness. + """ + game: str = "Kingdom Hearts 2" + web = KingdomHearts2Web() + data_version = 1 + option_definitions = KH2_Options + item_name_to_id = {name: data.code for name, data in item_dictionary_table.items()} + location_name_to_id = {item_name: data.code for item_name, data in all_locations.items() if data.code} + item_name_groups = item_groups + + def __init__(self, multiworld: "MultiWorld", player: int): + super().__init__(multiworld, player) + self.BountiesRequired = None + self.BountiesAmount = None + self.hitlist = None + self.LocalItems = {} + self.RandomSuperBoss = list() + self.filler_items = list() + self.item_quantity_dict = {} + self.donald_ability_pool = list() + self.goofy_ability_pool = list() + self.sora_keyblade_ability_pool = list() + self.keyblade_slot_copy = list(Locations.Keyblade_Slots.keys()) + self.keyblade_slot_copy.remove(LocationName.KingdomKeySlot) + self.totalLocations = len(all_locations.items()) + self.growth_list = list() + for x in range(4): + self.growth_list.extend(Movement_Table.keys()) + self.visitlocking_dict = Progression_Dicts["AllVisitLocking"] + + def fill_slot_data(self) -> dict: + return {"hitlist": self.hitlist, + "LocalItems": self.LocalItems, + "Goal": self.multiworld.Goal[self.player].value, + "FinalXemnas": self.multiworld.FinalXemnas[self.player].value, + "LuckyEmblemsRequired": self.multiworld.LuckyEmblemsRequired[self.player].value, + "BountyRequired": self.multiworld.BountyRequired[self.player].value} + + def create_item(self, name: str, ) -> Item: + data = item_dictionary_table[name] + if name in Progression_Dicts["Progression"]: + item_classification = ItemClassification.progression + else: + item_classification = ItemClassification.filler + + created_item = KH2Item(name, item_classification, data.code, self.player) + + return created_item + + def generate_early(self) -> None: + # Item Quantity dict because Abilities can be a problem for KH2's Software. + for item, data in item_dictionary_table.items(): + self.item_quantity_dict[item] = data.quantity + + if self.multiworld.KeybladeAbilities[self.player] == "support": + self.sora_keyblade_ability_pool.extend(SupportAbility_Table.keys()) + elif self.multiworld.KeybladeAbilities[self.player] == "action": + self.sora_keyblade_ability_pool.extend(ActionAbility_Table.keys()) + else: + # both action and support on keyblades. + # TODO: make option to just exclude scom + self.sora_keyblade_ability_pool.extend(ActionAbility_Table.keys()) + self.sora_keyblade_ability_pool.extend(SupportAbility_Table.keys()) + + for item, value in self.multiworld.start_inventory[self.player].value.items(): + if item in ActionAbility_Table.keys() or item in SupportAbility_Table.keys() or exclusionItem_table["StatUps"]: + # cannot have more than the quantity for abilties + if value > item_dictionary_table[item].quantity: + logging.info(f"{self.multiworld.get_file_safe_player_name(self.player)} cannot have more than {item_dictionary_table[item].quantity} of {item}" + f"Changing the amount to the max amount") + value = item_dictionary_table[item].quantity + self.item_quantity_dict[item] -= value + + # Option to turn off Promise Charm Item + if not self.multiworld.Promise_Charm[self.player]: + self.item_quantity_dict[ItemName.PromiseCharm] = 0 + + for ability in self.multiworld.BlacklistKeyblade[self.player].value: + if ability in self.sora_keyblade_ability_pool: + self.sora_keyblade_ability_pool.remove(ability) + + # Option to turn off all superbosses. Can do this individually but its like 20+ checks + if not self.multiworld.SuperBosses[self.player] and not self.multiworld.Goal[self.player] == "hitlist": + for superboss in exclusion_table["Datas"]: + self.multiworld.exclude_locations[self.player].value.add(superboss) + for superboss in exclusion_table["SuperBosses"]: + self.multiworld.exclude_locations[self.player].value.add(superboss) + + # Option to turn off Olympus Colosseum Cups. + if self.multiworld.Cups[self.player] == "no_cups": + for cup in exclusion_table["Cups"]: + self.multiworld.exclude_locations[self.player].value.add(cup) + # exclude only hades paradox. If cups and hades paradox then nothing is excluded + elif self.multiworld.Cups[self.player] == "cups": + self.multiworld.exclude_locations[self.player].value.add(LocationName.HadesCupTrophyParadoxCups) + + if self.multiworld.Goal[self.player] == "lucky_emblem_hunt": + luckyemblemamount = self.multiworld.LuckyEmblemsAmount[self.player].value + luckyemblemrequired = self.multiworld.LuckyEmblemsRequired[self.player].value + if luckyemblemamount < luckyemblemrequired: + logging.info(f"Lucky Emblem Amount {self.multiworld.LuckyEmblemsAmount[self.player].value} is less than required " + f"{self.multiworld.LuckyEmblemsRequired[self.player].value} for player {self.multiworld.get_file_safe_player_name(self.player)}." + f" Setting amount to {self.multiworld.LuckyEmblemsRequired[self.player].value}") + luckyemblemamount = max(luckyemblemamount, luckyemblemrequired) + self.multiworld.LuckyEmblemsAmount[self.player].value = luckyemblemamount + + self.item_quantity_dict[ItemName.LuckyEmblem] = item_dictionary_table[ItemName.LuckyEmblem].quantity + luckyemblemamount + # give this proof to unlock the final door once the player has the amount of lucky emblem required + self.item_quantity_dict[ItemName.ProofofNonexistence] = 0 + + # hitlist + elif self.multiworld.Goal[self.player] == "hitlist": + self.RandomSuperBoss.extend(exclusion_table["Hitlist"]) + self.BountiesAmount = self.multiworld.BountyAmount[self.player].value + self.BountiesRequired = self.multiworld.BountyRequired[self.player].value + + for location in self.multiworld.exclude_locations[self.player].value: + if location in self.RandomSuperBoss: + self.RandomSuperBoss.remove(location) + # Testing if the player has the right amount of Bounties for Completion. + if len(self.RandomSuperBoss) < self.BountiesAmount: + logging.info(f"{self.multiworld.get_file_safe_player_name(self.player)} has too many bounties than bosses." + f" Setting total bounties to {len(self.RandomSuperBoss)}") + self.BountiesAmount = len(self.RandomSuperBoss) + self.multiworld.BountyAmount[self.player].value = self.BountiesAmount + + if len(self.RandomSuperBoss) < self.BountiesRequired: + logging.info(f"{self.multiworld.get_file_safe_player_name(self.player)} has too many required bounties." + f" Setting required bounties to {len(self.RandomSuperBoss)}") + self.BountiesRequired = len(self.RandomSuperBoss) + self.multiworld.BountyRequired[self.player].value = self.BountiesRequired + + if self.BountiesAmount < self.BountiesRequired: + logging.info(f"Bounties Amount {self.multiworld.BountyAmount[self.player].value} is less than required " + f"{self.multiworld.BountyRequired[self.player].value} for player {self.multiworld.get_file_safe_player_name(self.player)}." + f" Setting amount to {self.multiworld.BountyRequired[self.player].value}") + self.BountiesAmount = max(self.BountiesAmount, self.BountiesRequired) + self.multiworld.BountyAmount[self.player].value = self.BountiesAmount + + self.multiworld.start_hints[self.player].value.add(ItemName.Bounty) + self.item_quantity_dict[ItemName.ProofofNonexistence] = 0 + + while len(self.sora_keyblade_ability_pool) < len(self.keyblade_slot_copy): + self.sora_keyblade_ability_pool.append( + self.multiworld.per_slot_randoms[self.player].choice(list(SupportAbility_Table.keys()))) + + for item in DonaldAbility_Table.keys(): + data = self.item_quantity_dict[item] + for _ in range(data): + self.donald_ability_pool.append(item) + self.item_quantity_dict[item] = 0 + # 32 is the amount of donald abilities + while len(self.donald_ability_pool) < 32: + self.donald_ability_pool.append(self.multiworld.per_slot_randoms[self.player].choice(self.donald_ability_pool)) + + for item in GoofyAbility_Table.keys(): + data = self.item_quantity_dict[item] + for _ in range(data): + self.goofy_ability_pool.append(item) + self.item_quantity_dict[item] = 0 + # 32 is the amount of goofy abilities + while len(self.goofy_ability_pool) < 33: + self.goofy_ability_pool.append(self.multiworld.per_slot_randoms[self.player].choice(self.goofy_ability_pool)) + + def generate_basic(self): + itempool: typing.List[KH2Item] = [] + + self.hitlist = list() + self.filler_items.extend(item_groups["Filler"]) + + if self.multiworld.FinalXemnas[self.player]: + self.multiworld.get_location(LocationName.FinalXemnas, self.player).place_locked_item( + self.create_item(ItemName.Victory)) + else: + self.multiworld.get_location(LocationName.FinalXemnas, self.player).place_locked_item( + self.create_item(self.multiworld.per_slot_randoms[self.player].choice(self.filler_items))) + self.totalLocations -= 1 + + if self.multiworld.Goal[self.player] == "hitlist": + for bounty in range(self.BountiesAmount): + randomBoss = self.multiworld.per_slot_randoms[self.player].choice(self.RandomSuperBoss) + self.multiworld.get_location(randomBoss, self.player).place_locked_item( + self.create_item(ItemName.Bounty)) + self.hitlist.append(self.location_name_to_id[randomBoss]) + self.RandomSuperBoss.remove(randomBoss) + self.totalLocations -= 1 + + # Kingdom Key cannot have No Experience so plandoed here instead of checking 26 times if its kingdom key + random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.sora_keyblade_ability_pool) + while random_ability == ItemName.NoExperience: + random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.sora_keyblade_ability_pool) + self.multiworld.get_location(LocationName.KingdomKeySlot, self.player).place_locked_item(self.create_item(random_ability)) + self.item_quantity_dict[random_ability] -= 1 + self.sora_keyblade_ability_pool.remove(random_ability) + self.totalLocations -= 1 + + # plando keyblades because they can only have abilities + for keyblade in self.keyblade_slot_copy: + random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.sora_keyblade_ability_pool) + self.multiworld.get_location(keyblade, self.player).place_locked_item(self.create_item(random_ability)) + self.item_quantity_dict[random_ability] -= 1 + self.sora_keyblade_ability_pool.remove(random_ability) + self.totalLocations -= 1 + + # Placing Donald Abilities on donald locations + for donaldLocation in Locations.Donald_Checks.keys(): + random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.donald_ability_pool) + self.multiworld.get_location(donaldLocation, self.player).place_locked_item( + self.create_item(random_ability)) + self.totalLocations -= 1 + self.donald_ability_pool.remove(random_ability) + + # Placing Goofy Abilities on goofy locations + for goofyLocation in Locations.Goofy_Checks.keys(): + random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.goofy_ability_pool) + self.multiworld.get_location(goofyLocation, self.player).place_locked_item(self.create_item(random_ability)) + self.totalLocations -= 1 + self.goofy_ability_pool.remove(random_ability) + + # same item placed because you can only get one of these 2 locations + # they are both under the same flag so the player gets both locations just one of the two items + random_stt_item = self.multiworld.per_slot_randoms[self.player].choice(self.filler_items) + self.multiworld.get_location(LocationName.JunkChampionBelt, self.player).place_locked_item( + self.create_item(random_stt_item)) + self.multiworld.get_location(LocationName.JunkMedal, self.player).place_locked_item( + self.create_item(random_stt_item)) + self.totalLocations -= 2 + + if self.multiworld.Schmovement[self.player] != "level_0": + for _ in range(self.multiworld.Schmovement[self.player].value): + for name in {ItemName.HighJump, ItemName.QuickRun, ItemName.DodgeRoll, ItemName.AerialDodge, + ItemName.Glide}: + self.item_quantity_dict[name] -= 1 + self.growth_list.remove(name) + self.multiworld.push_precollected(self.create_item(name)) + + if self.multiworld.RandomGrowth[self.player] != 0: + max_growth = min(self.multiworld.RandomGrowth[self.player].value, len(self.growth_list)) + for _ in range(max_growth): + random_growth = self.multiworld.per_slot_randoms[self.player].choice(self.growth_list) + self.item_quantity_dict[random_growth] -= 1 + self.growth_list.remove(random_growth) + self.multiworld.push_precollected(self.create_item(random_growth)) + + # no visit locking + if self.multiworld.Visitlocking[self.player] == "no_visit_locking": + for item, amount in Progression_Dicts["AllVisitLocking"].items(): + for _ in range(amount): + self.multiworld.push_precollected(self.create_item(item)) + self.item_quantity_dict[item] -= 1 + if self.visitlocking_dict[item] == 0: + self.visitlocking_dict.pop(item) + self.visitlocking_dict[item] -= 1 + + # first and second visit locking + elif self.multiworld.Visitlocking[self.player] == "second_visit_locking": + for item in Progression_Dicts["2VisitLocking"]: + self.item_quantity_dict[item] -= 1 + self.visitlocking_dict[item] -= 1 + if self.visitlocking_dict[item] == 0: + self.visitlocking_dict.pop(item) + self.multiworld.push_precollected(self.create_item(item)) + + for _ in range(self.multiworld.RandomVisitLockingItem[self.player].value): + if sum(self.visitlocking_dict.values()) <= 0: + break + visitlocking_set = list(self.visitlocking_dict.keys()) + item = self.multiworld.per_slot_randoms[self.player].choice(visitlocking_set) + self.item_quantity_dict[item] -= 1 + self.visitlocking_dict[item] -= 1 + if self.visitlocking_dict[item] == 0: + self.visitlocking_dict.pop(item) + self.multiworld.push_precollected(self.create_item(item)) + + # there are levels but level 1 is there to keep code clean + if self.multiworld.LevelDepth[self.player] == "level_99_sanity": + # level 99 sanity + self.totalLocations -= 1 + elif self.multiworld.LevelDepth[self.player] == "level_50_sanity": + # level 50 sanity + self.totalLocations -= 50 + elif self.multiworld.LevelDepth[self.player] == "level_1": + # level 1. No checks on levels + self.totalLocations -= 99 + else: + # level 50/99 since they contain the same amount of levels + self.totalLocations -= 76 + + for item in item_dictionary_table: + data = self.item_quantity_dict[item] + for _ in range(data): + itempool.append(self.create_item(item)) + + # Creating filler for unfilled locations + while len(itempool) < self.totalLocations: + item = self.multiworld.per_slot_randoms[self.player].choice(self.filler_items) + itempool += [self.create_item(item)] + self.multiworld.itempool += itempool + + def create_regions(self): + location_table = setup_locations() + create_regions(self.multiworld, self.player, location_table) + connect_regions(self.multiworld, self.player) + + def set_rules(self): + set_rules(self.multiworld, self.player) + + def generate_output(self, output_directory: str): + patch_kh2(self, output_directory) diff --git a/worlds/kh2/docs/en_Kingdom Hearts 2.md b/worlds/kh2/docs/en_Kingdom Hearts 2.md new file mode 100644 index 0000000000..bb14731699 --- /dev/null +++ b/worlds/kh2/docs/en_Kingdom Hearts 2.md @@ -0,0 +1,107 @@ +# Kingdom Hearts 2 + +

Changes from the vanilla game

+ +This randomizer takes Kingdom Hearts 2 and randomizes the locations of the items for a more dynamic play experience. The items that randomize currently are all items within Chests, Popups, Get Bonuses, Form Levels, and Sora's Levels. This allows abilities that Sora would normally have to also be placed on Keyblades with random stats. With several options on ways to finish the game. + +

Where is the settings page

+ +The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file. + + +

What is randomized in this game?

+ + +The Chests, Popups, Get Bonuses, Form Levels, and Sora's Levels. + +

What Kingdom Hearts 2 items can appear in other players' worlds?

+ + +Every item in the game with the exception being party members' abilities. + +

What is The Garden of Assemblage "GoA"?

+ + +The Garden of Assemblage Mod made by Sonicshadowsilver2 and Num turns the Garden of Assemblage into a “World Hub” where each portal takes you to one of the game worlds (as opposed to having a world map). This allows you to enter worlds at any time, and world progression is maintained for each world individually. + +

What does another world's item look like in Kingdom Hearts 2?

+ + +In Kingdom Hearts 2, items which need to be sent to other worlds appear in any location that has a item in the vanilla game. They are represented by the Archipelago icon, and must be "picked up" as if it were a normal item. Upon obtaining the item, it will be sent to its home world. + +

When the player receives an item, what happens?

+ + +It is added to your inventory. If you obtain magic, you will need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable. + +

What Happens if I die before Room Saving?

+ + +When you die in Kingdom Hearts 2, you are reverted to the last non-boss room you entered and your status is reverted to what it was at that time. However, in archipelago, any item that you have sent/received will not be taken away from the player, any chest you have opened will remain open, and you will keep your level but lose the expereince. Unlike vanilla Kingdom Hearts 2. + + +For example, if you are fighting Roxas and you receive Reflect Element and you die fighting Roxas, you will keep that reflect. You will still need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable. + +

Customization options:

+ + +- Choose a goal from the list below (with an additional option to Kill Final Xemnas alongside your goal). + 1. Obtain Three Proofs. + 2. Obtain a desired amount of Lucky Emblems. + 3. Obtain a desired amount of Bounties that are on late locations. +- Customize how many World Locking Items You Need to Progress in that World. +- Customize the Amount of World Locking Items You Start With. +- Customize how many locations you want on Sora's Levels. +- Customize the EXP Multiplier of everything that affects Sora. +- Customize the Available Abilities on Keyblades. +- Customize the level of Progressive Movement (Growth Abilities) you start with. +- Customize the amount of Progressive Movement (Growth Abilities) you start with. +- Customize start inventory, i.e., begin every run with certain items or spells of your choice. + +

Quality of life:

+ + +With the help of Shananas, Num, and ZakTheRobot we have many QoL features such are: + + +- Faster Wardrobe. +- Faster Water Jafar Chase. +- Carpet Skip. +- Start with Lion Dash. +- Faster Urns. +- Removal of Absent Silhouette and go straight into the Data Fights. +- And much more can be found at [Kingdom Hearts 2 GoA Overview](https://tommadness.github.io/KH2Randomizer/overview/) + +

Recommendation

+ +- Recommended making a save at the start of the GoA before opening anything. This will be the recommended file to load if/when your game crashes. + - If you don't want to have a save in the GoA. Disconnect the client, load the auto save, and then reconnect the client after it loads the auto save. +- Recommended to set fps limit to 60fps. +- Recommended to run the game in windows/borderless windowed mode. Fullscreen is stable but the game can crash if you alt-tab out. +- Recommend viewing [Requirements/logic sheet](https://docs.google.com/spreadsheets/d/1Embae0t7pIrbzvX-NRywk7bTHHEvuFzzQBUUpSUL7Ak/edit?usp=sharing) + +

F.A.Q.

+ +- Why am I not getting magic? + - If you obtain magic, you will need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable. +- Why am I missing worlds/portals in the GoA? + - You are missing the required visit locking item to access the world/portal. +- What versions of Kingdom Hearts 2 are supported? + - Currently `only` the most up to date version on the Epic Game Store is supported `1.0.0.8_WW`. Emulator may be added in the future. +- Why did I crash? + - The port of Kingdom Hearts 2 can and will randomly crash, this is the fault of the game not the randomizer or the archipelago client. + - If you have a continuous/constant crash (in the same area/event every time) you will want to reverify your installed files. This can be done by doing the following: Open Epic Game Store --> Library --> Click Triple Dots --> Manage --> Verify +- Why am I getting dummy items or letters? + - You will need to get the `JaredWeakStrike/APCompanion` (you can find how to get this in the setup guide) +- Why is my HP/MP continuously increasing without stopping? + - You do not have `JaredWeakStrike/APCompanion` setup correctly. Make Sure it is above the GOA in the mod manager. +- Why am I not sending or receiving items? + - Make sure you are connected to the KH2 client and the correct room (for more information reference the setup guide) +- Why did I not load in to the correct visit + - You need to trigger a cutscene or visit The World That Never Was for it to update you have recevied the item. +- Why should I install the auto save mod at `KH2FM-Mods-equations19/auto-save`? + - Because Kingdom Hearts 2 is prone to crashes and will keep you from losing your progress. +- How do I load an auto save? + - To load an auto-save, hold down the Select or your equivalent on your prefered controller while choosing a file. Make sure to hold the button down the whole time. +- How do I do a soft reset? + - Hold L1+L2+R1+R2+Start or your equivalent on your prefered controller at the same time to immediately reset the game to the start screen. diff --git a/worlds/kh2/docs/setup_en.md b/worlds/kh2/docs/setup_en.md new file mode 100644 index 0000000000..e54d5c40b3 --- /dev/null +++ b/worlds/kh2/docs/setup_en.md @@ -0,0 +1,48 @@ +# Kingdom Hearts 2 Archipelago Setup Guide +

Quick Links

+ +- [Main Page](../../../../games/Kingdom%20Hearts%202/info/en) +- [Settings Page](../../../../games/Kingdom%20Hearts%202/player-settings) + +

Setting up the Mod Manager

+ +Follow this Guide [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/) + +

Loading A Seed

+ +When you generate a game you will see a download link for a KH2 .zip seed on the room page. Download the seed then open OpenKH Mod Manager and click the green plus and `Select and install Mod Archive`. Make sure the seed is on the top of the list (Highest Priority) + +

Archipelago Compainion Mod and recommended mods

+ +Load this mod just like the GoA ROM you did during the KH2 Rando setup. `JaredWeakStrike/APCompanion` Have this mod second highest priority below the .zip seed +Load this mod just like the GoA ROM you did during the KH2 Rando setup. `KH2FM-Mods-equations19/auto-save` Location doesn't matter, recommended in case of crashes. +Load this mod just like the GoA ROM you did during the KH2 Rando setup. `KH2FM-Mods-equations19/soft-reset` Location doesn't matter, recommneded in case of soft locks. + +

Using the KH2 Client

+ +Once you have started the game through OpenKH Mod Manager and are on the title screen run the ArchipelagoKH2Client.exe. When you successfully connect to the server the client will automatically hook into the game to send/receive checks. If the client ever loses connection to the game, it will also disconnect from the server and you will need to reconnect. Make sure the game is open whenever you try to connect the client to the server otherwise it will immediately disconnect you. Most checks will be sent to you anywhere outside of a load or cutscene but if you obtain magic, you will need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable. + +

Generating a game

+ +

What is a YAML?

+ +YAML is the file format which Archipelago uses in order to configure a player's world. It allows you to dictate which +game you will be playing as well as the settings you would like for that game. + +YAML is a format very similar to JSON however it is made to be more human-readable. If you are ever unsure of the +validity of your YAML file you may check the file by uploading it to the check page on the Archipelago website. Check +page: [YAML Validation Page](/mysterycheck) + +

Creating a YAML

+ +YAML files may be generated on the Archipelago website by visiting the games page and clicking the "Settings Page" link +under any game. Clicking "Export Settings" in a game's settings page will download the YAML to your system. Games +page: [Archipelago Games List](/games) + +In a multiworld there must be one YAML per world. Any number of players can play on each world using either the game's +native coop system or using Archipelago's coop support. Each world will hold one slot in the multiworld and will have a +slot name and, if the relevant game requires it, files to associate it with that multiworld. + +If multiple people plan to play in one world cooperatively then they will only need one YAML for their coop world. If +each player is planning on playing their own game then they will each need a YAML. + diff --git a/worlds/kh2/logic.py b/worlds/kh2/logic.py new file mode 100644 index 0000000000..1c5883f5ce --- /dev/null +++ b/worlds/kh2/logic.py @@ -0,0 +1,312 @@ +from .Names import ItemName +from ..AutoWorld import LogicMixin + + +class KH2Logic(LogicMixin): + def kh_lod_unlocked(self, player, amount): + return self.has(ItemName.SwordoftheAncestor, player, amount) + + def kh_oc_unlocked(self, player, amount): + return self.has(ItemName.BattlefieldsofWar, player, amount) + + def kh_twtnw_unlocked(self, player, amount): + return self.has(ItemName.WaytotheDawn, player, amount) + + def kh_ht_unlocked(self, player, amount): + return self.has(ItemName.BoneFist, player, amount) + + def kh_tt_unlocked(self, player, amount): + return self.has(ItemName.IceCream, player, amount) + + def kh_pr_unlocked(self, player, amount): + return self.has(ItemName.SkillandCrossbones, player, amount) + + def kh_sp_unlocked(self, player, amount): + return self.has(ItemName.IdentityDisk, player, amount) + + def kh_stt_unlocked(self, player: int, amount): + return self.has(ItemName.NamineSketches, player, amount) + + # Using Dummy 13 for this + def kh_dc_unlocked(self, player: int, amount): + return self.has(ItemName.CastleKey, player, amount) + + def kh_hb_unlocked(self, player, amount): + return self.has(ItemName.MembershipCard, player, amount) + + def kh_pl_unlocked(self, player, amount): + return self.has(ItemName.ProudFang, player, amount) + + def kh_ag_unlocked(self, player, amount): + return self.has(ItemName.Scimitar, player, amount) + + def kh_bc_unlocked(self, player, amount): + return self.has(ItemName.BeastsClaw, player, amount) + + def kh_amount_of_forms(self, player, amount, requiredform="None"): + level = 0 + formList = [ItemName.ValorForm, ItemName.WisdomForm, ItemName.LimitForm, ItemName.MasterForm, + ItemName.FinalForm] + # required form is in the logic for region connections + if requiredform != "None": + formList.remove(requiredform) + for form in formList: + if self.has(form, player): + level += 1 + return level >= amount + + def kh_visit_locking_amount(self, player, amount): + visit = 0 + # torn pages are not added since you cannot get exp from that world + for item in {ItemName.CastleKey, ItemName.BattlefieldsofWar, ItemName.SwordoftheAncestor, ItemName.BeastsClaw, + ItemName.BoneFist, ItemName.ProudFang, ItemName.SkillandCrossbones, ItemName.Scimitar, + ItemName.MembershipCard, + ItemName.IceCream, ItemName.WaytotheDawn, + ItemName.IdentityDisk, ItemName.NamineSketches}: + visit += self.item_count(item, player) + return visit >= amount + + def kh_three_proof_unlocked(self, player): + return self.has(ItemName.ProofofConnection, player, 1) \ + and self.has(ItemName.ProofofNonexistence, player, 1) \ + and self.has(ItemName.ProofofPeace, player, 1) + + def kh_hitlist(self, player, amount): + return self.has(ItemName.Bounty, player, amount) + + def kh_lucky_emblem_unlocked(self, player, amount): + return self.has(ItemName.LuckyEmblem, player, amount) + + def kh_victory(self, player): + return self.has(ItemName.Victory, player, 1) + + def kh_summon(self, player, amount): + summonlevel = 0 + for summon in {ItemName.Genie, ItemName.ChickenLittle, ItemName.Stitch, ItemName.PeterPan}: + if self.has(summon, player): + summonlevel += 1 + return summonlevel >= amount + + # magic progression + def kh_fire(self, player): + return self.has(ItemName.FireElement, player, 1) + + def kh_fira(self, player): + return self.has(ItemName.FireElement, player, 2) + + def kh_firaga(self, player): + return self.has(ItemName.FireElement, player, 3) + + def kh_blizzard(self, player): + return self.has(ItemName.BlizzardElement, player, 1) + + def kh_blizzara(self, player): + return self.has(ItemName.BlizzardElement, player, 2) + + def kh_blizzaga(self, player): + return self.has(ItemName.BlizzardElement, player, 3) + + def kh_thunder(self, player): + return self.has(ItemName.ThunderElement, player, 1) + + def kh_thundara(self, player): + return self.has(ItemName.ThunderElement, player, 2) + + def kh_thundaga(self, player): + return self.has(ItemName.ThunderElement, player, 3) + + def kh_magnet(self, player): + return self.has(ItemName.MagnetElement, player, 1) + + def kh_magnera(self, player): + return self.has(ItemName.MagnetElement, player, 2) + + def kh_magnega(self, player): + return self.has(ItemName.MagnetElement, player, 3) + + def kh_reflect(self, player): + return self.has(ItemName.ReflectElement, player, 1) + + def kh_reflera(self, player): + return self.has(ItemName.ReflectElement, player, 2) + + def kh_reflega(self, player): + return self.has(ItemName.ReflectElement, player, 3) + + def kh_highjump(self, player, amount): + return self.has(ItemName.HighJump, player, amount) + + def kh_quickrun(self, player, amount): + return self.has(ItemName.QuickRun, player, amount) + + def kh_dodgeroll(self, player, amount): + return self.has(ItemName.DodgeRoll, player, amount) + + def kh_aerialdodge(self, player, amount): + return self.has(ItemName.AerialDodge, player, amount) + + def kh_glide(self, player, amount): + return self.has(ItemName.Glide, player, amount) + + def kh_comboplus(self, player, amount): + return self.has(ItemName.ComboPlus, player, amount) + + def kh_aircomboplus(self, player, amount): + return self.has(ItemName.AirComboPlus, player, amount) + + def kh_valorgenie(self, player): + return self.has(ItemName.Genie, player) and self.has(ItemName.ValorForm, player) + + def kh_wisdomgenie(self, player): + return self.has(ItemName.Genie, player) and self.has(ItemName.WisdomForm, player) + + def kh_mastergenie(self, player): + return self.has(ItemName.Genie, player) and self.has(ItemName.MasterForm, player) + + def kh_finalgenie(self, player): + return self.has(ItemName.Genie, player) and self.has(ItemName.FinalForm, player) + + def kh_rsr(self, player): + return self.has(ItemName.Slapshot, player, 1) and self.has(ItemName.ComboMaster, player) and self.kh_reflect( + player) + + def kh_gapcloser(self, player): + return self.has(ItemName.FlashStep, player, 1) or self.has(ItemName.SlideDash, player) + + # Crowd Control and Berserk Hori will be used when I add hard logic. + + def kh_crowdcontrol(self, player): + return self.kh_magnera(player) and self.has(ItemName.ChickenLittle, player) \ + or self.kh_magnega(player) and self.kh_mastergenie(player) + + def kh_berserkhori(self, player): + return self.has(ItemName.HorizontalSlash, player, 1) and self.has(ItemName.BerserkCharge, player) + + def kh_donaldlimit(self, player): + return self.has(ItemName.FlareForce, player, 1) or self.has(ItemName.Fantasia, player) + + def kh_goofylimit(self, player): + return self.has(ItemName.TornadoFusion, player, 1) or self.has(ItemName.Teamwork, player) + + def kh_basetools(self, player): + # TODO: if option is easy then add reflect,gap closer and second chance&once more. #option east scom option normal adds gap closer or combo master #hard is what is right now + return self.has(ItemName.Guard, player, 1) and self.has(ItemName.AerialRecovery, player, 1) \ + and self.has(ItemName.FinishingPlus, player, 1) + + def kh_roxastools(self, player): + return self.kh_basetools(player) and ( + self.has(ItemName.QuickRun, player) or self.has(ItemName.NegativeCombo, player, 2)) + + def kh_painandpanic(self, player): + return (self.kh_goofylimit(player) or self.kh_donaldlimit(player)) and self.kh_dc_unlocked(player, 2) + + def kh_cerberuscup(self, player): + return self.kh_amount_of_forms(player, 2) and self.kh_thundara(player) \ + and self.kh_ag_unlocked(player, 1) and self.kh_ht_unlocked(player, 1) \ + and self.kh_pl_unlocked(player, 1) + + def kh_titan(self, player: int): + return self.kh_summon(player, 2) and (self.kh_thundara(player) or self.kh_magnera(player)) \ + and self.kh_oc_unlocked(player, 2) + + def kh_gof(self, player): + return self.kh_titan(player) and self.kh_cerberuscup(player) \ + and self.kh_painandpanic(player) and self.kh_twtnw_unlocked(player, 1) + + def kh_dataroxas(self, player): + return self.kh_basetools(player) and \ + ((self.has(ItemName.LimitForm, player) and self.kh_amount_of_forms(player, 3) and self.has( + ItemName.TrinityLimit, player) and self.kh_gapcloser(player)) + or (self.has(ItemName.NegativeCombo, player, 2) or self.kh_quickrun(player, 2))) + + def kh_datamarluxia(self, player): + return self.kh_basetools(player) and self.kh_reflera(player) \ + and ((self.kh_amount_of_forms(player, 3) and self.has(ItemName.FinalForm, player) and self.kh_fira( + player)) or self.has(ItemName.NegativeCombo, player, 2) or self.kh_donaldlimit(player)) + + def kh_datademyx(self, player): + return self.kh_basetools(player) and self.kh_amount_of_forms(player, 5) and self.kh_firaga(player) \ + and (self.kh_donaldlimit(player) or self.kh_blizzard(player)) + + def kh_datalexaeus(self, player): + return self.kh_basetools(player) and self.kh_amount_of_forms(player, 3) and self.kh_reflera(player) \ + and (self.has(ItemName.NegativeCombo, player, 2) or self.kh_donaldlimit(player)) + + def kh_datasaix(self, player): + return self.kh_basetools(player) and (self.kh_thunder(player) or self.kh_blizzard(player)) \ + and self.kh_highjump(player, 2) and self.kh_aerialdodge(player, 2) and self.kh_glide(player, 2) and self.kh_amount_of_forms(player, 3) \ + and (self.kh_rsr(player) or self.has(ItemName.NegativeCombo, player, 2) or self.has(ItemName.PeterPan, + player)) + + def kh_dataxaldin(self, player): + return self.kh_basetools(player) and self.kh_donaldlimit(player) and self.kh_goofylimit(player) \ + and self.kh_highjump(player, 2) and self.kh_aerialdodge(player, 2) and self.kh_glide(player, + 2) and self.kh_magnet( + player) + # and (self.kh_form_level_unlocked(player, 3) or self.kh_berserkhori(player)) + + def kh_dataxemnas(self, player): + return self.kh_basetools(player) and self.kh_rsr(player) and self.kh_gapcloser(player) \ + and (self.has(ItemName.LimitForm, player) or self.has(ItemName.TrinityLimit, player)) + + def kh_dataxigbar(self, player): + return self.kh_basetools(player) and self.kh_donaldlimit(player) and self.has(ItemName.FinalForm, player) \ + and self.kh_amount_of_forms(player, 3) and self.kh_reflera(player) + + def kh_datavexen(self, player): + return self.kh_basetools(player) and self.kh_donaldlimit(player) and self.has(ItemName.FinalForm, player) \ + and self.kh_amount_of_forms(player, 4) and self.kh_reflera(player) and self.kh_fira(player) + + def kh_datazexion(self, player): + return self.kh_basetools(player) and self.kh_donaldlimit(player) and self.has(ItemName.FinalForm, player) \ + and self.kh_amount_of_forms(player, 3) \ + and self.kh_reflera(player) and self.kh_fira(player) + + def kh_dataaxel(self, player): + return self.kh_basetools(player) \ + and ((self.kh_reflera(player) and self.kh_blizzara(player)) or self.has(ItemName.NegativeCombo, player, 2)) + + def kh_dataluxord(self, player): + return self.kh_basetools(player) and self.kh_reflect(player) + + def kh_datalarxene(self, player): + return self.kh_basetools(player) and self.kh_reflera(player) \ + and ((self.has(ItemName.FinalForm, player) and self.kh_amount_of_forms(player, 4) and self.kh_fire( + player)) + or (self.kh_donaldlimit(player) and self.kh_amount_of_forms(player, 2))) + + def kh_sephi(self, player): + return self.kh_dataxemnas(player) + + def kh_onek(self, player): + return self.kh_reflect(player) or self.has(ItemName.Guard, player) + + def kh_terra(self, player): + return self.has(ItemName.ProofofConnection, player) and self.kh_basetools(player) \ + and self.kh_dodgeroll(player, 2) and self.kh_aerialdodge(player, 2) and self.kh_glide(player, 3) \ + and ((self.kh_comboplus(player, 2) and self.has(ItemName.Explosion, player)) or self.has( + ItemName.NegativeCombo, player, 2)) + + def kh_cor(self, player): + return self.kh_reflect(player) \ + and self.kh_highjump(player, 2) and self.kh_quickrun(player, 2) and self.kh_aerialdodge(player, 2) \ + and (self.has(ItemName.MasterForm, player) and self.kh_fire(player) + or (self.has(ItemName.ChickenLittle, player) and self.kh_donaldlimit(player) and self.kh_glide(player, + 2))) + + def kh_transport(self, player): + return self.kh_basetools(player) and self.kh_reflera(player) \ + and ((self.kh_mastergenie(player) and self.kh_magnera(player) and self.kh_donaldlimit(player)) + or (self.has(ItemName.FinalForm, player) and self.kh_amount_of_forms(player, 4) and self.kh_fira( + player))) + + def kh_gr2(self, player): + return (self.has(ItemName.MasterForm, player) or self.has(ItemName.Stitch, player)) \ + and (self.kh_fire(player) or self.kh_blizzard(player) or self.kh_thunder(player)) + + def kh_xaldin(self, player): + return self.kh_basetools(player) and (self.kh_donaldlimit(player) or self.kh_amount_of_forms(player, 1)) + + def kh_mcp(self, player): + return self.kh_reflect(player) and ( + self.has(ItemName.MasterForm, player) or self.has(ItemName.FinalForm, player)) diff --git a/worlds/kh2/mod_template/mod.yml b/worlds/kh2/mod_template/mod.yml new file mode 100644 index 0000000000..0ceda5181a --- /dev/null +++ b/worlds/kh2/mod_template/mod.yml @@ -0,0 +1,50 @@ +assets: +- method: binarc + multi: + - name: msg/us/jm.bar + - name: msg/uk/jm.bar + name: msg/jp/jm.bar + source: + - method: kh2msg + name: jm + source: + - language: en + name: jm.yml + type: list +- method: binarc + name: 00battle.bin + source: + - method: listpatch + name: fmlv + source: + - name: FmlvList.yml + type: fmlv + type: List + - method: listpatch + name: lvup + source: + - name: LvupList.yml + type: lvup + type: List + - method: listpatch + name: bons + source: + - name: BonsList.yml + type: bons + type: List +- method: binarc + name: 03system.bin + source: + - method: listpatch + name: trsr + source: + - name: TrsrList.yml + type: trsr + type: List + - method: listpatch + name: item + source: + - name: ItemList.yml + type: item + type: List +title: Randomizer Seed diff --git a/worlds/kh2/requirements.txt b/worlds/kh2/requirements.txt new file mode 100644 index 0000000000..14a8bddde2 --- /dev/null +++ b/worlds/kh2/requirements.txt @@ -0,0 +1 @@ +Pymem>=1.10.0 \ No newline at end of file diff --git a/worlds/kh2/test/TestGoal.py b/worlds/kh2/test/TestGoal.py new file mode 100644 index 0000000000..6cc63da334 --- /dev/null +++ b/worlds/kh2/test/TestGoal.py @@ -0,0 +1,27 @@ +from . import KH2TestBase +from ..Names import ItemName,LocationName,RegionName + +class TestDefault(KH2TestBase): + options = {} + + def testEverything(self): + self.collect_all_but([ItemName.Victory]) + self.assertBeatable(True) + + +class TestLuckyEmblem(KH2TestBase): + options = { + "Goal": 1, + } + + def testEverything(self): + self.collect_all_but([ItemName.LuckyEmblem]) + self.assertBeatable(True) + +class TestHitList(KH2TestBase): + options = { + "Goal": 2, + } + def testEverything(self): + self.collect_all_but([ItemName.Bounty]) + self.assertBeatable(True) diff --git a/worlds/kh2/test/__init__.py b/worlds/kh2/test/__init__.py new file mode 100644 index 0000000000..dfef227627 --- /dev/null +++ b/worlds/kh2/test/__init__.py @@ -0,0 +1,5 @@ +from test.TestBase import WorldTestBase + + +class KH2TestBase(WorldTestBase): + game = "Kingdom Hearts 2" diff --git a/worlds/ladx/Common.py b/worlds/ladx/Common.py new file mode 100644 index 0000000000..e85e9767b9 --- /dev/null +++ b/worlds/ladx/Common.py @@ -0,0 +1,2 @@ +LINKS_AWAKENING = "Links Awakening DX" +BASE_ID = 10000000 \ No newline at end of file diff --git a/worlds/ladx/GpsTracker.py b/worlds/ladx/GpsTracker.py new file mode 100644 index 0000000000..1ea465eb16 --- /dev/null +++ b/worlds/ladx/GpsTracker.py @@ -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)) diff --git a/worlds/ladx/ItemTracker.py b/worlds/ladx/ItemTracker.py new file mode 100644 index 0000000000..92ef71633e --- /dev/null +++ b/worlds/ladx/ItemTracker.py @@ -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)) \ No newline at end of file diff --git a/worlds/ladx/Items.py b/worlds/ladx/Items.py new file mode 100644 index 0000000000..ff5db6950a --- /dev/null +++ b/worlds/ladx/Items.py @@ -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 +} diff --git a/worlds/ladx/LADXR/.tinyci b/worlds/ladx/LADXR/.tinyci new file mode 100644 index 0000000000..292c640496 --- /dev/null +++ b/worlds/ladx/LADXR/.tinyci @@ -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 diff --git a/worlds/ladx/LADXR/LADXR_LICENSE b/worlds/ladx/LADXR/LADXR_LICENSE new file mode 100644 index 0000000000..3a83b309af --- /dev/null +++ b/worlds/ladx/LADXR/LADXR_LICENSE @@ -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. diff --git a/worlds/ladx/LADXR/README.md b/worlds/ladx/LADXR/README.md new file mode 100644 index 0000000000..eeea602daf --- /dev/null +++ b/worlds/ladx/LADXR/README.md @@ -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 diff --git a/worlds/ladx/LADXR/assembler.py b/worlds/ladx/LADXR/assembler.py new file mode 100644 index 0000000000..07fcfde566 --- /dev/null +++ b/worlds/ladx/LADXR/assembler.py @@ -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' diff --git a/worlds/ladx/LADXR/backgroundEditor.py b/worlds/ladx/LADXR/backgroundEditor.py new file mode 100644 index 0000000000..cab58850fa --- /dev/null +++ b/worlds/ladx/LADXR/backgroundEditor.py @@ -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 diff --git a/worlds/ladx/LADXR/checkMetadata.py b/worlds/ladx/LADXR/checkMetadata.py new file mode 100644 index 0000000000..e8b91c05e8 --- /dev/null +++ b/worlds/ladx/LADXR/checkMetadata.py @@ -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"), +} diff --git a/worlds/ladx/LADXR/entityData.py b/worlds/ladx/LADXR/entityData.py new file mode 100644 index 0000000000..405a1ea65c --- /dev/null +++ b/worlds/ladx/LADXR/entityData.py @@ -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) diff --git a/worlds/ladx/LADXR/entranceInfo.py b/worlds/ladx/LADXR/entranceInfo.py new file mode 100644 index 0000000000..de1a247355 --- /dev/null +++ b/worlds/ladx/LADXR/entranceInfo.py @@ -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"), +} diff --git a/worlds/ladx/LADXR/generator.py b/worlds/ladx/LADXR/generator.py new file mode 100644 index 0000000000..cb9de281b9 --- /dev/null +++ b/worlds/ladx/LADXR/generator.py @@ -0,0 +1,424 @@ +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, MusicChangeCondition + + +# Function to generate a final rom, this patches the rom with all required patches +def generateRom(args, settings, ap_settings, auth, seed_name, 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, auth, 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) + if ap_settings['music_change_condition'] == MusicChangeCondition.option_always: + 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 > 100: + # There are only 101 player name slots (99 + "The Server" + "another world"), so don't use more than that + mw = 100 + 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, auth, seed_name, 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 + # Patch over the title + assert(len(auth) == 12) + rom.patch(0x00, SEED_LOCATION, None, binascii.hexlify(auth)) + + + for pymod in pymods: + pymod.postPatch(rom) + + + return rom diff --git a/worlds/ladx/LADXR/getGFX.py b/worlds/ladx/LADXR/getGFX.py new file mode 100644 index 0000000000..e7d327ef65 --- /dev/null +++ b/worlds/ladx/LADXR/getGFX.py @@ -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) diff --git a/worlds/ladx/LADXR/hints.py b/worlds/ladx/LADXR/hints.py new file mode 100644 index 0000000000..f7ef78a3e8 --- /dev/null +++ b/worlds/ladx/LADXR/hints.py @@ -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") diff --git a/worlds/ladx/LADXR/itempool.py b/worlds/ladx/LADXR/itempool.py new file mode 100644 index 0000000000..5031488337 --- /dev/null +++ b/worlds/ladx/LADXR/itempool.py @@ -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() diff --git a/worlds/ladx/LADXR/locations/all.py b/worlds/ladx/LADXR/locations/all.py new file mode 100644 index 0000000000..a617346051 --- /dev/null +++ b/worlds/ladx/LADXR/locations/all.py @@ -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 * diff --git a/worlds/ladx/LADXR/locations/anglerKey.py b/worlds/ladx/LADXR/locations/anglerKey.py new file mode 100644 index 0000000000..79382de862 --- /dev/null +++ b/worlds/ladx/LADXR/locations/anglerKey.py @@ -0,0 +1,6 @@ +from .droppedKey import DroppedKey + + +class AnglerKey(DroppedKey): + def __init__(self): + super().__init__(0x0CE) \ No newline at end of file diff --git a/worlds/ladx/LADXR/locations/beachSword.py b/worlds/ladx/LADXR/locations/beachSword.py new file mode 100644 index 0000000000..51fc4388e3 --- /dev/null +++ b/worlds/ladx/LADXR/locations/beachSword.py @@ -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) diff --git a/worlds/ladx/LADXR/locations/birdKey.py b/worlds/ladx/LADXR/locations/birdKey.py new file mode 100644 index 0000000000..12418c61aa --- /dev/null +++ b/worlds/ladx/LADXR/locations/birdKey.py @@ -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) diff --git a/worlds/ladx/LADXR/locations/boomerangGuy.py b/worlds/ladx/LADXR/locations/boomerangGuy.py new file mode 100644 index 0000000000..92d76cebdf --- /dev/null +++ b/worlds/ladx/LADXR/locations/boomerangGuy.py @@ -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() diff --git a/worlds/ladx/LADXR/locations/chest.py b/worlds/ladx/LADXR/locations/chest.py new file mode 100644 index 0000000000..578201bc1e --- /dev/null +++ b/worlds/ladx/LADXR/locations/chest.py @@ -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) diff --git a/worlds/ladx/LADXR/locations/constants.py b/worlds/ladx/LADXR/locations/constants.py new file mode 100644 index 0000000000..7bb8df5b35 --- /dev/null +++ b/worlds/ladx/LADXR/locations/constants.py @@ -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, +} diff --git a/worlds/ladx/LADXR/locations/droppedKey.py b/worlds/ladx/LADXR/locations/droppedKey.py new file mode 100644 index 0000000000..baa093bb38 --- /dev/null +++ b/worlds/ladx/LADXR/locations/droppedKey.py @@ -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) diff --git a/worlds/ladx/LADXR/locations/faceKey.py b/worlds/ladx/LADXR/locations/faceKey.py new file mode 100644 index 0000000000..1585535f63 --- /dev/null +++ b/worlds/ladx/LADXR/locations/faceKey.py @@ -0,0 +1,6 @@ +from .droppedKey import DroppedKey + + +class FaceKey(DroppedKey): + def __init__(self): + super().__init__(0x27F) diff --git a/worlds/ladx/LADXR/locations/fishingMinigame.py b/worlds/ladx/LADXR/locations/fishingMinigame.py new file mode 100644 index 0000000000..0caaf7fddb --- /dev/null +++ b/worlds/ladx/LADXR/locations/fishingMinigame.py @@ -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] diff --git a/worlds/ladx/LADXR/locations/goldLeaf.py b/worlds/ladx/LADXR/locations/goldLeaf.py new file mode 100644 index 0000000000..10ebab42cc --- /dev/null +++ b/worlds/ladx/LADXR/locations/goldLeaf.py @@ -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) diff --git a/worlds/ladx/LADXR/locations/heartContainer.py b/worlds/ladx/LADXR/locations/heartContainer.py new file mode 100644 index 0000000000..e1a8d77569 --- /dev/null +++ b/worlds/ladx/LADXR/locations/heartContainer.py @@ -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] diff --git a/worlds/ladx/LADXR/locations/heartPiece.py b/worlds/ladx/LADXR/locations/heartPiece.py new file mode 100644 index 0000000000..b6dddf0b7b --- /dev/null +++ b/worlds/ladx/LADXR/locations/heartPiece.py @@ -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] diff --git a/worlds/ladx/LADXR/locations/hookshot.py b/worlds/ladx/LADXR/locations/hookshot.py new file mode 100644 index 0000000000..1e2d24584a --- /dev/null +++ b/worlds/ladx/LADXR/locations/hookshot.py @@ -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) diff --git a/worlds/ladx/LADXR/locations/instrument.py b/worlds/ladx/LADXR/locations/instrument.py new file mode 100644 index 0000000000..2eadb31c65 --- /dev/null +++ b/worlds/ladx/LADXR/locations/instrument.py @@ -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)] diff --git a/worlds/ladx/LADXR/locations/itemInfo.py b/worlds/ladx/LADXR/locations/itemInfo.py new file mode 100644 index 0000000000..dcd4205f4c --- /dev/null +++ b/worlds/ladx/LADXR/locations/itemInfo.py @@ -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" diff --git a/worlds/ladx/LADXR/locations/items.py b/worlds/ladx/LADXR/locations/items.py new file mode 100644 index 0000000000..50186ef2a3 --- /dev/null +++ b/worlds/ladx/LADXR/locations/items.py @@ -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" diff --git a/worlds/ladx/LADXR/locations/keyLocation.py b/worlds/ladx/LADXR/locations/keyLocation.py new file mode 100644 index 0000000000..675bfe0f90 --- /dev/null +++ b/worlds/ladx/LADXR/locations/keyLocation.py @@ -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 diff --git a/worlds/ladx/LADXR/locations/madBatter.py b/worlds/ladx/LADXR/locations/madBatter.py new file mode 100644 index 0000000000..33ce971422 --- /dev/null +++ b/worlds/ladx/LADXR/locations/madBatter.py @@ -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) diff --git a/worlds/ladx/LADXR/locations/owlStatue.py b/worlds/ladx/LADXR/locations/owlStatue.py new file mode 100644 index 0000000000..1e62519319 --- /dev/null +++ b/worlds/ladx/LADXR/locations/owlStatue.py @@ -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 diff --git a/worlds/ladx/LADXR/locations/seashell.py b/worlds/ladx/LADXR/locations/seashell.py new file mode 100644 index 0000000000..5b30cf7e24 --- /dev/null +++ b/worlds/ladx/LADXR/locations/seashell.py @@ -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 diff --git a/worlds/ladx/LADXR/locations/shop.py b/worlds/ladx/LADXR/locations/shop.py new file mode 100644 index 0000000000..b68726665f --- /dev/null +++ b/worlds/ladx/LADXR/locations/shop.py @@ -0,0 +1,48 @@ +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].encode('ascii', 'replace').decode()}" + + + if self.custom_item_name: + name = self.custom_item_name + else: + name = "{"+option+"}" + + 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 {name} 200 {{RUPEES}}{mw_text}!", 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"{name} only 980 {{RUPEES}}{mw_text}!", 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)) \ No newline at end of file diff --git a/worlds/ladx/LADXR/locations/song.py b/worlds/ladx/LADXR/locations/song.py new file mode 100644 index 0000000000..25937dcef4 --- /dev/null +++ b/worlds/ladx/LADXR/locations/song.py @@ -0,0 +1,5 @@ +from .droppedKey import DroppedKey + + +class Song(DroppedKey): + pass diff --git a/worlds/ladx/LADXR/locations/startItem.py b/worlds/ladx/LADXR/locations/startItem.py new file mode 100644 index 0000000000..95dd6ba54a --- /dev/null +++ b/worlds/ladx/LADXR/locations/startItem.py @@ -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) diff --git a/worlds/ladx/LADXR/locations/toadstool.py b/worlds/ladx/LADXR/locations/toadstool.py new file mode 100644 index 0000000000..381b023ec8 --- /dev/null +++ b/worlds/ladx/LADXR/locations/toadstool.py @@ -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) diff --git a/worlds/ladx/LADXR/locations/tradeSequence.py b/worlds/ladx/LADXR/locations/tradeSequence.py new file mode 100644 index 0000000000..1587bb5e13 --- /dev/null +++ b/worlds/ladx/LADXR/locations/tradeSequence.py @@ -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 diff --git a/worlds/ladx/LADXR/locations/tunicFairy.py b/worlds/ladx/LADXR/locations/tunicFairy.py new file mode 100644 index 0000000000..84fc9ca735 --- /dev/null +++ b/worlds/ladx/LADXR/locations/tunicFairy.py @@ -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) diff --git a/worlds/ladx/LADXR/locations/witch.py b/worlds/ladx/LADXR/locations/witch.py new file mode 100644 index 0000000000..6435a30e32 --- /dev/null +++ b/worlds/ladx/LADXR/locations/witch.py @@ -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)) diff --git a/worlds/ladx/LADXR/logic/__init__.py b/worlds/ladx/LADXR/logic/__init__.py new file mode 100644 index 0000000000..11a0acfd01 --- /dev/null +++ b/worlds/ladx/LADXR/logic/__init__.py @@ -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)) diff --git a/worlds/ladx/LADXR/logic/dungeon1.py b/worlds/ladx/LADXR/logic/dungeon1.py new file mode 100644 index 0000000000..82321a1c0d --- /dev/null +++ b/worlds/ladx/LADXR/logic/dungeon1.py @@ -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 diff --git a/worlds/ladx/LADXR/logic/dungeon2.py b/worlds/ladx/LADXR/logic/dungeon2.py new file mode 100644 index 0000000000..3bb95edbc8 --- /dev/null +++ b/worlds/ladx/LADXR/logic/dungeon2.py @@ -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 diff --git a/worlds/ladx/LADXR/logic/dungeon3.py b/worlds/ladx/LADXR/logic/dungeon3.py new file mode 100644 index 0000000000..e65c7da0ba --- /dev/null +++ b/worlds/ladx/LADXR/logic/dungeon3.py @@ -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 diff --git a/worlds/ladx/LADXR/logic/dungeon4.py b/worlds/ladx/LADXR/logic/dungeon4.py new file mode 100644 index 0000000000..7d71c89f0c --- /dev/null +++ b/worlds/ladx/LADXR/logic/dungeon4.py @@ -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 diff --git a/worlds/ladx/LADXR/logic/dungeon5.py b/worlds/ladx/LADXR/logic/dungeon5.py new file mode 100644 index 0000000000..b8e013066c --- /dev/null +++ b/worlds/ladx/LADXR/logic/dungeon5.py @@ -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 diff --git a/worlds/ladx/LADXR/logic/dungeon6.py b/worlds/ladx/LADXR/logic/dungeon6.py new file mode 100644 index 0000000000..d67138b334 --- /dev/null +++ b/worlds/ladx/LADXR/logic/dungeon6.py @@ -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, AND(KEY6, FOUND(KEY6, 2))) # top right chest horseheads + boss_key = Location(dungeon=6).add(DungeonChest(0x1B6)).connect(center_2_and_upper_right_side, AND(AND(KEY6, FOUND(KEY6, 3), 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 diff --git a/worlds/ladx/LADXR/logic/dungeon7.py b/worlds/ladx/LADXR/logic/dungeon7.py new file mode 100644 index 0000000000..594b4d083c --- /dev/null +++ b/worlds/ladx/LADXR/logic/dungeon7.py @@ -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 diff --git a/worlds/ladx/LADXR/logic/dungeon8.py b/worlds/ladx/LADXR/logic/dungeon8.py new file mode 100644 index 0000000000..4444ecbb14 --- /dev/null +++ b/worlds/ladx/LADXR/logic/dungeon8.py @@ -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 diff --git a/worlds/ladx/LADXR/logic/dungeonColor.py b/worlds/ladx/LADXR/logic/dungeonColor.py new file mode 100644 index 0000000000..aa58c0bafa --- /dev/null +++ b/worlds/ladx/LADXR/logic/dungeonColor.py @@ -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 diff --git a/worlds/ladx/LADXR/logic/location.py b/worlds/ladx/LADXR/logic/location.py new file mode 100644 index 0000000000..18615a1164 --- /dev/null +++ b/worlds/ladx/LADXR/logic/location.py @@ -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)) diff --git a/worlds/ladx/LADXR/logic/overworld.py b/worlds/ladx/LADXR/logic/overworld.py new file mode 100644 index 0000000000..551cf8353f --- /dev/null +++ b/worlds/ladx/LADXR/logic/overworld.py @@ -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" diff --git a/worlds/ladx/LADXR/logic/requirements.py b/worlds/ladx/LADXR/logic/requirements.py new file mode 100644 index 0000000000..acc969ba93 --- /dev/null +++ b/worlds/ladx/LADXR/logic/requirements.py @@ -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 diff --git a/worlds/ladx/LADXR/main.py b/worlds/ladx/LADXR/main.py new file mode 100644 index 0000000000..5b563675c0 --- /dev/null +++ b/worlds/ladx/LADXR/main.py @@ -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 + diff --git a/worlds/ladx/LADXR/mapgen/__init__.py b/worlds/ladx/LADXR/mapgen/__init__.py new file mode 100644 index 0000000000..d38c27fbdd --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/__init__.py @@ -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 diff --git a/worlds/ladx/LADXR/mapgen/enemygen.py b/worlds/ladx/LADXR/mapgen/enemygen.py new file mode 100644 index 0000000000..45020b93a9 --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/enemygen.py @@ -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)) diff --git a/worlds/ladx/LADXR/mapgen/imagegenerator.py b/worlds/ladx/LADXR/mapgen/imagegenerator.py new file mode 100644 index 0000000000..fc9d5bbeee --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/imagegenerator.py @@ -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 diff --git a/worlds/ladx/LADXR/mapgen/locationgen.py b/worlds/ladx/LADXR/mapgen/locationgen.py new file mode 100644 index 0000000000..0a30d80bd5 --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/locationgen.py @@ -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 diff --git a/worlds/ladx/LADXR/mapgen/locations/base.py b/worlds/ladx/LADXR/mapgen/locations/base.py new file mode 100644 index 0000000000..a6526193fc --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/locations/base.py @@ -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__) diff --git a/worlds/ladx/LADXR/mapgen/locations/chest.py b/worlds/ladx/LADXR/mapgen/locations/chest.py new file mode 100644 index 0000000000..4cfeb0bc6b --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/locations/chest.py @@ -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) diff --git a/worlds/ladx/LADXR/mapgen/locations/entrance.py b/worlds/ladx/LADXR/mapgen/locations/entrance.py new file mode 100644 index 0000000000..be4dde6634 --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/locations/entrance.py @@ -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 {} diff --git a/worlds/ladx/LADXR/mapgen/locations/entrance_info.py b/worlds/ladx/LADXR/mapgen/locations/entrance_info.py new file mode 100644 index 0000000000..9de2b86101 --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/locations/entrance_info.py @@ -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 +} diff --git a/worlds/ladx/LADXR/mapgen/locations/seashell.py b/worlds/ladx/LADXR/mapgen/locations/seashell.py new file mode 100644 index 0000000000..521d0c500b --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/locations/seashell.py @@ -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) diff --git a/worlds/ladx/LADXR/mapgen/logic.py b/worlds/ladx/LADXR/mapgen/logic.py new file mode 100644 index 0000000000..607a00c26f --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/logic.py @@ -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 diff --git a/worlds/ladx/LADXR/mapgen/map.py b/worlds/ladx/LADXR/mapgen/map.py new file mode 100644 index 0000000000..0c9be58bcd --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/map.py @@ -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 diff --git a/worlds/ladx/LADXR/mapgen/roomgen.py b/worlds/ladx/LADXR/mapgen/roomgen.py new file mode 100644 index 0000000000..189bb25d72 --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/roomgen.py @@ -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)) diff --git a/worlds/ladx/LADXR/mapgen/roomtype/base.py b/worlds/ladx/LADXR/mapgen/roomtype/base.py new file mode 100644 index 0000000000..b524c4fb23 --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/roomtype/base.py @@ -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) diff --git a/worlds/ladx/LADXR/mapgen/roomtype/forest.py b/worlds/ladx/LADXR/mapgen/roomtype/forest.py new file mode 100644 index 0000000000..25c71eefc8 --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/roomtype/forest.py @@ -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) diff --git a/worlds/ladx/LADXR/mapgen/roomtype/mountain.py b/worlds/ladx/LADXR/mapgen/roomtype/mountain.py new file mode 100644 index 0000000000..d80d5dc581 --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/roomtype/mountain.py @@ -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}) diff --git a/worlds/ladx/LADXR/mapgen/roomtype/town.py b/worlds/ladx/LADXR/mapgen/roomtype/town.py new file mode 100644 index 0000000000..2553a13308 --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/roomtype/town.py @@ -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) diff --git a/worlds/ladx/LADXR/mapgen/roomtype/water.py b/worlds/ladx/LADXR/mapgen/roomtype/water.py new file mode 100644 index 0000000000..e3f4830ecf --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/roomtype/water.py @@ -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) \ No newline at end of file diff --git a/worlds/ladx/LADXR/mapgen/tileset.py b/worlds/ladx/LADXR/mapgen/tileset.py new file mode 100644 index 0000000000..b634c30223 --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/tileset.py @@ -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 diff --git a/worlds/ladx/LADXR/mapgen/util.py b/worlds/ladx/LADXR/mapgen/util.py new file mode 100644 index 0000000000..ab22755b2e --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/util.py @@ -0,0 +1,5 @@ + +def xyrange(w, h): + for y in range(h): + for x in range(w): + yield x, y diff --git a/worlds/ladx/LADXR/mapgen/wfc.py b/worlds/ladx/LADXR/mapgen/wfc.py new file mode 100644 index 0000000000..e40b6af127 --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/wfc.py @@ -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() diff --git a/worlds/ladx/LADXR/patches/aesthetics.py b/worlds/ladx/LADXR/patches/aesthetics.py new file mode 100644 index 0000000000..ff8cd5d856 --- /dev/null +++ b/worlds/ladx/LADXR/patches/aesthetics.py @@ -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 diff --git a/worlds/ladx/LADXR/patches/bank34.py b/worlds/ladx/LADXR/patches/bank34.py new file mode 100644 index 0000000000..22abd48b39 --- /dev/null +++ b/worlds/ladx/LADXR/patches/bank34.py @@ -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)) \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/bank3e.asm/bowwow.asm b/worlds/ladx/LADXR/patches/bank3e.asm/bowwow.asm new file mode 100644 index 0000000000..3480838e2f --- /dev/null +++ b/worlds/ladx/LADXR/patches/bank3e.asm/bowwow.asm @@ -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 diff --git a/worlds/ladx/LADXR/patches/bank3e.asm/chest.asm b/worlds/ladx/LADXR/patches/bank3e.asm/chest.asm new file mode 100644 index 0000000000..b19e879dc3 --- /dev/null +++ b/worlds/ladx/LADXR/patches/bank3e.asm/chest.asm @@ -0,0 +1,994 @@ +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: + ld hl, $0014 + jr AddRupees + +AddRupees50: + ld hl, $0032 + jr AddRupees + +AddRupees100: + ld hl, $0064 + jr AddRupees + +AddRupees200: + ld hl, $00C8 + jr AddRupees + +AddRupees500: + ld hl, $01F4 + jr AddRupees + +AddRupees: + ld a, [$DB8F] + ld d, a + ld a, [$DB90] + ld e, a + add hl, de + ld a, h + ld [$DB8F], a + ld a, l + 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 diff --git a/worlds/ladx/LADXR/patches/bank3e.asm/itemnames.asm b/worlds/ladx/LADXR/patches/bank3e.asm/itemnames.asm new file mode 100644 index 0000000000..0c1bc9d699 --- /dev/null +++ b/worlds/ladx/LADXR/patches/bank3e.asm/itemnames.asm @@ -0,0 +1,500 @@ + +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 + + cp 101 + jr C, .continue + ld a, 100 +.continue: + 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: \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/bank3e.asm/link.asm b/worlds/ladx/LADXR/patches/bank3e.asm/link.asm new file mode 100644 index 0000000000..266dd5fc5b --- /dev/null +++ b/worlds/ladx/LADXR/patches/bank3e.asm/link.asm @@ -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 diff --git a/worlds/ladx/LADXR/patches/bank3e.asm/message.asm b/worlds/ladx/LADXR/patches/bank3e.asm/message.asm new file mode 100644 index 0000000000..33062c6e9b --- /dev/null +++ b/worlds/ladx/LADXR/patches/bank3e.asm/message.asm @@ -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 diff --git a/worlds/ladx/LADXR/patches/bank3e.asm/multiworld.asm b/worlds/ladx/LADXR/patches/bank3e.asm/multiworld.asm new file mode 100644 index 0000000000..d7804cba6b --- /dev/null +++ b/worlds/ladx/LADXR/patches/bank3e.asm/multiworld.asm @@ -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 diff --git a/worlds/ladx/LADXR/patches/bank3e.asm/owl.asm b/worlds/ladx/LADXR/patches/bank3e.asm/owl.asm new file mode 100644 index 0000000000..35bc53f59e --- /dev/null +++ b/worlds/ladx/LADXR/patches/bank3e.asm/owl.asm @@ -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 diff --git a/worlds/ladx/LADXR/patches/bank3e.py b/worlds/ladx/LADXR/patches/bank3e.py new file mode 100644 index 0000000000..d2b31adf91 --- /dev/null +++ b/worlds/ladx/LADXR/patches/bank3e.py @@ -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")) diff --git a/worlds/ladx/LADXR/patches/bank3f.py b/worlds/ladx/LADXR/patches/bank3f.py new file mode 100644 index 0000000000..8c6b86a7f3 --- /dev/null +++ b/worlds/ladx/LADXR/patches/bank3f.py @@ -0,0 +1,386 @@ +from ..assembler import ASM +from .. import utils + + +def addBank3F(rom): + # Bank3F is used to initialize the tile data in VRAM:1 at the start of the rom. + # The normal rom does not use this tile data to maintain GB compatibility. + rom.patch(0, 0x0150, ASM(""" + cp $11 ; is running on Game Boy Color? + jr nz, notGBC + ldh a, [$4d] + and $80 ; do we need to switch the CPU speed? + jr nz, speedSwitchDone + ; switch to GBC speed + ld a, $30 + ldh [$00], a + ld a, $01 + ldh [$4d], a + xor a + ldh [$ff], a + stop + db $00 + + speedSwitchDone: + xor a + ldh [$70], a + ld a, $01 ; isGBC = true + jr Init + + notGBC: + xor a ; isGBC = false + Init: + """), ASM(""" + ; Check if we are a color gameboy, we require a color version now. + cp $11 + jr nz, notGBC + + ; Switch to bank $3F to run our custom initializer + ld a, $3F + ld [$2100], a + call $4000 + ; Switch back to bank 0 after loading our own initializer + ld a, $01 + ld [$2100], a + + ; set a to 1 to indicate GBC + ld a, $01 + jr Init + notGBC: + xor a + Init: + """), fill_nop=True) + + rom.patch(0x3F, 0x0000, None, ASM(""" + ; switch speed + ld a, $30 + ldh [$00], a + ld a, $01 + ldh [$4d], a + xor a + ldh [$ff], a + stop + db $00 + + ; Switch VRAM bank + ld a, $01 + ldh [$4F], a + + call $28CF ; display off + + ; Use the GBC DMA to transfer our tile data + ld a, $68 + ldh [$51], a + ld a, $00 + ldh [$52], a + + ld a, $80 + ldh [$53], a + ld a, $00 + ldh [$54], a + + ld a, $7F + ldh [$55], a + + waitTillTransferDone: + ldh a, [$55] + and $80 + jr z, waitTillTransferDone + + ld a, $70 + ldh [$51], a + ld a, $00 + ldh [$52], a + + ld a, $88 + ldh [$53], a + ld a, $00 + ldh [$54], a + + ld a, $7F + ldh [$55], a + + waitTillTransferDone2: + ldh a, [$55] + and $80 + jr z, waitTillTransferDone2 + + ld a, $68 + ldh [$51], a + ld a, $00 + ldh [$52], a + + ld a, $90 + ldh [$53], a + ld a, $00 + ldh [$54], a + + ld a, $7F + ldh [$55], a + + waitTillTransferDone3: + ldh a, [$55] + and $80 + jr z, waitTillTransferDone3 + + ; Switch VRAM bank back + ld a, $00 + ldh [$4F], a + + ; Switch the display back on, else the later code hangs + ld a, $80 + ldh [$40], a + + speedSwitchDone: + xor a + ldh [$70], a + + ; Check if we are running on a bad emulator + ldh [$02], a + ldh a, [$02] + and $7c + cp $7c + jr nz, badEmu + + ; Enable the timer to run 32 times per second + xor a + ldh [$06], a + ld a, $04 + ldh [$07], a + + ; Set SB to $FF to indicate we have no data from hardware + ld a, $FF + ldh [$01], a + ret +badEmu: + xor a + ldh [$40], a ; switch display off + ; Load some palette + ld a, $80 + ldh [$68], a + xor a + ldh [$69], a + ldh [$69], a + ldh [$69], a + ldh [$69], a + + ; Load a different gfx tile for the first gfx + cpl + ld hl, $8000 + ld c, $10 +.loop: + ldi [hl], a + dec c + jr nz, .loop + + ld a, $01 + ld [$9800], a + ld [$9820], a + ld [$9840], a + ld [$9860], a + ld [$9880], a + + ld [$9801], a + ld [$9841], a + ld [$9881], a + + ld [$9822], a + ld [$9862], a + + ld [$9824], a + ld [$9844], a + ld [$9864], a + ld [$9884], a + + ld [$9805], a + ld [$9845], a + + ld [$9826], a + ld [$9846], a + ld [$9866], a + ld [$9886], a + + ld [$9808], a + ld [$9828], a + ld [$9848], a + ld [$9868], a + ld [$9888], a + + ld [$9809], a + ld [$9889], a + + ld [$982A], a + ld [$984A], a + ld [$986A], a + + ld [$9900], a + ld [$9920], a + ld [$9940], a + ld [$9960], a + ld [$9980], a + + ld [$9901], a + ld [$9941], a + ld [$9981], a + + ld [$9903], a + ld [$9923], a + ld [$9943], a + ld [$9963], a + ld [$9983], a + + ld [$9904], a + ld [$9925], a + ld [$9906], a + + ld [$9907], a + ld [$9927], a + ld [$9947], a + ld [$9967], a + ld [$9987], a + + ld [$9909], a + ld [$9929], a + ld [$9949], a + ld [$9969], a + ld [$9989], a + + ld [$998A], a + + ld [$990B], a + ld [$992B], a + ld [$994B], a + ld [$996B], a + ld [$998B], a + + ; lcd on + ld a, $91 + ldh [$40], a +blockBadEmu: + di + jr blockBadEmu + + """)) + + # Copy all normal item graphics + rom.banks[0x3F][0x2800:0x2B00] = rom.banks[0x2C][0x0800:0x0B00] # main items + rom.banks[0x3F][0x2B00:0x2C00] = rom.banks[0x2C][0x0C00:0x0D00] # overworld key items + rom.banks[0x3F][0x2C00:0x2D00] = rom.banks[0x32][0x3D00:0x3E00] # dungeon key items + # Create ruppee for palettes 0-3 + rom.banks[0x3F][0x2B80:0x2BA0] = rom.banks[0x3F][0x2A60:0x2A80] + for n in range(0x2B80, 0x2BA0, 2): + rom.banks[0x3F][n+1] ^= rom.banks[0x3F][n] + + # Create capacity upgrade arrows + rom.banks[0x3F][0x2A30:0x2A40] = utils.createTileData(""" + 33 + 3113 + 311113 +33311333 + 3113 + 3333 +""") + rom.banks[0x3F][0x2A20:0x2A30] = rom.banks[0x3F][0x2A30:0x2A40] + for n in range(0x2A20, 0x2A40, 2): + rom.banks[0x3F][n] |= rom.banks[0x3F][n + 1] + + # Add the slime key and mushroom which are not in the above sets + rom.banks[0x3F][0x2CC0:0x2D00] = rom.banks[0x2C][0x28C0:0x2900] + # Add tunic sprites as well. + rom.banks[0x3F][0x2C80:0x2CA0] = rom.banks[0x35][0x0F00:0x0F20] + + # Add the bowwow sprites + rom.banks[0x3F][0x2D00:0x2E00] = rom.banks[0x2E][0x2400:0x2500] + + # Zol sprites, so we can have zol anywhere from a chest + rom.banks[0x3F][0x2E00:0x2E60] = rom.banks[0x2E][0x1120:0x1180] + # Patch gel(zol) entity to load sprites from the 2nd bank + rom.patch(0x06, 0x3C09, "5202522254025422" "5200522054005420", "600A602A620A622A" "6008602862086228") + rom.patch(0x07, 0x329B, "FFFFFFFF" "FFFFFFFF" "54005420" "52005220" "56005600", + "FFFFFFFF" "FFFFFFFF" "62086228" "60086028" "64086408") + rom.patch(0x06, 0x3BFA, "56025622", "640A642A"); + + + # Cucco + rom.banks[0x3F][0x2E80:0x2F00] = rom.banks[0x32][0x2500:0x2580] + # Patch the cucco graphics to load from 2nd vram bank + rom.patch(0x05, 0x0514, + "5001" "5201" "5401" "5601" "5221" "5021" "5621" "5421", + "6809" "6A09" "6C09" "6E09" "6A29" "6829" "6E29" "6C29") + # Song symbols + rom.banks[0x3F][0x2F00:0x2F60] = utils.createTileData(""" + + + ... + . .222 + .2.2222 +.22.222. +.22222.3 +.2..22.3 + .33...3 + .33.3.3 + ..233.3 +.22.2333 +.222.233 + .222... + ... +""" + """ + + + .. + .22 + .223 + ..222 + .33.22 + .3..22 + .33.33 + ..23. + ..233. + .22.333 +.22..233 + .. .23 + .. +""" + """ + + + ... + .222. + .2.332 + .23.32 + .233.2 + .222222 +.2222222 +.2..22.2 +.2.3.222 +.22...22 + .2333.. + .23333 + .....""", " .23") + + # Ghost + rom.banks[0x3F][0x2F60:0x2FE0] = rom.banks[0x32][0x1800:0x1880] + + # Instruments + rom.banks[0x3F][0x3000:0x3200] = rom.banks[0x31][0x1000:0x1200] + # Patch the egg song event to use the 2nd vram sprites + rom.patch(0x19, 0x0BAC, + "5006520654065606" + "58065A065C065E06" + "6006620664066606" + "68066A066C066E06", + "800E820E840E860E" + "880E8A0E8C0E8E0E" + "900E920E940E960E" + "980E9A0E9C0E9E0E" + ) + + # Rooster + rom.banks[0x3F][0x3200:0x3300] = rom.banks[0x32][0x1D00:0x1E00] + rom.patch(0x19, 0x19BC, + "42234023" "46234423" "40034203" "44034603" "4C034C23" "4E034E23" "48034823" "4A034A23", + "A22BA02B" "A62BA42B" "A00BA20B" "A40BA60B" "AC0BAC2B" "AE0BAE2B" "A80BA82B" "AA0BAA2B") + # Replace some main item graphics with the rooster + rom.banks[0x2C][0x0900:0x0940] = utils.createTileData(utils.tileDataToString(rom.banks[0x32][0x1D00:0x1D40]), " 321") + + # Trade sequence items + rom.banks[0x3F][0x3300:0x3640] = rom.banks[0x2C][0x0400:0x0740] diff --git a/worlds/ladx/LADXR/patches/bingo.py b/worlds/ladx/LADXR/patches/bingo.py new file mode 100644 index 0000000000..05a48c6980 --- /dev/null +++ b/worlds/ladx/LADXR/patches/bingo.py @@ -0,0 +1,1036 @@ +from ..backgroundEditor import BackgroundEditor +from ..roomEditor import RoomEditor, ObjectWarp +from ..assembler import ASM +from ..locations.constants import * +from ..utils import formatText + +# Few unused rooms that we can use the room status variables for to store data. +UNUSED_ROOMS = [0x15D, 0x17E, 0x17F, 0x1AD] +next_bit_flag_index = 0 + + +def getUnusedBitFlag(): + global next_bit_flag_index + addr = UNUSED_ROOMS[next_bit_flag_index // 8] + 0xD800 + bit_nr = next_bit_flag_index & 0x07 + mask = 1 << bit_nr + next_bit_flag_index += 1 + check_code = checkMemoryMask("$%04x" % (addr), "$%02x" % (mask)) + set_code = "ld hl, $%04x\nset %d, [hl]" % (addr, bit_nr) + return check_code, set_code + + +class Goal: + def __init__(self, description, code, tile_info, *, kill_code=None, group=None, extra_patches=None): + self.description = description + self.code = code + self.tile_info = tile_info + self.kill_code = kill_code + self.group = group + self.extra_patches = extra_patches or [] + + +class TileInfo: + def __init__(self, index1, index2=None, index3=None, index4=None, *, shift4=False, colormap=None, flipH=False): + self.index1 = index1 + self.index2 = index2 if index2 is not None else self.index1 + 1 + self.index3 = index3 + self.index4 = index4 + if self.index3 is None: + self.index3 = self.index1 if flipH else self.index1 + 2 + if self.index4 is None: + self.index4 = self.index2 if flipH else self.index1 + 3 + self.shift4 = shift4 + self.colormap = colormap + self.flipH = flipH + + def getTile(self, rom, idx): + return rom.banks[0x2C + idx // 0x400][(idx % 0x400) * 0x10:((idx % 0x400) + 1) * 0x10] + + def get(self, rom): + data = self.getTile(rom, self.index1) + self.getTile(rom, self.index2) + self.getTile(rom, + self.index3) + self.getTile( + rom, self.index4) + if self.shift4: + a = [] + b = [] + for c in data[0:32]: + a.append(c >> 4) + b.append((c << 4) & 0xFF) + data = bytes(a + b) + if self.flipH: + a = [] + for c in data[32:64]: + d = 0 + for bit in range(8): + if c & (1 << bit): + d |= 0x80 >> bit + a.append(d) + data = data[0:32] + bytes(a) + if self.colormap: + d = [] + for n in range(0, 64, 2): + a = data[n] + b = data[n + 1] + for bit in range(8): + col = 0 + if a & (1 << bit): + col |= 1 + if b & (1 << bit): + col |= 2 + col = self.colormap[col] + a &= ~(1 << bit) + b &= ~(1 << bit) + if col & 1: + a |= 1 << bit + if col & 2: + b |= 1 << bit + d.append(a) + d.append(b) + data = bytes(d) + return data + + +ITEM_TILES = { + BOMB: TileInfo(0x80, shift4=True), + POWER_BRACELET: TileInfo(0x82, shift4=True), + SWORD: TileInfo(0x84, shift4=True), + SHIELD: TileInfo(0x86, shift4=True), + BOW: TileInfo(0x88, shift4=True), + HOOKSHOT: TileInfo(0x8A, shift4=True), + MAGIC_ROD: TileInfo(0x8C, shift4=True), + MAGIC_POWDER: TileInfo(0x8E, shift4=True), + OCARINA: TileInfo(0x4E90, shift4=True), + FEATHER: TileInfo(0x4E92, shift4=True), + FLIPPERS: TileInfo(0x94, shift4=True), + SHOVEL: TileInfo(0x96, shift4=True), + PEGASUS_BOOTS: TileInfo(0x98, shift4=True), + SEASHELL: TileInfo(0x9E, shift4=True), + MEDICINE: TileInfo(0xA0, shift4=True), + BOOMERANG: TileInfo(0xA4, shift4=True), + TOADSTOOL: TileInfo(0x28C, shift4=True), + GOLD_LEAF: TileInfo(0xCA, shift4=True), +} + + +def checkMemoryEqualCode(location, value): + return """ + ld a, [%s] + cp %s + ret + """ % (location, value) + + +def checkMemoryNotZero(*locations): + if len(locations) == 1: + return """ + ld a, [%s] + and a + jp flipZ + """ % (locations[0]) + code = "" + for location in locations: + code += """ + ld a, [%s] + and a + jp z, flipZ + """ % (location) + code += "jp flipZ" + return code + + +def checkMemoryMask(location, mask): + if isinstance(location, tuple): + code = "" + for loc in location: + code += """ + ld a, [%s] + and %s + jp z, clearZ + """ % (loc, mask) + code += "jp setZ" + return code + return """ + ld a, [%s] + and %s + jp flipZ + """ % (location, mask) + + +def checkForSeashellsCode(count): + return """ + ld a, [wSeashellsCount] + cp $%02x + jp nc, setZ + ld a, [$DAE9] + and $10 + jp flipZ + """ % (count) + + +def checkMemoryEqualGreater(location, count): + return """ + ld a, [%s] + cp %s + jp nc, setZ + jp clearZ + """ % (location, count) + + +def InventoryGoal(item, *, memory_location=None, msg=None, group=None): + if memory_location is not None: + code = checkMemoryNotZero(memory_location) + elif item in INVENTORY_MAP: + code = """ + ld hl, $DB00 + ld e, INV_SIZE + + .checkLoop: + ldi a, [hl] + cp $%s + ret z ; item found, return with zero flag set to indicate goal done. + dec e + jr nz, .checkLoop + rra ; clear z flag + ret + """ % (INVENTORY_MAP[item]) + else: + code = """ + rra ; clear z flag + ret + """ + if msg is None: + msg = "Find the {%s}" % (item) + return Goal(msg, code, ITEM_TILES[item], group=group) + + +def KillGoal(description, entity_id, tile_info): + check_code, set_code = getUnusedBitFlag() + return Goal(description, check_code, tile_info, kill_code=""" + cp $%02x + jr nz, skip_%02x + %s + jp done + skip_%02x: + """ % (entity_id, entity_id, set_code, entity_id)) + + +def MonkeyGoal(description, tile_info): + check_code, set_code = getUnusedBitFlag() + return Goal(description, check_code, tile_info, extra_patches=[ + (0x15, 0x36EC, 0x36EF, ASM("jp $7FCE")), + (0x15, 0x3FCE, "00" * 8, ASM(""" + ld [hl], $FA + %s + ret + """ % (set_code))) + ]) + + +def BuzzBlobTalkGoal(description, tile_info): + check_code, set_code = getUnusedBitFlag() + return Goal(description, check_code, tile_info, extra_patches=[ + (0x18, 0x37C9, ASM("call $237C"), ASM("call $7FDE")), + (0x18, 0x3FDE, "00" * 11, ASM(""" + call $237C + ld [hl], $FA + %s + ret + """ % (set_code))) + ]) + + +def KillDethlGoal(description, tile_info): + check_code, set_code = getUnusedBitFlag() + return Goal(description, check_code, tile_info, extra_patches=[ + (0x15, 0x0606, 0x060B, ASM(set_code)), + ]) + + +def FishDaPondGoal(description, tile_info): + check_code, set_code = getUnusedBitFlag() + return Goal(description, check_code, tile_info, extra_patches=[ + (0x04, 0x21F7, 0x21FC, ASM(set_code)), + ]) + + +BINGO_GOALS = [ + InventoryGoal(BOOMERANG), + InventoryGoal(HOOKSHOT), + InventoryGoal(MAGIC_ROD), + InventoryGoal(PEGASUS_BOOTS), + InventoryGoal(FEATHER), + InventoryGoal(POWER_BRACELET), + Goal("Find the L2 {POWER_BRACELET}", checkMemoryEqualCode("$DB43", "2"), TileInfo(0x82, 0x83, 0x06, 0xB2)), + InventoryGoal(FLIPPERS, memory_location="wHasFlippers"), + InventoryGoal(OCARINA), + InventoryGoal(MEDICINE, memory_location="wHasMedicine", msg="Have the {MEDICINE}"), + InventoryGoal(BOW), + InventoryGoal(SHOVEL), + # InventoryGoal(MAGIC_POWDER), + InventoryGoal(TOADSTOOL, msg="Have the {TOADSTOOL}", group="witch"), + Goal("Find the L2 {SHIELD}", checkMemoryEqualCode("$DB44", "2"), TileInfo(0x86, 0x87, 0x06, 0xB2)), + Goal("Find 10 Secret Seashells", checkForSeashellsCode(10), ITEM_TILES[SEASHELL]), + Goal("Find the L2 {SWORD}", checkMemoryEqualCode("$DB4E", "2"), TileInfo(0x84, 0x85, 0x06, 0xB2)), + Goal("Find the {TAIL_KEY}", checkMemoryNotZero("$DB11"), TileInfo(0xC0, shift4=True)), + Goal("Find the {SLIME_KEY}", checkMemoryNotZero("$DB15"), TileInfo(0x28E, shift4=True)), + Goal("Find the {ANGLER_KEY}", checkMemoryNotZero("$DB12"), TileInfo(0xC2, shift4=True)), + Goal("Find the {FACE_KEY}", checkMemoryNotZero("$DB13"), TileInfo(0xC4, shift4=True)), + Goal("Find the {BIRD_KEY}", checkMemoryNotZero("$DB14"), TileInfo(0xC6, shift4=True)), + # {"description": "Marin's Cucco Killing Text"}, + # {"description": "Pick up Crane Game Owner"}, + BuzzBlobTalkGoal("Talk to a buzz blob", TileInfo(0x179C, colormap=[2, 3, 1, 0])), + # {"description": "Moblin King"}, + Goal("Turtle Rock Entrance Boss", checkMemoryMask("$D810", "$20"), + TileInfo(0x1413, flipH=True, colormap=[2, 3, 1, 0])), + Goal("Kill Master Stalfos", checkMemoryMask("$D980", "$10"), TileInfo(0x1622, colormap=[2, 3, 1, 0])), + # {"description": "Gohma"}, + # {"description": "Grim Creeper"}, + # {"description": "Blaino"}, + KillDethlGoal("Kill Dethl", TileInfo(0x1B38, colormap=[2, 3, 1, 0])), + # {"description": "Rooster"}, + # {"description": "Marin"}, + # {"description": "Bow-wow"}, + # {"description": "Return Bow-wow"}, + # {"description": "8 Heart Pieces"}, + # {"description": "12 Heart Pieces"}, + Goal("{BOMB} upgrade", checkMemoryEqualCode("$DB77", "$60"), TileInfo(0x80, 0x81, 0x06, 0xA3)), + Goal("Arrow upgrade", checkMemoryEqualCode("$DB78", "$60"), TileInfo(0x88, 0x89, 0x06, 0xA3)), + Goal("{MAGIC_POWDER} upgrade", checkMemoryEqualCode("$DB76", "$40"), TileInfo(0x8E, 0x8F, 0x06, 0xA3)), + # {"description": "Steal From Shop 5 Times"}, + KillGoal("Kill the giant ghini", 0x11, TileInfo(0x08A6, colormap=[2, 3, 1, 0])), + Goal("Got the Ballad of the Wind Fish", checkMemoryMask("$DB49", "4"), + TileInfo(0x298, flipH=True, colormap=[2, 3, 1, 0])), + Goal("Got the Manbo's Mambo", checkMemoryMask("$DB49", "2"), TileInfo(0x29A, flipH=True, colormap=[2, 3, 1, 0])), + Goal("Got the Frog's Song of Soul", checkMemoryMask("$DB49", "1"), + TileInfo(0x29C, flipH=True, colormap=[2, 3, 1, 0])), + Goal("Map and Compass in Tail Cave", checkMemoryNotZero("$DB16", "$DB17"), TileInfo(0x1BD0, index4=0xB1)), + Goal("Map and Compass in Bottle Grotto", checkMemoryNotZero("$DB1B", "$DB1C"), TileInfo(0x1BD0, index4=0xB2)), + Goal("Map and Compass in Key Cavern", checkMemoryNotZero("$DB20", "$DB21"), TileInfo(0x1BD0, index4=0xB3)), + Goal("Map and Compass in Angler's Tunnel", checkMemoryNotZero("$DB25", "$DB26"), TileInfo(0x1BD0, index4=0xB4)), + Goal("Map and Compass in Catfish's Maw", checkMemoryNotZero("$DB2A", "$DB2B"), TileInfo(0x1BD0, index4=0xB5)), + Goal("Map and Compass in Face Shrine", checkMemoryNotZero("$DB2F", "$DB30"), TileInfo(0x1BD0, index4=0xB6)), + Goal("Map and Compass in Eagle's Tower", checkMemoryNotZero("$DB34", "$DB35"), TileInfo(0x1BD0, index4=0xB7)), + Goal("Map and Compass in Turtle Rock", checkMemoryNotZero("$DB39", "$DB3A"), TileInfo(0x1BD0, index4=0xB8)), + Goal("Map and Compass in Color Dungeon", checkMemoryNotZero("$DDDA", "$DDDB"), TileInfo(0x1BD0, index4=0xB0)), + # {"description": "Talk to all Owl Statues in Tail Cave"}, + # {"description": "Talk to all Owl Statues in Bottle Grotto"}, + # {"description": "Talk to all Owl Statues in Key Cavern"}, + # {"description": "Talk to all Owl Statues in Angler's Tunnel"}, + # {"description": "Talk to all Owl Statues in Catfish's Maw"}, + # {"description": "Talk to all Owl Statues in Face Shrine"}, + # {"description": "Talk to all Owl Statues in Eagle's Tower"}, + # {"description": "Talk to all Owl Statues in Turtle Rock"}, + # {"description": "Talk to all Owl Statues in Color Dungeon"}, + # {"description": "Defeat 2 Sets of 3-Of-A-Kind (D1, D7)"}, + # {"description": "Stand 6 HorseHeads in D6"}, + Goal("Find the 5 Golden Leaves", checkMemoryEqualGreater("wGoldenLeaves", "5"), ITEM_TILES[GOLD_LEAF]), + # {"description": "Defeat Mad Bomber (outside Kanalet Castle)"}, + # {"description": "Totaka's Song in Richard's Villa"}, + Goal("Get the Yoshi Doll", checkMemoryMask("$DAA0", "$20"), TileInfo(0x9A)), + # {"description": "Save Papahl on the mountain"}, + Goal("Give the banana to Kiki", checkMemoryMask("$D87B", "$20"), TileInfo(0x1670, colormap=[2, 3, 1, 0])), + Goal("Have 99 or less rupees", checkMemoryEqualCode("$DB5D", "0"), TileInfo(0xA6, 0xA7, shift4=True), group="rupees"), + Goal("Have 900 or more rupees", checkMemoryEqualGreater("$DB5D", "9"), TileInfo(0xA6, 0xA7, 0xA6, 0xA7), group="rupees"), + MonkeyGoal("Bonk the Beach Monkey", TileInfo(0x1946, colormap=[2, 3, 1, 0])), + # {"description": "Kill an enemy after transforming"}, + + Goal("Got the Red Tunic", checkMemoryMask("wCollectedTunics", "1"), + TileInfo(0x2400, 0x0D11, 0x2400, 0x2401, flipH=True, colormap=[2, 3, 1, 0])), + Goal("Got the Blue Tunic", checkMemoryMask("wCollectedTunics", "2"), + TileInfo(0x2400, 0x0D01, 0x2400, 0x2401, flipH=True, colormap=[2, 3, 1, 0])), + Goal("Buy the first shop item", checkMemoryMask("$DAA1", "$10"), TileInfo(0x0880, colormap=[2, 3, 1, 0])), + Goal("Buy the second shop item", checkMemoryMask("$DAA1", "$20"), TileInfo(0x0880, colormap=[2, 3, 1, 0])), + Goal("Find the {INSTRUMENT1}", checkMemoryMask("$DB65", "2"), TileInfo(0x1500, colormap=[2, 3, 1, 0])), + Goal("Find the {INSTRUMENT2}", checkMemoryMask("$DB66", "2"), TileInfo(0x1504, colormap=[2, 3, 1, 0])), + Goal("Find the {INSTRUMENT3}", checkMemoryMask("$DB67", "2"), TileInfo(0x1508, colormap=[2, 3, 1, 0])), + Goal("Find the {INSTRUMENT4}", checkMemoryMask("$DB68", "2"), TileInfo(0x150C, colormap=[2, 3, 1, 0])), + Goal("Find the {INSTRUMENT5}", checkMemoryMask("$DB69", "2"), TileInfo(0x1510, colormap=[2, 3, 1, 0])), + Goal("Find the {INSTRUMENT6}", checkMemoryMask("$DB6A", "2"), TileInfo(0x1514, colormap=[2, 3, 1, 0])), + Goal("Find the {INSTRUMENT7}", checkMemoryMask("$DB6B", "2"), TileInfo(0x1518, colormap=[2, 3, 1, 0])), + Goal("Find the {INSTRUMENT8}", checkMemoryMask("$DB6C", "2"), TileInfo(0x151C, colormap=[2, 3, 1, 0])), + # {"description": "Moldorm", "group": "d1"}, + # {"description": "Genie in a Bottle", "group": "d2"}, + # {"description": "Slime Eyes", "group": "d3"}, + # {"description": "Angler Fish", "group": "d4"}, + # {"description": "Slime Eel", "group": "d5"}, + # {"description": "Facade", "group": "d6"}, + # {"description": "Evil Eagle", "group": "d7"}, + # {"description": "Hot Head", "group": "d8"}, + # {"description": "2 Followers at the same time", "group": "multifollower"}, + # {"description": "3 Followers at the same time", "group": "multifollower"}, + Goal("Visit the 4 Fountain Fairies", checkMemoryMask(("$D853", "$D9AC", "$D9F3", "$D9FB"), "$80"), + TileInfo(0x20, shift4=True, colormap=[2, 3, 1, 0])), + Goal("Have at least 8 Heart Containers", checkMemoryEqualGreater("$DB5B", "8"), TileInfo(0xAA, flipH=True), group="Health"), + Goal("Have at least 9 Heart Containers", checkMemoryEqualGreater("$DB5B", "9"), TileInfo(0xAA, flipH=True), group="Health"), + Goal("Have at least 10 Heart Containers", checkMemoryEqualGreater("$DB5B", "10"), TileInfo(0xAA, flipH=True), group="Health"), + Goal("Got photo 1: Here Stands A Brave Man", checkMemoryMask("$DC0C", "$01"), TileInfo(0x3008, 0x0D0F, colormap=[2, 3, 1, 0])), + Goal("Got photo 2: Looking over the sea with Marin", checkMemoryMask("$DC0C", "$02"), TileInfo(0x08F0, 0x0D0F, colormap=[2, 3, 1, 0])), + Goal("Got photo 3: Heads up!", checkMemoryMask("$DC0C", "$04"), TileInfo(0x0E6A, 0x0D0F, 0x0E6B, 0x0E7B)), + Goal("Got photo 4: Say Mushroom!", checkMemoryMask("$DC0C", "$08"), TileInfo(0x0E60, 0x0D0F, 0x0E61, 0x0E71)), + Goal("Got photo 5: Ulrira's Secret!", checkMemoryMask("$DC0C", "$10"), + TileInfo(0x1461, 0x1464, 0x1463, 0x0D0F, colormap=[2, 3, 1, 0])), + Goal("Got photo 6: Playing with Bowwow!", checkMemoryMask("$DC0C", "$20"), + TileInfo(0x1A42, 0x0D0F, 0x1A42, 0x1A43, flipH=True, colormap=[2, 3, 1, 0])), + Goal("Got photo 7: Thief!", checkMemoryMask("$DC0C", "$40"), TileInfo(0x0880, 0x0D0F, colormap=[2, 3, 1, 0])), + Goal("Got photo 8: Be more careful next time!", checkMemoryMask("$DC0C", "$80"), TileInfo(0x14E0, index4=0x0D0F, colormap=[2, 3, 1, 0])), + Goal("Got photo 9: I Found Zora!", checkMemoryMask("$DC0D", "$01"), + TileInfo(0x1906, 0x0D0F, 0x1906, 0x1907, flipH=True, colormap=[2, 3, 1, 0])), + Goal("Got photo 10: Richard at Kanalet Castle", checkMemoryMask("$DC0D", "$02"), TileInfo(0x15B0, 0x0D0F, colormap=[2, 3, 1, 0])), + Goal("Got photo 11: Ghost", checkMemoryMask("$DC0D", "$04"), TileInfo(0x1980, 0x0D0F, colormap=[2, 3, 1, 0])), + Goal("Got photo 12: Close Call", checkMemoryMask("$DC0D", "$08"), TileInfo(0x0FED, 0x0D0F, 0x0FED, 0x0FFD)), + # {"description": "Collect 4 Pictures", "group": "pics"}, + # {"description": "Collect 5 Pictures", "group": "pics"}, + # {"description": "Collect 6 Pictures", "group": "pics"}, + Goal("Open the 4 Overworld Warp Holes", checkMemoryMask(("$D801", "$D82C", "$D895", "$D8EC"), "$80"), + TileInfo(0x3E, 0x3E, 0x3E, 0x3E, colormap=[2, 1, 3, 0])), + Goal("Finish the Raft Minigame", checkMemoryMask("$D87F", "$80"), TileInfo(0x087C, flipH=True, colormap=[2, 3, 1, 0])), + Goal("Kill the Ball and Chain Trooper", checkMemoryMask("$DAC6", "$10"), TileInfo(0x09A4, colormap=[2, 3, 1, 0])), + Goal("Destroy all Pillars with the Ball", checkMemoryMask(("$DA14", "$DA15", "$DA18", "$DA19"), "$20"), + TileInfo(0x166C, flipH=True)), + FishDaPondGoal("Fish the pond empty", TileInfo(0x0A00, colormap=[2, 3, 1, 0])), + KillGoal("Kill the Anti-Kirby", 0x91, TileInfo(0x1550, colormap=[2, 3, 1, 0])), + KillGoal("Kill a Rolling Bones", 0x81, TileInfo(0x0AB6, colormap=[2, 3, 1, 0])), + KillGoal("Kill a Hinox", 0x89, TileInfo(0x1542, colormap=[2, 3, 1, 0])), + KillGoal("Kill a Stone Hinox", 0xF4, TileInfo(0x2482, colormap=[2, 3, 1, 0])), + KillGoal("Kill a Dodongo", 0x60, TileInfo(0x0AA0, flipH=True, colormap=[2, 3, 1, 0])), + KillGoal("Kill a Cue Ball", 0x8E, TileInfo(0x1566, flipH=True, colormap=[2, 3, 1, 0])), + KillGoal("Kill a Smasher", 0x92, TileInfo(0x1576, colormap=[2, 3, 1, 0])), + + Goal("Save Marin on the Mountain Bridge", checkMemoryMask("$D808", "$10"), TileInfo(0x1A6C, colormap=[2, 3, 1, 0])), + Goal("Save Raccoon Tarin", checkMemoryMask("$D851", "$10"), TileInfo(0x1888, colormap=[2, 3, 1, 0])), + Goal("Trade the {TOADSTOOL} with the witch", checkMemoryMask("$DAA2", "$20"), TileInfo(0x0A30, colormap=[2, 3, 1, 0]), group="witch"), +] + + +def randomizeGoals(rnd, options): + goals = BINGO_GOALS.copy() + rnd.shuffle(goals) + has_group = set() + for n in range(len(goals)): + if goals[n].group: + if goals[n].group in has_group: + goals[n] = None + else: + has_group.add(goals[n].group) + goals = [goal for goal in goals if goal is not None] + return goals[:25] + + +def setBingoGoal(rom, goals, mode): + assert len(goals) == 25 + + for goal in goals: + for bank, addr, current, target in goal.extra_patches: + rom.patch(bank, addr, current, target) + + # Setup the bingo card visuals + be = BackgroundEditor(rom, 0x15) + ba = BackgroundEditor(rom, 0x15, attributes=True) + for y in range(18): + for x in range(20): + be.tiles[0x9800 + x + y * 0x20] = (x + (y & 2)) % 4 | ((y % 2) * 4) + ba.tiles[0x9800 + x + y * 0x20] = 0x01 + + for y in range(5): + for x in range(5): + idx = x + y * 5 + be.tiles[0x9843 + x * 3 + y * 3 * 0x20] = 8 + idx * 4 + be.tiles[0x9844 + x * 3 + y * 3 * 0x20] = 10 + idx * 4 + be.tiles[0x9863 + x * 3 + y * 3 * 0x20] = 9 + idx * 4 + be.tiles[0x9864 + x * 3 + y * 3 * 0x20] = 11 + idx * 4 + + ba.tiles[0x9843 + x * 3 + y * 3 * 0x20] = 0x03 + ba.tiles[0x9844 + x * 3 + y * 3 * 0x20] = 0x03 + ba.tiles[0x9863 + x * 3 + y * 3 * 0x20] = 0x03 + ba.tiles[0x9864 + x * 3 + y * 3 * 0x20] = 0x03 + be.store(rom) + ba.store(rom) + + tiles = rom.banks[0x30][0x3000:0x3040] + rom.banks[0x30][0x3100:0x3140] + for goal in goals: + tiles += goal.tile_info.get(rom) + rom.banks[0x30][0x3000:0x3000 + len(tiles)] = tiles + + # Patch the mural palette to have more useful entries for us + rom.patch(0x21, 0x36EE, ASM("dw $7C00, $7C00, $7C00, $7C00"), ASM("dw $7FFF, $56b5, $294a, $0000")) + rom.patch(0x21, 0x36F6, ASM("dw $7C00, $7C00, $7C00, $7C00"), ASM("dw $43f0, $32ac, $1946, $0000")) + + # Patch the face shrine mural handler stage 4, we want to jump to bank 0x0C, which normally contains + # DMG graphics, but gives us a lot of room for our own code and graphics. + rom.patch(0x01, 0x2B81, 0x2B99, ASM(""" + ld a, $0D + ld hl, $4000 + push hl + jp $080C ; switch bank + """), fill_nop=True) + # Fix that the mural is always centered on screen, instead offset it properly + rom.patch(0x18, 0x1E3D, ASM("add hl, bc\nld [hl], $50"), ASM("call $7FD6")) + rom.patch(0x18, 0x3FD6, "00" * 8, ASM(""" + add hl, bc + ld a, [hl] + and $F0 + add a, $0F + ld [hl], a + ret + """)) + + # In stage 5, just exit the mural without waiting for a button press. + rom.patch(0x01, 0x2B9E, ASM("jr z, $07"), "", fill_nop=True) + + # Our custom stage 4 + rom.patch(0x0D, 0x0000, 0x3000, ASM(""" +wState := $C3C4 ; Our internal state, guaranteed to be 0 on the first entry. +wCursorX := $D100 +wCursorY := $D101 + + call mainHandler + ; Make sure we return with bank 1 active. + ld a, $01 + jp $080C ; switch bank + +mainHandler: + ld a, [wState] + rst 0 + dw init + dw checkGoalDone + dw chooseSquare + dw waitDialogDone + dw finishGame + +init: + xor a + ld [wCursorX], a + ld [wCursorY], a + inc a + ld [wState], a + di + ldh [$4F], a ; 2nd vram bank + + ld hl, $9843 + ld c, 25 + ld b, $00 +.checkDoneLoop: + push bc + push hl + ld a, b + call goalCheck + pop hl + jr nz, .notDone +.statWait1: + ldh a, [$41] ;STAT + and $02 + jr nz, .statWait1 + ld a, $04 + ldi [hl], a + ldd [hl], a + + ld bc, $0020 + add hl, bc +.statWait2: + ldh a, [$41] ;STAT + and $02 + jr nz, .statWait2 + ld a, $04 + ldi [hl], a + ldd [hl], a + + ld bc, $FFE0 + add hl, bc +.notDone: + inc hl + inc hl + inc hl + ld a, l + and $1F + cp $12 + jr nz, .noRowSkip + ld bc, $0060 - 5*3 + add hl, bc +.noRowSkip: + pop bc + inc b + dec c + jr nz, .checkDoneLoop + + xor a + ldh [$4F], a ; 1st vram bank + ei + +checkGoalDone: + ld a, $02 + ld [wState], a + ; Check if the egg event is already triggered + ld a, [$D806] + and $10 + ret nz + + call checkAnyGoal + ret nz + + ; Goal done, give a message and goto state to finish the game + ld a, $04 + ld [wState], a + ld a, $E7 + call $2385 ; open dialog + ret + +chooseSquare: + ld hl, $C000 ; oam buffer + + ; Draw the cursor + ld a, [wCursorY] + call multiA24 + add a, $27 + ldi [hl], a + ld a, [wCursorX] + call multiA24 + add a, $24 + ldi [hl], a + ld a, $A2 + ldi [hl], a + ld a, $02 + ldi [hl], a + + ldh a, [$CC] ; button presses + bit 0, a + jr nz, .right + bit 1, a + jr nz, .left + bit 2, a + jr nz, .up + bit 3, a + jr nz, .down + bit 4, a + jr nz, .showText + bit 5, a + jr nz, exitMural + bit 7, a + jr nz, exitMural + ret + +.right: + ld a, [wCursorX] + cp $04 + ret z + inc a + ld [wCursorX], a + ret + +.left: + ld a, [wCursorX] + and a + ret z + dec a + ld [wCursorX], a + ret + +.down: + ld a, [wCursorY] + cp $04 + ret z + inc a + ld [wCursorY], a + ret + +.up: + ld a, [wCursorY] + and a + ret z + dec a + ld [wCursorY], a + ret + +.showText: + ld a, [wCursorY] + ld c, a + add a, a + add a, a + add a, c + ld c, a + ld a, [wCursorX] + add a, c + add a, a + ld l, a + ld h, $00 + ld de, messageTable + add hl, de + ldi a, [hl] + ld h, [hl] + ld l, a + ld de, wCustomMessage +.copyLoop: + ldi a, [hl] + ld [de], a + inc de + inc a + jr nz, .copyLoop + + ld a, $C9 + call $2385 ; open dialog + ld a, $03 + ld [wState], a + ret + +waitDialogDone: + ld a, [$C19F] ; dialog state + and a + ret nz + ld a, $02 ; choose square + ld [wState], a + ret + +finishGame: + ld a, [$C19F] ; dialog state + and a + ret nz + + ldh a, [$CC] ; button presses + and a + ret z + + ; Goto "credits" + xor a + ld [$DB96], a + inc a + ld [$DB95], a + ret + +exitMural: + ld hl, $DB96 ;gameplay subtype + inc [hl] + ret + +multiA24: + ld c, a + add a, a + add a, c + add a, a + add a, a + add a, a + ret + +flipZ: + jr nz, setZ +clearZ: + rra + ret +setZ: + cp a + ret + +checkAnyGoal: +#IF {mode} + call goalcheck_0 + ret nz + call goalcheck_1 + ret nz + call goalcheck_2 + ret nz + call goalcheck_3 + ret nz + call goalcheck_4 + ret nz + call goalcheck_5 + ret nz + call goalcheck_6 + ret nz + call goalcheck_7 + ret nz + call goalcheck_8 + ret nz + call goalcheck_9 + ret nz + call goalcheck_10 + ret nz + call goalcheck_11 + ret nz + call goalcheck_12 + ret nz + call goalcheck_13 + ret nz + call goalcheck_14 + ret nz + call goalcheck_15 + ret nz + call goalcheck_16 + ret nz + call goalcheck_17 + ret nz + call goalcheck_18 + ret nz + call goalcheck_19 + ret nz + call goalcheck_20 + ret nz + call goalcheck_21 + ret nz + call goalcheck_22 + ret nz + call goalcheck_23 + ret nz + call goalcheck_24 + ret +#ELSE + call checkGoalRow1 + ret z + call checkGoalRow2 + ret z + call checkGoalRow3 + ret z + call checkGoalRow4 + ret z + call checkGoalRow5 + ret z + call checkGoalCol1 + ret z + call checkGoalCol2 + ret z + call checkGoalCol3 + ret z + call checkGoalCol4 + ret z + call checkGoalCol5 + ret z + call checkGoalDiagonal0 + ret z + call checkGoalDiagonal1 + ret + +checkGoalRow1: + call goalcheck_0 + ret nz + call goalcheck_1 + ret nz + call goalcheck_2 + ret nz + call goalcheck_3 + ret nz + call goalcheck_4 + ret + +checkGoalRow2: + call goalcheck_5 + ret nz + call goalcheck_6 + ret nz + call goalcheck_7 + ret nz + call goalcheck_8 + ret nz + call goalcheck_9 + ret + +checkGoalRow3: + call goalcheck_10 + ret nz + call goalcheck_11 + ret nz + call goalcheck_12 + ret nz + call goalcheck_13 + ret nz + call goalcheck_14 + ret + +checkGoalRow4: + call goalcheck_15 + ret nz + call goalcheck_16 + ret nz + call goalcheck_17 + ret nz + call goalcheck_18 + ret nz + call goalcheck_19 + ret + +checkGoalRow5: + call goalcheck_20 + ret nz + call goalcheck_21 + ret nz + call goalcheck_22 + ret nz + call goalcheck_23 + ret nz + call goalcheck_24 + ret + +checkGoalCol1: + call goalcheck_0 + ret nz + call goalcheck_5 + ret nz + call goalcheck_10 + ret nz + call goalcheck_15 + ret nz + call goalcheck_20 + ret + +checkGoalCol2: + call goalcheck_1 + ret nz + call goalcheck_6 + ret nz + call goalcheck_11 + ret nz + call goalcheck_16 + ret nz + call goalcheck_21 + ret + +checkGoalCol3: + call goalcheck_2 + ret nz + call goalcheck_7 + ret nz + call goalcheck_12 + ret nz + call goalcheck_17 + ret nz + call goalcheck_22 + ret + +checkGoalCol4: + call goalcheck_3 + ret nz + call goalcheck_8 + ret nz + call goalcheck_13 + ret nz + call goalcheck_18 + ret nz + call goalcheck_23 + ret + +checkGoalCol5: + call goalcheck_4 + ret nz + call goalcheck_9 + ret nz + call goalcheck_14 + ret nz + call goalcheck_19 + ret nz + call goalcheck_24 + ret + +checkGoalDiagonal0: + call goalcheck_0 + ret nz + call goalcheck_6 + ret nz + call goalcheck_12 + ret nz + call goalcheck_18 + ret nz + call goalcheck_24 + ret + +checkGoalDiagonal1: + call goalcheck_4 + ret nz + call goalcheck_8 + ret nz + call goalcheck_12 + ret nz + call goalcheck_16 + ret nz + call goalcheck_20 + ret +#ENDIF + +messageTable: +""".format(mode=1 if mode == "bingo-full" else 0) + + "\n".join(["dw message_%d" % (n) for n in range(25)]) + "\n" + + "\n".join(["message_%d:\n db m\"%s\"" % (n, goal.description) for n, goal in + enumerate(goals)]) + "\n" + + """ + goalCheck: + rst 0 + """ + + "\n".join(["dw goalcheck_%d" % (n) for n in range(25)]) + "\n" + + "\n".join(["goalcheck_%d:\n %s\n" % (n, goal.code) for n, goal in + enumerate(goals)]) + "\n", 0x4000), fill_nop=True) + rom.texts[0xE7] = formatText("BINGO!\nPress any button to finish.") + + # Patch the game to call a bit of our code when an enemy is killed by patching into the drop item handling + rom.patch(0x00, 0x3F50, ASM("ld a, $03\nld [$C113], a\nld [$2100], a\ncall $55CF"), ASM(""" + ld a, $0D + ld [$C113], a + ld [$2100], a + call $7000 + """)) + rom.patch(0x0D, 0x3000, 0x4000, ASM(""" + ldh a, [$EB] ; active entity + """ + "\n".join([goal.kill_code for goal in goals if goal.kill_code is not None]) + """ +done: ; Return to normal item drop handler + ld a, $03 ;normal drop item handler bank + ld hl, $55CF ;normal drop item handler address + push hl + jp $080F ; switch bank + """, 0x7000), fill_nop=True) + + # Patch Dethl to warp you outside + rom.patch(0x15, 0x0682, 0x069B, ASM(""" + ld a, $0B + ld [$DB95], a + call $0C7D + + ld a, $07 + ld [$DB96], a + """), fill_nop=True) + re = RoomEditor(rom, 0x274) + re.objects += [ObjectWarp(0, 0, 0x06, 0x58, 0x40)] * 4 + re.store(rom) + # Patch the egg to be always open + rom.patch(0x00, 0x31f5, ASM("ld a, [$D806]\nand $10\njr z, $25"), ASM(""), fill_nop=True) + rom.patch(0x20, 0x2dea, ASM("ld a, [$D806]\nand $10\njr z, $29"), ASM(""), fill_nop=True) + + # Patch unused entity 4C into our bingo board. + rom.patch(0x03, 0x004C, "41", "82") + rom.patch(0x03, 0x0147, "00", "98") + rom.patch(0x20, 0x00e4, "000000", ASM("dw $5e1b\ndb $18")) + + # Add graphics for our bingo board to 2nd WRAM bank. + rom.banks[0x3F][0x3700:0x3780] = rom.banks[0x32][0x1500:0x1580] + rom.banks[0x3F][0x3728:0x373A] = b'\x55\xAA\x00\xFF\x55\xAA\x00\xFF\x55\xAA\x00\xFF\x55\xAA\x00\xFF\x00\xFF' + rom.banks[0x3F][0x3748:0x375A] = b'\x55\xAA\x00\xFF\x55\xAA\x00\xFF\x55\xAA\x00\xFF\x55\xAA\x00\xFF\x00\xFF' + rom.patch(0x18, 0x1E0B, + "00F85003" + "00005203" + "00085403" + "00105603", + "00F8F00B" + "0000F20B" + "0008F40B" + "0010F60B") + + # Add the bingo board to marins house + re = RoomEditor(rom, 0x2A3) + re.entities.append((2, 0, 0x4C)) + re.store(rom) + + # Add the bingo board to the room before the egg + re = RoomEditor(rom, 0x016) + re.removeObject(4, 5) + re.entities.append((3, 4, 0x4C)) + re.updateOverlay() + re.store(rom) + + # Remove the egg event from the egg room (no bomb triggers for you!) + re = RoomEditor(rom, 0x006) + re.entities = [] + re.store(rom) + + rom.texts[0xCF] = formatText(""" + Bingo! + Young lad, I mean... #####, the hero! + You have bingo! + You have proven your wisdom, courage and power! + ... ... ... ... + As part of the Wind Fish's spirit, I am the guardian of his dream world... + But one day, we decided to have a bingo game. + Then you, #####, came to win the bingo... + Thank you, #####... + My work is done... + The Wind Fish will wake soon. + Good bye...Bingo! + """) + rom.texts[0xCE] = rom.texts[0xCF] diff --git a/worlds/ladx/LADXR/patches/bomb.py b/worlds/ladx/LADXR/patches/bomb.py new file mode 100644 index 0000000000..4d9b2891d4 --- /dev/null +++ b/worlds/ladx/LADXR/patches/bomb.py @@ -0,0 +1,20 @@ +from ..assembler import ASM + + +def onlyDropBombsWhenHaveBombs(rom): + rom.patch(0x03, 0x1FC5, ASM("call $608C"), ASM("call $50B2")) + # We use some of the unused chest code space here to remove the bomb if you do not have bombs in your inventory. + rom.patch(0x03, 0x10B2, 0x112A, ASM(""" + ld e, INV_SIZE + ld hl, $DB00 + ld a, $02 +loop: + cp [hl] + jr z, resume + dec e + inc hl + jr nz, loop + jp $3F8D ; unload entity +resume: + jp $608C + """), fill_nop=True) \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/bowwow.py b/worlds/ladx/LADXR/patches/bowwow.py new file mode 100644 index 0000000000..479f360514 --- /dev/null +++ b/worlds/ladx/LADXR/patches/bowwow.py @@ -0,0 +1,207 @@ +from ..assembler import ASM +from ..roomEditor import RoomEditor + + +def fixBowwow(rom, everywhere=False): + ### BowWow patches + rom.patch(0x03, 0x1E0E, ASM("ld [$DB56], a"), "", fill_nop=True) # Do not mark BowWow as kidnapped after we complete dungeon 1. + rom.patch(0x15, 0x06B6, ASM("ld a, [$DB56]\ncp $80"), ASM("xor a"), fill_nop=True) # always load the moblin boss + rom.patch(0x03, 0x182D, ASM("ld a, [$DB56]\ncp $80"), ASM("ld a, [$DAE2]\nand $10")) # load the cave moblins if the chest is not opened + rom.patch(0x07, 0x3947, ASM("ld a, [$DB56]\ncp $80"), ASM("ld a, [$DAE2]\nand $10")) # load the cave moblin with sword if the chest is not opened + + # Modify the moblin cave to contain a chest at the end, which contains bowwow + re = RoomEditor(rom, 0x2E2) + re.removeEntities(0x6D) + re.changeObject(8, 3, 0xA0) + re.store(rom) + # Place bowwow in the chest table + rom.banks[0x14][0x560 + 0x2E2] = 0x81 + + # Patch bowwow follower sprite to be used from 2nd vram bank + rom.patch(0x05, 0x001C, + b"40034023" + b"42034223" + b"44034603" + b"48034A03" + b"46234423" + b"4A234823" + b"4C034C23", + b"500B502B" + b"520B522B" + b"540B560B" + b"580B5A0B" + b"562B542B" + b"5A2B582B" + b"5C0B5C2B") + # Patch to use the chain sprite from second vram bank (however, the chain bugs out various things) + rom.patch(0x05, 0x0282, + ASM("ld a, $4E\njr nz, $02\nld a, $7E\nld [de], a\ninc de\nld a, $00"), + ASM("ld a, $5E\nld [de], a\ninc de\nld a, $08"), fill_nop=True) + # Never load the bowwow tiles in the first VRAM bank, as we do not need them. + rom.patch(0x00, 0x2EB0, ASM("ld a, [$DB56]\ncp $01\nld a, $A4\njr z, $18"), "", fill_nop=True) + + # Patch the location where bowwow stores chain X/Y positions so it does not conflict with a lot of other things + rom.patch(0x05, 0x00BE, ASM("ld hl, $D100"), ASM("ld hl, $D180")) + rom.patch(0x05, 0x0275, ASM("ld hl, $D100"), ASM("ld hl, $D180")) + rom.patch(0x05, 0x03AD, ASM("ld [$D100], a"), ASM("ld [$D180], a")) + rom.patch(0x05, 0x03BD, ASM("ld de, $D100"), ASM("ld de, $D180")) + rom.patch(0x05, 0x049F, ASM("ld hl, $D100"), ASM("ld hl, $D180")) + rom.patch(0x05, 0x04C2, ASM("ld a, [$D100]"), ASM("ld a, [$D180]")) + rom.patch(0x05, 0x03C0, ASM("ld hl, $D101"), ASM("ld hl, $D181")) + rom.patch(0x05, 0x0418, ASM("ld [$D106], a"), ASM("ld [$D186], a")) + rom.patch(0x05, 0x0423, ASM("ld de, $D106"), ASM("ld de, $D186")) + rom.patch(0x05, 0x0426, ASM("ld hl, $D105"), ASM("ld hl, $D185")) + + rom.patch(0x19, 0x3A4E, ASM("ld hl, $D100"), ASM("ld hl, $D180")) + rom.patch(0x19, 0x3A5A, ASM("ld hl, $D110"), ASM("ld hl, $D190")) + + rom.patch(0x05, 0x00D9, ASM("ld hl, $D110"), ASM("ld hl, $D190")) + rom.patch(0x05, 0x026E, ASM("ld hl, $D110"), ASM("ld hl, $D190")) + rom.patch(0x05, 0x03BA, ASM("ld [$D110], a"), ASM("ld [$D190], a")) + rom.patch(0x05, 0x03DD, ASM("ld de, $D110"), ASM("ld de, $D190")) + rom.patch(0x05, 0x0480, ASM("ld hl, $D110"), ASM("ld hl, $D190")) + rom.patch(0x05, 0x04B5, ASM("ld a, [$D110]"), ASM("ld a, [$D190]")) + rom.patch(0x05, 0x03E0, ASM("ld hl, $D111"), ASM("ld hl, $D191")) + rom.patch(0x05, 0x0420, ASM("ld [$D116], a"), ASM("ld [$D196], a")) + rom.patch(0x05, 0x044d, ASM("ld de, $D116"), ASM("ld de, $D196")) + rom.patch(0x05, 0x0450, ASM("ld hl, $D115"), ASM("ld hl, $D195")) + + rom.patch(0x05, 0x0039, ASM("ld [$D154], a"), "", fill_nop=True) # normally this stores the index to bowwow, for the kiki fight + rom.patch(0x05, 0x013C, ASM("ld [$D150], a"), ASM("ld [$D197], a")) + rom.patch(0x05, 0x0144, ASM("ld [$D151], a"), ASM("ld [$D198], a")) + rom.patch(0x05, 0x02F9, ASM("ld [$D152], a"), ASM("ld [$D199], a")) + rom.patch(0x05, 0x0335, ASM("ld a, [$D152]"), ASM("ld a, [$D199]")) + rom.patch(0x05, 0x0485, ASM("ld a, [$D151]"), ASM("ld a, [$D198]")) + rom.patch(0x05, 0x04A4, ASM("ld a, [$D150]"), ASM("ld a, [$D197]")) + + # Patch the bowwow create code to call our custom check of we are in swamp function. + if everywhere: + # Load followers in dungeons, caves, etc + rom.patch(0x01, 0x1FC1, ASM("ret z"), "", fill_nop=True) + rom.patch(0x01, 0x1FC4, ASM("ret z"), "", fill_nop=True) + rom.patch(0x01, 0x1FC7, ASM("ret z"), "", fill_nop=True) + rom.patch(0x01, 0x1FCA, ASM("ret c"), "", fill_nop=True) # dungeon + # rom.patch(0x01, 0x1FBC, ASM("ret nz"), "", fill_nop=True) # sidescroller: TOFIX this breaks fishing minigame reward + else: + # Patch the bowwow create code to call our custom check of we are in swamp function. + rom.patch(0x01, 0x211F, ASM("ldh a, [$F6]\ncp $A7\nret z\nld a, [$DB56]\ncp $01\njr nz, $36"), ASM(""" + ld a, $07 + rst 8 + ld a, e + and a + ret z + """), fill_nop=True) + # Patch bowwow to not stay around when we move from map to map + rom.patch(0x05, 0x0049, 0x0054, ASM(""" + cp [hl] + jr z, Continue + ld hl, $C280 + add hl, bc + ld [hl], b + ret +Continue: + """), fill_nop=True) + + # Patch madam meow meow to not take bowwow + rom.patch(0x06, 0x1BD7, ASM("ld a, [$DB66]\nand $02"), ASM("ld a, $00\nand $02"), fill_nop=True) + + # Patch kiki not to react to bowwow, as bowwow is not with link at this map + rom.patch(0x07, 0x18A8, ASM("ld a, [$DB56]\ncp $01"), ASM("ld a, $00\ncp $01"), fill_nop=True) + + # Patch the color dungeon entrance not to check for bowwow + rom.patch(0x02, 0x340D, ASM("ld hl, $DB56\nor [hl]"), "", fill_nop=True) + + # Patch richard to ignore bowwow + rom.patch(0x06, 0x006C, ASM("ld a, [$DB56]"), ASM("xor a"), fill_nop=True) + + # Patch to modify how bowwow eats enemies, normally it just unloads them, but we call our handler in bank 3E + rom.patch(0x05, 0x03A0, 0x03A8, ASM(""" + push bc + ld b, d + ld c, e + ld a, $08 + rst 8 + pop bc + ret + """), fill_nop=True) + rom.patch(0x05, 0x0387, ASM("ld a, $03\nldh [$F2], a"), "", fill_nop=True) # remove the default chomp sfx + + # Various enemies + rom.banks[0x14][0x1218 + 0xC5] = 0x01 # Urchin + rom.banks[0x14][0x1218 + 0x93] = 0x01 # MadBomber + rom.banks[0x14][0x1218 + 0x51] = 0x01 # Swinging ball&chain golden leaf enemy + rom.banks[0x14][0x1218 + 0xF2] = 0x01 # Color dungeon flying hopper + rom.banks[0x14][0x1218 + 0xF3] = 0x01 # Color dungeon hopper + rom.banks[0x14][0x1218 + 0xE9] = 0x01 # Color dungeon shell + rom.banks[0x14][0x1218 + 0xEA] = 0x01 # Color dungeon shell + rom.banks[0x14][0x1218 + 0xEB] = 0x01 # Color dungeon shell + rom.banks[0x14][0x1218 + 0xEC] = 0x01 # Color dungeon thing + rom.banks[0x14][0x1218 + 0xED] = 0x01 # Color dungeon thing + rom.banks[0x14][0x1218 + 0xEE] = 0x01 # Color dungeon thing + rom.banks[0x14][0x1218 + 0x87] = 0x01 # Lanmola (for D4 key) + rom.banks[0x14][0x1218 + 0x88] = 0x01 # Armos knight (for D6 key) + rom.banks[0x14][0x1218 + 0x16] = 0x01 # Spark + rom.banks[0x14][0x1218 + 0x17] = 0x01 # Spark + rom.banks[0x14][0x1218 + 0x2C] = 0x01 # Spiked beetle + rom.banks[0x14][0x1218 + 0x90] = 0x01 # Three of a kind (screw these guys) + rom.banks[0x14][0x1218 + 0x18] = 0x01 # Pols voice + rom.banks[0x14][0x1218 + 0x50] = 0x01 # Boo buddy + rom.banks[0x14][0x1218 + 0xA2] = 0x01 # Pirana plant + rom.banks[0x14][0x1218 + 0x52] = 0x01 # Tractor device + rom.banks[0x14][0x1218 + 0x53] = 0x01 # Tractor device (D3) + rom.banks[0x14][0x1218 + 0x55] = 0x01 # Bounding bombite + rom.banks[0x14][0x1218 + 0x56] = 0x01 # Timer bombite + rom.banks[0x14][0x1218 + 0x57] = 0x01 # Pairod + rom.banks[0x14][0x1218 + 0x15] = 0x01 # Antifairy + rom.banks[0x14][0x1218 + 0xA0] = 0x01 # Peahat + rom.banks[0x14][0x1218 + 0x9C] = 0x01 # Star + rom.banks[0x14][0x1218 + 0xA1] = 0x01 # Snake + rom.banks[0x14][0x1218 + 0xBD] = 0x01 # Vire + rom.banks[0x14][0x1218 + 0xE4] = 0x01 # Moblin boss + + # Bosses + rom.banks[0x14][0x1218 + 0x59] = 0x01 # Moldorm + rom.banks[0x14][0x1218 + 0x5C] = 0x01 # Genie + rom.banks[0x14][0x1218 + 0x5B] = 0x01 # Slime Eye + rom.patch(0x04, 0x0AC4, ASM("ld [hl], $28"), ASM("ld [hl], $FF")) # give more time before slimeeye unsplits + rom.patch(0x04, 0x0B05, ASM("ld [hl], $50"), ASM("ld [hl], $FF")) # give more time before slimeeye unsplits + rom.banks[0x14][0x1218 + 0x65] = 0x01 # Angler fish + rom.banks[0x14][0x1218 + 0x5D] = 0x01 # Slime eel + rom.banks[0x14][0x1218 + 0x5A] = 0x01 # Facade + rom.banks[0x14][0x1218 + 0x63] = 0x01 # Eagle + rom.banks[0x14][0x1218 + 0x62] = 0x01 # Hot head + rom.banks[0x14][0x1218 + 0xF9] = 0x01 # Hardhit beetle + rom.banks[0x14][0x1218 + 0xE6] = 0x01 # Nightmare + + # Minibosses + rom.banks[0x14][0x1218 + 0x81] = 0x01 # Rolling bones + rom.banks[0x14][0x1218 + 0x89] = 0x01 # Hinox + rom.banks[0x14][0x1218 + 0x8E] = 0x01 # Cue ball + rom.banks[0x14][0x1218 + 0x5E] = 0x01 # Gnoma + rom.banks[0x14][0x1218 + 0x5F] = 0x01 # Master stalfos + rom.banks[0x14][0x1218 + 0x92] = 0x01 # Smasher + rom.banks[0x14][0x1218 + 0xBC] = 0x01 # Grim creeper + rom.banks[0x14][0x1218 + 0xBE] = 0x01 # Blaino + rom.banks[0x14][0x1218 + 0xF8] = 0x01 # Giant buzz blob + rom.banks[0x14][0x1218 + 0xF4] = 0x01 # Avalaunch + + # NPCs + rom.banks[0x14][0x1218 + 0x6F] = 0x01 # Dog + rom.banks[0x14][0x1218 + 0x6E] = 0x01 # Butterfly + rom.banks[0x14][0x1218 + 0x6C] = 0x01 # Cucco + rom.banks[0x14][0x1218 + 0x70] = 0x01 # Kid + rom.banks[0x14][0x1218 + 0x71] = 0x01 # Kid + rom.banks[0x14][0x1218 + 0x72] = 0x01 # Kid + rom.banks[0x14][0x1218 + 0x73] = 0x01 # Kid + rom.banks[0x14][0x1218 + 0xD0] = 0x01 # Animal + rom.banks[0x14][0x1218 + 0xD1] = 0x01 # Animal + rom.banks[0x14][0x1218 + 0xD2] = 0x01 # Animal + rom.banks[0x14][0x1218 + 0xD3] = 0x01 # Animal + + +def bowwowMapPatches(rom): + # Remove all the cystal things that can only be destroyed with a sword. + for n in range(0x100, 0x2FF): + re = RoomEditor(rom, n) + re.objects = list(filter(lambda obj: obj.type_id != 0xDD, re.objects)) + re.store(rom) diff --git a/worlds/ladx/LADXR/patches/chest.py b/worlds/ladx/LADXR/patches/chest.py new file mode 100644 index 0000000000..c092fc75f5 --- /dev/null +++ b/worlds/ladx/LADXR/patches/chest.py @@ -0,0 +1,59 @@ +from ..assembler import ASM +from ..utils import formatText +from ..locations.constants import CHEST_ITEMS + + +def fixChests(rom): + # Patch the chest code, so it can give a lvl1 sword. + # Normally, there is some code related to the owl event when getting the tail key, + # as we patched out the owl. We use it to jump to our custom code in bank $3E to handle getting the item + rom.patch(0x03, 0x109C, ASM(""" + cp $11 ; if not tail key, skip + jr nz, end + push af + ld a, [$C501] + ld e, a + ld hl, $C2F0 + add hl, de + ld [hl], $38 + pop af + end: + ld e, a + cp $21 ; if is message chest or higher number, next instruction is to skip giving things. + """), ASM(""" + ld a, $06 ; GiveItemMultiworld + rst 8 + + and a ; clear the carry flag to always skip giving stuff. + """), fill_nop=True) + + # Instead of the normal logic to on which sprite data to show, we jump to our custom code in bank 3E. + rom.patch(0x07, 0x3C36, None, ASM(""" + ld a, $01 + rst 8 + jp $7C5E + """), fill_nop=True) + + # Instead of the normal logic of showing the proper dialog, we jump to our custom code in bank 3E. + rom.patch(0x07, 0x3C9C, None, ASM(""" + ld a, $0A ; showItemMessageMultiworld + rst 8 + jp $7CE9 + """)) + + # Sound to play is normally loaded from a table, which is no longer big enough. So always use the same sound. + rom.patch(0x07, 0x3C81, ASM(""" + add hl, de + ld a, [hl] + """), ASM("ld a, $01"), fill_nop=True) + + # Always spawn seashells even if you have the L2 sword + rom.patch(0x14, 0x192F, ASM("ld a, $1C"), ASM("ld a, $20")) + + rom.texts[0x9A] = formatText("You found 10 {BOMB}!") + + +def setMultiChest(rom, option): + room = 0x2F2 + addr = room + 0x560 + rom.banks[0x14][addr] = CHEST_ITEMS[option] diff --git a/worlds/ladx/LADXR/patches/core.py b/worlds/ladx/LADXR/patches/core.py new file mode 100644 index 0000000000..a202e661f9 --- /dev/null +++ b/worlds/ladx/LADXR/patches/core.py @@ -0,0 +1,539 @@ +from ..assembler import ASM +from ..entranceInfo import ENTRANCE_INFO +from ..roomEditor import RoomEditor, ObjectWarp, ObjectHorizontal +from ..backgroundEditor import BackgroundEditor +from .. import utils + + +def bugfixWrittingWrongRoomStatus(rom): + # The normal rom contains a pretty nasty bug where door closing triggers in D7/D8 can effect doors in + # dungeons D1-D6. This fix should prevent this. + rom.patch(0x02, 0x1D21, 0x1D3C, ASM("call $5B9F"), fill_nop=True) + +def fixEggDeathClearingItems(rom): + rom.patch(0x01, 0x1E79, ASM("cp $0A"), ASM("cp $08")) + +def fixWrongWarp(rom): + rom.patch(0x00, 0x18CE, ASM("cp $04"), ASM("cp $03")) + re = RoomEditor(rom, 0x2b) + for x in range(10): + re.removeObject(x, 7) + re.objects.append(ObjectHorizontal(0, 7, 0x2C, 10)) + while len(re.getWarps()) < 4: + re.objects.append(ObjectWarp(1, 3, 0x7a, 80, 124)) + re.store(rom) + +def bugfixBossroomTopPush(rom): + rom.patch(0x14, 0x14D9, ASM(""" + ldh a, [$99] + dec a + ldh [$99], a + """), ASM(""" + jp $7F80 + """), fill_nop=True) + rom.patch(0x14, 0x3F80, "00" * 0x80, ASM(""" + ldh a, [$99] + cp $50 + jr nc, up +down: + inc a + ldh [$99], a + jp $54DE +up: + dec a + ldh [$99], a + jp $54DE + """), fill_nop=True) + +def bugfixPowderBagSprite(rom): + rom.patch(0x03, 0x2055, "8E16", "0E1E") + +def easyColorDungeonAccess(rom): + re = RoomEditor(rom, 0x312) + re.entities = [(3, 1, 246), (6, 1, 247)] + re.store(rom) + +def removeGhost(rom): + ## Ghost patch + # Do not have the ghost follow you after dungeon 4 + rom.patch(0x03, 0x1E1B, ASM("LD [$DB79], A"), "", fill_nop=True) + +def alwaysAllowSecretBook(rom): + rom.patch(0x15, 0x3F23, ASM("ld a, [$DB0E]\ncp $0E"), ASM("xor a\ncp $00"), fill_nop=True) + rom.patch(0x15, 0x3F2A, 0x3F30, "", fill_nop=True) + +def cleanup(rom): + # Remove unused rooms to make some space in the rom + re = RoomEditor(rom, 0x2C4) + re.objects = [] + re.entities = [] + re.store(rom, 0x2C4) + re.store(rom, 0x2D4) + re.store(rom, 0x277) + re.store(rom, 0x278) + re.store(rom, 0x279) + re.store(rom, 0x1ED) + re.store(rom, 0x1FC) # Beta room + + rom.texts[0x02B] = b'' # unused text + + +def disablePhotoPrint(rom): + rom.patch(0x28, 0x07CC, ASM("ldh [$01], a\nldh [$02], a"), "", fill_nop=True) # do not reset the serial link + rom.patch(0x28, 0x0483, ASM("ld a, $13"), ASM("jr $EA", 0x4483)) # Do not print on A press, but jump to cancel + rom.patch(0x28, 0x0492, ASM("ld hl, $4439"), ASM("ret"), fill_nop=True) # Do not show the print/cancel overlay + +def fixMarinFollower(rom): + # Allow opening of D0 with marin + rom.patch(0x02, 0x3402, ASM("ld a, [$DB73]"), ASM("xor a"), fill_nop=True) + # Instead of uselessly checking for sidescroller rooms for follower spawns, check for color dungeon instead + rom.patch(0x01, 0x1FCB, 0x1FD3, ASM("cp $FF\nret z"), fill_nop=True) + # Do not load marin graphics in color dungeon + rom.patch(0x00, 0x2EA6, 0x2EB0, ASM("cp $FF\njp $2ED3"), fill_nop=True) + # Fix marin on taltal bridge causing a lockup if you have marin with you + # This changes the location where the index to the marin entity is stored from it's normal location + # To the memory normal reserved for progress on the egg maze (which is reset to 0 on a warp) + rom.patch(0x18, 0x1EF7, ASM("ld [$C50F], a"), ASM("ld [$C5AA], a")) + rom.patch(0x18, 0x2126, ASM("ld a, [$C50F]"), ASM("ld a, [$C5AA]")) + rom.patch(0x18, 0x2139, ASM("ld a, [$C50F]"), ASM("ld a, [$C5AA]")) + rom.patch(0x18, 0x214F, ASM("ld a, [$C50F]"), ASM("ld a, [$C5AA]")) + rom.patch(0x18, 0x2166, ASM("ld a, [$C50F]"), ASM("ld a, [$C5AA]")) + +def quickswap(rom, button): + rom.patch(0x00, 0x1094, ASM("jr c, $49"), ASM("jr nz, $49")) # prevent agressive key repeat + rom.patch(0x00, 0x10BC, # Patch the open minimap code to swap the your items instead + ASM("xor a\nld [$C16B], a\nld [$C16C], a\nld [$DB96], a\nld a, $07\nld [$DB95], a"), ASM(""" + ld a, [$DB%02X] + ld e, a + ld a, [$DB%02X] + ld [$DB%02X], a + ld a, e + ld [$DB%02X], a + ret + """ % (button, button + 2, button, button + 2))) + +def injectMainLoop(rom): + rom.patch(0x00, 0x0346, ASM(""" + ldh a, [$FE] + and a + jr z, $08 + """), ASM(""" + ; Call the mainloop handler + xor a + rst 8 + """), fill_nop=True) + +def warpHome(rom): + # Patch the S&Q menu to allow 3 options + rom.patch(0x01, 0x012A, 0x0150, ASM(""" + ld hl, $C13F + call $6BA8 ; make sound on keypress + ldh a, [$CC] ; load joystick status + and $04 ; if up + jr z, noUp + dec [hl] +noUp: + ldh a, [$CC] ; load joystick status + and $08 ; if down + jr z, noDown + inc [hl] +noDown: + + ld a, [hl] + cp $ff + jr nz, noWrapUp + ld a, $02 +noWrapUp: + cp $03 + jr nz, noWrapDown + xor a +noWrapDown: + ld [hl], a + jp $7E02 + """), fill_nop=True) + rom.patch(0x01, 0x3E02, 0x3E20, ASM(""" + swap a + add a, $48 + ld hl, $C018 + ldi [hl], a + ld a, $24 + ldi [hl], a + ld a, $BE + ldi [hl], a + ld [hl], $00 + ret + """), fill_nop=True) + + rom.patch(0x01, 0x00B7, ASM(""" + ld a, [$C13F] + cp $01 + jr z, $3B + """), ASM(""" + ld a, [$C13F] + jp $7E20 + """), fill_nop=True) + + re = RoomEditor(rom, 0x2a3) + warp = re.getWarps()[0] + + type = 0x00 + map = 0x00 + room = warp.room + x = warp.target_x + y = warp.target_y + + one_way = [ + 'd0', + 'd1', + 'd3', + 'd4', + 'd6', + 'd8', + 'animal_cave', + 'right_fairy', + 'rooster_grave', + 'prairie_left_cave2', + 'prairie_left_fairy', + 'armos_fairy', + 'boomerang_cave', + 'madbatter_taltal', + 'forest_madbatter', + ] + + one_way = {ENTRANCE_INFO[x].room for x in one_way} + + if warp.room in one_way: + # we're starting at a one way exit room + # warp indoors to avoid soft locks + type = 0x01 + map = 0x10 + room = 0xa3 + x = 0x50 + y = 0x7f + + rom.patch(0x01, 0x3E20, 0x3E6B, ASM(""" + ; First, handle save & quit + cp $01 + jp z, $40F9 + and a + jp z, $40BE ; return to normal "return to game" handling + + ld a, [$C509] ; Check if we have an item in the shop + and a + jp nz, $40BE ; return to normal "return to game" handling + + ld a, $0B + ld [$DB95], a + call $0C7D + + ; Replace warp0 tile data, and put link on that tile. + ld a, $%02x ; Type + ld [$D401], a + ld a, $%02x ; Map + ld [$D402], a + ld a, $%02x ; Room + ld [$D403], a + ld a, $%02x ; X + ld [$D404], a + ld a, $%02x ; Y + 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 + + ld a, $07 + ld [$DB96], a + ret + jp $40BE ; return to normal "return to game" handling + """ % (type, map, room, x, y)), fill_nop=True) + + # Patch the RAM clear not to delete our custom dialog when we screen transition + rom.patch(0x01, 0x042C, "C629", "6B7E") + rom.patch(0x01, 0x3E6B, 0x3FFF, ASM(""" + ld bc, $A0 + call $29DC + ld bc, $1200 + ld hl, $C100 + call $29DF + ret + """), fill_nop=True) + # Patch the S&Q screen to have 3 options. + be = BackgroundEditor(rom, 0x0D) + for n in range(2, 18): + be.tiles[0x99C0 + n] = be.tiles[0x9980 + n] + be.tiles[0x99A0 + n] = be.tiles[0x9960 + n] + be.tiles[0x9980 + n] = be.tiles[0x9940 + n] + be.tiles[0x9960 + n] = be.tiles[0x98e0 + n] + be.tiles[0x9960 + 10] = 0xCE + be.tiles[0x9960 + 11] = 0xCF + be.tiles[0x9960 + 12] = 0xC4 + be.tiles[0x9960 + 13] = 0x7F + be.tiles[0x9960 + 14] = 0x7F + be.store(rom) + + sprite_data = [ + 0b00000000, + 0b01000100, + 0b01000101, + 0b01000101, + 0b01111101, + 0b01000101, + 0b01000101, + 0b01000100, + + 0b00000000, + 0b11100100, + 0b00010110, + 0b00010101, + 0b00010100, + 0b00010100, + 0b00010100, + 0b11100100, + ] + for n in range(32): + rom.banks[0x0F][0x08E0 + n] = sprite_data[n // 2] + + +def addFrameCounter(rom, check_count): + # Patch marin giving the start the game to jump to a custom handler + rom.patch(0x05, 0x1299, ASM("ld a, $01\ncall $2385"), ASM("push hl\nld a, $0D\nrst 8\npop hl"), fill_nop=True) + + # Add code that needs to be called every frame to tick our ingame time counter. + rom.patch(0x00, 0x0091, "00" * (0x100 - 0x91), ASM(""" + ld a, [$DB95] ;Get the gameplay type + dec a ; and if it was 1 + ret z ; we are at the credits and the counter should stop. + + ; Check if the timer expired + ld hl, $FF0F + bit 2, [hl] + ret z + res 2, [hl] + + ; Increase the "subsecond" counter, and continue if it "overflows" + call $27D0 ; Enable SRAM + ld hl, $B000 + ld a, [hl] + inc a + cp $20 + ld [hl], a + ret nz + xor a + ldi [hl], a + + ; Increase the seconds counter/minutes/hours counter +increaseSecMinHours: + ld a, [hl] + inc a + daa + ld [hl], a + cp $60 + ret nz + xor a + ldi [hl], a + jr increaseSecMinHours + """), fill_nop=True) + # Replace a cgb check with the call to our counter code. + rom.patch(0x00, 0x0367, ASM("ld a, $0C\ncall $0B0B"), ASM("call $0091\nld a, $2C")) + + # Do not switch to 8x8 sprite mode + rom.patch(0x17, 0x2E9E, ASM("res 2, [hl]"), "", fill_nop=True) + # We need to completely reorder link sitting on the raft to work with 16x8 sprites. + sprites = rom.banks[0x38][0x1600:0x1800] + sprites[0x1F0:0x200] = b'\x00' * 16 + for index, position in enumerate( + (0, 0x1F, + 1, 0x1F, 2, 0x1F, + 7, 8, + 3, 9, 4, 10, 5, 11, 6, 12, + 3, 13, 4, 14, 5, 15, 6, 16, + 3, 17, 4, 18, 5, 19, 6, 20, + )): + rom.banks[0x38][0x1600+index*0x10:0x1610+index*0x10] = sprites[position*0x10:0x10+position*0x10] + rom.patch(0x27, 0x376E, 0x3776, "00046601", fill_nop=True) + rom.patch(0x27, 0x384E, ASM("ld c, $08"), ASM("ld c, $04")) + rom.patch(0x27, 0x3776, 0x3826, + "FA046002" + "0208640402006204" + "0A106E030A086C030A006A030AF86803" + + "FA046002" + "0208640402006204" + "0A1076030A0874030A0072030AF87003" + + "FA046002" + "0208640402006204" + "0A107E030A087C030A007A030AF87803" + , fill_nop=True) + rom.patch(0x27, 0x382E, ASM("ld a, $6C"), ASM("ld a, $80")) # OAM start position + rom.patch(0x27, 0x384E, ASM("ld c, $08"), ASM("ld c, $04")) # Amount of overlay OAM data + rom.patch(0x27, 0x3826, 0x382E, ASM("dw $7776, $7792, $77AE, $7792")) # pointers to animation + rom.patch(0x27, 0x3846, ASM("ld c, $2C"), ASM("ld c, $1C")) # Amount of OAM data + + # TODO: fix flying windfish + # Upper line of credits roll into "TIME" + rom.patch(0x17, 0x069D, 0x0713, ASM(""" + ld hl, OAMData + ld de, $C000 ; OAM Buffer + ld bc, $0048 + call $2914 + ret +OAMData: + db $20, $18, $34, $00 ;T + db $20, $20, $20, $00 ;I + db $20, $28, $28, $00 ;M + db $20, $30, $18, $00 ;E + + db $20, $70, $16, $00 ;D + db $20, $78, $18, $00 ;E + db $20, $80, $10, $00 ;A + db $20, $88, $34, $00 ;T + db $20, $90, $1E, $00 ;H + + db $50, $18, $14, $00 ;C + db $50, $20, $1E, $00 ;H + db $50, $28, $18, $00 ;E + db $50, $30, $14, $00 ;C + db $50, $38, $24, $00 ;K + db $50, $40, $32, $00 ;S + + db $68, $38, $%02x, $00 ;0 + db $68, $40, $%02x, $00 ;0 + db $68, $48, $%02x, $00 ;0 + + """ % ((((check_count // 100) % 10) * 2) | 0x40, (((check_count // 10) % 10) * 2) | 0x40, ((check_count % 10) * 2) | 0x40), 0x469D), fill_nop=True) + # Lower line of credits roll into XX XX XX + rom.patch(0x17, 0x0784, 0x082D, ASM(""" + ld hl, OAMData + ld de, $C048 ; OAM Buffer + ld bc, $0038 + call $2914 + + call $27D0 ; Enable SRAM + ld hl, $C04A + ld a, [$B003] ; hours + call updateOAM + ld a, [$B002] ; minutes + call updateOAM + ld a, [$B001] ; seconds + call updateOAM + + ld a, [$DB58] ; death count high + call updateOAM + ld a, [$DB57] ; death count low + call updateOAM + + ld a, [$B011] ; check count high + call updateOAM + ld a, [$B010] ; check count low + call updateOAM + ret + +updateOAM: + ld de, $0004 + ld b, a + swap a + and $0F + add a, a + or $40 + ld [hl], a + add hl, de + + ld a, b + and $0F + add a, a + or $40 + ld [hl], a + add hl, de + ret +OAMData: + db $38, $18, $40, $00 ;0 (10 hours) + db $38, $20, $40, $00 ;0 (1 hours) + db $38, $30, $40, $00 ;0 (10 minutes) + db $38, $38, $40, $00 ;0 (1 minutes) + db $38, $48, $40, $00 ;0 (10 seconds) + db $38, $50, $40, $00 ;0 (1 seconds) + + db $00, $00, $40, $00 ;0 (1000 death) + db $38, $80, $40, $00 ;0 (100 death) + + db $38, $88, $40, $00 ;0 (10 death) + db $38, $90, $40, $00 ;0 (1 death) + + ; checks + db $00, $00, $40, $00 ;0 + db $68, $18, $40, $00 ;0 + db $68, $20, $40, $00 ;0 + db $68, $28, $40, $00 ;0 + + """, 0x4784), fill_nop=True) + + # Grab the "mostly" complete A-Z font + sprites = rom.banks[0x38][0x1100:0x1400] + for index, position in enumerate(( + 0x10, 0x20, # A + 0x11, 0x21, # B + 0x12, 0x12 | 0x100, # C + 0x13, 0x23, # D + 0x14, 0x24, # E + 0x14, 0x25, # F + 0x12, 0x22, # G + 0x20 | 0x100, 0x26, # H + 0x17, 0x17 | 0x100, # I + 0x28, 0x28, # J + 0x19, 0x29, # K + 0x06, 0x07, # L + 0x1A, 0x2A, # M + 0x1B, 0x2B, # N + 0x00, 0x00, # O? + 0x00, 0x00, # P? + #0x00, 0x00, # Q? + 0x11, 0x18, # R + 0x1C, 0x2C, # S + 0x1D, 0x2D, # T + 0x26, 0x10, # U + 0x00, 0x00, # V? + 0x1E, 0x2E, # W + #0x00, 0x00, # X? + #0x00, 0x00, # Y? + 0x27, 0x27, # Z + )): + sprite = sprites[(position&0xFF)*0x10:0x10+(position&0xFF)*0x10] + if position & 0x100: + for n in range(4): + sprite[n * 2], sprite[14 - n * 2] = sprite[14 - n * 2], sprite[n * 2] + sprite[n * 2 + 1], sprite[15 - n * 2] = sprite[15 - n * 2], sprite[n * 2 + 1] + rom.banks[0x38][0x1100+index*0x10:0x1110+index*0x10] = sprite + + + # Number graphics change for the end + tile_graphics = """ +........ ........ ........ ........ ........ ........ ........ ........ ........ ........ +.111111. ..1111.. .111111. .111111. ..11111. 11111111 .111111. 11111111 .111111. .111111. +11333311 .11331.. 11333311 11333311 .113331. 13333331 11333311 13333331 11333311 11333311 +13311331 113331.. 13311331 13311331 1133331. 13311111 13311331 11111331 13311331 13311331 +13311331 133331.. 13311331 11111331 1331331. 1331.... 13311331 ...11331 13311331 13311331 +13311331 133331.. 11111331 ....1331 1331331. 1331.... 13311111 ...13311 13311331 13311331 +13311331 111331.. ...13311 .1111331 1331331. 1331111. 1331.... ..11331. 13311331 13311331 +13311331 ..1331.. ..11331. .1333331 13313311 13333311 1331111. ..13311. 11333311 11333331 +13311331 ..1331.. ..13311. .1111331 13333331 13311331 13333311 .11331.. 13311331 .1111331 +13311331 ..1331.. .11331.. ....1331 11113311 11111331 13311331 .13311.. 13311331 ....1331 +13311331 ..1331.. .13311.. ....1331 ...1331. ....1331 13311331 11331... 13311331 ....1331 +13311331 ..1331.. 11331... 11111331 ...1331. 11111331 13311331 13311... 13311331 11111331 +13311331 ..1331.. 13311111 13311331 ...1331. 13311331 13311331 1331.... 13311331 13311331 +11333311 ..1331.. 13333331 11333311 ...1331. 11333311 11333311 1331.... 11333311 11333311 +.111111. ..1111.. 11111111 .111111. ...1111. .111111. .111111. 1111.... .111111. .111111. +........ ........ ........ ........ ........ ........ ........ ........ ........ ........ +""".strip() + for n in range(10): + gfx_high = "\n".join([line.split(" ")[n] for line in tile_graphics.split("\n")[:8]]) + gfx_low = "\n".join([line.split(" ")[n] for line in tile_graphics.split("\n")[8:]]) + rom.banks[0x38][0x1400+n*0x20:0x1410+n*0x20] = utils.createTileData(gfx_high) + rom.banks[0x38][0x1410+n*0x20:0x1420+n*0x20] = utils.createTileData(gfx_low) diff --git a/worlds/ladx/LADXR/patches/desert.py b/worlds/ladx/LADXR/patches/desert.py new file mode 100644 index 0000000000..e3f008661c --- /dev/null +++ b/worlds/ladx/LADXR/patches/desert.py @@ -0,0 +1,7 @@ +from ..roomEditor import RoomEditor + + +def desertAccess(rom): + re = RoomEditor(rom, 0x0FD) + re.entities = [(6, 2, 0xC4)] + re.store(rom) diff --git a/worlds/ladx/LADXR/patches/droppedKey.py b/worlds/ladx/LADXR/patches/droppedKey.py new file mode 100644 index 0000000000..d24b8b76c7 --- /dev/null +++ b/worlds/ladx/LADXR/patches/droppedKey.py @@ -0,0 +1,134 @@ +from ..assembler import ASM + + +def fixDroppedKey(rom): + # Patch the rendering code to use the dropped key rendering code. + rom.patch(0x03, 0x1C99, None, ASM(""" + ld a, $04 + rst 8 + jp $5CA6 + """)) + + # Patch the key pickup code to use the chest pickup code. + rom.patch(0x03, 0x248F, None, ASM(""" + ldh a, [$F6] ; load room nr + cp $7C ; L4 Side-view room where the key drops + jr nz, notSpecialSideView + + ld hl, $D969 ; status of the room above the side-view where the key drops in dungeon 4 + set 4, [hl] +notSpecialSideView: + call $512A ; mark room as done + + ; Handle item effect + ld a, $06 ; giveItemMultiworld + rst 8 + + ldh a, [$F1] ; Load active sprite variant to see if this is just a normal small key + cp $1A + jr z, isAKey + + ;Show message (if not a key) + ld a, $0A ; showMessageMultiworld + rst 8 +isAKey: + ret + """)) + rom.patch(0x03, 0x24B7, "3E", "3E") # sanity check + + # Mark all dropped keys as keys by default. + for n in range(0x316): + rom.banks[0x3E][0x3800 + n] = 0x1A + # Set the proper angler key by default + rom.banks[0x3E][0x3800 + 0x0CE] = 0x12 + rom.banks[0x3E][0x3800 + 0x1F8] = 0x12 + # Set the proper bird key by default + rom.banks[0x3E][0x3800 + 0x27A] = 0x14 + # Set the proper face key by default + rom.banks[0x3E][0x3800 + 0x27F] = 0x13 + + # Set the proper hookshot key by default + rom.banks[0x3E][0x3800 + 0x180] = 0x03 + + # Set the proper golden leaves + rom.banks[0x3E][0x3800 + 0x058] = 0x15 + rom.banks[0x3E][0x3800 + 0x05a] = 0x15 + rom.banks[0x3E][0x3800 + 0x2d2] = 0x15 + rom.banks[0x3E][0x3800 + 0x2c5] = 0x15 + rom.banks[0x3E][0x3800 + 0x2c6] = 0x15 + + # Set the slime key drop. + rom.banks[0x3E][0x3800 + 0x0C6] = 0x0F + + # Set the heart pieces + rom.banks[0x3E][0x3800 + 0x000] = 0x80 + rom.banks[0x3E][0x3800 + 0x2A4] = 0x80 + rom.banks[0x3E][0x3800 + 0x2B1] = 0x80 # fishing game, unused + rom.banks[0x3E][0x3800 + 0x044] = 0x80 + rom.banks[0x3E][0x3800 + 0x2AB] = 0x80 + rom.banks[0x3E][0x3800 + 0x2DF] = 0x80 + rom.banks[0x3E][0x3800 + 0x2E5] = 0x80 + rom.banks[0x3E][0x3800 + 0x078] = 0x80 + rom.banks[0x3E][0x3800 + 0x2E6] = 0x80 + rom.banks[0x3E][0x3800 + 0x1E8] = 0x80 + rom.banks[0x3E][0x3800 + 0x1F2] = 0x80 + rom.banks[0x3E][0x3800 + 0x2BA] = 0x80 + + # Set the seashells + rom.banks[0x3E][0x3800 + 0x0A3] = 0x20 + rom.banks[0x3E][0x3800 + 0x2B2] = 0x20 + rom.banks[0x3E][0x3800 + 0x0A5] = 0x20 + rom.banks[0x3E][0x3800 + 0x0A6] = 0x20 + rom.banks[0x3E][0x3800 + 0x08B] = 0x20 + rom.banks[0x3E][0x3800 + 0x074] = 0x20 + rom.banks[0x3E][0x3800 + 0x0A4] = 0x20 + rom.banks[0x3E][0x3800 + 0x0D2] = 0x20 + rom.banks[0x3E][0x3800 + 0x0E9] = 0x20 + rom.banks[0x3E][0x3800 + 0x0B9] = 0x20 + rom.banks[0x3E][0x3800 + 0x0F8] = 0x20 + rom.banks[0x3E][0x3800 + 0x0A8] = 0x20 + rom.banks[0x3E][0x3800 + 0x0FF] = 0x20 + rom.banks[0x3E][0x3800 + 0x1E3] = 0x20 + rom.banks[0x3E][0x3800 + 0x0DA] = 0x20 + rom.banks[0x3E][0x3800 + 0x00C] = 0x20 + + # Set heart containers + rom.banks[0x3E][0x3800 + 0x106] = 0x89 + rom.banks[0x3E][0x3800 + 0x12B] = 0x89 + rom.banks[0x3E][0x3800 + 0x15A] = 0x89 + rom.banks[0x3E][0x3800 + 0x1FF] = 0x89 + rom.banks[0x3E][0x3800 + 0x185] = 0x89 + rom.banks[0x3E][0x3800 + 0x1BC] = 0x89 + rom.banks[0x3E][0x3800 + 0x2E8] = 0x89 + rom.banks[0x3E][0x3800 + 0x234] = 0x89 + + # Toadstool + rom.banks[0x3E][0x3800 + 0x050] = 0x50 + # Sword on beach + rom.banks[0x3E][0x3800 + 0x0F2] = 0x0B + # Sword upgrade + rom.banks[0x3E][0x3800 + 0x2E9] = 0x0B + + # Songs + rom.banks[0x3E][0x3800 + 0x092] = 0x8B # song 1 + rom.banks[0x3E][0x3800 + 0x0DC] = 0x8B # song 1 + rom.banks[0x3E][0x3800 + 0x2FD] = 0x8C # song 2 + rom.banks[0x3E][0x3800 + 0x2FB] = 0x8D # song 3 + + # Instruments + rom.banks[0x3E][0x3800 + 0x102] = 0x8E + rom.banks[0x3E][0x3800 + 0x12a] = 0x8F + rom.banks[0x3E][0x3800 + 0x159] = 0x90 + rom.banks[0x3E][0x3800 + 0x162] = 0x91 + rom.banks[0x3E][0x3800 + 0x182] = 0x92 + rom.banks[0x3E][0x3800 + 0x1b5] = 0x93 + rom.banks[0x3E][0x3800 + 0x22c] = 0x94 + rom.banks[0x3E][0x3800 + 0x230] = 0x95 + + # Start item + rom.banks[0x3E][0x3800 + 0x2a3] = 0x01 + + # Master stalfos overkill drops + rom.banks[0x3E][0x3800 + 0x195] = 0x1A + rom.banks[0x3E][0x3800 + 0x192] = 0x1A + rom.banks[0x3E][0x3800 + 0x184] = 0x1A diff --git a/worlds/ladx/LADXR/patches/dungeon.py b/worlds/ladx/LADXR/patches/dungeon.py new file mode 100644 index 0000000000..f99d99fe49 --- /dev/null +++ b/worlds/ladx/LADXR/patches/dungeon.py @@ -0,0 +1,129 @@ +from ..roomEditor import RoomEditor, Object, ObjectHorizontal + + +KEY_DOORS = { + 0xEC: 0xF4, + 0xED: 0xF5, + 0xEE: 0xF6, + 0xEF: 0xF7, + 0xF8: 0xF4, +} + +def removeKeyDoors(rom): + for n in range(0x100, 0x316): + if n == 0x2FF: + continue + update = False + re = RoomEditor(rom, n) + for obj in re.objects: + if obj.type_id in KEY_DOORS: + obj.type_id = KEY_DOORS[obj.type_id] + update = True + if obj.type_id == 0xDE: # Keyblocks + obj.type_id = re.floor_object & 0x0F + update = True + if update: + re.store(rom) + + +def patchNoDungeons(rom): + def setMinimap(dungeon_nr, x, y, room): + for n in range(64): + if rom.banks[0x14][0x0220 + 64 * dungeon_nr + n] == room: + rom.banks[0x14][0x0220 + 64 * dungeon_nr + n] = 0xFF + rom.banks[0x14][0x0220 + 64 * dungeon_nr + x + y * 8] = room + #D1 + setMinimap(0, 3, 6, 0x06) + setMinimap(0, 3, 5, 0x02) + re = RoomEditor(rom, 0x117) + for n in range(1, 7): + re.removeObject(n, 0) + re.removeObject(0, n) + re.removeObject(9, n) + re.objects += [Object(4, 0, 0xf0)] + re.store(rom) + re = RoomEditor(rom, 0x11A) + re.getWarps()[0].room = 0x117 + re.store(rom) + re = RoomEditor(rom, 0x11B) + re.getWarps()[0].room = 0x117 + re.store(rom) + + #D2 + setMinimap(1, 2, 6, 0x2B) + setMinimap(1, 1, 6, 0x2A) + re = RoomEditor(rom, 0x136) + for n in range(1, 7): + re.removeObject(n, 0) + re.objects += [Object(4, 0, 0xf0)] + re.store(rom) + + #D3 + setMinimap(2, 1, 6, 0x5A) + setMinimap(2, 1, 5, 0x59) + re = RoomEditor(rom, 0x152) + for n in range(2, 7): + re.removeObject(9, n) + re.store(rom) + + #D4 + setMinimap(3, 3, 6, 0x66) + setMinimap(3, 3, 5, 0x62) + re = RoomEditor(rom, 0x17A) + for n in range(3, 7): + re.removeObject(n, 0) + re.objects += [Object(4, 0, 0xf0)] + re.store(rom) + + #D5 + setMinimap(4, 7, 6, 0x85) + setMinimap(4, 7, 5, 0x82) + re = RoomEditor(rom, 0x1A1) + for n in range(3, 8): + re.removeObject(n, 0) + re.removeObject(0, n) + for n in range(4, 6): + re.removeObject(n, 1) + re.removeObject(n, 2) + re.objects += [Object(4, 0, 0xf0)] + re.store(rom) + + #D6 + setMinimap(5, 3, 6, 0xBC) + setMinimap(5, 3, 5, 0xB5) + re = RoomEditor(rom, 0x1D4) + for n in range(2, 8): + re.removeObject(0, n) + re.removeObject(9, n) + re.objects += [Object(4, 0, 0xf0)] + re.store(rom) + + #D7 + setMinimap(6, 1, 6, 0x2E) + setMinimap(6, 1, 5, 0x2C) + re = RoomEditor(rom, 0x20E) + for n in range(1, 8): + re.removeObject(0, n) + re.removeObject(9, n) + re.objects += [Object(3, 0, 0x29), ObjectHorizontal(4, 0, 0x0D, 2), Object(6, 0, 0x2A)] + re.store(rom) + re = RoomEditor(rom, 0x22E) + re.objects = [Object(4, 0, 0xf0), Object(3, 7, 0x2B), ObjectHorizontal(4, 7, 0x0D, 2), Object(6, 7, 0x2C), Object(1, 0, 0xA8)] + re.getWarps() + re.floor_object = 13 + re.store(rom) + re = RoomEditor(rom, 0x22C) + re.removeObject(0, 7) + re.removeObject(2, 7) + re.objects.append(ObjectHorizontal(0, 7, 0x03, 3)) + re.store(rom) + + #D8 + setMinimap(7, 3, 6, 0x34) + setMinimap(7, 3, 5, 0x30) + re = RoomEditor(rom, 0x25D) + re.objects += [Object(3, 0, 0x25), Object(4, 0, 0xf0), Object(6, 0, 0x26)] + re.store(rom) + + #D0 + setMinimap(11, 2, 6, 0x00) + setMinimap(11, 3, 6, 0x01) diff --git a/worlds/ladx/LADXR/patches/endscreen.py b/worlds/ladx/LADXR/patches/endscreen.py new file mode 100644 index 0000000000..843120f1c0 --- /dev/null +++ b/worlds/ladx/LADXR/patches/endscreen.py @@ -0,0 +1,139 @@ +from ..assembler import ASM +import os + + +def updateEndScreen(rom): + # Call our custom data loader in bank 3F + rom.patch(0x00, 0x391D, ASM(""" + ld a, $20 + ld [$2100], a + jp $7de6 + """), ASM(""" + ld a, $3F + ld [$2100], a + jp $4200 + """)) + rom.patch(0x17, 0x2FCE, "B170", "D070") # Ignore the final tile data load + + rom.patch(0x3F, 0x0200, None, ASM(""" + ; Disable LCD + xor a + ldh [$40], a + + ld hl, $8000 + ld de, $5000 +copyLoop: + ld a, [de] + inc de + ldi [hl], a + bit 4, h + jr z, copyLoop + + ld a, $01 + ldh [$4F], a + + ld hl, $8000 + ld de, $6000 +copyLoop2: + ld a, [de] + inc de + ldi [hl], a + bit 4, h + jr z, copyLoop2 + + ld hl, $9800 + ld de, $0190 +clearLoop1: + xor a + ldi [hl], a + dec de + ld a, d + or e + jr nz, clearLoop1 + + ld de, $0190 +clearLoop2: + ld a, $08 + ldi [hl], a + dec de + ld a, d + or e + jr nz, clearLoop2 + + xor a + ldh [$4F], a + + + ld hl, $9800 + ld de, $000C + xor a +loadLoop1: + ldi [hl], a + ld b, a + ld a, l + and $1F + cp $14 + jr c, .noLineSkip + add hl, de +.noLineSkip: + ld a, b + inc a + jr nz, loadLoop1 + +loadLoop2: + ldi [hl], a + ld b, a + ld a, l + and $1F + cp $14 + jr c, .noLineSkip + add hl, de +.noLineSkip: + ld a, b + inc a + jr nz, loadLoop2 + + ; Load palette + ld hl, $DC10 + ld a, $00 + ldi [hl], a + ld a, $00 + ldi [hl], a + + ld a, $ad + ldi [hl], a + ld a, $35 + ldi [hl], a + + ld a, $94 + ldi [hl], a + ld a, $52 + ldi [hl], a + + ld a, $FF + ldi [hl], a + ld a, $7F + ldi [hl], a + + ld a, $00 + ld [$DDD3], a + ld a, $04 + ld [$DDD4], a + ld a, $81 + ld [$DDD1], a + + ; Enable LCD + ld a, $91 + ldh [$40], a + ld [$d6fd], a + + xor a + ldh [$96], a + ldh [$97], a + ret + """)) + + addr = 0x1000 + for c in open(os.path.join(os.path.dirname(__file__), "nyan.bin"), "rb").read(): + rom.banks[0x3F][addr] = c + addr += 1 diff --git a/worlds/ladx/LADXR/patches/enemies.py b/worlds/ladx/LADXR/patches/enemies.py new file mode 100644 index 0000000000..f5e1df1313 --- /dev/null +++ b/worlds/ladx/LADXR/patches/enemies.py @@ -0,0 +1,462 @@ +from ..roomEditor import RoomEditor, Object, ObjectWarp, ObjectHorizontal +from ..assembler import ASM +from ..locations import constants +from typing import List + + +# Room containing the boss +BOSS_ROOMS = [ + 0x106, + 0x12b, + 0x15a, + 0x166, + 0x185, + 0x1bc, + 0x223, # Note: unused room normally + 0x234, + 0x300, +] +BOSS_ENTITIES = [ + (3, 2, 0x59), + (4, 2, 0x5C), + (4, 3, 0x5B), + None, + (4, 3, 0x5D), + (4, 3, 0x5A), + None, + (4, 3, 0x62), + (5, 2, 0xF9), +] +MINIBOSS_ENTITIES = { + "ROLLING_BONES": [(8, 3, 0x81), (6, 3, 0x82)], + "HINOX": [(5, 2, 0x89)], + "DODONGO": [(3, 2, 0x60), (5, 2, 0x60)], + "CUE_BALL": [(1, 1, 0x8e)], + "GHOMA": [(2, 1, 0x5e), (2, 4, 0x5e)], + "SMASHER": [(5, 2, 0x92)], + "GRIM_CREEPER": [(4, 0, 0xbc)], + "BLAINO": [(5, 3, 0xbe)], + "AVALAUNCH": [(5, 1, 0xf4)], + "GIANT_BUZZ_BLOB": [(4, 2, 0xf8)], + "MOBLIN_KING": [(5, 5, 0xe4)], + "ARMOS_KNIGHT": [(4, 3, 0x88)], +} +MINIBOSS_ROOMS = { + 0: 0x111, 1: 0x128, 2: 0x145, 3: 0x164, 4: 0x193, 5: 0x1C5, 6: 0x228, 7: 0x23F, + "c1": 0x30C, "c2": 0x303, + "moblin_cave": 0x2E1, + "armos_temple": 0x27F, +} + + +def fixArmosKnightAsMiniboss(rom): + # Make the armos temple room with armos knight drop a ceiling key on kill. + # This makes the door always open, but that's fine. + rom.patch(0x14, 0x017F, "21", "81") + + # Do not change the drop from Armos knight into a ceiling key. + rom.patch(0x06, 0x12E8, ASM("ld [hl], $30"), "", fill_nop=True) + + +def getBossRoomStatusFlagLocation(dungeon_nr): + if BOSS_ROOMS[dungeon_nr] >= 0x300: + return 0xDDE0 - 0x300 + BOSS_ROOMS[dungeon_nr] + return 0xD800 + BOSS_ROOMS[dungeon_nr] + + +def fixDungeonItem(item_chest_id, dungeon_nr): + if item_chest_id == constants.CHEST_ITEMS[constants.MAP]: + return constants.CHEST_ITEMS["MAP%d" % (dungeon_nr + 1)] + if item_chest_id == constants.CHEST_ITEMS[constants.COMPASS]: + return constants.CHEST_ITEMS["COMPASS%d" % (dungeon_nr + 1)] + if item_chest_id == constants.CHEST_ITEMS[constants.KEY]: + return constants.CHEST_ITEMS["KEY%d" % (dungeon_nr + 1)] + if item_chest_id == constants.CHEST_ITEMS[constants.NIGHTMARE_KEY]: + return constants.CHEST_ITEMS["NIGHTMARE_KEY%d" % (dungeon_nr + 1)] + if item_chest_id == constants.CHEST_ITEMS[constants.STONE_BEAK]: + return constants.CHEST_ITEMS["STONE_BEAK%d" % (dungeon_nr + 1)] + return item_chest_id + + +def getCleanBossRoom(rom, dungeon_nr): + re = RoomEditor(rom, BOSS_ROOMS[dungeon_nr]) + new_objects = [] + for obj in re.objects: + if isinstance(obj, ObjectWarp): + continue + if obj.type_id == 0xBE: # Remove staircases + continue + if obj.type_id == 0x06: # Remove lava + continue + if obj.type_id == 0x1c: # Change D1 pits into normal pits + obj.type_id = 0x01 + if obj.type_id == 0x1e: # Change D1 pits into normal pits + obj.type_id = 0xaf + if obj.type_id == 0x1f: # Change D1 pits into normal pits + obj.type_id = 0xb0 + if obj.type_id == 0xF5: # Change open doors into closing doors. + obj.type_id = 0xF1 + new_objects.append(obj) + + + # Make D4 room a valid fighting room by removing most content. + if dungeon_nr == 3: + new_objects = new_objects[:2] + [Object(1, 1, 0xAC), Object(8, 1, 0xAC), Object(1, 6, 0xAC), Object(8, 6, 0xAC)] + + # D7 has an empty room we use for most bosses, but it needs some adjustments. + if dungeon_nr == 6: + # Move around the unused and instrument room. + rom.banks[0x14][0x03a0 + 6 + 1 * 8] = 0x00 + rom.banks[0x14][0x03a0 + 7 + 2 * 8] = 0x2C + rom.banks[0x14][0x03a0 + 7 + 3 * 8] = 0x23 + rom.banks[0x14][0x03a0 + 6 + 5 * 8] = 0x00 + + rom.banks[0x14][0x0520 + 7 + 2 * 8] = 0x2C + rom.banks[0x14][0x0520 + 7 + 3 * 8] = 0x23 + rom.banks[0x14][0x0520 + 6 + 5 * 8] = 0x00 + + re.floor_object &= 0x0F + new_objects += [ + Object(4, 0, 0xF0), + Object(1, 6, 0xBE), + ObjectWarp(1, dungeon_nr, 0x22E, 24, 16) + ] + + # Set the stairs towards the eagle tower top to our new room. + r = RoomEditor(rom, 0x22E) + r.objects[-1] = ObjectWarp(1, dungeon_nr, re.room, 24, 112) + r.store(rom) + + # Remove the normal door to the instrument room + r = RoomEditor(rom, 0x22e) + r.removeObject(4, 0) + r.store(rom) + rom.banks[0x14][0x22e - 0x100] = 0x00 + + r = RoomEditor(rom, 0x22c) + r.changeObject(0, 7, 0x03) + r.changeObject(2, 7, 0x03) + r.store(rom) + + re.objects = new_objects + re.entities = [] + return re + + +def changeBosses(rom, mapping: List[int]): + # Fix the color dungeon not properly warping to room 0 with the boss. + for addr in range(0x04E0, 0x04E0 + 64): + if rom.banks[0x14][addr] == 0x00 and addr not in {0x04E0 + 1 + 3 * 8, 0x04E0 + 2 + 6 * 8}: + rom.banks[0x14][addr] = 0xFF + # Fix the genie death not really liking pits/water. + rom.patch(0x04, 0x0521, ASM("ld [hl], $81"), ASM("ld [hl], $91")) + + # For the sidescroll bosses, we need to update this check to be the evil eagle dungeon. + # But if evil eagle is not there we still need to remove this check to make angler fish work in D7 + dungeon_nr = mapping.index(6) if 6 in mapping else 0xFE + rom.patch(0x02, 0x1FC8, ASM("cp $06"), ASM("cp $%02x" % (dungeon_nr if dungeon_nr < 8 else 0xff))) + + for dungeon_nr in range(9): + target = mapping[dungeon_nr] + if target == dungeon_nr: + continue + + if target == 3: # D4 fish boss + # If dungeon_nr == 6: use normal eagle door towards fish. + if dungeon_nr == 6: + # Add the staircase to the boss, and fix the warp back. + re = RoomEditor(rom, 0x22E) + for obj in re.objects: + if isinstance(obj, ObjectWarp): + obj.type_id = 2 + obj.map_nr = 3 + obj.room = 0x1EF + obj.target_x = 24 + obj.target_y = 16 + re.store(rom) + re = RoomEditor(rom, 0x1EF) + re.objects[-1] = ObjectWarp(1, dungeon_nr if dungeon_nr < 8 else 0xff, 0x22E, 24, 16) + re.store(rom) + else: + # Set the proper room event flags + rom.banks[0x14][BOSS_ROOMS[dungeon_nr] - 0x100] = 0x2A + + # Add the staircase to the boss, and fix the warp back. + re = getCleanBossRoom(rom, dungeon_nr) + re.objects += [Object(4, 4, 0xBE), ObjectWarp(2, 3, 0x1EF, 24, 16)] + re.store(rom) + re = RoomEditor(rom, 0x1EF) + re.objects[-1] = ObjectWarp(1, dungeon_nr if dungeon_nr < 8 else 0xff, BOSS_ROOMS[dungeon_nr], 72, 80) + re.store(rom) + + # Patch the fish heart container to open up the right room. + if dungeon_nr == 6: + rom.patch(0x03, 0x1A0F, ASM("ld hl, $D966"), ASM("ld hl, $%04x" % (0xD800 + 0x22E))) + else: + rom.patch(0x03, 0x1A0F, ASM("ld hl, $D966"), ASM("ld hl, $%04x" % (getBossRoomStatusFlagLocation(dungeon_nr)))) + + # Patch the proper item towards the D4 boss + rom.banks[0x3E][0x3800 + 0x01ff] = fixDungeonItem(rom.banks[0x3E][0x3800 + BOSS_ROOMS[dungeon_nr]], dungeon_nr) + rom.banks[0x3E][0x3300 + 0x01ff] = fixDungeonItem(rom.banks[0x3E][0x3300 + BOSS_ROOMS[dungeon_nr]], dungeon_nr) + elif target == 6: # Evil eagle + rom.banks[0x14][BOSS_ROOMS[dungeon_nr] - 0x100] = 0x2A + + # Patch the eagle heart container to open up the right room. + rom.patch(0x03, 0x1A04, ASM("ld hl, $DA2E"), ASM("ld hl, $%04x" % (getBossRoomStatusFlagLocation(dungeon_nr)))) + + # Add the staircase to the boss, and fix the warp back. + re = getCleanBossRoom(rom, dungeon_nr) + re.objects += [Object(4, 4, 0xBE), ObjectWarp(2, 6, 0x2F8, 72, 80)] + re.store(rom) + re = RoomEditor(rom, 0x2F8) + re.objects[-1] = ObjectWarp(1, dungeon_nr if dungeon_nr < 8 else 0xff, BOSS_ROOMS[dungeon_nr], 72, 80) + re.store(rom) + + # Patch the proper item towards the D7 boss + rom.banks[0x3E][0x3800 + 0x02E8] = fixDungeonItem(rom.banks[0x3E][0x3800 + BOSS_ROOMS[dungeon_nr]], dungeon_nr) + rom.banks[0x3E][0x3300 + 0x02E8] = fixDungeonItem(rom.banks[0x3E][0x3300 + BOSS_ROOMS[dungeon_nr]], dungeon_nr) + else: + rom.banks[0x14][BOSS_ROOMS[dungeon_nr] - 0x100] = 0x21 + re = getCleanBossRoom(rom, dungeon_nr) + re.entities = [BOSS_ENTITIES[target]] + + if target == 4: + # For slime eel, we need to setup the right wall tiles. + rom.banks[0x20][0x2EB3 + BOSS_ROOMS[dungeon_nr] - 0x100] = 0x06 + if target == 5: + # Patch facade so he doesn't use the spinning tiles, which is a problem for the sprites. + rom.patch(0x04, 0x121D, ASM("cp $14"), ASM("cp $00")) + rom.patch(0x04, 0x1226, ASM("cp $04"), ASM("cp $00")) + rom.patch(0x04, 0x127F, ASM("cp $14"), ASM("cp $00")) + if target == 7: + pass + # For hot head, add some lava (causes graphical glitches) + # re.animation_id = 0x06 + # re.objects += [ + # ObjectHorizontal(3, 2, 0x06, 4), + # ObjectHorizontal(2, 3, 0x06, 6), + # ObjectHorizontal(2, 4, 0x06, 6), + # ObjectHorizontal(3, 5, 0x06, 4), + # ] + + re.store(rom) + + +def readBossMapping(rom): + mapping = [] + for dungeon_nr in range(9): + r = RoomEditor(rom, BOSS_ROOMS[dungeon_nr]) + if r.entities: + mapping.append(BOSS_ENTITIES.index(r.entities[0])) + elif isinstance(r.objects[-1], ObjectWarp) and r.objects[-1].room == 0x1ef: + mapping.append(3) + elif isinstance(r.objects[-1], ObjectWarp) and r.objects[-1].room == 0x2f8: + mapping.append(6) + else: + mapping.append(dungeon_nr) + return mapping + + +def changeMiniBosses(rom, mapping): + # Fix avalaunch not working when entering a room from the left or right + rom.patch(0x03, 0x0BE0, ASM(""" + ld [hl], $50 + ld hl, $C2D0 + add hl, bc + ld [hl], $00 + jp $4B56 + """), ASM(""" + ld a, [hl] + sub $08 + ld [hl], a + ld hl, $C2D0 + add hl, bc + ld [hl], b ; b is always zero here + ret + """), fill_nop=True) + # Fix avalaunch waiting until the room event is done (and not all rooms have a room event on enter) + rom.patch(0x36, 0x1C14, ASM("ret z"), "", fill_nop=True) + # Fix giant buzz blob waiting until the room event is done (and not all rooms have a room event on enter) + rom.patch(0x36, 0x153B, ASM("ret z"), "", fill_nop=True) + + # Remove the powder fairy from giant buzz blob + rom.patch(0x36, 0x14F7, ASM("jr nz, $05"), ASM("jr $05")) + + # Do not allow the force barrier in D3 dodongo room + rom.patch(0x14, 0x14AC, 0x14B5, ASM("jp $7FE0"), fill_nop=True) + rom.patch(0x14, 0x3FE0, "00" * 0x20, ASM(""" + ld a, [$C124] ; room transition + ld hl, $C17B + or [hl] + ret nz + ldh a, [$F6] ; room + cp $45 ; check for D3 dodogo room + ret z + cp $7F ; check for armos temple room + ret z + jp $54B5 + """), fill_nop=True) + + # Patch smasher to spawn the ball closer, so it doesn't spawn on the wall in the armos temple + rom.patch(0x06, 0x0533, ASM("add a, $30"), ASM("add a, $20")) + + for target, name in mapping.items(): + re = RoomEditor(rom, MINIBOSS_ROOMS[target]) + re.entities = [e for e in re.entities if e[2] == 0x61] # Only keep warp, if available + re.entities += MINIBOSS_ENTITIES[name] + + if re.room == 0x228 and name != "GRIM_CREEPER": + for x in range(3, 7): + for y in range(0, 3): + re.removeObject(x, y) + + if name == "CUE_BALL": + re.objects += [ + Object(3, 3, 0x2c), + ObjectHorizontal(4, 3, 0x22, 2), + Object(6, 3, 0x2b), + Object(3, 4, 0x2a), + ObjectHorizontal(4, 4, 0x21, 2), + Object(6, 4, 0x29), + ] + if name == "BLAINO": + # BLAINO needs a warp object to hit you to the entrance of the dungeon. + if len(re.getWarps()) < 1: + # Default to start house. + target = (0x10, 0x2A3, 0x50, 0x7c) + if 0x100 <= re.room < 0x11D: #D1 + target = (0, 0x117, 80, 80) + elif 0x11D <= re.room < 0x140: #D2 + target = (1, 0x136, 80, 80) + elif 0x140 <= re.room < 0x15D: #D3 + target = (2, 0x152, 80, 80) + elif 0x15D <= re.room < 0x180: #D4 + target = (3, 0x174, 80, 80) + elif 0x180 <= re.room < 0x1AC: #D5 + target = (4, 0x1A1, 80, 80) + elif 0x1B0 <= re.room < 0x1DE: #D6 + target = (5, 0x1D4, 80, 80) + elif 0x200 <= re.room < 0x22D: #D7 + target = (6, 0x20E, 80, 80) + elif 0x22D <= re.room < 0x26C: #D8 + target = (7, 0x25D, 80, 80) + elif re.room >= 0x300: #D0 + target = (0xFF, 0x312, 80, 80) + elif re.room == 0x2E1: #Moblin cave + target = (0x15, 0x2F0, 0x50, 0x7C) + elif re.room == 0x27F: #Armos temple + target = (0x16, 0x28F, 0x50, 0x7C) + re.objects.append(ObjectWarp(1, *target)) + if name == "DODONGO": + # Remove breaking floor tiles from the room. + re.objects = [obj for obj in re.objects if obj.type_id != 0xDF] + if name == "ROLLING_BONES" and target == 2: + # Make rolling bones pass trough walls so it does not get stuck here. + rom.patch(0x03, 0x02F1 + 0x81, "84", "95") + re.store(rom) + + +def readMiniBossMapping(rom): + mapping = {} + for key, room in MINIBOSS_ROOMS.items(): + r = RoomEditor(rom, room) + for me_key, me_data in MINIBOSS_ENTITIES.items(): + if me_data[-1][2] == r.entities[-1][2]: + mapping[key] = me_key + return mapping + + +def doubleTrouble(rom): + for n in range(0x316): + if n == 0x2FF: + continue + re = RoomEditor(rom, n) + # Bosses + if re.hasEntity(0x59): # Moldorm (TODO; double heart container drop) + re.removeEntities(0x59) + re.entities += [(3, 2, 0x59), (4, 2, 0x59)] + re.store(rom) + if re.hasEntity(0x5C): # Ghini + re.removeEntities(0x5C) + re.entities += [(3, 2, 0x5C), (4, 2, 0x5C)] + re.store(rom) + if re.hasEntity(0x5B): # slime eye + re.removeEntities(0x5B) + re.entities += [(3, 2, 0x5B), (6, 2, 0x5B)] + re.store(rom) + if re.hasEntity(0x65): # angler fish + re.removeEntities(0x65) + re.entities += [(6, 2, 0x65), (6, 5, 0x65)] + re.store(rom) + # Slime eel bugs out on death if duplicated. + # if re.hasEntity(0x5D): # slime eel + # re.removeEntities(0x5D) + # re.entities += [(6, 2, 0x5D), (6, 5, 0x5D)] + # re.store(rom) + if re.hasEntity(0x5A): # facade (TODO: Drops two hearts, shared health?) + re.removeEntities(0x5A) + re.entities += [(2, 3, 0x5A), (6, 3, 0x5A)] + re.store(rom) + # Evil eagle causes a crash, and messes up the intro sequence and generally is just a mess if I spawn multiple + # if re.hasEntity(0x63): # evil eagle + # re.removeEntities(0x63) + # re.entities += [(3, 4, 0x63), (2, 4, 0x63)] + # re.store(rom) + # # Remove that links movement is blocked + # rom.patch(0x05, 0x2258, ASM("ldh [$A1], a"), "0000") + # rom.patch(0x05, 0x1AE3, ASM("ldh [$A1], a"), "0000") + # rom.patch(0x05, 0x1C5D, ASM("ldh [$A1], a"), "0000") + # rom.patch(0x05, 0x1C8D, ASM("ldh [$A1], a"), "0000") + # rom.patch(0x05, 0x1CAF, ASM("ldh [$A1], a"), "0000") + if re.hasEntity(0x62): # hot head (TODO: Drops thwo hearts) + re.removeEntities(0x62) + re.entities += [(2, 2, 0x62), (4, 4, 0x62)] + re.store(rom) + if re.hasEntity(0xF9): # hardhit beetle + re.removeEntities(0xF9) + re.entities += [(2, 2, 0xF9), (5, 4, 0xF9)] + re.store(rom) + # Minibosses + if re.hasEntity(0x89): + re.removeEntities(0x89) + re.entities += [(2, 3, 0x89), (6, 3, 0x89)] + re.store(rom) + if re.hasEntity(0x81): + re.removeEntities(0x81) + re.entities += [(2, 3, 0x81), (6, 3, 0x81)] + re.store(rom) + if re.hasEntity(0x60): + dodongo = [e for e in re.entities if e[2] == 0x60] + x = (dodongo[0][0] + dodongo[1][0]) // 2 + y = (dodongo[0][1] + dodongo[1][1]) // 2 + re.entities += [(x, y, 0x60)] + re.store(rom) + if re.hasEntity(0x8e): + re.removeEntities(0x8e) + re.entities += [(1, 1, 0x8e), (7, 1, 0x8e)] + re.store(rom) + if re.hasEntity(0x92): + re.removeEntities(0x92) + re.entities += [(2, 3, 0x92), (4, 3, 0x92)] + re.store(rom) + if re.hasEntity(0xf4): + re.removeEntities(0xf4) + re.entities += [(2, 1, 0xf4), (6, 1, 0xf4)] + re.store(rom) + if re.hasEntity(0xf8): + re.removeEntities(0xf8) + re.entities += [(2, 2, 0xf8), (6, 2, 0xf8)] + re.store(rom) + if re.hasEntity(0xe4): + re.removeEntities(0xe4) + re.entities += [(5, 2, 0xe4), (5, 5, 0xe4)] + re.store(rom) + + if re.hasEntity(0x88): # Armos knight (TODO: double item drop) + re.removeEntities(0x88) + re.entities += [(3, 3, 0x88), (6, 3, 0x88)] + re.store(rom) + if re.hasEntity(0x87): # Lanmola (TODO: killing one drops the item, and marks as done) + re.removeEntities(0x87) + re.entities += [(2, 2, 0x87), (1, 1, 0x87)] + re.store(rom) diff --git a/worlds/ladx/LADXR/patches/entrances.py b/worlds/ladx/LADXR/patches/entrances.py new file mode 100644 index 0000000000..82a09edf58 --- /dev/null +++ b/worlds/ladx/LADXR/patches/entrances.py @@ -0,0 +1,58 @@ +from ..roomEditor import RoomEditor, ObjectWarp +from ..worldSetup import ENTRANCE_INFO + + +def changeEntrances(rom, mapping): + warp_to_indoor = {} + warp_to_outdoor = {} + for key in mapping.keys(): + info = ENTRANCE_INFO[key] + re = RoomEditor(rom, info.alt_room if info.alt_room is not None else info.room) + warp = re.getWarps()[info.index if info.index not in (None, "all") else 0] + warp_to_indoor[key] = warp + assert info.target == warp.room, "%s != %03x" % (key, warp.room) + + re = RoomEditor(rom, warp.room) + for warp in re.getWarps(): + if warp.room == info.room: + warp_to_outdoor[key] = warp + assert key in warp_to_outdoor, "Missing warp to outdoor on %s" % (key) + + # First collect all the changes we need to do per room + changes_per_room = {} + def addChange(source_room, target_room, new_warp): + if source_room not in changes_per_room: + changes_per_room[source_room] = {} + changes_per_room[source_room][target_room] = new_warp + for key, target in mapping.items(): + if key == target: + continue + info = ENTRANCE_INFO[key] + # Change the entrance to point to the new indoor room + addChange(info.room, warp_to_indoor[key].room, warp_to_indoor[target]) + if info.alt_room: + addChange(info.alt_room, warp_to_indoor[key].room, warp_to_indoor[target]) + + # Change the exit to point to the right outside + addChange(warp_to_indoor[target].room, ENTRANCE_INFO[target].room, warp_to_outdoor[key]) + if ENTRANCE_INFO[target].instrument_room is not None: + addChange(ENTRANCE_INFO[target].instrument_room, ENTRANCE_INFO[target].room, warp_to_outdoor[key]) + + # Finally apply the changes, we need to do this once per room to prevent A->B->C issues. + for room, changes in changes_per_room.items(): + re = RoomEditor(rom, room) + for idx, obj in enumerate(re.objects): + if isinstance(obj, ObjectWarp) and obj.room in changes: + re.objects[idx] = changes[obj.room].copy() + re.store(rom) + + +def readEntrances(rom): + result = {} + for key, info in ENTRANCE_INFO.items(): + re = RoomEditor(rom, info.alt_room if info.alt_room is not None else info.room) + warp = re.getWarps()[info.index if info.index not in (None, "all") else 0] + for other_key, other_info in ENTRANCE_INFO.items(): + if warp.room == other_info.target: + result[key] = other_key + return result diff --git a/worlds/ladx/LADXR/patches/fishingMinigame.py b/worlds/ladx/LADXR/patches/fishingMinigame.py new file mode 100644 index 0000000000..a0c079a6e2 --- /dev/null +++ b/worlds/ladx/LADXR/patches/fishingMinigame.py @@ -0,0 +1,19 @@ +from ..assembler import ASM +from ..roomEditor import RoomEditor + + +def updateFinishingMinigame(rom): + rom.patch(0x04, 0x26BE, 0x26DF, ASM(""" + ld a, $0E ; GiveItemAndMessageForRoomMultiworld + rst 8 + + ; Mark selection as stopping minigame, as we are not asking a question. + ld a, $01 + ld [$C177], a + + ; Check if we got rupees from the item skip getting rupees from the fish. + ld a, [$DB90] + ld hl, $DB8F + or [hl] + jp nz, $66FE + """), fill_nop=True) diff --git a/worlds/ladx/LADXR/patches/goal.py b/worlds/ladx/LADXR/patches/goal.py new file mode 100644 index 0000000000..cb932aa1d9 --- /dev/null +++ b/worlds/ladx/LADXR/patches/goal.py @@ -0,0 +1,317 @@ +from ..assembler import ASM +from ..roomEditor import RoomEditor, Object, ObjectVertical, ObjectHorizontal, ObjectWarp +from ..utils import formatText + + +def setRequiredInstrumentCount(rom, count): + rom.texts[0x1A3] = formatText("You need %d instruments" % (count)) + if count >= 8: + return + if count < 0: + rom.patch(0x00, 0x31f5, ASM("ld a, [$D806]\nand $10\njr z, $25"), ASM(""), fill_nop=True) + rom.patch(0x20, 0x2dea, ASM("ld a, [$D806]\nand $10\njr z, $29"), ASM(""), fill_nop=True) + count = 0 + + # TODO: Music bugs out at the end, unless you have all instruments. + rom.patch(0x19, 0x0B79, None, "0000") # always spawn all instruments, we need the last one as that handles opening the egg. + rom.patch(0x19, 0x0BF4, ASM("jp $3BC0"), ASM("jp $7FE0")) # instead of rendering the instrument, jump to the code below. + rom.patch(0x19, 0x0BFE, ASM(""" + ; Normal check fo all instruments + ld e, $08 + ld hl, $DB65 + loop: + ldi a, [hl] + and $02 + jr z, $12 + dec e + jr nz, loop + """), ASM(""" + jp $7F2B ; jump to the end of the bank, where there is some space for code. + """), fill_nop=True) + # Add some code at the end of the bank, as we do not have enough space to do this "in place" + rom.patch(0x19, 0x3F2B, "0000000000000000000000000000000000000000000000000000", ASM(""" + ld d, $00 + ld e, $08 + ld hl, $DB65 ; start of has instrument memory +loop: + ld a, [hl] + and $02 + jr z, noinc + inc d +noinc: + inc hl + dec e + jr nz, loop + ld a, d + cp $%02x ; check if we have a minimal of this amount of instruments. + jp c, $4C1A ; not enough instruments + jp $4C0B ; enough instruments + """ % (count)), fill_nop=True) + rom.patch(0x19, 0x3FE0, "0000000000000000000000000000000000000000000000000000", ASM(""" + ; Entry point of render code + ld hl, $DB65 ; table of having instruments + push bc + ldh a, [$F1] + ld c, a + add hl, bc + pop bc + ld a, [hl] + and $02 ; check if we have this instrument + ret z + jp $3BC0 ; jump to render code + """), fill_nop=True) + + +def setSeashellGoal(rom, count): + rom.texts[0x1A3] = formatText("You need %d {SEASHELL}s" % (count)) + + # Remove the seashell mansion handler (as it will take your seashells) but put a heartpiece instead + re = RoomEditor(rom, 0x2E9) + re.entities = [(4, 4, 0x35)] + re.store(rom) + + rom.patch(0x19, 0x0ACB, 0x0C21, ASM(""" + ldh a, [$F8] ; room status + and $10 + ret nz + ldh a, [$F0] ; active entity state + rst 0 + dw state0, state1, state2, state3, state4 + +state0: + ld a, [$C124] ; room transition state + and a + ret nz + ldh a, [$99] ; link position Y + cp $70 + ret nc + jp $3B12 ; increase entity state + +state1: + call $0C05 ; get entity transition countdown + jr nz, renderShells + ld [hl], $10 + call renderShells + + ld hl, $C2B0 ; private state 1 table + add hl, bc + ld a, [wSeashellsCount] + cp [hl] + jp z, $3B12 ; increase entity state + ld a, [hl] ; increase the amount of compared shells + inc a + daa + ld [hl], a + ld hl, $C2C0 ; private state 2 table + add hl, bc + inc [hl] ; increase amount of displayed shells + ld a, $2B + ldh [$F4], a ; SFX + ret + +state2: + ld a, [wSeashellsCount] + cp $%02d + jr c, renderShells + ; got enough shells + call $3B12 ; increase entity state + call $0C05 ; get entity transition countdown + ld [hl], $40 + jp renderShells + +state3: + ld a, $23 + ldh [$F2], a ; SFX: Dungeon opened + ld hl, $D806 ; egg room status + set 4, [hl] + ld a, [hl] + ldh [$F8], a ; current room status + call $3B12 ; increase entity state + + ld a, $00 + jp $4C2E + +state4: + ret + +renderShells: + ld hl, $C2C0 ; private state 2 table + add hl, bc + ld a, [hl] + cp $14 + jr c, .noMax + ld a, $14 +.noMax: + and a + ret z + ld c, a + ld hl, spriteRect + call $3CE6 ; RenderActiveEntitySpritesRect + ret + +spriteRect: + db $10, $1E, $1E, $0C + db $10, $2A, $1E, $0C + db $10, $36, $1E, $0C + db $10, $42, $1E, $0C + db $10, $4E, $1E, $0C + + db $10, $5A, $1E, $0C + db $10, $66, $1E, $0C + db $10, $72, $1E, $0C + db $10, $7E, $1E, $0C + db $10, $8A, $1E, $0C + + db $24, $1E, $1E, $0C + db $24, $2A, $1E, $0C + db $24, $36, $1E, $0C + db $24, $42, $1E, $0C + db $24, $4E, $1E, $0C + + db $24, $5A, $1E, $0C + db $24, $66, $1E, $0C + db $24, $72, $1E, $0C + db $24, $7E, $1E, $0C + db $24, $8A, $1E, $0C + """ % (count), 0x4ACB), fill_nop=True) + + +def setRaftGoal(rom): + rom.texts[0x1A3] = formatText("Just sail away.") + + # Remove the egg and egg event handler. + re = RoomEditor(rom, 0x006) + for x in range(4, 7): + for y in range(0, 4): + re.removeObject(x, y) + re.objects.append(ObjectHorizontal(4, 1, 0x4d, 3)) + re.objects.append(ObjectHorizontal(4, 2, 0x03, 3)) + re.objects.append(ObjectHorizontal(4, 3, 0x03, 3)) + re.entities = [] + re.updateOverlay() + re.store(rom) + + re = RoomEditor(rom, 0x08D) + re.objects[6].count = 4 + re.objects[7].x += 2 + re.objects[7].type_id = 0x2B + re.objects[8].x += 2 + re.objects[8].count = 2 + re.objects[9].x += 1 + re.objects[11] = ObjectVertical(7, 5, 0x37, 2) + re.objects[12].x -= 1 + re.objects[13].x -= 1 + re.objects[14].x -= 1 + re.objects[14].type_id = 0x34 + re.objects[17].x += 3 + re.objects[17].count -= 3 + re.updateOverlay() + re.overlay[7 + 60] = 0x33 + re.store(rom) + + re = RoomEditor(rom, 0x0E9) + re.objects[30].count = 1 + re.objects[30].x += 2 + re.overlay[7 + 70] = 0x0E + re.overlay[8 + 70] = 0x0E + re.store(rom) + re = RoomEditor(rom, 0x0F9) + re.objects = [ + ObjectHorizontal(4, 0, 0x0E, 6), + ObjectVertical(9, 0, 0xCA, 8), + ObjectVertical(8, 0, 0x0E, 8), + + Object(3, 0, 0x38), + Object(3, 1, 0x32), + ObjectHorizontal(4, 1, 0x2C, 3), + Object(7, 1, 0x2D), + ObjectVertical(7, 2, 0x38, 5), + Object(7, 7, 0x34), + ObjectHorizontal(0, 7, 0x2F, 7), + + ObjectVertical(2, 3, 0xE8, 4), + ObjectVertical(3, 2, 0xE8, 5), + ObjectVertical(4, 2, 0xE8, 2), + + ObjectVertical(4, 4, 0x5C, 3), + ObjectVertical(5, 2, 0x5C, 5), + ObjectVertical(6, 2, 0x5C, 5), + + Object(6, 4, 0xC6), + ObjectWarp(1, 0x1F, 0xF6, 136, 112) + ] + re.updateOverlay(True) + re.entities.append((0, 0, 0x41)) + re.store(rom) + re = RoomEditor(rom, 0x1F6) + re.objects[-1].target_x -= 16 + re.store(rom) + + # Fix the raft graphics (this overrides some unused graphic tiles) + rom.banks[0x31][0x21C0:0x2200] = rom.banks[0x2E][0x07C0:0x0800] + + # Patch the owl entity to run our custom end handling. + rom.patch(0x06, 0x27F5, 0x2A77, ASM(""" + ld a, [$DB95] + cp $0B + ret nz + ; If map is not fully loaded, return + ld a, [$C124] + and a + ret nz + ; Check if we are moving off the bottom of the map + ldh a, [$99] + cp $7D + ret c + ; Move link back so it does not move off the map + ld a, $7D + ldh [$99], a + + xor a + ld e, a + ld d, a + +raftSearchLoop: + ld hl, $C280 + add hl, de + ld a, [hl] + and a + jr z, .skipEntity + + ld hl, $C3A0 + add hl, de + ld a, [hl] + cp $6A + jr nz, .skipEntity + + ; Raft found, check if near the bottom of the screen. + ld hl, $C210 + add hl, de + ld a, [hl] + cp $70 + jr nc, raftOffWorld + +.skipEntity: + inc e + ld a, e + cp $10 + jr nz, raftSearchLoop + ret + +raftOffWorld: + ; Switch to the end credits + ld a, $01 + ld [$DB95], a + ld a, $00 + ld [$DB96], a + ret + """), fill_nop=True) + + # We need to run quickly trough part of the credits, or else it bugs out + # Skip the whole windfish part. + rom.patch(0x17, 0x0D39, None, ASM("ld a, $18\nld [$D00E], a\nret")) + # And skip the zoomed out laying on the log + rom.patch(0x17, 0x20ED, None, ASM("ld a, $00")) + # Finally skip some waking up on the log. + rom.patch(0x17, 0x23BC, None, ASM("jp $4CD9")) + rom.patch(0x17, 0x2476, None, ASM("jp $4CD9")) diff --git a/worlds/ladx/LADXR/patches/goldenLeaf.py b/worlds/ladx/LADXR/patches/goldenLeaf.py new file mode 100644 index 0000000000..87cefae0f6 --- /dev/null +++ b/worlds/ladx/LADXR/patches/goldenLeaf.py @@ -0,0 +1,34 @@ +from ..assembler import ASM + + +def fixGoldenLeaf(rom): + # Patch the golden leaf code so it jumps to the dropped key handling in bank 3E + rom.patch(3, 0x2007, ASM(""" + ld de, $5FFB + call $3C77 ; RenderActiveEntitySprite + """), ASM(""" + ld a, $04 + rst 8 + """), fill_nop=True) + rom.patch(3, 0x2018, None, ASM(""" + ld a, $06 ; giveItemMultiworld + rst 8 + jp $602F + """)) + rom.patch(3, 0x2037, None, ASM(""" + ld a, $0a ; showMessageMultiworld + rst 8 + jp $604B + """)) + + # Patch all over the place to move the golden leafs to a different memory location. + # We use $DB6D (dungeon 9 status), but we could also use $DB7A (which is only used by the ghost) + rom.patch(0x00, 0x2D17, ASM("ld a, [$DB15]"), ASM("ld a, $06"), fill_nop=True) # Always load the slime tiles + rom.patch(0x02, 0x3005, ASM("cp $06"), ASM("cp $01"), fill_nop=True) # Instead of checking for 6 leaves a the keyhole, just check for the key + rom.patch(0x20, 0x1AD1, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # For the status screen, load the number of leafs from the proper memory + rom.patch(0x03, 0x0980, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # If leaves >= 6 move richard + rom.patch(0x06, 0x0059, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # If leaves >= 6 move richard + rom.patch(0x06, 0x007D, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # Richard message if no leaves + rom.patch(0x06, 0x00B8, ASM("ld [$DB15], a"), ASM("ld [wGoldenLeaves], a")) # Stores FF in the leaf counter if we opened the path + # 6:40EE uses leaves == 6 to check if we have collected the key, but only to change the message. + # rom.patch(0x06, 0x2AEF, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # Telephone message handler diff --git a/worlds/ladx/LADXR/patches/hardMode.py b/worlds/ladx/LADXR/patches/hardMode.py new file mode 100644 index 0000000000..3ecceda919 --- /dev/null +++ b/worlds/ladx/LADXR/patches/hardMode.py @@ -0,0 +1,64 @@ +from ..assembler import ASM + + +def oracleMode(rom): + # Reduce iframes + rom.patch(0x03, 0x2DB2, ASM("ld a, $50"), ASM("ld a, $20")) + + # Make bomb explosions damage you. + rom.patch(0x03, 0x2618, ASM(""" + ld hl, $C440 + add hl, bc + ld a, [hl] + and a + jr nz, $05 + """), ASM(""" + call $6625 + """), fill_nop=True) + # Reduce bomb blast push back on link + rom.patch(0x03, 0x2643, ASM("sla [hl]"), ASM("sra [hl]"), fill_nop=True) + rom.patch(0x03, 0x2648, ASM("sla [hl]"), ASM("sra [hl]"), fill_nop=True) + + # Never spawn a piece of power or acorn + rom.patch(0x03, 0x1608, ASM("jr nz, $05"), ASM("jr $05")) + rom.patch(0x03, 0x1642, ASM("jr nz, $04"), ASM("jr $04")) + + # Let hearts only recover half a container instead of a full one. + rom.patch(0x03, 0x24B7, ASM("ld a, $08"), ASM("ld a, $04")) + # Don't randomly drop fairies from enemies, drop a rupee instead + rom.patch(0x03, 0x15C7, "2E2D382F2E2D3837", "2E2D382E2E2D3837") + + # Make dropping in water without flippers damage you. + rom.patch(0x02, 0x3722, ASM("ldh a, [$AF]"), ASM("ld a, $06")) + + +def heroMode(rom): + # Don't randomly drop fairies and hearts from enemies, drop a rupee instead + rom.patch(0x03, 0x159D, + "2E2E2D2D372DFFFF2F37382E2F2F", + "2E2EFFFF37FFFFFFFF37382EFFFF") + rom.patch(0x03, 0x15C7, + "2E2D382F2E2D3837", + "2E2E382E2E2E3837") + rom.patch(0x00, 0x168F, ASM("ld a, $2D"), "", fill_nop=True) + rom.patch(0x02, 0x0CDB, ASM("ld a, $2D"), "", fill_nop=True) + # Double damage + rom.patch(0x03, 0x2DAB, + ASM("ld a, [$DB94]\nadd a, e\nld [$DB94], a"), + ASM("ld hl, $DB94\nld a, [hl]\nadd a, e\nadd a, e\nld [hl], a")) + rom.patch(0x02, 0x11B2, ASM("add a, $04"), ASM("add a, $08")) + rom.patch(0x02, 0x127E, ASM("add a, $04"), ASM("add a, $08")) + rom.patch(0x02, 0x291C, ASM("add a, $04"), ASM("add a, $08")) + rom.patch(0x02, 0x362B, ASM("add a, $04"), ASM("add a, $08")) + rom.patch(0x06, 0x041C, ASM("ld a, $02"), ASM("ld a, $04")) + rom.patch(0x15, 0x09B8, ASM("add a, $08"), ASM("add a, $10")) + rom.patch(0x15, 0x32FD, ASM("ld a, $08"), ASM("ld a, $10")) + rom.patch(0x18, 0x370E, ASM("ld a, $08"), ASM("ld a, $10")) + rom.patch(0x07, 0x3103, ASM("ld a, $08"), ASM("ld a, $10")) + rom.patch(0x06, 0x1166, ASM("ld a, $08"), ASM("ld a, $10")) + + + + +def oneHitKO(rom): + rom.patch(0x02, 0x238C, ASM("ld [$DB94], a"), "", fill_nop=True) diff --git a/worlds/ladx/LADXR/patches/health.py b/worlds/ladx/LADXR/patches/health.py new file mode 100644 index 0000000000..7488e6280a --- /dev/null +++ b/worlds/ladx/LADXR/patches/health.py @@ -0,0 +1,33 @@ +from ..assembler import ASM +from ..utils import formatText + + +def setStartHealth(rom, amount): + rom.patch(0x01, 0x0B1C, ASM("ld [hl], $03"), ASM("ld [hl], $%02X" % (amount))) # max health of new save + rom.patch(0x01, 0x0B14, ASM("ld [hl], $18"), ASM("ld [hl], $%02X" % (amount * 8))) # current health of new save + + +def upgradeHealthContainers(rom): + # Reuse 2 unused shop messages for the heart containers. + rom.texts[0x2A] = formatText("You found a {HEART_CONTAINER}!") + rom.texts[0x2B] = formatText("You lost a heart!") + + rom.patch(0x03, 0x19DC, ASM(""" + ld de, $59D8 + call $3BC0 + """), ASM(""" + ld a, $05 ; renderHeartPiece + rst 8 + """), fill_nop=True) + rom.patch(0x03, 0x19F0, ASM(""" + ld hl, $DB5B + inc [hl] + ld hl, $DB93 + ld [hl], $FF + """), ASM(""" + ld a, $06 ; giveItemMultiworld + rst 8 + ld a, $0A ; messageForItemMultiworld + rst 8 +skip: + """), fill_nop=True) # add heart->remove heart on heart container diff --git a/worlds/ladx/LADXR/patches/heartPiece.py b/worlds/ladx/LADXR/patches/heartPiece.py new file mode 100644 index 0000000000..4147c8fe95 --- /dev/null +++ b/worlds/ladx/LADXR/patches/heartPiece.py @@ -0,0 +1,42 @@ +from ..assembler import ASM + + +def fixHeartPiece(rom): + # Patch all locations where the piece of heart is rendered. + rom.patch(0x03, 0x1b52, ASM("ld de, $5A4D\ncall $3BC0"), ASM("ld a, $04\nrst 8"), fill_nop=True) # state 0 + + # Write custom code in the first state handler, this overwrites all state handlers + # Till state 5. + rom.patch(0x03, 0x1A74, 0x1A98, ASM(""" + ; Render sprite + ld a, $05 + rst 8 + + ; Handle item effect + ld a, $06 ; giveItemMultiworld + rst 8 + + ;Show message + ld a, $0A ; showMessageMultiworld + rst 8 + + ; Switch to state 5 + ld hl, $C290; stateTable + add hl, bc + ld [hl], $05 + ret + """), fill_nop=True) + # Insert a state 5 handler + rom.patch(0x03, 0x1A98, 0x1B17, ASM(""" + ; Render sprite + ld a, $05 + rst 8 + + ld a, [$C19F] ; dialog state + and a + ret nz + + call $512A ; mark room as done + call $3F8D ; unload entity + ret + """), fill_nop=True) diff --git a/worlds/ladx/LADXR/patches/instrument.py b/worlds/ladx/LADXR/patches/instrument.py new file mode 100644 index 0000000000..9e1cfecc40 --- /dev/null +++ b/worlds/ladx/LADXR/patches/instrument.py @@ -0,0 +1,24 @@ +from ..assembler import ASM + + +def fixInstruments(rom): + rom.patch(0x03, 0x1EA9, 0x1EAE, "", fill_nop=True) + rom.patch(0x03, 0x1EB9, 0x1EC8, ASM(""" + ; Render sprite + ld a, $05 + rst 8 + """), fill_nop=True) + + # Patch the message and instrument giving code + rom.patch(0x03, 0x1EE3, 0x1EF6, ASM(""" + ; Handle item effect + ld a, $06 ; giveItemMultiworld + rst 8 + + ;Show message + ld a, $0A ; showMessageMultiworld + rst 8 + """), fill_nop=True) + + # Color cycle palette 7 instead of 1 + rom.patch(0x36, 0x30F0, ASM("ld de, $DC5C"), ASM("ld de, $DC84")) diff --git a/worlds/ladx/LADXR/patches/inventory.py b/worlds/ladx/LADXR/patches/inventory.py new file mode 100644 index 0000000000..c3ca96e01b --- /dev/null +++ b/worlds/ladx/LADXR/patches/inventory.py @@ -0,0 +1,421 @@ +from ..assembler import ASM +from ..backgroundEditor import BackgroundEditor + + +def selectToSwitchSongs(rom): + # Do not ignore left/right keys when ocarina is selected + rom.patch(0x20, 0x1F18, ASM("and a"), ASM("xor a")) + # Change the keys which switch the ocarina song to select and no key. + rom.patch(0x20, 0x21A9, ASM("and $01"), ASM("and $40")) + rom.patch(0x20, 0x21C7, ASM("and $02"), ASM("and $00")) + +def songSelectAfterOcarinaSelect(rom): + rom.patch(0x20, 0x2002, ASM("ld [$DB00], a"), ASM("call $5F96")) + rom.patch(0x20, 0x1FE0, ASM("ld [$DB01], a"), ASM("call $5F9B")) + # Remove the code that opens the ocerina on cursor movement, but use it to insert code + # for opening the menu on item select + rom.patch(0x20, 0x1F93, 0x1FB2, ASM(""" + jp $5FB2 + itemToB: + ld [$DB00], a + jr checkForOcarina + itemToA: + ld [$DB01], a + checkForOcarina: + cp $09 + jp nz, $6010 + ld a, [$DB49] + and a + ret z + ld a, $08 + ldh [$90], a ; load ocarina song select graphics + ;ld a, $10 + ;ld [$C1B8], a ; shows the opening animation + ld a, $01 + ld [$C1B5], a + ret + """), fill_nop=True) + # More code that opens the menu, use this to close the menu + rom.patch(0x20, 0x200D, 0x2027, ASM(""" + jp $6027 + closeOcarinaMenu: + ld a, [$C1B5] + and a + ret z + xor a + ld [$C1B5], a + ld a, $10 + ld [$C1B9], a ; shows the closing animation + ret + """), fill_nop=True) + rom.patch(0x20, 0x2027, 0x2036, "", fill_nop=True) # Code that closes the ocarina menu on item select + + rom.patch(0x20, 0x22A2, ASM(""" + ld a, [$C159] + inc a + ld [$C159], a + and $10 + jr nz, $30 + """), ASM(""" + ld a, [$C1B5] + and a + ret nz + ldh a, [$E7] ; frame counter + and $10 + ret nz + """), fill_nop=True) + +def moreSlots(rom): + #Move flippers, medicine, trade item and seashells to DB3E+ + rom.patch(0x02, 0x292B, ASM("ld a, [$DB0C]"), ASM("ld a, [$DB3E]")) + #rom.patch(0x02, 0x2E8F, ASM("ld a, [$DB0C]"), ASM("ld a, [$DB3E]")) + rom.patch(0x02, 0x3713, ASM("ld a, [$DB0C]"), ASM("ld a, [$DB3E]")) + rom.patch(0x20, 0x1A23, ASM("ld de, $DB0C"), ASM("ld de, $DB3E")) + rom.patch(0x02, 0x23a3, ASM("ld a, [$DB0D]"), ASM("ld a, [$DB3F]")) + rom.patch(0x02, 0x23d7, ASM("ld a, [$DB0D]"), ASM("ld a, [$DB3F]")) + rom.patch(0x02, 0x23aa, ASM("ld [$DB0D], a"), ASM("ld [$DB3F], a")) + rom.patch(0x04, 0x3b1f, ASM("ld [$DB0D], a"), ASM("ld [$DB3F], a")) + rom.patch(0x06, 0x1f58, ASM("ld a, [$DB0D]"), ASM("ld a, [$DB3F]")) + rom.patch(0x06, 0x1ff5, ASM("ld hl, $DB0D"), ASM("ld hl, $DB3F")) + rom.patch(0x07, 0x3c33, ASM("ld [$DB0D], a"), ASM("ld [$DB3F], a")) + rom.patch(0x00, 0x1e01, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x00, 0x2d21, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x00, 0x3199, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x03, 0x0ae6, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x03, 0x0b6d, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x03, 0x0f68, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x04, 0x2faa, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x04, 0x3502, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x04, 0x3624, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x05, 0x0bff, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x05, 0x0d20, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x05, 0x0db1, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x05, 0x0dd5, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x05, 0x0e8e, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x05, 0x11ce, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x06, 0x1a2c, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x06, 0x1a7c, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x06, 0x1ab1, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x06, 0x2214, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x06, 0x223e, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x02f8, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x04bf, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x057f, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x07, 0x0797, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x0856, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x07, 0x0a21, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x0a33, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x0a58, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x0a81, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x0acf, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x0af9, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x0b31, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x07, 0x0bcc, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x0c23, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x0c3c, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x0c60, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x07, 0x0d73, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x07, 0x1549, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x155d, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x159f, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x18e6, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x19ce, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + #rom.patch(0x15, 0x3F23, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x18, 0x0966, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x18, 0x0972, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x18, 0x09f3, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x18, 0x0bf1, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x18, 0x0c2c, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x18, 0x0c6d, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x18, 0x0c8b, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x18, 0x0ce4, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x18, 0x0d3c, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x18, 0x0d4a, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x18, 0x0d95, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x18, 0x0da3, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x18, 0x0de4, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x18, 0x0e7a, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x18, 0x0e91, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x18, 0x0eb6, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x18, 0x219e, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x19, 0x05ec, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x19, 0x2d54, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x19, 0x2df2, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x19, 0x2ef1, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x19, 0x2f95, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x20, 0x1b04, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x20, 0x1e42, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x36, 0x0948, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x19, 0x31Ca, ASM("ld a, [$DB0F]"), ASM("ld a, [$DB41]")) + rom.patch(0x19, 0x3215, ASM("ld a, [$DB0F]"), ASM("ld a, [$DB41]")) + rom.patch(0x19, 0x32a2, ASM("ld a, [$DB0F]"), ASM("ld a, [$DB41]")) + rom.patch(0x19, 0x3700, ASM("ld [$DB0F], a"), ASM("ld [$DB41], a")) + rom.patch(0x19, 0x38b3, ASM("ld a, [$DB0F]"), ASM("ld a, [$DB41]")) + rom.patch(0x19, 0x38c3, ASM("ld [$DB0F], a"), ASM("ld [$DB41], a")) + rom.patch(0x20, 0x1a83, ASM("ld a, [$DB0F]"), ASM("ld a, [$DB41]")) + + # Fix the whole inventory rendering, this needs to extend a few tables with more entries so it moves tables + # to the end of the bank as well. + rom.patch(0x20, 0x3E53, "00" * 32, + "9C019C06" + "9C619C65" + "9CA19CA5" + "9CE19CE5" + "9D219D25" + "9D619D65" + "9DA19DA5" + "9DE19DE5") # New table with tile addresses for all slots + rom.patch(0x20, 0x1CC7, ASM("ld hl, $5C84"), ASM("ld hl, $7E53")) # use the new table + rom.patch(0x20, 0x1BCC, ASM("ld hl, $5C84"), ASM("ld hl, $7E53")) # use the new table + rom.patch(0x20, 0x1CF0, ASM("ld hl, $5C84"), ASM("ld hl, $7E53")) # use the new table + + # sprite positions for inventory cursor, new table, placed at the end of the bank + rom.patch(0x20, 0x3E90, "00" * 16, "28283838484858586868787888889898") + rom.patch(0x20, 0x22b3, ASM("ld hl, $6298"), ASM("ld hl, $7E90")) + rom.patch(0x20, 0x2298, "28284040", "08280828") # Extend the sprite X positions for the inventory table + + # Piece of power overlay positions + rom.patch(0x20, 0x233A, + "1038103010301030103010300E0E2626", + "10381030103010301030103010301030") + rom.patch(0x20, 0x3E73, "00" * 16, + "0E0E2626363646465656666676768686") + rom.patch(0x20, 0x2377, ASM("ld hl, $6346"), ASM("ld hl, $7E73")) + + # Allow selecting the 4 extra slots. + rom.patch(0x20, 0x1F33, ASM("ld a, $09"), ASM("ld a, $0D")) + rom.patch(0x20, 0x1F54, ASM("ld a, $09"), ASM("ld a, $0D")) + rom.patch(0x20, 0x1F2A, ASM("cp $0A"), ASM("cp $0E")) + rom.patch(0x20, 0x1F4B, ASM("cp $0A"), ASM("cp $0E")) + rom.patch(0x02, 0x217E, ASM("ld a, $0B"), ASM("ld a, $0F")) + + # Patch all the locations that iterate over inventory to check the extra slots + rom.patch(0x02, 0x33FC, ASM("cp $0C"), ASM("cp $10")) + rom.patch(0x03, 0x2475, ASM("ld e, $0C"), ASM("ld e, $10")) + rom.patch(0x03, 0x248a, ASM("cp $0C"), ASM("cp $10")) + rom.patch(0x04, 0x3849, ASM("ld c, $0B"), ASM("ld c, $0F")) + rom.patch(0x04, 0x3862, ASM("ld c, $0B"), ASM("ld c, $0F")) + rom.patch(0x04, 0x39C2, ASM("ld d, $0C"), ASM("ld d, $10")) + rom.patch(0x04, 0x39E0, ASM("ld d, $0C"), ASM("ld d, $10")) + rom.patch(0x04, 0x39FE, ASM("ld d, $0C"), ASM("ld d, $10")) + rom.patch(0x05, 0x0F95, ASM("ld e, $0B"), ASM("ld e, $0F")) + rom.patch(0x05, 0x0FD1, ASM("ld c, $0B"), ASM("ld c, $0F")) + rom.patch(0x05, 0x1324, ASM("ld e, $0C"), ASM("ld e, $10")) + rom.patch(0x05, 0x1339, ASM("cp $0C"), ASM("cp $10")) + rom.patch(0x18, 0x005A, ASM("ld e, $0B"), ASM("ld e, $0F")) + rom.patch(0x18, 0x0571, ASM("ld e, $0B"), ASM("ld e, $0F")) + rom.patch(0x19, 0x0703, ASM("cp $0C"), ASM("cp $10")) + rom.patch(0x20, 0x235C, ASM("ld d, $0C"), ASM("ld d, $10")) + rom.patch(0x36, 0x31B8, ASM("ld e, $0C"), ASM("ld e, $10")) + + ## Patch the toadstool as a different item + rom.patch(0x20, 0x1C84, "9C019C" "069C61", "4C7F7F" "4D7F7F") # Which tiles are used for the toadstool + rom.patch(0x20, 0x1C8A, "9C659C" "C19CC5", "90927F" "91937F") # Which tiles are used for the rooster + rom.patch(0x20, 0x1C6C, "927F7F" "937F7F", "127F7F" "137F7F") # Which tiles are used for the feather (to make space for rooster) + rom.patch(0x20, 0x1C66, "907F7F" "917F7F", "107F7F" "117F7F") # Which tiles are used for the ocarina (to make space for rooster) + + # Move the inventory tile numbers to a higher address, so there is space for the table above it. + rom.banks[0x20][0x1C34:0x1C94] = rom.banks[0x20][0x1C30:0x1C90] + rom.patch(0x20, 0x1CDB, ASM("ld hl, $5C30"), ASM("ld hl, $5C34")) + rom.patch(0x20, 0x1D0D, ASM("ld hl, $5C33"), ASM("ld hl, $5C37")) + rom.patch(0x20, 0x1C30, "7F7F", "0A0B") # Toadstool tile attributes + rom.patch(0x20, 0x1C32, "7F7F", "0101") # Rooster tile attributes + rom.patch(0x20, 0x1C28, "0303", "0B0B") # Feather tile attributes (due to rooster) + rom.patch(0x20, 0x1C26, "0202", "0A0A") # Ocarina tile attributes (due to rooster) + + # Allow usage of the toadstool (replace the whole manual jump table with an rst 0 jumptable + rom.patch(0x00, 0x129D, 0x12D8, ASM(""" + rst 0 ; jump table + dw $12ED ; no item + dw $1528 ; Sword + dw $135A ; Bomb + dw $1382 ; Bracelet + dw $12EE ; Shield + dw $13BD ; Bow + dw $1319 ; Hookshot + dw $12D8 ; Magic rod + dw $12ED ; Boots (no action) + dw $41FC ; Ocarina + dw $14CB ; Feather + dw $12F8 ; Shovel + dw $148D ; Magic powder + dw $1383 ; Boomerang + dw $1498 ; Toadstool + dw RoosterUse ; Rooster +RoosterUse: + ld a, $01 + ld [$DB7B], a ; has rooster + call $3958 ; spawn followers + xor a + ld [$DB7B], a ; has rooster + ret + """, 0x129D), fill_nop=True) + # Fix the graphics of the toadstool hold over your head + rom.patch(0x02, 0x121E, ASM("ld e, $8E"), ASM("ld e, $4C")) + rom.patch(0x02, 0x1241, ASM("ld a, $14"), ASM("ld a, $1C")) + + # Do not remove powder when it is used up. + rom.patch(0x20, 0x0C59, ASM("jr nz, $12"), ASM("jr $12")) + + # Patch the toadstool entity code to give the proper item, and not set the has-toadstool flag. + rom.patch(0x03, 0x1D6F, ASM(""" + ld a, $0A + ldh [$A5], a + ld d, $0C + call $6472 + ld a, $01 + ld [$DB4B], a + """), ASM(""" + ld d, $0E + call $6472 + """), fill_nop=True) + + # Patch the debug save game so it does not give a bunch of swords + rom.patch(0x01, 0x0673, "01010100", "0D0E0F00") + + # Patch the witch to use the new toadstool instead of the old flag + rom.patch(0x05, 0x081A, ASM("ld a, [$DB4B]"), ASM("ld a, $01"), fill_nop=True) + rom.patch(0x05, 0x082A, ASM("cp $0C"), ASM("cp $0E")) + rom.patch(0x05, 0x083E, ASM("cp $0C"), ASM("cp $0E")) + + +def advancedInventorySubscreen(rom): + # Instrument positions + rom.patch(0x01, 0x2BCF, + "0F51B1EFECAA4A0C", + "090C0F12494C4F52") + + be = BackgroundEditor(rom, 2) + be.tiles[0x9DA9] = 0x4A + be.tiles[0x9DC9] = 0x4B + for x in range(1, 10): + be.tiles[0x9DE9 + x] = 0xB0 + (x % 9) + be.tiles[0x9DE9] = 0xBA + be.store(rom) + be = BackgroundEditor(rom, 2, attributes=True) + + # Remove all attributes out of range. + for y in range(0x9C00, 0x9E40, 0x20): + for x in range(0x14, 0x20): + del be.tiles[x + y] + for n in range(0x9E40, 0xA020): + del be.tiles[n] + + # Remove palette of instruments + for y in range(0x9D00, 0x9E20, 0x20): + for x in range(0x00, 0x14): + be.tiles[x + y] = 0x01 + # And place it at the proper location + for y in range(0x9D00, 0x9D80, 0x20): + for x in range(0x09, 0x14): + be.tiles[x + y] = 0x07 + + # Key from 2nd vram bank + be.tiles[0x9DA9] = 0x09 + be.tiles[0x9DC9] = 0x09 + # Nightmare heads from 2nd vram bank with proper palette + for n in range(1, 10): + be.tiles[0x9DA9 + n] = 0x0E + + be.store(rom) + + rom.patch(0x20, 0x19D3, ASM("ld bc, $5994\nld e, $33"), ASM("ld bc, $7E08\nld e, $%02x" % (0x33 + 24))) + rom.banks[0x20][0x3E08:0x3E08+0x33] = rom.banks[0x20][0x1994:0x1994+0x33] + rom.patch(0x20, 0x3E08+0x32, "00" * 25, "9DAA08464646464646464646" "9DCA08B0B0B0B0B0B0B0B0B0" "00") + + # instead of doing an GBC specific check, jump to our custom handling + rom.patch(0x20, 0x19DE, ASM("ldh a, [$FE]\nand a\njr z, $40"), ASM("call $7F00"), fill_nop=True) + + rom.patch(0x20, 0x3F00, "00" * 0x100, ASM(""" + ld a, [$DBA5] ; isIndoor + and a + jr z, RenderKeysCounts + ldh a, [$F7] ; mapNr + cp $FF + jr z, RenderDungeonFix + cp $06 + jr z, D7RenderDungeonFix + cp $08 + jr c, RenderDungeonFix + +RenderKeysCounts: + ; Check if we have each nightmare key, and else null out the rendered tile + ld hl, $D636 + ld de, $DB19 + ld c, $08 +NKeyLoop: + ld a, [de] + and a + jr nz, .hasNKey + ld a, $7F + ld [hl], a +.hasNKey: + inc hl + inc de + inc de + inc de + inc de + inc de + dec c + jr nz, NKeyLoop + + ld a, [$DDDD] + and a + jr nz, .hasCNKey + ld a, $7F + ld [hl], a +.hasCNKey: + + ; Check the small key count for each dungeon and increase the tile to match the number + ld hl, $D642 + ld de, $DB1A + ld c, $08 +KeyLoop: + ld a, [de] + add a, $B0 + ld [hl], a + inc hl + inc de + inc de + inc de + inc de + inc de + dec c + jr nz, KeyLoop + + ld a, [$DDDE] + add a, $B0 + ld [hl], a + ret + +D7RenderDungeonFix: + ld de, D7DungeonFix + ld c, $11 + jr RenderDungeonFixGo + +RenderDungeonFix: + ld de, DungeonFix + ld c, $0D +RenderDungeonFixGo: + ld hl, $D633 +.copyLoop: + ld a, [de] + inc de + ldi [hl], a + dec c + jr nz, .copyLoop + ret + +DungeonFix: + db $9D, $09, $C7, $7F + db $9D, $0A, $C7, $7F + db $9D, $13, $C3, $7F + db $00 +D7DungeonFix: + db $9D, $09, $C7, $7F + db $9D, $0A, $C7, $7F + db $9D, $6B, $48, $7F + db $9D, $0F, $C7, $7F + db $00 + + """, 0x7F00), fill_nop=True) diff --git a/worlds/ladx/LADXR/patches/madBatter.py b/worlds/ladx/LADXR/patches/madBatter.py new file mode 100644 index 0000000000..601c5ac51e --- /dev/null +++ b/worlds/ladx/LADXR/patches/madBatter.py @@ -0,0 +1,42 @@ +from ..assembler import ASM +from ..utils import formatText + + +def upgradeMadBatter(rom): + # Normally the madbatter won't do anything if you have full capacity. Remove that check. + rom.patch(0x18, 0x0F05, 0x0F1D, "", fill_nop=True) + # Remove the code that finds which upgrade to apply, + rom.patch(0x18, 0x0F9E, 0x0FC4, "", fill_nop=True) + rom.patch(0x18, 0x0FD2, 0x0FD8, "", fill_nop=True) + + # Finally, at the last step, give the item and the item message. + rom.patch(0x18, 0x1016, 0x101B, "", fill_nop=True) + rom.patch(0x18, 0x101E, 0x1051, ASM(""" + ; Mad batter rooms are E0,E1 and E2, load the item type from a table in the rom + ; which only has 3 entries, and store it where bank 3E wants it. + ldh a, [$F6] ; current room + and $0F + ld d, $00 + ld e, a + ld hl, $4F90 + add hl, de + ld a, [hl] + ldh [$F1], a + + ; Give item + ld a, $06 ; giveItemMultiworld + rst 8 + ; Message + ld a, $0A ; showMessageMultiworld + rst 8 + ; Force the dialog at the bottom + ld a, [$C19F] + or $80 + ld [$C19F], a + """), fill_nop=True) + # Setup the default items + rom.patch(0x18, 0x0F90, "406060", "848586") + + rom.texts[0xE2] = formatText("You can now carry more Magic Powder!") + rom.texts[0xE3] = formatText("You can now carry more Bombs!") + rom.texts[0xE4] = formatText("You can now carry more Arrows!") diff --git a/worlds/ladx/LADXR/patches/maptweaks.py b/worlds/ladx/LADXR/patches/maptweaks.py new file mode 100644 index 0000000000..c25dd83dca --- /dev/null +++ b/worlds/ladx/LADXR/patches/maptweaks.py @@ -0,0 +1,27 @@ +from ..roomEditor import RoomEditor, ObjectWarp, ObjectVertical + + +def tweakMap(rom): + # 5 holes at the castle, reduces to 3 + re = RoomEditor(rom, 0x078) + re.objects[-1].count = 3 + re.overlay[7 + 6 * 10] = re.overlay[9 + 6 * 10] + re.overlay[8 + 6 * 10] = re.overlay[9 + 6 * 10] + re.store(rom) + + +def addBetaRoom(rom): + re = RoomEditor(rom, 0x1FC) + re.objects[-1].target_y -= 0x10 + re.store(rom) + re = RoomEditor(rom, 0x038) + re.changeObject(5, 1, 0xE1) + re.removeObject(0, 0) + re.removeObject(0, 1) + re.removeObject(0, 2) + re.removeObject(6, 1) + re.objects.append(ObjectVertical(0, 0, 0x38, 3)) + re.objects.append(ObjectWarp(1, 0x1F, 0x1FC, 0x50, 0x7C)) + re.store(rom) + + rom.room_sprite_data_indoor[0x0FC] = rom.room_sprite_data_indoor[0x1A1] diff --git a/worlds/ladx/LADXR/patches/multiworld.py b/worlds/ladx/LADXR/patches/multiworld.py new file mode 100644 index 0000000000..e41dacf35b --- /dev/null +++ b/worlds/ladx/LADXR/patches/multiworld.py @@ -0,0 +1,308 @@ +from ..assembler import ASM +from ..roomEditor import RoomEditor, ObjectHorizontal, ObjectVertical, Object +from .. import entityData + + +def addMultiworldShop(rom, this_player, player_count): + # Make a copy of the shop into GrandpaUlrira house + re = RoomEditor(rom, 0x2A9) + re.objects = [ + ObjectHorizontal(1,1, 0x00, 8), + ObjectHorizontal(1,2, 0x00, 8), + ObjectHorizontal(1,3, 0xCD, 8), + Object(2, 0, 0xC7), + Object(7, 0, 0xC7), + Object(7, 7, 0xFD), + ] + re.getWarps() + re.entities = [(0, 6, 0xD4)] + for n in range(player_count): + if n != this_player: + re.entities.append((n + 1, 6, 0xD4)) + re.animation_id = 0x04 + re.floor_object = 0x0D + re.store(rom) + # Fix the tileset + rom.banks[0x20][0x2EB3 + 0x2A9 - 0x100] = rom.banks[0x20][0x2EB3 + 0x2A1 - 0x100] + + re = RoomEditor(rom, 0x0B1) + re.getWarps()[0].target_x = 128 + re.store(rom) + + # Load the shopkeeper sprites + entityData.SPRITE_DATA[0xD4] = entityData.SPRITE_DATA[0x4D] + rom.patch(0x03, 0x01CF, "00", "98") # Fix the hitbox of the ghost to be 16x16 + + # Patch Ghost to work as a multiworld shop + rom.patch(0x19, 0x1E18, 0x20B0, ASM(""" + ld a, $01 + ld [$C50A], a ; this stops link from using items + + ldh a, [$EE] ; X + cp $08 + ; Jump to other code which is placed on the old owl code. As we do not have enough space here. + jp z, shopItemsHandler + +;Draw shopkeeper + ld de, OwnerSpriteData + call $3BC0 ; render sprite pair + ldh a, [$E7] ; frame counter + swap a + and $01 + call $3B0C ; set sprite variant + + ldh a, [$F0] + and a + jr nz, checkTalkingResult + + call $7CA2 ; prevent link from moving into the sprite + call $7CF0 ; check if talking to NPC + call c, talkHandler ; talk handling + ret + +checkTalkingResult: + ld a, [$C19F] + and a + ret nz ; still taking + call $3B12 ; increase entity state + ld [hl], $00 + ld a, [$C177] ; dialog selection + and a + ret nz + jp TalkResultHandler + +OwnerSpriteData: + ;db $60, $03, $62, $03, $62, $23, $60, $23 ; down + db $64, $03, $66, $03, $66, $23, $64, $23 ; up + ;db $68, $03, $6A, $03, $6C, $03, $6E, $03 ; left + ;db $6A, $23, $68, $23, $6E, $23, $6C, $23 ; right + +shopItemsHandler: +; Render the shop items + ld h, $00 +loop: + ; First load links position to render the item at + ldh a, [$98] ; LinkX + ldh [$EE], a ; X + ldh a, [$99] ; LinkY + sub $0E + ldh [$EC], a ; Y + ; Check if this is the item we have picked up + ld a, [$C509] ; picked up item in shop + dec a + cp h + jr z, .renderCarry + + ld a, h + swap a + add a, $20 + ldh [$EE], a ; X + ld a, $30 + ldh [$EC], a ; Y +.renderCarry: + ld a, h + push hl + ldh [$F1], a ; variant + cp $03 + jr nc, .singleSprite + ld de, ItemsDualSpriteData + call $3BC0 ; render sprite pair + jr .renderDone +.singleSprite: + ld de, ItemsSingleSpriteData + call $3C77 ; render sprite +.renderDone: + + pop hl +.skipItem: + inc h + ld a, $07 + cp h + jr nz, loop + +; check if we want to pickup or drop an item + ldh a, [$CC] + and $30 ; A or B button + call nz, checkForPickup + +; check if we have an item + ld a, [$C509] ; carry item + and a + ret z + + ; Set that link has picked something up + ld a, $01 + ld [$C15C], a + call $0CAF ; reset spin attack... + + ; Check if we are trying to exit the shop and so drop our item. + ldh a, [$99] + cp $78 + ret c + xor a + ld [$C509], a + + ret + +checkForPickup: + ldh a, [$9E] ; direction + cp $02 + ret nz + ldh a, [$99] ; LinkY + cp $48 + ret nc + + ld a, $13 + ldh [$F2], a ; play SFX + + ld a, [$C509] ; picked up shop item + and a + jr nz, .drop + + ldh a, [$98] ; LinkX + sub $08 + swap a + and $07 + ld [$C509], a ; picked up shop item + ret +.drop: + xor a + ld [$C509], a + ret + +ItemsDualSpriteData: + db $60, $08, $60, $28 ; zol + db $68, $09 ; chicken (left) +ItemsSingleSpriteData: ; (first 3 entries are still dual sprites) + db $6A, $09 ; chicken (right) + db $14, $02, $14, $22 ; piece of power +;Real single sprite data starts here + db $00, $0F ; bomb + db $38, $0A ; rupees + db $20, $0C ; medicine + db $28, $0C ; heart + +;------------------------------------trying to buy something starts here +talkHandler: + ld a, [$C509] ; carry item + add a, a + ret z ; check if we have something to buy + sub $02 + + ld hl, itemNames + ld e, a + ld d, b ; b=0 + add hl, de + ld e, [hl] + inc hl + ld d, [hl] + + ld hl, wCustomMessage + call appendString + dec hl + call padString + ld de, postMessage + call appendString + dec hl + ld a, $fe + ld [hl], a + ld de, $FFEF + add hl, de + ldh a, [$EE] + swap a + and $0F + add a, $30 + ld [hl], a + ld a, $C9 + call $2385 ; open dialog + call $3B12 ; increase entity state + ret + +appendString: + ld a, [de] + inc de + and a + ret z + ldi [hl], a + jr appendString + +padString: + ld a, l + and $0F + ret z + ld a, $20 + ldi [hl], a + jr padString + +itemNames: + dw itemZol + dw itemChicken + dw itemPieceOfPower + dw itemBombs + dw itemRupees + dw itemMedicine + dw itemHealth + +postMessage: + db "For player X? Yes No ", $00 + +itemZol: + db m"Slime storm|100 {RUPEES}", $00 +itemChicken: + db m"Coccu party|50 {RUPEES}", $00 +itemPieceOfPower: + db m"Piece of Power|50 {RUPEES}", $00 +itemBombs: + db m"10 Bombs|50 {RUPEES}", $00 +itemRupees: + db m"100 {RUPEES}|200 {RUPEES}", $00 +itemMedicine: + db m"Medicine|100 {RUPEES}", $00 +itemHealth: + db m"Health refill|10 {RUPEES}", $00 + +TalkResultHandler: + ld hl, ItemPriceTableBCD + ld a, [$C509] + dec a + add a, a + ld c, a ; b=0 + add hl, bc + ldi a, [hl] + ld d, [hl] + ld e, a + ld a, [$DB5D] + cp d + ret c + jr nz, .highEnough + ld a, [$DB5E] + cp e + ret c +.highEnough: + ; Got enough money, take it. + ld hl, ItemPriceTableDEC + ld a, [$C509] + dec a + ld c, a ; b=0 + add hl, bc + ld a, [hl] + ld [$DB92], a ; set substract buffer + + ; Set the item to send + ld hl, $DDFE + ld a, [$C509] ; currently picked up item + ldi [hl], a + ldh a, [$EE] ; X position of NPC + ldi [hl], a + ld hl, $DDF7 + set 2, [hl] + + ; No longer picked up item + xor a + ld [$C509], a + ret + +ItemPriceTableBCD: + dw $0100, $0050, $0050, $0050, $0200, $0100, $0010 +ItemPriceTableDEC: + db $64, $32, $32, $32, $C8, $64, $0A + """, 0x5E18), fill_nop=True) diff --git a/worlds/ladx/LADXR/patches/music.py b/worlds/ladx/LADXR/patches/music.py new file mode 100644 index 0000000000..f7478a80c5 --- /dev/null +++ b/worlds/ladx/LADXR/patches/music.py @@ -0,0 +1,27 @@ +from ..assembler import ASM + + +_LOOPING_MUSIC = (1, 2, 3, 4, 5, 6, 7, 8, 9, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1C, 0x1D, 0x1F, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x2F, 0x31, 0x32, 0x33, 0x37, + 0x39, 0x3A, 0x3C, 0x3E, 0x40, 0x48, 0x49, 0x4A, 0x4B, 0x4E, 0x50, 0x53, 0x54, 0x55, 0x57, 0x58, 0x59, + 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, 0x60, 0x61) + + +def randomizeMusic(rom, rnd): + # Randomize overworld + for x in range(0, 16, 2): + for y in range(0, 16, 2): + idx = x + y * 16 + result = rnd.choice(_LOOPING_MUSIC) + rom.banks[0x02][idx] = result + rom.banks[0x02][idx+1] = result + rom.banks[0x02][idx+16] = result + rom.banks[0x02][idx+17] = result + # Random music in dungeons/caves + for n in range(0x20): + rom.banks[0x02][0x100 + n] = rnd.choice(_LOOPING_MUSIC) + + +def noMusic(rom): + rom.patch(0x1B, 0x001E, ASM("ld hl, $D368\nldi a, [hl]"), ASM("xor a"), fill_nop=True) + rom.patch(0x1E, 0x001E, ASM("ld hl, $D368\nldi a, [hl]"), ASM("xor a"), fill_nop=True) diff --git a/worlds/ladx/LADXR/patches/nyan.bin b/worlds/ladx/LADXR/patches/nyan.bin new file mode 100644 index 0000000000..65f1772e7d Binary files /dev/null and b/worlds/ladx/LADXR/patches/nyan.bin differ diff --git a/worlds/ladx/LADXR/patches/overworld.py b/worlds/ladx/LADXR/patches/overworld.py new file mode 100644 index 0000000000..04668efca0 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld.py @@ -0,0 +1,224 @@ +from ..assembler import ASM +from ..roomEditor import RoomEditor, ObjectWarp, Object, WARP_TYPE_IDS +from .. import entityData +import os +import json + + +def patchOverworldTilesets(rom): + rom.patch(0x00, 0x0D5B, 0x0D79, ASM(""" + ; Instead of loading tileset info from a small 8x8 table, load it from a 16x16 table to give + ; full control. + ; A=MapRoom + ld hl, $2100 + ld [hl], $3F + ld d, $00 + ld e, a + ld hl, $7F00 + add hl, de + ldh a, [$94] ; We need to load the currently loaded tileset in E to compare it + ld e, a + ld a, [hl] + ld hl, $2100 + ld [hl], $20 + """), fill_nop=True) + # Remove the camera shop exception + rom.patch(0x00, 0x0D80, 0x0D8B, "", fill_nop=True) + + for x in range(16): + for y in range(16): + rom.banks[0x3F][0x3F00+x+y*16] = rom.banks[0x20][0x2E73 + (x // 2) + (y // 2) * 8] + rom.banks[0x3F][0x3F07] = rom.banks[0x3F][0x3F08] # Fix the room next to the egg + rom.banks[0x3F][0x3F17] = rom.banks[0x3F][0x3F08] # Fix the room next to the egg + rom.banks[0x3F][0x3F3A] = 0x0F # room below mambo cave + rom.banks[0x3F][0x3F3B] = 0x0F # room below D4 + rom.banks[0x3F][0x3F4B] = 0x0F # room next to castle + rom.banks[0x3F][0x3F5B] = 0x0F # room next to castle + # Fix the rooms around the camera shop + rom.banks[0x3F][0x3F26] = 0x0F + rom.banks[0x3F][0x3F27] = 0x0F + rom.banks[0x3F][0x3F36] = 0x0F + + +def createDungeonOnlyOverworld(rom): + # Skip the whole egg maze. + rom.patch(0x14, 0x0453, "75", "73") + + instrument_rooms = [0x102, 0x12A, 0x159, 0x162, 0x182, 0x1B5, 0x22C, 0x230, 0x301] + path = os.path.dirname(__file__) + + # Start with clearing all the maps, because this just generates a bunch of room in the rom. + for n in range(0x100): + re = RoomEditor(rom, n) + re.entities = [] + re.objects = [] + if os.path.exists("%s/overworld/dive/%02X.json" % (path, n)): + re.loadFromJson("%s/overworld/dive/%02X.json" % (path, n)) + entrances = list(filter(lambda obj: obj.type_id in WARP_TYPE_IDS, re.objects)) + for obj in re.objects: + if isinstance(obj, ObjectWarp) and entrances: + e = entrances.pop(0) + + other = RoomEditor(rom, obj.room) + for o in other.objects: + if isinstance(o, ObjectWarp) and o.warp_type == 0: + o.room = n + o.target_x = e.x * 16 + 8 + o.target_y = e.y * 16 + 16 + other.store(rom) + + if obj.room == 0x1F5: + # Patch the boomang guy exit + other = RoomEditor(rom, "Alt1F5") + other.getWarps()[0].room = n + other.getWarps()[0].target_x = e.x * 16 + 8 + other.getWarps()[0].target_y = e.y * 16 + 16 + other.store(rom) + + if obj.warp_type == 1 and (obj.map_nr < 8 or obj.map_nr == 0xFF) and obj.room not in (0x1B0, 0x23A, 0x23D): + other = RoomEditor(rom, instrument_rooms[min(8, obj.map_nr)]) + for o in other.objects: + if isinstance(o, ObjectWarp) and o.warp_type == 0: + o.room = n + o.target_x = e.x * 16 + 8 + o.target_y = e.y * 16 + 16 + other.store(rom) + re.store(rom) + + +def exportOverworld(rom): + import PIL.Image + + path = os.path.dirname(__file__) + for room_index in list(range(0x100)) + ["Alt06", "Alt0E", "Alt1B", "Alt2B", "Alt79", "Alt8C"]: + room = RoomEditor(rom, room_index) + if isinstance(room_index, int): + room_nr = room_index + else: + room_nr = int(room_index[3:], 16) + tileset_index = rom.banks[0x3F][0x3F00 + room_nr] + attributedata_bank = rom.banks[0x1A][0x2476 + room_nr] + attributedata_addr = rom.banks[0x1A][0x1E76 + room_nr * 2] + attributedata_addr |= rom.banks[0x1A][0x1E76 + room_nr * 2 + 1] << 8 + attributedata_addr -= 0x4000 + + metatile_info = rom.banks[0x1A][0x2B1D:0x2B1D + 0x400] + attrtile_info = rom.banks[attributedata_bank][attributedata_addr:attributedata_addr+0x400] + + palette_index = rom.banks[0x21][0x02EF + room_nr] + palette_addr = rom.banks[0x21][0x02B1 + palette_index * 2] + palette_addr |= rom.banks[0x21][0x02B1 + palette_index * 2 + 1] << 8 + palette_addr -= 0x4000 + + hidden_warp_tiles = [] + for obj in room.objects: + if obj.type_id in WARP_TYPE_IDS and room.overlay[obj.x + obj.y * 10] != obj.type_id: + if obj.type_id != 0xE1 or room.overlay[obj.x + obj.y * 10] != 0x53: # Ignore the waterfall 'caves' + hidden_warp_tiles.append(obj) + if obj.type_id == 0xC5 and room_nr < 0x100 and room.overlay[obj.x + obj.y * 10] == 0xC4: + # Pushable gravestones have the wrong overlay by default + room.overlay[obj.x + obj.y * 10] = 0xC5 + if obj.type_id == 0xDC and room_nr < 0x100: + # Flowers above the rooster windmill need a different tile + hidden_warp_tiles.append(obj) + + image_filename = "tiles_%02x_%02x_%02x_%02x_%04x.png" % (tileset_index, room.animation_id, palette_index, attributedata_bank, attributedata_addr) + data = { + "width": 10, "height": 8, + "type": "map", "renderorder": "right-down", "tiledversion": "1.4.3", "version": 1.4, + "tilewidth": 16, "tileheight": 16, "orientation": "orthogonal", + "tilesets": [ + { + "columns": 16, "firstgid": 1, + "image": image_filename, "imageheight": 256, "imagewidth": 256, + "margin": 0, "name": "main", "spacing": 0, + "tilecount": 256, "tileheight": 16, "tilewidth": 16 + } + ], + "layers": [{ + "data": [n+1 for n in room.overlay], + "width": 10, "height": 8, + "id": 1, "name": "Tiles", "type": "tilelayer", "visible": True, "opacity": 1, "x": 0, "y": 0, + }, { + "id": 2, "name": "EntityLayer", "type": "objectgroup", "visible": True, "opacity": 1, "x": 0, "y": 0, + "objects": [ + {"width": 16, "height": 16, "x": entity[0] * 16, "y": entity[1] * 16, "name": entityData.NAME[entity[2]], "type": "entity"} for entity in room.entities + ] + [ + {"width": 8, "height": 8, "x": 0, "y": idx * 8, "name": "%x:%02x:%03x:%02x:%02x" % (obj.warp_type, obj.map_nr, obj.room, obj.target_x, obj.target_y), "type": "warp"} for idx, obj in enumerate(room.getWarps()) if isinstance(obj, ObjectWarp) + ] + [ + {"width": 16, "height": 16, "x": obj.x * 16, "y": obj.y * 16, "name": "%02X" % (obj.type_id), "type": "hidden_tile"} for obj in hidden_warp_tiles + ], + }], + "properties": [ + {"name": "tileset", "type": "string", "value": "%02X" % (tileset_index)}, + {"name": "animationset", "type": "string", "value": "%02X" % (room.animation_id)}, + {"name": "attribset", "type": "string", "value": "%02X:%04X" % (attributedata_bank, attributedata_addr)}, + {"name": "palette", "type": "string", "value": "%02X" % (palette_index)}, + ] + } + if isinstance(room_index, str): + json.dump(data, open("%s/overworld/export/%s.json" % (path, room_index), "wt")) + else: + json.dump(data, open("%s/overworld/export/%02X.json" % (path, room_index), "wt")) + + if not os.path.exists("%s/overworld/export/%s" % (path, image_filename)): + tilemap = rom.banks[0x2F][tileset_index*0x100:tileset_index*0x100+0x200] + tilemap += rom.banks[0x2C][0x1200:0x1800] + tilemap += rom.banks[0x2C][0x0800:0x1000] + anim_addr = {2: 0x2B00, 3: 0x2C00, 4: 0x2D00, 5: 0x2E00, 6: 0x2F00, 7: 0x2D00, 8: 0x3000, 9: 0x3100, 10: 0x3200, 11: 0x2A00, 12: 0x3300, 13: 0x3500, 14: 0x3600, 15: 0x3400, 16: 0x3700}.get(room.animation_id, 0x0000) + tilemap[0x6C0:0x700] = rom.banks[0x2C][anim_addr:anim_addr + 0x40] + + palette = [] + for n in range(8*4): + p0 = rom.banks[0x21][palette_addr] + p1 = rom.banks[0x21][palette_addr + 1] + pal = p0 | p1 << 8 + palette_addr += 2 + r = (pal & 0x1F) << 3 + g = ((pal >> 5) & 0x1F) << 3 + b = ((pal >> 10) & 0x1F) << 3 + palette += [r, g, b] + + img = PIL.Image.new("P", (16*16, 16*16)) + img.putpalette(palette) + def drawTile(x, y, index, attr): + for py in range(8): + a = tilemap[index * 16 + py * 2] + b = tilemap[index * 16 + py * 2 + 1] + if attr & 0x40: + a = tilemap[index * 16 + 14 - py * 2] + b = tilemap[index * 16 + 15 - py * 2] + for px in range(8): + bit = 0x80 >> px + if attr & 0x20: + bit = 0x01 << px + c = (attr & 7) << 2 + if a & bit: + c |= 1 + if b & bit: + c |= 2 + img.putpixel((x+px, y+py), c) + for x in range(16): + for y in range(16): + idx = x+y*16 + metatiles = metatile_info[idx*4:idx*4+4] + attrtiles = attrtile_info[idx*4:idx*4+4] + drawTile(x * 16 + 0, y * 16 + 0, metatiles[0], attrtiles[0]) + drawTile(x * 16 + 8, y * 16 + 0, metatiles[1], attrtiles[1]) + drawTile(x * 16 + 0, y * 16 + 8, metatiles[2], attrtiles[2]) + drawTile(x * 16 + 8, y * 16 + 8, metatiles[3], attrtiles[3]) + img.save("%s/overworld/export/%s" % (path, image_filename)) + + world = { + "maps": [ + {"fileName": "%02X.json" % (n), "height": 128, "width": 160, "x": (n & 0x0F) * 160, "y": (n >> 4) * 128} + for n in range(0x100) + ], + "onlyShowAdjacentMaps": False, + "type": "world" + } + json.dump(world, open("%s/overworld/export/world.world" % (path), "wt")) + + +def isNormalOverworld(rom): + return len(RoomEditor(rom, 0x010).getWarps()) > 0 diff --git a/worlds/ladx/LADXR/patches/overworld/dive/00.json b/worlds/ladx/LADXR/patches/overworld/dive/00.json new file mode 100644 index 0000000000..fd16fa6675 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/00.json @@ -0,0 +1,124 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 125, 126, 1, 129, 78, 78, 78, 130, 1, 125, 240, 240, 240, 56, 11, 11, 11, 57, 240, 240, 2, 2, 30, 47, 73, 225, 74, 79, 94, 2, 2, 2, 56, 58, 226, 225, 59, 60, 57, 2, 2, 2, 56, 10, 10, 10, 10, 10, 123, 123, 2, 2, 56, 10, 10, 10, 10, 10, 57, 2, 2, 2, 47, 48, 48, 48, 48, 48, 79, 2], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":16, + "id":1, + "name":"HEART_PIECE", + "rotation":0, + "type":"entity", + "visible":true, + "width":16, + "x":64, + "y":32 + }, + { + "height":16, + "id":2, + "name":"CROW", + "rotation":0, + "type":"entity", + "visible":true, + "width":16, + "x":96, + "y":32 + }, + { + "height":16, + "id":3, + "name":"MINI_MOLDORM", + "rotation":0, + "type":"entity", + "visible":true, + "width":16, + "x":112, + "y":96 + }, + { + "height":8, + "id":4, + "name":"1:07:23a:58:10", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":5, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"0B" + }, + { + "name":"attribset", + "type":"string", + "value":"25:3400" + }, + { + "name":"palette", + "type":"string", + "value":"0F" + }, + { + "name":"tileset", + "type":"string", + "value":"1C" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_1c_0b_0f_25_3400.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/01.json b/worlds/ladx/LADXR/patches/overworld/dive/01.json new file mode 100644 index 0000000000..0441d875a1 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/01.json @@ -0,0 +1,102 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[125, 126, 1, 1, 1, 1, 1, 1, 1, 125, 29, 29, 126, 1, 1, 129, 78, 130, 125, 29, 240, 240, 240, 240, 240, 56, 4, 57, 240, 240, 2, 2, 30, 81, 81, 47, 48, 79, 94, 2, 2, 2, 56, 4, 4, 206, 226, 216, 57, 2, 123, 123, 123, 11, 4, 4, 4, 4, 57, 2, 2, 2, 56, 11, 11, 11, 11, 11, 57, 2, 2, 2, 47, 48, 48, 48, 48, 48, 79, 2], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":16, + "id":1, + "name":"CROW", + "rotation":0, + "type":"entity", + "visible":true, + "width":16, + "x":32, + "y":80 + }, + { + "height":8, + "id":2, + "name":"1:07:23d:58:10", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":3, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"0B" + }, + { + "name":"attribset", + "type":"string", + "value":"25:3400" + }, + { + "name":"palette", + "type":"string", + "value":"0F" + }, + { + "name":"tileset", + "type":"string", + "value":"1C" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_1c_0b_0f_25_3400.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/06.json b/worlds/ladx/LADXR/patches/overworld/dive/06.json new file mode 100644 index 0000000000..405a7aa73c --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/06.json @@ -0,0 +1,113 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[1, 1, 1, 1, 6, 7, 8, 1, 1, 1, 125, 126, 1, 129, 100, 101, 102, 130, 125, 126, 240, 240, 240, 56, 114, 29, 128, 57, 240, 240, 230, 230, 30, 56, 170, 171, 192, 57, 94, 230, 230, 230, 56, 47, 73, 225, 74, 79, 57, 230, 230, 230, 56, 63, 59, 225, 59, 64, 57, 230, 230, 30, 47, 48, 73, 225, 74, 48, 79, 94, 230, 56, 63, 59, 59, 225, 59, 59, 64, 57], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":16, + "id":1, + "name":"EGG_SONG_EVENT", + "rotation":0, + "type":"entity", + "visible":true, + "width":16, + "x":0, + "y":0 + }, + { + "height":8, + "id":2, + "name":"1:08:270:50:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }, + { + "height":16, + "id":3, + "name":"E1", + "rotation":0, + "type":"hidden_tile", + "visible":true, + "width":16, + "x":80, + "y":48 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":4, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"0B" + }, + { + "name":"attribset", + "type":"string", + "value":"27:1620" + }, + { + "name":"palette", + "type":"string", + "value":"13" + }, + { + "name":"tileset", + "type":"string", + "value":"3C" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_3c_0b_13_27_1620.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/16.json b/worlds/ladx/LADXR/patches/overworld/dive/16.json new file mode 100644 index 0000000000..25538084ca --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/16.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[30, 47, 48, 48, 73, 225, 48, 48, 48, 79, 56, 63, 59, 59, 59, 225, 59, 59, 59, 64, 56, 58, 59, 59, 59, 225, 59, 59, 59, 60, 47, 48, 48, 48, 73, 225, 74, 48, 48, 48, 58, 59, 226, 59, 59, 225, 59, 59, 59, 59, 201, 213, 4, 4, 4, 4, 4, 4, 4, 201, 201, 4, 4, 4, 4, 4, 4, 4, 4, 201, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"0:00:082:48:30", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"0B" + }, + { + "name":"attribset", + "type":"string", + "value":"27:1620" + }, + { + "name":"palette", + "type":"string", + "value":"13" + }, + { + "name":"tileset", + "type":"string", + "value":"3C" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_3c_0b_13_27_1620.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/62.json b/worlds/ladx/LADXR/patches/overworld/dive/62.json new file mode 100644 index 0000000000..26d6c9de6c --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/62.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[2, 2, 2, 115, 117, 117, 117, 116, 2, 2, 2, 2, 2, 115, 118, 215, 119, 116, 2, 2, 2, 2, 30, 115, 117, 226, 117, 116, 94, 2, 2, 2, 56, 183, 117, 120, 117, 184, 57, 2, 2, 2, 56, 4, 4, 4, 4, 4, 57, 2, 2, 2, 56, 4, 4, 4, 4, 4, 57, 2, 2, 2, 47, 48, 73, 225, 74, 48, 79, 2, 2, 2, 63, 59, 59, 225, 59, 59, 64, 2], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"1:06:20e:50:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"00" + }, + { + "name":"attribset", + "type":"string", + "value":"27:1E40" + }, + { + "name":"palette", + "type":"string", + "value":"16" + }, + { + "name":"tileset", + "type":"string", + "value":"30" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_30_00_16_27_1e40.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/6C.json b/worlds/ladx/LADXR/patches/overworld/dive/6C.json new file mode 100644 index 0000000000..79b3f96174 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/6C.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 44, 45, 45, 46, 15, 15, 15, 15, 15, 15, 56, 161, 199, 57, 15, 15, 15, 15, 15, 15, 56, 5, 5, 57, 15, 15, 15, 15, 15, 15, 52, 48, 48, 53, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"1:05:1b0:78:10", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"03" + }, + { + "name":"attribset", + "type":"string", + "value":"22:0000" + }, + { + "name":"palette", + "type":"string", + "value":"16" + }, + { + "name":"tileset", + "type":"string", + "value":"0F" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_0f_03_16_22_0000.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/71.json b/worlds/ladx/LADXR/patches/overworld/dive/71.json new file mode 100644 index 0000000000..52910db6c4 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/71.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[3, 56, 63, 59, 59, 59, 59, 59, 64, 48, 3, 56, 58, 183, 59, 226, 59, 183, 60, 10, 3, 56, 33, 184, 10, 10, 10, 184, 10, 10, 3, 56, 33, 10, 10, 10, 10, 10, 10, 4, 3, 56, 201, 10, 10, 10, 10, 10, 4, 4, 3, 56, 201, 201, 10, 10, 10, 10, 10, 4, 3, 47, 48, 48, 48, 48, 48, 48, 48, 48, 3, 63, 59, 59, 59, 59, 59, 59, 59, 59], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"1:07:25d:50:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"00" + }, + { + "name":"attribset", + "type":"string", + "value":"25:3400" + }, + { + "name":"palette", + "type":"string", + "value":"19" + }, + { + "name":"tileset", + "type":"string", + "value":"1C" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_1c_00_19_25_3400.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/72.json b/worlds/ladx/LADXR/patches/overworld/dive/72.json new file mode 100644 index 0000000000..aa79f1a655 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/72.json @@ -0,0 +1,80 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[48, 54, 58, 59, 59, 225, 59, 59, 60, 46, 4, 4, 4, 4, 4, 4, 4, 4, 4, 57, 4, 4, 4, 4, 4, 93, 93, 93, 4, 77, 4, 4, 12, 12, 4, 93, 93, 93, 93, 4, 4, 4, 12, 4, 4, 4, 93, 93, 4, 4, 4, 4, 12, 4, 4, 4, 4, 4, 4, 4, 48, 73, 225, 74, 48, 73, 75, 74, 48, 48, 59, 59, 225, 59, 59, 59, 59, 59, 59, 59], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":1, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"00" + }, + { + "name":"attribset", + "type":"string", + "value":"22:0000" + }, + { + "name":"palette", + "type":"string", + "value":"16" + }, + { + "name":"tileset", + "type":"string", + "value":"0F" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_0f_00_16_22_0000.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/73.json b/worlds/ladx/LADXR/patches/overworld/dive/73.json new file mode 100644 index 0000000000..b762b0ce87 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/73.json @@ -0,0 +1,113 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[2, 2, 2, 180, 180, 180, 180, 180, 2, 2, 2, 2, 44, 180, 180, 180, 180, 180, 46, 2, 81, 81, 76, 174, 178, 232, 174, 178, 57, 2, 4, 4, 4, 175, 179, 228, 175, 179, 57, 2, 4, 4, 4, 4, 4, 4, 4, 4, 57, 2, 4, 4, 4, 4, 4, 4, 4, 4, 57, 2, 48, 48, 48, 48, 48, 48, 48, 48, 53, 2, 59, 59, 59, 59, 59, 59, 59, 59, 64, 2], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":16, + "id":1, + "name":"ARMOS_STATUE", + "rotation":0, + "type":"entity", + "visible":true, + "width":16, + "x":64, + "y":64 + }, + { + "height":16, + "id":2, + "name":"ARMOS_STATUE", + "rotation":0, + "type":"entity", + "visible":true, + "width":16, + "x":96, + "y":64 + }, + { + "height":8, + "id":3, + "name":"1:05:1d4:50:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":4, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"00" + }, + { + "name":"attribset", + "type":"string", + "value":"25:3C00" + }, + { + "name":"palette", + "type":"string", + "value":"1C" + }, + { + "name":"tileset", + "type":"string", + "value":"2A" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_2a_00_1c_25_3c00.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/81.json b/worlds/ladx/LADXR/patches/overworld/dive/81.json new file mode 100644 index 0000000000..e85673835c --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/81.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[11, 57, 63, 59, 214, 215, 216, 59, 64, 63, 11, 57, 58, 59, 206, 226, 207, 59, 60, 63, 11, 57, 15, 15, 15, 12, 15, 15, 15, 58, 11, 57, 15, 15, 15, 12, 15, 15, 15, 5, 11, 57, 15, 15, 15, 12, 15, 15, 5, 5, 55, 53, 15, 15, 15, 12, 12, 12, 5, 5, 38, 39, 10, 15, 15, 15, 15, 15, 15, 38, 40, 42, 39, 38, 39, 38, 39, 38, 39, 40], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"1:03:17a:50:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"03" + }, + { + "name":"attribset", + "type":"string", + "value":"27:2640" + }, + { + "name":"palette", + "type":"string", + "value":"01" + }, + { + "name":"tileset", + "type":"string", + "value":"34" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_34_03_01_27_2640.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/82.json b/worlds/ladx/LADXR/patches/overworld/dive/82.json new file mode 100644 index 0000000000..ad62948706 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/82.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[59, 59, 225, 59, 59, 59, 59, 59, 59, 59, 59, 59, 225, 59, 59, 59, 59, 59, 59, 59, 59, 59, 225, 59, 187, 59, 59, 59, 59, 59, 5, 5, 12, 10, 93, 10, 5, 5, 5, 5, 5, 5, 12, 5, 10, 5, 11, 11, 11, 5, 5, 5, 12, 5, 5, 11, 93, 93, 93, 11, 39, 5, 5, 5, 5, 11, 93, 93, 93, 11, 41, 5, 5, 5, 5, 5, 11, 11, 11, 62], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"0:00:016:28:50", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"00" + }, + { + "name":"attribset", + "type":"string", + "value":"25:0C00" + }, + { + "name":"palette", + "type":"string", + "value":"01" + }, + { + "name":"tileset", + "type":"string", + "value":"0F" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_0f_00_01_25_0c00.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/83.json b/worlds/ladx/LADXR/patches/overworld/dive/83.json new file mode 100644 index 0000000000..a97454979e --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/83.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[59, 64, 38, 39, 72, 59, 59, 59, 59, 60, 59, 64, 40, 41, 57, 183, 184, 103, 82, 82, 59, 60, 212, 212, 57, 104, 228, 105, 82, 203, 5, 5, 5, 5, 57, 15, 15, 15, 15, 203, 5, 5, 62, 225, 53, 15, 15, 15, 15, 203, 5, 5, 57, 15, 15, 15, 15, 15, 82, 203, 5, 33, 57, 15, 15, 15, 15, 82, 82, 203, 48, 61, 51, 45, 45, 45, 45, 45, 45, 45], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"1:04:1a1:50:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"03" + }, + { + "name":"attribset", + "type":"string", + "value":"22:2400" + }, + { + "name":"palette", + "type":"string", + "value":"09" + }, + { + "name":"tileset", + "type":"string", + "value":"3A" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_3a_03_09_22_2400.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/91.json b/worlds/ladx/LADXR/patches/overworld/dive/91.json new file mode 100644 index 0000000000..1c9fe427d3 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/91.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[43, 42, 63, 59, 59, 59, 59, 59, 64, 42, 42, 43, 63, 59, 183, 216, 183, 59, 64, 40, 43, 41, 58, 183, 184, 226, 184, 183, 60, 11, 41, 82, 82, 28, 28, 28, 28, 82, 19, 5, 82, 28, 28, 28, 28, 28, 28, 27, 23, 5, 82, 82, 28, 28, 82, 82, 28, 19, 33, 5, 82, 82, 82, 28, 28, 28, 28, 19, 33, 5, 38, 39, 38, 39, 38, 39, 38, 39, 38, 39], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"1:01:136:50:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"03" + }, + { + "name":"attribset", + "type":"string", + "value":"22:3400" + }, + { + "name":"palette", + "type":"string", + "value":"0E" + }, + { + "name":"tileset", + "type":"string", + "value":"36" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_36_03_0e_22_3400.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/92.json b/worlds/ladx/LADXR/patches/overworld/dive/92.json new file mode 100644 index 0000000000..1f78b1bebe --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/92.json @@ -0,0 +1,102 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[39, 5, 5, 5, 5, 5, 5, 5, 5, 51, 41, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 11, 11, 11, 5, 38, 70, 39, 5, 5, 5, 83, 83, 83, 11, 40, 226, 41, 5, 5, 5, 92, 227, 92, 11, 11, 93, 11, 5, 5, 5, 5, 5, 5, 11, 11, 11, 5, 10, 5, 5, 5, 5, 5, 5, 5, 5, 10, 10, 38, 39, 5, 5, 5, 5, 5, 5, 10, 111], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"1:10:2cb:50:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }, + { + "height":8, + "id":2, + "name":"1:0e:2a1:50:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":8 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":3, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"03" + }, + { + "name":"attribset", + "type":"string", + "value":"22:0000" + }, + { + "name":"palette", + "type":"string", + "value":"0E" + }, + { + "name":"tileset", + "type":"string", + "value":"0F" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_0f_03_0e_22_0000.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/93.json b/worlds/ladx/LADXR/patches/overworld/dive/93.json new file mode 100644 index 0000000000..c9ac830b6d --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/93.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[45, 50, 63, 59, 59, 59, 59, 59, 64, 63, 5, 5, 58, 107, 109, 109, 109, 107, 60, 63, 5, 5, 183, 108, 99, 228, 99, 108, 183, 63, 5, 5, 184, 18, 28, 28, 28, 19, 184, 63, 5, 5, 5, 22, 17, 17, 17, 23, 5, 63, 10, 5, 10, 183, 5, 5, 5, 183, 5, 58, 10, 10, 10, 184, 5, 5, 5, 184, 5, 38, 111, 38, 39, 38, 39, 38, 39, 38, 39, 40], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"1:02:152:50:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"03" + }, + { + "name":"attribset", + "type":"string", + "value":"22:1800" + }, + { + "name":"palette", + "type":"string", + "value":"01" + }, + { + "name":"tileset", + "type":"string", + "value":"2E" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_2e_03_01_22_1800.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/A1.json b/worlds/ladx/LADXR/patches/overworld/dive/A1.json new file mode 100644 index 0000000000..8ed9e355bb --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/A1.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[43, 42, 44, 45, 45, 45, 46, 42, 43, 42, 42, 43, 47, 48, 48, 48, 79, 40, 41, 40, 43, 41, 58, 99, 228, 99, 60, 11, 11, 5, 41, 11, 11, 10, 10, 10, 10, 10, 10, 10, 111, 11, 183, 10, 10, 10, 183, 10, 10, 10, 111, 5, 184, 10, 10, 10, 184, 10, 5, 5, 111, 5, 5, 10, 10, 10, 10, 5, 5, 5, 48, 48, 73, 75, 74, 48, 48, 48, 48, 48], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"1:00:117:50:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"00" + }, + { + "name":"attribset", + "type":"string", + "value":"22:0C00" + }, + { + "name":"palette", + "type":"string", + "value":"01" + }, + { + "name":"tileset", + "type":"string", + "value":"24" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_24_00_01_22_0c00.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/A2.json b/worlds/ladx/LADXR/patches/overworld/dive/A2.json new file mode 100644 index 0000000000..540f5c97b2 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/A2.json @@ -0,0 +1,113 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[43, 41, 5, 5, 5, 5, 5, 10, 10, 111, 41, 5, 5, 5, 5, 5, 5, 5, 5, 10, 5, 5, 38, 39, 93, 93, 93, 38, 39, 11, 10, 5, 40, 41, 83, 83, 83, 40, 41, 11, 10, 5, 11, 11, 92, 227, 92, 11, 11, 11, 5, 5, 11, 11, 11, 12, 11, 11, 11, 11, 5, 5, 5, 11, 12, 12, 5, 5, 5, 11, 61, 4, 4, 4, 12, 4, 4, 4, 4, 62], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":16, + "id":1, + "name":"BUTTERFLY", + "rotation":0, + "type":"entity", + "visible":true, + "width":16, + "x":32, + "y":48 + }, + { + "height":16, + "id":2, + "name":"BUTTERFLY", + "rotation":0, + "type":"entity", + "visible":true, + "width":16, + "x":112, + "y":80 + }, + { + "height":8, + "id":3, + "name":"1:10:2a3:50:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":4, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"00" + }, + { + "name":"attribset", + "type":"string", + "value":"22:0000" + }, + { + "name":"palette", + "type":"string", + "value":"01" + }, + { + "name":"tileset", + "type":"string", + "value":"0F" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_0f_00_01_22_0000.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/A3.json b/worlds/ladx/LADXR/patches/overworld/dive/A3.json new file mode 100644 index 0000000000..10eebc4c6f --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/A3.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[111, 82, 82, 82, 82, 82, 82, 82, 82, 82, 11, 11, 11, 5, 5, 5, 5, 11, 11, 82, 11, 183, 184, 10, 183, 201, 184, 5, 11, 82, 11, 206, 207, 10, 206, 226, 207, 5, 11, 82, 11, 5, 5, 11, 11, 11, 11, 11, 11, 82, 11, 11, 11, 5, 197, 10, 197, 11, 11, 82, 11, 5, 11, 11, 11, 11, 5, 5, 11, 82, 48, 48, 48, 48, 48, 48, 48, 48, 48, 61], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"1:ff:312:50:5c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"00" + }, + { + "name":"attribset", + "type":"string", + "value":"25:2800" + }, + { + "name":"palette", + "type":"string", + "value":"01" + }, + { + "name":"tileset", + "type":"string", + "value":"38" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_38_00_01_25_2800.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/B0.json b/worlds/ladx/LADXR/patches/overworld/dive/B0.json new file mode 100644 index 0000000000..9203a9ec03 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/B0.json @@ -0,0 +1,80 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[56, 4, 4, 4, 4, 4, 4, 4, 4, 57, 56, 183, 184, 4, 4, 4, 4, 62, 48, 53, 56, 206, 207, 4, 4, 4, 4, 57, 183, 184, 56, 161, 93, 4, 4, 4, 4, 57, 206, 207, 56, 93, 93, 62, 73, 75, 74, 79, 183, 184, 56, 93, 4, 57, 59, 59, 59, 60, 206, 207, 47, 48, 48, 79, 31, 31, 31, 31, 31, 31, 58, 59, 59, 60, 32, 32, 32, 32, 32, 32], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":1, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"02" + }, + { + "name":"attribset", + "type":"string", + "value":"22:1000" + }, + { + "name":"palette", + "type":"string", + "value":"01" + }, + { + "name":"tileset", + "type":"string", + "value":"22" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_22_02_01_22_1000.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/B1.json b/worlds/ladx/LADXR/patches/overworld/dive/B1.json new file mode 100644 index 0000000000..9ec04a19cd --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/B1.json @@ -0,0 +1,102 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[44, 46, 44, 45, 45, 45, 45, 46, 44, 46, 52, 53, 52, 48, 227, 48, 48, 53, 52, 53, 183, 184, 9, 9, 9, 9, 201, 183, 184, 9, 206, 207, 9, 9, 9, 9, 9, 206, 207, 9, 183, 184, 201, 9, 9, 9, 9, 183, 184, 9, 206, 207, 9, 9, 9, 9, 9, 206, 207, 9, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":16, + "id":1, + "name":"MARIN", + "rotation":0, + "type":"entity", + "visible":true, + "width":16, + "x":48, + "y":48 + }, + { + "height":8, + "id":2, + "name":"1:1f:1e1:88:50", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":3, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"02" + }, + { + "name":"attribset", + "type":"string", + "value":"22:1000" + }, + { + "name":"palette", + "type":"string", + "value":"01" + }, + { + "name":"tileset", + "type":"string", + "value":"22" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_22_02_01_22_1000.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/B2.json b/worlds/ladx/LADXR/patches/overworld/dive/B2.json new file mode 100644 index 0000000000..b16f6fe221 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/B2.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[56, 4, 4, 4, 12, 4, 4, 4, 4, 57, 52, 54, 4, 4, 4, 9, 9, 9, 55, 53, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 183, 184, 9, 9, 9, 9, 9, 37, 9, 9, 206, 207, 36, 9, 9, 9, 9, 9, 9, 9, 9, 36, 9, 9, 9, 9, 9, 9, 9, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":16, + "id":1, + "name":"HEART_PIECE", + "rotation":0, + "type":"entity", + "visible":true, + "width":16, + "x":80, + "y":80 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"02" + }, + { + "name":"attribset", + "type":"string", + "value":"22:1000" + }, + { + "name":"palette", + "type":"string", + "value":"01" + }, + { + "name":"tileset", + "type":"string", + "value":"22" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_22_02_01_22_1000.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/B3.json b/worlds/ladx/LADXR/patches/overworld/dive/B3.json new file mode 100644 index 0000000000..608aed75d3 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/B3.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[63, 59, 59, 63, 59, 59, 59, 64, 59, 56, 58, 59, 59, 63, 59, 59, 59, 64, 59, 56, 9, 9, 9, 58, 59, 187, 59, 60, 9, 56, 9, 9, 36, 9, 9, 36, 9, 9, 9, 56, 9, 9, 9, 9, 9, 9, 9, 9, 9, 56, 9, 9, 9, 9, 36, 9, 9, 9, 37, 56, 31, 31, 31, 31, 31, 31, 31, 31, 31, 52, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"1:1f:1f5:48:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"02" + }, + { + "name":"attribset", + "type":"string", + "value":"22:1000" + }, + { + "name":"palette", + "type":"string", + "value":"01" + }, + { + "name":"tileset", + "type":"string", + "value":"22" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_22_02_01_22_1000.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/owl.py b/worlds/ladx/LADXR/patches/owl.py new file mode 100644 index 0000000000..b22386a6cb --- /dev/null +++ b/worlds/ladx/LADXR/patches/owl.py @@ -0,0 +1,144 @@ +from ..roomEditor import RoomEditor +from ..assembler import ASM +from ..utils import formatText + + +def removeOwlEvents(rom): + # Remove all the owl events from the entity tables. + for room in range(0x100): + re = RoomEditor(rom, room) + if re.hasEntity(0x41): + re.removeEntities(0x41) + re.store(rom) + # Clear texts used by the owl. Potentially reused somewhere o else. + rom.texts[0x0D9] = b'\xff' # used by boomerang + # 1 Used by empty chest (master stalfos message) + # 8 unused (0x0C0-0x0C7) + # 1 used by bowwow in chest + # 1 used by item for other player message + # 2 used by arrow chest messages + # 2 used by tunics + for idx in range(0x0BE, 0x0CE): + rom.texts[idx] = b'\xff' + + + # Patch the owl entity into a ghost to allow refill of powder/bombs/arrows + rom.texts[0xC0] = formatText("Everybody hates me, so I give away free things in the hope people will love me. Want something?", ask="Okay No") + rom.texts[0xC1] = formatText("Good for you.") + rom.patch(0x06, 0x27F5, 0x2A77, ASM(""" + ; Check if we have powder or bombs. + ld e, INV_SIZE + ld hl, $DB00 +loop: + ldi a, [hl] + cp $02 ; bombs + jr z, hasProperItem + cp $0C ; powder + jr z, hasProperItem + cp $05 ; bow + jr z, hasProperItem + dec e + jr nz, loop + ret +hasProperItem: + + ; Render ghost + ld de, sprite + call $3BC0 + + call $64C6 ; check if game is busy (pops this stack frame if busy) + + ldh a, [$E7] ; frame counter + swap a + and $01 + call $3B0C ; set entity sprite variant + call $641A ; check collision + ldh a, [$F0] ;entity state + rst 0 + dw waitForTalk + dw talking + +waitForTalk: + call $645D ; check if talked to + ret nc + ld a, $C0 + call $2385 ; open dialog + call $3B12 ; increase entity state + ret + +talking: + ; Check if we are still talking + ld a, [$C19F] + and a + ret nz + call $3B12 ; increase entity state + ld [hl], $00 ; set to state 0 + ld a, [$C177] ; get which option we selected + and a + ret nz + + ; Give powder + ld a, [$DB4C] + cp $10 + jr nc, doNotGivePowder + ld a, $10 + ld [$DB4C], a +doNotGivePowder: + + ld a, [$DB4D] + cp $10 + jr nc, doNotGiveBombs + ld a, $10 + ld [$DB4D], a +doNotGiveBombs: + + ld a, [$DB45] + cp $10 + jr nc, doNotGiveArrows + ld a, $10 + ld [$DB45], a +doNotGiveArrows: + + ld a, $C1 + call $2385 ; open dialog + ret + +sprite: + db $76, $09, $78, $09, $7A, $09, $7C, $09 +""", 0x67F5), fill_nop=True) + rom.patch(0x20, 0x0322 + 0x41 * 2, "734A", "564B") # Remove the owl init handler + + re = RoomEditor(rom, 0x2A3) + re.entities.append((7, 6, 0x41)) + re.store(rom) + + +def upgradeDungeonOwlStatues(rom): + # Call our custom handler after the check for the stone beak + rom.patch(0x18, 0x1EA2, ASM("ldh a, [$F7]\ncp $FF\njr nz, $05"), ASM("ld a, $09\nrst 8\nret"), fill_nop=True) + +def upgradeOverworldOwlStatues(rom): + # Replace the code that handles signs/owl statues on the overworld + # This removes a "have marin with you" special case to make some room for our custom owl handling. + rom.patch(0x00, 0x201A, ASM(""" + cp $6F + jr z, $2B + cp $D4 + jr z, $27 + ld a, [$DB73] + and a + jr z, $08 + ld a, $78 + call $237C + jp $20CF + """), ASM(""" + cp $D4 + jr z, $2B + cp $6F + jr nz, skip + + ld a, $09 + rst 8 + jp $20CF +skip: + """), fill_nop=True) diff --git a/worlds/ladx/LADXR/patches/phone.py b/worlds/ladx/LADXR/patches/phone.py new file mode 100644 index 0000000000..f38745606c --- /dev/null +++ b/worlds/ladx/LADXR/patches/phone.py @@ -0,0 +1,60 @@ +from ..assembler import ASM + + +def patchPhone(rom): + rom.texts[0x141] = b"" + rom.texts[0x142] = b"" + rom.texts[0x143] = b"" + rom.texts[0x144] = b"" + rom.texts[0x145] = b"" + rom.texts[0x146] = b"" + rom.texts[0x147] = b"" + rom.texts[0x148] = b"" + rom.texts[0x149] = b"" + rom.texts[0x14A] = b"" + rom.texts[0x14B] = b"" + rom.texts[0x14C] = b"" + rom.texts[0x14D] = b"" + rom.texts[0x14E] = b"" + rom.texts[0x14F] = b"" + rom.texts[0x16E] = b"" + rom.texts[0x1FD] = b"" + rom.texts[0x228] = b"" + rom.texts[0x229] = b"" + rom.texts[0x22A] = b"" + rom.texts[0x240] = b"" + rom.texts[0x241] = b"" + rom.texts[0x242] = b"" + rom.texts[0x243] = b"" + rom.texts[0x244] = b"" + rom.texts[0x245] = b"" + rom.texts[0x247] = b"" + rom.texts[0x248] = b"" + rom.patch(0x06, 0x2A8F, 0x2BBC, ASM(""" + ; We use $DB6D to store which tunics we have. This is normally the Dungeon9 instrument, which does not exist. + ld a, [$DC0F] + ld hl, wCollectedTunics + inc a + + cp $01 + jr nz, notTunic1 + bit 0, [HL] + jr nz, notTunic1 + inc a +notTunic1: + + cp $02 + jr nz, notTunic2 + bit 1, [HL] + jr nz, notTunic2 + inc a +notTunic2: + + cp $03 + jr nz, noWrap + xor a +noWrap: + + ld [$DC0F], a + ret + """), fill_nop=True) diff --git a/worlds/ladx/LADXR/patches/photographer.py b/worlds/ladx/LADXR/patches/photographer.py new file mode 100644 index 0000000000..2b377c4d89 --- /dev/null +++ b/worlds/ladx/LADXR/patches/photographer.py @@ -0,0 +1,19 @@ +from ..assembler import ASM + + +def fixPhotographer(rom): + # Allow richard photo without slime key + rom.patch(0x36, 0x3234, ASM("jr nz, $52"), "", fill_nop=True) + rom.patch(0x36, 0x3240, ASM("jr z, $46"), "", fill_nop=True) + # Allow richard photo when castle is opened + rom.patch(0x36, 0x31FF, ASM("jp nz, $7288"), "", fill_nop=True) + # Allow photographer with bowwow saved + rom.patch(0x36, 0x0398, ASM("or [hl]"), "", fill_nop=True) + rom.patch(0x36, 0x3183, ASM("ret nz"), "", fill_nop=True) + rom.patch(0x36, 0x31CB, ASM("jp nz, $7288"), "", fill_nop=True) + rom.patch(0x36, 0x03DC, ASM("and $7F"), ASM("and $00")) + # Allow bowwow photo with follower + rom.patch(0x36, 0x31DA, ASM("jp nz, $7288"), "", fill_nop=True) + # Allow bridge photo with follower + rom.patch(0x36, 0x004D, ASM("call nz, $3F8D"), "", fill_nop=True) + rom.patch(0x36, 0x006D, ASM("ret nz"), "", fill_nop=True) # Checks if any entity is alive diff --git a/worlds/ladx/LADXR/patches/reduceRNG.py b/worlds/ladx/LADXR/patches/reduceRNG.py new file mode 100644 index 0000000000..cfeb4da6e4 --- /dev/null +++ b/worlds/ladx/LADXR/patches/reduceRNG.py @@ -0,0 +1,9 @@ +from ..assembler import ASM + + +def slowdownThreeOfAKind(rom): + rom.patch(0x06, 0x096B, ASM("ldh a, [$E7]\nand $0F"), ASM("ldh a, [$E7]\nand $3F")) + + +def fixHorseHeads(rom): + rom.patch(0x07, 0x3653, "00010400", "00010000") diff --git a/worlds/ladx/LADXR/patches/rooster.py b/worlds/ladx/LADXR/patches/rooster.py new file mode 100644 index 0000000000..c8bd831c88 --- /dev/null +++ b/worlds/ladx/LADXR/patches/rooster.py @@ -0,0 +1,40 @@ +from ..assembler import ASM +from ..utils import formatText + + +def patchRooster(rom): + # Do not give the rooster + rom.patch(0x19, 0x0E9D, ASM("ld [$DB7B], a"), "", fill_nop=True) + + # Do not load the rooster sprites + rom.patch(0x00, 0x2EC7, ASM("jr nz, $08"), "", fill_nop=True) + + # Draw the found item + rom.patch(0x19, 0x0E4A, ASM("ld hl, $4E37\nld c, $03\ncall $3CE6"), ASM("ld a, $0C\nrst $08"), fill_nop=True) + rom.patch(0x19, 0x0E7B, ASM("ld hl, $4E37\nld c, $03\ncall $3CE6"), ASM("ld a, $0C\nrst $08"), fill_nop=True) + # Give the item and message + rom.patch(0x19, 0x0E69, ASM("ld a, $6D\ncall $2373"), ASM("ld a, $0E\nrst $08"), fill_nop=True) + + # Reuse unused evil eagle text slot for rooster message + rom.texts[0x0B8] = formatText("Got the {ROOSTER}!") + + # Allow rooster pickup with special rooster item + rom.patch(0x19, 0x1ABC, ASM("cp $03"), ASM("cp $0F")) + rom.patch(0x19, 0x1AAE, ASM("cp $03"), ASM("cp $0F")) + + # Ignore the has-rooster flag in the rooster entity (do not despawn) + rom.patch(0x19, 0x19E0, ASM("jp z, $7E61"), "", fill_nop=True) + + # If we are spawning the rooster, and the rooster is already existing, do not do anything, instead of despawning the rooster. + rom.patch(0x01, 0x1FEF, ASM("ld [hl], d"), ASM("ret")) + # Allow rooster to unload when changing rooms + rom.patch(0x19, 0x19E9, ASM("ld [hl], a"), "", fill_nop=True) + + # Do not take away the rooster after D7 + rom.patch(0x03, 0x1E25, ASM("ld [$DB7B], a"), "", fill_nop=True) + + # Patch the color dungeon entrance not to check for rooster + rom.patch(0x02, 0x3409, ASM("ld hl, $DB7B\nor [hl]"), "", fill_nop=True) + + # Spawn marin at taltal even with rooster + rom.patch(0x18, 0x1EE3, ASM("jp nz, $7F08"), "", fill_nop=True) diff --git a/worlds/ladx/LADXR/patches/save.py b/worlds/ladx/LADXR/patches/save.py new file mode 100644 index 0000000000..c55d6314a4 --- /dev/null +++ b/worlds/ladx/LADXR/patches/save.py @@ -0,0 +1,54 @@ +from ..assembler import ASM +from ..backgroundEditor import BackgroundEditor + + +def singleSaveSlot(rom): + # Do not validate/erase slots 2 and 3 at rom start + rom.patch(0x01, 0x06B3, ASM("call $4794"), "", fill_nop=True) + rom.patch(0x01, 0x06B9, ASM("call $4794"), "", fill_nop=True) + + # Patch the code that checks if files have proper filenames to skip file 2/3 + rom.patch(0x01, 0x1DD9, ASM("ld b, $02"), ASM("ret"), fill_nop=True) + + # Remove the part that writes death counters for save2/3 on the file select screen + rom.patch(0x01, 0x0821, 0x084B, "", fill_nop=True) + # Remove the call that updates the hearts for save2 + rom.patch(0x01, 0x0800, ASM("call $4DBE"), "", fill_nop=True) + # Remove the call that updates the hearts for save3 + rom.patch(0x01, 0x0806, ASM("call $4DD6"), "", fill_nop=True) + + # Remove the call that updates the names for save2 and save3 + rom.patch(0x01, 0x0D70, ASM("call $4D94\ncall $4D9D"), "", fill_nop=True) + + # Remove the 2/3 slots from the screen and remove the copy text + be = BackgroundEditor(rom, 0x03) + del be.tiles[0x9924] + del be.tiles[0x9984] + be.store(rom) + be = BackgroundEditor(rom, 0x04) + del be.tiles[0x9924] + del be.tiles[0x9984] + for n in range(0x99ED, 0x99F1): + del be.tiles[n] + be.store(rom) + + # Do not do left/right for erase/copy selection. + rom.patch(0x01, 0x092B, ASM("jr z, $0B"), ASM("jr $0B")) + # Only switch between players + rom.patch(0x01, 0x08FA, 0x091D, ASM(""" + ld a, [$DBA7] + and a + ld a, [$DBA6] + jr z, skip + xor $03 +skip: + """), fill_nop=True) + + # On the erase screen, only switch between save 1 and return + rom.patch(0x01, 0x0E12, ASM("inc a\nand $03"), ASM("xor $03"), fill_nop=True) + rom.patch(0x01, 0x0E21, ASM("dec a\ncp $ff\njr nz, $02\nld a,$03"), ASM("xor $03"), fill_nop=True) + + be = BackgroundEditor(rom, 0x06) + del be.tiles[0x9924] + del be.tiles[0x9984] + be.store(rom) diff --git a/worlds/ladx/LADXR/patches/seashell.py b/worlds/ladx/LADXR/patches/seashell.py new file mode 100644 index 0000000000..7e53516d5e --- /dev/null +++ b/worlds/ladx/LADXR/patches/seashell.py @@ -0,0 +1,64 @@ +from ..assembler import ASM + + +def fixSeashell(rom): + # Do not unload if we have the lvl2 sword. + rom.patch(0x03, 0x1FD3, ASM("ld a, [$DB4E]\ncp $02\njp nc, $3F8D"), "", fill_nop=True) + # Do not unload in the ghost house + rom.patch(0x03, 0x1FE8, ASM("ldh a, [$F8]\nand $40\njp z, $3F8D"), "", fill_nop=True) + + # Call our special rendering code + rom.patch(0x03, 0x1FF2, ASM("ld de, $5FD1\ncall $3C77"), ASM("ld a, $05\nrst 8"), fill_nop=True) + + # Call our special handlers for messages and pickup + rom.patch(0x03, 0x2368, 0x237C, ASM(""" + ld a, $0A ; showMessageMultiworld + rst 8 + ld a, $06 ; giveItemMultiworld + rst 8 + call $512A + ret + """), fill_nop=True) + + +def upgradeMansion(rom): + rom.patch(0x19, 0x38EC, ASM(""" + ld hl, $78DC + jr $03 + """), "", fill_nop=True) + rom.patch(0x19, 0x38F1, ASM(""" + ld hl, $78CC + ld c, $04 + call $3CE6 + """), ASM(""" + ld a, $0C + rst 8 + """), fill_nop=True) + rom.patch(0x19, 0x3718, ASM("sub $13"), ASM("sub $0D")) + rom.patch(0x19, 0x3697, ASM(""" + cp $70 + jr c, $15 + ld [hl], $70 + """), ASM(""" + cp $73 + jr c, $15 + ld [hl], $73 + """)) + rom.patch(0x19, 0x36F5, ASM(""" + ld a, $02 + ld [$DB4E], a + """), ASM(""" + ld a, $0E ; give item and message for current room multiworld + rst 8 + """), fill_nop=True) + rom.patch(0x19, 0x36E6, ASM(""" + ld a, $9F + call $2385 + """), "", fill_nop=True) + rom.patch(0x19, 0x31E8, ASM(""" + ld a, [$DB4E] + and $02 + """), ASM(""" + ld a, [$DAE9] + and $10 + """)) diff --git a/worlds/ladx/LADXR/patches/shop.py b/worlds/ladx/LADXR/patches/shop.py new file mode 100644 index 0000000000..197fe09b18 --- /dev/null +++ b/worlds/ladx/LADXR/patches/shop.py @@ -0,0 +1,152 @@ +from ..assembler import ASM + + +def fixShop(rom): + # Move shield visuals to the 2nd slot, and arrow to 3th slot + rom.patch(0x04, 0x3732 + 22, "986A027FB2B098AC01BAB1", "9867027FB2B098A801BAB1") + rom.patch(0x04, 0x3732 + 55, "986302B1B07F98A4010A09", "986B02B1B07F98AC010A09") + + # Just use a fixed location in memory to store which inventory we give. + rom.patch(0x04, 0x37C5, "0708", "0802") + + # Patch the code that decides which shop to show. + rom.patch(0x04, 0x3839, 0x388E, ASM(""" + push bc + jr skipSubRoutine + +checkInventory: + ld hl, $DB00 ; inventory + ld c, INV_SIZE +loop: + cp [hl] + ret z + inc hl + dec c + jr nz, loop + and a + ret + +skipSubRoutine: + ; Set the shop table to all nothing. + ld hl, $C505 + xor a + ldi [hl], a + ldi [hl], a + ldi [hl], a + ldi [hl], a + ld de, $C505 + + ; Check if we want to load a key item into the shop. + ldh a, [$F8] + bit 4, a + jr nz, checkForSecondKeyItem + ld a, $01 + ld [de], a + jr checkForShield +checkForSecondKeyItem: + bit 5, a + jr nz, checkForShield + ld a, $05 + ld [de], a + +checkForShield: + inc de + ; Check if we have the shield or the bow to see if we need to remove certain entries from the shop + ld a, [$DB44] + and a + jr z, hasNoShieldLevel + ld a, $03 + ld [de], a ; Add shield buy option +hasNoShieldLevel: + + inc de + ld a, $05 + call checkInventory + jr nz, hasNoBow + ld a, $06 + ld [de], a ; Add arrow buy option +hasNoBow: + + inc de + ld a, $02 + call checkInventory + jr nz, hasNoBombs + ld a, $04 + ld [de], a ; Add bomb buy option +hasNoBombs: + + pop bc + call $3B12 ; increase entity state + """, 0x7839), fill_nop=True) + + # We do not have enough room at the shovel/bow buy entry to handle this + # So jump to a bit where we have some more space to work, as there is some dead code in the shop. + rom.patch(0x04, 0x3AA9, 0x3AAE, ASM("jp $7AC3"), fill_nop=True) + + # Patch over the "you stole it" dialog + rom.patch(0x00, 0x1A1C, 0x1A21, ASM("""ld a, $C9 + call $2385"""), fill_nop=True) + rom.patch(0x04, 0x3AC3, 0x3AD8, ASM(""" + ; No room override needed, we're in the proper room + ; Call our chest item giving code. + ld a, $0E + rst 8 + ; Update the room status to mark first item as bought + ld hl, $DAA1 + ld a, [hl] + or $10 + ld [hl], a + ret + """), fill_nop=True) + rom.patch(0x04, 0x3A73, 0x3A7E, ASM("jp $7A91"), fill_nop=True) + rom.patch(0x04, 0x3A91, 0x3AA9, ASM(""" + ; Override the room - luckily nothing will go wrong here if we leave it as is + ld a, $A7 + ldh [$F6], a + ; Call our chest item giving code. + ld a, $0E + rst 8 + ; Update the room status to mark second item as bought + ld hl, $DAA1 + ld a, [hl] + or $20 + ld [hl], a + ret + """), fill_nop=True) + + # Patch shop item graphics rendering to use some new code at the end of the bank. + rom.patch(0x04, 0x3B91, 0x3BAC, ASM(""" + call $7FD0 + """), fill_nop=True) + rom.patch(0x04, 0x3BD3, 0x3BE3, ASM(""" + jp $7FD0 + """), fill_nop=True) + rom.patch(0x04, 0x3FD0, "00" * 42, ASM(""" + ; Check if first key item + and a + jr nz, notShovel + ld a, [$77C5] + ldh [$F1], a + ld a, $01 + rst 8 + ret +notShovel: + cp $04 + jr nz, notBow + ld a, [$77C6] + ldh [$F1], a + ld a, $01 + rst 8 + ret +notBow: + cp $05 + jr nz, notArrows + ; Load arrow graphics and render then as a dual sprite + ld de, $7B58 + call $3BC0 + ret +notArrows: + ; Load the normal graphics + ld de, $7B5A + jp $3C77 + """), fill_nop=True) diff --git a/worlds/ladx/LADXR/patches/softlock.py b/worlds/ladx/LADXR/patches/softlock.py new file mode 100644 index 0000000000..15a9119538 --- /dev/null +++ b/worlds/ladx/LADXR/patches/softlock.py @@ -0,0 +1,93 @@ +from ..roomEditor import RoomEditor, Object +from ..assembler import ASM + + +def fixAll(rom): + # Prevent soft locking in the first mountain cave if we do not have a feather + re = RoomEditor(rom, 0x2B7) + re.removeObject(3, 3) + re.store(rom) + + # Prevent getting stuck in the sidescroll room in the beginning of dungeon 5 + re = RoomEditor(rom, 0x1A9) + re.objects[6].count = 7 + re.store(rom) + + # Cave that allows you to escape from D4 without flippers, make it no longer require a feather + re = RoomEditor(rom, 0x1EA) + re.objects[9].count = 8 + re.removeObject(5, 4) + re.moveObject(4, 4, 7, 5) + re.store(rom) + + # D3 west side room requires feather to get the key. But feather is not required to unlock the door, potentially softlocking you. + re = RoomEditor(rom, 0x155) + re.changeObject(4, 1, 0xcf) + re.changeObject(4, 6, 0xd0) + re.store(rom) + + # D3 boots room requires boots to escape + re = RoomEditor(rom, 0x146) + re.removeObject(5, 6) + re.store(rom) + + allowRaftGameWithoutFlippers(rom) + # We cannot access thes holes in logic: + # removeBirdKeyHoleDrop(rom) + fixDoghouse(rom) + flameThrowerShieldRequirement(rom) + fixLessThen3MaxHealth(rom) + +def fixDoghouse(rom): + # Fix entering the dog house from the back, and ending up out of bounds. + re = RoomEditor(rom, 0x0A1) + re.objects.append(Object(6, 2, 0x0E2)) + re.objects.append(re.objects[20]) # Move the flower patch after the warp entry definition so it overrules the tile + re.objects.append(re.objects[3]) + + re.objects.pop(22) + re.objects.pop(21) + re.objects.pop(20) # Remove the flower patch at the normal entry index + re.objects.pop(11) # Duplicate object, we can just remove it, gives room for our custom entry door + re.store(rom) + +def allowRaftGameWithoutFlippers(rom): + # Allow jumping down the waterfall in the raft game without the flippers. + rom.patch(0x02, 0x2E8F, ASM("ld a, [$DB0C]"), ASM("ld a, $01"), fill_nop=True) + # Change the room that goes back up to the raft game from the bottom, so we no longer need flippers + re = RoomEditor(rom, 0x1F7) + re.changeObject(3, 2, 0x1B) + re.changeObject(2, 3, 0x1B) + re.changeObject(3, 4, 0x1B) + re.changeObject(4, 5, 0x1B) + re.changeObject(6, 6, 0x1B) + re.store(rom) + +def removeBirdKeyHoleDrop(rom): + # Prevent the cave with the bird key from dropping you in the water + # (if you do not have flippers this would softlock you) + rom.patch(0x02, 0x1176, ASM(""" + ldh a, [$F7] + cp $0A + jr nz, $30 + """), ASM(""" + nop + nop + nop + nop + jr $30 + """)) + # Remove the hole that drops you all the way from dungeon7 entrance to the water in the cave + re = RoomEditor(rom, 0x01E) + re.removeObject(5, 4) + re.store(rom) + +def flameThrowerShieldRequirement(rom): + # if you somehow get a lvl3 shield or higher, it no longer works against the flamethrower, easy fix. + rom.patch(0x03, 0x2EBA, + ASM("ld a, [$DB44]\ncp $02\nret nz"), # if not shield level 2 + ASM("ld a, [$DB44]\ncp $02\nret c")) # if not shield level 2 or higher + +def fixLessThen3MaxHealth(rom): + # The table that starts your start HP when you die is not working for less then 3 HP, and locks the game. + rom.patch(0x01, 0x1295, "18181818", "08081018") diff --git a/worlds/ladx/LADXR/patches/songs.py b/worlds/ladx/LADXR/patches/songs.py new file mode 100644 index 0000000000..59ca01c4c8 --- /dev/null +++ b/worlds/ladx/LADXR/patches/songs.py @@ -0,0 +1,159 @@ +from ..assembler import ASM + + +def upgradeMarin(rom): + # Show marin outside, even without a sword. + rom.patch(0x05, 0x0E78, ASM("ld a, [$DB4E]"), ASM("ld a, $01"), fill_nop=True) + # Make marin ignore the fact that you did not save the tarin yet, and allowing getting her song + rom.patch(0x05, 0x0E87, ASM("ld a, [$D808]"), ASM("ld a, $10"), fill_nop=True) + rom.patch(0x05, 0x0F73, ASM("ld a, [$D808]"), ASM("ld a, $10"), fill_nop=True) + rom.patch(0x05, 0x0FB0, ASM("ld a, [$DB48]"), ASM("ld a, $01"), fill_nop=True) + # Show marin in the animal village + rom.patch(0x03, 0x0A86, ASM("ld a, [$DB74]"), ASM("ld a, $01"), fill_nop=True) + rom.patch(0x05, 0x3F2E, ASM("ld a, [$DB74]"), ASM("ld a, $01"), fill_nop=True) # animal d0 + rom.patch(0x15, 0x3F96, ASM("ld a, [$DB74]"), ASM("ld a, $01"), fill_nop=True) # animal d1 + rom.patch(0x18, 0x11B0, ASM("ld a, [$DB74]"), ASM("ld a, $01"), fill_nop=True) # animal d2 + + # Instead of checking if we have the ballad, check if we have a specific room flag set + rom.patch(0x05, 0x0F89, ASM(""" + ld a, [$DB49] + and $04 + """), ASM(""" + ld a, [$D892] + and $10 + """), fill_nop=True) + rom.patch(0x05, 0x0FDF, ASM(""" + ld a, [$DB49] + and $04 + """), ASM(""" + ld a, [$D892] + and $10 + """), fill_nop=True) + rom.patch(0x05, 0x1042, ASM(""" + ld a, [$DB49] + and $04 + """), ASM(""" + ld a, [$D892] + and $10 + """), fill_nop=True) + + # Patch that we call our specific handler instead of giving the song + rom.patch(0x05, 0x1170, ASM(""" + ld hl, $DB49 + set 2, [hl] + xor a + ld [$DB4A], a + """), ASM(""" + ; Mark Marin as done. + ld a, [$D892] + or $10 + ld [$D892], a + """), fill_nop=True) + + + # Show the right item instead of the ocerina + rom.patch(0x05, 0x11B3, ASM(""" + ld de, $515F + xor a + ldh [$F1], a + jp $3C77 + """), ASM(""" + ld a, $0C + rst 8 + ret + """), fill_nop=True) + + # Patch the message that tells we got the song, to give the item and show the right message + rom.patch(0x05, 0x119C, ASM(""" + ld a, $13 + call $2385 + """), ASM(""" + ld a, $0E + rst 8 + """), fill_nop=True) + + +def upgradeManbo(rom): + # Instead of checking if we have the song, check if we have a specific room flag set + rom.patch(0x18, 0x0536, ASM(""" + ld a, [$DB49] + and $02 + """), ASM(""" + ld a, [$DAFD] + and $20 + """), fill_nop=True) + + # Show the right item instead of the ocerina + rom.patch(0x18, 0x0786, ASM(""" + ld de, $474D + xor a + ldh [$F1], a + jp $3C77 + """), ASM(""" + ld a, $0C + rst 8 + ret + """), fill_nop=True) + + # Patch to replace song giving to give the right item + rom.patch(0x18, 0x0757, ASM(""" + ld a, $01 + ld [$DB4A], a + ld hl, $DB49 + set 1, [hl] + """), ASM(""" + ; Mark Manbo as done. + ld hl, $DAFD + set 5, [hl] + ; Show item message and give item + ld a, $0E + rst 8 + """), fill_nop=True) + # Remove the normal "got song message") + rom.patch(0x18, 0x076F, 0x0774, "", fill_nop=True) + +def upgradeMamu(rom): + # Always allow the sign maze instead of only allowing the sign maze if you do not have song3 + rom.patch(0x00, 0x2057, ASM("ld a, [$DB49]"), ASM("ld a, $00"), fill_nop=True) + + # Patch the condition at which Mamu gives you the option to listen to him + rom.patch(0x18, 0x0031, ASM(""" + ld a, [$DB49] + and $01 + """), ASM(""" + ld a, [$DAFB] ; load room flag of the Mamu room + and $10 + """), fill_nop=True) + + # Show the right item instead of the ocerina + rom.patch(0x18, 0x0299, ASM(""" + ld de, $474D + xor a + ldh [$F1], a + call $3C77 + """), ASM(""" + ld a, $0C + rst 8 + """), fill_nop=True) + + # Patch given an item + rom.patch(0x18, 0x0270, ASM(""" + ld a, $02 + ld [$DB4A], a + ld hl, $DB49 + set 0, [hl] + """), ASM(""" + ; Set the room complete flag. + ld hl, $DAFB + set 4, [hl] + """), fill_nop=True) + + # Patch to show the right message for the item + rom.patch(0x18, 0x0282, ASM(""" + ld a, $DF + call $4087 + """), ASM(""" + ; Give item and message for room. + ld a, $0E + rst 8 + """), fill_nop=True) diff --git a/worlds/ladx/LADXR/patches/tarin.py b/worlds/ladx/LADXR/patches/tarin.py new file mode 100644 index 0000000000..d84935634e --- /dev/null +++ b/worlds/ladx/LADXR/patches/tarin.py @@ -0,0 +1,51 @@ +from ..assembler import ASM +from ..utils import formatText + + +def updateTarin(rom): + # Do not give the shield. + rom.patch(0x05, 0x0CD0, ASM(""" + ld d, $04 + call $5321 + ld a, $01 + ld [$DB44], a + """), "", fill_nop=True) + + # Instead of showing the usual "your shield back" message, give the proper message and give the item. + rom.patch(0x05, 0x0CDE, ASM(""" + ld a, $91 + call $2385 + """), ASM(""" + ld a, $0B ; GiveItemAndMessageForRoom + rst 8 + """), fill_nop=True) + + rom.patch(0x05, 0x0CF0, ASM(""" + xor a + ldh [$F1], a + ld de, $4CC6 + call $3C77 + """), ASM(""" + ld a, $0C ; RenderItemForRoom + rst 8 + xor a + ldh [$F1], a + """), fill_nop=True) + + # Set the room status to finished. (replaces a GBC check) + rom.patch(0x05, 0x0CAB, 0x0CB0, ASM(""" + ld a, $20 + call $36C4 + """), fill_nop=True) + + # Instead of checking for the shield level to put you in the bed, check the room flag. + rom.patch(0x05, 0x1202, ASM("ld a, [$DB44]\nand a"), ASM("ldh a, [$F8]\nand $20")) + rom.patch(0x05, 0x0C6D, ASM("ld a, [$DB44]\nand a"), ASM("ldh a, [$F8]\nand $20")) + + # If the starting item is picked up, load the right palette when entering the room + rom.patch(0x21, 0x0176, ASM("ld a, [$DB48]\ncp $01"), ASM("ld a, [$DAA3]\ncp $A1"), fill_nop=True) + rom.patch(0x05, 0x0C94, "FF473152C5280000", "FD2ED911CE100000") + rom.patch(0x05, 0x0CB0, ASM("ld hl, $DC88"), ASM("ld hl, $DC80")) + + # Patch the text that Tarin uses to give your shield back. + rom.texts[0x54] = formatText("#####, it is dangerous to go alone!\nTake this!") diff --git a/worlds/ladx/LADXR/patches/titleScreen.py b/worlds/ladx/LADXR/patches/titleScreen.py new file mode 100644 index 0000000000..b81f1460eb --- /dev/null +++ b/worlds/ladx/LADXR/patches/titleScreen.py @@ -0,0 +1,84 @@ +from ..backgroundEditor import BackgroundEditor +import subprocess +import binascii + + +CHAR_MAP = {'z': 0x3E, '-': 0x3F, '.': 0x39, ':': 0x42, '?': 0x3C, '!': 0x3D} + + +def _encode(s): + result = bytearray() + for char in s: + if ord("A") <= ord(char) <= ord("Z"): + result.append(ord(char) - ord("A")) + elif ord("a") <= ord(char) <= ord("y"): + result.append(ord(char) - ord("a") + 26) + elif ord("0") <= ord(char) <= ord("9"): + result.append(ord(char) - ord("0") + 0x70) + else: + result.append(CHAR_MAP.get(char, 0x7E)) + return result + + +def setRomInfo(rom, seed, seed_name, settings, player_name, player_id): + try: + seednr = int(seed, 16) + except: + import hashlib + seednr = int(hashlib.md5(seed).hexdigest(), 16) + + if settings.race: + seed_name = "Race" + if isinstance(settings.race, str): + seed_name += " " + settings.race + rom.patch(0x00, 0x07, "00", "01") + else: + rom.patch(0x00, 0x07, "00", "52") + + line_1_hex = _encode(seed_name[:20]) + #line_2_hex = _encode(seed[16:]) + BASE_DRAWING_AREA = 0x98a0 + LINE_WIDTH = 0x20 + player_id_text = f"Player {player_id}:" + for n in (3, 4): + be = BackgroundEditor(rom, n) + ba = BackgroundEditor(rom, n, attributes=True) + + for n, v in enumerate(_encode(player_id_text)): + be.tiles[BASE_DRAWING_AREA + LINE_WIDTH * 5 + 2 + n] = v + ba.tiles[BASE_DRAWING_AREA + LINE_WIDTH * 5 + 2 + n] = 0x00 + for n, v in enumerate(_encode(player_name)): + be.tiles[BASE_DRAWING_AREA + LINE_WIDTH * 6 + 0x13 - len(player_name) + n] = v + ba.tiles[BASE_DRAWING_AREA + LINE_WIDTH * 6 + 0x13 - len(player_name) + n] = 0x00 + for n, v in enumerate(line_1_hex): + be.tiles[0x9a20 + n] = v + ba.tiles[0x9a20 + n] = 0x00 + + for n in range(0x09, 0x14): + be.tiles[0x9820 + n] = 0x7F + be.tiles[0x9840 + n] = 0xA0 + (n % 2) + be.tiles[0x9860 + n] = 0xA2 + sn = seednr + for n in range(0x0A, 0x14): + tilenr = sn % 30 + sn //= 30 + if tilenr > 12: + tilenr += 2 + if tilenr > 16: + tilenr += 1 + if tilenr > 19: + tilenr += 3 + if tilenr > 27: + tilenr += 1 + if tilenr > 29: + tilenr += 2 + if tilenr > 35: + tilenr += 1 + be.tiles[0x9800 + n] = tilenr * 2 + be.tiles[0x9820 + n] = tilenr * 2 + 1 + pal = sn % 8 + sn //= 8 + ba.tiles[0x9800 + n] = 0x08 | pal + ba.tiles[0x9820 + n] = 0x08 | pal + be.store(rom) + ba.store(rom) diff --git a/worlds/ladx/LADXR/patches/tradeSequence.py b/worlds/ladx/LADXR/patches/tradeSequence.py new file mode 100644 index 0000000000..5b608977f2 --- /dev/null +++ b/worlds/ladx/LADXR/patches/tradeSequence.py @@ -0,0 +1,355 @@ +from ..assembler import ASM + + +def patchTradeSequence(rom, boomerang_option): + patchTrendy(rom) + patchPapahlsWife(rom) + patchYipYip(rom) + patchBananasSchule(rom) + patchKiki(rom) + patchTarin(rom) + patchBear(rom) + patchPapahl(rom) + patchGoatMrWrite(rom) + patchGrandmaUlrira(rom) + patchFisherman(rom) + patchMermaid(rom) + patchMermaidStatue(rom) + patchSharedCode(rom) + patchVarious(rom, boomerang_option) + patchInventoryMenu(rom) + + +def patchTrendy(rom): + # Trendy game yoshi + rom.patch(0x04, 0x3502, 0x350F, ASM(""" + ldh a, [$F8] ; room status + and a, $20 + jp nz, $6D7A ; clear entity + ; Render sprite + ld a, $0F + rst 8 + ; Reset the sprite variant, else the code gets confused + xor a + ldh [$F1], a ; sprite variant + """), fill_nop=True) + rom.patch(0x04, 0x2E80, ASM("ldh a, [$F8]"), ASM("ld a, $10")) # Prevent marin cutscene from triggering, as that locks the game now. + rom.patch(0x04, 0x3622, 0x3627, "", fill_nop=True) # Dont set the trade item + + +def patchPapahlsWife(rom): + # Rewrite how the first dialog is generated. + rom.patch(0x18, 0x0E7A, 0x0EA8, ASM(""" + ldh a, [$F8] ; room status + and a, $20 + jr nz, tradeDone + + ld a, [wTradeSequenceItem] + and $01 + jr nz, requestTrade + + ld a, $2A ; Dialog about wanting a yoshi doll + jp $2373 ; OpenDialogInTable1 +tradeDone: + ld a, $2C ; Dialog about kids, after trade is done + jp $2373 ; OpenDialogInTable1 +requestTrade: + ld a, $2B ; Dialog about kids, after trade is done + call $3B12; IncrementEntityState + jp $2373 ; OpenDialogInTable1 + """), fill_nop=True) + rom.patch(0x18, 0x0EB4, 0x0EBD, ASM("ld hl, wTradeSequenceItem\nres 0, [hl]"), fill_nop=True) # Take the trade item + + +def patchYipYip(rom): + # Change how the decision is made to draw yipyip with a ribbon + rom.patch(0x06, 0x1A2C, 0x1A36, ASM(""" + ldh a, [$F8] ; room status + and $20 + jr z, tradeNotDone + ld de, $59C8 ; yipyip with ribbon +tradeNotDone: + """), fill_nop=True) + # Check if we have the ribbon + rom.patch(0x06, 0x1A7C, 0x1A83, ASM(""" + ld a, [wTradeSequenceItem] + and $02 + jr z, $07 + """), fill_nop=True) + rom.patch(0x06, 0x1AAF, 0x1AB8, ASM("ld hl, wTradeSequenceItem\nres 1, [hl]"), fill_nop=True) # Take the trade item + + +def patchBananasSchule(rom): + # Change how to check if we have the right trade item + rom.patch(0x19, 0x2D54, 0x2D5B, ASM(""" + ld a, [wTradeSequenceItem] + and $04 + jr z, $08 + """), fill_nop=True) + rom.patch(0x19, 0x2DF0, 0x2DF9, ASM("ld hl, wTradeSequenceItem\nres 2, [hl]"), fill_nop=True) # Take the trade item + # Change how the decision is made to render less bananas + rom.patch(0x19, 0x2EF1, 0x2EFA, ASM(""" + ldh a, [$F8] + and $20 + jr z, skip + dec c + dec c +skip: """), fill_nop=True) + + # Part of the same entity code, but this is the painter, which changes the dialog depending on mermaid scale or magnifier + rom.patch(0x19, 0x2F95, 0x2F9C, ASM(""" + ld a, [wTradeSequenceItem2] + and $10 ; Check for mermaid scale + jr z, $04 + """)) + rom.patch(0x19, 0x2FA0, 0x2FA4, ASM(""" + and $20 ; Check for magnifier + jr z, $07 + """)) + rom.patch(0x19, 0x2CE3, "9A159C15", "B41DB61D") # Properly draw the dog food + + +def patchKiki(rom): + rom.patch(0x07, 0x18E6, 0x18ED, ASM(""" + ld a, [wTradeSequenceItem] + and $08 ; check for banana + jr z, $08 + """)) + rom.patch(0x07, 0x19AF, 0x19B4, "", fill_nop=True) # Do not change trading item memory + rom.patch(0x07, 0x19CC, 0x19D5, ASM("ld hl, wTradeSequenceItem\nres 3, [hl]"), fill_nop=True) # Take the trade item + rom.patch(0x07, 0x194D, "9A179C17", "B81FBA1F") # Properly draw the banana above kiki + + +def patchTarin(rom): + rom.patch(0x07, 0x0EC5, 0x0ECA, ASM(""" + ld a, [wTradeSequenceItem] + and $10 ; check for stick + """)) + rom.patch(0x07, 0x0F30, 0x0F33, "", fill_nop=True) # Take the trade item + # Honeycomb, change how we detect that it should fall on entering the room + rom.patch(0x07, 0x0CCC, 0x0CD3, ASM(""" + ld a, [$D887] + and $40 + jr z, $14 + """)) + # Something about tarin changing messages or not showing up depending on the trade sequence + rom.patch(0x05, 0x0BFF, 0x0C07, "", fill_nop=True) # Just ignore the trade sequence + rom.patch(0x05, 0x0D20, 0x0D27, "", fill_nop=True) # Just ignore the trade sequence + rom.patch(0x05, 0x0DAF, 0x0DB8, "", fill_nop=True) # Tarin giving bananas? + + rom.patch(0x07, 0x0D6D, 0x0D7A, ASM("ld hl, wTradeSequenceItem\nres 4, [hl]"), fill_nop=True) # Take the trade item + + +def patchBear(rom): + # Change the trade item check + rom.patch(0x07, 0x0BCC, 0x0BD3, ASM(""" + ld a, [wTradeSequenceItem] + and $20 ; check for honeycomb + jr z, $0E + """)) + rom.patch(0x07, 0x0C21, ASM("jr nz, $22"), "", fill_nop=True) + rom.patch(0x07, 0x0C23, 0x0C2A, ASM(""" + ld a, [wTradeSequenceItem] + and $20 ; check for honeycomb + jr z, $08 + """)) + + rom.patch(0x07, 0x0C3C, 0x0C43, ASM(""" + nop + nop + nop + nop + nop + jr $02 + """)) + rom.patch(0x07, 0x0C5E, 0x0C67, ASM("ld hl, wTradeSequenceItem\nres 5, [hl]"), fill_nop=True) # Take the trade item + + +def patchPapahl(rom): + rom.patch(0x07, 0x0A21, 0x0A30, ASM("call $7EA4"), fill_nop=True) # Never show indoor papahl + # Render the bag condition + rom.patch(0x07, 0x0A81, 0x0A88, ASM(""" + ldh a, [$F8] ; current room status + and $20 + nop + jr nz, $18 + """)) + # Check for the right item + rom.patch(0x07, 0x0ACF, 0x0AD4, ASM(""" + ld a, [wTradeSequenceItem] + and $40 ; pineapple + """)) + rom.patch(0x07, 0x0AD6, ASM("jr z, $02"), ASM("jr nz, $02")) + + rom.patch(0x07, 0x0AF9, 0x0B00, ASM(""" + ld a, [wTradeSequenceItem] + and $40 ; pineapple + jr z, $0E + """)) + rom.patch(0x07, 0x0B2F, 0x0B38, ASM("ld hl, wTradeSequenceItem\nres 6, [hl]"), fill_nop=True) # Take the trade item + + +def patchGoatMrWrite(rom): # The goat and mrwrite are the same entity + rom.patch(0x18, 0x0BF1, 0x0BF8, ASM(""" + ldh a, [$F8] + and $20 + nop + jr nz, $03 + """)) # Check if we made the trade with the goat + rom.patch(0x18, 0x0C2C, 0x0C33, ASM(""" + ld a, [wTradeSequenceItem] + and $80 ; hibiscus + jr z, $08 + """)) # Check if we have the hibiscus + rom.patch(0x18, 0x0C3D, 0x0C41, "", fill_nop=True) + rom.patch(0x18, 0x0C6B, 0x0C74, ASM("ld hl, wTradeSequenceItem\nres 7, [hl]"), fill_nop=True) # Take the trade item for the goat + + rom.patch(0x18, 0x0C8B, 0x0C92, ASM(""" + ld a, [wTradeSequenceItem2] + and $01 ; letter + jr z, $08 + """)) # Check if we have the letter + rom.patch(0x18, 0x0C9C, 0x0CA0, "", fill_nop=True) + rom.patch(0x18, 0x0CE2, 0x0CEB, ASM("ld hl, wTradeSequenceItem2\nres 0, [hl]"), fill_nop=True) # Take the trade item for mrwrite + + +def patchGrandmaUlrira(rom): + rom.patch(0x18, 0x0D2C, ASM("jr z, $02"), "", fill_nop=True) # Always show up in animal village + rom.patch(0x18, 0x0D3C, 0x0D51, ASM(""" + ldh a, [$F8] + and $20 + jp nz, $4D58 + """), fill_nop=True) + rom.patch(0x18, 0x0D95, 0x0D9A, "", fill_nop=True) + rom.patch(0x18, 0x0D9C, 0x0DA0, "", fill_nop=True) + rom.patch(0x18, 0x0DA3, 0x0DAA, ASM(""" + ld a, [wTradeSequenceItem2] + and $02 ; broom + jr z, $0B + """)) + rom.patch(0x18, 0x0DC4, 0x0DC7, "", fill_nop=True) + rom.patch(0x18, 0x0DE2, 0x0DEB, ASM("ld hl, wTradeSequenceItem2\nres 1, [hl]"), fill_nop=True) # Take the trade item + rom.patch(0x18, 0x0E1D, 0x0E20, "", fill_nop=True) + rom.patch(0x18, 0x0D13, "9A149C14", "D01CD21C") + + +def patchFisherman(rom): + # Not sure what this first check is for + rom.patch(0x07, 0x02F8, 0x0300, ASM(""" + """), fill_nop=True) + # Check for the hook + rom.patch(0x07, 0x04BF, 0x04C6, ASM(""" + ld a, [wTradeSequenceItem2] + and $04 ; hook + jr z, $08 + """)) + rom.patch(0x07, 0x04F3, 0x04F6, "", fill_nop=True) + rom.patch(0x07, 0x057D, 0x0586, ASM("ld hl, wTradeSequenceItem2\nres 2, [hl]"), fill_nop=True) # Take the trade item + rom.patch(0x04, 0x1F88, 0x1F8B, "", fill_nop=True) + + +def patchMermaid(rom): + # Check for the right trade item + rom.patch(0x07, 0x0797, 0x079E, ASM(""" + ld a, [wTradeSequenceItem2] + and $08 ; necklace + jr z, $0B + """)) + rom.patch(0x07, 0x0854, 0x085B, ASM("ld hl, wTradeSequenceItem2\nres 3, [hl]"), fill_nop=True) # Take the trade item + + +def patchMermaidStatue(rom): + rom.patch(0x18, 0x095D, 0x0962, "", fill_nop=True) + rom.patch(0x18, 0x0966, 0x097A, ASM(""" + ld a, [wTradeSequenceItem2] + and $10 ; scale + ret z + ldh a, [$F8] + and $20 + ret nz + """), fill_nop=True) + + +def patchSharedCode(rom): + # Trade item render code override. + rom.patch(0x07, 0x1535, 0x1575, ASM(""" + ldh a, [$F9] + and a + jr z, notSideScroll + + ldh a, [$EC]; hActiveEntityVisualPosY + add a, $02 + ldh [$EC], a +notSideScroll: + ; Render sprite + ld a, $0F + rst 8 + """), fill_nop=True) + # Trade item message code + # rom.patch(0x07, 0x159F, 0x15B9, ASM(""" + # ld a, $09 ; give message and item (from alt item table) + # rst 8 + # """), fill_nop=True) + rom.patch(0x07, 0x159F, 0x15B9, ASM(""" + ldh a, [$F6] ; map room + cp $B2 + jr nz, NotYipYip + add a, 2 ; Add 2 to room to set room pointer to an empty room for trade items + ldh [$F6], a + ld a, $0e ; giveItemMultiworld + rst 8 + ldh a, [$F6] ; map room + sub a, 2 ; ...and undo it + ldh [$F6], a + jr Done + NotYipYip: + ld a, $0e ; giveItemMultiworld + rst 8 + Done: + """), fill_nop=True) + + + # Prevent changing the 2nd trade item memory + rom.patch(0x07, 0x15BD, 0x15C1, ASM(""" + call $7F7F + xor a ; we need to exit with A=00 + """), fill_nop=True) + rom.patch(0x07, 0x3F7F, "00" * 7, ASM("ldh a, [$F8]\nor $20\nldh [$F8], a\nret")) + + +def patchVarious(rom, boomerang_option): + # Make the zora photo work with the magnifier + rom.patch(0x18, 0x09F3, 0x0A02, ASM(""" + ld a, [wTradeSequenceItem2] + and $20 ; MAGNIFYING_GLASS + jp z, $7F08 ; ClearEntityStatusBank18 + """), fill_nop=True) + rom.patch(0x03, 0x0B6D, 0x0B75, ASM(""" + ld a, [wTradeSequenceItem2] + and $20 ; MAGNIFYING_GLASS + jp z, $3F8D ; UnloadEntity + """), fill_nop=True) + # Mimic invisibility + rom.patch(0x18, 0x2AC8, 0x2ACE, "", fill_nop=True) + # Ignore trade quest state for marin at beach + rom.patch(0x18, 0x219E, 0x21A6, "", fill_nop=True) + # Shift the magnifier 8 pixels + rom.patch(0x03, 0x0F68, 0x0F6F, ASM(""" + ldh a, [$F6] ; map room + cp $97 ; check if we are in the maginfier room + jp z, $4F83 + """), fill_nop=True) + # Something with the photographer + rom.patch(0x36, 0x0948, 0x0950, "", fill_nop=True) + + if boomerang_option not in {'trade', 'gift'}: # Boomerang cave is not patched, so adjust it + rom.patch(0x19, 0x05EC, ASM("ld a, [wTradeSequenceItem]\ncp $0E\njp nz, $7E61"), ASM("ld a, [wTradeSequenceItem2]\nand $20\njp z, $7E61")) # show the guy + rom.patch(0x00, 0x3199, ASM("ld a, [wTradeSequenceItem]\ncp $0E\njr nz, $06"), ASM("ld a, [wTradeSequenceItem2]\nand $20\njr z, $06")) # load the proper room layout + rom.patch(0x19, 0x05F4, 0x05FB, "", fill_nop=True) + + +def patchInventoryMenu(rom): + # Never draw the trade item the normal way + rom.patch(0x20, 0x1A2E, ASM("ld a, [wTradeSequenceItem2]\nand a\njr nz, $23"), ASM("jp $5A57"), fill_nop=True) + + rom.patch(0x20, 0x1EB5, ASM("ldh a, [$FE]\nand a\njr z, $34"), ASM("ld a, $10\nrst 8"), fill_nop=True) diff --git a/worlds/ladx/LADXR/patches/trendy.py b/worlds/ladx/LADXR/patches/trendy.py new file mode 100644 index 0000000000..21118274fc --- /dev/null +++ b/worlds/ladx/LADXR/patches/trendy.py @@ -0,0 +1,3 @@ + +def fixTrendy(rom): + rom.patch(0x04, 0x2F29, "04", "02") # Patch the trendy game shield to be a ruppee diff --git a/worlds/ladx/LADXR/patches/tunicFairy.py b/worlds/ladx/LADXR/patches/tunicFairy.py new file mode 100644 index 0000000000..d61f634087 --- /dev/null +++ b/worlds/ladx/LADXR/patches/tunicFairy.py @@ -0,0 +1,45 @@ +from ..utils import formatText +from ..assembler import ASM + + +def upgradeTunicFairy(rom): + rom.texts[0x268] = formatText("Welcome, #####. I admire you for coming this far.") + rom.texts[0x0CC] = formatText("Got the {RED_TUNIC}! You can change Tunics at the phone booths.") + rom.texts[0x0CD] = formatText("Got the {BLUE_TUNIC}! You can change Tunics at the phone booths.") + + rom.patch(0x36, 0x111C, 0x1133, ASM(""" + call $3B12 + ld a, [$DDE1] + and $10 + jr z, giveItems + ld [hl], $09 + ret + +giveItems: + ld a, [$DDE1] + or $10 + ld [$DDE1], a + """), fill_nop=True) + rom.patch(0x36, 0x1139, 0x1144, ASM(""" + ld a, $04 + ldh [$F6], a + ld a, $0E + rst 8 + """), fill_nop=True) + + rom.patch(0x36, 0x1162, 0x1192, ASM(""" + ld a, $01 + ldh [$F6], a + ld a, $0E + rst 8 + """), fill_nop=True) + + rom.patch(0x36, 0x119D, 0x11A2, "", fill_nop=True) + rom.patch(0x36, 0x11B5, 0x11BE, ASM(""" + ; Skip to the end ignoring all the tunic giving animation. + call $3B12 + ld [hl], $09 + """), fill_nop=True) + + rom.banks[0x36][0x11BF] = 0x87 + rom.banks[0x36][0x11C0] = 0x88 diff --git a/worlds/ladx/LADXR/patches/weapons.py b/worlds/ladx/LADXR/patches/weapons.py new file mode 100644 index 0000000000..9c949934c9 --- /dev/null +++ b/worlds/ladx/LADXR/patches/weapons.py @@ -0,0 +1,64 @@ +from ..assembler import ASM +from ..roomEditor import RoomEditor + + +def patchSuperWeapons(rom): + # Feather jump height + rom.patch(0x00, 0x1508, ASM("ld a, $20"), ASM("ld a, $2C")) + # Boots charge speed + rom.patch(0x00, 0x1731, ASM("cp $20"), ASM("cp $01")) + # Power bracelet pickup speed + rom.patch(0x00, 0x2121, ASM("ld e, $08"), ASM("ld e, $01")) + # Throwing speed (of pickups and bombs) + rom.patch(0x14, 0x1313, "30D0000018E80000", "60A0000040C00000") + rom.patch(0x14, 0x1323, "0000D0300000E818", "0000A0600000C040") + + # Allow as many bombs to be placed as you want! + rom.patch(0x00, 0x135F, ASM("ret nc"), "", fill_nop=True) + + # Maximum amount of arrows in the air + rom.patch(0x00, 0x13C5, ASM("cp $02"), ASM("cp $05")) + # Delay between arrow shots + rom.patch(0x00, 0x13C9, ASM("ld a, $10"), ASM("ld a, $01")) + + # Maximum amount of firerod fires + rom.patch(0x00, 0x12E4, ASM("cp $02"), ASM("cp $05")) + + # Projectile speed (arrows, firerod) + rom.patch(0x00, 0x13AD, + "30D0000040C00000" "0000D0300000C040", + "60A0000060A00000" "0000A0600000A060") + + # Hookshot shoot speed + rom.patch(0x02, 0x024C, + "30D00000" "0000D030", + "60A00000" "0000A060") + # Hookshot retract speed + rom.patch(0x18, 0x3C41, ASM("ld a, $30"), ASM("ld a, $60")) + # Hookshot pull speed + rom.patch(0x18, 0x3C21, ASM("ld a, $30"), ASM("ld a, $60")) + + # Super shovel, always price! + rom.patch(0x02, 0x0CC6, ASM("jr nz, $57"), "", fill_nop=True) + + # Unlimited boomerangs! + rom.patch(0x00, 0x1387, ASM("ret nz"), "", fill_nop=True) + + # Increase shield push power + rom.patch(0x03, 0x2FC5, ASM("ld a, $08"), ASM("ld a, $10")) + rom.patch(0x03, 0x2FCA, ASM("ld a, $20"), ASM("ld a, $40")) + # Decrease link pushback of shield + rom.patch(0x03, 0x2FB9, ASM("ld a, $12"), ASM("ld a, $04")) + rom.patch(0x03, 0x2F9A, ASM("ld a, $0C"), ASM("ld a, $03")) + + # Super charge the ocarina + rom.patch(0x02, 0x0AD8, ASM("cp $38"), ASM("cp $08")) + rom.patch(0x02, 0x0B05, ASM("cp $14"), ASM("cp $04")) + + re = RoomEditor(rom, 0x23D) + tiles = re.getTileArray() + tiles[11] = 0x0D + tiles[12] = 0xA7 + tiles[22] = 0x98 + re.buildObjectList(tiles) + re.store(rom) \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/witch.py b/worlds/ladx/LADXR/patches/witch.py new file mode 100644 index 0000000000..d87190eedd --- /dev/null +++ b/worlds/ladx/LADXR/patches/witch.py @@ -0,0 +1,58 @@ +from ..assembler import ASM +from ..roomEditor import RoomEditor + + +def updateWitch(rom): + # Add a heartpiece at the toadstool, the item patches turn this into a 1 time toadstool item + # Or depending on flags, in something else. + re = RoomEditor(rom, 0x050) + re.addEntity(2, 3, 0x35) + re.store(rom) + + # Change what happens when you trade the toadstool with the witch + # Note that the 2nd byte of this code gets patched with the item to give from the witch. + rom.patch(0x05, 0x08D4, 0x08F0, ASM(""" + ; Get the room flags and mark the witch as done. + ld hl, $DAA2 + ld a, [hl] + and $30 + set 4, [hl] + set 5, [hl] + jr z, item +powder: + ld e, $09 ; give powder every time after the first time. + ld a, e + ldh [$F1], a + ld a, $11 + rst 8 + jp $48F0 +item: + ld a, $0E + rst 8 + """), fill_nop=True) + + # Patch the toadstool to unload when you haven't delivered something to the witch yet. + rom.patch(0x03, 0x1D4B, ASM(""" + ld hl, $DB4B + ld a, [$DB4C] + or [hl] + jp nz, $3F8D + """), ASM(""" + ld a, [$DAA2] + and $20 + jp z, $3F8D + """), fill_nop=True) + + # Patch what happens when we pickup the toadstool, call our chest code to give a toadstool. + rom.patch(0x03, 0x1D6F, 0x1D7D, ASM(""" + ld a, $50 + ldh [$F1], a + ld a, $02 ; give item + rst 8 + + ld hl, $DAA2 + res 5, [hl] + """), fill_nop=True) + +def witchIsPatched(rom): + return sum(rom.banks[0x05][0x08D4:0x08F0]) != 0x0DC2 diff --git a/worlds/ladx/LADXR/plan.py b/worlds/ladx/LADXR/plan.py new file mode 100644 index 0000000000..df1908f433 --- /dev/null +++ b/worlds/ladx/LADXR/plan.py @@ -0,0 +1,38 @@ + + +# Helper class to read and store planomizer data +class Plan: + def __init__(self, filename): + self.forced_items = {} + self.item_pool = {} + item_group = {} + + for line in open(filename, "rt"): + line = line.strip() + if ";" in line: + line = line[:line.find(";")] + if "#" in line: + line = line[:line.find("#")] + if ":" not in line: + continue + entry_type, params = map(str.strip, line.upper().split(":", 1)) + + if entry_type == "LOCATION" and ":" in params: + location, item = map(str.strip, params.split(":", 1)) + if item == "": + continue + if item.startswith("[") and item.endswith("]"): + item = item_group[item[1:-1]] + if "," in item: + item = list(map(str.strip, item.split(","))) + self.forced_items[location] = item + elif entry_type == "POOL" and ":" in params: + item, count = map(str.strip, params.split(":", 1)) + self.item_pool[item] = self.item_pool.get(item, 0) + int(count) + elif entry_type == "GROUP" and ":" in params: + name, item = map(str.strip, params.split(":", 1)) + if item == "": + continue + if "," in item: + item = list(map(str.strip, item.split(","))) + item_group[name] = item diff --git a/worlds/ladx/LADXR/pointerTable.py b/worlds/ladx/LADXR/pointerTable.py new file mode 100644 index 0000000000..6b56b6ff44 --- /dev/null +++ b/worlds/ladx/LADXR/pointerTable.py @@ -0,0 +1,207 @@ +import copy +import struct + + +class PointerTable: + END_OF_DATA = (0xff, ) + + """ + Class to manage a list of pointers to data objects + Can rewrite the rom to modify the data objects and still keep the pointers intact. + """ + def __init__(self, rom, info): + assert "count" in info + assert "pointers_bank" in info + assert "pointers_addr" in info + assert ("banks_bank" in info and "banks_addr" in info) or ("data_bank" in info) + self.__info = info + + self.__data = [] + self.__alt_data = {} + self.__banks = [] + self.__storage = [] + + count = info["count"] + addr = info["pointers_addr"] + pointers_bank = rom.banks[info["pointers_bank"]] + if "data_addr" in info: + pointers_raw = [] + for n in range(count): + pointers_raw.append(info["data_addr"] + 0x4000 + pointers_bank[addr + n] * info["data_size"]) + else: + pointers_raw = struct.unpack("<" + "H" * count, pointers_bank[addr:addr+count*2]) + if "data_bank" in info: + banks = [info["data_bank"]] * count + else: + addr = info["banks_addr"] + banks = rom.banks[info["banks_bank"]][addr:addr+count] + + if "alt_pointers" in self.__info: + for key, (bank, addr) in self.__info["alt_pointers"].items(): + pointer = struct.unpack("= len(s) and st["bank"] == bank: + my_storage = st + break + assert my_storage is not None, "Not enough room in storage... %s" % (storage) + + pointer = my_storage["start"] + my_storage["start"] = pointer + len(s) + rom.banks[bank][pointer:pointer + len(s)] = s + + rom.banks[ptr_bank][ptr_addr] = pointer & 0xFF + rom.banks[ptr_bank][ptr_addr + 1] = (pointer >> 8) | 0x40 + + for n, s in enumerate(self.__data): + if isinstance(s, int): + pointer = s + else: + s = bytes(s) + bank = self.__banks[n] + if s in done[bank]: + pointer = done[bank][s] + assert rom.banks[bank][pointer:pointer+len(s)] == s + else: + my_storage = None + for st in storage: + if st["end"] - st["start"] >= len(s) and st["bank"] == bank: + my_storage = st + break + assert my_storage is not None, "Not enough room in storage... %d/%d %s id:%x(%d) bank:%d" % (n, len(self.__data), storage, n, n, bank) + + pointer = my_storage["start"] + my_storage["start"] = pointer + len(s) + rom.banks[bank][pointer:pointer+len(s)] = s + + if "data_size" not in self.__info: + # aggressive de-duplication. + for skip in range(len(s)): + done[bank][s[skip:]] = pointer + skip + done[bank][s] = pointer + + if "data_addr" in self.__info: + offset = pointer - self.__info["data_addr"] + if "data_size" in self.__info: + assert offset % self.__info["data_size"] == 0 + offset //= self.__info["data_size"] + rom.banks[pointers_bank][pointers_addr + n] = offset + else: + rom.banks[pointers_bank][pointers_addr+n*2] = pointer & 0xff + rom.banks[pointers_bank][pointers_addr+n*2+1] = ((pointer >> 8) & 0xff) | 0x40 + + space_left = sum(map(lambda n: n["end"] - n["start"], storage)) + # print(self.__class__.__name__, "Space left:", space_left) + return storage + + def _readData(self, rom, bank_nr, pointer): + bank = rom.banks[bank_nr] + start = pointer + if "data_size" in self.__info: + pointer += self.__info["data_size"] + else: + while bank[pointer] not in self.END_OF_DATA: + pointer += 1 + pointer += 1 + self._addStorage(bank_nr, start, pointer) + return bank[start:pointer] + + def _addStorage(self, bank, start, end): + for n, data in enumerate(self.__storage): + if data["bank"] == bank: + if data["start"] == end: + data["start"] = start + return + if data["end"] == start: + data["end"] = end + return + if data["start"] <= start and data["end"] >= end: + return + self.__storage.append({"bank": bank, "start": start, "end": end}) + + def __mergeStorage(self): + for n in range(len(self.__storage)): + n_end = self.__storage[n]["end"] + n_start = self.__storage[n]["start"] + for m in range(len(self.__storage)): + if m == n or self.__storage[n]["bank"] != self.__storage[m]["bank"]: + continue + m_end = self.__storage[m]["end"] + m_start = self.__storage[m]["start"] + if m_start - 1 <= n_end <= m_end: + self.__storage[n]["start"] = min(self.__storage[n]["start"], self.__storage[m]["start"]) + self.__storage[n]["end"] = self.__storage[m]["end"] + self.__storage.pop(m) + return True + return False + + def addStorage(self, extra_storage): + for data in extra_storage: + self._addStorage(data["bank"], data["start"], data["end"]) + while self.__mergeStorage(): + pass + self.__storage.sort(key=lambda n: n["start"]) + + def adjustDataStart(self, new_start): + self.__info["data_addr"] = new_start \ No newline at end of file diff --git a/worlds/ladx/LADXR/rom.py b/worlds/ladx/LADXR/rom.py new file mode 100644 index 0000000000..ea4f14089f --- /dev/null +++ b/worlds/ladx/LADXR/rom.py @@ -0,0 +1,76 @@ +import binascii +import Utils + +b2h = binascii.hexlify +h2b = binascii.unhexlify + + +class ROM: + def __init__(self, filename): + data = open(Utils.local_path(filename), "rb").read() + #assert len(data) == 1024 * 1024 + self.banks = [] + for n in range(0x40): + self.banks.append(bytearray(data[n*0x4000:(n+1)*0x4000])) + + def patch(self, bank_nr, addr, old, new, *, fill_nop=False): + new = h2b(new) + bank = self.banks[bank_nr] + if old is not None: + if isinstance(old, int): + old = bank[addr:old] + else: + old = h2b(old) + if fill_nop: + assert len(old) >= len(new), "Length mismatch: %d != %d (%s != %s)" % (len(old), len(new), b2h(old), b2h(new)) + new += b'\x00' * (len(old) - len(new)) + else: + assert len(old) == len(new), "Length mismatch: %d != %d (%s != %s)" % (len(old), len(new), b2h(old), b2h(new)) + assert addr >= 0 and addr + len(old) <= 16*1024 + if bank[addr:addr+len(old)] != old: + if bank[addr:addr + len(old)] == new: + # Patch is already applied. + return + loc = bank.find(old) + while loc > -1: + print("Possible at:", hex(loc)) + loc = bank.find(old, loc+1) + assert False, "Patch mismatch:\n%s !=\n%s at 0x%04x" % (b2h(bank[addr:addr+len(old)]), b2h(old), addr) + bank[addr:addr+len(new)] = new + assert len(bank) == 0x4000 + + def fixHeader(self, *, name=None): + if name is not None: + name = name.encode("utf-8") + name = (name + (b"\x00" * 15))[:15] + self.banks[0][0x134:0x143] = name + + checksum = 0 + for c in self.banks[0][0x134:0x14D]: + checksum -= c + 1 + self.banks[0][0x14D] = checksum & 0xFF + + # zero out the checksum before calculating it. + self.banks[0][0x14E] = 0 + self.banks[0][0x14F] = 0 + checksum = 0 + for bank in self.banks: + checksum = (checksum + sum(bank)) & 0xFFFF + self.banks[0][0x14E] = checksum >> 8 + self.banks[0][0x14F] = checksum & 0xFF + + def save(self, file, *, name=None): + # don't pass the name to fixHeader + self.fixHeader() + if isinstance(file, str): + f = open(file, "wb") + for bank in self.banks: + f.write(bank) + f.close() + print("Saved:", file) + else: + for bank in self.banks: + file.write(bank) + + def readHexSeed(self): + return self.banks[0x3E][0x2F00:0x2F10].hex().upper() diff --git a/worlds/ladx/LADXR/romTables.py b/worlds/ladx/LADXR/romTables.py new file mode 100644 index 0000000000..fbabe7595f --- /dev/null +++ b/worlds/ladx/LADXR/romTables.py @@ -0,0 +1,219 @@ +from .rom import ROM +from .pointerTable import PointerTable +from .assembler import ASM + + +class Texts(PointerTable): + END_OF_DATA = (0xfe, 0xff) + + def __init__(self, rom): + super().__init__(rom, { + "count": 0x2B0, + "pointers_addr": 1, + "pointers_bank": 0x1C, + "banks_addr": 0x741, + "banks_bank": 0x1C, + }) + + +class Entities(PointerTable): + def __init__(self, rom): + super().__init__(rom, { + "count": 0x320, + "pointers_addr": 0, + "pointers_bank": 0x16, + "data_bank": 0x16, + }) + +class RoomsTable(PointerTable): + HEADER = 2 + + def _readData(self, rom, bank_nr, pointer): + bank = rom.banks[bank_nr] + start = pointer + pointer += self.HEADER + while bank[pointer] != 0xFE: + obj_type = (bank[pointer] & 0xF0) + if obj_type == 0xE0: + pointer += 5 + elif obj_type == 0xC0 or obj_type == 0x80: + pointer += 3 + else: + pointer += 2 + pointer += 1 + self._addStorage(bank_nr, start, pointer) + return bank[start:pointer] + + +class RoomsOverworldTop(RoomsTable): + def __init__(self, rom): + super().__init__(rom, { + "count": 0x080, + "pointers_addr": 0x000, + "pointers_bank": 0x09, + "data_bank": 0x09, + "alt_pointers": { + "Alt06": (0x00, 0x31FD), + "Alt0E": (0x00, 0x31CD), + "Alt1B": (0x00, 0x320D), + "Alt2B": (0x00, 0x321D), + "Alt79": (0x00, 0x31ED), + } + }) + + +class RoomsOverworldBottom(RoomsTable): + def __init__(self, rom): + super().__init__(rom, { + "count": 0x080, + "pointers_addr": 0x100, + "pointers_bank": 0x09, + "data_bank": 0x1A, + "alt_pointers": { + "Alt8C": (0x00, 0x31DD), + } + }) + + +class RoomsIndoorA(RoomsTable): + # TODO: The color dungeon tables are in the same bank, but the pointer table is after the room data. + def __init__(self, rom): + super().__init__(rom, { + "count": 0x100, + "pointers_addr": 0x000, + "pointers_bank": 0x0A, + "data_bank": 0x0A, + "alt_pointers": { + "Alt1F5": (0x00, 0x31A1), + } + }) + + +class RoomsIndoorB(RoomsTable): + # Most likely, this table can be expanded all the way to the end of the bank, + # giving a few 100 extra bytes to work with. + def __init__(self, rom): + super().__init__(rom, { + "count": 0x0FF, + "pointers_addr": 0x000, + "pointers_bank": 0x0B, + "data_bank": 0x0B, + }) + + +class RoomsColorDungeon(RoomsTable): + def __init__(self, rom): + super().__init__(rom, { + "count": 0x016, + "pointers_addr": 0x3B77, + "pointers_bank": 0x0A, + "data_bank": 0x0A, + "expand_to_end_of_bank": True + }) + + +class BackgroundTable(PointerTable): + def _readData(self, rom, bank_nr, pointer): + bank = rom.banks[bank_nr] + start = pointer + while bank[pointer] != 0x00: + addr = bank[pointer] << 8 | bank[pointer + 1] + amount = (bank[pointer + 2] & 0x3F) + 1 + repeat = (bank[pointer + 2] & 0x40) == 0x40 + vertical = (bank[pointer + 2] & 0x80) == 0x80 + pointer += 3 + if not repeat: + pointer += amount + if repeat: + pointer += 1 + pointer += 1 + self._addStorage(bank_nr, start, pointer) + return bank[start:pointer] + + +class BackgroundTilesTable(BackgroundTable): + def __init__(self, rom): + super().__init__(rom, { + "count": 0x26, + "pointers_addr": 0x052B, + "pointers_bank": 0x20, + "data_bank": 0x08, + "expand_to_end_of_bank": True + }) + + +class BackgroundAttributeTable(BackgroundTable): + def __init__(self, rom): + super().__init__(rom, { + "count": 0x26, + "pointers_addr": 0x1C4B, + "pointers_bank": 0x24, + "data_bank": 0x24, + "expand_to_end_of_bank": True + }) + + +class OverworldRoomSpriteData(PointerTable): + def __init__(self, rom): + super().__init__(rom, { + "count": 0x100, + "pointers_addr": 0x30D3, + "pointers_bank": 0x20, + "data_bank": 0x20, + "data_addr": 0x33F3, + "data_size": 4, + "claim_storage_gaps": True, + }) + + +class IndoorRoomSpriteData(PointerTable): + def __init__(self, rom): + super().__init__(rom, { + "count": 0x220, + "pointers_addr": 0x31D3, + "pointers_bank": 0x20, + "data_bank": 0x20, + "data_addr": 0x363B, + "data_size": 4, + "claim_storage_gaps": True, + }) + + +class ROMWithTables(ROM): + def __init__(self, filename): + super().__init__(filename) + + # Ability to patch any text in the game with different text + self.texts = Texts(self) + # Ability to modify rooms + self.entities = Entities(self) + self.rooms_overworld_top = RoomsOverworldTop(self) + self.rooms_overworld_bottom = RoomsOverworldBottom(self) + self.rooms_indoor_a = RoomsIndoorA(self) + self.rooms_indoor_b = RoomsIndoorB(self) + self.rooms_color_dungeon = RoomsColorDungeon(self) + self.room_sprite_data_overworld = OverworldRoomSpriteData(self) + self.room_sprite_data_indoor = IndoorRoomSpriteData(self) + + # Backgrounds for things like the title screen. + self.background_tiles = BackgroundTilesTable(self) + self.background_attributes = BackgroundAttributeTable(self) + + self.itemNames = {} + + def save(self, filename, *, name=None): + self.texts.store(self) + self.entities.store(self) + self.rooms_overworld_top.store(self) + self.rooms_overworld_bottom.store(self) + self.rooms_indoor_a.store(self) + self.rooms_indoor_b.store(self) + self.rooms_color_dungeon.store(self) + leftover_storage = self.room_sprite_data_overworld.store(self) + self.room_sprite_data_indoor.addStorage(leftover_storage) + self.patch(0x00, 0x0DFA, ASM("ld hl, $763B"), ASM("ld hl, $%04x" % (leftover_storage[0]["start"] | 0x4000))) + self.room_sprite_data_indoor.adjustDataStart(leftover_storage[0]["start"]) + self.room_sprite_data_indoor.store(self) + self.background_tiles.store(self) + self.background_attributes.store(self) + super().save(filename, name=name) diff --git a/worlds/ladx/LADXR/roomEditor.py b/worlds/ladx/LADXR/roomEditor.py new file mode 100644 index 0000000000..c6cc631363 --- /dev/null +++ b/worlds/ladx/LADXR/roomEditor.py @@ -0,0 +1,584 @@ +import json +from . import entityData + + +WARP_TYPE_IDS = {0xE1, 0xE2, 0xE3, 0xBA, 0xA8, 0xBE, 0xCB, 0xC2, 0xC6} +ALT_ROOM_OVERLAYS = {"Alt06": 0x1040, "Alt0E": 0x1090, "Alt1B": 0x10E0, "Alt2B": 0x1130, "Alt79": 0x1180, "Alt8C": 0x11D0} + + +class RoomEditor: + def __init__(self, rom, room=None): + assert room is not None + self.room = room + self.entities = [] + self.objects = [] + self.tileset_index = None + self.palette_index = None + self.attribset = None + + if isinstance(room, int): + entities_raw = rom.entities[room] + idx = 0 + while entities_raw[idx] != 0xFF: + x = entities_raw[idx] & 0x0F + y = entities_raw[idx] >> 4 + id = entities_raw[idx + 1] + self.entities.append((x, y, id)) + idx += 2 + assert idx == len(entities_raw) - 1 + + if isinstance(room, str): + if room in rom.rooms_overworld_top: + objects_raw = rom.rooms_overworld_top[room] + elif room in rom.rooms_overworld_bottom: + objects_raw = rom.rooms_overworld_bottom[room] + elif room in rom.rooms_indoor_a: + objects_raw = rom.rooms_indoor_a[room] + else: + assert False, "Failed to find alt room: %s" % (room) + else: + if room < 0x080: + objects_raw = rom.rooms_overworld_top[room] + elif room < 0x100: + objects_raw = rom.rooms_overworld_bottom[room - 0x80] + elif room < 0x200: + objects_raw = rom.rooms_indoor_a[room - 0x100] + elif room < 0x300: + objects_raw = rom.rooms_indoor_b[room - 0x200] + else: + objects_raw = rom.rooms_color_dungeon[room - 0x300] + + self.animation_id = objects_raw[0] + self.floor_object = objects_raw[1] + idx = 2 + while objects_raw[idx] != 0xFE: + x = objects_raw[idx] & 0x0F + y = objects_raw[idx] >> 4 + if y == 0x08: # horizontal + count = x + x = objects_raw[idx + 1] & 0x0F + y = objects_raw[idx + 1] >> 4 + self.objects.append(ObjectHorizontal(x, y, objects_raw[idx + 2], count)) + idx += 3 + elif y == 0x0C: # vertical + count = x + x = objects_raw[idx + 1] & 0x0F + y = objects_raw[idx + 1] >> 4 + self.objects.append(ObjectVertical(x, y, objects_raw[idx + 2], count)) + idx += 3 + elif y == 0x0E: # warp + self.objects.append(ObjectWarp(objects_raw[idx] & 0x0F, objects_raw[idx + 1], objects_raw[idx + 2], objects_raw[idx + 3], objects_raw[idx + 4])) + idx += 5 + else: + self.objects.append(Object(x, y, objects_raw[idx + 1])) + idx += 2 + if room is not None: + assert idx == len(objects_raw) - 1 + + if isinstance(room, int) and room < 0x0CC: + self.overlay = rom.banks[0x26][room * 80:room * 80+80] + elif isinstance(room, int) and room < 0x100: + self.overlay = rom.banks[0x27][(room - 0xCC) * 80:(room - 0xCC) * 80 + 80] + elif room in ALT_ROOM_OVERLAYS: + self.overlay = rom.banks[0x27][ALT_ROOM_OVERLAYS[room]:ALT_ROOM_OVERLAYS[room] + 80] + else: + self.overlay = None + + def store(self, rom, new_room_nr=None): + if new_room_nr is None: + new_room_nr = self.room + objects_raw = bytearray([self.animation_id, self.floor_object]) + for obj in self.objects: + objects_raw += obj.export() + objects_raw += bytearray([0xFE]) + + if isinstance(new_room_nr, str): + if new_room_nr in rom.rooms_overworld_top: + rom.rooms_overworld_top[new_room_nr] = objects_raw + elif new_room_nr in rom.rooms_overworld_bottom: + rom.rooms_overworld_bottom[new_room_nr] = objects_raw + elif new_room_nr in rom.rooms_indoor_a: + rom.rooms_indoor_a[new_room_nr] = objects_raw + else: + assert False, "Failed to find alt room: %s" % (new_room_nr) + elif new_room_nr < 0x080: + rom.rooms_overworld_top[new_room_nr] = objects_raw + elif new_room_nr < 0x100: + rom.rooms_overworld_bottom[new_room_nr - 0x80] = objects_raw + elif new_room_nr < 0x200: + rom.rooms_indoor_a[new_room_nr - 0x100] = objects_raw + elif new_room_nr < 0x300: + rom.rooms_indoor_b[new_room_nr - 0x200] = objects_raw + else: + rom.rooms_color_dungeon[new_room_nr - 0x300] = objects_raw + + if isinstance(new_room_nr, int) and new_room_nr < 0x100: + if self.tileset_index is not None: + rom.banks[0x3F][0x3F00 + new_room_nr] = self.tileset_index & 0xFF + if self.attribset is not None: + # With a tileset, comes metatile gbc data that we need to store a proper bank+pointer. + rom.banks[0x1A][0x2476 + new_room_nr] = self.attribset[0] + rom.banks[0x1A][0x1E76 + new_room_nr*2] = self.attribset[1] & 0xFF + rom.banks[0x1A][0x1E76 + new_room_nr*2+1] = self.attribset[1] >> 8 + if self.palette_index is not None: + rom.banks[0x21][0x02ef + new_room_nr] = self.palette_index + + if isinstance(new_room_nr, int): + entities_raw = bytearray() + for entity in self.entities: + entities_raw += bytearray([entity[0] | entity[1] << 4, entity[2]]) + entities_raw += bytearray([0xFF]) + rom.entities[new_room_nr] = entities_raw + + if new_room_nr < 0x0CC: + rom.banks[0x26][new_room_nr * 80:new_room_nr * 80 + 80] = self.overlay + elif new_room_nr < 0x100: + rom.banks[0x27][(new_room_nr - 0xCC) * 80:(new_room_nr - 0xCC) * 80 + 80] = self.overlay + elif new_room_nr in ALT_ROOM_OVERLAYS: + rom.banks[0x27][ALT_ROOM_OVERLAYS[new_room_nr]:ALT_ROOM_OVERLAYS[new_room_nr] + 80] = self.overlay + + def addEntity(self, x, y, type_id): + self.entities.append((x, y, type_id)) + + def removeEntities(self, type_id): + self.entities = list(filter(lambda e: e[2] != type_id, self.entities)) + + def hasEntity(self, type_id): + return any(map(lambda e: e[2] == type_id, self.entities)) + + def changeObject(self, x, y, new_type): + for obj in self.objects: + if obj.x == x and obj.y == y: + obj.type_id = new_type + if self.overlay is not None: + self.overlay[x + y * 10] = new_type + + def removeObject(self, x, y): + self.objects = list(filter(lambda obj: obj.x != x or obj.y != y, self.objects)) + + def moveObject(self, x, y, new_x, new_y): + for obj in self.objects: + if obj.x == x and obj.y == y: + if self.overlay is not None: + self.overlay[x + y * 10] = self.floor_object + self.overlay[new_x + new_y * 10] = obj.type_id + obj.x = new_x + obj.y = new_y + + def getWarps(self): + return list(filter(lambda obj: isinstance(obj, ObjectWarp), self.objects)) + + def updateOverlay(self, preserve_floor=False): + if self.overlay is None: + return + if not preserve_floor: + for n in range(80): + self.overlay[n] = self.floor_object + for obj in self.objects: + if isinstance(obj, ObjectHorizontal): + for n in range(obj.count): + self.overlay[obj.x + n + obj.y * 10] = obj.type_id + elif isinstance(obj, ObjectVertical): + for n in range(obj.count): + self.overlay[obj.x + n * 10 + obj.y * 10] = obj.type_id + elif not isinstance(obj, ObjectWarp): + self.overlay[obj.x + obj.y * 10] = obj.type_id + + def loadFromJson(self, filename): + self.objects = [] + self.entities = [] + self.animation_id = 0 + self.tileset_index = 0x0F + self.palette_index = 0x01 + + data = json.load(open(filename)) + + for prop in data.get("properties", []): + if prop["name"] == "palette": + self.palette_index = int(prop["value"], 16) + elif prop["name"] == "tileset": + self.tileset_index = int(prop["value"], 16) + elif prop["name"] == "animationset": + self.animation_id = int(prop["value"], 16) + elif prop["name"] == "attribset": + bank, _, addr = prop["value"].partition(":") + self.attribset = (int(bank, 16), int(addr, 16) + 0x4000) + + tiles = [0] * 80 + for layer in data["layers"]: + if "data" in layer: + for n in range(80): + if layer["data"][n] > 0: + tiles[n] = (layer["data"][n] - 1) & 0xFF + if "objects" in layer: + for obj in layer["objects"]: + x = int((obj["x"] + obj["width"] / 2) // 16) + y = int((obj["y"] + obj["height"] / 2) // 16) + if obj["type"] == "warp": + warp_type, map_nr, room, x, y = obj["name"].split(":") + self.objects.append(ObjectWarp(int(warp_type), int(map_nr, 16), int(room, 16) & 0xFF, int(x, 16), int(y, 16))) + elif obj["type"] == "entity": + type_id = entityData.NAME.index(obj["name"]) + self.addEntity(x, y, type_id) + elif obj["type"] == "hidden_tile": + self.objects.append(Object(x, y, int(obj["name"], 16))) + self.buildObjectList(tiles, reduce_size=True) + return data + + def getTileArray(self): + if self.room < 0x100: + tiles = [self.floor_object] * 80 + else: + tiles = [self.floor_object & 0x0F] * 80 + def objHSize(type_id): + if type_id == 0xF5: + return 2 + return 1 + def objVSize(type_id): + if type_id == 0xF5: + return 2 + return 1 + def getObject(x, y): + x, y = (x & 15), (y & 15) + if x < 10 and y < 8: + return tiles[x + y * 10] + return 0 + if self.room < 0x100: + def placeObject(x, y, type_id): + if type_id == 0xF5: + if getObject(x, y) in (0x1B, 0x28, 0x29, 0x83, 0x90): + placeObject(x, y, 0x29) + else: + placeObject(x, y, 0x25) + if getObject(x + 1, y) in (0x1B, 0x27, 0x82, 0x86, 0x8A, 0x90, 0x2A): + placeObject(x + 1, y, 0x2A) + else: + placeObject(x + 1, y, 0x26) + if getObject(x, y + 1) in (0x26, 0x2A): + placeObject(x, y + 1, 0x2A) + elif getObject(x, y + 1) == 0x90: + placeObject(x, y + 1, 0x82) + else: + placeObject(x, y + 1, 0x27) + if getObject(x + 1, y + 1) in (0x25, 0x29): + placeObject(x + 1, y + 1, 0x29) + elif getObject(x + 1, y + 1) == 0x90: + placeObject(x + 1, y + 1, 0x83) + else: + placeObject(x + 1, y + 1, 0x28) + elif type_id == 0xF6: # two door house + placeObject(x + 0, y, 0x55) + placeObject(x + 1, y, 0x5A) + placeObject(x + 2, y, 0x5A) + placeObject(x + 3, y, 0x5A) + placeObject(x + 4, y, 0x56) + placeObject(x + 0, y + 1, 0x57) + placeObject(x + 1, y + 1, 0x59) + placeObject(x + 2, y + 1, 0x59) + placeObject(x + 3, y + 1, 0x59) + placeObject(x + 4, y + 1, 0x58) + placeObject(x + 0, y + 2, 0x5B) + placeObject(x + 1, y + 2, 0xE2) + placeObject(x + 2, y + 2, 0x5B) + placeObject(x + 3, y + 2, 0xE2) + placeObject(x + 4, y + 2, 0x5B) + elif type_id == 0xF7: # large house + placeObject(x + 0, y, 0x55) + placeObject(x + 1, y, 0x5A) + placeObject(x + 2, y, 0x56) + placeObject(x + 0, y + 1, 0x57) + placeObject(x + 1, y + 1, 0x59) + placeObject(x + 2, y + 1, 0x58) + placeObject(x + 0, y + 2, 0x5B) + placeObject(x + 1, y + 2, 0xE2) + placeObject(x + 2, y + 2, 0x5B) + elif type_id == 0xF8: # catfish + placeObject(x + 0, y, 0xB6) + placeObject(x + 1, y, 0xB7) + placeObject(x + 2, y, 0x66) + placeObject(x + 0, y + 1, 0x67) + placeObject(x + 1, y + 1, 0xE3) + placeObject(x + 2, y + 1, 0x68) + elif type_id == 0xF9: # palace door + placeObject(x + 0, y, 0xA4) + placeObject(x + 1, y, 0xA5) + placeObject(x + 2, y, 0xA6) + placeObject(x + 0, y + 1, 0xA7) + placeObject(x + 1, y + 1, 0xE3) + placeObject(x + 2, y + 1, 0xA8) + elif type_id == 0xFA: # stone pig head + placeObject(x + 0, y, 0xBB) + placeObject(x + 1, y, 0xBC) + placeObject(x + 0, y + 1, 0xBD) + placeObject(x + 1, y + 1, 0xBE) + elif type_id == 0xFB: # palmtree + if x == 15: + placeObject(x + 1, y + 1, 0xB7) + placeObject(x + 1, y + 2, 0xCE) + else: + placeObject(x + 0, y, 0xB6) + placeObject(x + 0, y + 1, 0xCD) + placeObject(x + 1, y + 0, 0xB7) + placeObject(x + 1, y + 1, 0xCE) + elif type_id == 0xFC: # square "hill with hole" (seen near lvl4 entrance) + placeObject(x + 0, y, 0x2B) + placeObject(x + 1, y, 0x2C) + placeObject(x + 2, y, 0x2D) + placeObject(x + 0, y + 1, 0x37) + placeObject(x + 1, y + 1, 0xE8) + placeObject(x + 2, y + 1, 0x38) + placeObject(x - 1, y + 2, 0x0A) + placeObject(x + 0, y + 2, 0x33) + placeObject(x + 1, y + 2, 0x2F) + placeObject(x + 2, y + 2, 0x34) + placeObject(x + 0, y + 3, 0x0A) + placeObject(x + 1, y + 3, 0x0A) + placeObject(x + 2, y + 3, 0x0A) + placeObject(x + 3, y + 3, 0x0A) + elif type_id == 0xFD: # small house + placeObject(x + 0, y, 0x52) + placeObject(x + 1, y, 0x52) + placeObject(x + 2, y, 0x52) + placeObject(x + 0, y + 1, 0x5B) + placeObject(x + 1, y + 1, 0xE2) + placeObject(x + 2, y + 1, 0x5B) + else: + x, y = (x & 15), (y & 15) + if x < 10 and y < 8: + tiles[x + y * 10] = type_id + else: + def placeObject(x, y, type_id): + x, y = (x & 15), (y & 15) + if type_id == 0xEC: # key door + placeObject(x, y, 0x2D) + placeObject(x + 1, y, 0x2E) + elif type_id == 0xED: + placeObject(x, y, 0x2F) + placeObject(x + 1, y, 0x30) + elif type_id == 0xEE: + placeObject(x, y, 0x31) + placeObject(x, y + 1, 0x32) + elif type_id == 0xEF: + placeObject(x, y, 0x33) + placeObject(x, y + 1, 0x34) + elif type_id == 0xF0: # closed door + placeObject(x, y, 0x35) + placeObject(x + 1, y, 0x36) + elif type_id == 0xF1: + placeObject(x, y, 0x37) + placeObject(x + 1, y, 0x38) + elif type_id == 0xF2: + placeObject(x, y, 0x39) + placeObject(x, y + 1, 0x3A) + elif type_id == 0xF3: + placeObject(x, y, 0x3B) + placeObject(x, y + 1, 0x3C) + elif type_id == 0xF4: # open door + placeObject(x, y, 0x43) + placeObject(x + 1, y, 0x44) + elif type_id == 0xF5: + placeObject(x, y, 0x8C) + placeObject(x + 1, y, 0x08) + elif type_id == 0xF6: + placeObject(x, y, 0x09) + placeObject(x, y + 1, 0x0A) + elif type_id == 0xF7: + placeObject(x, y, 0x0B) + placeObject(x, y + 1, 0x0C) + elif type_id == 0xF8: # boss door + placeObject(x, y, 0xA4) + placeObject(x + 1, y, 0xA5) + elif type_id == 0xF9: # stairs door + placeObject(x, y, 0xAF) + placeObject(x + 1, y, 0xB0) + elif type_id == 0xFA: # flipwall + placeObject(x, y, 0xB1) + placeObject(x + 1, y, 0xB2) + elif type_id == 0xFB: # one way arrow + placeObject(x, y, 0x45) + placeObject(x + 1, y, 0x46) + elif type_id == 0xFC: # entrance + placeObject(x + 0, y, 0xB3) + placeObject(x + 1, y, 0xB4) + placeObject(x + 2, y, 0xB4) + placeObject(x + 3, y, 0xB5) + placeObject(x + 0, y + 1, 0xB6) + placeObject(x + 1, y + 1, 0xB7) + placeObject(x + 2, y + 1, 0xB8) + placeObject(x + 3, y + 1, 0xB9) + placeObject(x + 0, y + 2, 0xBA) + placeObject(x + 1, y + 2, 0xBB) + placeObject(x + 2, y + 2, 0xBC) + placeObject(x + 3, y + 2, 0xBD) + elif type_id == 0xFD: # entrance + placeObject(x, y, 0xC1) + placeObject(x + 1, y, 0xC2) + else: + if x < 10 and y < 8: + tiles[x + y * 10] = type_id + + def addWalls(flags): + for x in range(0, 10): + if flags & 0b0010: + placeObject(x, 0, 0x21) + if flags & 0b0001: + placeObject(x, 7, 0x22) + for y in range(0, 8): + if flags & 0b1000: + placeObject(0, y, 0x23) + if flags & 0b0100: + placeObject(9, y, 0x24) + if flags & 0b1000 and flags & 0b0010: + placeObject(0, 0, 0x25) + if flags & 0b0100 and flags & 0b0010: + placeObject(9, 0, 0x26) + if flags & 0b1000 and flags & 0b0001: + placeObject(0, 7, 0x27) + if flags & 0b0100 and flags & 0b0001: + placeObject(9, 7, 0x28) + + if self.floor_object & 0xF0 == 0x00: + addWalls(0b1111) + if self.floor_object & 0xF0 == 0x10: + addWalls(0b1101) + if self.floor_object & 0xF0 == 0x20: + addWalls(0b1011) + if self.floor_object & 0xF0 == 0x30: + addWalls(0b1110) + if self.floor_object & 0xF0 == 0x40: + addWalls(0b0111) + if self.floor_object & 0xF0 == 0x50: + addWalls(0b1001) + if self.floor_object & 0xF0 == 0x60: + addWalls(0b0101) + if self.floor_object & 0xF0 == 0x70: + addWalls(0b0110) + if self.floor_object & 0xF0 == 0x80: + addWalls(0b1010) + for obj in self.objects: + if isinstance(obj, ObjectWarp): + pass + elif isinstance(obj, ObjectHorizontal): + for n in range(0, obj.count): + placeObject(obj.x + n * objHSize(obj.type_id), obj.y, obj.type_id) + elif isinstance(obj, ObjectVertical): + for n in range(0, obj.count): + placeObject(obj.x, obj.y + n * objVSize(obj.type_id), obj.type_id) + else: + placeObject(obj.x, obj.y, obj.type_id) + return tiles + + def buildObjectList(self, tiles, *, reduce_size=False): + self.objects = [obj for obj in self.objects if isinstance(obj, ObjectWarp)] + tiles = tiles.copy() + if self.overlay: + for n in range(80): + self.overlay[n] = tiles[n] + if reduce_size: + if tiles[n] in {0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, + 0x33, 0x34, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, + 0x48, 0x49, 0x4B, 0x4C, 0x4E, + 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F}: + tiles[n] = 0x3A # Solid tiles + if tiles[n] in {0x08, 0x09, 0x0C, 0x44, + 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF}: + tiles[n] = 0x04 # Open tiles + + is_overworld = isinstance(self.room, str) or self.room < 0x100 + counts = {} + for n in tiles: + if n < 0x0F or is_overworld: + counts[n] = counts.get(n, 0) + 1 + self.floor_object = max(counts, key=counts.get) + for y in range(8) if is_overworld else range(1, 7): + for x in range(10) if is_overworld else range(1, 9): + if tiles[x + y * 10] == self.floor_object: + tiles[x + y * 10] = -1 + for y in range(8): + for x in range(10): + obj = tiles[x + y * 10] + if obj == -1: + continue + w = 1 + h = 1 + while x + w < 10 and tiles[x + w + y * 10] == obj: + w += 1 + while y + h < 8 and tiles[x + (y + h) * 10] == obj: + h += 1 + if obj in {0xE1, 0xE2, 0xE3, 0xBA, 0xC6}: # Entrances should never be horizontal/vertical lists + w = 1 + h = 1 + if w > h: + for n in range(w): + tiles[x + n + y * 10] = -1 + self.objects.append(ObjectHorizontal(x, y, obj, w)) + elif h > 1: + for n in range(h): + tiles[x + (y + n) * 10] = -1 + self.objects.append(ObjectVertical(x, y, obj, h)) + else: + self.objects.append(Object(x, y, obj)) + + +class Object: + def __init__(self, x, y, type_id): + self.x = x + self.y = y + self.type_id = type_id + + def export(self): + return bytearray([self.x | (self.y << 4), self.type_id]) + + def __repr__(self): + return "%s:%d,%d:%02X" % (self.__class__.__name__, self.x, self.y, self.type_id) + + +class ObjectHorizontal(Object): + def __init__(self, x, y, type_id, count): + super().__init__(x, y, type_id) + self.count = count + + def export(self): + return bytearray([0x80 | self.count, self.x | (self.y << 4), self.type_id]) + + def __repr__(self): + return "%s:%d,%d:%02Xx%d" % (self.__class__.__name__, self.x, self.y, self.type_id, self.count) + + +class ObjectVertical(Object): + def __init__(self, x, y, type_id, count): + super().__init__(x, y, type_id) + self.count = count + + def export(self): + return bytearray([0xC0 | self.count, self.x | (self.y << 4), self.type_id]) + + def __repr__(self): + return "%s:%d,%d:%02Xx%d" % (self.__class__.__name__, self.x, self.y, self.type_id, self.count) + + +class ObjectWarp(Object): + def __init__(self, warp_type, map_nr, room_nr, target_x, target_y): + super().__init__(None, None, None) + if warp_type > 0: + # indoor map + if map_nr == 0xff: + room_nr += 0x300 # color dungeon + elif 0x06 <= map_nr < 0x1A: + room_nr += 0x200 # indoor B + else: + room_nr += 0x100 # indoor A + self.warp_type = warp_type + self.room = room_nr + self.map_nr = map_nr + self.target_x = target_x + self.target_y = target_y + + def export(self): + return bytearray([0xE0 | self.warp_type, self.map_nr, self.room & 0xFF, self.target_x, self.target_y]) + + def copy(self): + return ObjectWarp(self.warp_type, self.map_nr, self.room & 0xFF, self.target_x, self.target_y) + + def __repr__(self): + return "%s:%d:%03x:%02x:%d,%d" % (self.__class__.__name__, self.warp_type, self.room, self.map_nr, self.target_x, self.target_y) diff --git a/worlds/ladx/LADXR/settings.py b/worlds/ladx/LADXR/settings.py new file mode 100644 index 0000000000..848d64390d --- /dev/null +++ b/worlds/ladx/LADXR/settings.py @@ -0,0 +1,312 @@ +from typing import List, Tuple, Optional, Union +import os + + +class Setting: + def __init__(self, key: str, + category: str, short_key: str, label: str, *, + description: str, multiworld: bool = True, aesthetic: bool = False, options: Optional[List[Tuple[str, str, str]]] = None, + default: Optional[Union[bool, float, str]] = None, placeholder: Optional[str] = None): + if options: + assert default in [option_key for option_key, option_short, option_label in options], f"{default} not in {options}" + short_options = set() + for option_key, option_short, option_label in options: + assert option_short != "" or option_key == default, f"No short option for non default {label}:{option_key}" + assert option_short not in short_options, "Duplicate short option value..." + short_options.add(option_short) + + self.key = key + self.category = category + self.short_key = short_key + self.label = label + self.description = description + self.multiworld = multiworld + self.aesthetic = aesthetic + self.options = options + self.default = default + self.placeholder = placeholder + + self.value = default + + def set(self, value): + if isinstance(self.default, bool): + if not isinstance(value, bool): + value = bool(int(value)) + elif not isinstance(value, type(self.default)): + try: + value = type(self.default)(value) + except ValueError: + raise ValueError(f"{value} is not an accepted value for {self.key} setting") + if self.options: + if value not in [k for k, s, v in self.options]: + raise ValueError(f"{value} is not an accepted value for {self.key} setting") + self.value = value + + def getShortValue(self): + if self.options: + for option_key, option_short, option_label in self.options: + if self.value == option_key: + return option_short + return self.value + ">" + + def toJson(self): + result = { + "key": self.key, + "category": self.category, + "short_key": self.short_key, + "label": self.label, + "description": self.description, + "multiworld": self.multiworld, + "aesthetic": self.aesthetic, + "default": self.default, + } + if self.options: + result["options"] = [{"key": option_key, "short": option_short, "label": option_label} for option_key, option_short, option_label in self.options] + if self.placeholder: + result["placeholder"] = self.placeholder + return result + + +class Settings: + def __init__(self, ap_options): + self.__all = [ + Setting('seed', 'Main', '<', 'Seed', placeholder='Leave empty for random seed', default="", multiworld=False, + description="""For multiple people to generate the same randomization result, enter the generated seed number here. +Note, not all strings are valid seeds."""), + Setting('logic', 'Main', 'L', 'Logic', options=[('casual', 'c', 'Casual'), ('normal', 'n', 'Normal'), ('hard', 'h', 'Hard'), ('glitched', 'g', 'Glitched'), ('hell', 'H', 'Hell')], default='normal', + description="""Affects where items are allowed to be placed. +[Casual] Same as normal, except that a few more complex options are removed, like removing bushes with powder and killing enemies with powder or bombs. +[Normal] playable without using any tricks or glitches. Requires nothing to be done outside of normal item usage. +[Hard] More advanced techniques may be required, but glitches are not. Examples include tricky jumps, killing enemies with only pots and skipping keys with smart routing. +[Glitched] Advanced glitches and techniques may be required, but extremely difficult or tedious tricks are not required. Examples include Bomb Triggers, Super Jumps and Jesus Jumps. +[Hell] Obscure and hard techniques may be required. Examples include featherless jumping with boots and/or hookshot, sequential pit buffers and unclipped superjumps. Things in here can be extremely hard to do or very time consuming. Only insane people go for this."""), + Setting('forwardfactor', 'Main', 'F', 'Forward Factor', default=0.0, + description="Forward item weight adjustment factor, lower values generate more rear heavy seeds while higher values generate front heavy seeds. Default is 0.5."), + Setting('accessibility', 'Main', 'A', 'Accessibility', options=[('all', 'a', '100% Locations'), ('goal', 'g', 'Beatable')], default='all', + description=""" +[100% Locations] guaranteed that every single item can be reached and gained. +[Beatable] only guarantees that the game is beatable. Certain items/chests might never be reachable."""), + Setting('race', 'Main', 'V', 'Race mode', default=False, multiworld=False, + description=""" +Spoiler logs can not be generated for ROMs generated with race mode enabled, and seed generation is slightly different."""), +# Setting('spoilerformat', 'Main', 'Spoiler Format', options=[('none', 'None'), ('text', 'Text'), ('json', 'JSON')], default='none', multiworld=False, +# description="""Affects how the spoiler log is generated. +# [None] No spoiler log is generated. One can still be manually dumped later. +# [Text] Creates a .txt file meant for a human to read. +# [JSON] Creates a .json file with a little more information and meant for a computer to read.""") + Setting('heartpiece', 'Items', 'h', 'Randomize heart pieces', default=True, + description='Includes heart pieces in the item pool'), + Setting('seashells', 'Items', 's', 'Randomize hidden seashells', default=True, + description='Randomizes the secret sea shells hiding in the ground/trees. (chest are always randomized)'), + Setting('heartcontainers', 'Items', 'H', 'Randomize heart containers', default=True, + description='Includes boss heart container drops in the item pool'), + Setting('instruments', 'Items', 'I', 'Randomize instruments', default=False, + description='Instruments are placed on random locations, dungeon goal will just contain a random item.'), + Setting('tradequest', 'Items', 'T', 'Randomize trade quest', default=True, + description='Trade quest items are randomized, each NPC takes its normal trade quest item, but gives a random item'), + Setting('witch', 'Items', 'W', 'Randomize item given by the witch', default=True, + description='Adds both the toadstool and the reward for giving the toadstool to the witch to the item pool'), + Setting('rooster', 'Items', 'R', 'Add the rooster', default=True, + description='Adds the rooster to the item pool. Without this option, the rooster spot is still a check giving an item. But you will never find the rooster. Any rooster spot is accessible without rooster by other means.'), + Setting('boomerang', 'Items', 'Z', 'Boomerang trade', options=[('default', 'd', 'Normal'), ('trade', 't', 'Trade'), ('gift', 'g', 'Gift')], default='gift', + description=""" +[Normal], requires magnifier to get the boomerang. +[Trade], allows to trade an inventory item for a random other inventory item boomerang is shuffled. +[Gift], You get a random gift of any item, and the boomerang is shuffled."""), + Setting('randomstartlocation', 'Gameplay', 'r', 'Random start location', default=False, + description='Randomize where your starting house is located'), + Setting('dungeonshuffle', 'Gameplay', 'u', 'Dungeon shuffle', default=False, + description='Randomizes the dungeon that each dungeon entrance leads to'), + Setting('entranceshuffle', 'Gameplay', 'E', 'Entrance randomizer', options=[("none", '', "Default"), ("simple", 's', "Simple"), ("advanced", 'a', "Advanced"), ("expert", 'E', "Expert"), ("insanity", 'I', "Insanity")], default='none', + description="""Randomizes where overworld entrances lead to. +[Simple] single entrance caves that contain items are randomized +[Advanced] Connector caves are also randomized +[Expert] Caves/houses without items are also randomized +[Insanity] A few very annoying entrances will be randomized as well. +If random start location and/or dungeon shuffle is enabled, then these will be shuffled with all the entrances. +Note, some entrances can lead into water, use the warp-to-home from the save&quit menu to escape this."""), + Setting('boss', 'Gameplay', 'B', 'Boss shuffle', options=[('default', '', 'Normal'), ('shuffle', 's', 'Shuffle'), ('random', 'r', 'Randomize')], default='default', + description='Randomizes the dungeon bosses that each dungeon has'), + Setting('miniboss', 'Gameplay', 'b', 'Miniboss shuffle', options=[('default', '', 'Normal'), ('shuffle', 's', 'Shuffle'), ('random', 'r', 'Randomize')], default='default', + description='Randomizes the dungeon minibosses that each dungeon has'), + Setting('goal', 'Gameplay', 'G', 'Goal', options=[('8', '8', '8 instruments'), ('7', '7', '7 instruments'), ('6', '6', '6 instruments'), + ('5', '5', '5 instruments'), ('4', '4', '4 instruments'), ('3', '3', '3 instruments'), + ('2', '2', '2 instruments'), ('1', '1', '1 instrument'), ('0', '0', 'No instruments'), + ('open', 'O', 'Egg already open'), ('random', 'R', 'Random instrument count'), + ('open-4', '<', 'Random short game (0-4)'), ('5-8', '>', 'Random long game (5-8)'), + ('seashells', 'S', 'Seashell hunt (20)'), ('bingo', 'b', 'Bingo!'), + ('bingo-full', 'B', 'Bingo-25!')], default='8', + description="""Changes the goal of the game. +[1-8 instruments], number of instruments required to open the egg. +[No instruments] open the egg without instruments, still requires the ocarina with the balled of the windfish +[Egg already open] the egg is already open, just head for it once you have the items needed to defeat the boss. +[Randomized instrument count] random number of instruments required to open the egg, between 0 and 8. +[Random short/long game] random number of instruments required to open the egg, chosen between 0-4 and 5-8 respectively. +[Seashell hunt] egg will open once you collected 20 seashells. Instruments are replaced by seashells and shuffled. +[Bingo] Generate a 5x5 bingo board with various goals. Complete one row/column or diagonal to win! +[Bingo-25] Bingo, but need to fill the whole bingo card to win!"""), + Setting('itempool', 'Gameplay', 'P', 'Item pool', options=[('', '', 'Normal'), ('casual', 'c', 'Casual'), ('pain', 'p', 'Path of Pain'), ('keyup', 'k', 'More keys')], default='', + description="""Effects which items are shuffled. +[Casual] places more inventory and key items so the seed is easier. +[More keys] adds more small keys and extra nightmare keys so dungeons are easier. +[Path of pain]... just find out yourself."""), + Setting('hpmode', 'Gameplay', 'm', 'Health mode', options=[('default', '', 'Normal'), ('inverted', 'i', 'Inverted'), ('1', '1', 'Start with 1 heart'), ('low', 'l', 'Low max')], default='default', + description=""" +[Normal} health works as you would expect. +[Inverted] you start with 9 heart containers, but killing a boss will take a heartcontainer instead of giving one. +[Start with 1] normal game, you just start with 1 heart instead of 3. +[Low max] replace heart containers with heart pieces."""), + Setting('hardmode', 'Gameplay', 'X', 'Hard mode', options=[('none', '', 'Disabled'), ('oracle', 'O', 'Oracle'), ('hero', 'H', 'Hero'), ('ohko', '1', 'One hit KO')], default='none', + description=""" +[Oracle] Less iframes and heath from drops. Bombs damage yourself. Water damages you without flippers. No piece of power or acorn. +[Hero] Switch version hero mode, double damage, no heart/fairy drops. +[One hit KO] You die on a single hit, always."""), + Setting('steal', 'Gameplay', 't', 'Stealing from the shop', + options=[('always', 'a', 'Always'), ('never', 'n', 'Never'), ('default', '', 'Normal')], default='default', + description="""Effects when you can steal from the shop. Stealing is bad and never in logic. +[Normal] requires the sword before you can steal. +[Always] you can always steal from the shop +[Never] you can never steal from the shop."""), + Setting('bowwow', 'Special', 'g', 'Good boy mode', options=[('normal', '', 'Disabled'), ('always', 'a', 'Enabled'), ('swordless', 's', 'Enabled (swordless)')], default='normal', + description='Allows BowWow to be taken into any area, damage bosses and more enemies. If enabled you always start with bowwow. Swordless option removes the swords from the game and requires you to beat the game without a sword and just bowwow.'), + Setting('overworld', 'Special', 'O', 'Overworld', options=[('normal', '', 'Normal'), ('dungeondive', 'D', 'Dungeon dive'), ('nodungeons', 'N', 'No dungeons'), ('random', 'R', 'Randomized')], default='normal', + description=""" +[Dungeon Dive] Create a different overworld where all the dungeons are directly accessible and almost no chests are located in the overworld. +[No dungeons] All dungeons only consist of a boss fight and a instrument reward. Rest of the dungeon is removed. +[Random] Creates a randomized overworld WARNING: This will error out often during generation, work in progress."""), + Setting('owlstatues', 'Special', 'o', 'Owl statues', options=[('', '', 'Never'), ('dungeon', 'D', 'In dungeons'), ('overworld', 'O', 'On the overworld'), ('both', 'B', 'Dungeons and Overworld')], default='', + description='Replaces the hints from owl statues with additional randomized items'), + Setting('superweapons', 'Special', 'q', 'Enable super weapons', default=False, + description='All items will be more powerful, faster, harder, bigger stronger. You name it.'), + Setting('quickswap', 'User options', 'Q', 'Quickswap', options=[('none', '', 'Disabled'), ('a', 'a', 'Swap A button'), ('b', 'b', 'Swap B button')], default='none', + description='Adds that the select button swaps with either A or B. The item is swapped with the top inventory slot. The map is not available when quickswap is enabled.', + aesthetic=True), + Setting('textmode', 'User options', 'f', 'Text mode', options=[('fast', '', 'Fast'), ('default', 'd', 'Normal'), ('none', 'n', 'No-text')], default='fast', + description="""[Fast] makes text appear twice as fast. +[No-Text] removes all text from the game""", aesthetic=True), + Setting('lowhpbeep', 'User options', 'p', 'Low HP beeps', options=[('none', 'D', 'Disabled'), ('slow', 'S', 'Slow'), ('default', 'N', 'Normal')], default='slow', + description='Slows or disables the low health beeping sound', aesthetic=True), + Setting('noflash', 'User options', 'l', 'Remove flashing lights', default=True, + description='Remove the flashing light effects from Mamu, shopkeeper and MadBatter. Useful for capture cards and people that are sensitive for these things.', + aesthetic=True), + Setting('nagmessages', 'User options', 'S', 'Show nag messages', default=False, + description='Enables the nag messages normally shown when touching stones and crystals', + aesthetic=True), + Setting('gfxmod', 'User options', 'c', 'Graphics', default='', + description='Generally affects at least Link\'s sprite, but can alter any graphics in the game', + aesthetic=True), + Setting('linkspalette', 'User options', 'C', "Link's color", + options=[('-1', '-', 'Normal'), ('0', '0', 'Green'), ('1', '1', 'Yellow'), ('2', '2', 'Red'), ('3', '3', 'Blue'), + ('4', '4', '?? A'), ('5', '5', '?? B'), ('6', '6', '?? C'), ('7', '7', '?? D')], default='-1', aesthetic=True, + description="""Allows you to force a certain color on link. +[Normal] color of link depends on the tunic. +[Green/Yellow/Red/Blue] forces link into one of these colors. +[?? A/B/C/D] colors of link are usually inverted and color depends on the area you are in."""), + Setting('music', 'User options', 'M', 'Music', options=[('', '', 'Default'), ('random', 'r', 'Random'), ('off', 'o', 'Disable')], default='', + description=""" +[Random] Randomizes overworld and dungeon music' +[Disable] no music in the whole game""", + aesthetic=True), + ] + self.__by_key = {s.key: s for s in self.__all} + + # Make sure all short keys are unique + short_keys = set() + for s in self.__all: + assert s.short_key not in short_keys, s.label + short_keys.add(s.short_key) + self.ap_options = ap_options + + for option in self.ap_options.values(): + if not hasattr(option, 'to_ladxr_option'): + continue + name, value = option.to_ladxr_option(self.ap_options) + if value == "true": + value = 1 + elif value == "false": + value = 0 + + if name: + self.set( f"{name}={value}") + + def __getattr__(self, item): + return self.__by_key[item].value + + def __setattr__(self, key, value): + if not key.startswith("_") and key in self.__by_key: + self.__by_key[key].set(value) + else: + super().__setattr__(key, value) + + def loadShortString(self, value): + for setting in self.__all: + if isinstance(setting.default, bool): + setting.value = False + index = 0 + while index < len(value): + key = value[index] + index += 1 + for setting in self.__all: + if setting.short_key != key: + continue + if isinstance(setting.default, bool): + setting.value = True + elif setting.options: + for option_key, option_short, option_label in setting.options: + if option_key != setting.default and value[index:].startswith(option_short): + setting.value = option_key + index += len(option_short) + break + else: + end_of_param = value.find(">", index) + setting.value = value[index:end_of_param] + index = end_of_param + 1 + + def getShortString(self): + result = "" + for setting in self.__all: + if isinstance(setting.default, bool): + if setting.value: + result += setting.short_key + elif setting.value != setting.default: + result += setting.short_key + setting.getShortValue() + return result + + def validate(self): + def req(setting: str, value: str, message: str) -> None: + if getattr(self, setting) != value: + print("Warning: %s (setting adjusted automatically)" % message) + setattr(self, setting, value) + + def dis(setting: str, value: str, new_value: str, message: str) -> None: + if getattr(self, setting) == value: + print("Warning: %s (setting adjusted automatically)" % message) + setattr(self, setting, new_value) + + if self.goal in ("bingo", "bingo-full"): + req("overworld", "normal", "Bingo goal does not work with dungeondive") + req("accessibility", "all", "Bingo goal needs 'all' accessibility") + dis("steal", "never", "default", "With bingo goal, stealing should be allowed") + dis("boss", "random", "shuffle", "With bingo goal, bosses need to be on normal or shuffle") + dis("miniboss", "random", "shuffle", "With bingo goal, minibosses need to be on normal or shuffle") + if self.overworld == "dungeondive": + dis("goal", "seashells", "8", "Dungeon dive does not work with seashell goal") + if self.overworld == "nodungeons": + dis("goal", "seashells", "8", "No dungeons does not work with seashell goal") + if self.overworld == "random": + self.goal = "4" # Force 4 dungeon goal for random overworld right now. + + def set(self, value: str) -> None: + if "=" in value: + key, value = value.split("=", 1) + else: + key, value = value, "1" + if key not in self.__by_key: + raise ValueError(f"Setting {key} not found") + self.__by_key[key].set(value) + + def toJson(self): + return [s.toJson() for s in self.__all] + + def __iter__(self): + return iter(self.__all) diff --git a/worlds/ladx/LADXR/utils.py b/worlds/ladx/LADXR/utils.py new file mode 100644 index 0000000000..fcf1d2bb56 --- /dev/null +++ b/worlds/ladx/LADXR/utils.py @@ -0,0 +1,222 @@ +from typing import Optional + +from .locations.items import * + +_NAMES = { + SWORD: "Sword", + BOMB: "Bombs", + POWER_BRACELET: "Power Bracelet", + SHIELD: "Shield", + BOW: "Bow", + HOOKSHOT: "Hookshot", + MAGIC_ROD: "Magic Rod", + PEGASUS_BOOTS: "Pegasus Boots", + OCARINA: "Ocarina", + FEATHER: "Roc's Feather", + SHOVEL: "Shovel", + MAGIC_POWDER: "Magic Powder", + BOOMERANG: "Boomerang", + ROOSTER: "Flying Rooster", + + FLIPPERS: "Flippers", + SLIME_KEY: "Slime key", + TAIL_KEY: "Tail key", + ANGLER_KEY: "Angler key", + FACE_KEY: "Face key", + BIRD_KEY: "Bird key", + GOLD_LEAF: "Golden leaf", + + "RUPEE": "Rupee", + "RUPEES": "Rupees", + RUPEES_50: "50 Rupees", + RUPEES_20: "20 Rupees", + RUPEES_100: "100 Rupees", + RUPEES_200: "200 Rupees", + RUPEES_500: "500 Rupees", + SEASHELL: "Secret Seashell", + + KEY: "Small Key", + KEY1: "Key for Tail Cave", + KEY2: "Key for Bottle Grotto", + KEY3: "Key for Key Cavern", + KEY4: "Key for Angler's Tunnel", + KEY5: "Key for Catfish's Maw", + KEY6: "Key for Face Shrine", + KEY7: "Key for Eagle's Tower", + KEY8: "Key for Turtle Rock", + KEY9: "Key for Color Dungeon", + + MAP: "Dungeon Map", + MAP1: "Map for Tail Cave", + MAP2: "Map for Bottle Grotto", + MAP3: "Map for Key Cavern", + MAP4: "Map for Angler's Tunnel", + MAP5: "Map for Catfish's Maw", + MAP6: "Map for Face Shrine", + MAP7: "Map for Eagle's Tower", + MAP8: "Map for Turtle Rock", + MAP9: "Map for Color Dungeon", + + COMPASS: "Dungeon Compass", + COMPASS1: "Compass for Tail Cave", + COMPASS2: "Compass for Bottle Grotto", + COMPASS3: "Compass for Key Cavern", + COMPASS4: "Compass for Angler's Tunnel", + COMPASS5: "Compass for Catfish's Maw", + COMPASS6: "Compass for Face Shrine", + COMPASS7: "Compass for Eagle's Tower", + COMPASS8: "Compass for Turtle Rock", + COMPASS9: "Compass for Color Dungeon", + + STONE_BEAK: "Stone Beak", + STONE_BEAK1: "Stone Beak for Tail Cave", + STONE_BEAK2: "Stone Beak for Bottle Grotto", + STONE_BEAK3: "Stone Beak for Key Cavern", + STONE_BEAK4: "Stone Beak for Angler's Tunnel", + STONE_BEAK5: "Stone Beak for Catfish's Maw", + STONE_BEAK6: "Stone Beak for Face Shrine", + STONE_BEAK7: "Stone Beak for Eagle's Tower", + STONE_BEAK8: "Stone Beak for Turtle Rock", + STONE_BEAK9: "Stone Beak for Color Dungeon", + + NIGHTMARE_KEY: "Nightmare Key", + NIGHTMARE_KEY1: "Nightmare Key for Tail Cave", + NIGHTMARE_KEY2: "Nightmare Key for Bottle Grotto", + NIGHTMARE_KEY3: "Nightmare Key for Key Cavern", + NIGHTMARE_KEY4: "Nightmare Key for Angler's Tunnel", + NIGHTMARE_KEY5: "Nightmare Key for Catfish's Maw", + NIGHTMARE_KEY6: "Nightmare Key for Face Shrine", + NIGHTMARE_KEY7: "Nightmare Key for Eagle's Tower", + NIGHTMARE_KEY8: "Nightmare Key for Turtle Rock", + NIGHTMARE_KEY9: "Nightmare Key for Color Dungeon", + + HEART_PIECE: "Piece of Heart", + BOWWOW: "Bowwow", + ARROWS_10: "10 Arrows", + SINGLE_ARROW: "Single Arrow", + MEDICINE: "Medicine", + + MAX_POWDER_UPGRADE: "Magic Powder upgrade", + MAX_BOMBS_UPGRADE: "Bombs upgrade", + MAX_ARROWS_UPGRADE: "Arrows upgrade", + + RED_TUNIC: "Red Tunic", + BLUE_TUNIC: "Blue Tunic", + + HEART_CONTAINER: "Heart Container", + BAD_HEART_CONTAINER: "Anti-Heart Container", + + TOADSTOOL: "Toadstool", + + 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 Lens", + GEL: "Slimy Surprise", + MESSAGE: "A Special Message From Our Sponsors" +} + + +def setReplacementName(key: str, value: str) -> None: + _NAMES[key] = value + + +def formatText(instr: str, *, center: bool = False, ask: Optional[str] = None) -> bytes: + instr = instr.format(**_NAMES) + s = instr.encode("ascii") + s = s.replace(b"'", b"^") + + def padLine(line: bytes) -> bytes: + return line + b' ' * (16 - len(line)) + if center: + def padLine(line: bytes) -> bytes: + padding = (16 - len(line)) + return b' ' * (padding // 2) + line + b' ' * (padding - padding // 2) + + result = b'' + for line in s.split(b'\n'): + result_line = b'' + for word in line.split(b' '): + if len(result_line) + 1 + len(word) > 16: + result += padLine(result_line) + result_line = b'' + elif result_line: + result_line += b' ' + result_line += word + if result_line: + result += padLine(result_line) + if ask is not None: + askbytes = ask.encode("ascii") + result = result.rstrip() + while len(result) % 32 != 16: + result += b' ' + return result + b' ' + askbytes + b'\xfe' + return result.rstrip() + b'\xff' + + +def tileDataToString(data: bytes, key: str = " 123") -> str: + result = "" + for n in range(0, len(data), 2): + a = data[n] + b = data[n+1] + for m in range(8): + bit = 0x80 >> m + if (a & bit) and (b & bit): + result += key[3] + elif (b & bit): + result += key[2] + elif (a & bit): + result += key[1] + else: + result += key[0] + result += "\n" + return result.rstrip("\n") + + +def createTileData(data: str, key: str = " 123") -> bytes: + result = [] + for line in data.split("\n"): + line = line + " " + a = 0 + b = 0 + for n in range(8): + if line[n] == key[3]: + a |= 0x80 >> n + b |= 0x80 >> n + elif line[n] == key[2]: + b |= 0x80 >> n + elif line[n] == key[1]: + a |= 0x80 >> n + result.append(a) + result.append(b) + assert (len(result) % 16) == 0, len(result) + return bytes(result) + + +if __name__ == "__main__": + data = formatText("It is dangurous to go alone.\nTake\nthis\na\nline.") + for i in range(0, len(data), 16): + print(data[i:i+16]) diff --git a/worlds/ladx/LADXR/worldSetup.py b/worlds/ladx/LADXR/worldSetup.py new file mode 100644 index 0000000000..d7ca37f203 --- /dev/null +++ b/worlds/ladx/LADXR/worldSetup.py @@ -0,0 +1,136 @@ +from .patches import enemies, bingo +from .locations.items import * +from .entranceInfo import ENTRANCE_INFO + + + +MULTI_CHEST_OPTIONS = [MAGIC_POWDER, BOMB, MEDICINE, RUPEES_50, RUPEES_20, RUPEES_100, RUPEES_200, RUPEES_500, SEASHELL, GEL, ARROWS_10, SINGLE_ARROW] +MULTI_CHEST_WEIGHTS = [20, 20, 20, 50, 50, 20, 10, 5, 5, 20, 10, 10] + +# List of all the possible locations where we can place our starting house +start_locations = [ + "phone_d8", + "rooster_house", + "writes_phone", + "castle_phone", + "photo_house", + "start_house", + "prairie_right_phone", + "banana_seller", + "prairie_low_phone", + "animal_phone", +] + + +class WorldSetup: + def __init__(self): + self.entrance_mapping = {k: k for k in ENTRANCE_INFO.keys()} + self.boss_mapping = list(range(9)) + self.miniboss_mapping = { + # Main minibosses + 0: "ROLLING_BONES", 1: "HINOX", 2: "DODONGO", 3: "CUE_BALL", 4: "GHOMA", 5: "SMASHER", 6: "GRIM_CREEPER", 7: "BLAINO", + # Color dungeon needs to be special, as always. + "c1": "AVALAUNCH", "c2": "GIANT_BUZZ_BLOB", + # Overworld + "moblin_cave": "MOBLIN_KING", + "armos_temple": "ARMOS_KNIGHT", + } + self.goal = None + self.bingo_goals = None + self.multichest = RUPEES_20 + self.map = None # Randomly generated map data + + def getEntrancePool(self, settings, connectorsOnly=False): + entrances = [] + + if connectorsOnly: + if settings.entranceshuffle in ("advanced", "expert", "insanity"): + entrances = [k for k, v in ENTRANCE_INFO.items() if v.type == "connector"] + + return entrances + + if settings.dungeonshuffle and settings.entranceshuffle == "none": + entrances = [k for k, v in ENTRANCE_INFO.items() if v.type == "dungeon"] + if settings.entranceshuffle in ("simple", "advanced", "expert", "insanity"): + types = {"single"} + if settings.tradequest: + types.add("trade") + if settings.entranceshuffle in ("expert", "insanity"): + types.update(["dummy", "trade"]) + if settings.entranceshuffle in ("insanity",): + types.add("insanity") + if settings.randomstartlocation: + types.add("start") + if settings.dungeonshuffle: + types.add("dungeon") + entrances = [k for k, v in ENTRANCE_INFO.items() if v.type in types] + + return entrances + + def randomize(self, settings, rnd): + if settings.overworld == "dungeondive": + self.entrance_mapping = {"d%d" % (n): "d%d" % (n) for n in range(9)} + if settings.randomstartlocation and settings.entranceshuffle == "none": + start_location = start_locations[rnd.randrange(len(start_locations))] + if start_location != "start_house": + self.entrance_mapping[start_location] = "start_house" + self.entrance_mapping["start_house"] = start_location + + entrances = self.getEntrancePool(settings) + for entrance in entrances.copy(): + self.entrance_mapping[entrance] = entrances.pop(rnd.randrange(len(entrances))) + + # Shuffle connectors among themselves + entrances = self.getEntrancePool(settings, connectorsOnly=True) + for entrance in entrances.copy(): + self.entrance_mapping[entrance] = entrances.pop(rnd.randrange(len(entrances))) + + if settings.boss != "default": + values = list(range(9)) + if settings.heartcontainers: + # Color dungeon boss does not drop a heart container so we cannot shuffle him when we + # have heart container shuffling + values.remove(8) + self.boss_mapping = [] + for n in range(8 if settings.heartcontainers else 9): + value = rnd.choice(values) + self.boss_mapping.append(value) + if value in (3, 6) or settings.boss == "shuffle": + values.remove(value) + if settings.heartcontainers: + self.boss_mapping += [8] + if settings.miniboss != "default": + values = [name for name in self.miniboss_mapping.values()] + for key in self.miniboss_mapping.keys(): + self.miniboss_mapping[key] = rnd.choice(values) + if settings.miniboss == 'shuffle': + values.remove(self.miniboss_mapping[key]) + + if settings.goal == 'random': + self.goal = rnd.randint(-1, 8) + elif settings.goal == 'open': + self.goal = -1 + elif settings.goal in {"seashells", "bingo", "bingo-full"}: + self.goal = settings.goal + elif "-" in settings.goal: + a, b = settings.goal.split("-") + if a == "open": + a = -1 + self.goal = rnd.randint(int(a), int(b)) + else: + self.goal = int(settings.goal) + if self.goal in {"bingo", "bingo-full"}: + self.bingo_goals = bingo.randomizeGoals(rnd, settings) + + self.multichest = rnd.choices(MULTI_CHEST_OPTIONS, MULTI_CHEST_WEIGHTS)[0] + + def loadFromRom(self, rom): + import patches.overworld + if patches.overworld.isNormalOverworld(rom): + import patches.entrances + self.entrance_mapping = patches.entrances.readEntrances(rom) + else: + self.entrance_mapping = {"d%d" % (n): "d%d" % (n) for n in range(9)} + self.boss_mapping = patches.enemies.readBossMapping(rom) + self.miniboss_mapping = patches.enemies.readMiniBossMapping(rom) + self.goal = 8 # Better then nothing diff --git a/worlds/ladx/Locations.py b/worlds/ladx/Locations.py new file mode 100644 index 0000000000..69eb78dd88 --- /dev/null +++ b/worlds/ladx/Locations.py @@ -0,0 +1,247 @@ +from BaseClasses import Region, Entrance, Location +from worlds.AutoWorld import LogicMixin + + +from .LADXR.checkMetadata import checkMetadataTable +from .Common import * +from worlds.generic.Rules import add_item_rule +from .Items import ladxr_item_to_la_item_name, ItemName, LinksAwakeningItem +from .LADXR.locations.tradeSequence import TradeRequirements, TradeSequenceItem + +prefilled_events = ["ANGLER_KEYHOLE", "RAFT", "MEDICINE2", "CASTLE_BUTTON"] + +links_awakening_dungeon_names = [ + "Tail Cave", + "Bottle Grotto", + "Key Cavern", + "Angler's Tunnel", + "Catfish's Maw", + "Face Shrine", + "Eagle's Tower", + "Turtle Rock", + "Color Dungeon" +] + + +def meta_to_name(meta): + return f"{meta.name} ({meta.area})" + + +def get_locations_to_id(): + ret = { + + } + + # Magic to generate unique ids + for s, v in checkMetadataTable.items(): + if s == "None": + continue + splits = s.split("-") + + main_id = int(splits[0], 16) + sub_id = 0 + if len(splits) > 1: + sub_id = splits[1] + if sub_id.isnumeric(): + sub_id = (int(sub_id) + 1) * 1000 + else: + sub_id = 1000 + name = f"{v.name} ({v.area})" + ret[name] = BASE_ID + main_id + sub_id + + return ret + + +locations_to_id = get_locations_to_id() + + +class LinksAwakeningLocation(Location): + game = LINKS_AWAKENING + dungeon = None + + def __init__(self, player: int, region, ladxr_item): + name = meta_to_name(ladxr_item.metadata) + + self.event = ladxr_item.event is not None + if self.event: + name = ladxr_item.event + + address = None + if not self.event: + address = locations_to_id[name] + super().__init__(player, name, address) + self.parent_region = region + self.ladxr_item = ladxr_item + + def filter_item(item): + if not ladxr_item.MULTIWORLD and item.player != player: + return False + return True + add_item_rule(self, filter_item) + + +def has_free_weapon(state: "CollectionState", player: int) -> bool: + return state.has("Progressive Sword", player) or state.has("Magic Rod", player) or state.has("Boomerang", player) or state.has("Hookshot", player) + +# If the player has access to farm enough rupees to afford a game, we assume that they can keep beating the game +def can_farm_rupees(state: "CollectionState", player: int) -> bool: + return has_free_weapon(state, player) and (state.has("Can Play Trendy Game", player=player) or state.has("RAFT", player=player)) + + +class LinksAwakeningLogic(LogicMixin): + rupees = { + ItemName.RUPEES_20: 0, + ItemName.RUPEES_50: 0, + ItemName.RUPEES_100: 100, + ItemName.RUPEES_200: 200, + ItemName.RUPEES_500: 500, + } + + def get_credits(self, player: int): + if can_farm_rupees(self, player): + return 999999999 + return sum(self.count(item_name, player) * amount for item_name, amount in self.rupees.items()) + + +class LinksAwakeningRegion(Region): + dungeon_index = None + ladxr_region = None + + def __init__(self, name, ladxr_region, hint, player, world): + super().__init__(name, player, world, hint) + if ladxr_region: + self.ladxr_region = ladxr_region + if ladxr_region.dungeon: + self.dungeon_index = ladxr_region.dungeon + + +def translate_item_name(item): + if item in ladxr_item_to_la_item_name: + return ladxr_item_to_la_item_name[item] + + return item + + +class GameStateAdapater: + def __init__(self, state, player): + self.state = state + self.player = player + + def __contains__(self, item): + if item.endswith("_USED"): + return False + if item in ladxr_item_to_la_item_name: + item = ladxr_item_to_la_item_name[item] + + return self.state.has(item, self.player) + + def get(self, item, default): + if item == "RUPEES": + return self.state.get_credits(self.player) + elif item.endswith("_USED"): + return 0 + else: + item = ladxr_item_to_la_item_name[item] + return self.state.prog_items.get((item, self.player), default) + + +class LinksAwakeningEntrance(Entrance): + def __init__(self, player: int, name, region, condition): + super().__init__(player, name, region) + if isinstance(condition, str): + if condition in ladxr_item_to_la_item_name: + # Test if in inventory + self.condition = ladxr_item_to_la_item_name[condition] + else: + # Event + self.condition = condition + elif condition: + # rewrite condition + # .copyWithModifiedItemNames(translate_item_name) + self.condition = condition + else: + self.condition = None + + def access_rule(self, state): + if isinstance(self.condition, str): + return state.has(self.condition, self.player) + if self.condition is None: + return True + + return self.condition.test(GameStateAdapater(state, self.player)) + + +# Helper to apply function to every ladxr region +def walk_ladxdr(f, n, walked=set()): + if n in walked: + return + f(n) + walked.add(n) + + for o, req in n.simple_connections: + walk_ladxdr(f, o, walked) + for o, req in n.gated_connections: + walk_ladxdr(f, o, walked) + + +def ladxr_region_to_name(n): + name = n.name + if not name: + if len(n.items) == 1: + meta = n.items[0].metadata + name = f"{meta.name} ({meta.area})" + elif n.dungeon: + name = f"D{n.dungeon} Room" + else: + name = "No Name" + + return name + + +def create_regions_from_ladxr(player, multiworld, logic): + tmp = set() + + def print_items(n): + print(f"Creating Region {ladxr_region_to_name(n)}") + print("Has simple connections:") + for region, info in n.simple_connections: + print(" " + ladxr_region_to_name(region) + " | " + str(info)) + print("Has gated connections:") + + for region, info in n.gated_connections: + print(" " + ladxr_region_to_name(region) + " | " + str(info)) + + print("Has Locations:") + for item in n.items: + print(" " + str(item.metadata)) + print() + + used_names = {} + + regions = {} + + # Create regions + for l in logic.location_list: + # Temporarily uniqueify the name, until all regions are named + name = ladxr_region_to_name(l) + index = used_names.get(name, 0) + 1 + used_names[name] = index + if index != 1: + name += f" {index}" + + r = LinksAwakeningRegion( + name=name, ladxr_region=l, hint="", player=player, world=multiworld) + r.locations = [LinksAwakeningLocation(player, r, i) for i in l.items] + regions[l] = r + + for ladxr_location in logic.location_list: + for connection_location, connection_condition in ladxr_location.simple_connections + ladxr_location.gated_connections: + region_a = regions[ladxr_location] + region_b = regions[connection_location] + # TODO: This name ain't gonna work for entrance rando, we need to cross reference with logic.world.overworld_entrance + entrance = LinksAwakeningEntrance( + player, f"{region_a.name} -> {region_b.name}", region_a, connection_condition) + region_a.exits.append(entrance) + entrance.connect(region_b) + + return list(regions.values()) diff --git a/worlds/ladx/Options.py b/worlds/ladx/Options.py new file mode 100644 index 0000000000..8d30186670 --- /dev/null +++ b/worlds/ladx/Options.py @@ -0,0 +1,400 @@ +import os.path +import typing +import logging +from Options import Choice, Option, Toggle, DefaultOnToggle, Range, FreeText +from collections import defaultdict +import Utils + +DefaultOffToggle = Toggle + +logger = logging.getLogger("Link's Awakening Logger") + + +class LADXROption: + def to_ladxr_option(self, all_options): + if not self.ladxr_name: + return None, None + + return (self.ladxr_name, self.name_lookup[self.value].replace("_", "")) + + +class Logic(Choice, LADXROption): + """ + Affects where items are allowed to be placed. + [Normal] Playable without using any tricks or glitches. Can require knowledge from a vanilla playthrough, such as how to open Color Dungeon. + [Hard] More advanced techniques may be required, but glitches are not. Examples include tricky jumps, killing enemies with only pots. + [Glitched] Advanced glitches and techniques may be required, but extremely difficult or tedious tricks are not required. Examples include Bomb Triggers, Super Jumps and Jesus Jumps. + [Hell] Obscure knowledge and hard techniques may be required. Examples include featherless jumping with boots and/or hookshot, sequential pit buffers and unclipped superjumps. Things in here can be extremely hard to do or very time consuming.""" + display_name = "Logic" + ladxr_name = "logic" + # option_casual = 0 + option_normal = 1 + option_hard = 2 + option_glitched = 3 + option_hell = 4 + + default = option_normal + +class TradeQuest(DefaultOffToggle, LADXROption): + """ + [On] adds the trade items to the pool (the trade locations will always be local items) + [Off] (default) doesn't add them + """ + ladxr_name = "tradequest" + +class Boomerang(Choice): + """ + [Normal] requires Magnifying Lens to get the boomerang. + [Gift] The boomerang salesman will give you a random item, and the boomerang is shuffled. + """ + + normal = 0 + gift = 1 + default = gift + +class EntranceShuffle(Choice, LADXROption): + """ + [WARNING] Experimental, may fail to fill + Randomizes where overworld entrances lead to. + [Simple] Single-entrance caves/houses that have items are shuffled amongst each other. + If random start location and/or dungeon shuffle is enabled, then these will be shuffled with all the non-connector entrance pool. + Note, some entrances can lead into water, use the warp-to-home from the save&quit menu to escape this.""" + + #[Advanced] Simple, but two-way connector caves are shuffled in their own pool as well. + #[Expert] Advanced, but caves/houses without items are also shuffled into the Simple entrance pool. + #[Insanity] Expert, but the Raft Minigame hut and Mamu's cave are added to the non-connector pool. + + option_none = 0 + option_simple = 1 + #option_advanced = 2 + #option_expert = 3 + #option_insanity = 4 + default = option_none + ladxr_name = "entranceshuffle" + +class DungeonShuffle(DefaultOffToggle, LADXROption): + """ + [WARNING] Experimental, may fail to fill + Randomizes dungeon entrances within eachother + """ + ladxr_name = "dungeonshuffle" + +class BossShuffle(Choice): + none = 0 + shuffle = 1 + random = 2 + default = none + + +class DungeonItemShuffle(Choice): + option_original_dungeon = 0 + option_own_dungeons = 1 + option_own_world = 2 + option_any_world = 3 + option_different_world = 4 + #option_delete = 5 + #option_start_with = 6 + alias_true = 3 + alias_false = 0 + +class ShuffleNightmareKeys(DungeonItemShuffle): + """ + Shuffle Nightmare Keys + [Original Dungeon] The item will be within its original dungeon + [Own Dungeons] The item will be within a dungeon in your world + [Own World] The item will be somewhere in your world + [Any World] The item could be anywhere + [Different World] The item will be somewhere in another world + """ + ladxr_item = "NIGHTMARE_KEY" + +class ShuffleSmallKeys(DungeonItemShuffle): + """ + Shuffle Small Keys + [Original Dungeon] The item will be within its original dungeon + [Own Dungeons] The item will be within a dungeon in your world + [Own World] The item will be somewhere in your world + [Any World] The item could be anywhere + [Different World] The item will be somewhere in another world + """ + ladxr_item = "KEY" +class ShuffleMaps(DungeonItemShuffle): + """ + Shuffle Dungeon Maps + [Original Dungeon] The item will be within its original dungeon + [Own Dungeons] The item will be within a dungeon in your world + [Own World] The item will be somewhere in your world + [Any World] The item could be anywhere + [Different World] The item will be somewhere in another world + """ + ladxr_item = "MAP" + +class ShuffleCompasses(DungeonItemShuffle): + """ + Shuffle Dungeon Compasses + [Original Dungeon] The item will be within its original dungeon + [Own Dungeons] The item will be within a dungeon in your world + [Own World] The item will be somewhere in your world + [Any World] The item could be anywhere + [Different World] The item will be somewhere in another world + """ + ladxr_item = "COMPASS" + +class ShuffleStoneBeaks(DungeonItemShuffle): + """ + Shuffle Owl Beaks + [Original Dungeon] The item will be within its original dungeon + [Own Dungeons] The item will be within a dungeon in your world + [Own World] The item will be somewhere in your world + [Any World] The item could be anywhere + [Different World] The item will be somewhere in another world + """ + ladxr_item = "STONE_BEAK" + +class Goal(Choice, LADXROption): + """ + The Goal of the game + [Instruments] The Wind Fish's Egg will only open if you have the required number of Instruments of the Sirens, and play the Ballad of the Wind Fish. + [Seashells] The Egg will open when you bring 20 seashells. The Ballad and Ocarina are not needed. + [Open] The Egg will start pre-opened. + """ + display_name = "Goal" + ladxr_name = "goal" + option_instruments = 1 + option_seashells = 2 + option_open = 3 + + default = option_instruments + + def to_ladxr_option(self, all_options): + if self.value == self.option_instruments: + return ("goal", all_options["instrument_count"]) + else: + return LADXROption.to_ladxr_option(self, all_options) + +class InstrumentCount(Range, LADXROption): + """ + Sets the number of instruments required to open the Egg + """ + ladxr_name = None + range_start = 0 + range_end = 8 + default = 8 + +class NagMessages(DefaultOffToggle, LADXROption): + """ + Controls if nag messages are shown when rocks and crystals are touched. Useful for glitches, annoying for everyone else. + """ + + ladxr_name = "nagmessages" + +class MusicChangeCondition(Choice): + """ + Controls how the music changes. + [Sword] When you pick up a sword, the music changes + [Always] You always have the post-sword music + """ + option_sword = 0 + option_always = 1 + default = option_always +# Setting('hpmode', 'Gameplay', 'm', 'Health mode', options=[('default', '', 'Normal'), ('inverted', 'i', 'Inverted'), ('1', '1', 'Start with 1 heart'), ('low', 'l', 'Low max')], default='default', +# description=""" +# [Normal} health works as you would expect. +# [Inverted] you start with 9 heart containers, but killing a boss will take a heartcontainer instead of giving one. +# [Start with 1] normal game, you just start with 1 heart instead of 3. +# [Low max] replace heart containers with heart pieces."""), + +# Setting('hardmode', 'Gameplay', 'X', 'Hard mode', options=[('none', '', 'Disabled'), ('oracle', 'O', 'Oracle'), ('hero', 'H', 'Hero'), ('ohko', '1', 'One hit KO')], default='none', +# description=""" +# [Oracle] Less iframes and heath from drops. Bombs damage yourself. Water damages you without flippers. No piece of power or acorn. +# [Hero] Switch version hero mode, double damage, no heart/fairy drops. +# [One hit KO] You die on a single hit, always."""), + +# Setting('steal', 'Gameplay', 't', 'Stealing from the shop', +# options=[('always', 'a', 'Always'), ('never', 'n', 'Never'), ('default', '', 'Normal')], default='default', +# description="""Effects when you can steal from the shop. Stealing is bad and never in logic. +# [Normal] requires the sword before you can steal. +# [Always] you can always steal from the shop +# [Never] you can never steal from the shop."""), +class Bowwow(Choice): + """Allows BowWow to be taken into any area. Certain enemies and bosses are given a new weakness to BowWow. + [Normal] BowWow is in the item pool, but can be logically expected as a damage source. + [Swordless] The progressive swords are removed from the item pool. + """ + normal = 0 + swordless = 1 + default = normal + +class Overworld(Choice, LADXROption): + """ + [Dungeon Dive] Create a different overworld where all the dungeons are directly accessible and almost no chests are located in the overworld. + [Tiny dungeons] All dungeons only consist of a boss fight and a instrument reward. Rest of the dungeon is removed. + """ + display_name = "Overworld" + ladxr_name = "overworld" + option_normal = 0 + option_dungeon_dive = 1 + option_tiny_dungeons = 2 + # option_shuffled = 3 + default = option_normal + +#Setting('superweapons', 'Special', 'q', 'Enable super weapons', default=False, +# description='All items will be more powerful, faster, harder, bigger stronger. You name it.'), +#Setting('quickswap', 'User options', 'Q', 'Quickswap', options=[('none', '', 'Disabled'), ('a', 'a', 'Swap A button'), ('b', 'b', 'Swap B button')], default='none', +# description='Adds that the select button swaps with either A or B. The item is swapped with the top inventory slot. The map is not available when quickswap is enabled.', +# aesthetic=True), +# Setting('textmode', 'User options', 'f', 'Text mode', options=[('fast', '', 'Fast'), ('default', 'd', 'Normal'), ('none', 'n', 'No-text')], default='fast', +# description="""[Fast] makes text appear twice as fast. +# [No-Text] removes all text from the game""", aesthetic=True), +# Setting('lowhpbeep', 'User options', 'p', 'Low HP beeps', options=[('none', 'D', 'Disabled'), ('slow', 'S', 'Slow'), ('default', 'N', 'Normal')], default='slow', +# description='Slows or disables the low health beeping sound', aesthetic=True), +# Setting('noflash', 'User options', 'l', 'Remove flashing lights', default=True, +# description='Remove the flashing light effects from Mamu, shopkeeper and MadBatter. Useful for capture cards and people that are sensitive for these things.', +# aesthetic=True), +# Setting('nagmessages', 'User options', 'S', 'Show nag messages', default=False, +# description='Enables the nag messages normally shown when touching stones and crystals', +# aesthetic=True), +# Setting('gfxmod', 'User options', 'c', 'Graphics', options=gfx_options, default='', +# description='Generally affects at least Link\'s sprite, but can alter any graphics in the game', +# aesthetic=True), +# Setting('linkspalette', 'User options', 'C', "Link's color", +# options=[('-1', '-', 'Normal'), ('0', '0', 'Green'), ('1', '1', 'Yellow'), ('2', '2', 'Red'), ('3', '3', 'Blue'), +# ('4', '4', '?? A'), ('5', '5', '?? B'), ('6', '6', '?? C'), ('7', '7', '?? D')], default='-1', aesthetic=True, +# description="""Allows you to force a certain color on link. +# [Normal] color of link depends on the tunic. +# [Green/Yellow/Red/Blue] forces link into one of these colors. +# [?? A/B/C/D] colors of link are usually inverted and color depends on the area you are in."""), +# Setting('music', 'User options', 'M', 'Music', options=[('', '', 'Default'), ('random', 'r', 'Random'), ('off', 'o', 'Disable')], default='', +# description=""" +# [Random] Randomizes overworld and dungeon music' +# [Disable] no music in the whole game""", +# aesthetic=True), + +class LinkPalette(Choice, LADXROption): + """ + Sets link's palette + A-D are color palettes usually used during the damage animation and can change based on where you are. + """ + display_name = "Links Palette" + ladxr_name = "linkspalette" + option_normal = -1 + option_green = 0 + option_yellow = 1 + option_red = 2 + option_blue = 3 + option_invert_a = 4 + option_invert_b = 5 + option_invert_c = 6 + option_invert_d = 7 + default = option_normal + + def to_ladxr_option(self, all_options): + return self.ladxr_name, str(self.value) + +class TrendyGame(Choice): + """ + [Easy] All of the items hold still for you + [Normal] The vanilla behavior + [Hard] The trade item also moves + [Harder] The items move faster + [Hardest] The items move diagonally + [Impossible] The items move impossibly fast, may scroll on and off the screen + """ + option_easy = 0 + option_normal = 1 + option_hard = 2 + option_harder = 3 + option_hardest = 4 + option_impossible = 5 + default = option_normal + +class GfxMod(FreeText, LADXROption): + """ + Sets the sprite for link, among other things + The option should be the same name as a with sprite (and optional name) file in data/sprites/ladx + """ + display_name = "GFX Modification" + ladxr_name = "gfxmod" + normal = '' + default = 'Link' + + __spriteFiles: typing.DefaultDict[str, typing.List[str]] = defaultdict(list) + __spriteDir: str = None + + extensions = [".bin", ".bdiff", ".png", ".bmp"] + def __init__(self, value: str): + super().__init__(value) + if not GfxMod.__spriteDir: + GfxMod.__spriteDir = Utils.local_path(os.path.join('data', 'sprites','ladx')) + for file in os.listdir(GfxMod.__spriteDir): + name, extension = os.path.splitext(file) + if extension in self.extensions: + GfxMod.__spriteFiles[name].append(file) + + def verify(self, world, player_name: str, plando_options) -> None: + if self.value == "Link" or self.value in GfxMod.__spriteFiles: + return + raise Exception(f"LADX Sprite '{self.value}' not found. Possible sprites are: {['Link'] + list(GfxMod.__spriteFiles.keys())}") + + + def to_ladxr_option(self, all_options): + if self.value == -1 or self.value == "Link": + return None, None + + assert self.value in GfxMod.__spriteFiles + + if len(GfxMod.__spriteFiles[self.value]) > 1: + logger.warning(f"{self.value} does not uniquely identify a file. Possible matches: {GfxMod.__spriteFiles[self.value]}. Using {GfxMod.__spriteFiles[self.value][0]}") + + return self.ladxr_name, self.__spriteDir + "/" + GfxMod.__spriteFiles[self.value][0] + +class Palette(Choice): + """ + Sets the palette for the game. + Note: A few places aren't patched, such as the menu and a few color dungeon tiles. + [Normal] The vanilla palette + [1-Bit] One bit of color per channel + [2-Bit] Two bits of color per channel + [Greyscale] Shades of grey + [Pink] Aesthetic + [Inverted] Inverted + """ + option_normal = 0 + option_1bit = 1 + option_2bit = 2 + option_greyscale = 3 + option_pink = 4 + option_inverted = 5 + +links_awakening_options: typing.Dict[str, typing.Type[Option]] = { + 'logic': Logic, + # 'heartpiece': DefaultOnToggle, # description='Includes heart pieces in the item pool'), + # 'seashells': DefaultOnToggle, # description='Randomizes the secret sea shells hiding in the ground/trees. (chest are always randomized)'), + # 'heartcontainers': DefaultOnToggle, # description='Includes boss heart container drops in the item pool'), + # 'instruments': DefaultOffToggle, # description='Instruments are placed on random locations, dungeon goal will just contain a random item.'), + 'tradequest': TradeQuest, # description='Trade quest items are randomized, each NPC takes its normal trade quest item, but gives a random item'), + # 'witch': DefaultOnToggle, # description='Adds both the toadstool and the reward for giving the toadstool to the witch to the item pool'), + # 'rooster': DefaultOnToggle, # description='Adds the rooster to the item pool. Without this option, the rooster spot is still a check giving an item. But you will never find the rooster. Any rooster spot is accessible without rooster by other means.'), + # 'boomerang': Boomerang, + # 'randomstartlocation': DefaultOffToggle, # 'Randomize where your starting house is located'), + 'experimental_dungeon_shuffle': DungeonShuffle, # 'Randomizes the dungeon that each dungeon entrance leads to'), + 'experimental_entrance_shuffle': EntranceShuffle, + # 'bossshuffle': BossShuffle, + # 'minibossshuffle': BossShuffle, + 'goal': Goal, + 'instrument_count': InstrumentCount, + # 'itempool': ItemPool, + # 'bowwow': Bowwow, + # 'overworld': Overworld, + 'link_palette': LinkPalette, + 'trendy_game': TrendyGame, + 'gfxmod': GfxMod, + 'palette': Palette, + 'shuffle_nightmare_keys': ShuffleNightmareKeys, + 'shuffle_small_keys': ShuffleSmallKeys, + 'shuffle_maps': ShuffleMaps, + 'shuffle_compasses': ShuffleCompasses, + 'shuffle_stone_beaks': ShuffleStoneBeaks, + 'music_change_condition': MusicChangeCondition, + 'nag_messages': NagMessages +} diff --git a/worlds/ladx/Rom.py b/worlds/ladx/Rom.py new file mode 100644 index 0000000000..eb573fe5b2 --- /dev/null +++ b/worlds/ladx/Rom.py @@ -0,0 +1,40 @@ + +import worlds.Files +import hashlib +import Utils +import os +LADX_HASH = "07c211479386825042efb4ad31bb525f" + +class LADXDeltaPatch(worlds.Files.APDeltaPatch): + hash = LADX_HASH + game = "Links Awakening DX" + patch_file_ending = ".apladx" + result_file_ending: str = ".gbc" + + @classmethod + def get_source_data(cls) -> bytes: + return get_base_rom_bytes() + + +def get_base_rom_bytes(file_name: str = "") -> bytes: + base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None) + if not base_rom_bytes: + file_name = get_base_rom_path(file_name) + base_rom_bytes = bytes(open(file_name, "rb").read()) + + basemd5 = hashlib.md5() + basemd5.update(base_rom_bytes) + if LADX_HASH != basemd5.hexdigest(): + raise Exception('Supplied Base Rom does not match known MD5 for USA release. ' + 'Get the correct game and version, then dump it') + get_base_rom_bytes.base_rom_bytes = base_rom_bytes + return base_rom_bytes + + +def get_base_rom_path(file_name: str = "") -> str: + options = Utils.get_options() + if not file_name: + file_name = options["ladx_options"]["rom_file"] + if not os.path.exists(file_name): + file_name = Utils.user_path(file_name) + return file_name diff --git a/worlds/ladx/Tracker.py b/worlds/ladx/Tracker.py new file mode 100644 index 0000000000..b85086298a --- /dev/null +++ b/worlds/ladx/Tracker.py @@ -0,0 +1,237 @@ +from worlds.ladx.LADXR.checkMetadata import checkMetadataTable +import json +import logging +import websockets +import asyncio + +logger = logging.getLogger("Tracker") + + +# kbranch you're a hero +# https://github.com/kbranch/Magpie/blob/master/autotracking/checks.py +class Check: + def __init__(self, id, address, mask, alternateAddress=None): + self.id = id + self.address = address + self.alternateAddress = alternateAddress + self.mask = mask + self.value = None + self.diff = 0 + + def set(self, bytes): + oldValue = self.value + + self.value = 0 + + for byte in bytes: + maskedByte = byte + if self.mask: + maskedByte &= self.mask + + self.value |= int(maskedByte > 0) + + if oldValue != self.value: + self.diff += self.value - (oldValue or 0) +# Todo: unify this with existing item tables? + + +class LocationTracker: + all_checks = [] + + def __init__(self, gameboy): + self.gameboy = gameboy + maskOverrides = { + '0x106': 0x20, + '0x12B': 0x20, + '0x15A': 0x20, + '0x166': 0x20, + '0x185': 0x20, + '0x1E4': 0x20, + '0x1BC': 0x20, + '0x1E0': 0x20, + '0x1E1': 0x20, + '0x1E2': 0x20, + '0x223': 0x20, + '0x234': 0x20, + '0x2A3': 0x20, + '0x2FD': 0x20, + '0x2A7': 0x20, + '0x1F5': 0x06, + '0x301-0': 0x10, + '0x301-1': 0x10, + } + + addressOverrides = { + '0x30A-Owl': 0xDDEA, + '0x30F-Owl': 0xDDEF, + '0x308-Owl': 0xDDE8, + '0x302': 0xDDE2, + '0x306': 0xDDE6, + '0x307': 0xDDE7, + '0x308': 0xDDE8, + '0x30F': 0xDDEF, + '0x311': 0xDDF1, + '0x314': 0xDDF4, + '0x1F5': 0xDB7D, + '0x301-0': 0xDDE1, + '0x301-1': 0xDDE1, + '0x223': 0xDA2E, + '0x169': 0xD97C, + '0x2A7': 0xD800 + 0x2A1 + } + + alternateAddresses = { + '0x0F2': 0xD8B2, + } + + blacklist = {'None', '0x2A1-2'} + + # in no dungeons boss shuffle, the d3 boss in d7 set 0x20 in fascade's room (0x1BC) + # after beating evil eagile in D6, 0x1BC is now 0xAC (other things may have happened in between) + # entered d3, slime eye flag had already been set (0x15A 0x20). after killing angler fish, bits 0x0C were set + lowest_check = 0xffff + highest_check = 0 + + for check_id in [x for x in checkMetadataTable if x not in blacklist]: + room = check_id.split('-')[0] + mask = 0x10 + address = addressOverrides[check_id] if check_id in addressOverrides else 0xD800 + int( + room, 16) + + if 'Trade' in check_id or 'Owl' in check_id: + mask = 0x20 + + if check_id in maskOverrides: + mask = maskOverrides[check_id] + + lowest_check = min(lowest_check, address) + highest_check = max(highest_check, address) + if check_id in alternateAddresses: + lowest_check = min(lowest_check, alternateAddresses[check_id]) + highest_check = max( + highest_check, alternateAddresses[check_id]) + + check = Check(check_id, address, mask, + alternateAddresses[check_id] if check_id in alternateAddresses else None) + if check_id == '0x2A3': + self.start_check = check + self.all_checks.append(check) + self.remaining_checks = [check for check in self.all_checks] + self.gameboy.set_cache_limits( + lowest_check, highest_check - lowest_check + 1) + + def has_start_item(self): + return self.start_check not in self.remaining_checks + + async def readChecks(self, cb): + new_checks = [] + for check in self.remaining_checks: + addresses = [check.address] + if check.alternateAddress: + addresses.append(check.alternateAddress) + bytes = await self.gameboy.read_memory_cache(addresses) + if not bytes: + return False + check.set(list(bytes.values())) + + if check.value: + self.remaining_checks.remove(check) + new_checks.append(check) + if new_checks: + cb(new_checks) + return True + + +class MagpieBridge: + port = 17026 + server = None + checks = None + item_tracker = None + ws = None + + async def handler(self, websocket): + self.ws = websocket + while True: + message = json.loads(await websocket.recv()) + if message["type"] == "handshake": + logger.info( + f"Connected, supported features: {message['features']}") + if "items" in message["features"]: + await self.send_all_inventory() + if "checks" in message["features"]: + await self.send_all_checks() + # Translate renamed IDs back to LADXR IDs + @staticmethod + def fixup_id(the_id): + if the_id == "0x2A1": + return "0x2A1-0" + if the_id == "0x2A7": + return "0x2A1-1" + return the_id + + async def send_all_checks(self): + while self.checks == None: + await asyncio.sleep(0.1) + logger.info("sending all checks to magpie") + + message = { + "type": "check", + "refresh": True, + "version": "1.0", + "diff": False, + "checks": [{"id": self.fixup_id(check.id), "checked": check.value} for check in self.checks] + } + + await self.ws.send(json.dumps(message)) + + async def send_new_checks(self, checks): + if not self.ws: + return + + logger.debug("Sending new {checks} to magpie") + message = { + "type": "check", + "refresh": True, + "version": "1.0", + "diff": True, + "checks": [{"id": self.fixup_id(check), "checked": True} for check in checks] + } + + await self.ws.send(json.dumps(message)) + + async def send_all_inventory(self): + logger.info("Sending inventory to magpie") + + while self.item_tracker == None: + await asyncio.sleep(0.1) + + await self.item_tracker.sendItems(self.ws) + + async def send_inventory_diffs(self): + if not self.ws: + return + if not self.item_tracker: + return + await self.item_tracker.sendItems(self.ws, diff=True) + + async def send_gps(self, gps): + if not self.ws: + return + await gps.send_location(self.ws) + + async def serve(self): + async with websockets.serve(lambda w: self.handler(w), "", 17026, logger=logger): + await asyncio.Future() # run forever + + def set_checks(self, checks): + self.checks = checks + + async def set_item_tracker(self, item_tracker): + stale_tracker = self.item_tracker != item_tracker + self.item_tracker = item_tracker + if stale_tracker: + if self.ws: + await self.send_all_inventory() + else: + await self.send_inventory_diffs() + diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py new file mode 100644 index 0000000000..47c601a1f7 --- /dev/null +++ b/worlds/ladx/__init__.py @@ -0,0 +1,424 @@ +import binascii +import os + +from BaseClasses import Entrance, Item, ItemClassification, Location, Tutorial +from Fill import fill_restrictive +from worlds.AutoWorld import WebWorld, World + +from .Common import * +from .Items import (DungeonItemData, DungeonItemType, LinksAwakeningItem, + ladxr_item_to_la_item_name, links_awakening_items, + links_awakening_items_by_name) +from .LADXR import generator +from .LADXR.itempool import ItemPool as LADXRItemPool +from .LADXR.locations.tradeSequence import TradeSequenceItem +from .LADXR.logic import Logic as LAXDRLogic +from .LADXR.main import get_parser +from .LADXR.settings import Settings as LADXRSettings +from .LADXR.worldSetup import WorldSetup as LADXRWorldSetup +from .LADXR.locations.instrument import Instrument +from .LADXR.locations.constants import CHEST_ITEMS +from .Locations import (LinksAwakeningLocation, LinksAwakeningRegion, + create_regions_from_ladxr, get_locations_to_id) +from .Options import links_awakening_options +from .Rom import LADXDeltaPatch + +DEVELOPER_MODE = False + +class LinksAwakeningWebWorld(WebWorld): + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to setting up Links Awakening DX for MultiWorld.", + "English", + "setup_en.md", + "setup/en", + ["zig"] + )] + theme = "dirt" + +class LinksAwakeningWorld(World): + """ + After a previous adventure, Link is stranded on Koholint Island, full of mystery and familiar faces. + Gather the 8 Instruments of the Sirens to wake the Wind Fish, so that Link can go home! + """ + game: str = LINKS_AWAKENING # name of the game/world + web = LinksAwakeningWebWorld() + + option_definitions = links_awakening_options # options the player can set + topology_present = True # show path to required location checks in spoiler + + # data_version is used to signal that items, locations or their names + # changed. Set this to 0 during development so other games' clients do not + # cache any texts, then increase by 1 for each release that makes changes. + data_version = 1 + + # ID of first item and location, could be hard-coded but code may be easier + # to read with this as a propery. + base_id = BASE_ID + # Instead of dynamic numbering, IDs could be part of data. + + # The following two dicts are required for the generation to know which + # items exist. They could be generated from json or something else. They can + # include events, but don't have to since events will be placed manually. + item_name_to_id = { + item.item_name : BASE_ID + item.item_id for item in links_awakening_items + } + + item_name_to_data = links_awakening_items_by_name + + location_name_to_id = get_locations_to_id() + + # Items can be grouped using their names to allow easy checking if any item + # from that group has been collected. Group names can also be used for !hint + #item_name_groups = { + # "weapons": {"sword", "lance"} + #} + + prefill_dungeon_items = None + + player_options = None + + def convert_ap_options_to_ladxr_logic(self): + self.player_options = { + option: getattr(self.multiworld, option)[self.player] for option in self.option_definitions + } + + self.laxdr_options = LADXRSettings(self.player_options) + + self.laxdr_options.validate() + world_setup = LADXRWorldSetup() + world_setup.randomize(self.laxdr_options, self.multiworld.random) + self.ladxr_logic = LAXDRLogic(configuration_options=self.laxdr_options, world_setup=world_setup) + self.ladxr_itempool = LADXRItemPool(self.ladxr_logic, self.laxdr_options, self.multiworld.random).toDict() + + + def create_regions(self) -> None: + # Initialize + self.convert_ap_options_to_ladxr_logic() + regions = create_regions_from_ladxr(self.player, self.multiworld, self.ladxr_logic) + self.multiworld.regions += regions + + # Connect Menu -> Start + start = None + for region in regions: + if region.name == "Start House": + start = region + break + + assert(start) + + menu_region = LinksAwakeningRegion("Menu", None, "Menu", self.player, self.multiworld) + menu_region.exits = [Entrance(self.player, "Start Game", menu_region)] + menu_region.exits[0].connect(start) + + self.multiworld.regions.append(menu_region) + + # Place RAFT, other access events + for region in regions: + for loc in region.locations: + if loc.event: + loc.place_locked_item(self.create_event(loc.ladxr_item.event)) + + # Connect Windfish -> Victory + windfish = self.multiworld.get_region("Windfish", self.player) + l = Location(self.player, "Windfish", parent=windfish) + windfish.locations = [l] + + l.place_locked_item(self.create_event("An Alarm Clock")) + + self.multiworld.completion_condition[self.player] = lambda state: state.has("An Alarm Clock", player=self.player) + + def create_item(self, item_name: str): + return LinksAwakeningItem(self.item_name_to_data[item_name], self, self.player) + + def create_event(self, event: str): + return Item(event, ItemClassification.progression, None, self.player) + + def create_items(self) -> None: + exclude = [item.name for item in self.multiworld.precollected_items[self.player]] + + self.trade_items = [] + + dungeon_item_types = { + + } + from .Options import DungeonItemShuffle + self.prefill_original_dungeon = [ [], [], [], [], [], [], [], [], [] ] + self.prefill_own_dungeons = [] + # For any and different world, set item rule instead + + for option in ["maps", "compasses", "small_keys", "nightmare_keys", "stone_beaks"]: + option = "shuffle_" + option + option = self.player_options[option] + + dungeon_item_types[option.ladxr_item] = option.value + + if option.value == DungeonItemShuffle.option_own_world: + self.multiworld.local_items[self.player].value |= { + ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, 10) + } + elif option.value == DungeonItemShuffle.option_different_world: + self.multiworld.non_local_items[self.player].value |= { + ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, 10) + } + # option_original_dungeon = 0 + # option_own_dungeons = 1 + # option_own_world = 2 + # option_any_world = 3 + # option_different_world = 4 + # option_delete = 5 + + for ladx_item_name, count in self.ladxr_itempool.items(): + # event + if ladx_item_name not in ladxr_item_to_la_item_name: + continue + item_name = ladxr_item_to_la_item_name[ladx_item_name] + for _ in range(count): + if item_name in exclude: + exclude.remove(item_name) # this is destructive. create unique list above + self.multiworld.itempool.append(self.create_item("Master Stalfos' Message")) + else: + item = self.create_item(item_name) + + if not self.multiworld.tradequest[self.player] and ladx_item_name.startswith("TRADING_"): + self.trade_items.append(item) + continue + if isinstance(item.item_data, DungeonItemData): + if item.item_data.dungeon_item_type == DungeonItemType.INSTRUMENT: + # Find instrument, lock + # TODO: we should be able to pinpoint the region we want, save a lookup table please + found = False + for r in self.multiworld.get_regions(): + if r.player != self.player: + continue + if r.dungeon_index != item.item_data.dungeon_index: + continue + for loc in r.locations: + if not isinstance(loc, LinksAwakeningLocation): + continue + if not isinstance(loc.ladxr_item, Instrument): + continue + loc.place_locked_item(item) + found = True + break + if found: + break + else: + item_type = item.item_data.ladxr_id[:-1] + shuffle_type = dungeon_item_types[item_type] + if shuffle_type == DungeonItemShuffle.option_original_dungeon: + self.prefill_original_dungeon[item.item_data.dungeon_index - 1].append(item) + elif shuffle_type == DungeonItemShuffle.option_own_dungeons: + self.prefill_own_dungeons.append(item) + else: + self.multiworld.itempool.append(item) + else: + self.multiworld.itempool.append(item) + + def pre_fill(self): + self.multi_key = self.generate_multi_key() + + dungeon_locations = [] + dungeon_locations_by_dungeon = [[], [], [], [], [], [], [], [], []] + all_state = self.multiworld.get_all_state(use_cache=False) + + # Add special case for trendy shop access + trendy_region = self.multiworld.get_region("Trendy Shop", self.player) + event_location = Location(self.player, "Can Play Trendy Game", parent=trendy_region) + trendy_region.locations.insert(0, event_location) + event_location.place_locked_item(self.create_event("Can Play Trendy Game")) + + # For now, special case first item + FORCE_START_ITEM = True + if FORCE_START_ITEM: + start_loc = self.multiworld.get_location("Tarin's Gift (Mabe Village)", self.player) + if not start_loc.item: + possible_start_items = [index for index, item in enumerate(self.multiworld.itempool) + if item.player == self.player + and item.item_data.ladxr_id in start_loc.ladxr_item.OPTIONS] + + index = self.multiworld.random.choice(possible_start_items) + start_item = self.multiworld.itempool.pop(index) + start_loc.place_locked_item(start_item) + + for r in self.multiworld.get_regions(): + if r.player != self.player: + continue + + # Set aside dungeon locations + if r.dungeon_index: + dungeon_locations += r.locations + dungeon_locations_by_dungeon[r.dungeon_index - 1] += r.locations + for location in r.locations: + if location.name == "Pit Button Chest (Tail Cave)": + # Don't place dungeon items on pit button chest, to reduce chance of the filler blowing up + # TODO: no need for this if small key shuffle + dungeon_locations.remove(location) + dungeon_locations_by_dungeon[r.dungeon_index - 1].remove(location) + # Properly fill locations within dungeon + location.dungeon = r.dungeon_index + + # Tell the filler that if we're placing a dungeon item, restrict it to the dungeon the item associates with + # This will need changed once keysanity is implemented + #orig_rule = location.item_rule + #location.item_rule = lambda item, orig_rule=orig_rule: \ + # (not isinstance(item, DungeonItemData) or item.dungeon_index == location.dungeon) and orig_rule(item) + + for location in r.locations: + # If tradequests are disabled, place trade items directly in their proper location + if not self.multiworld.tradequest[self.player] and isinstance(location, LinksAwakeningLocation) and isinstance(location.ladxr_item, TradeSequenceItem): + item = next(i for i in self.trade_items if i.item_data.ladxr_id == location.ladxr_item.default_item) + location.place_locked_item(item) + + for dungeon_index in range(0, 9): + locs = dungeon_locations_by_dungeon[dungeon_index] + locs = [loc for loc in locs if not loc.item] + self.multiworld.random.shuffle(locs) + self.multiworld.random.shuffle(self.prefill_original_dungeon[dungeon_index]) + fill_restrictive(self.multiworld, all_state, locs, self.prefill_original_dungeon[dungeon_index], lock=True) + assert not self.prefill_original_dungeon[dungeon_index] + + # Fill dungeon items first, to not torture the fill algo + dungeon_locations = [loc for loc in dungeon_locations if not loc.item] + # dungeon_items = sorted(self.prefill_own_dungeons, key=lambda item: item.item_data.dungeon_item_type) + self.multiworld.random.shuffle(self.prefill_own_dungeons) + self.multiworld.random.shuffle(dungeon_locations) + fill_restrictive(self.multiworld, all_state, dungeon_locations, self.prefill_own_dungeons, lock=True) + + name_cache = {} + + # Tries to associate an icon from another game with an icon we have + def guess_icon_for_other_world(self, other): + if not self.name_cache: + forbidden = [ + "TRADING", + "ITEM", + "BAD", + "SINGLE", + "UPGRADE", + "BLUE", + "RED", + "NOTHING", + "MESSAGE", + ] + for item in ladxr_item_to_la_item_name.keys(): + self.name_cache[item] = item + splits = item.split("_") + self.name_cache["".join(splits)] = item + if 'RUPEES' in splits: + self.name_cache["".join(reversed(splits))] = item + + for word in item.split("_"): + if word not in forbidden and not word.isnumeric(): + self.name_cache[word] = item + others = { + 'KEY': 'KEY', + 'COMPASS': 'COMPASS', + 'BIGKEY': 'NIGHTMARE_KEY', + 'MAP': 'MAP', + 'FLUTE': 'OCARINA', + 'SONG': 'OCARINA', + 'MUSHROOM': 'TOADSTOOL', + 'GLOVE': 'POWER_BRACELET', + 'BOOT': 'PEGASUS_BOOTS', + 'SHOE': 'PEGASUS_BOOTS', + 'SHOES': 'PEGASUS_BOOTS', + 'SANCTUARYHEARTCONTAINER': 'HEART_CONTAINER', + 'BOSSHEARTCONTAINER': 'HEART_CONTAINER', + 'HEARTCONTAINER': 'HEART_CONTAINER', + 'ENERGYTANK': 'HEART_CONTAINER', + 'MISSILE': 'SINGLE_ARROW', + 'BOMBS': 'BOMB', + 'BLUEBOOMERANG': 'BOOMERANG', + 'MAGICMIRROR': 'TRADING_ITEM_MAGNIFYING_GLASS', + 'MIRROR': 'TRADING_ITEM_MAGNIFYING_GLASS', + 'MESSAGE': 'TRADING_ITEM_LETTER', + # TODO: Also use AP item name + } + for name in others.values(): + assert name in self.name_cache, name + assert name in CHEST_ITEMS, name + self.name_cache.update(others) + + + uppered = other.upper() + if "BIG KEY" in uppered: + return 'NIGHTMARE_KEY' + possibles = other.upper().split(" ") + rejoined = "".join(possibles) + if rejoined in self.name_cache: + return self.name_cache[rejoined] + for name in possibles: + if name in self.name_cache: + return self.name_cache[name] + + return "TRADING_ITEM_LETTER" + + + + + def generate_output(self, output_directory: str): + # copy items back to locations + for r in self.multiworld.get_regions(self.player): + for loc in r.locations: + if isinstance(loc, LinksAwakeningLocation): + assert(loc.item) + # If we're a links awakening item, just use the item + if isinstance(loc.item, LinksAwakeningItem): + loc.ladxr_item.item = loc.item.item_data.ladxr_id + + # TODO: if the item name contains "sword", use a sword icon, etc + # Otherwise, use a cute letter as the icon + else: + loc.ladxr_item.item = self.guess_icon_for_other_world(loc.item.name) + loc.ladxr_item.custom_item_name = loc.item.name + + if loc.item: + loc.ladxr_item.item_owner = loc.item.player + else: + loc.ladxr_item.item_owner = self.player + + # Kind of kludge, make it possible for the location to differentiate between local and remote items + loc.ladxr_item.location_owner = self.player + + rom_path = "Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc" + out_name = f"AP-{self.multiworld.seed_name}-P{self.player}-{self.multiworld.player_name[self.player]}.gbc" + out_file = os.path.join(output_directory, out_name) + + rompath = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.gbc") + + + + parser = get_parser() + args = parser.parse_args([rom_path, "-o", out_name, "--dump"]) + + name_for_rom = self.multiworld.player_name[self.player] + + all_names = [self.multiworld.player_name[i + 1] for i in range(len(self.multiworld.player_name))] + + rom = generator.generateRom( + args, + self.laxdr_options, + self.player_options, + self.multi_key, + self.multiworld.seed_name, + self.ladxr_logic, + rnd=self.multiworld.per_slot_randoms[self.player], + player_name=name_for_rom, + player_names=all_names, + player_id = self.player) + + handle = open(rompath, "wb") + rom.save(handle, name="LADXR") + handle.close() + patch = LADXDeltaPatch(os.path.splitext(rompath)[0]+LADXDeltaPatch.patch_file_ending, player=self.player, + player_name=self.multiworld.player_name[self.player], patched_path=rompath) + patch.write() + if not DEVELOPER_MODE: + os.unlink(rompath) + + def generate_multi_key(self): + return bytearray(self.multiworld.random.getrandbits(8) for _ in range(10)) + self.player.to_bytes(2, 'big') + + def modify_multidata(self, multidata: dict): + multidata["connect_names"][binascii.hexlify(self.multi_key).decode()] = multidata["connect_names"][self.multiworld.player_name[self.player]] \ No newline at end of file diff --git a/worlds/ladx/docs/en_Links Awakening DX.md b/worlds/ladx/docs/en_Links Awakening DX.md new file mode 100644 index 0000000000..4e109284dd --- /dev/null +++ b/worlds/ladx/docs/en_Links Awakening DX.md @@ -0,0 +1,93 @@ +# Links Awakening DX + +## Where is the settings page? + +The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +config file. + +## What does randomization do to this game? + +Items which the player would normally acquire throughout the game have been moved around. Logic remains, so the game is +always able to be completed, but because of the item shuffle the player may need to access certain areas before they +would in the vanilla game. + +## What items and locations get shuffled? + +All main inventory items, collectables, and ammunition can be shuffled, and all locations in the game which could +contain any of those items may have their contents changed. + +## Which items can be in another player's world? + +Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to limit +certain items to your own world. + +## What does another world's item look like in Link's Awakening? + +The game will try to pick an appropriate sprite for the item (a LttP sword will be a sword!) - it may, however, be a little odd (a Missile Pack may be a single arrow). + +If there's no appropriate sprite, a Letter will be shown. + +## When the player receives an item, what happens? + +When the player receives an item, Link will hold the item above his head and display it to the world. It's good for +business! + +## I don't know what to do! + +That's not a question - but I'd suggest clicking the crow icon on your client, which will load an AP compatible autotracker for LADXR. + +## What is this randomizer based on? + +This randomizer is based on (forked from) the wonderful work daid did on LADXR - https://github.com/daid/LADXR + +The autotracker code for communication with magpie tracker is directly copied from kbranch's repo - https://github.com/kbranch/Magpie/tree/master/autotracking + +### Sprites + +The following sprite sheets have been included with permission of their respective authors: + +* by Madam Materia (https://www.twitch.tv/isabelle_zephyr) + * Matty_LA +* by Linker (https://twitter.com/BenjaminMaksym) + * Bowwow + * Bunny + * Luigi + * Mario + * Richard + * Tarin + +## Some tips from LADXR... + +

Locations

+

All chests and dungeon keys are always randomized. Also, the 3 songs (Marin, Mambo, and Manu) give a you an item if you present them the Ocarina. The seashell mansion 20 shells reward is also shuffled, but the 5 and 10 shell reward is not, as those can be missed.

+

The moblin cave with Bowwow contains a chest instead. The color dungeon gives 2 items at the end instead of a choice of tunic. Other item locations are: The toadstool, the reward for delivering the toadstool, hidden seashells, heart pieces, heart containers, golden leaves, the Mad Batters (capacity upgrades), the shovel/bow in the shop, the rooster's grave, and all of the keys' (tail,slime,angler,face,bird) locations.

+

Finally, new players often forget the following locations: the heart piece hidden in the water at the castle, the heart piece hidden in the bomb cave (screen before the honey), bonk seashells (run with pegasus boots against the tree in at the Tail Cave, and the tree right of Mabe Village, next to the phone booth), and the hookshop drop from Master Stalfos in D5.

+ +

Color Dungeon

+

The Color Dungeon is part of the item shuffle, and the red/blue tunics are shuffled in the item pool. Which means the fairy at the end of the color dungeon gives out two random items.

+

To access the color dungeon, you need the power bracelet, and you need to push the gravestones in the right order: "down, left, up, right, up", going from the lower right gravestone, to the one left of it, above it, and then to the right.

+ +

Bowwow

+

Bowwow is in a chest, somewhere. After you find him, he will always be in the swamp with you, but not anywhere else.

+ +

Added things

+

In your save and quit menu, there is a 3rd option to return to your home. This has two main uses: it speeds up the game, and prevents softlocks (common in entrance rando).

+

If you have weapons that require ammunition (bombs, powder, arrows), a ghost will show up inside Marin's house. He will refill you up to 10 ammunition, so you do not run out.

+

The flying rooster is (optionally) available as an item.

+

You can access the Bird Key cave item with the L2 Power Bracelet.

+

Boomerang cave is now a random item gift by default (available post-bombs), and boomerang is in the item pool.

+

Your inventory has been increased by four, to accommodate these items now coexisting with eachother.

+ +

Removed things

+

The ghost mini-quest after D4 never shows up, his seashell reward is always available.

+

The walrus is moved a bit, so that you can access the desert without taking Marin on a date.

+ +

Logic

+

Depending on your settings, you can only steal after you find the sword, always, or never.

+

Do not forget that there are two items in the rafting ride. You can access this with just Hookshot or Flippers.

+

Killing enemies with bombs is in normal logic. You can switch to casual logic if you do not want this.

+

D7 confuses some people, but by dropping down pits on the 2nd floor you can access almost all of this dungeon, even without feather and power bracelet.

+ +

Tech

+

The toadstool and magic powder used to be the same type of item. LADXR turns this into two items that you can have a the same time. 4 extra item slots in your inventory were added to support this extra item, and have the ability to own the boomerang.

+

The glitch where the slime key is effectively a 6th golden leaf is fixed, and golden leaves can be collected fine next to the slime key.

diff --git a/worlds/ladx/docs/setup_en.md b/worlds/ladx/docs/setup_en.md new file mode 100644 index 0000000000..2fbd67dafa --- /dev/null +++ b/worlds/ladx/docs/setup_en.md @@ -0,0 +1,100 @@ +# Links Awakening DX Multiworld Setup Guide + +## Required Software + +- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `Links Awakening DX` +- Software capable of loading and playing GBC ROM files + - [RetroArch](https://retroarch.com?page=platforms) 1.10.3 or newer. + - [BizHawk](https://tasvideos.org/BizHawk) 2.8 or newer. +- Your American 1.0 ROM file, probably named `Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc` + +## Installation Procedures + +1. Download and install LinksAwakeningClient from the link above, making sure to install the most recent version. + **The installer file is located in the assets section at the bottom of the version information**. + - During setup, you will be asked to locate your base ROM file. This is your Links Awakening DX ROM file. + +2. You should assign your emulator as your default program for launching ROM + files. + 1. Extract your emulator's folder to your Desktop, or somewhere you will remember. + 2. Right-click on a ROM file and select **Open with...** + 3. Check the box next to **Always use this app to open .gbc files** + 4. Scroll to the bottom of the list and click the grey text **Look for another App on this PC** + 5. Browse for your emulator's `.exe` file and click **Open**. This file should be located inside the folder you + extracted in step one. + +## Create a Config (.yaml) File + +### What is a config file and why do I need one? + +Your config file contains a set of configuration options which provide the generator with information about how it +should generate your game. Each player of a multiworld will provide their own config file. This setup allows each player +to enjoy an experience customized for their taste, and different players in the same multiworld can all have different +options. + +### Where do I get a config file? + +The [Player Settings](/games/Links%20Awakening%20DX/player-settings) page on the website allows you to configure +your personal settings and export a config file from them. + +### Verifying your config file + +If you would like to validate your config file to make sure it works, you may do so on the +[YAML Validator](/mysterycheck) page. + +## Generating a Single-Player Game + +1. Navigate to the [Player Settings](/games/Links%20Awakening%20DX/player-settings) page, configure your options, + and click the "Generate Game" button. +2. You will be presented with a "Seed Info" page. +3. Click the "Create New Room" link. +4. You will be presented with a server page, from which you can download your patch file. +5. Double-click on your patch file, and Links Awakening DX will launch automatically, and create your ROM from the patch file. +6. Since this is a single-player game, you will no longer need the client, so feel free to close it. + +## Joining a MultiWorld Game + +### Obtain your patch file and create your ROM + +When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that is done, +the host will provide you with either a link to download your patch file, or with a zip file containing everyone's patch +files. Your patch file should have a `.apladx` extension. + +Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically launch the +client, and will also create your ROM in the same place as your patch file. + +### Connect to the client + +#### RetroArch 1.10.3 or newer + +You only have to do these steps once. Note, RetroArch 1.9.x will not work as it is older than 1.10.3. + +1. Enter the RetroArch main menu screen. +2. Go to Settings --> User Interface. Set "Show Advanced Settings" to ON. +3. Go to Settings --> Network. Set "Network Commands" to ON. (It is found below Request Device 16.) Leave the default + Network Command Port at 55355. + +![Screenshot of Network Commands setting](/static/generated/docs/A%20Link%20to%20the%20Past/retroarch-network-commands-en.png) +4. Go to Main Menu --> Online Updater --> Core Downloader. Scroll down and select "Nintendo - Gameboy / Color (SameBoy)". + +#### BizHawk 2.8 or newer (older versions untested) + +1. With the ROM loaded, click on "Tools" --> "Lua Console" +2. In the new window, click on "Script" --> "Open Script..." +3. Navigate to the folder Archipelago is installed in, and choose data/lua/connector_ladx_bizhawk.lua +4. Keep the Lua Console open during gameplay (minimizing it is fine!) + +### Connect to the Archipelago Server + +The patch file which launched your client should have automatically connected you to the AP Server. There are a few +reasons this may not happen, however, including if the game is hosted on the website but was generated elsewhere. If the +client window shows "Server Status: Not Connected", simply ask the host for the address of the server, and copy/paste it +into the "Server" input field then press enter. + +The client will attempt to reconnect to the new server address, and should momentarily show "Server Status: Connected". + +### Play the game + +When the client shows both Retroarch and Server as connected, you're ready to begin playing. Congratulations on +successfully joining a multiworld game! You can execute various commands in your client. For more information regarding +these commands you can use `/help` for local client commands and `!help` for server commands. diff --git a/worlds/ladx/test/TestDungeonLogic.py b/worlds/ladx/test/TestDungeonLogic.py new file mode 100644 index 0000000000..b9b9672b9b --- /dev/null +++ b/worlds/ladx/test/TestDungeonLogic.py @@ -0,0 +1,35 @@ +from . import LADXTestBase + +from ..Items import ItemName + +class TestD6(LADXTestBase): + # Force keys into pool for testing + options = { + "shuffle_small_keys": "any_world" + } + + def test_keylogic(self): + keys = self.get_items_by_name(ItemName.KEY6) + self.collect_by_name([ItemName.FACE_KEY, ItemName.HOOKSHOT, ItemName.POWER_BRACELET, ItemName.BOMB, ItemName.FEATHER, ItemName.FLIPPERS]) + # Can reach an un-keylocked item in the dungeon + self.assertTrue(self.can_reach_location("L2 Bracelet Chest (Face Shrine)")) + + # For each location, add a key and check that the right thing unlocks + location_1 = "Tile Room Key (Face Shrine)" + location_2 = "Top Right Horse Heads Chest (Face Shrine)" + location_3 = "Pot Locked Chest (Face Shrine)" + self.assertFalse(self.can_reach_location(location_1)) + self.assertFalse(self.can_reach_location(location_2)) + self.assertFalse(self.can_reach_location(location_3)) + self.collect(keys[0]) + self.assertTrue(self.can_reach_location(location_1)) + self.assertFalse(self.can_reach_location(location_2)) + self.assertFalse(self.can_reach_location(location_3)) + self.collect(keys[1]) + self.assertTrue(self.can_reach_location(location_1)) + self.assertTrue(self.can_reach_location(location_2)) + self.assertFalse(self.can_reach_location(location_3)) + self.collect(keys[2]) + self.assertTrue(self.can_reach_location(location_1)) + self.assertTrue(self.can_reach_location(location_2)) + self.assertTrue(self.can_reach_location(location_3)) diff --git a/worlds/ladx/test/__init__.py b/worlds/ladx/test/__init__.py new file mode 100644 index 0000000000..0e616ac557 --- /dev/null +++ b/worlds/ladx/test/__init__.py @@ -0,0 +1,4 @@ +from test.TestBase import WorldTestBase +from ..Common import LINKS_AWAKENING +class LADXTestBase(WorldTestBase): + game = LINKS_AWAKENING diff --git a/worlds/lufia2ac/Client.py b/worlds/lufia2ac/Client.py index 002b257dbd..58ee7f87f9 100644 --- a/worlds/lufia2ac/Client.py +++ b/worlds/lufia2ac/Client.py @@ -2,10 +2,11 @@ import logging import time import typing from logging import Logger -from typing import Dict +from typing import Optional from NetUtils import ClientStatus, NetworkItem from worlds.AutoSNIClient import SNIClient +from .Enemies import enemy_id_to_name from .Items import start_id as items_start_id from .Locations import start_id as locations_start_id @@ -24,233 +25,6 @@ L2AC_DEATH_ADDR: int = SRAM_START + 0x203D L2AC_TX_ADDR: int = SRAM_START + 0x2040 L2AC_RX_ADDR: int = SRAM_START + 0x2800 -enemy_names: Dict[int, str] = { - 0x00: "a Goblin", - 0x01: "an Armor goblin", - 0x02: "a Regal Goblin", - 0x03: "a Goblin Mage", - 0x04: "a Troll", - 0x05: "an Ork", - 0x06: "a Fighter ork", - 0x07: "an Ork Mage", - 0x08: "a Lizardman", - 0x09: "a Skull Lizard", - 0x0A: "an Armour Dait", - 0x0B: "a Dragonian", - 0x0C: "a Cyclops", - 0x0D: "a Mega Cyclops", - 0x0E: "a Flame genie", - 0x0F: "a Well Genie", - 0x10: "a Wind Genie", - 0x11: "an Earth Genie", - 0x12: "a Cobalt", - 0x13: "a Merman", - 0x14: "an Aqualoi", - 0x15: "an Imp", - 0x16: "a Fiend", - 0x17: "an Archfiend", - 0x18: "a Hound", - 0x19: "a Doben", - 0x1A: "a Winger", - 0x1B: "a Serfaco", - 0x1C: "a Pug", - 0x1D: "a Salamander", - 0x1E: "a Brinz Lizard", - 0x1F: "a Seahorse", - 0x20: "a Seirein", - 0x21: "an Earth Viper", - 0x22: "a Gnome", - 0x23: "a Wispy", - 0x24: "a Thunderbeast", - 0x25: "a Lunar bear", - 0x26: "a Shadowfly", - 0x27: "a Shadow", - 0x28: "a Lion", - 0x29: "a Sphinx", - 0x2A: "a Mad horse", - 0x2B: "an Armor horse", - 0x2C: "a Buffalo", - 0x2D: "a Bruse", - 0x2E: "a Bat", - 0x2F: "a Big Bat", - 0x30: "a Red Bat", - 0x31: "an Eagle", - 0x32: "a Hawk", - 0x33: "a Crow", - 0x34: "a Baby Frog", - 0x35: "a King Frog", - 0x36: "a Lizard", - 0x37: "a Newt", - 0x38: "a Needle Lizard", - 0x39: "a Poison Lizard", - 0x3A: "a Medusa", - 0x3B: "a Ramia", - 0x3C: "a Basilisk", - 0x3D: "a Cokatoris", - 0x3E: "a Scorpion", - 0x3F: "an Antares", - 0x40: "a Small Crab", - 0x41: "a Big Crab", - 0x42: "a Red Lobster", - 0x43: "a Spider", - 0x44: "a Web Spider", - 0x45: "a Beetle", - 0x46: "a Poison Beetle", - 0x47: "a Mosquito", - 0x48: "a Coridras", - 0x49: "a Spinner", - 0x4A: "a Tartona", - 0x4B: "an Armour Nail", - 0x4C: "a Moth", - 0x4D: "a Mega Moth", - 0x4E: "a Big Bee", - 0x4F: "a Dark Fly", - 0x50: "a Stinger", - 0x51: "an Armor Bee", - 0x52: "a Sentopez", - 0x53: "a Cancer", - 0x54: "a Garbost", - 0x55: "a Bolt Fish", - 0x56: "a Moray", - 0x57: "a She Viper", - 0x58: "an Angler fish", - 0x59: "a Unicorn", - 0x5A: "an Evil Shell", - 0x5B: "a Drill Shell", - 0x5C: "a Snell", - 0x5D: "an Ammonite", - 0x5E: "an Evil Fish", - 0x5F: "a Squid", - 0x60: "a Kraken", - 0x61: "a Killer Whale", - 0x62: "a White Whale", - 0x63: "a Grianos", - 0x64: "a Behemoth", - 0x65: "a Perch", - 0x66: "a Current", - 0x67: "a Vampire Rose", - 0x68: "a Desert Rose", - 0x69: "a Venus Fly", - 0x6A: "a Moray Vine", - 0x6B: "a Torrent", - 0x6C: "a Mad Ent", - 0x6D: "a Crow Kelp", - 0x6E: "a Red Plant", - 0x6F: "La Fleshia", - 0x70: "a Wheel Eel", - 0x71: "a Skeleton", - 0x72: "a Ghoul", - 0x73: "a Zombie", - 0x74: "a Specter", - 0x75: "a Dark Spirit", - 0x76: "a Snatcher", - 0x77: "a Jurahan", - 0x78: "a Demise", - 0x79: "a Leech", - 0x7A: "a Necromancer", - 0x7B: "a Hade Chariot", - 0x7C: "a Hades", - 0x7D: "a Dark Skull", - 0x7E: "a Hades Skull", - 0x7F: "a Mummy", - 0x80: "a Vampire", - 0x81: "a Nosferato", - 0x82: "a Ghost Ship", - 0x83: "a Deadly Sword", - 0x84: "a Deadly Armor", - 0x85: "a T Rex", - 0x86: "a Brokion", - 0x87: "a Pumpkin Head", - 0x88: "a Mad Head", - 0x89: "a Snow Gas", - 0x8A: "a Great Coca", - 0x8B: "a Gargoyle", - 0x8C: "a Rogue Shape", - 0x8D: "a Bone Gorem", - 0x8E: "a Nuborg", - 0x8F: "a Wood Gorem", - 0x90: "a Mad Gorem", - 0x91: "a Green Clay", - 0x92: "a Sand Gorem", - 0x93: "a Magma Gorem", - 0x94: "an Iron Gorem", - 0x95: "a Gold Gorem", - 0x96: "a Hidora", - 0x97: "a Sea Hidora", - 0x98: "a High Hidora", - 0x99: "a King Hidora", - 0x9A: "an Orky", - 0x9B: "a Waiban", - 0x9C: "a White Dragon", - 0x9D: "a Red Dragon", - 0x9E: "a Blue Dragon", - 0x9F: "a Green Dragon", - 0xA0: "a Black Dragon", - 0xA1: "a Copper Dragon", - 0xA2: "a Silver Dragon", - 0xA3: "a Gold Dragon", - 0xA4: "a Red Jelly", - 0xA5: "a Blue Jelly", - 0xA6: "a Bili Jelly", - 0xA7: "a Red Core", - 0xA8: "a Blue Core", - 0xA9: "a Green Core", - 0xAA: "a No Core", - 0xAB: "a Mimic", - 0xAC: "a Blue Mimic", - 0xAD: "an Ice Roge", - 0xAE: "a Mushroom", - 0xAF: "a Big Mushr'm", - 0xB0: "a Minataurus", - 0xB1: "a Gorgon", - 0xB2: "a Ninja", - 0xB3: "an Asashin", - 0xB4: "a Samurai", - 0xB5: "a Dark Warrior", - 0xB6: "an Ochi Warrior", - 0xB7: "a Sly Fox", - 0xB8: "a Tengu", - 0xB9: "a Warm Eye", - 0xBA: "a Wizard", - 0xBB: "a Dark Sum'ner", - 0xBC: "the Big Catfish", - 0xBD: "a Follower", - 0xBE: "the Tarantula", - 0xBF: "Pierre", - 0xC0: "Daniele", - 0xC1: "the Venge Ghost", - 0xC2: "the Fire Dragon", - 0xC3: "the Tank", - 0xC4: "Idura", - 0xC5: "Camu", - 0xC6: "Gades", - 0xC7: "Amon", - 0xC8: "Erim", - 0xC9: "Daos", - 0xCA: "a Lizard Man", - 0xCB: "a Goblin", - 0xCC: "a Skeleton", - 0xCD: "a Regal Goblin", - 0xCE: "a Goblin", - 0xCF: "a Goblin Mage", - 0xD0: "a Slave", - 0xD1: "a Follower", - 0xD2: "a Groupie", - 0xD3: "the Egg Dragon", - 0xD4: "a Mummy", - 0xD5: "a Troll", - 0xD6: "Gades", - 0xD7: "Idura", - 0xD8: "a Lion", - 0xD9: "the Rogue Flower", - 0xDA: "a Gargoyle", - 0xDB: "a Ghost Ship", - 0xDC: "Idura", - 0xDD: "a Soldier", - 0xDE: "Gades", - 0xDF: "the Master", -} - class L2ACSNIClient(SNIClient): game: str = "Lufia II Ancient Cave" @@ -258,7 +32,7 @@ class L2ACSNIClient(SNIClient): async def validate_rom(self, ctx: SNIContext) -> bool: from SNIClient import snes_read - rom_name: bytes = await snes_read(ctx, L2AC_ROMNAME_START, 0x15) + rom_name: Optional[bytes] = await snes_read(ctx, L2AC_ROMNAME_START, 0x15) if rom_name is None or rom_name[:4] != b"L2AC": return False @@ -272,7 +46,7 @@ class L2ACSNIClient(SNIClient): async def game_watcher(self, ctx: SNIContext) -> None: from SNIClient import snes_buffered_write, snes_flush_writes, snes_read - rom: bytes = await snes_read(ctx, L2AC_ROMNAME_START, 0x15) + rom: Optional[bytes] = await snes_read(ctx, L2AC_ROMNAME_START, 0x15) if rom != ctx.rom: ctx.rom = None return @@ -281,30 +55,30 @@ class L2ACSNIClient(SNIClient): # not successfully connected to a multiworld server, cannot process the game sending items return - signature: bytes = await snes_read(ctx, L2AC_SIGN_ADDR, 16) + signature: Optional[bytes] = await snes_read(ctx, L2AC_SIGN_ADDR, 16) if signature != b"ArchipelagoLufia": return # Goal if not ctx.finished_game: - goal_data: bytes = await snes_read(ctx, L2AC_GOAL_ADDR, 10) + goal_data: Optional[bytes] = await snes_read(ctx, L2AC_GOAL_ADDR, 10) if goal_data is not None and goal_data[goal_data[0]] == 0x01: await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) ctx.finished_game = True # DeathLink TX - death_data: bytes = await snes_read(ctx, L2AC_DEATH_ADDR, 3) + death_data: Optional[bytes] = await snes_read(ctx, L2AC_DEATH_ADDR, 3) if death_data is not None: await ctx.update_death_link(bool(death_data[0])) if death_data[1] != 0x00: snes_buffered_write(ctx, L2AC_DEATH_ADDR + 1, b"\x00") if "DeathLink" in ctx.tags and ctx.last_death_link + 1 < time.time(): player_name: str = ctx.player_names.get(ctx.slot, str(ctx.slot)) - enemy_name: str = enemy_names.get(death_data[1] - 1, hex(death_data[1] - 1)) + enemy_name: str = enemy_id_to_name.get(death_data[1] - 1, hex(death_data[1] - 1)) await ctx.send_death(f"{player_name} was totally defeated by {enemy_name}.") # TX - tx_data: bytes = await snes_read(ctx, L2AC_TX_ADDR, 8) + tx_data: Optional[bytes] = await snes_read(ctx, L2AC_TX_ADDR, 8) if tx_data is not None: snes_items_sent = int.from_bytes(tx_data[:2], "little") client_items_sent = int.from_bytes(tx_data[2:4], "little") @@ -316,7 +90,7 @@ class L2ACSNIClient(SNIClient): client_items_sent += 1 ctx.locations_checked.add(location_id) - await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}]) + await ctx.send_msgs([{"cmd": "LocationChecks", "locations": [location_id]}]) snes_logger.info("New Check: %s (%d/%d)" % ( location, @@ -329,7 +103,7 @@ class L2ACSNIClient(SNIClient): snes_buffered_write(ctx, L2AC_TX_ADDR + 4, ap_items_found.to_bytes(2, "little")) # RX - rx_data: bytes = await snes_read(ctx, L2AC_RX_ADDR, 4) + rx_data: Optional[bytes] = await snes_read(ctx, L2AC_RX_ADDR, 4) if rx_data is not None: snes_items_received = int.from_bytes(rx_data[:2], "little") @@ -343,7 +117,7 @@ class L2ACSNIClient(SNIClient): ctx.player_names[item.player], ctx.location_names[item.location], snes_items_received, len(ctx.items_received))) - snes_buffered_write(ctx, L2AC_RX_ADDR + 2 * (snes_items_received + 1), item_code.to_bytes(2, 'little')) + snes_buffered_write(ctx, L2AC_RX_ADDR + 2 * (snes_items_received + 1), item_code.to_bytes(2, "little")) snes_buffered_write(ctx, L2AC_RX_ADDR, snes_items_received.to_bytes(2, "little")) await snes_flush_writes(ctx) @@ -352,7 +126,7 @@ class L2ACSNIClient(SNIClient): from SNIClient import DeathState, snes_buffered_write, snes_flush_writes # DeathLink RX - if "DeathLink" in ctx.tags and ctx.last_death_link + 1 < time.time(): + if "DeathLink" in ctx.tags: snes_buffered_write(ctx, L2AC_DEATH_ADDR + 2, b"\x01") else: snes_buffered_write(ctx, L2AC_DEATH_ADDR + 2, b"\x00") diff --git a/worlds/lufia2ac/Enemies.py b/worlds/lufia2ac/Enemies.py new file mode 100644 index 0000000000..5d82966c0c --- /dev/null +++ b/worlds/lufia2ac/Enemies.py @@ -0,0 +1,382 @@ +from typing import Dict + +enemy_id_to_name: Dict[int, str] = { + 0x00: "a Goblin", + 0x01: "an Armor goblin", + 0x02: "a Regal Goblin", + 0x03: "a Goblin Mage", + 0x04: "a Troll", + 0x05: "an Ork", + 0x06: "a Fighter ork", + 0x07: "an Ork Mage", + 0x08: "a Lizardman", + 0x09: "a Skull Lizard", + 0x0A: "an Armour Dait", + 0x0B: "a Dragonian", + 0x0C: "a Cyclops", + 0x0D: "a Mega Cyclops", + 0x0E: "a Flame genie", + 0x0F: "a Well Genie", + 0x10: "a Wind Genie", + 0x11: "an Earth Genie", + 0x12: "a Cobalt", + 0x13: "a Merman", + 0x14: "an Aqualoi", + 0x15: "an Imp", + 0x16: "a Fiend", + 0x17: "an Archfiend", + 0x18: "a Hound", + 0x19: "a Doben", + 0x1A: "a Winger", + 0x1B: "a Serfaco", + 0x1C: "a Pug", + 0x1D: "a Salamander", + 0x1E: "a Brinz Lizard", + 0x1F: "a Seahorse", + 0x20: "a Seirein", + 0x21: "an Earth Viper", + 0x22: "a Gnome", + 0x23: "a Wispy", + 0x24: "a Thunderbeast", + 0x25: "a Lunar bear", + 0x26: "a Shadowfly", + 0x27: "a Shadow", + 0x28: "a Lion", + 0x29: "a Sphinx", + 0x2A: "a Mad horse", + 0x2B: "an Armor horse", + 0x2C: "a Buffalo", + 0x2D: "a Bruse", + 0x2E: "a Bat", + 0x2F: "a Big Bat", + 0x30: "a Red Bat", + 0x31: "an Eagle", + 0x32: "a Hawk", + 0x33: "a Crow", + 0x34: "a Baby Frog", + 0x35: "a King Frog", + 0x36: "a Lizard", + 0x37: "a Newt", + 0x38: "a Needle Lizard", + 0x39: "a Poison Lizard", + 0x3A: "a Medusa", + 0x3B: "a Ramia", + 0x3C: "a Basilisk", + 0x3D: "a Cokatoris", + 0x3E: "a Scorpion", + 0x3F: "an Antares", + 0x40: "a Small Crab", + 0x41: "a Big Crab", + 0x42: "a Red Lobster", + 0x43: "a Spider", + 0x44: "a Web Spider", + 0x45: "a Beetle", + 0x46: "a Poison Beetle", + 0x47: "a Mosquito", + 0x48: "a Coridras", + 0x49: "a Spinner", + 0x4A: "a Tartona", + 0x4B: "an Armour Nail", + 0x4C: "a Moth", + 0x4D: "a Mega Moth", + 0x4E: "a Big Bee", + 0x4F: "a Dark Fly", + 0x50: "a Stinger", + 0x51: "an Armor Bee", + 0x52: "a Sentopez", + 0x53: "a Cancer", + 0x54: "a Garbost", + 0x55: "a Bolt Fish", + 0x56: "a Moray", + 0x57: "a She Viper", + 0x58: "an Angler fish", + 0x59: "a Unicorn", + 0x5A: "an Evil Shell", + 0x5B: "a Drill Shell", + 0x5C: "a Snell", + 0x5D: "an Ammonite", + 0x5E: "an Evil Fish", + 0x5F: "a Squid", + 0x60: "a Kraken", + 0x61: "a Killer Whale", + 0x62: "a White Whale", + 0x63: "a Grianos", + 0x64: "a Behemoth", + 0x65: "a Perch", + 0x66: "a Current", + 0x67: "a Vampire Rose", + 0x68: "a Desert Rose", + 0x69: "a Venus Fly", + 0x6A: "a Moray Vine", + 0x6B: "a Torrent", + 0x6C: "a Mad Ent", + 0x6D: "a Crow Kelp", + 0x6E: "a Red Plant", + 0x6F: "La Fleshia", + 0x70: "a Wheel Eel", + 0x71: "a Skeleton", + 0x72: "a Ghoul", + 0x73: "a Zombie", + 0x74: "a Specter", + 0x75: "a Dark Spirit", + 0x76: "a Snatcher", + 0x77: "a Jurahan", + 0x78: "a Demise", + 0x79: "a Leech", + 0x7A: "a Necromancer", + 0x7B: "a Hade Chariot", + 0x7C: "a Hades", + 0x7D: "a Dark Skull", + 0x7E: "a Hades Skull", + 0x7F: "a Mummy", + 0x80: "a Vampire", + 0x81: "a Nosferato", + 0x82: "a Ghost Ship", + 0x83: "a Deadly Sword", + 0x84: "a Deadly Armor", + 0x85: "a T Rex", + 0x86: "a Brokion", + 0x87: "a Pumpkin Head", + 0x88: "a Mad Head", + 0x89: "a Snow Gas", + 0x8A: "a Great Coca", + 0x8B: "a Gargoyle", + 0x8C: "a Rogue Shape", + 0x8D: "a Bone Gorem", + 0x8E: "a Nuborg", + 0x8F: "a Wood Gorem", + 0x90: "a Mad Gorem", + 0x91: "a Green Clay", + 0x92: "a Sand Gorem", + 0x93: "a Magma Gorem", + 0x94: "an Iron Gorem", + 0x95: "a Gold Gorem", + 0x96: "a Hidora", + 0x97: "a Sea Hidora", + 0x98: "a High Hidora", + 0x99: "a King Hidora", + 0x9A: "an Orky", + 0x9B: "a Waiban", + 0x9C: "a White Dragon", + 0x9D: "a Red Dragon", + 0x9E: "a Blue Dragon", + 0x9F: "a Green Dragon", + 0xA0: "a Black Dragon", + 0xA1: "a Copper Dragon", + 0xA2: "a Silver Dragon", + 0xA3: "a Gold Dragon", + 0xA4: "a Red Jelly", + 0xA5: "a Blue Jelly", + 0xA6: "a Bili Jelly", + 0xA7: "a Red Core", + 0xA8: "a Blue Core", + 0xA9: "a Green Core", + 0xAA: "a No Core", + 0xAB: "a Mimic", + 0xAC: "a Blue Mimic", + 0xAD: "an Ice Roge", + 0xAE: "a Mushroom", + 0xAF: "a Big Mushr'm", + 0xB0: "a Minataurus", + 0xB1: "a Gorgon", + 0xB2: "a Ninja", + 0xB3: "an Asashin", + 0xB4: "a Samurai", + 0xB5: "a Dark Warrior", + 0xB6: "an Ochi Warrior", + 0xB7: "a Sly Fox", + 0xB8: "a Tengu", + 0xB9: "a Warm Eye", + 0xBA: "a Wizard", + 0xBB: "a Dark Sum'ner", + 0xBC: "the Big Catfish", + 0xBD: "a Follower", + 0xBE: "the Tarantula", + 0xBF: "Pierre", + 0xC0: "Daniele", + 0xC1: "the Venge Ghost", + 0xC2: "the Fire Dragon", + 0xC3: "the Tank", + 0xC4: "Idura", + 0xC5: "Camu", + 0xC6: "Gades", + 0xC7: "Amon", + 0xC8: "Erim", + 0xC9: "Daos", + 0xCA: "a Lizard Man", + 0xCB: "a Goblin", + 0xCC: "a Skeleton", + 0xCD: "a Regal Goblin", + 0xCE: "a Goblin", + 0xCF: "a Goblin Mage", + 0xD0: "a Slave", + 0xD1: "a Follower", + 0xD2: "a Groupie", + 0xD3: "the Egg Dragon", + 0xD4: "a Mummy", + 0xD5: "a Troll", + 0xD6: "Gades", + 0xD7: "Idura", + 0xD8: "a Lion", + 0xD9: "the Rogue Flower", + 0xDA: "a Gargoyle", + 0xDB: "a Ghost Ship", + 0xDC: "Idura", + 0xDD: "a Soldier", + 0xDE: "Gades", + 0xDF: "the Master", +} + +enemy_name_to_sprite: Dict[str, int] = { + "Ammonite": 0x81, + "Antares": 0x8B, + "Archfiend": 0xBD, + "Armor Bee": 0x98, + "Armor goblin": 0x9D, + "Armour Dait": 0xEF, + "Armour Nail": 0xEB, + "Asashin": 0x82, + "Baby Frog": 0xBE, + "Basilisk": 0xB6, + "Bat": 0x8F, + "Beetle": 0x86, + "Behemoth": 0xB6, + "Big Bat": 0x8F, + "Big Mushr'm": 0xDB, + "Bili Jelly": 0xDE, + "Black Dragon": 0xC0, + "Blue Core": 0x95, + "Blue Dragon": 0xC0, + "Blue Jelly": 0xDD, + "Blue Mimic": 0xF0, + "Bone Gorem": 0xA0, + "Brinz Lizard": 0xEE, + "Brokion": 0xD3, + "Buffalo": 0x84, + "Cobalt": 0xA6, + "Cokatoris": 0xD2, + "Copper Dragon": 0xC0, + "Coridras": 0xEA, + "Crow": 0xB4, + "Crow Kelp": 0xBC, + "Cyclops": 0xB9, + "Dark Skull": 0xB5, + "Dark Spirit": 0xE7, + "Dark Sum'ner": 0xAB, + "Dark Warrior": 0xB0, + "Deadly Armor": 0x99, + "Deadly Sword": 0x90, + "Demise": 0xAD, + "Desert Rose": 0x96, + "Dragonian": 0xEF, + "Drill Shell": 0x81, + "Eagle": 0xB4, + "Earth Genie": 0xB9, + "Earth Viper": 0xB3, + "Evil Fish": 0x80, + "Fiend": 0xBD, + "Fighter ork": 0xA5, + "Flame genie": 0xB9, + "Garbost": 0xD8, + "Ghost Ship": 0xD1, + "Ghoul": 0xE1, + "Gnome": 0xA5, + "Goblin": 0x9D, + "Gold Dragon": 0xC0, + "Gold Gorem": 0xE2, + "Gorgon": 0xAA, + "Great Coca": 0xD2, + "Green Core": 0x95, + "Green Dragon": 0xC0, + "Grianos": 0xB6, + "Hade Chariot": 0xBA, + "Hades": 0xBA, + "Hades Skull": 0xB5, + "Hidora": 0xBF, + "High Hidora": 0xBF, + "Hound": 0x8A, + "Ice Roge": 0xBD, + "Imp": 0xAC, + "Iron Gorem": 0xA1, + "Jurahan": 0xD5, + "Leech": 0xAD, + "Lion": 0xB7, + "Lizard": 0x83, + "Lizardman": 0x9E, + "Lunar bear": 0x9B, + "Mad Ent": 0x8E, + "Mad Gorem": 0xA3, + "Mad Head": 0xAF, + "Mad horse": 0x85, + "Magma Gorem": 0xE3, + "Medusa": 0x9C, + "Mega Moth": 0xDC, + "Mega Cyclops": 0xB9, + "Mimic": 0xA4, + "Minataurus": 0xAA, + "Moray Vine": 0x9A, + "Mosquito": 0x92, + "Moth": 0x93, + "Mummy": 0xA8, + "Mushroom": 0x8C, + "Necromancer": 0xAB, + "Needle Lizard": 0xD6, + "Newt": 0x83, + "Ninja": 0x82, + "No Core": 0x95, + "Nosferato": 0x9F, + "Nuborg": 0xE5, + "Ochi Warrior": 0xB0, + "Ork": 0xA5, + "Orky": 0xBF, + "Poison Beetle": 0xD7, + "Pug": 0x8D, + "Pumpkin Head": 0xAF, + "Ramia": 0xAE, + "Red Bat": 0x8F, + "Red Core": 0x95, + "Red Dragon": 0xC0, + "Red Jelly": 0x94, + "Red Plant": 0xEC, + "Regal Goblin": 0x9D, + "Rogue Shape": 0xC4, + "Salamander": 0xC1, + "Samurai": 0xB0, + "Sand Gorem": 0xE4, + "Scorpion": 0x8B, + "Sea Hidora": 0xBF, + "Seirein": 0xAE, + "Sentopez": 0xDA, + "Serfaco": 0xE8, + "Shadow": 0xB2, + "Silver Dragon": 0xC0, + "Skeleton": 0xA0, + "Skull Lizard": 0x9E, + "Sly Fox": 0xED, + "Snow Gas": 0xD2, + "Specter": 0xE7, + "Sphinx": 0xB7, + "Spider": 0xD9, + "Spinner": 0xE9, + "Squid": 0x80, + "Stinger": 0x98, + "T Rex": 0xD3, + "Tartona": 0xB8, + "Tengu": 0xD4, + "Thunderbeast": 0x9B, + "Troll": 0xA9, + "Vampire": 0x9F, + "Vampire Rose": 0x96, + "Venus Fly": 0xE0, + "Waiban": 0xC3, + "Warm Eye": 0x88, + "Well Genie": 0xB9, + "Wheel Eel": 0x97, + "White Dragon": 0xC3, + "Wind Genie": 0xB9, + "Winger": 0xB1, + "Wispy": 0x91, + "Wizard": 0xAB, + "Wood Gorem": 0xA2, + "Zombie": 0xA7, +} diff --git a/worlds/lufia2ac/Options.py b/worlds/lufia2ac/Options.py index 47b183c8b1..df71ef44a9 100644 --- a/worlds/lufia2ac/Options.py +++ b/worlds/lufia2ac/Options.py @@ -1,10 +1,16 @@ from __future__ import annotations import random -from itertools import chain, combinations -from typing import Any, cast, Dict, List, Optional, Set, Tuple +from dataclasses import dataclass +from itertools import accumulate, chain, combinations +from typing import Any, cast, Dict, Iterator, List, Mapping, Optional, Set, Tuple, Type, TYPE_CHECKING, Union -from Options import AssembleOptions, Choice, DeathLink, Option, Range, SpecialRange, TextChoice, Toggle +from Options import AssembleOptions, Choice, DeathLink, ItemDict, Range, SpecialRange, TextChoice, Toggle +from .Enemies import enemy_name_to_sprite + +if TYPE_CHECKING: + from BaseClasses import PlandoOptions + from worlds.AutoWorld import World class AssembleCustomizableChoices(AssembleOptions): @@ -37,6 +43,22 @@ class RandomGroupsChoice(Choice, metaclass=AssembleCustomizableChoices): return super().from_text(text) +class EnemyChoice(TextChoice): + _valid_sprites: Dict[str, int] = {enemy_name.lower(): sprite for enemy_name, sprite in enemy_name_to_sprite.items()} + + def verify(self, world: Type[World], player_name: str, plando_options: PlandoOptions) -> None: + if isinstance(self.value, int): + return + if str(self.value).lower() in self._valid_sprites: + return + raise ValueError(f"Could not find option '{self.value}' for '{self.__class__.__name__}', known options are:\n" + f"{', '.join(self.options)}, {', '.join(enemy_name_to_sprite)}.") + + @property + def sprite(self) -> Optional[int]: + return self._valid_sprites.get(str(self.value).lower()) + + class LevelMixin: xp_coefficients: List[int] = sorted([191, 65, 50, 32, 18, 14, 6, 3, 3, 2, 2, 2, 2] * 8, reverse=True) @@ -61,8 +83,7 @@ class BlueChestChance(Range): """The chance of a chest being a blue chest. It is given in units of 1/256, i.e., a value of 25 corresponds to 25/256 ~ 9.77%. - If you increase the blue chest chance, then the chance of finding consumables is decreased in return. - The chance of finding red chest equipment or spells is unaffected. + If you increase the blue chest chance, then the red chest chance is decreased in return. Supported values: 5 – 75 Default value: 25 (five times as much as in an unmodified game) """ @@ -72,6 +93,14 @@ class BlueChestChance(Range): range_end = 75 default = 25 + @property + def chest_type_thresholds(self) -> bytes: + ratio: float = (256 - self.value) / (256 - 5) + # unmodified chances are: consumable (mostly non-restorative) = 36/256, consumable (restorative) = 58/256, + # blue chest = 5/256, spell = 30/256, gear = 45/256 (and the remaining part, weapon = 82/256) + chest_type_chances: List[float] = [36 * ratio, 58 * ratio, float(self.value), 30 * ratio, 45 * ratio] + return bytes(round(threshold) for threshold in reversed(tuple(accumulate(chest_type_chances)))) + class BlueChestCount(Range): """The number of blue chest items that will be in your item pool. @@ -152,7 +181,7 @@ class Boss(RandomGroupsChoice): "random-high": ["venge_ghost", "white_dragon_x3", "fire_dragon", "ghost_ship", "tank"], "random-sinistral": ["gades_c", "amon", "erim", "daos"], } - extra_options = frozenset(random_groups) + extra_options = set(random_groups) @property def flag(self) -> int: @@ -242,6 +271,34 @@ class CrowdedFloorChance(Range): default = 16 +class CustomItemPool(ItemDict, Mapping[str, int]): + """Customize your multiworld item pool. + + Using this option you can place any cave item in your multiworld item pool. (By default, the pool is filled with + blue chest items.) Here you can add any valid item from the Lufia II Ancient Cave section of the datapackage + (see https://archipelago.gg/datapackage). The value of this option has to be a mapping of item name to count, + e.g., to add two Deadly rods and one Dekar Blade: {Deadly rod: 2, Dekar blade: 1} + The maximum total amount of custom items you can place is limited by the chosen blue_chest_count; any remaining, + non-customized space in the pool will be occupied by random blue chest items. + """ + + display_name = "Custom item pool" + value: Dict[str, int] + + @property + def count(self) -> int: + return sum(self.values()) + + def __getitem__(self, key: str) -> int: + return self.value.__getitem__(key) + + def __iter__(self) -> Iterator[str]: + return self.value.__iter__() + + def __len__(self) -> int: + return self.value.__len__() + + class DefaultCapsule(Choice): """Preselect the active capsule monster. @@ -277,7 +334,8 @@ class DefaultParty(RandomGroupsChoice, TextChoice): """ display_name = "Default party lineup" - default = "M" + default: Union[str, int] = "M" + value: Union[str, int] random_groups = { "random-2p": ["M" + "".join(p) for p in combinations("ADGLST", 1)], @@ -288,7 +346,7 @@ class DefaultParty(RandomGroupsChoice, TextChoice): _valid_sorted_parties: List[List[str]] = [sorted(party) for party in ("M", *chain(*random_groups.values()))] _members_to_bytes: bytes = bytes.maketrans(b"MSGATDL", bytes(range(7))) - def verify(self, *args, **kwargs) -> None: + def verify(self, world: Type[World], player_name: str, plando_options: PlandoOptions) -> None: if str(self.value).lower() in self.random_groups: return if sorted(str(self.value).upper()) in self._valid_sorted_parties: @@ -317,6 +375,97 @@ class DefaultParty(RandomGroupsChoice, TextChoice): return len(str(self.value)) +class EnemyFloorNumbers(Choice): + """Change which enemy types are encountered at which floor numbers. + + Supported values: + vanilla + Ninja, e.g., is allowed to appear on the 3 floors B44-B46 + shuffle — The existing enemy types are redistributed among nearby floors. Shifts by up to 6 floors are possible. + Ninja, e.g., will be allowed to appear on exactly 3 consecutive floors somewhere from B38-B40 to B50-B52 + randomize — For each floor, new enemy types are chosen randomly from the set usually possible on floors [-6, +6]. + Ninja, e.g., is among the various possible selections for any enemy slot affecting the floors from B38 to B52 + Default value: vanilla (same as in an unmodified game) + """ + + display_name = "Enemy floor numbers" + option_vanilla = 0 + option_shuffle = 1 + option_randomize = 2 + default = option_vanilla + + +class EnemyMovementPatterns(EnemyChoice): + """Change the movement patterns of enemies. + + Supported values: + vanilla + shuffle_by_pattern — The existing movement patterns are redistributed among each other. + Sprites that usually share a movement pattern will still share movement patterns after shuffling + randomize_by_pattern — For each movement pattern, a new one is chosen randomly from the set of existing patterns. + Sprites that usually share a movement pattern will still share movement patterns after randomizing + shuffle_by_sprite — The existing movement patterns of sprites are redistributed among the enemy sprites. + Sprites that usually share a movement pattern can end up with different movement patterns after shuffling + randomize_by_sprite — For each sprite, a new movement is chosen randomly from the set of existing patterns. + Sprites that usually share a movement pattern can end up with different movement patterns after randomizing + singularity — All enemy sprites use the same, randomly selected movement pattern + Alternatively, you can directly specify an enemy name such as "Red Jelly" as the value of this option. + In that case, the movement pattern usually associated with this sprite will be used by all enemy sprites + Default value: vanilla (same as in an unmodified game) + """ + + display_name = "Enemy movement patterns" + option_vanilla = 0 + option_shuffle_by_pattern = 1 + option_randomize_by_pattern = 2 + option_shuffle_by_sprite = 3 + option_randomize_by_sprite = 4 + option_singularity = 5 + default = option_vanilla + + +class EnemySprites(EnemyChoice): + """Change the appearance of enemies. + + Supported values: + vanilla + shuffle — The existing sprites are redistributed among the enemy types. + This means that, after shuffling, exactly 1 enemy type will be dressing up as the "Red Jelly" sprite + randomize — For each enemy type, a new sprite is chosen randomly from the set of existing sprites. + This means that, after randomizing, any number of enemy types could end up using the "Red Jelly" sprite + singularity — All enemies use the same, randomly selected sprite + Alternatively, you can directly specify an enemy name such as "Red Jelly" as the value of this option. + In this case, the sprite usually associated with that enemy will be used by all enemies + Default value: vanilla (same as in an unmodified game) + """ + + display_name = "Enemy sprites" + option_vanilla = 0 + option_shuffle = 1 + option_randomize = 2 + option_singularity = 3 + default = option_vanilla + + +class ExpModifier(Range): + """Percentage modifier for EXP gained from enemies. + + Supported values: 100 – 500 + Default value: 100 (same as in an unmodified game) + """ + + display_name = "EXP modifier" + range_start = 100 + range_end = 500 + default = 100 + + def __call__(self, exp: bytes) -> bytes: + try: + return (int.from_bytes(exp, "little") * self.value // 100).to_bytes(2, "little") + except OverflowError: + return b"\xFF\xFF" + + class FinalFloor(Range): """The final floor, where the boss resides. @@ -424,28 +573,18 @@ class IrisTreasuresRequired(Range): default = 9 -class MasterHp(SpecialRange): +class MasterHp(Range): """The number of hit points of the Master - Supported values: - 1 – 9980, - scale — scales the HP depending on the value of final_floor + (Only has an effect if boss is set to master.) + Supported values: 1 – 9980 Default value: 9980 (same as in an unmodified game) """ display_name = "Master HP" - range_start = 0 + range_start = 1 range_end = 9980 default = 9980 - special_range_cutoff = 1 - special_range_names = { - "default": 9980, - "scale": 0, - } - - @staticmethod - def scale(final_floor: int) -> int: - return final_floor * 100 + 80 class PartyStartingLevel(LevelMixin, Range): @@ -503,7 +642,8 @@ class ShufflePartyMembers(Toggle): Supported values: false — all 6 optional party members are present in the cafe and can be recruited right away true — only Maxim is available from the start; 6 new "items" are added to your pool and shuffled into the - multiworld; when one of these items is found, the corresponding party member is unlocked for you to use + multiworld; when one of these items is found, the corresponding party member is unlocked for you to use. + While cave diving, you can add newly unlocked ones to your party by using the character items from the inventory Default value: false (same as in an unmodified game) """ @@ -514,27 +654,32 @@ class ShufflePartyMembers(Toggle): return 0b00000000 if self.value else 0b11111100 -l2ac_option_definitions: Dict[str, type(Option)] = { - "blue_chest_chance": BlueChestChance, - "blue_chest_count": BlueChestCount, - "boss": Boss, - "capsule_cravings_jp_style": CapsuleCravingsJPStyle, - "capsule_starting_form": CapsuleStartingForm, - "capsule_starting_level": CapsuleStartingLevel, - "crowded_floor_chance": CrowdedFloorChance, - "death_link": DeathLink, - "default_capsule": DefaultCapsule, - "default_party": DefaultParty, - "final_floor": FinalFloor, - "gear_variety_after_b9": GearVarietyAfterB9, - "goal": Goal, - "healing_floor_chance": HealingFloorChance, - "initial_floor": InitialFloor, - "iris_floor_chance": IrisFloorChance, - "iris_treasures_required": IrisTreasuresRequired, - "master_hp": MasterHp, - "party_starting_level": PartyStartingLevel, - "run_speed": RunSpeed, - "shuffle_capsule_monsters": ShuffleCapsuleMonsters, - "shuffle_party_members": ShufflePartyMembers, -} +@dataclass +class L2ACOptions: + blue_chest_chance: BlueChestChance + blue_chest_count: BlueChestCount + boss: Boss + capsule_cravings_jp_style: CapsuleCravingsJPStyle + capsule_starting_form: CapsuleStartingForm + capsule_starting_level: CapsuleStartingLevel + crowded_floor_chance: CrowdedFloorChance + custom_item_pool: CustomItemPool + death_link: DeathLink + default_capsule: DefaultCapsule + default_party: DefaultParty + enemy_floor_numbers: EnemyFloorNumbers + enemy_movement_patterns: EnemyMovementPatterns + enemy_sprites: EnemySprites + exp_modifier: ExpModifier + final_floor: FinalFloor + gear_variety_after_b9: GearVarietyAfterB9 + goal: Goal + healing_floor_chance: HealingFloorChance + initial_floor: InitialFloor + iris_floor_chance: IrisFloorChance + iris_treasures_required: IrisTreasuresRequired + master_hp: MasterHp + party_starting_level: PartyStartingLevel + run_speed: RunSpeed + shuffle_capsule_monsters: ShuffleCapsuleMonsters + shuffle_party_members: ShufflePartyMembers diff --git a/worlds/lufia2ac/Rom.py b/worlds/lufia2ac/Rom.py index ac168c8864..1da8d235a6 100644 --- a/worlds/lufia2ac/Rom.py +++ b/worlds/lufia2ac/Rom.py @@ -22,15 +22,15 @@ class L2ACDeltaPatch(APDeltaPatch): def get_base_rom_bytes(file_name: str = "") -> bytes: base_rom_bytes: Optional[bytes] = getattr(get_base_rom_bytes, "base_rom_bytes", None) if not base_rom_bytes: - file_name: str = get_base_rom_path(file_name) - base_rom_bytes = bytes(Utils.read_snes_rom(open(file_name, "rb"))) + file_path: str = get_base_rom_path(file_name) + base_rom_bytes = bytes(Utils.read_snes_rom(open(file_path, "rb"))) basemd5 = hashlib.md5() basemd5.update(base_rom_bytes) if L2USHASH != basemd5.hexdigest(): raise Exception("Supplied Base Rom does not match known MD5 for US release. " "Get the correct game and version, then dump it") - get_base_rom_bytes.base_rom_bytes = base_rom_bytes + setattr(get_base_rom_bytes, "base_rom_bytes", base_rom_bytes) return base_rom_bytes diff --git a/worlds/lufia2ac/Utils.py b/worlds/lufia2ac/Utils.py new file mode 100644 index 0000000000..6c2e28d137 --- /dev/null +++ b/worlds/lufia2ac/Utils.py @@ -0,0 +1,21 @@ +from random import Random +from typing import Dict, List, MutableSequence, Sequence, Set, Tuple + + +def constrained_choices(population: Sequence[int], d: int, *, k: int, random: Random) -> List[int]: + n: int = len(population) + constraints: Dict[int, Tuple[int, ...]] = { + i: tuple(dict.fromkeys(population[j] for j in range(max(0, i - d), min(i + d + 1, n)))) for i in range(n) + } + + return [random.choice(constraints[i]) for i in range(k)] + + +def constrained_shuffle(x: MutableSequence[int], d: int, random: Random) -> None: + n: int = len(x) + constraints: Dict[int, Set[int]] = {i: set(x[j] for j in range(max(0, i - d), min(i + d + 1, n))) for i in range(n)} + + for _ in range(d * n * n): + i, j = random.randrange(n), random.randrange(n) + if x[i] in constraints[j] and x[j] in constraints[i]: + x[i], x[j] = x[j], x[i] diff --git a/worlds/lufia2ac/__init__.py b/worlds/lufia2ac/__init__.py index e54f84283d..587792a58d 100644 --- a/worlds/lufia2ac/__init__.py +++ b/worlds/lufia2ac/__init__.py @@ -2,19 +2,21 @@ import base64 import itertools import os from enum import IntFlag -from typing import Any, ClassVar, Dict, List, Optional, Set, Tuple +from random import Random +from typing import Any, ClassVar, Dict, get_type_hints, Iterator, List, Set, Tuple from BaseClasses import Entrance, Item, ItemClassification, MultiWorld, Region, Tutorial -from Main import __version__ from Options import AssembleOptions +from Utils import __version__ from worlds.AutoWorld import WebWorld, World from worlds.generic.Rules import add_rule, set_rule from .Client import L2ACSNIClient # noqa: F401 from .Items import ItemData, ItemType, l2ac_item_name_to_id, l2ac_item_table, L2ACItem, start_id as items_start_id from .Locations import l2ac_location_name_to_id, L2ACLocation -from .Options import Boss, CapsuleStartingForm, CapsuleStartingLevel, DefaultParty, Goal, l2ac_option_definitions, \ - MasterHp, PartyStartingLevel, ShuffleCapsuleMonsters, ShufflePartyMembers +from .Options import CapsuleStartingLevel, DefaultParty, EnemyFloorNumbers, EnemyMovementPatterns, EnemySprites, \ + ExpModifier, Goal, L2ACOptions from .Rom import get_base_rom_bytes, get_base_rom_path, L2ACDeltaPatch +from .Utils import constrained_choices, constrained_shuffle from .basepatch import apply_basepatch CHESTS_PER_SPHERE: int = 5 @@ -42,7 +44,7 @@ class L2ACWorld(World): game: ClassVar[str] = "Lufia II Ancient Cave" web: ClassVar[WebWorld] = L2ACWeb() - option_definitions: ClassVar[Dict[str, AssembleOptions]] = l2ac_option_definitions + option_definitions: ClassVar[Dict[str, AssembleOptions]] = get_type_hints(L2ACOptions) item_name_to_id: ClassVar[Dict[str, int]] = l2ac_item_name_to_id location_name_to_id: ClassVar[Dict[str, int]] = l2ac_location_name_to_id item_name_groups: ClassVar[Dict[str, Set[str]]] = { @@ -54,30 +56,8 @@ class L2ACWorld(World): required_client_version: Tuple[int, int, int] = (0, 3, 6) # L2ACWorld specific properties - rom_name: Optional[bytearray] - - blue_chest_chance: Optional[int] - blue_chest_count: Optional[int] - boss: Optional[Boss] - capsule_cravings_jp_style: Optional[int] - capsule_starting_form: Optional[CapsuleStartingForm] - capsule_starting_level: Optional[CapsuleStartingLevel] - crowded_floor_chance: Optional[int] - death_link: Optional[int] - default_capsule: Optional[int] - default_party: Optional[DefaultParty] - final_floor: Optional[int] - gear_variety_after_b9: Optional[int] - goal: Optional[int] - healing_floor_chance: Optional[int] - initial_floor: Optional[int] - iris_floor_chance: Optional[int] - iris_treasures_required: Optional[int] - master_hp: Optional[int] - party_starting_level: Optional[PartyStartingLevel] - run_speed: Optional[int] - shuffle_capsule_monsters: Optional[ShuffleCapsuleMonsters] - shuffle_party_members: Optional[ShufflePartyMembers] + rom_name: bytearray + o: L2ACOptions @classmethod def stage_assert_generate(cls, multiworld: MultiWorld) -> None: @@ -95,37 +75,17 @@ class L2ACWorld(World): bytearray(f"L2AC{__version__.replace('.', '')[:3]}_{self.player}_{self.multiworld.seed}", "utf8")[:21] self.rom_name.extend([0] * (21 - len(self.rom_name))) - self.blue_chest_chance = self.multiworld.blue_chest_chance[self.player].value - self.blue_chest_count = self.multiworld.blue_chest_count[self.player].value - self.boss = self.multiworld.boss[self.player] - self.capsule_cravings_jp_style = self.multiworld.capsule_cravings_jp_style[self.player].value - self.capsule_starting_form = self.multiworld.capsule_starting_form[self.player] - self.capsule_starting_level = self.multiworld.capsule_starting_level[self.player] - self.crowded_floor_chance = self.multiworld.crowded_floor_chance[self.player].value - self.death_link = self.multiworld.death_link[self.player].value - self.default_capsule = self.multiworld.default_capsule[self.player].value - self.default_party = self.multiworld.default_party[self.player] - self.final_floor = self.multiworld.final_floor[self.player].value - self.gear_variety_after_b9 = self.multiworld.gear_variety_after_b9[self.player].value - self.goal = self.multiworld.goal[self.player].value - self.healing_floor_chance = self.multiworld.healing_floor_chance[self.player].value - self.initial_floor = self.multiworld.initial_floor[self.player].value - self.iris_floor_chance = self.multiworld.iris_floor_chance[self.player].value - self.iris_treasures_required = self.multiworld.iris_treasures_required[self.player].value - self.master_hp = self.multiworld.master_hp[self.player].value - self.party_starting_level = self.multiworld.party_starting_level[self.player] - self.run_speed = self.multiworld.run_speed[self.player].value - self.shuffle_capsule_monsters = self.multiworld.shuffle_capsule_monsters[self.player] - self.shuffle_party_members = self.multiworld.shuffle_party_members[self.player] + self.o = L2ACOptions(**{opt: getattr(self.multiworld, opt)[self.player] for opt in self.option_definitions}) - if self.capsule_starting_level.value == CapsuleStartingLevel.special_range_names["party_starting_level"]: - self.capsule_starting_level.value = self.party_starting_level.value - if self.initial_floor >= self.final_floor: - self.initial_floor = self.final_floor - 1 - if self.master_hp == MasterHp.special_range_names["scale"]: - self.master_hp = MasterHp.scale(self.final_floor) - if self.shuffle_party_members: - self.default_party.value = DefaultParty.default + if self.o.blue_chest_count < self.o.custom_item_pool.count: + raise ValueError(f"Number of items in custom_item_pool ({self.o.custom_item_pool.count}) is " + f"greater than blue_chest_count ({self.o.blue_chest_count}).") + if self.o.capsule_starting_level == CapsuleStartingLevel.special_range_names["party_starting_level"]: + self.o.capsule_starting_level.value = int(self.o.party_starting_level) + if self.o.initial_floor >= self.o.final_floor: + self.o.initial_floor.value = self.o.final_floor - 1 + if self.o.shuffle_party_members: + self.o.default_party.value = DefaultParty.default def create_regions(self) -> None: menu = Region("Menu", self.player, self.multiworld) @@ -134,10 +94,10 @@ class L2ACWorld(World): ancient_dungeon = Region("AncientDungeon", self.player, self.multiworld, "Ancient Dungeon") ancient_dungeon.exits.append(Entrance(self.player, "FinalFloorEntrance", ancient_dungeon)) - item_count: int = self.blue_chest_count - if self.shuffle_capsule_monsters: + item_count: int = int(self.o.blue_chest_count) + if self.o.shuffle_capsule_monsters: item_count += len(self.item_name_groups["Capsule monsters"]) - if self.shuffle_party_members: + if self.o.shuffle_party_members: item_count += len(self.item_name_groups["Party members"]) for location_name, location_id in itertools.islice(l2ac_location_name_to_id.items(), item_count): ancient_dungeon.locations.append(L2ACLocation(self.player, location_name, location_id, ancient_dungeon)) @@ -167,21 +127,23 @@ class L2ACWorld(World): .connect(self.multiworld.get_region("FinalFloor", self.player)) def create_items(self) -> None: - item_pool: List[str] = \ - self.multiworld.random.choices(sorted(self.item_name_groups["Blue chest items"]), k=self.blue_chest_count) - if self.shuffle_capsule_monsters: + item_pool: List[str] = self.multiworld.random.choices(sorted(self.item_name_groups["Blue chest items"]), + k=self.o.blue_chest_count - self.o.custom_item_pool.count) + item_pool += [item_name for item_name, count in self.o.custom_item_pool.items() for _ in range(count)] + + if self.o.shuffle_capsule_monsters: item_pool += self.item_name_groups["Capsule monsters"] - self.blue_chest_count += len(self.item_name_groups["Capsule monsters"]) - if self.shuffle_party_members: + self.o.blue_chest_count.value += len(self.item_name_groups["Capsule monsters"]) + if self.o.shuffle_party_members: item_pool += self.item_name_groups["Party members"] - self.blue_chest_count += len(self.item_name_groups["Party members"]) + self.o.blue_chest_count.value += len(self.item_name_groups["Party members"]) for item_name in item_pool: item_data: ItemData = l2ac_item_table[item_name] item_id: int = items_start_id + item_data.code self.multiworld.itempool.append(L2ACItem(item_name, item_data.classification, item_id, self.player)) def set_rules(self) -> None: - for i in range(1, self.blue_chest_count): + for i in range(1, self.o.blue_chest_count): if i % CHESTS_PER_SPHERE == 0: set_rule(self.multiworld.get_location(f"Blue chest {i + 1}", self.player), lambda state, j=i: state.has("Progressive chest access", self.player, j // CHESTS_PER_SPHERE)) @@ -192,27 +154,27 @@ class L2ACWorld(World): lambda state, j=i: state.can_reach(f"Blue chest {j}", "Location", self.player)) set_rule(self.multiworld.get_entrance("FinalFloorEntrance", self.player), - lambda state: state.can_reach(f"Blue chest {self.blue_chest_count}", "Location", self.player)) + lambda state: state.can_reach(f"Blue chest {self.o.blue_chest_count}", "Location", self.player)) set_rule(self.multiworld.get_location("Iris Treasures", self.player), - lambda state: state.can_reach(f"Blue chest {self.blue_chest_count}", "Location", self.player)) + lambda state: state.can_reach(f"Blue chest {self.o.blue_chest_count}", "Location", self.player)) set_rule(self.multiworld.get_location("Boss", self.player), - lambda state: state.can_reach(f"Blue chest {self.blue_chest_count}", "Location", self.player)) - if self.shuffle_capsule_monsters: + lambda state: state.can_reach(f"Blue chest {self.o.blue_chest_count}", "Location", self.player)) + if self.o.shuffle_capsule_monsters: add_rule(self.multiworld.get_location("Boss", self.player), lambda state: state.has("DARBI", self.player)) - if self.shuffle_party_members: + if self.o.shuffle_party_members: add_rule(self.multiworld.get_location("Boss", self.player), lambda state: state.has("Dekar", self.player) and state.has("Guy", self.player) and state.has("Arty", self.player)) - if self.goal == Goal.option_final_floor: + if self.o.goal == Goal.option_final_floor: self.multiworld.completion_condition[self.player] = \ lambda state: state.has("Final Floor access", self.player) - elif self.goal == Goal.option_iris_treasure_hunt: + elif self.o.goal == Goal.option_iris_treasure_hunt: self.multiworld.completion_condition[self.player] = \ lambda state: state.has("Treasures collected", self.player) - elif self.goal == Goal.option_boss: + elif self.o.goal == Goal.option_boss: self.multiworld.completion_condition[self.player] = \ lambda state: state.has("Boss victory", self.player) - elif self.goal == Goal.option_boss_iris_treasure_hunt: + elif self.o.goal == Goal.option_boss_iris_treasure_hunt: self.multiworld.completion_condition[self.player] = \ lambda state: state.has("Boss victory", self.player) and state.has("Treasures collected", self.player) @@ -223,39 +185,45 @@ class L2ACWorld(World): rom_bytearray = bytearray(apply_basepatch(get_base_rom_bytes())) # start and stop indices are offsets in the ROM file, not LoROM mapped SNES addresses rom_bytearray[0x007FC0:0x007FC0 + 21] = self.rom_name - rom_bytearray[0x014308:0x014308 + 1] = self.capsule_starting_level.value.to_bytes(1, "little") - rom_bytearray[0x01432F:0x01432F + 1] = self.capsule_starting_form.unlock.to_bytes(1, "little") - rom_bytearray[0x01433C:0x01433C + 1] = self.capsule_starting_form.value.to_bytes(1, "little") - rom_bytearray[0x0190D5:0x0190D5 + 1] = self.iris_floor_chance.to_bytes(1, "little") - rom_bytearray[0x019153:0x019153 + 1] = (0x63 - self.blue_chest_chance).to_bytes(1, "little") - rom_bytearray[0x019176] = 0x38 if self.gear_variety_after_b9 else 0x18 - rom_bytearray[0x019477:0x019477 + 1] = self.healing_floor_chance.to_bytes(1, "little") - rom_bytearray[0x0194A2:0x0194A2 + 1] = self.crowded_floor_chance.to_bytes(1, "little") - rom_bytearray[0x019E82:0x019E82 + 1] = self.final_floor.to_bytes(1, "little") - rom_bytearray[0x01FC75:0x01FC75 + 1] = self.run_speed.to_bytes(1, "little") - rom_bytearray[0x01FC81:0x01FC81 + 1] = self.run_speed.to_bytes(1, "little") - rom_bytearray[0x02B2A1:0x02B2A1 + 5] = self.default_party.roster + rom_bytearray[0x014308:0x014308 + 1] = self.o.capsule_starting_level.value.to_bytes(1, "little") + rom_bytearray[0x01432F:0x01432F + 1] = self.o.capsule_starting_form.unlock.to_bytes(1, "little") + rom_bytearray[0x01433C:0x01433C + 1] = self.o.capsule_starting_form.value.to_bytes(1, "little") + rom_bytearray[0x0190D5:0x0190D5 + 1] = self.o.iris_floor_chance.value.to_bytes(1, "little") + rom_bytearray[0x019147:0x019157 + 1:4] = self.o.blue_chest_chance.chest_type_thresholds + rom_bytearray[0x019176] = 0x38 if self.o.gear_variety_after_b9 else 0x18 + rom_bytearray[0x019477:0x019477 + 1] = self.o.healing_floor_chance.value.to_bytes(1, "little") + rom_bytearray[0x0194A2:0x0194A2 + 1] = self.o.crowded_floor_chance.value.to_bytes(1, "little") + rom_bytearray[0x019E82:0x019E82 + 1] = self.o.final_floor.value.to_bytes(1, "little") + rom_bytearray[0x01FC75:0x01FC75 + 1] = self.o.run_speed.value.to_bytes(1, "little") + rom_bytearray[0x01FC81:0x01FC81 + 1] = self.o.run_speed.value.to_bytes(1, "little") + rom_bytearray[0x02B2A1:0x02B2A1 + 5] = self.o.default_party.roster for offset in range(0x02B395, 0x02B452, 0x1B): - rom_bytearray[offset:offset + 1] = self.party_starting_level.value.to_bytes(1, "little") + rom_bytearray[offset:offset + 1] = self.o.party_starting_level.value.to_bytes(1, "little") for offset in range(0x02B39A, 0x02B457, 0x1B): - rom_bytearray[offset:offset + 3] = self.party_starting_level.xp.to_bytes(3, "little") + rom_bytearray[offset:offset + 3] = self.o.party_starting_level.xp.to_bytes(3, "little") rom_bytearray[0x05699E:0x05699E + 147] = self.get_goal_text_bytes() - rom_bytearray[0x056AA3:0x056AA3 + 24] = self.default_party.event_script - rom_bytearray[0x072742:0x072742 + 1] = self.boss.value.to_bytes(1, "little") - rom_bytearray[0x072748:0x072748 + 1] = self.boss.flag.to_bytes(1, "little") + rom_bytearray[0x056AA3:0x056AA3 + 24] = self.o.default_party.event_script + rom_bytearray[0x072742:0x072742 + 1] = self.o.boss.value.to_bytes(1, "little") + rom_bytearray[0x072748:0x072748 + 1] = self.o.boss.flag.to_bytes(1, "little") rom_bytearray[0x09D59B:0x09D59B + 256] = self.get_node_connection_table() - rom_bytearray[0x0B4F02:0x0B4F02 + 2] = self.master_hp.to_bytes(2, "little") - rom_bytearray[0x280010:0x280010 + 2] = self.blue_chest_count.to_bytes(2, "little") - rom_bytearray[0x280012:0x280012 + 3] = self.capsule_starting_level.xp.to_bytes(3, "little") - rom_bytearray[0x280015:0x280015 + 1] = self.initial_floor.to_bytes(1, "little") - rom_bytearray[0x280016:0x280016 + 1] = self.default_capsule.to_bytes(1, "little") - rom_bytearray[0x280017:0x280017 + 1] = self.iris_treasures_required.to_bytes(1, "little") - rom_bytearray[0x280018:0x280018 + 1] = self.shuffle_party_members.unlock.to_bytes(1, "little") - rom_bytearray[0x280019:0x280019 + 1] = self.shuffle_capsule_monsters.unlock.to_bytes(1, "little") - rom_bytearray[0x280030:0x280030 + 1] = self.goal.to_bytes(1, "little") - rom_bytearray[0x28003D:0x28003D + 1] = self.death_link.to_bytes(1, "little") + rom_bytearray[0x0B05C0:0x0B05C0 + 18843] = self.get_enemy_stats() + rom_bytearray[0x0B4F02:0x0B4F02 + 2] = self.o.master_hp.value.to_bytes(2, "little") + rom_bytearray[0x280010:0x280010 + 2] = self.o.blue_chest_count.value.to_bytes(2, "little") + rom_bytearray[0x280012:0x280012 + 3] = self.o.capsule_starting_level.xp.to_bytes(3, "little") + rom_bytearray[0x280015:0x280015 + 1] = self.o.initial_floor.value.to_bytes(1, "little") + rom_bytearray[0x280016:0x280016 + 1] = self.o.default_capsule.value.to_bytes(1, "little") + rom_bytearray[0x280017:0x280017 + 1] = self.o.iris_treasures_required.value.to_bytes(1, "little") + rom_bytearray[0x280018:0x280018 + 1] = self.o.shuffle_party_members.unlock.to_bytes(1, "little") + rom_bytearray[0x280019:0x280019 + 1] = self.o.shuffle_capsule_monsters.unlock.to_bytes(1, "little") + rom_bytearray[0x280030:0x280030 + 1] = self.o.goal.value.to_bytes(1, "little") + rom_bytearray[0x28003D:0x28003D + 1] = self.o.death_link.value.to_bytes(1, "little") rom_bytearray[0x281200:0x281200 + 470] = self.get_capsule_cravings_table() + (rom_bytearray[0x08A1D4:0x08A1D4 + 128], + rom_bytearray[0x0A595C:0x0A595C + 200], + rom_bytearray[0x0A5DF6:0x0A5DF6 + 192], + rom_bytearray[0x27F6B5:0x27F6B5 + 113]) = self.get_enemy_floors_sprites_and_movement_patterns() + with open(rom_path, "wb") as f: f.write(rom_bytearray) except Exception as e: @@ -276,13 +244,19 @@ class L2ACWorld(World): # end of ordered Main.py calls def create_item(self, name: str) -> Item: - item_data: ItemData = l2ac_item_table.get(name) + item_data: ItemData = l2ac_item_table[name] return L2ACItem(name, item_data.classification, items_start_id + item_data.code, self.player) + def get_filler_item_name(self) -> str: + return ["Potion", "Hi-Magic", "Miracle", "Hi-Potion", "Potion", "Ex-Potion", "Regain", "Ex-Magic", "Hi-Magic"][ + (self.multiworld.random.randrange(9) + self.multiworld.random.randrange(9)) // 2] + + # end of overridden AutoWorld.py methods + def get_capsule_cravings_table(self) -> bytes: rom: bytes = get_base_rom_bytes() - if self.capsule_cravings_jp_style: + if self.o.capsule_cravings_jp_style: number_of_items: int = 467 items_offset: int = 0x0B4F69 value_thresholds: List[int] = \ @@ -307,17 +281,92 @@ class L2ACWorld(World): else: return rom[0x0AFF16:0x0AFF16 + 470] + def get_enemy_floors_sprites_and_movement_patterns(self) -> Tuple[bytes, bytes, bytes, bytes]: + rom: bytes = get_base_rom_bytes() + + if self.o.enemy_floor_numbers == EnemyFloorNumbers.default \ + and self.o.enemy_sprites == EnemySprites.default \ + and self.o.enemy_movement_patterns == EnemyMovementPatterns.default: + return rom[0x08A1D4:0x08A1D4 + 128], rom[0x0A595C:0x0A595C + 200], \ + rom[0x0A5DF6:0x0A5DF6 + 192], rom[0x27F6B5:0x27F6B5 + 113] + + formations: bytes = rom[0x0A595C:0x0A595C + 200] + sprites: bytes = rom[0x0A5DF6:0x0A5DF6 + 192] + indices: bytes = rom[0x27F6B5:0x27F6B5 + 113] + pointers: List[bytes] = [rom[0x08A1D4 + 2 * index:0x08A1D4 + 2 * index + 2] for index in range(64)] + + used_formations: List[int] = list(formations) + formation_set: Set[int] = set(used_formations) + used_sprites: List[int] = [sprite for formation, sprite in enumerate(sprites) if formation in formation_set] + sprite_set: Set[int] = set(used_sprites) + used_indices: List[int] = [index for sprite, index in enumerate(indices, 128) if sprite in sprite_set] + index_set: Set[int] = set(used_indices) + used_pointers: List[bytes] = [pointer for index, pointer in enumerate(pointers) if index in index_set] + + slot_random: Random = self.multiworld.per_slot_randoms[self.player] + + d: int = 2 * 6 + if self.o.enemy_floor_numbers == EnemyFloorNumbers.option_shuffle: + constrained_shuffle(used_formations, d, random=slot_random) + elif self.o.enemy_floor_numbers == EnemyFloorNumbers.option_randomize: + used_formations = constrained_choices(used_formations, d, k=len(used_formations), random=slot_random) + + if self.o.enemy_sprites == EnemySprites.option_shuffle: + slot_random.shuffle(used_sprites) + elif self.o.enemy_sprites == EnemySprites.option_randomize: + used_sprites = slot_random.choices(tuple(dict.fromkeys(used_sprites)), k=len(used_sprites)) + elif self.o.enemy_sprites == EnemySprites.option_singularity: + used_sprites = [slot_random.choice(tuple(dict.fromkeys(used_sprites)))] * len(used_sprites) + elif self.o.enemy_sprites.sprite: + used_sprites = [self.o.enemy_sprites.sprite] * len(used_sprites) + + if self.o.enemy_movement_patterns == EnemyMovementPatterns.option_shuffle_by_pattern: + slot_random.shuffle(used_pointers) + elif self.o.enemy_movement_patterns == EnemyMovementPatterns.option_randomize_by_pattern: + used_pointers = slot_random.choices(tuple(dict.fromkeys(used_pointers)), k=len(used_pointers)) + elif self.o.enemy_movement_patterns == EnemyMovementPatterns.option_shuffle_by_sprite: + slot_random.shuffle(used_indices) + elif self.o.enemy_movement_patterns == EnemyMovementPatterns.option_randomize_by_sprite: + used_indices = slot_random.choices(tuple(dict.fromkeys(used_indices)), k=len(used_indices)) + elif self.o.enemy_movement_patterns == EnemyMovementPatterns.option_singularity: + used_indices = [slot_random.choice(tuple(dict.fromkeys(used_indices)))] * len(used_indices) + elif self.o.enemy_movement_patterns.sprite: + used_indices = [indices[self.o.enemy_movement_patterns.sprite - 128]] * len(used_indices) + + sprite_iter: Iterator[int] = iter(used_sprites) + index_iter: Iterator[int] = iter(used_indices) + pointer_iter: Iterator[bytes] = iter(used_pointers) + formations = bytes(used_formations) + sprites = bytes(next(sprite_iter) if form in formation_set else sprite for form, sprite in enumerate(sprites)) + indices = bytes(next(index_iter) if sprite in sprite_set else idx for sprite, idx in enumerate(indices, 128)) + pointers = [next(pointer_iter) if idx in index_set else pointer for idx, pointer in enumerate(pointers)] + return b"".join(pointers), formations, sprites, indices + + def get_enemy_stats(self) -> bytes: + rom: bytes = get_base_rom_bytes() + + if self.o.exp_modifier == ExpModifier.default: + return rom[0x0B05C0:0x0B05C0 + 18843] + + number_of_enemies: int = 224 + enemy_stats = bytearray(rom[0x0B05C0:0x0B05C0 + 18843]) + + for enemy_id in range(number_of_enemies): + pointer: int = int.from_bytes(enemy_stats[2 * enemy_id:2 * enemy_id + 2], "little") + enemy_stats[pointer + 29:pointer + 31] = self.o.exp_modifier(enemy_stats[pointer + 29:pointer + 31]) + return enemy_stats + def get_goal_text_bytes(self) -> bytes: goal_text: List[str] = [] - iris: str = f"{self.iris_treasures_required} Iris treasure{'s' if self.iris_treasures_required > 1 else ''}" - if self.goal == Goal.option_boss: - goal_text = ["You have to defeat", f"the boss on B{self.final_floor}."] - elif self.goal == Goal.option_iris_treasure_hunt: + iris: str = f"{self.o.iris_treasures_required} Iris treasure{'s' if self.o.iris_treasures_required > 1 else ''}" + if self.o.goal == Goal.option_boss: + goal_text = ["You have to defeat", f"the boss on B{self.o.final_floor}."] + elif self.o.goal == Goal.option_iris_treasure_hunt: goal_text = ["You have to find", f"{iris}."] - elif self.goal == Goal.option_boss_iris_treasure_hunt: - goal_text = ["You have to retrieve", f"{iris} and", f"defeat the boss on B{self.final_floor}."] - elif self.goal == Goal.option_final_floor: - goal_text = [f"You need to get to B{self.final_floor}."] + elif self.o.goal == Goal.option_boss_iris_treasure_hunt: + goal_text = ["You have to retrieve", f"{iris} and", f"defeat the boss on B{self.o.final_floor}."] + elif self.o.goal == Goal.option_final_floor: + goal_text = [f"You need to get to B{self.o.final_floor}."] assert len(goal_text) <= 4 and all(len(line) <= 28 for line in goal_text), goal_text goal_text_bytes = bytes((0x08, *b"\x03".join(line.encode("ascii") for line in goal_text), 0x00)) return goal_text_bytes + b"\x00" * (147 - len(goal_text_bytes)) diff --git a/worlds/lufia2ac/basepatch/basepatch.asm b/worlds/lufia2ac/basepatch/basepatch.asm index d263b3d4f0..a2ea539fd4 100644 --- a/worlds/lufia2ac/basepatch/basepatch.asm +++ b/worlds/lufia2ac/basepatch/basepatch.asm @@ -3,13 +3,13 @@ lorom org $DFFFFD ; expand ROM to 3MB DB "EOF" -org $80FFD8 ; expand SRAM to 16KB - DB $04 ; overwrites DB $03 +org $80FFD8 ; expand SRAM to 32KB + DB $05 ; overwrites DB $03 org $80809A ; patch copy protection - CMP $704000 ; overwrites CMP $702000 + CMP $710000 ; overwrites CMP $702000 org $8080A6 ; patch copy protection - CMP $704000 ; overwrites CMP $702000 + CMP $710000 ; overwrites CMP $702000 @@ -34,8 +34,8 @@ org $8AF681 ; skip gruberik lexis dialogue org $8EA349 ; skip ancient cave entrance dialogue DB $1C,$B0,$01 ; L2SASM JMP $8EA1AD+$01B0 -org $8EA384 ; skip ancient cave exit dialogue - DB $1C,$2B,$02 ; L2SASM JMP $8EA1AD+$022B +org $8EA384 ; reset architect mode, skip ancient cave exit dialogue + DB $1B,$E1,$1C,$2B,$02 ; clear flag $E1, L2SASM JMP $8EA1AD+$022B org $8EA565 ; skip ancient cave leaving dialogue DB $1C,$E9,$03 ; L2SASM JMP $8EA1AD+$03E9 @@ -108,11 +108,13 @@ Init: STX $4302 ; A-bus destination address $F02000 (SRAM) LDA.b #$F0 STA $4304 - STX $4305 ; transfer 8kB + LDX.w #$6000 + STX $4305 ; transfer 24kB LDA.b #$01 STA $420B ; start DMA channel 1 ; sign expanded SRAM PHB + TDC LDA.b #$3F LDX.w #$8000 LDY.w #$2000 @@ -213,6 +215,8 @@ RX: JSR SpecialItemGet SEP #$20 JSL $8EC1EF ; call chest opening routine (but without chest opening animation) + STZ $A7 ; cleanup + JSL $83AB4F ; cleanup +: SEP #$20 RTS @@ -268,11 +272,15 @@ SpecialItemUse: SBC.w #$01B1 ; party member items range from $01B2 to $01B7 BMI + ASL + TAX ASL ASL ADC.w #$FD2E STA $09B7 ; set pointer to L2SASM join script SEP #$20 + LDA $8ED8C7,X ; load predefined bitmask with a single bit set + BIT $077E ; check against EV flags $02 to $07 (party member flags) + BNE + ; abort if character already present LDA $07A9 ; load EV register $11 (party counter) CMP.b #$03 BPL + ; abort if party full @@ -593,18 +601,16 @@ FinalFloor: pushpc org $8488BB ; DB=$84, x=0, m=0 - SEC ; {carry clear = disable this feature, carry set = enable this feature} JSL Providence ; overwrites LDX.w #$1402 : STX $0A8D - NOP ; + NOP #2 pullpc Providence: LDX.w #$1402 ; (overwritten instruction) STX $0A8D ; (overwritten instruction) add Potion x10 - BCC + - LDX.w #$022D ; + LDX.w #$022D STX $0A8F ; add Providence -+: RTL + RTL @@ -646,6 +652,142 @@ StartInventory: +; architect mode +pushpc +org $8EA1E7 +base = $8EA1AD ; ancient cave entrance script base + DB $15,$E1 : DW .locked-base ; L2SASM JMP .locked if flag $E1 set + DB $08,"Did you like the layout",$03, \ + "of the last cave? I can",$03, \ + "lock it down and prevent",$03, \ + "the cave from changing.",$01 + DB $08,"Do you want to lock",$03, \ + "the cave layout?",$01 + DB $10,$02 : DW .cancel-base,.lock-base ; setup 2 choices: .cancel and .lock + DB $08,"Cancel",$0F,"LOCK IT DOWN!",$0B +.cancel: + DB $4C,$54,$00 ; play sound $54, END +.lock: + DB $5A,$05,$03,$7F,$37,$28,$56,$4C,$6B,$1A,$E1 ; shake, delay $28 f, stop shake, play sound $6B, set flag $E1 +.locked: + DB $08,"It's locked down.",$00 + warnpc $8EA344 +org $839018 + ; DB=$83, x=0, m=1 + JSL ArchitectMode ; overwrites LDA.b #$7E : PHA : PLB +pullpc + +ArchitectMode: +; check current mode + LDA $079A + BIT.b #$02 + BEQ + ; go to write mode if flag $E1 (i.e., bit $02 in $079A) not set +; read mode (replaying the locked down layout) + JSR ArchitectBlockAddress + LDA $F00000,X ; check if current block is marked as filled + BEQ + ; go to write mode if block unused + TDC + LDA.b #$36 + LDY.w #$0521 + INX + MVN $7E,$F0 ; restore 55 RNG values from $F00000,X to $7E0521 + INX + LDA $F00000,X + STA $0559 ; restore current RNG index from $F00000,X to $7E0559 + BRA ++ +; write mode (recording the layout) ++: JSR ArchitectClearBlocks + JSR ArchitectBlockAddress + LDA $7FE696 + STA $F00000,X ; mark block as used + TDC + LDA.b #$36 + LDX.w #$0521 + INY + MVN $F0,$7E ; backup 55 RNG values from $7E0521 to $F00000,Y + INY + LDA $7E0559 + STA $0000,Y ; backup current RNG index from $7E0559 to $F00000,Y + LDA.b #$7E ; (overwritten instruction) set DB=$7E + PHA ; (overwritten instruction) + PLB ; (overwritten instruction) +++: RTL + +ArchitectClearBlocks: + LDA $7FE696 ; read next floor number + CMP $D08015 ; compare initial floor number + BEQ + + BRL ++ ; skip if not initial floor ++: LDA.b #$F0 + PHA + PLB + !floor = 1 + while !floor < 99 ; mark all blocks as unused + STZ !floor*$40+$6000 + !floor #= !floor+1 + endwhile +++: RTS + +ArchitectBlockAddress: +; calculate target SRAM address + TDC + LDA $7FE696 ; read next floor number + REP #$20 + ASL #6 + ADC.w #$6000 ; target SRAM address = next_floor * $40 + $6000 + TAX + TAY + SEP #$20 + RTS + + + +; for architect mode: make red chest behavior for iris treasure replacements independent of current inventory +; by ensuring the same number of RNG calls, no matter if you have the iris item already or not +; (done by prefilling *all* chests first and potentially overwriting one of them with an iris item afterwards, +; instead of checking the iris item first and then potentially filling *one fewer* regular chest) +pushpc +org $8390C9 + ; DB=$96, x=0, m=1 + NOP ; overwrites LDY.w #$0000 + BRA + ; go to regular red chest generation +-: ; iris treasure handling happens below +org $839114 + ; DB=$7F, x=0, m=1 + NOP #36 ; overwrites all of providence handling + LDA.b #$83 ; (overwritten instruction from org $8391E9) set DB=$83 for floor layout generation + PHA ; (overwritten instruction from org $8391E9) + PLB ; (overwritten instruction from org $8391E9) + BRL ++ ; go to end ++: LDY.w #$0000 ; (overwritten instruction from org $8390C9) initialize chest index + ; red chests are filled below +org $8391E9 + ; DB=$7F, x=0, m=1 + NOP ; overwrites LDA.b #$83 : PHA : PLB + BRL - ; go to iris treasure handling +++: ; floor layout generation happens below +pullpc + + + +; for architect mode: make red chest behavior for spell replacements independent of currently learned spells +; by ensuring the same number of RNG calls, no matter if you have the spell already or not +pushpc +org $8391A6 + ; DB=$7F, x=0, m=1 + JSL SpellRNG ; overwrites LDA.b #$80 : STA $E747,Y + NOP +pullpc + +SpellRNG: + LDA.b #$80 ; (overwritten instruction) mark chest item as spell + STA $E747,Y ; (overwritten instruction) + JSL $8082C7 ; + JSL $8082C7 ; advance RNG twice + RTL + + + ; increase variety of red chest gear after B9 pushpc org $839176 @@ -891,3 +1033,4 @@ pullpc ; $F02800 2 received counter ; $F02802 2 processed counter ; $F02804 inf list of received items +; $F06000 inf architect mode RNG state backups diff --git a/worlds/lufia2ac/basepatch/basepatch.bsdiff4 b/worlds/lufia2ac/basepatch/basepatch.bsdiff4 index e31ac74a52..7d622537c1 100644 Binary files a/worlds/lufia2ac/basepatch/basepatch.bsdiff4 and b/worlds/lufia2ac/basepatch/basepatch.bsdiff4 differ diff --git a/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md b/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md index 375c673236..d1247a9e20 100644 --- a/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md +++ b/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md @@ -41,18 +41,26 @@ Your Party Leader will hold up the item they received when not in a fight or in - Choose a goal for your world. Possible goals are: 1) Reach the final floor; 2) Defeat the boss on the final floor; 3) Retrieve a (customizable) number of iris treasures from the cave; 4) Retrieve the iris treasures *and* defeat the boss - You can also randomize the goal; The blue-haired NPC in front of the cafe can tell you about the selected objective -- Customize (or randomize) the chances of encountering blue chests, healing tiles, iris treasures, etc. -- Customize (or randomize) the default party lineup and capsule monster -- Customize (or randomize) the party starting level as well as capsule monster level and form -- Customize (or randomize) the initial and final floor numbers -- Customize (or randomize) the boss that resides on the final floor +- Customize the chances of encountering blue chests, healing tiles, iris treasures, etc. +- Customize the default party lineup and capsule monster +- Customize the party starting level as well as capsule monster level and form +- Customize the initial and final floor numbers +- Customize the boss that resides on the final floor +- Customize the multiworld item pool. (By default, your pool is filled with random blue chest items, but you can place + any cave item you want instead) - Customize start inventory, i.e., begin every run with certain items or spells of your choice +- Adjust how much EXP is gained from enemies +- Randomize enemy movement patterns, enemy sprites, and which enemy types can appear at which floor numbers - Option to shuffle your party members and/or capsule monsters into the multiworld, meaning that someone will have to - find them in order to unlock them for you to use + find them in order to unlock them for you to use. While cave diving, you can add newly unlocked members to your party + by using the character items from your inventory ###### Quality of life: - Various streamlining tweaks (removed cutscenes, dialogue, transitions) +- You can elect to lock the cave layout for the next run, giving you exactly the same floors and red chest contents as + on your previous attempt. This functionality is accessed via the bald NPC behind the counter at the Ancient Cave + Entrance - Always start with Providence already in your inventory. (It is no longer obtained from red chests) - (optional) Run button that allows you to move at faster than normal speed diff --git a/worlds/lufia2ac/test/TestCustomItemPool.py b/worlds/lufia2ac/test/TestCustomItemPool.py new file mode 100644 index 0000000000..97d4cab2f2 --- /dev/null +++ b/worlds/lufia2ac/test/TestCustomItemPool.py @@ -0,0 +1,57 @@ +from argparse import Namespace + +from BaseClasses import PlandoOptions +from Generate import handle_option +from . import L2ACTestBase +from ..Options import CustomItemPool + + +class TestEmpty(L2ACTestBase): + options = { + "custom_item_pool": {}, + } + + def test_empty(self) -> None: + self.assertEqual(0, len(self.get_items_by_name("Dekar blade"))) + + +class TestINeedDekarBlade(L2ACTestBase): + options = { + "custom_item_pool": { + "Dekar blade": 2, + }, + } + + def test_i_need_dekar_blade(self) -> None: + self.assertEqual(2, len(self.get_items_by_name("Dekar blade"))) + + +class TestVerifyCount(L2ACTestBase): + auto_construct = False + options = { + "custom_item_pool": { + "Dekar blade": 26, + }, + } + + def test_verify_count(self) -> None: + self.assertRaisesRegex(ValueError, + "Number of items in custom_item_pool \\(26\\) is greater than blue_chest_count \\(25\\)", + lambda: self.world_setup()) + + +class TestVerifyItemName(L2ACTestBase): + auto_construct = False + options = { + "custom_item_pool": { + "The car blade": 2, + }, + } + + def test_verify_item_name(self) -> None: + self.assertRaisesRegex(Exception, + "Item The car blade from option CustomItemPool\\(The car blade: 2\\) is not a " + "valid item name from Lufia II Ancient Cave\\. Did you mean 'Dekar blade'", + lambda: handle_option(Namespace(game="Lufia II Ancient Cave", name="Player"), + self.options, "custom_item_pool", CustomItemPool, + PlandoOptions(0))) diff --git a/worlds/lufia2ac/test/TestGoal.py b/worlds/lufia2ac/test/TestGoal.py index 06393ff1ef..6dc78e66d2 100644 --- a/worlds/lufia2ac/test/TestGoal.py +++ b/worlds/lufia2ac/test/TestGoal.py @@ -2,13 +2,12 @@ from . import L2ACTestBase class TestDefault(L2ACTestBase): - options = {} - def testEverything(self): + def test_everything(self) -> None: self.collect_all_but(["Boss victory"]) self.assertBeatable(True) - def testNothing(self): + def test_nothing(self) -> None: self.assertBeatable(True) @@ -17,15 +16,15 @@ class TestShuffleCapsuleMonsters(L2ACTestBase): "shuffle_capsule_monsters": True, } - def testEverything(self): + def test_everything(self) -> None: self.collect_all_but(["Boss victory"]) self.assertBeatable(True) - def testBestParty(self): + def test_best_party(self) -> None: self.collect_by_name("DARBI") self.assertBeatable(True) - def testNoDarbi(self): + def test_no_darbi(self) -> None: self.collect_all_but(["Boss victory", "DARBI"]) self.assertBeatable(False) @@ -35,23 +34,23 @@ class TestShufflePartyMembers(L2ACTestBase): "shuffle_party_members": True, } - def testEverything(self): + def test_everything(self) -> None: self.collect_all_but(["Boss victory"]) self.assertBeatable(True) - def testBestParty(self): + def test_best_party(self) -> None: self.collect_by_name(["Dekar", "Guy", "Arty"]) self.assertBeatable(True) - def testNoDekar(self): + def test_no_dekar(self) -> None: self.collect_all_but(["Boss victory", "Dekar"]) self.assertBeatable(False) - def testNoGuy(self): + def test_no_guy(self) -> None: self.collect_all_but(["Boss victory", "Guy"]) self.assertBeatable(False) - def testNoArty(self): + def test_no_arty(self) -> None: self.collect_all_but(["Boss victory", "Arty"]) self.assertBeatable(False) @@ -62,26 +61,26 @@ class TestShuffleBoth(L2ACTestBase): "shuffle_party_members": True, } - def testEverything(self): + def test_everything(self) -> None: self.collect_all_but(["Boss victory"]) self.assertBeatable(True) - def testBestParty(self): + def test_best_party(self) -> None: self.collect_by_name(["Dekar", "Guy", "Arty", "DARBI"]) self.assertBeatable(True) - def testNoDekar(self): + def test_no_dekar(self) -> None: self.collect_all_but(["Boss victory", "Dekar"]) self.assertBeatable(False) - def testNoGuy(self): + def test_no_guy(self) -> None: self.collect_all_but(["Boss victory", "Guy"]) self.assertBeatable(False) - def testNoArty(self): + def test_no_arty(self) -> None: self.collect_all_but(["Boss victory", "Arty"]) self.assertBeatable(False) - def testNoDarbi(self): + def test_no_darbi(self) -> None: self.collect_all_but(["Boss victory", "DARBI"]) self.assertBeatable(False) diff --git a/worlds/messenger/Constants.py b/worlds/messenger/Constants.py new file mode 100644 index 0000000000..f967fec8cb --- /dev/null +++ b/worlds/messenger/Constants.py @@ -0,0 +1,153 @@ +# items +# listing individual groups first for easy lookup +NOTES = [ + "Key of Hope", + "Key of Chaos", + "Key of Courage", + "Key of Love", + "Key of Strength", + "Key of Symbiosis", +] + +PROG_ITEMS = [ + "Wingsuit", + "Rope Dart", + "Ninja Tabi", + "Power Thistle", + "Demon King Crown", + "Ruxxtin's Amulet", + "Fairy Bottle", + "Sun Crest", + "Moon Crest", + # "Astral Seed", + # "Astral Tea Leaves", +] + +PHOBEKINS = [ + "Necro", + "Pyro", + "Claustro", + "Acro", +] + +USEFUL_ITEMS = [ + "Windmill Shuriken", +] + +# item_name_to_id needs to be deterministic and match upstream +ALL_ITEMS = [ + *NOTES, + "Windmill Shuriken", + "Wingsuit", + "Rope Dart", + "Ninja Tabi", + # "Astral Seed", + # "Astral Tea Leaves", + "Candle", + "Seashell", + "Power Thistle", + "Demon King Crown", + "Ruxxtin's Amulet", + "Fairy Bottle", + "Sun Crest", + "Moon Crest", + *PHOBEKINS, + "Power Seal", + "Time Shard", # there's 45 separate instances of this in the client lookup, but hopefully we don't care? +] + +# locations +# the names of these don't actually matter, but using the upstream's names for now +# order must be exactly the same as upstream +ALWAYS_LOCATIONS = [ + # notes + "Key of Love", + "Key of Courage", + "Key of Chaos", + "Key of Symbiosis", + "Key of Strength", + "Key of Hope", + # upgrades + "Wingsuit", + "Rope Dart", + "Ninja Tabi", + "Climbing Claws", + # quest items + "Astral Seed", + "Astral Tea Leaves", + "Candle", + "Seashell", + "Power Thistle", + "Demon King Crown", + "Ruxxtin's Amulet", + "Fairy Bottle", + "Sun Crest", + "Moon Crest", + # phobekins + "Necro", + "Pyro", + "Claustro", + "Acro", +] + +SEALS = [ + "Ninja Village Seal - Tree House", + + "Autumn Hills Seal - Trip Saws", + "Autumn Hills Seal - Double Swing Saws", + "Autumn Hills Seal - Spike Ball Swing", + "Autumn Hills Seal - Spike Ball Darts", + + "Catacombs Seal - Triple Spike Crushers", + "Catacombs Seal - Crusher Gauntlet", + "Catacombs Seal - Dirty Pond", + + "Bamboo Creek Seal - Spike Crushers and Doors", + "Bamboo Creek Seal - Spike Ball Pits", + "Bamboo Creek Seal - Spike Crushers and Doors v2", + + "Howling Grotto Seal - Windy Saws and Balls", + "Howling Grotto Seal - Crushing Pits", + "Howling Grotto Seal - Breezy Crushers", + + "Quillshroom Marsh Seal - Spikey Window", + "Quillshroom Marsh Seal - Sand Trap", + "Quillshroom Marsh Seal - Do the Spike Wave", + + "Searing Crags Seal - Triple Ball Spinner", + "Searing Crags Seal - Raining Rocks", + "Searing Crags Seal - Rhythm Rocks", + + "Glacial Peak Seal - Ice Climbers", + "Glacial Peak Seal - Projectile Spike Pit", + "Glacial Peak Seal - Glacial Air Swag", + + "Tower of Time Seal - Time Waster Seal", + "Tower of Time Seal - Lantern Climb", + "Tower of Time Seal - Arcane Orbs", + + "Cloud Ruins Seal - Ghost Pit", + "Cloud Ruins Seal - Toothbrush Alley", + "Cloud Ruins Seal - Saw Pit", + "Cloud Ruins Seal - Money Farm Room", + + "Underworld Seal - Sharp and Windy Climb", + "Underworld Seal - Spike Wall", + "Underworld Seal - Fireball Wave", + "Underworld Seal - Rising Fanta", + + "Forlorn Temple Seal - Rocket Maze", + "Forlorn Temple Seal - Rocket Sunset", + + "Sunken Shrine Seal - Ultra Lifeguard", + "Sunken Shrine Seal - Waterfall Paradise", + "Sunken Shrine Seal - Tabi Gauntlet", + + "Riviere Turquoise Seal - Bounces and Balls", + "Riviere Turquoise Seal - Launch of Faith", + "Riviere Turquoise Seal - Flower Power", + + "Elemental Skylands Seal - Air", + "Elemental Skylands Seal - Water", + "Elemental Skylands Seal - Fire", +] diff --git a/worlds/messenger/Options.py b/worlds/messenger/Options.py new file mode 100644 index 0000000000..47ebf66f28 --- /dev/null +++ b/worlds/messenger/Options.py @@ -0,0 +1,76 @@ +from Options import DefaultOnToggle, DeathLink, Range, Accessibility, Choice + + +class MessengerAccessibility(Accessibility): + default = Accessibility.option_locations + # defaulting to locations accessibility since items makes certain items self-locking + __doc__ = Accessibility.__doc__.replace(f"default {Accessibility.default}", f"default {default}") + + +class Logic(Choice): + """ + The level of logic to use when determining what locations in your world are accessible. + Normal can require damage boosts, but otherwise approachable for someone who has beaten the game. + Hard has some easier speedrunning tricks in logic. May need to leash. + Challenging contains more medium and hard difficulty speedrunning tricks. + OoB places everything with the minimum amount of rules possible. Expect to do OoB. Not guaranteed completable. + """ + display_name = "Logic Level" + option_normal = 0 + option_hard = 1 + option_challenging = 2 + option_oob = 3 + + +class PowerSeals(DefaultOnToggle): + """Whether power seal locations should be randomized.""" + display_name = "Shuffle Seals" + + +class Goal(Choice): + """Requirement to finish the game. Power Seal Hunt will force power seal locations to be shuffled.""" + display_name = "Goal" + option_open_music_box = 0 + option_power_seal_hunt = 1 + + +class MusicBox(DefaultOnToggle): + """Whether the music box gauntlet needs to be done.""" + display_name = "Music Box Gauntlet" + + +class NotesNeeded(Range): + """How many notes are needed to access the Music Box.""" + display_name = "Notes Needed" + range_start = 1 + range_end = 6 + default = range_end + + +class AmountSeals(Range): + """Number of power seals that exist in the item pool when power seal hunt is the goal.""" + display_name = "Total Power Seals" + range_start = 1 + range_end = 45 + default = range_end + + +class RequiredSeals(Range): + """Percentage of total seals required to open the shop chest.""" + display_name = "Percent Seals Required" + range_start = 10 + range_end = 100 + default = range_end + + +messenger_options = { + "accessibility": MessengerAccessibility, + "logic_level": Logic, + "shuffle_seals": PowerSeals, + "goal": Goal, + "music_box": MusicBox, + "notes_needed": NotesNeeded, + "total_seals": AmountSeals, + "percent_seals_required": RequiredSeals, + "death_link": DeathLink, +} diff --git a/worlds/messenger/Regions.py b/worlds/messenger/Regions.py new file mode 100644 index 0000000000..ab84f0b3ce --- /dev/null +++ b/worlds/messenger/Regions.py @@ -0,0 +1,52 @@ +from typing import Dict, Set, List + +REGIONS: Dict[str, List[str]] = { + "Menu": [], + "Tower HQ": [], + "The Shop": [], + "Tower of Time": [], + "Ninja Village": ["Candle", "Astral Seed"], + "Autumn Hills": ["Climbing Claws", "Key of Hope"], + "Forlorn Temple": ["Demon King Crown"], + "Catacombs": ["Necro", "Ruxxtin's Amulet"], + "Bamboo Creek": ["Claustro"], + "Howling Grotto": ["Wingsuit"], + "Quillshroom Marsh": ["Seashell"], + "Searing Crags": ["Rope Dart"], + "Searing Crags Upper": ["Power Thistle", "Key of Strength", "Astral Tea Leaves"], + "Glacial Peak": [], + "Cloud Ruins": ["Acro"], + "Underworld": ["Pyro", "Key of Chaos"], + "Dark Cave": [], + "Riviere Turquoise": ["Fairy Bottle"], + "Sunken Shrine": ["Ninja Tabi", "Sun Crest", "Moon Crest", "Key of Love"], + "Elemental Skylands": ["Key of Symbiosis"], + "Corrupted Future": ["Key of Courage"], + "Music Box": ["Rescue Phantom"], +} +"""seal locations have the region in their name and may not need to be created so skip them here""" + + +REGION_CONNECTIONS: Dict[str, Set[str]] = { + "Menu": {"Tower HQ"}, + "Tower HQ": {"Autumn Hills", "Howling Grotto", "Searing Crags", "Glacial Peak", "Tower of Time", "Riviere Turquoise", + "Sunken Shrine", "Corrupted Future", "The Shop", "Music Box"}, + "Tower of Time": set(), + "Ninja Village": set(), + "Autumn Hills": {"Ninja Village", "Forlorn Temple", "Catacombs"}, + "Forlorn Temple": {"Catacombs", "Bamboo Creek"}, + "Catacombs": {"Autumn Hills", "Bamboo Creek", "Dark Cave"}, + "Bamboo Creek": {"Catacombs", "Howling Grotto"}, + "Howling Grotto": {"Bamboo Creek", "Quillshroom Marsh", "Sunken Shrine"}, + "Quillshroom Marsh": {"Howling Grotto", "Searing Crags"}, + "Searing Crags": {"Searing Crags Upper", "Quillshroom Marsh", "Underworld"}, + "Searing Crags Upper": {"Searing Crags", "Glacial Peak"}, + "Glacial Peak": {"Searing Crags Upper", "Tower HQ", "Cloud Ruins", "Elemental Skylands"}, + "Cloud Ruins": {"Underworld"}, + "Underworld": set(), + "Dark Cave": {"Catacombs", "Riviere Turquoise"}, + "Riviere Turquoise": set(), + "Sunken Shrine": {"Howling Grotto"}, + "Elemental Skylands": set(), +} +"""Vanilla layout mapping with all Tower HQ portals open. from -> to""" diff --git a/worlds/messenger/Rules.py b/worlds/messenger/Rules.py new file mode 100644 index 0000000000..a459fdb7d0 --- /dev/null +++ b/worlds/messenger/Rules.py @@ -0,0 +1,225 @@ +from typing import Dict, Callable, TYPE_CHECKING + +from BaseClasses import CollectionState, MultiWorld +from worlds.generic.Rules import set_rule, allow_self_locking_items, add_rule +from .Options import MessengerAccessibility, Goal +from .Constants import NOTES, PHOBEKINS + +if TYPE_CHECKING: + from . import MessengerWorld +else: + MessengerWorld = object + + +class MessengerRules: + player: int + world: MessengerWorld + region_rules: Dict[str, Callable[[CollectionState], bool]] + location_rules: Dict[str, Callable[[CollectionState], bool]] + + def __init__(self, world: MessengerWorld) -> None: + self.player = world.player + self.world = world + + self.region_rules = { + "Ninja Village": self.has_wingsuit, + "Autumn Hills": self.has_wingsuit, + "Catacombs": self.has_wingsuit, + "Bamboo Creek": self.has_wingsuit, + "Searing Crags Upper": self.has_vertical, + "Cloud Ruins": lambda state: self.has_wingsuit(state) and state.has("Ruxxtin's Amulet", self.player), + "Underworld": self.has_tabi, + "Forlorn Temple": lambda state: state.has_all({"Wingsuit", *PHOBEKINS}, self.player), + "Glacial Peak": self.has_vertical, + "Elemental Skylands": lambda state: state.has("Fairy Bottle", self.player), + "Music Box": lambda state: state.has_all(set(NOTES), self.player) and self.has_vertical(state), + } + + self.location_rules = { + # ninja village + "Ninja Village Seal - Tree House": self.has_dart, + # autumn hills + "Key of Hope": self.has_dart, + # howling grotto + "Howling Grotto Seal - Windy Saws and Balls": self.has_wingsuit, + "Howling Grotto Seal - Crushing Pits": lambda state: self.has_wingsuit(state) and self.has_dart(state), + # searing crags + "Astral Tea Leaves": lambda state: state.can_reach("Astral Seed", "Location", self.player), + "Key of Strength": lambda state: state.has("Power Thistle", self.player), + # glacial peak + "Glacial Peak Seal - Ice Climbers": self.has_dart, + "Glacial Peak Seal - Projectile Spike Pit": self.has_vertical, + "Glacial Peak Seal - Glacial Air Swag": self.has_vertical, + # tower of time + "Tower of Time Seal - Time Waster Seal": self.has_dart, + "Tower of Time Seal - Lantern Climb": self.has_wingsuit, + "Tower of Time Seal - Arcane Orbs": lambda state: self.has_wingsuit(state) and self.has_dart(state), + # underworld + "Underworld Seal - Sharp and Windy Climb": self.has_wingsuit, + "Underworld Seal - Fireball Wave": self.has_wingsuit, + "Underworld Seal - Rising Fanta": self.has_dart, + # sunken shrine + "Sun Crest": self.has_tabi, + "Moon Crest": self.has_tabi, + "Key of Love": lambda state: state.has_all({"Sun Crest", "Moon Crest"}, self.player), + "Sunken Shrine Seal - Waterfall Paradise": self.has_tabi, + "Sunken Shrine Seal - Tabi Gauntlet": self.has_tabi, + # riviere turquoise + "Fairy Bottle": self.has_vertical, + "Riviere Turquoise Seal - Flower Power": self.has_vertical, + # elemental skylands + "Key of Symbiosis": self.has_dart, + "Elemental Skylands Seal - Air": self.has_wingsuit, + "Elemental Skylands Seal - Water": self.has_dart, + "Elemental Skylands Seal - Fire": self.has_dart, + # corrupted future + "Key of Courage": lambda state: state.has_all({"Demon King Crown", "Fairy Bottle"}, self.player), + # the shop + "Shop Chest": self.has_enough_seals, + } + + def has_wingsuit(self, state: CollectionState) -> bool: + return state.has("Wingsuit", self.player) + + def has_dart(self, state: CollectionState) -> bool: + return state.has("Rope Dart", self.player) + + def has_tabi(self, state: CollectionState) -> bool: + return state.has("Ninja Tabi", self.player) + + def has_vertical(self, state: CollectionState) -> bool: + return self.has_wingsuit(state) or self.has_dart(state) + + def has_enough_seals(self, state: CollectionState) -> bool: + return not self.world.required_seals or state.has("Power Seal", self.player, self.world.required_seals) + + def true(self, state: CollectionState) -> bool: + """I know this is stupid, but it's easier to read in the dicts.""" + return True + + def set_messenger_rules(self) -> None: + multiworld = self.world.multiworld + + for region in multiworld.get_regions(self.player): + if region.name in self.region_rules: + for entrance in region.entrances: + entrance.access_rule = self.region_rules[region.name] + for loc in region.locations: + if loc.name in self.location_rules: + loc.access_rule = self.location_rules[loc.name] + if multiworld.goal[self.player] == Goal.option_power_seal_hunt: + set_rule(multiworld.get_entrance("Tower HQ -> Music Box", self.player), + lambda state: state.has("Shop Chest", self.player)) + + multiworld.completion_condition[self.player] = lambda state: state.has("Rescue Phantom", self.player) + if multiworld.accessibility[self.player] > MessengerAccessibility.option_locations: + set_self_locking_items(multiworld, self.player) + + +class MessengerHardRules(MessengerRules): + extra_rules: Dict[str, Callable[[CollectionState], bool]] + + def __init__(self, world: MessengerWorld) -> None: + super().__init__(world) + + self.region_rules.update({ + "Ninja Village": self.has_vertical, + "Autumn Hills": self.has_vertical, + "Catacombs": self.has_vertical, + "Bamboo Creek": self.has_vertical, + "Forlorn Temple": lambda state: self.has_vertical(state) and state.has_all(set(PHOBEKINS), self.player), + "Searing Crags Upper": self.true, + "Glacial Peak": self.true, + }) + + self.location_rules.update({ + "Howling Grotto Seal - Windy Saws and Balls": self.true, + "Glacial Peak Seal - Projectile Spike Pit": self.true, + "Claustro": self.has_wingsuit, + }) + + self.extra_rules = { + "Key of Strength": lambda state: self.has_dart(state) or self.has_windmill(state), + "Key of Symbiosis": self.has_windmill, + "Autumn Hills Seal - Spike Ball Darts": lambda state: (self.has_dart(state) and self.has_windmill(state)) + or self.has_wingsuit(state), + "Glacial Peak Seal - Glacial Air Swag": self.has_windmill, + "Underworld Seal - Fireball Wave": lambda state: state.has_all({"Ninja Tabi", "Windmill Shuriken"}, + self.player), + } + + def has_windmill(self, state: CollectionState) -> bool: + return state.has("Windmill Shuriken", self.player) + + def set_messenger_rules(self) -> None: + super().set_messenger_rules() + for loc, rule in self.extra_rules.items(): + if not self.world.multiworld.shuffle_seals[self.player] and "Seal" in loc: + continue + add_rule(self.world.multiworld.get_location(loc, self.player), rule, "or") + + +class MessengerChallengeRules(MessengerHardRules): + def __init__(self, world: MessengerWorld) -> None: + super().__init__(world) + + self.region_rules.update({ + "Forlorn Temple": lambda state: (self.has_vertical(state) and state.has_all(set(PHOBEKINS), self.player)) + or state.has_all({"Wingsuit", "Windmill Shuriken"}, self.player), + "Elemental Skylands": lambda state: self.has_wingsuit(state) or state.has("Fairy Bottle", self.player), + }) + + self.location_rules.update({ + "Fairy Bottle": self.true, + "Howling Grotto Seal - Crushing Pits": self.true, + "Underworld Seal - Sharp and Windy Climb": self.true, + "Riviere Turquoise Seal - Flower Power": self.true, + }) + + self.extra_rules.update({ + "Key of Hope": self.has_vertical, + "Key of Symbiosis": lambda state: self.has_vertical(state) or self.has_windmill(state), + }) + + +class MessengerOOBRules(MessengerRules): + def __init__(self, world: MessengerWorld) -> None: + self.world = world + self.player = world.player + + self.region_rules = { + "Elemental Skylands": lambda state: state.has_any({"Wingsuit", "Rope Dart", "Fairy Bottle"}, self.player), + "Music Box": lambda state: state.has_all(set(NOTES), self.player), + } + + self.location_rules = { + "Claustro": self.has_wingsuit, + "Key of Strength": lambda state: self.has_vertical(state) or state.has("Power Thistle", self.player), + "Key of Love": lambda state: state.has_all({"Sun Crest", "Moon Crest"}, self.player), + "Pyro": self.has_tabi, + "Key of Chaos": self.has_tabi, + "Key of Courage": lambda state: state.has_all({"Demon King Crown", "Fairy Bottle"}, self.player), + "Autumn Hills Seal - Spike Ball Darts": self.has_dart, + "Ninja Village Seal - Tree House": self.has_dart, + "Underworld Seal - Fireball Wave": lambda state: state.has_any({"Wingsuit", "Windmill Shuriken"}, + self.player), + "Tower of Time Seal - Time Waster Seal": self.has_dart, + "Shop Chest": self.has_enough_seals, + } + + def set_messenger_rules(self) -> None: + super().set_messenger_rules() + self.world.multiworld.completion_condition[self.player] = lambda state: True + self.world.multiworld.accessibility[self.player].value = MessengerAccessibility.option_minimal + + +def set_self_locking_items(multiworld: MultiWorld, player: int) -> None: + # do the ones for seal shuffle on and off first + allow_self_locking_items(multiworld.get_location("Key of Strength", player), "Power Thistle") + allow_self_locking_items(multiworld.get_location("Key of Love", player), "Sun Crest", "Moon Crest") + allow_self_locking_items(multiworld.get_location("Key of Courage", player), "Demon King Crown") + + # add these locations when seals aren't shuffled + if not multiworld.shuffle_seals[player]: + allow_self_locking_items(multiworld.get_region("Cloud Ruins", player), "Ruxxtin's Amulet") + allow_self_locking_items(multiworld.get_region("Forlorn Temple", player), *PHOBEKINS) diff --git a/worlds/messenger/SubClasses.py b/worlds/messenger/SubClasses.py new file mode 100644 index 0000000000..1f26a4265b --- /dev/null +++ b/worlds/messenger/SubClasses.py @@ -0,0 +1,58 @@ +from typing import Set, TYPE_CHECKING, Optional, Dict + +from BaseClasses import Region, Location, Item, ItemClassification, Entrance +from .Constants import SEALS, NOTES, PROG_ITEMS, PHOBEKINS, USEFUL_ITEMS +from .Options import Goal +from .Regions import REGIONS + +if TYPE_CHECKING: + from . import MessengerWorld +else: + MessengerWorld = object + + +class MessengerRegion(Region): + def __init__(self, name: str, world: MessengerWorld) -> None: + super().__init__(name, world.player, world.multiworld) + self.add_locations(self.multiworld.worlds[self.player].location_name_to_id) + world.multiworld.regions.append(self) + + def add_locations(self, name_to_id: Dict[str, int]) -> None: + for loc in REGIONS[self.name]: + self.locations.append(MessengerLocation(loc, self, name_to_id.get(loc, None))) + if self.name == "The Shop" and self.multiworld.goal[self.player] > Goal.option_open_music_box: + self.locations.append(MessengerLocation("Shop Chest", self, name_to_id.get("Shop Chest", None))) + # putting some dumb special case for searing crags and ToT so i can split them into 2 regions + if self.multiworld.shuffle_seals[self.player] and self.name not in {"Searing Crags", "Tower HQ"}: + for seal_loc in SEALS: + if seal_loc.startswith(self.name.split(" ")[0]): + self.locations.append(MessengerLocation(seal_loc, self, name_to_id.get(seal_loc, None))) + + def add_exits(self, exits: Set[str]) -> None: + for exit in exits: + ret = Entrance(self.player, f"{self.name} -> {exit}", self) + self.exits.append(ret) + ret.connect(self.multiworld.get_region(exit, self.player)) + + +class MessengerLocation(Location): + game = "The Messenger" + + def __init__(self, name: str, parent: MessengerRegion, loc_id: Optional[int]) -> None: + super().__init__(parent.player, name, loc_id, parent) + if loc_id is None: + self.place_locked_item(MessengerItem(name, parent.player, None)) + + +class MessengerItem(Item): + game = "The Messenger" + + def __init__(self, name: str, player: int, item_id: Optional[int] = None, override_progression: bool = False) -> None: + if name in {*NOTES, *PROG_ITEMS, *PHOBEKINS} or item_id is None or override_progression: + item_class = ItemClassification.progression + elif name in USEFUL_ITEMS: + item_class = ItemClassification.useful + else: + item_class = ItemClassification.filler + super().__init__(name, item_class, item_id, player) + diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py new file mode 100644 index 0000000000..d23f4da34f --- /dev/null +++ b/worlds/messenger/__init__.py @@ -0,0 +1,136 @@ +from typing import Dict, Any, List, Optional + +from BaseClasses import Tutorial, ItemClassification +from worlds.AutoWorld import World, WebWorld +from .Constants import NOTES, PROG_ITEMS, PHOBEKINS, USEFUL_ITEMS, ALWAYS_LOCATIONS, SEALS, ALL_ITEMS +from .Options import messenger_options, NotesNeeded, Goal, PowerSeals, Logic +from .Regions import REGIONS, REGION_CONNECTIONS +from .SubClasses import MessengerRegion, MessengerItem +from . import Rules + + +class MessengerWeb(WebWorld): + theme = "ocean" + + bug_report_page = "https://github.com/alwaysintreble/TheMessengerRandomizerModAP/issues" + + tut_en = Tutorial( + "Multiworld Setup Tutorial", + "A guide to setting up The Messenger randomizer on your computer.", + "English", + "setup_en.md", + "setup/en", + ["alwaysintreble"], + ) + + tutorials = [tut_en] + + +class MessengerWorld(World): + """ + As a demon army besieges his village, a young ninja ventures through a cursed world, to deliver a scroll paramount + to his clan’s survival. What begins as a classic action platformer soon unravels into an expansive time-traveling + adventure full of thrills, surprises, and humor. + """ + game = "The Messenger" + + item_name_groups = { + "Notes": set(NOTES), + "Keys": set(NOTES), + "Crest": {"Sun Crest", "Moon Crest"}, + "Phobe": set(PHOBEKINS), + "Phobekin": set(PHOBEKINS), + "Shuriken": {"Windmill Shuriken"}, + } + + option_definitions = messenger_options + + base_offset = 0xADD_000 + item_name_to_id = {item: item_id + for item_id, item in enumerate(ALL_ITEMS, base_offset)} + location_name_to_id = {location: location_id + for location_id, location in enumerate([*ALWAYS_LOCATIONS, *SEALS], base_offset)} + + data_version = 1 + + web = MessengerWeb() + + total_seals: Optional[int] = None + required_seals: Optional[int] = None + + def generate_early(self) -> None: + if self.multiworld.goal[self.player] == Goal.option_power_seal_hunt: + self.multiworld.shuffle_seals[self.player].value = PowerSeals.option_true + self.total_seals = self.multiworld.total_seals[self.player].value + self.required_seals = int(self.multiworld.percent_seals_required[self.player].value / 100 * self.total_seals) + + def create_regions(self) -> None: + for region in [MessengerRegion(reg_name, self) for reg_name in REGIONS]: + if region.name in REGION_CONNECTIONS: + region.add_exits(REGION_CONNECTIONS[region.name]) + + def create_items(self) -> None: + itempool: List[MessengerItem] = [] + if self.multiworld.goal[self.player] == Goal.option_power_seal_hunt: + seals = [self.create_item("Power Seal") for _ in range(self.total_seals)] + for i in range(self.required_seals): + seals[i].classification = ItemClassification.progression_skip_balancing + itempool += seals + else: + notes = self.multiworld.random.sample(NOTES, k=len(NOTES)) + precollected_notes_amount = NotesNeeded.range_end - self.multiworld.notes_needed[self.player] + if precollected_notes_amount: + for note in notes[:precollected_notes_amount]: + self.multiworld.push_precollected(self.create_item(note)) + itempool += [self.create_item(note) for note in notes[precollected_notes_amount:]] + + itempool += [self.create_item(item) + for item in self.item_name_to_id + if item not in + { + "Power Seal", "Time Shard", *NOTES, + *{collected_item.name for collected_item in self.multiworld.precollected_items[self.player]}, + # this is a set and currently won't create items for anything that appears in here at all + # if we get in a position where this can have duplicates of items that aren't Power Seals + # or Time shards, this will need to be redone. + }] + itempool += [self.create_filler() + for _ in range(len(self.multiworld.get_unfilled_locations(self.player)) - len(itempool))] + + self.multiworld.itempool += itempool + + def set_rules(self) -> None: + logic = self.multiworld.logic_level[self.player] + if logic == Logic.option_normal: + Rules.MessengerRules(self).set_messenger_rules() + elif logic == Logic.option_hard: + Rules.MessengerHardRules(self).set_messenger_rules() + elif logic == Logic.option_challenging: + Rules.MessengerChallengeRules(self).set_messenger_rules() + else: + Rules.MessengerOOBRules(self).set_messenger_rules() + + def fill_slot_data(self) -> Dict[str, Any]: + locations: Dict[int, List[str]] = {} + for loc in self.multiworld.get_filled_locations(self.player): + if loc.item.code: + locations[loc.address] = [loc.item.name, self.multiworld.player_name[loc.item.player]] + + return { + "deathlink": self.multiworld.death_link[self.player].value, + "goal": self.multiworld.goal[self.player].current_key, + "music_box": self.multiworld.music_box[self.player].value, + "required_seals": self.required_seals, + "locations": locations, + "settings": {"Difficulty": "Basic" if not self.multiworld.shuffle_seals[self.player] else "Advanced"}, + "logic": self.multiworld.logic_level[self.player].current_key, + } + + def get_filler_item_name(self) -> str: + return "Time Shard" + + def create_item(self, name: str) -> MessengerItem: + item_id: Optional[int] = self.item_name_to_id.get(name, None) + override_prog = name in {"Windmill Shuriken"} and getattr(self, "multiworld") is not None \ + and self.multiworld.logic_level[self.player] > Logic.option_normal + return MessengerItem(name, self.player, item_id, override_prog) diff --git a/worlds/messenger/docs/en_The Messenger.md b/worlds/messenger/docs/en_The Messenger.md new file mode 100644 index 0000000000..e25be4b907 --- /dev/null +++ b/worlds/messenger/docs/en_The Messenger.md @@ -0,0 +1,77 @@ +# The Messenger + +## Quick Links +- [Setup](../../../../tutorial/The%20Messenger/setup/en) +- [Settings Page](../../../../games/The%20Messenger/player-settings) +- [Courier Github](https://github.com/Brokemia/Courier) +- [The Messenger Randomizer Github](https://github.com/minous27/TheMessengerRandomizerMod) +- [The Messenger Randomizer AP Github](https://github.com/alwaysintreble/TheMessengerRandomizerModAP) +- [Jacksonbird8237's Item Tracker](https://github.com/Jacksonbird8237/TheMessengerItemTracker) +- [PopTracker Pack](https://github.com/alwaysintreble/TheMessengerTrackPack) + +## What does randomization do in this game? + +All items and upgrades that can be picked up by the player in the game are randomized. The player starts in the Tower of +Time HQ with the past section finished, all area portals open, and with the cloud step, and climbing claws already +obtained. You'll be forced to do sections of the game in different ways with your current abilities. Currently, logic +assumes you already have all shop upgrades. + +## What items can appear in other players' worlds? + +* The player's movement items +* Quest and pedestal items +* Music Box notes +* The Phobekins +* Time shards +* Power Seals + +## Where can I find items? + +You can find items wherever items can be picked up in the original game. This includes: +* Shopkeeper dialog where the player originally gains movement items +* Quest Item pickups +* Music Box notes +* Phobekins +* Power seals + +## What are the item name groups? + +When you attempt to hint for items in Archipelago you can use either the name for the specific item, or the name of a +group of items. Hinting for a group will choose a random item from the group that you do not currently have and hint +for it. The groups you can use for The Messenger are: +* Notes - This covers the music notes +* Keys - An alternative name for the music notes +* Crest - The Sun and Moon Crests +* Phobekin - Any of the Phobekins +* Phobe - An alternative name for the Phobekins +* Shuriken - The windmill shuriken + +## Other changes + +* The player can return to the Tower of Time HQ at any point by selecting the button from the options menu + * This can cause issues if used at specific times. Current known: + * During Boss fights + * After Courage Note collection (Corrupted Future chase) + * This is currently an expected action in logic. If you do need to teleport during this chase sequence, it + is recommended to quit to title and reload the save +* After reaching ninja village a teleport option is added to the menu to reach it quickly +* Toggle Windmill Shuriken button is added to option menu once the item is received +* The mod option menu will also have a hint item button, as well as a release and collect button that are all placed when + the player fulfills the necessary conditions. + +## Currently known issues +* Necro cutscene will sometimes not play correctly, but will still reward the item +* Ruxxtin Coffin cutscene will sometimes not play correctly, but will still reward the item +* If you receive the Fairy Bottle while in Quillshroom Marsh, The De-curse Queen cutscene will not play. You can exit + to Searing Crags and re-enter to get it to play correctly. +* If you defeat Barma'thazÃĢl, the cutscene afterward will not play correctly since that is what normally transitions + you to 2nd quest. The game will not kill you if you fall here, so you can teleport to HQ at any point after defeating him. +* Sometimes upon teleporting back to HQ, Ninja will run left and enter a different portal than the one entered by the + player. This may also cause a softlock. +* Text entry menus don't accept controller input +* Opening the shop chest in power seal hunt mode from the tower of time HQ will softlock the game. + +## What do I do if I have a problem? + +If you believe something happened that isn't intended, please get the `log.txt`from the folder of your game installation +and send a bug report either on GitHub or the [Archipelago Discord Server](http://archipelago.gg/discord) diff --git a/worlds/messenger/docs/setup_en.md b/worlds/messenger/docs/setup_en.md new file mode 100644 index 0000000000..0a57c2b83f --- /dev/null +++ b/worlds/messenger/docs/setup_en.md @@ -0,0 +1,65 @@ +# The Messenger Randomizer Setup Guide + +## Quick Links +- [Game Info](../../../../games/The%20Messenger/info/en) +- [Settings Page](../../../../games/The%20Messenger/player-settings) +- [Courier Github](https://github.com/Brokemia/Courier) +- [The Messenger Randomizer AP Github](https://github.com/alwaysintreble/TheMessengerRandomizerModAP) +- [Jacksonbird8237's Item Tracker](https://github.com/Jacksonbird8237/TheMessengerItemTracker) +- [PopTracker Pack](https://github.com/alwaysintreble/TheMessengerTrackPack) + +## Installation + +1. Read the [Game Info Page](../../../../games/The%20Messenger/info/en) for how the game works, caveats and known issues +2. Download and install Courier Mod Loader using the instructions on the release page + * [Latest release is currently 0.7.1](https://github.com/Brokemia/Courier/releases) +3. Download and install the randomizer mod + 1. Download the latest TheMessengerRandomizerAP.zip from + [The Messenger Randomizer Mod AP releases page](https://github.com/alwaysintreble/TheMessengerRandomizerModAP/releases) + 2. Extract the zip file to `TheMessenger/Mods/` of your game's install location + * You cannot have both the non-AP randomizer and the AP randomizer installed at the same time. The AP randomizer + is backwards compatible, so the non-AP mod can be safely removed, and you can still play seeds generated from the + non-AP randomizer. + 3. Optionally, Backup your save game + * On Windows + 1. Press `Windows Key + R` to open run + 2. Type `%appdata%` to access AppData + 3. Navigate to `AppData/locallow/SabotageStudios/The Messenger` + 4. Rename `SaveGame.txt` to any name of your choice + * On Linux + 1. Navigate to `steamapps/compatdata/764790/pfx/drive_c/users/steamuser/AppData/LocalLow/Sabotage Studio/The Messenger` + 2. Rename `SaveGame.txt` to any name of your choice + +## Joining a MultiWorld Game + +1. Launch the game +2. Navigate to `Options > Third Party Mod Options` +3. Select `Reset Randomizer File Slots` + * This will set up all of your save slots with new randomizer save files. You can have up to 3 randomizer files at a + time, but must do this step again to start new runs afterward. +4. Enter connection info using the relevant option buttons + * **The game is limited to alphanumerical characters and `-` so when entering the host name replace `.` with ` `. + Ensure that your player name when generating a settings file follows these constrictions** + * This defaults to `archipelago.gg` and does not need to be manually changed if connecting to a game hosted on the + website. +5. Select the `Connect to Archipelago` button +6. Navigate to save file selection +7. Select a new valid randomizer save + +## Continuing a MultiWorld Game + +At any point while playing, it is completely safe to quit. Returning to the title screen or closing the game will +disconnect you from the server. To reconnect to an in progress MultiWorld, simply load the correct save file for that +MultiWorld. + +If the reconnection fails, the message on screen will state you are disconnected. If this happens, you can return to the +main menu and connect to the server as in [Joining a Multiworld Game](#joining-a-multiworld-game), then load the correct +save file. + +## Troubleshooting + +If you launch the game, and it hangs on the splash screen for more than 30 seconds try these steps: +1. Close the game and remove `TheMessengerRandomizerAP` from the `Mods` folder. +2. Launch The Messenger +3. Delete any save slot +4. Reinstall the randomizer mod following step 2 of the installation. \ No newline at end of file diff --git a/worlds/messenger/test/TestAccess.py b/worlds/messenger/test/TestAccess.py new file mode 100644 index 0000000000..84b29406c2 --- /dev/null +++ b/worlds/messenger/test/TestAccess.py @@ -0,0 +1,137 @@ +from . import MessengerTestBase +from ..Constants import NOTES, PHOBEKINS + + +class AccessTest(MessengerTestBase): + + def testTabi(self) -> None: + """locations that hard require the Ninja Tabi""" + locations = ["Pyro", "Key of Chaos", "Underworld Seal - Sharp and Windy Climb", "Underworld Seal - Spike Wall", + "Underworld Seal - Fireball Wave", "Underworld Seal - Rising Fanta", "Sun Crest", "Moon Crest", + "Sunken Shrine Seal - Waterfall Paradise", "Sunken Shrine Seal - Tabi Gauntlet"] + items = [["Ninja Tabi"]] + self.assertAccessDependency(locations, items) + + def testDart(self) -> None: + """locations that hard require the Rope Dart""" + locations = ["Ninja Village Seal - Tree House", "Key of Hope", "Howling Grotto Seal - Crushing Pits", + "Glacial Peak Seal - Ice Climbers", "Tower of Time Seal - Time Waster Seal", + "Tower of Time Seal - Arcane Orbs", "Underworld Seal - Rising Fanta", "Key of Symbiosis", + "Elemental Skylands Seal - Water", "Elemental Skylands Seal - Fire"] + items = [["Rope Dart"]] + self.assertAccessDependency(locations, items) + + def testWingsuit(self) -> None: + """locations that hard require the Wingsuit""" + locations = ["Candle", "Ninja Village Seal - Tree House", "Climbing Claws", "Key of Hope", + "Autumn Hills Seal - Trip Saws", "Autumn Hills Seal - Double Swing Saws", + "Autumn Hills Seal - Spike Ball Swing", "Autumn Hills Seal - Spike Ball Darts", "Necro", + "Ruxxtin's Amulet", "Catacombs Seal - Triple Spike Crushers", "Catacombs Seal - Crusher Gauntlet", + "Catacombs Seal - Dirty Pond", "Claustro", "Acro", "Bamboo Creek Seal - Spike Crushers and Doors", + "Bamboo Creek Seal - Spike Ball Pits", "Bamboo Creek Seal - Spike Crushers and Doors v2", + "Howling Grotto Seal - Crushing Pits", "Howling Grotto Seal - Windy Saws and Balls", + "Tower of Time Seal - Lantern Climb", "Demon King Crown", "Cloud Ruins Seal - Ghost Pit", + "Cloud Ruins Seal - Toothbrush Alley", "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room", + "Tower of Time Seal - Lantern Climb", "Tower of Time Seal - Arcane Orbs", + "Underworld Seal - Sharp and Windy Climb", "Underworld Seal - Fireball Wave", + "Elemental Skylands Seal - Air", "Forlorn Temple Seal - Rocket Maze", + "Forlorn Temple Seal - Rocket Sunset", "Astral Seed", "Astral Tea Leaves"] + items = [["Wingsuit"]] + self.assertAccessDependency(locations, items) + + def testVertical(self) -> None: + """locations that require either the Rope Dart or the Wingsuit""" + locations = ["Ninja Village Seal - Tree House", "Key of Hope", "Howling Grotto Seal - Crushing Pits", + "Glacial Peak Seal - Ice Climbers", "Tower of Time Seal - Time Waster Seal", + "Underworld Seal - Rising Fanta", "Key of Symbiosis", + "Elemental Skylands Seal - Water", "Elemental Skylands Seal - Fire", "Candle", + "Climbing Claws", "Key of Hope", "Autumn Hills Seal - Trip Saws", + "Autumn Hills Seal - Double Swing Saws", "Autumn Hills Seal - Spike Ball Swing", + "Autumn Hills Seal - Spike Ball Darts", "Necro", "Ruxxtin's Amulet", + "Catacombs Seal - Triple Spike Crushers", "Catacombs Seal - Crusher Gauntlet", + "Catacombs Seal - Dirty Pond", "Claustro", "Acro", "Bamboo Creek Seal - Spike Crushers and Doors", + "Bamboo Creek Seal - Spike Ball Pits", "Bamboo Creek Seal - Spike Crushers and Doors v2", + "Howling Grotto Seal - Crushing Pits", "Howling Grotto Seal - Windy Saws and Balls", + "Demon King Crown", "Cloud Ruins Seal - Ghost Pit", "Cloud Ruins Seal - Toothbrush Alley", + "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room", + "Tower of Time Seal - Lantern Climb", "Tower of Time Seal - Arcane Orbs", + "Underworld Seal - Sharp and Windy Climb", "Underworld Seal - Fireball Wave", + "Elemental Skylands Seal - Air", "Forlorn Temple Seal - Rocket Maze", "Forlorn Temple Seal - Rocket Sunset", + "Power Thistle", "Key of Strength", "Glacial Peak Seal - Projectile Spike Pit", + "Glacial Peak Seal - Glacial Air Swag", "Fairy Bottle", "Riviere Turquoise Seal - Flower Power", + "Searing Crags Seal - Triple Ball Spinner", "Searing Crags Seal - Raining Rocks", + "Searing Crags Seal - Rhythm Rocks", "Astral Seed", "Astral Tea Leaves", "Rescue Phantom"] + items = [["Wingsuit", "Rope Dart"]] + self.assertAccessDependency(locations, items) + + def testAmulet(self) -> None: + """Locations that require Ruxxtin's Amulet""" + locations = ["Acro", "Cloud Ruins Seal - Ghost Pit", "Cloud Ruins Seal - Toothbrush Alley", + "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room"] + # Cloud Ruins requires Ruxxtin's Amulet + items = [["Ruxxtin's Amulet"]] + self.assertAccessDependency(locations, items) + + def testBottle(self) -> None: + """Elemental Skylands and Corrupted Future require the Fairy Bottle""" + locations = ["Key of Symbiosis", "Elemental Skylands Seal - Air", "Elemental Skylands Seal - Fire", + "Elemental Skylands Seal - Water", "Key of Courage"] + items = [["Fairy Bottle"]] + self.assertAccessDependency(locations, items) + + def testCrests(self) -> None: + """Test Key of Love nonsense""" + locations = ["Key of Love"] + items = [["Sun Crest", "Moon Crest"]] + self.assertAccessDependency(locations, items) + self.collect_all_but("Sun Crest") + self.assertEqual(self.can_reach_location("Key of Love"), False) + self.remove(self.get_item_by_name("Moon Crest")) + self.collect_by_name("Sun Crest") + self.assertEqual(self.can_reach_location("Key of Love"), False) + + def testThistle(self) -> None: + """I'm a chuckster!""" + locations = ["Key of Strength"] + items = [["Power Thistle"]] + self.assertAccessDependency(locations, items) + + def testCrown(self) -> None: + """Crocomire but not""" + locations = ["Key of Courage"] + items = [["Demon King Crown"]] + self.assertAccessDependency(locations, items) + + def testGoal(self) -> None: + """Test some different states to verify goal requires the correct items""" + self.collect_all_but([*NOTES, "Rescue Phantom"]) + self.assertEqual(self.can_reach_location("Rescue Phantom"), False) + self.collect_all_but(["Key of Love", "Rescue Phantom"]) + self.assertBeatable(False) + self.collect_by_name(["Key of Love"]) + self.assertEqual(self.can_reach_location("Rescue Phantom"), True) + self.assertBeatable(True) + + +class ItemsAccessTest(MessengerTestBase): + options = { + "shuffle_seals": "false", + "accessibility": "items", + } + + def testSelfLockingItems(self) -> None: + """Force items that can be self locked to ensure it's valid placement.""" + location_lock_pairs = { + "Key of Strength": ["Power Thistle"], + "Key of Love": ["Sun Crest", "Moon Crest"], + "Key of Courage": ["Demon King Crown"], + "Acro": ["Ruxxtin's Amulet"], + "Demon King Crown": PHOBEKINS + } + + for loc in location_lock_pairs: + for item_name in location_lock_pairs[loc]: + item = self.get_item_by_name(item_name) + with self.subTest("Fulfills Accessibility", location=loc, item=item_name): + self.assertTrue(self.multiworld.get_location(loc, self.player).can_fill(self.multiworld.state, item, True)) + diff --git a/worlds/messenger/test/TestLogic.py b/worlds/messenger/test/TestLogic.py new file mode 100644 index 0000000000..2fd8111030 --- /dev/null +++ b/worlds/messenger/test/TestLogic.py @@ -0,0 +1,108 @@ +from BaseClasses import ItemClassification +from . import MessengerTestBase + + +class HardLogicTest(MessengerTestBase): + options = { + "logic_level": "hard", + } + + def testVertical(self) -> None: + """Test the locations that still require wingsuit or rope dart.""" + locations = [ + # tower of time + "Tower of Time Seal - Time Waster Seal", "Tower of Time Seal - Lantern Climb", + "Tower of Time Seal - Arcane Orbs", + # ninja village + "Candle", "Astral Seed", "Ninja Village Seal - Tree House", "Astral Tea Leaves", + # autumn hills + "Climbing Claws", "Key of Hope", + "Autumn Hills Seal - Trip Saws", "Autumn Hills Seal - Double Swing Saws", + "Autumn Hills Seal - Spike Ball Swing", "Autumn Hills Seal - Spike Ball Darts", + # forlorn temple + "Demon King Crown", + "Forlorn Temple Seal - Rocket Maze", "Forlorn Temple Seal - Rocket Sunset", + # catacombs + "Necro", "Ruxxtin's Amulet", + "Catacombs Seal - Triple Spike Crushers", "Catacombs Seal - Crusher Gauntlet", "Catacombs Seal - Dirty Pond", + # bamboo creek + "Claustro", + "Bamboo Creek Seal - Spike Crushers and Doors", "Bamboo Creek Seal - Spike Ball Pits", + "Bamboo Creek Seal - Spike Crushers and Doors v2", + # howling grotto + "Howling Grotto Seal - Crushing Pits", "Howling Grotto Seal - Crushing Pits", + # glacial peak + "Glacial Peak Seal - Ice Climbers", + # cloud ruins + "Acro", "Cloud Ruins Seal - Ghost Pit", + "Cloud Ruins Seal - Toothbrush Alley", "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room", + # underworld + "Underworld Seal - Rising Fanta", "Underworld Seal - Sharp and Windy Climb", + # riviere turquoise + "Fairy Bottle", "Riviere Turquoise Seal - Flower Power", + # elemental skylands + "Elemental Skylands Seal - Air", "Elemental Skylands Seal - Water", "Elemental Skylands Seal - Fire", + # phantom + "Rescue Phantom", + ] + items = [["Wingsuit", "Rope Dart"]] + self.assertAccessDependency(locations, items) + + def testWindmill(self) -> None: + """Windmill Shuriken isn't progression on normal difficulty, so test it's marked correctly and required.""" + self.assertEqual(ItemClassification.progression, self.get_item_by_name("Windmill Shuriken").classification) + windmill_locs = [ + "Key of Strength", + "Key of Symbiosis", + "Underworld Seal - Fireball Wave", + ] + for loc in windmill_locs: + with self.subTest("can't reach location with nothing", location=loc): + self.assertFalse(self.can_reach_location(loc)) + + items = self.get_items_by_name(["Windmill Shuriken", "Ninja Tabi", "Fairy Bottle"]) + self.collect(items) + for loc in windmill_locs: + with self.subTest("can reach with Windmill", location=loc): + self.assertTrue(self.can_reach_location(loc)) + + special_loc = "Autumn Hills Seal - Spike Ball Darts" + item = self.get_item_by_name("Wingsuit") + self.collect(item) + self.assertTrue(self.can_reach_location(special_loc)) + self.remove(item) + + item = self.get_item_by_name("Rope Dart") + self.collect(item) + self.assertTrue(self.can_reach_location(special_loc)) + + +class ChallengingLogicTest(MessengerTestBase): + options = { + "shuffle_seals": "false", + "logic_level": "challenging", + } + + +class NoLogicTest(MessengerTestBase): + options = { + "logic_level": "oob", + } + + def testAccess(self) -> None: + """Test the locations with rules still require things.""" + all_locations = [ + "Claustro", "Key of Strength", "Key of Symbiosis", "Key of Love", "Pyro", "Key of Chaos", "Key of Courage", + "Autumn Hills Seal - Spike Ball Darts", "Ninja Village Seal - Tree House", "Underworld Seal - Fireball Wave", + "Tower of Time Seal - Time Waster Seal", "Rescue Phantom", "Elemental Skylands Seal - Air", + "Elemental Skylands Seal - Water", "Elemental Skylands Seal - Fire", + ] + for loc in all_locations: + with self.subTest("Default unreachables", location=loc): + self.assertFalse(self.can_reach_location(loc)) + + def testNoLogic(self) -> None: + """Test some funny locations to make sure they aren't reachable, but we can still win""" + self.assertEqual(self.can_reach_location("Pyro"), False) + self.assertEqual(self.can_reach_location("Rescue Phantom"), False) + self.assertBeatable(True) diff --git a/worlds/messenger/test/TestNotes.py b/worlds/messenger/test/TestNotes.py new file mode 100644 index 0000000000..c4292e4900 --- /dev/null +++ b/worlds/messenger/test/TestNotes.py @@ -0,0 +1,35 @@ +from . import MessengerTestBase +from ..Constants import NOTES + + +class TwoNoteGoalTest(MessengerTestBase): + options = { + "notes_needed": 2, + } + + def testPrecollectedNotes(self) -> None: + self.assertEqual(self.multiworld.state.count_group("Notes", self.player), 4) + + +class FourNoteGoalTest(MessengerTestBase): + options = { + "notes_needed": 4, + } + + def testPrecollectedNotes(self) -> None: + self.assertEqual(self.multiworld.state.count_group("Notes", self.player), 2) + + +class DefaultGoalTest(MessengerTestBase): + def testPrecollectedNotes(self) -> None: + self.assertEqual(self.multiworld.state.count_group("Notes", self.player), 0) + + def testGoal(self) -> None: + self.assertBeatable(False) + self.collect_by_name(NOTES) + rope_dart = self.get_item_by_name("Rope Dart") + self.collect(rope_dart) + self.assertBeatable(True) + self.remove(rope_dart) + self.collect_by_name("Wingsuit") + self.assertBeatable(True) diff --git a/worlds/messenger/test/TestShopChest.py b/worlds/messenger/test/TestShopChest.py new file mode 100644 index 0000000000..9289ec9970 --- /dev/null +++ b/worlds/messenger/test/TestShopChest.py @@ -0,0 +1,79 @@ +from BaseClasses import ItemClassification, CollectionState +from . import MessengerTestBase + + +class NoLogicTest(MessengerTestBase): + options = { + "logic_level": "oob", + "goal": "power_seal_hunt", + } + + def testChestAccess(self) -> None: + """Test to make sure we can win even though we can't reach the chest.""" + self.assertEqual(self.can_reach_location("Shop Chest"), False) + self.assertBeatable(True) + + +class AllSealsRequired(MessengerTestBase): + options = { + "shuffle_seals": "false", + "goal": "power_seal_hunt", + } + + def testSealsShuffled(self) -> None: + """Shuffle seals should be forced on when shop chest is the goal so test it.""" + self.assertTrue(self.multiworld.shuffle_seals[self.player]) + + def testChestAccess(self) -> None: + """Defaults to a total of 45 power seals in the pool and required.""" + with self.subTest("Access Dependency"): + self.assertEqual(len([seal for seal in self.multiworld.itempool if seal.name == "Power Seal"]), + self.multiworld.total_seals[self.player]) + locations = ["Shop Chest"] + items = [["Power Seal"]] + self.assertAccessDependency(locations, items) + self.multiworld.state = CollectionState(self.multiworld) + + self.assertEqual(self.can_reach_location("Shop Chest"), False) + self.assertBeatable(False) + self.collect_all_but(["Power Seal", "Shop Chest", "Rescue Phantom"]) + self.assertEqual(self.can_reach_location("Shop Chest"), False) + self.assertBeatable(False) + self.collect_by_name("Power Seal") + self.assertEqual(self.can_reach_location("Shop Chest"), True) + self.assertBeatable(True) + + +class HalfSealsRequired(MessengerTestBase): + options = { + "goal": "power_seal_hunt", + "percent_seals_required": 50, + } + + def testSealsAmount(self) -> None: + """Should have 45 power seals in the item pool and half that required""" + self.assertEqual(self.multiworld.total_seals[self.player], 45) + self.assertEqual(self.multiworld.worlds[self.player].total_seals, 45) + self.assertEqual(self.multiworld.worlds[self.player].required_seals, 22) + total_seals = [seal for seal in self.multiworld.itempool if seal.name == "Power Seal"] + required_seals = [seal for seal in total_seals if seal.classification == ItemClassification.progression_skip_balancing] + self.assertEqual(len(total_seals), 45) + self.assertEqual(len(required_seals), 22) + + +class ThirtyThirtySeals(MessengerTestBase): + options = { + "goal": "power_seal_hunt", + "total_seals": 30, + "percent_seals_required": 34, + } + + def testSealsAmount(self) -> None: + """Should have 30 power seals in the pool and 33 percent of that required.""" + self.assertEqual(self.multiworld.total_seals[self.player], 30) + self.assertEqual(self.multiworld.worlds[self.player].total_seals, 30) + self.assertEqual(self.multiworld.worlds[self.player].required_seals, 10) + total_seals = [seal for seal in self.multiworld.itempool if seal.name == "Power Seal"] + required_seals = [seal for seal in total_seals if seal.classification == ItemClassification.progression_skip_balancing] + self.assertEqual(len(total_seals), 30) + self.assertEqual(len(required_seals), 10) diff --git a/worlds/messenger/test/__init__.py b/worlds/messenger/test/__init__.py new file mode 100644 index 0000000000..7ab1e11781 --- /dev/null +++ b/worlds/messenger/test/__init__.py @@ -0,0 +1,6 @@ +from test.TestBase import WorldTestBase + + +class MessengerTestBase(WorldTestBase): + game = "The Messenger" + player: int = 1 diff --git a/worlds/minecraft/Constants.py b/worlds/minecraft/Constants.py new file mode 100644 index 0000000000..0d1101e802 --- /dev/null +++ b/worlds/minecraft/Constants.py @@ -0,0 +1,26 @@ +import os +import json +import pkgutil + +def load_data_file(*args) -> dict: + fname = os.path.join("data", *args) + return json.loads(pkgutil.get_data(__name__, fname).decode()) + +# For historical reasons, these values are different. +# They remain different to ensure datapackage consistency. +# Do not separate other games' location and item IDs like this. +item_id_offset: int = 45000 +location_id_offset: int = 42000 + +item_info = load_data_file("items.json") +item_name_to_id = {name: item_id_offset + index \ + for index, name in enumerate(item_info["all_items"])} +item_name_to_id["Bee Trap"] = item_id_offset + 100 # historical reasons + +location_info = load_data_file("locations.json") +location_name_to_id = {name: location_id_offset + index \ + for index, name in enumerate(location_info["all_locations"])} + +exclusion_info = load_data_file("excluded_locations.json") + +region_info = load_data_file("regions.json") diff --git a/worlds/minecraft/ItemPool.py b/worlds/minecraft/ItemPool.py new file mode 100644 index 0000000000..78eeffca80 --- /dev/null +++ b/worlds/minecraft/ItemPool.py @@ -0,0 +1,52 @@ +from math import ceil +from typing import List + +from BaseClasses import MultiWorld, Item +from worlds.AutoWorld import World + +from . import Constants + +def get_junk_item_names(rand, k: int) -> str: + junk_weights = Constants.item_info["junk_weights"] + junk = rand.choices( + list(junk_weights.keys()), + weights=list(junk_weights.values()), + k=k) + return junk + +def build_item_pool(mc_world: World) -> List[Item]: + multiworld = mc_world.multiworld + player = mc_world.player + + itempool = [] + total_location_count = len(multiworld.get_unfilled_locations(player)) + + required_pool = Constants.item_info["required_pool"] + junk_weights = Constants.item_info["junk_weights"] + + # Add required progression items + for item_name, num in required_pool.items(): + itempool += [mc_world.create_item(item_name) for _ in range(num)] + + # Add structure compasses + if multiworld.structure_compasses[player]: + compasses = [name for name in mc_world.item_name_to_id if "Structure Compass" in name] + for item_name in compasses: + itempool.append(mc_world.create_item(item_name)) + + # Dragon egg shards + if multiworld.egg_shards_required[player] > 0: + num = multiworld.egg_shards_available[player] + itempool += [mc_world.create_item("Dragon Egg Shard") for _ in range(num)] + + # Bee traps + bee_trap_percentage = multiworld.bee_traps[player] * 0.01 + if bee_trap_percentage > 0: + bee_trap_qty = ceil(bee_trap_percentage * (total_location_count - len(itempool))) + itempool += [mc_world.create_item("Bee Trap") for _ in range(bee_trap_qty)] + + # Fill remaining itempool with randomly generated junk + junk = get_junk_item_names(multiworld.random, total_location_count - len(itempool)) + itempool += [mc_world.create_item(name) for name in junk] + + return itempool diff --git a/worlds/minecraft/Items.py b/worlds/minecraft/Items.py deleted file mode 100644 index 6cf8447c8f..0000000000 --- a/worlds/minecraft/Items.py +++ /dev/null @@ -1,108 +0,0 @@ -from BaseClasses import Item -import typing - - -class ItemData(typing.NamedTuple): - code: typing.Optional[int] - progression: bool - - -class MinecraftItem(Item): - game: str = "Minecraft" - - -item_table = { - "Archery": ItemData(45000, True), - "Progressive Resource Crafting": ItemData(45001, True), - # "Resource Blocks": ItemData(45002, True), - "Brewing": ItemData(45003, True), - "Enchanting": ItemData(45004, True), - "Bucket": ItemData(45005, True), - "Flint and Steel": ItemData(45006, True), - "Bed": ItemData(45007, True), - "Bottles": ItemData(45008, True), - "Shield": ItemData(45009, True), - "Fishing Rod": ItemData(45010, True), - "Campfire": ItemData(45011, True), - "Progressive Weapons": ItemData(45012, True), - "Progressive Tools": ItemData(45013, True), - "Progressive Armor": ItemData(45014, True), - "8 Netherite Scrap": ItemData(45015, True), - "8 Emeralds": ItemData(45016, False), - "4 Emeralds": ItemData(45017, False), - "Channeling Book": ItemData(45018, True), - "Silk Touch Book": ItemData(45019, True), - "Sharpness III Book": ItemData(45020, False), - "Piercing IV Book": ItemData(45021, True), - "Looting III Book": ItemData(45022, False), - "Infinity Book": ItemData(45023, False), - "4 Diamond Ore": ItemData(45024, False), - "16 Iron Ore": ItemData(45025, False), - "500 XP": ItemData(45026, False), - "100 XP": ItemData(45027, False), - "50 XP": ItemData(45028, False), - "3 Ender Pearls": ItemData(45029, True), - "4 Lapis Lazuli": ItemData(45030, False), - "16 Porkchops": ItemData(45031, False), - "8 Gold Ore": ItemData(45032, False), - "Rotten Flesh": ItemData(45033, False), - "Single Arrow": ItemData(45034, False), - "32 Arrows": ItemData(45035, False), - "Saddle": ItemData(45036, True), - "Structure Compass (Village)": ItemData(45037, True), - "Structure Compass (Pillager Outpost)": ItemData(45038, True), - "Structure Compass (Nether Fortress)": ItemData(45039, True), - "Structure Compass (Bastion Remnant)": ItemData(45040, True), - "Structure Compass (End City)": ItemData(45041, True), - "Shulker Box": ItemData(45042, False), - "Dragon Egg Shard": ItemData(45043, True), - "Spyglass": ItemData(45044, True), - "Lead": ItemData(45045, True), - - "Bee Trap": ItemData(45100, False), - "Blaze Rods": ItemData(None, True), - "Defeat Ender Dragon": ItemData(None, True), - "Defeat Wither": ItemData(None, True), -} - -# 33 required items -required_items = { - "Archery": 1, - "Progressive Resource Crafting": 2, - "Brewing": 1, - "Enchanting": 1, - "Bucket": 1, - "Flint and Steel": 1, - "Bed": 1, - "Bottles": 1, - "Shield": 1, - "Fishing Rod": 1, - "Campfire": 1, - "Progressive Weapons": 3, - "Progressive Tools": 3, - "Progressive Armor": 2, - "8 Netherite Scrap": 2, - "Channeling Book": 1, - "Silk Touch Book": 1, - "Sharpness III Book": 1, - "Piercing IV Book": 1, - "Looting III Book": 1, - "Infinity Book": 1, - "3 Ender Pearls": 4, - "Saddle": 1, - "Spyglass": 1, - "Lead": 1, -} - -junk_weights = { - "4 Emeralds": 2, - "4 Diamond Ore": 1, - "16 Iron Ore": 1, - "50 XP": 4, - "16 Porkchops": 2, - "8 Gold Ore": 1, - "Rotten Flesh": 1, - "32 Arrows": 1, -} - -lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code} diff --git a/worlds/minecraft/Locations.py b/worlds/minecraft/Locations.py deleted file mode 100644 index 46398ab11e..0000000000 --- a/worlds/minecraft/Locations.py +++ /dev/null @@ -1,192 +0,0 @@ -from BaseClasses import Location -import typing - - -class AdvData(typing.NamedTuple): - id: typing.Optional[int] - region: str - - -class MinecraftAdvancement(Location): - game: str = "Minecraft" - - def __init__(self, player: int, name: str, address: typing.Optional[int], parent): - super().__init__(player, name, address, parent) - self.event = not address - - -advancement_table = { - "Who is Cutting Onions?": AdvData(42000, 'Overworld'), - "Oh Shiny": AdvData(42001, 'Overworld'), - "Suit Up": AdvData(42002, 'Overworld'), - "Very Very Frightening": AdvData(42003, 'Overworld'), - "Hot Stuff": AdvData(42004, 'Overworld'), - "Free the End": AdvData(42005, 'The End'), - "A Furious Cocktail": AdvData(42006, 'Nether Fortress'), - "Best Friends Forever": AdvData(42007, 'Overworld'), - "Bring Home the Beacon": AdvData(42008, 'Nether Fortress'), - "Not Today, Thank You": AdvData(42009, 'Overworld'), - "Isn't It Iron Pick": AdvData(42010, 'Overworld'), - "Local Brewery": AdvData(42011, 'Nether Fortress'), - "The Next Generation": AdvData(42012, 'The End'), - "Fishy Business": AdvData(42013, 'Overworld'), - "Hot Tourist Destinations": AdvData(42014, 'The Nether'), - "This Boat Has Legs": AdvData(42015, 'The Nether'), - "Sniper Duel": AdvData(42016, 'Overworld'), - "Nether": AdvData(42017, 'The Nether'), - "Great View From Up Here": AdvData(42018, 'End City'), - "How Did We Get Here?": AdvData(42019, 'Nether Fortress'), - "Bullseye": AdvData(42020, 'Overworld'), - "Spooky Scary Skeleton": AdvData(42021, 'Nether Fortress'), - "Two by Two": AdvData(42022, 'The Nether'), - "Stone Age": AdvData(42023, 'Overworld'), - "Two Birds, One Arrow": AdvData(42024, 'Overworld'), - "We Need to Go Deeper": AdvData(42025, 'The Nether'), - "Who's the Pillager Now?": AdvData(42026, 'Pillager Outpost'), - "Getting an Upgrade": AdvData(42027, 'Overworld'), - "Tactical Fishing": AdvData(42028, 'Overworld'), - "Zombie Doctor": AdvData(42029, 'Overworld'), - "The City at the End of the Game": AdvData(42030, 'End City'), - "Ice Bucket Challenge": AdvData(42031, 'Overworld'), - "Remote Getaway": AdvData(42032, 'The End'), - "Into Fire": AdvData(42033, 'Nether Fortress'), - "War Pigs": AdvData(42034, 'Bastion Remnant'), - "Take Aim": AdvData(42035, 'Overworld'), - "Total Beelocation": AdvData(42036, 'Overworld'), - "Arbalistic": AdvData(42037, 'Overworld'), - "The End... Again...": AdvData(42038, 'The End'), - "Acquire Hardware": AdvData(42039, 'Overworld'), - "Not Quite \"Nine\" Lives": AdvData(42040, 'The Nether'), - "Cover Me With Diamonds": AdvData(42041, 'Overworld'), - "Sky's the Limit": AdvData(42042, 'End City'), - "Hired Help": AdvData(42043, 'Overworld'), - "Return to Sender": AdvData(42044, 'The Nether'), - "Sweet Dreams": AdvData(42045, 'Overworld'), - "You Need a Mint": AdvData(42046, 'The End'), - "Adventure": AdvData(42047, 'Overworld'), - "Monsters Hunted": AdvData(42048, 'Overworld'), - "Enchanter": AdvData(42049, 'Overworld'), - "Voluntary Exile": AdvData(42050, 'Pillager Outpost'), - "Eye Spy": AdvData(42051, 'Overworld'), - "The End": AdvData(42052, 'The End'), - "Serious Dedication": AdvData(42053, 'The Nether'), - "Postmortal": AdvData(42054, 'Village'), - "Monster Hunter": AdvData(42055, 'Overworld'), - "Adventuring Time": AdvData(42056, 'Overworld'), - "A Seedy Place": AdvData(42057, 'Overworld'), - "Those Were the Days": AdvData(42058, 'Bastion Remnant'), - "Hero of the Village": AdvData(42059, 'Village'), - "Hidden in the Depths": AdvData(42060, 'The Nether'), - "Beaconator": AdvData(42061, 'Nether Fortress'), - "Withering Heights": AdvData(42062, 'Nether Fortress'), - "A Balanced Diet": AdvData(42063, 'Village'), - "Subspace Bubble": AdvData(42064, 'The Nether'), - "Husbandry": AdvData(42065, 'Overworld'), - "Country Lode, Take Me Home": AdvData(42066, 'The Nether'), - "Bee Our Guest": AdvData(42067, 'Overworld'), - "What a Deal!": AdvData(42068, 'Village'), - "Uneasy Alliance": AdvData(42069, 'The Nether'), - "Diamonds!": AdvData(42070, 'Overworld'), - "A Terrible Fortress": AdvData(42071, 'Nether Fortress'), - "A Throwaway Joke": AdvData(42072, 'Overworld'), - "Minecraft": AdvData(42073, 'Overworld'), - "Sticky Situation": AdvData(42074, 'Overworld'), - "Ol' Betsy": AdvData(42075, 'Overworld'), - "Cover Me in Debris": AdvData(42076, 'The Nether'), - "The End?": AdvData(42077, 'The End'), - "The Parrots and the Bats": AdvData(42078, 'Overworld'), - "A Complete Catalogue": AdvData(42079, 'Village'), - "Getting Wood": AdvData(42080, 'Overworld'), - "Time to Mine!": AdvData(42081, 'Overworld'), - "Hot Topic": AdvData(42082, 'Overworld'), - "Bake Bread": AdvData(42083, 'Overworld'), - "The Lie": AdvData(42084, 'Overworld'), - "On a Rail": AdvData(42085, 'Overworld'), - "Time to Strike!": AdvData(42086, 'Overworld'), - "Cow Tipper": AdvData(42087, 'Overworld'), - "When Pigs Fly": AdvData(42088, 'Overworld'), - "Overkill": AdvData(42089, 'Nether Fortress'), - "Librarian": AdvData(42090, 'Overworld'), - "Overpowered": AdvData(42091, 'Bastion Remnant'), - "Wax On": AdvData(42092, 'Overworld'), - "Wax Off": AdvData(42093, 'Overworld'), - "The Cutest Predator": AdvData(42094, 'Overworld'), - "The Healing Power of Friendship": AdvData(42095, 'Overworld'), - "Is It a Bird?": AdvData(42096, 'Overworld'), - "Is It a Balloon?": AdvData(42097, 'The Nether'), - "Is It a Plane?": AdvData(42098, 'The End'), - "Surge Protector": AdvData(42099, 'Overworld'), - "Light as a Rabbit": AdvData(42100, 'Overworld'), - "Glow and Behold!": AdvData(42101, 'Overworld'), - "Whatever Floats Your Goat!": AdvData(42102, 'Overworld'), - "Caves & Cliffs": AdvData(42103, 'Overworld'), - "Feels like home": AdvData(42104, 'The Nether'), - "Sound of Music": AdvData(42105, 'Overworld'), - "Star Trader": AdvData(42106, 'Village'), - - # 1.19 advancements - "Birthday Song": AdvData(42107, 'Pillager Outpost'), - "Bukkit Bukkit": AdvData(42108, 'Overworld'), - "It Spreads": AdvData(42109, 'Overworld'), - "Sneak 100": AdvData(42110, 'Overworld'), - "When the Squad Hops into Town": AdvData(42111, 'Overworld'), - "With Our Powers Combined!": AdvData(42112, 'The Nether'), - "You've Got a Friend in Me": AdvData(42113, 'Pillager Outpost'), - - "Blaze Spawner": AdvData(None, 'Nether Fortress'), - "Ender Dragon": AdvData(None, 'The End'), - "Wither": AdvData(None, 'Nether Fortress'), -} - -exclusion_table = { - "hard": { - "Very Very Frightening", - "A Furious Cocktail", - "Two by Two", - "Two Birds, One Arrow", - "Arbalistic", - "Monsters Hunted", - "Beaconator", - "A Balanced Diet", - "Uneasy Alliance", - "Cover Me in Debris", - "A Complete Catalogue", - "Surge Protector", - "Sound of Music", - "Star Trader", - "When the Squad Hops into Town", - "With Our Powers Combined!", - }, - "unreasonable": { - "How Did We Get Here?", - "Adventuring Time", - }, -} - -def get_postgame_advancements(required_bosses): - - postgame_advancements = { - "ender_dragon": { - "Free the End", - "The Next Generation", - "The End... Again...", - "You Need a Mint", - "Monsters Hunted", - "Is It a Plane?", - }, - "wither": { - "Withering Heights", - "Bring Home the Beacon", - "Beaconator", - "A Furious Cocktail", - "How Did We Get Here?", - "Monsters Hunted", - } - } - - advancements = set() - if required_bosses in {"ender_dragon", "both"}: - advancements.update(postgame_advancements["ender_dragon"]) - if required_bosses in {"wither", "both"}: - advancements.update(postgame_advancements["wither"]) - return advancements diff --git a/worlds/minecraft/Options.py b/worlds/minecraft/Options.py index 161d44d9b8..084a611e44 100644 --- a/worlds/minecraft/Options.py +++ b/worlds/minecraft/Options.py @@ -6,7 +6,7 @@ class AdvancementGoal(Range): """Number of advancements required to spawn bosses.""" display_name = "Advancement Goal" range_start = 0 - range_end = 95 + range_end = 114 default = 40 @@ -14,7 +14,7 @@ class EggShardsRequired(Range): """Number of dragon egg shards to collect to spawn bosses.""" display_name = "Egg Shards Required" range_start = 0 - range_end = 40 + range_end = 74 default = 0 @@ -22,7 +22,7 @@ class EggShardsAvailable(Range): """Number of dragon egg shards available to collect.""" display_name = "Egg Shards Available" range_start = 0 - range_end = 40 + range_end = 74 default = 0 @@ -35,6 +35,14 @@ class BossGoal(Choice): option_both = 3 default = 1 + @property + def dragon(self): + return self.value % 2 == 1 + + @property + def wither(self): + return self.value > 1 + class ShuffleStructures(DefaultOnToggle): """Enables shuffling of villages, outposts, fortresses, bastions, and end cities.""" @@ -94,14 +102,16 @@ minecraft_options: typing.Dict[str, type(Option)] = { "egg_shards_required": EggShardsRequired, "egg_shards_available": EggShardsAvailable, "required_bosses": BossGoal, + "shuffle_structures": ShuffleStructures, "structure_compasses": StructureCompasses, - "bee_traps": BeeTraps, + "combat_difficulty": CombatDifficulty, "include_hard_advancements": HardAdvancements, "include_unreasonable_advancements": UnreasonableAdvancements, "include_postgame_advancements": PostgameAdvancements, + "bee_traps": BeeTraps, "send_defeated_mobs": SendDefeatedMobs, - "starting_items": StartingItems, "death_link": DeathLink, + "starting_items": StartingItems, } diff --git a/worlds/minecraft/Regions.py b/worlds/minecraft/Regions.py deleted file mode 100644 index d9f3f1b59e..0000000000 --- a/worlds/minecraft/Regions.py +++ /dev/null @@ -1,93 +0,0 @@ - -def link_minecraft_structures(world, player): - - # Link mandatory connections first - for (exit, region) in mandatory_connections: - world.get_entrance(exit, player).connect(world.get_region(region, player)) - - # Get all unpaired exits and all regions without entrances (except the Menu) - # This function is destructive on these lists. - exits = [exit.name for r in world.regions if r.player == player for exit in r.exits if exit.connected_region == None] - structs = [r.name for r in world.regions if r.player == player and r.entrances == [] and r.name != 'Menu'] - exits_spoiler = exits[:] # copy the original order for the spoiler log - try: - assert len(exits) == len(structs) - except AssertionError as e: # this should never happen - raise Exception(f"Could not obtain equal numbers of Minecraft exits and structures for player {player} ({world.player_name[player]})") - - pairs = {} - - def set_pair(exit, struct): - if (exit in exits) and (struct in structs) and (exit not in illegal_connections.get(struct, [])): - pairs[exit] = struct - exits.remove(exit) - structs.remove(struct) - else: - raise Exception(f"Invalid connection: {exit} => {struct} for player {player} ({world.player_name[player]})") - - # Connect plando structures first - if world.plando_connections[player]: - for conn in world.plando_connections[player]: - set_pair(conn.entrance, conn.exit) - - # The algorithm tries to place the most restrictive structures first. This algorithm always works on the - # relatively small set of restrictions here, but does not work on all possible inputs with valid configurations. - if world.shuffle_structures[player]: - structs.sort(reverse=True, key=lambda s: len(illegal_connections.get(s, []))) - for struct in structs[:]: - try: - exit = world.random.choice([e for e in exits if e not in illegal_connections.get(struct, [])]) - except IndexError: - raise Exception(f"No valid structure placements remaining for player {player} ({world.player_name[player]})") - set_pair(exit, struct) - else: # write remaining default connections - for (exit, struct) in default_connections: - if exit in exits: - set_pair(exit, struct) - - # Make sure we actually paired everything; might fail if plando - try: - assert len(exits) == len(structs) == 0 - except AssertionError: - raise Exception(f"Failed to connect all Minecraft structures for player {player} ({world.player_name[player]})") - - for exit in exits_spoiler: - world.get_entrance(exit, player).connect(world.get_region(pairs[exit], player)) - if world.shuffle_structures[player] or world.plando_connections[player]: - world.spoiler.set_entrance(exit, pairs[exit], 'entrance', player) - - - -# (Region name, list of exits) -mc_regions = [ - ('Menu', ['New World']), - ('Overworld', ['Nether Portal', 'End Portal', 'Overworld Structure 1', 'Overworld Structure 2']), - ('The Nether', ['Nether Structure 1', 'Nether Structure 2']), - ('The End', ['The End Structure']), - ('Village', []), - ('Pillager Outpost', []), - ('Nether Fortress', []), - ('Bastion Remnant', []), - ('End City', []) -] - -# (Entrance, region pointed to) -mandatory_connections = [ - ('New World', 'Overworld'), - ('Nether Portal', 'The Nether'), - ('End Portal', 'The End') -] - -default_connections = [ - ('Overworld Structure 1', 'Village'), - ('Overworld Structure 2', 'Pillager Outpost'), - ('Nether Structure 1', 'Nether Fortress'), - ('Nether Structure 2', 'Bastion Remnant'), - ('The End Structure', 'End City') -] - -# Structure: illegal locations -illegal_connections = { - 'Nether Fortress': ['The End Structure'] -} - diff --git a/worlds/minecraft/Rules.py b/worlds/minecraft/Rules.py index 2ec9523762..dae4241b99 100644 --- a/worlds/minecraft/Rules.py +++ b/worlds/minecraft/Rules.py @@ -1,317 +1,313 @@ -from ..generic.Rules import set_rule, add_rule -from .Locations import exclusion_table, get_postgame_advancements -from BaseClasses import MultiWorld -from ..AutoWorld import LogicMixin +import typing +from collections.abc import Callable + +from BaseClasses import CollectionState +from worlds.generic.Rules import exclusion_rules +from worlds.AutoWorld import World + +from . import Constants + +# Helper functions +# moved from logicmixin + +def has_iron_ingots(state: CollectionState, player: int) -> bool: + return state.has('Progressive Tools', player) and state.has('Progressive Resource Crafting', player) + +def has_copper_ingots(state: CollectionState, player: int) -> bool: + return state.has('Progressive Tools', player) and state.has('Progressive Resource Crafting', player) + +def has_gold_ingots(state: CollectionState, player: int) -> bool: + return state.has('Progressive Resource Crafting', player) and (state.has('Progressive Tools', player, 2) or state.can_reach('The Nether', 'Region', player)) + +def has_diamond_pickaxe(state: CollectionState, player: int) -> bool: + return state.has('Progressive Tools', player, 3) and has_iron_ingots(state, player) + +def craft_crossbow(state: CollectionState, player: int) -> bool: + return state.has('Archery', player) and has_iron_ingots(state, player) + +def has_bottle(state: CollectionState, player: int) -> bool: + return state.has('Bottles', player) and state.has('Progressive Resource Crafting', player) + +def has_spyglass(state: CollectionState, player: int) -> bool: + return has_copper_ingots(state, player) and state.has('Spyglass', player) and can_adventure(state, player) + +def can_enchant(state: CollectionState, player: int) -> bool: + return state.has('Enchanting', player) and has_diamond_pickaxe(state, player) # mine obsidian and lapis + +def can_use_anvil(state: CollectionState, player: int) -> bool: + return state.has('Enchanting', player) and state.has('Progressive Resource Crafting', player, 2) and has_iron_ingots(state, player) + +def fortress_loot(state: CollectionState, player: int) -> bool: # saddles, blaze rods, wither skulls + return state.can_reach('Nether Fortress', 'Region', player) and basic_combat(state, player) + +def can_brew_potions(state: CollectionState, player: int) -> bool: + return state.has('Blaze Rods', player) and state.has('Brewing', player) and has_bottle(state, player) + +def can_piglin_trade(state: CollectionState, player: int) -> bool: + return has_gold_ingots(state, player) and ( + state.can_reach('The Nether', 'Region', player) or + state.can_reach('Bastion Remnant', 'Region', player)) + +def overworld_villager(state: CollectionState, player: int) -> bool: + village_region = state.multiworld.get_region('Village', player).entrances[0].parent_region.name + if village_region == 'The Nether': # 2 options: cure zombie villager or build portal in village + return (state.can_reach('Zombie Doctor', 'Location', player) or + (has_diamond_pickaxe(state, player) and state.can_reach('Village', 'Region', player))) + elif village_region == 'The End': + return state.can_reach('Zombie Doctor', 'Location', player) + return state.can_reach('Village', 'Region', player) + +def enter_stronghold(state: CollectionState, player: int) -> bool: + return state.has('Blaze Rods', player) and state.has('Brewing', player) and state.has('3 Ender Pearls', player) + +# Difficulty-dependent functions +def combat_difficulty(state: CollectionState, player: int) -> bool: + return state.multiworld.combat_difficulty[player].current_key + +def can_adventure(state: CollectionState, player: int) -> bool: + death_link_check = not state.multiworld.death_link[player] or state.has('Bed', player) + if combat_difficulty(state, player) == 'easy': + return state.has('Progressive Weapons', player, 2) and has_iron_ingots(state, player) and death_link_check + elif combat_difficulty(state, player) == 'hard': + return True + return (state.has('Progressive Weapons', player) and death_link_check and + (state.has('Progressive Resource Crafting', player) or state.has('Campfire', player))) + +def basic_combat(state: CollectionState, player: int) -> bool: + if combat_difficulty(state, player) == 'easy': + return state.has('Progressive Weapons', player, 2) and state.has('Progressive Armor', player) and \ + state.has('Shield', player) and has_iron_ingots(state, player) + elif combat_difficulty(state, player) == 'hard': + return True + return state.has('Progressive Weapons', player) and (state.has('Progressive Armor', player) or state.has('Shield', player)) and has_iron_ingots(state, player) + +def complete_raid(state: CollectionState, player: int) -> bool: + reach_regions = state.can_reach('Village', 'Region', player) and state.can_reach('Pillager Outpost', 'Region', player) + if combat_difficulty(state, player) == 'easy': + return reach_regions and \ + state.has('Progressive Weapons', player, 3) and state.has('Progressive Armor', player, 2) and \ + state.has('Shield', player) and state.has('Archery', player) and \ + state.has('Progressive Tools', player, 2) and has_iron_ingots(state, player) + elif combat_difficulty(state, player) == 'hard': # might be too hard? + return reach_regions and state.has('Progressive Weapons', player, 2) and has_iron_ingots(state, player) and \ + (state.has('Progressive Armor', player) or state.has('Shield', player)) + return reach_regions and state.has('Progressive Weapons', player, 2) and has_iron_ingots(state, player) and \ + state.has('Progressive Armor', player) and state.has('Shield', player) + +def can_kill_wither(state: CollectionState, player: int) -> bool: + normal_kill = state.has("Progressive Weapons", player, 3) and state.has("Progressive Armor", player, 2) and can_brew_potions(state, player) and can_enchant(state, player) + if combat_difficulty(state, player) == 'easy': + return fortress_loot(state, player) and normal_kill and state.has('Archery', player) + elif combat_difficulty(state, player) == 'hard': # cheese kill using bedrock ceilings + return fortress_loot(state, player) and (normal_kill or state.can_reach('The Nether', 'Region', player) or state.can_reach('The End', 'Region', player)) + return fortress_loot(state, player) and normal_kill + +def can_respawn_ender_dragon(state: CollectionState, player: int) -> bool: + return state.can_reach('The Nether', 'Region', player) and state.can_reach('The End', 'Region', player) and \ + state.has('Progressive Resource Crafting', player) # smelt sand into glass + +def can_kill_ender_dragon(state: CollectionState, player: int) -> bool: + if combat_difficulty(state, player) == 'easy': + return state.has("Progressive Weapons", player, 3) and state.has("Progressive Armor", player, 2) and \ + state.has('Archery', player) and can_brew_potions(state, player) and can_enchant(state, player) + if combat_difficulty(state, player) == 'hard': + return (state.has('Progressive Weapons', player, 2) and state.has('Progressive Armor', player)) or \ + (state.has('Progressive Weapons', player, 1) and state.has('Bed', player)) + return state.has('Progressive Weapons', player, 2) and state.has('Progressive Armor', player) and state.has('Archery', player) + +def has_structure_compass(state: CollectionState, entrance_name: str, player: int) -> bool: + if not state.multiworld.structure_compasses[player]: + return True + return state.has(f"Structure Compass ({state.multiworld.get_entrance(entrance_name, player).connected_region.name})", player) -class MinecraftLogic(LogicMixin): +def get_rules_lookup(player: int): + rules_lookup: typing.Dict[str, typing.List[Callable[[CollectionState], bool]]] = { + "entrances": { + "Nether Portal": lambda state: (state.has('Flint and Steel', player) and + (state.has('Bucket', player) or state.has('Progressive Tools', player, 3)) and + has_iron_ingots(state, player)), + "End Portal": lambda state: enter_stronghold(state, player) and state.has('3 Ender Pearls', player, 4), + "Overworld Structure 1": lambda state: (can_adventure(state, player) and has_structure_compass(state, "Overworld Structure 1", player)), + "Overworld Structure 2": lambda state: (can_adventure(state, player) and has_structure_compass(state, "Overworld Structure 2", player)), + "Nether Structure 1": lambda state: (can_adventure(state, player) and has_structure_compass(state, "Nether Structure 1", player)), + "Nether Structure 2": lambda state: (can_adventure(state, player) and has_structure_compass(state, "Nether Structure 2", player)), + "The End Structure": lambda state: (can_adventure(state, player) and has_structure_compass(state, "The End Structure", player)), + }, + "locations": { + "Ender Dragon": lambda state: can_respawn_ender_dragon(state, player) and can_kill_ender_dragon(state, player), + "Wither": lambda state: can_kill_wither(state, player), + "Blaze Rods": lambda state: fortress_loot(state, player), - def _mc_has_iron_ingots(self, player: int): - return self.has('Progressive Tools', player) and self.has('Progressive Resource Crafting', player) - - def _mc_has_copper_ingots(self, player: int): - return self.has('Progressive Tools', player) and self.has('Progressive Resource Crafting', player) - - def _mc_has_gold_ingots(self, player: int): - return self.has('Progressive Resource Crafting', player) and (self.has('Progressive Tools', player, 2) or self.can_reach('The Nether', 'Region', player)) - - def _mc_has_diamond_pickaxe(self, player: int): - return self.has('Progressive Tools', player, 3) and self._mc_has_iron_ingots(player) - - def _mc_craft_crossbow(self, player: int): - return self.has('Archery', player) and self._mc_has_iron_ingots(player) - - def _mc_has_bottle(self, player: int): - return self.has('Bottles', player) and self.has('Progressive Resource Crafting', player) - - def _mc_has_spyglass(self, player: int): - return self._mc_has_copper_ingots(player) and self.has('Spyglass', player) and self._mc_can_adventure(player) - - def _mc_can_enchant(self, player: int): - return self.has('Enchanting', player) and self._mc_has_diamond_pickaxe(player) # mine obsidian and lapis - - def _mc_can_use_anvil(self, player: int): - return self.has('Enchanting', player) and self.has('Progressive Resource Crafting', player, 2) and self._mc_has_iron_ingots(player) - - def _mc_fortress_loot(self, player: int): # saddles, blaze rods, wither skulls - return self.can_reach('Nether Fortress', 'Region', player) and self._mc_basic_combat(player) - - def _mc_can_brew_potions(self, player: int): - return self.has('Blaze Rods', player) and self.has('Brewing', player) and self._mc_has_bottle(player) - - def _mc_can_piglin_trade(self, player: int): - return self._mc_has_gold_ingots(player) and ( - self.can_reach('The Nether', 'Region', player) or - self.can_reach('Bastion Remnant', 'Region', player)) - - def _mc_overworld_villager(self, player: int): - village_region = self.multiworld.get_region('Village', player).entrances[0].parent_region.name - if village_region == 'The Nether': # 2 options: cure zombie villager or build portal in village - return (self.can_reach('Zombie Doctor', 'Location', player) or - (self._mc_has_diamond_pickaxe(player) and self.can_reach('Village', 'Region', player))) - elif village_region == 'The End': - return self.can_reach('Zombie Doctor', 'Location', player) - return self.can_reach('Village', 'Region', player) - - def _mc_enter_stronghold(self, player: int): - return self.has('Blaze Rods', player) and self.has('Brewing', player) and self.has('3 Ender Pearls', player) - - # Difficulty-dependent functions - def _mc_combat_difficulty(self, player: int): - return self.multiworld.combat_difficulty[player].current_key - - def _mc_can_adventure(self, player: int): - death_link_check = not self.multiworld.death_link[player] or self.has('Bed', player) - if self._mc_combat_difficulty(player) == 'easy': - return self.has('Progressive Weapons', player, 2) and self._mc_has_iron_ingots(player) and death_link_check - elif self._mc_combat_difficulty(player) == 'hard': - return True - return (self.has('Progressive Weapons', player) and death_link_check and - (self.has('Progressive Resource Crafting', player) or self.has('Campfire', player))) - - def _mc_basic_combat(self, player: int): - if self._mc_combat_difficulty(player) == 'easy': - return self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player) and \ - self.has('Shield', player) and self._mc_has_iron_ingots(player) - elif self._mc_combat_difficulty(player) == 'hard': - return True - return self.has('Progressive Weapons', player) and (self.has('Progressive Armor', player) or self.has('Shield', player)) and self._mc_has_iron_ingots(player) - - def _mc_complete_raid(self, player: int): - reach_regions = self.can_reach('Village', 'Region', player) and self.can_reach('Pillager Outpost', 'Region', player) - if self._mc_combat_difficulty(player) == 'easy': - return reach_regions and \ - self.has('Progressive Weapons', player, 3) and self.has('Progressive Armor', player, 2) and \ - self.has('Shield', player) and self.has('Archery', player) and \ - self.has('Progressive Tools', player, 2) and self._mc_has_iron_ingots(player) - elif self._mc_combat_difficulty(player) == 'hard': # might be too hard? - return reach_regions and self.has('Progressive Weapons', player, 2) and self._mc_has_iron_ingots(player) and \ - (self.has('Progressive Armor', player) or self.has('Shield', player)) - return reach_regions and self.has('Progressive Weapons', player, 2) and self._mc_has_iron_ingots(player) and \ - self.has('Progressive Armor', player) and self.has('Shield', player) - - def _mc_can_kill_wither(self, player: int): - normal_kill = self.has("Progressive Weapons", player, 3) and self.has("Progressive Armor", player, 2) and self._mc_can_brew_potions(player) and self._mc_can_enchant(player) - if self._mc_combat_difficulty(player) == 'easy': - return self._mc_fortress_loot(player) and normal_kill and self.has('Archery', player) - elif self._mc_combat_difficulty(player) == 'hard': # cheese kill using bedrock ceilings - return self._mc_fortress_loot(player) and (normal_kill or self.can_reach('The Nether', 'Region', player) or self.can_reach('The End', 'Region', player)) - return self._mc_fortress_loot(player) and normal_kill - - def _mc_can_respawn_ender_dragon(self, player: int): - return self.can_reach('The Nether', 'Region', player) and self.can_reach('The End', 'Region', player) and \ - self.has('Progressive Resource Crafting', player) # smelt sand into glass - - def _mc_can_kill_ender_dragon(self, player: int): - if self._mc_combat_difficulty(player) == 'easy': - return self.has("Progressive Weapons", player, 3) and self.has("Progressive Armor", player, 2) and \ - self.has('Archery', player) and self._mc_can_brew_potions(player) and self._mc_can_enchant(player) - if self._mc_combat_difficulty(player) == 'hard': - return (self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player)) or \ - (self.has('Progressive Weapons', player, 1) and self.has('Bed', player)) - return self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player) and self.has('Archery', player) - - def _mc_has_structure_compass(self, entrance_name: str, player: int): - if not self.multiworld.structure_compasses[player]: - return True - return self.has(f"Structure Compass ({self.multiworld.get_entrance(entrance_name, player).connected_region.name})", player) - -# Sets rules on entrances and advancements that are always applied -def set_advancement_rules(world: MultiWorld, player: int): - - # Retrieves the appropriate structure compass for the given entrance - def get_struct_compass(entrance_name): - struct = world.get_entrance(entrance_name, player).connected_region.name - return f"Structure Compass ({struct})" - - set_rule(world.get_entrance("Nether Portal", player), lambda state: state.has('Flint and Steel', player) and - (state.has('Bucket', player) or state.has('Progressive Tools', player, 3)) and - state._mc_has_iron_ingots(player)) - set_rule(world.get_entrance("End Portal", player), lambda state: state._mc_enter_stronghold(player) and state.has('3 Ender Pearls', player, 4)) - set_rule(world.get_entrance("Overworld Structure 1", player), lambda state: state._mc_can_adventure(player) and state._mc_has_structure_compass("Overworld Structure 1", player)) - set_rule(world.get_entrance("Overworld Structure 2", player), lambda state: state._mc_can_adventure(player) and state._mc_has_structure_compass("Overworld Structure 2", player)) - set_rule(world.get_entrance("Nether Structure 1", player), lambda state: state._mc_can_adventure(player) and state._mc_has_structure_compass("Nether Structure 1", player)) - set_rule(world.get_entrance("Nether Structure 2", player), lambda state: state._mc_can_adventure(player) and state._mc_has_structure_compass("Nether Structure 2", player)) - set_rule(world.get_entrance("The End Structure", player), lambda state: state._mc_can_adventure(player) and state._mc_has_structure_compass("The End Structure", player)) - - set_rule(world.get_location("Ender Dragon", player), lambda state: state._mc_can_kill_ender_dragon(player)) - set_rule(world.get_location("Wither", player), lambda state: state._mc_can_kill_wither(player)) - set_rule(world.get_location("Blaze Spawner", player), lambda state: state._mc_fortress_loot(player)) - - set_rule(world.get_location("Who is Cutting Onions?", player), lambda state: state._mc_can_piglin_trade(player)) - set_rule(world.get_location("Oh Shiny", player), lambda state: state._mc_can_piglin_trade(player)) - set_rule(world.get_location("Suit Up", player), lambda state: state.has("Progressive Armor", player) and state._mc_has_iron_ingots(player)) - set_rule(world.get_location("Very Very Frightening", player), lambda state: state.has("Channeling Book", player) and - state._mc_can_use_anvil(player) and state._mc_can_enchant(player) and state._mc_overworld_villager(player)) - set_rule(world.get_location("Hot Stuff", player), lambda state: state.has("Bucket", player) and state._mc_has_iron_ingots(player)) - set_rule(world.get_location("Free the End", player), lambda state: state._mc_can_respawn_ender_dragon(player) and state._mc_can_kill_ender_dragon(player)) - set_rule(world.get_location("A Furious Cocktail", player), lambda state: state._mc_can_brew_potions(player) and - state.has("Fishing Rod", player) and # Water Breathing - state.can_reach('The Nether', 'Region', player) and # Regeneration, Fire Resistance, gold nuggets - state.can_reach('Village', 'Region', player) and # Night Vision, Invisibility - state.can_reach('Bring Home the Beacon', 'Location', player)) # Resistance - # set_rule(world.get_location("Best Friends Forever", player), lambda state: True) - set_rule(world.get_location("Bring Home the Beacon", player), lambda state: state._mc_can_kill_wither(player) and - state._mc_has_diamond_pickaxe(player) and state.has("Progressive Resource Crafting", player, 2)) - set_rule(world.get_location("Not Today, Thank You", player), lambda state: state.has("Shield", player) and state._mc_has_iron_ingots(player)) - set_rule(world.get_location("Isn't It Iron Pick", player), lambda state: state.has("Progressive Tools", player, 2) and state._mc_has_iron_ingots(player)) - set_rule(world.get_location("Local Brewery", player), lambda state: state._mc_can_brew_potions(player)) - set_rule(world.get_location("The Next Generation", player), lambda state: state._mc_can_kill_ender_dragon(player)) - set_rule(world.get_location("Fishy Business", player), lambda state: state.has("Fishing Rod", player)) - # set_rule(world.get_location("Hot Tourist Destinations", player), lambda state: True) - set_rule(world.get_location("This Boat Has Legs", player), lambda state: (state._mc_fortress_loot(player) or state._mc_complete_raid(player)) and - state.has("Saddle", player) and state.has("Fishing Rod", player)) - set_rule(world.get_location("Sniper Duel", player), lambda state: state.has("Archery", player)) - # set_rule(world.get_location("Nether", player), lambda state: True) - set_rule(world.get_location("Great View From Up Here", player), lambda state: state._mc_basic_combat(player)) - set_rule(world.get_location("How Did We Get Here?", player), lambda state: state._mc_can_brew_potions(player) and - state._mc_has_gold_ingots(player) and # Absorption - state.can_reach('End City', 'Region', player) and # Levitation - state.can_reach('The Nether', 'Region', player) and # potion ingredients - state.has("Fishing Rod", player) and state.has("Archery",player) and # Pufferfish, Nautilus Shells; spectral arrows - state.can_reach("Bring Home the Beacon", "Location", player) and # Haste - state.can_reach("Hero of the Village", "Location", player)) # Bad Omen, Hero of the Village - set_rule(world.get_location("Bullseye", player), lambda state: state.has("Archery", player) and state.has("Progressive Tools", player, 2) and state._mc_has_iron_ingots(player)) - set_rule(world.get_location("Spooky Scary Skeleton", player), lambda state: state._mc_basic_combat(player)) - set_rule(world.get_location("Two by Two", player), lambda state: state._mc_has_iron_ingots(player) and state.has("Bucket", player) and state._mc_can_adventure(player)) # shears > seagrass > turtles; buckets of tropical fish > axolotls; nether > striders; gold carrots > horses skips ingots - # set_rule(world.get_location("Stone Age", player), lambda state: True) - set_rule(world.get_location("Two Birds, One Arrow", player), lambda state: state._mc_craft_crossbow(player) and state._mc_can_enchant(player)) - # set_rule(world.get_location("We Need to Go Deeper", player), lambda state: True) - set_rule(world.get_location("Who's the Pillager Now?", player), lambda state: state._mc_craft_crossbow(player)) - set_rule(world.get_location("Getting an Upgrade", player), lambda state: state.has("Progressive Tools", player)) - set_rule(world.get_location("Tactical Fishing", player), lambda state: state.has("Bucket", player) and state._mc_has_iron_ingots(player)) - set_rule(world.get_location("Zombie Doctor", player), lambda state: state._mc_can_brew_potions(player) and state._mc_has_gold_ingots(player)) - # set_rule(world.get_location("The City at the End of the Game", player), lambda state: True) - set_rule(world.get_location("Ice Bucket Challenge", player), lambda state: state._mc_has_diamond_pickaxe(player)) - # set_rule(world.get_location("Remote Getaway", player), lambda state: True) - set_rule(world.get_location("Into Fire", player), lambda state: state._mc_basic_combat(player)) - set_rule(world.get_location("War Pigs", player), lambda state: state._mc_basic_combat(player)) - set_rule(world.get_location("Take Aim", player), lambda state: state.has("Archery", player)) - set_rule(world.get_location("Total Beelocation", player), lambda state: state.has("Silk Touch Book", player) and state._mc_can_use_anvil(player) and state._mc_can_enchant(player)) - set_rule(world.get_location("Arbalistic", player), lambda state: state._mc_craft_crossbow(player) and state.has("Piercing IV Book", player) and - state._mc_can_use_anvil(player) and state._mc_can_enchant(player)) - set_rule(world.get_location("The End... Again...", player), lambda state: state._mc_can_respawn_ender_dragon(player) and state._mc_can_kill_ender_dragon(player)) - set_rule(world.get_location("Acquire Hardware", player), lambda state: state._mc_has_iron_ingots(player)) - set_rule(world.get_location("Not Quite \"Nine\" Lives", player), lambda state: state._mc_can_piglin_trade(player) and state.has("Progressive Resource Crafting", player, 2)) - set_rule(world.get_location("Cover Me With Diamonds", player), lambda state: state.has("Progressive Armor", player, 2) and state.can_reach("Diamonds!", "Location", player)) - set_rule(world.get_location("Sky's the Limit", player), lambda state: state._mc_basic_combat(player)) - set_rule(world.get_location("Hired Help", player), lambda state: state.has("Progressive Resource Crafting", player, 2) and state._mc_has_iron_ingots(player)) - # set_rule(world.get_location("Return to Sender", player), lambda state: True) - set_rule(world.get_location("Sweet Dreams", player), lambda state: state.has("Bed", player) or state.can_reach('Village', 'Region', player)) - set_rule(world.get_location("You Need a Mint", player), lambda state: state._mc_can_respawn_ender_dragon(player) and state._mc_has_bottle(player)) - # set_rule(world.get_location("Adventure", player), lambda state: True) - set_rule(world.get_location("Monsters Hunted", player), lambda state: state._mc_can_respawn_ender_dragon(player) and state._mc_can_kill_ender_dragon(player) and - state._mc_can_kill_wither(player) and state.has("Fishing Rod", player)) # pufferfish for Water Breathing - set_rule(world.get_location("Enchanter", player), lambda state: state._mc_can_enchant(player)) - set_rule(world.get_location("Voluntary Exile", player), lambda state: state._mc_basic_combat(player)) - set_rule(world.get_location("Eye Spy", player), lambda state: state._mc_enter_stronghold(player)) - # set_rule(world.get_location("The End", player), lambda state: True) - set_rule(world.get_location("Serious Dedication", player), lambda state: state.can_reach("Hidden in the Depths", "Location", player) and state._mc_has_gold_ingots(player)) - set_rule(world.get_location("Postmortal", player), lambda state: state._mc_complete_raid(player)) - # set_rule(world.get_location("Monster Hunter", player), lambda state: True) - set_rule(world.get_location("Adventuring Time", player), lambda state: state._mc_can_adventure(player)) - # set_rule(world.get_location("A Seedy Place", player), lambda state: True) - # set_rule(world.get_location("Those Were the Days", player), lambda state: True) - set_rule(world.get_location("Hero of the Village", player), lambda state: state._mc_complete_raid(player)) - set_rule(world.get_location("Hidden in the Depths", player), lambda state: state._mc_can_brew_potions(player) and state.has("Bed", player) and state._mc_has_diamond_pickaxe(player)) # bed mining :) - set_rule(world.get_location("Beaconator", player), lambda state: state._mc_can_kill_wither(player) and state._mc_has_diamond_pickaxe(player) and - state.has("Progressive Resource Crafting", player, 2)) - set_rule(world.get_location("Withering Heights", player), lambda state: state._mc_can_kill_wither(player)) - set_rule(world.get_location("A Balanced Diet", player), lambda state: state._mc_has_bottle(player) and state._mc_has_gold_ingots(player) and # honey bottle; gapple - state.has("Progressive Resource Crafting", player, 2) and state.can_reach('The End', 'Region', player)) # notch apple, chorus fruit - set_rule(world.get_location("Subspace Bubble", player), lambda state: state._mc_has_diamond_pickaxe(player)) - # set_rule(world.get_location("Husbandry", player), lambda state: True) - set_rule(world.get_location("Country Lode, Take Me Home", player), lambda state: state.can_reach("Hidden in the Depths", "Location", player) and state._mc_has_gold_ingots(player)) - set_rule(world.get_location("Bee Our Guest", player), lambda state: state.has("Campfire", player) and state._mc_has_bottle(player)) - # set_rule(world.get_location("What a Deal!", player), lambda state: True) - set_rule(world.get_location("Uneasy Alliance", player), lambda state: state._mc_has_diamond_pickaxe(player) and state.has('Fishing Rod', player)) - set_rule(world.get_location("Diamonds!", player), lambda state: state.has("Progressive Tools", player, 2) and state._mc_has_iron_ingots(player)) - # set_rule(world.get_location("A Terrible Fortress", player), lambda state: True) # since you don't have to fight anything - set_rule(world.get_location("A Throwaway Joke", player), lambda state: state._mc_can_adventure(player)) # kill drowned - # set_rule(world.get_location("Minecraft", player), lambda state: True) - set_rule(world.get_location("Sticky Situation", player), lambda state: state.has("Campfire", player) and state._mc_has_bottle(player)) - set_rule(world.get_location("Ol' Betsy", player), lambda state: state._mc_craft_crossbow(player)) - set_rule(world.get_location("Cover Me in Debris", player), lambda state: state.has("Progressive Armor", player, 2) and - state.has("8 Netherite Scrap", player, 2) and state.has("Progressive Resource Crafting", player) and - state.can_reach("Diamonds!", "Location", player) and state.can_reach("Hidden in the Depths", "Location", player)) - # set_rule(world.get_location("The End?", player), lambda state: True) - # set_rule(world.get_location("The Parrots and the Bats", player), lambda state: True) - # set_rule(world.get_location("A Complete Catalogue", player), lambda state: True) # kill fish for raw - # set_rule(world.get_location("Getting Wood", player), lambda state: True) - # set_rule(world.get_location("Time to Mine!", player), lambda state: True) - set_rule(world.get_location("Hot Topic", player), lambda state: state.has("Progressive Resource Crafting", player)) - # set_rule(world.get_location("Bake Bread", player), lambda state: True) - set_rule(world.get_location("The Lie", player), lambda state: state._mc_has_iron_ingots(player) and state.has("Bucket", player)) - set_rule(world.get_location("On a Rail", player), lambda state: state._mc_has_iron_ingots(player) and state.has('Progressive Tools', player, 2)) # powered rails - # set_rule(world.get_location("Time to Strike!", player), lambda state: True) - # set_rule(world.get_location("Cow Tipper", player), lambda state: True) - set_rule(world.get_location("When Pigs Fly", player), lambda state: (state._mc_fortress_loot(player) or state._mc_complete_raid(player)) and - state.has("Saddle", player) and state.has("Fishing Rod", player) and state._mc_can_adventure(player)) - set_rule(world.get_location("Overkill", player), lambda state: state._mc_can_brew_potions(player) and - (state.has("Progressive Weapons", player) or state.can_reach('The Nether', 'Region', player))) # strength 1 + stone axe crit OR strength 2 + wood axe crit - set_rule(world.get_location("Librarian", player), lambda state: state.has("Enchanting", player)) - set_rule(world.get_location("Overpowered", player), lambda state: state._mc_has_iron_ingots(player) and - state.has('Progressive Tools', player, 2) and state._mc_basic_combat(player)) # mine gold blocks w/ iron pick - set_rule(world.get_location("Wax On", player), lambda state: state._mc_has_copper_ingots(player) and state.has('Campfire', player) and - state.has('Progressive Resource Crafting', player, 2)) - set_rule(world.get_location("Wax Off", player), lambda state: state._mc_has_copper_ingots(player) and state.has('Campfire', player) and - state.has('Progressive Resource Crafting', player, 2)) - set_rule(world.get_location("The Cutest Predator", player), lambda state: state._mc_has_iron_ingots(player) and state.has('Bucket', player)) - set_rule(world.get_location("The Healing Power of Friendship", player), lambda state: state._mc_has_iron_ingots(player) and state.has('Bucket', player)) - set_rule(world.get_location("Is It a Bird?", player), lambda state: state._mc_has_spyglass(player) and state._mc_can_adventure(player)) - set_rule(world.get_location("Is It a Balloon?", player), lambda state: state._mc_has_spyglass(player)) - set_rule(world.get_location("Is It a Plane?", player), lambda state: state._mc_has_spyglass(player) and state._mc_can_respawn_ender_dragon(player)) - set_rule(world.get_location("Surge Protector", player), lambda state: state.has("Channeling Book", player) and - state._mc_can_use_anvil(player) and state._mc_can_enchant(player) and state._mc_overworld_villager(player)) - set_rule(world.get_location("Light as a Rabbit", player), lambda state: state._mc_can_adventure(player) and state._mc_has_iron_ingots(player) and state.has('Bucket', player)) - set_rule(world.get_location("Glow and Behold!", player), lambda state: state._mc_can_adventure(player)) - set_rule(world.get_location("Whatever Floats Your Goat!", player), lambda state: state._mc_can_adventure(player)) - set_rule(world.get_location("Caves & Cliffs", player), lambda state: state._mc_has_iron_ingots(player) and state.has('Bucket', player) and state.has('Progressive Tools', player, 2)) - set_rule(world.get_location("Feels like home", player), lambda state: state._mc_has_iron_ingots(player) and state.has('Bucket', player) and state.has('Fishing Rod', player) and - (state._mc_fortress_loot(player) or state._mc_complete_raid(player)) and state.has("Saddle", player)) - set_rule(world.get_location("Sound of Music", player), lambda state: state.can_reach("Diamonds!", "Location", player) and state._mc_basic_combat(player)) - set_rule(world.get_location("Star Trader", player), lambda state: state._mc_has_iron_ingots(player) and state.has('Bucket', player) and - (state.can_reach("The Nether", 'Region', player) or state.can_reach("Nether Fortress", 'Region', player) or state._mc_can_piglin_trade(player)) and # soul sand for water elevator - state._mc_overworld_villager(player)) - - # 1.19 advancements - - # can make a cake, and a noteblock, and can reach a pillager outposts for allays - set_rule(world.get_location("Birthday Song", player), lambda state: state.can_reach("The Lie", "Location", player) and state.has("Progressive Tools", player, 2) and state._mc_has_iron_ingots(player)) - # can get to outposts. - # set_rule(world.get_location("You've Got a Friend in Me", player), lambda state: True) - # craft bucket and adventure to find frog spawning biome - set_rule(world.get_location("Bukkit Bukkit", player), lambda state: state.has("Bucket", player) and state._mc_has_iron_ingots(player) and state._mc_can_adventure(player)) - # I don't like this one its way to easy to get. just a pain to find. - set_rule(world.get_location("It Spreads", player), lambda state: state._mc_can_adventure(player) and state._mc_has_iron_ingots(player) and state.has("Progressive Tools", player, 2)) - # literally just a duplicate of It spreads. - set_rule(world.get_location("Sneak 100", player), lambda state: state._mc_can_adventure(player) and state._mc_has_iron_ingots(player) and state.has("Progressive Tools", player, 2)) - set_rule(world.get_location("When the Squad Hops into Town", player), lambda state: state._mc_can_adventure(player) and state.has("Lead", player)) - # lead frogs to the nether and a basalt delta's biomes to find magma cubes. - set_rule(world.get_location("With Our Powers Combined!", player), lambda state: state._mc_can_adventure(player) and state.has("Lead", player)) + "Who is Cutting Onions?": lambda state: can_piglin_trade(state, player), + "Oh Shiny": lambda state: can_piglin_trade(state, player), + "Suit Up": lambda state: state.has("Progressive Armor", player) and has_iron_ingots(state, player), + "Very Very Frightening": lambda state: (state.has("Channeling Book", player) and + can_use_anvil(state, player) and can_enchant(state, player) and overworld_villager(state, player)), + "Hot Stuff": lambda state: state.has("Bucket", player) and has_iron_ingots(state, player), + "Free the End": lambda state: can_respawn_ender_dragon(state, player) and can_kill_ender_dragon(state, player), + "A Furious Cocktail": lambda state: (can_brew_potions(state, player) and + state.has("Fishing Rod", player) and # Water Breathing + state.can_reach("The Nether", "Region", player) and # Regeneration, Fire Resistance, gold nuggets + state.can_reach("Village", "Region", player) and # Night Vision, Invisibility + state.can_reach("Bring Home the Beacon", "Location", player)), # Resistance + "Bring Home the Beacon": lambda state: (can_kill_wither(state, player) and + has_diamond_pickaxe(state, player) and state.has("Progressive Resource Crafting", player, 2)), + "Not Today, Thank You": lambda state: state.has("Shield", player) and has_iron_ingots(state, player), + "Isn't It Iron Pick": lambda state: state.has("Progressive Tools", player, 2) and has_iron_ingots(state, player), + "Local Brewery": lambda state: can_brew_potions(state, player), + "The Next Generation": lambda state: can_respawn_ender_dragon(state, player) and can_kill_ender_dragon(state, player), + "Fishy Business": lambda state: state.has("Fishing Rod", player), + "This Boat Has Legs": lambda state: ((fortress_loot(state, player) or complete_raid(state, player)) and + state.has("Saddle", player) and state.has("Fishing Rod", player)), + "Sniper Duel": lambda state: state.has("Archery", player), + "Great View From Up Here": lambda state: basic_combat(state, player), + "How Did We Get Here?": lambda state: (can_brew_potions(state, player) and + has_gold_ingots(state, player) and # Absorption + state.can_reach('End City', 'Region', player) and # Levitation + state.can_reach('The Nether', 'Region', player) and # potion ingredients + state.has("Fishing Rod", player) and state.has("Archery",player) and # Pufferfish, Nautilus Shells; spectral arrows + state.can_reach("Bring Home the Beacon", "Location", player) and # Haste + state.can_reach("Hero of the Village", "Location", player)), # Bad Omen, Hero of the Village + "Bullseye": lambda state: (state.has("Archery", player) and state.has("Progressive Tools", player, 2) and + has_iron_ingots(state, player)), + "Spooky Scary Skeleton": lambda state: basic_combat(state, player), + "Two by Two": lambda state: has_iron_ingots(state, player) and state.has("Bucket", player) and can_adventure(state, player), + "Two Birds, One Arrow": lambda state: craft_crossbow(state, player) and can_enchant(state, player), + "Who's the Pillager Now?": lambda state: craft_crossbow(state, player), + "Getting an Upgrade": lambda state: state.has("Progressive Tools", player), + "Tactical Fishing": lambda state: state.has("Bucket", player) and has_iron_ingots(state, player), + "Zombie Doctor": lambda state: can_brew_potions(state, player) and has_gold_ingots(state, player), + "Ice Bucket Challenge": lambda state: has_diamond_pickaxe(state, player), + "Into Fire": lambda state: basic_combat(state, player), + "War Pigs": lambda state: basic_combat(state, player), + "Take Aim": lambda state: state.has("Archery", player), + "Total Beelocation": lambda state: state.has("Silk Touch Book", player) and can_use_anvil(state, player) and can_enchant(state, player), + "Arbalistic": lambda state: (craft_crossbow(state, player) and state.has("Piercing IV Book", player) and + can_use_anvil(state, player) and can_enchant(state, player)), + "The End... Again...": lambda state: can_respawn_ender_dragon(state, player) and can_kill_ender_dragon(state, player), + "Acquire Hardware": lambda state: has_iron_ingots(state, player), + "Not Quite \"Nine\" Lives": lambda state: can_piglin_trade(state, player) and state.has("Progressive Resource Crafting", player, 2), + "Cover Me With Diamonds": lambda state: (state.has("Progressive Armor", player, 2) and + state.has("Progressive Tools", player, 2) and has_iron_ingots(state, player)), + "Sky's the Limit": lambda state: basic_combat(state, player), + "Hired Help": lambda state: state.has("Progressive Resource Crafting", player, 2) and has_iron_ingots(state, player), + "Sweet Dreams": lambda state: state.has("Bed", player) or state.can_reach('Village', 'Region', player), + "You Need a Mint": lambda state: can_respawn_ender_dragon(state, player) and has_bottle(state, player), + "Monsters Hunted": lambda state: (can_respawn_ender_dragon(state, player) and can_kill_ender_dragon(state, player) and + can_kill_wither(state, player) and state.has("Fishing Rod", player)), + "Enchanter": lambda state: can_enchant(state, player), + "Voluntary Exile": lambda state: basic_combat(state, player), + "Eye Spy": lambda state: enter_stronghold(state, player), + "Serious Dedication": lambda state: (can_brew_potions(state, player) and state.has("Bed", player) and + has_diamond_pickaxe(state, player) and has_gold_ingots(state, player)), + "Postmortal": lambda state: complete_raid(state, player), + "Adventuring Time": lambda state: can_adventure(state, player), + "Hero of the Village": lambda state: complete_raid(state, player), + "Hidden in the Depths": lambda state: can_brew_potions(state, player) and state.has("Bed", player) and has_diamond_pickaxe(state, player), + "Beaconator": lambda state: (can_kill_wither(state, player) and has_diamond_pickaxe(state, player) and + state.has("Progressive Resource Crafting", player, 2)), + "Withering Heights": lambda state: can_kill_wither(state, player), + "A Balanced Diet": lambda state: (has_bottle(state, player) and has_gold_ingots(state, player) and # honey bottle; gapple + state.has("Progressive Resource Crafting", player, 2) and state.can_reach('The End', 'Region', player)), # notch apple, chorus fruit + "Subspace Bubble": lambda state: has_diamond_pickaxe(state, player), + "Country Lode, Take Me Home": lambda state: state.can_reach("Hidden in the Depths", "Location", player) and has_gold_ingots(state, player), + "Bee Our Guest": lambda state: state.has("Campfire", player) and has_bottle(state, player), + "Uneasy Alliance": lambda state: has_diamond_pickaxe(state, player) and state.has('Fishing Rod', player), + "Diamonds!": lambda state: state.has("Progressive Tools", player, 2) and has_iron_ingots(state, player), + "A Throwaway Joke": lambda state: can_adventure(state, player), + "Sticky Situation": lambda state: state.has("Campfire", player) and has_bottle(state, player), + "Ol' Betsy": lambda state: craft_crossbow(state, player), + "Cover Me in Debris": lambda state: (state.has("Progressive Armor", player, 2) and + state.has("8 Netherite Scrap", player, 2) and state.has("Progressive Resource Crafting", player) and + has_diamond_pickaxe(state, player) and has_iron_ingots(state, player) and + can_brew_potions(state, player) and state.has("Bed", player)), + "Hot Topic": lambda state: state.has("Progressive Resource Crafting", player), + "The Lie": lambda state: has_iron_ingots(state, player) and state.has("Bucket", player), + "On a Rail": lambda state: has_iron_ingots(state, player) and state.has('Progressive Tools', player, 2), + "When Pigs Fly": lambda state: ((fortress_loot(state, player) or complete_raid(state, player)) and + state.has("Saddle", player) and state.has("Fishing Rod", player) and can_adventure(state, player)), + "Overkill": lambda state: (can_brew_potions(state, player) and + (state.has("Progressive Weapons", player) or state.can_reach('The Nether', 'Region', player))), + "Librarian": lambda state: state.has("Enchanting", player), + "Overpowered": lambda state: (has_iron_ingots(state, player) and + state.has('Progressive Tools', player, 2) and basic_combat(state, player)), + "Wax On": lambda state: (has_copper_ingots(state, player) and state.has('Campfire', player) and + state.has('Progressive Resource Crafting', player, 2)), + "Wax Off": lambda state: (has_copper_ingots(state, player) and state.has('Campfire', player) and + state.has('Progressive Resource Crafting', player, 2)), + "The Cutest Predator": lambda state: has_iron_ingots(state, player) and state.has('Bucket', player), + "The Healing Power of Friendship": lambda state: has_iron_ingots(state, player) and state.has('Bucket', player), + "Is It a Bird?": lambda state: has_spyglass(state, player) and can_adventure(state, player), + "Is It a Balloon?": lambda state: has_spyglass(state, player), + "Is It a Plane?": lambda state: has_spyglass(state, player) and can_respawn_ender_dragon(state, player), + "Surge Protector": lambda state: (state.has("Channeling Book", player) and + can_use_anvil(state, player) and can_enchant(state, player) and overworld_villager(state, player)), + "Light as a Rabbit": lambda state: can_adventure(state, player) and has_iron_ingots(state, player) and state.has('Bucket', player), + "Glow and Behold!": lambda state: can_adventure(state, player), + "Whatever Floats Your Goat!": lambda state: can_adventure(state, player), + "Caves & Cliffs": lambda state: has_iron_ingots(state, player) and state.has('Bucket', player) and state.has('Progressive Tools', player, 2), + "Feels like home": lambda state: (has_iron_ingots(state, player) and state.has('Bucket', player) and state.has('Fishing Rod', player) and + (fortress_loot(state, player) or complete_raid(state, player)) and state.has("Saddle", player)), + "Sound of Music": lambda state: state.has("Progressive Tools", player, 2) and has_iron_ingots(state, player) and basic_combat(state, player), + "Star Trader": lambda state: (has_iron_ingots(state, player) and state.has('Bucket', player) and + (state.can_reach("The Nether", 'Region', player) or + state.can_reach("Nether Fortress", 'Region', player) or # soul sand for water elevator + can_piglin_trade(state, player)) and + overworld_villager(state, player)), + "Birthday Song": lambda state: state.can_reach("The Lie", "Location", player) and state.has("Progressive Tools", player, 2) and has_iron_ingots(state, player), + "Bukkit Bukkit": lambda state: state.has("Bucket", player) and has_iron_ingots(state, player) and can_adventure(state, player), + "It Spreads": lambda state: can_adventure(state, player) and has_iron_ingots(state, player) and state.has("Progressive Tools", player, 2), + "Sneak 100": lambda state: can_adventure(state, player) and has_iron_ingots(state, player) and state.has("Progressive Tools", player, 2), + "When the Squad Hops into Town": lambda state: can_adventure(state, player) and state.has("Lead", player), + "With Our Powers Combined!": lambda state: can_adventure(state, player) and state.has("Lead", player), + } + } + return rules_lookup -# Sets rules on completion condition and postgame advancements -def set_completion_rules(world: MultiWorld, player: int): - def reachable_locations(state): - postgame_advancements = get_postgame_advancements(world.required_bosses[player].current_key) - return [location for location in world.get_locations() if - location.player == player and - location.name not in postgame_advancements and - location.address != None and - location.can_reach(state)] +def set_rules(mc_world: World) -> None: + multiworld = mc_world.multiworld + player = mc_world.player - def defeated_required_bosses(state): - return (world.required_bosses[player].current_key not in {"ender_dragon", "both"} or state.has("Defeat Ender Dragon", player)) and \ - (world.required_bosses[player].current_key not in {"wither", "both"} or state.has("Defeat Wither", player)) + rules_lookup = get_rules_lookup(player) - # 103 total advancements. Goal is to complete X advancements and then defeat the dragon. - # There are 11 possible postgame advancements; 5 for dragon, 5 for wither, 1 shared between them - # Hence the max for completion is 92 - egg_shards = min(world.egg_shards_required[player], world.egg_shards_available[player]) - completion_requirements = lambda state: len(reachable_locations(state)) >= world.advancement_goal[player] and \ - state.has("Dragon Egg Shard", player, egg_shards) - world.completion_condition[player] = lambda state: completion_requirements(state) and defeated_required_bosses(state) - # Set rules on postgame advancements - for adv_name in get_postgame_advancements(world.required_bosses[player].current_key): - add_rule(world.get_location(adv_name, player), completion_requirements) + # Set entrance rules + for entrance_name, rule in rules_lookup["entrances"].items(): + multiworld.get_entrance(entrance_name, player).access_rule = rule + + # Set location rules + for location_name, rule in rules_lookup["locations"].items(): + multiworld.get_location(location_name, player).access_rule = rule + + # Set rules surrounding completion + bosses = multiworld.required_bosses[player] + postgame_advancements = set() + if bosses.dragon: + postgame_advancements.update(Constants.exclusion_info["ender_dragon"]) + if bosses.wither: + postgame_advancements.update(Constants.exclusion_info["wither"]) + + def location_count(state: CollectionState) -> bool: + return len([location for location in multiworld.get_locations(player) if + location.address != None and + location.can_reach(state)]) + + def defeated_bosses(state: CollectionState) -> bool: + return ((not bosses.dragon or state.has("Ender Dragon", player)) + and (not bosses.wither or state.has("Wither", player))) + + egg_shards = min(multiworld.egg_shards_required[player], multiworld.egg_shards_available[player]) + completion_requirements = lambda state: (location_count(state) >= multiworld.advancement_goal[player] + and state.has("Dragon Egg Shard", player, egg_shards)) + multiworld.completion_condition[player] = lambda state: completion_requirements(state) and defeated_bosses(state) + + # Set exclusions on hard/unreasonable/postgame + excluded_advancements = set() + if not multiworld.include_hard_advancements[player]: + excluded_advancements.update(Constants.exclusion_info["hard"]) + if not multiworld.include_unreasonable_advancements[player]: + excluded_advancements.update(Constants.exclusion_info["unreasonable"]) + if not multiworld.include_postgame_advancements[player]: + excluded_advancements.update(postgame_advancements) + exclusion_rules(multiworld, player, excluded_advancements) diff --git a/worlds/minecraft/Structures.py b/worlds/minecraft/Structures.py new file mode 100644 index 0000000000..95bafc9efb --- /dev/null +++ b/worlds/minecraft/Structures.py @@ -0,0 +1,57 @@ +from worlds.AutoWorld import World + +from . import Constants + +def shuffle_structures(mc_world: World) -> None: + multiworld = mc_world.multiworld + player = mc_world.player + + default_connections = Constants.region_info["default_connections"] + illegal_connections = Constants.region_info["illegal_connections"] + + # Get all unpaired exits and all regions without entrances (except the Menu) + # This function is destructive on these lists. + exits = [exit.name for r in multiworld.regions if r.player == player for exit in r.exits if exit.connected_region == None] + structs = [r.name for r in multiworld.regions if r.player == player and r.entrances == [] and r.name != 'Menu'] + exits_spoiler = exits[:] # copy the original order for the spoiler log + + pairs = {} + + def set_pair(exit, struct): + if (exit in exits) and (struct in structs) and (exit not in illegal_connections.get(struct, [])): + pairs[exit] = struct + exits.remove(exit) + structs.remove(struct) + else: + raise Exception(f"Invalid connection: {exit} => {struct} for player {player} ({multiworld.player_name[player]})") + + # Connect plando structures first + if multiworld.plando_connections[player]: + for conn in multiworld.plando_connections[player]: + set_pair(conn.entrance, conn.exit) + + # The algorithm tries to place the most restrictive structures first. This algorithm always works on the + # relatively small set of restrictions here, but does not work on all possible inputs with valid configurations. + if multiworld.shuffle_structures[player]: + structs.sort(reverse=True, key=lambda s: len(illegal_connections.get(s, []))) + for struct in structs[:]: + try: + exit = multiworld.random.choice([e for e in exits if e not in illegal_connections.get(struct, [])]) + except IndexError: + raise Exception(f"No valid structure placements remaining for player {player} ({multiworld.player_name[player]})") + set_pair(exit, struct) + else: # write remaining default connections + for (exit, struct) in default_connections: + if exit in exits: + set_pair(exit, struct) + + # Make sure we actually paired everything; might fail if plando + try: + assert len(exits) == len(structs) == 0 + except AssertionError: + raise Exception(f"Failed to connect all Minecraft structures for player {player} ({multiworld.player_name[player]})") + + for exit in exits_spoiler: + multiworld.get_entrance(exit, player).connect(multiworld.get_region(pairs[exit], player)) + if multiworld.shuffle_structures[player] or multiworld.plando_connections[player]: + multiworld.spoiler.set_entrance(exit, pairs[exit], 'entrance', player) diff --git a/worlds/minecraft/__init__.py b/worlds/minecraft/__init__.py index cbd274ba84..a685d1ab4b 100644 --- a/worlds/minecraft/__init__.py +++ b/worlds/minecraft/__init__.py @@ -1,17 +1,16 @@ import os import json from base64 import b64encode, b64decode -from math import ceil +from typing import Dict, Any -from .Items import MinecraftItem, item_table, required_items, junk_weights -from .Locations import MinecraftAdvancement, advancement_table, exclusion_table, get_postgame_advancements -from .Regions import mc_regions, link_minecraft_structures, default_connections -from .Rules import set_advancement_rules, set_completion_rules -from worlds.generic.Rules import exclusion_rules +from BaseClasses import Region, Entrance, Item, Tutorial, ItemClassification, Location +from worlds.AutoWorld import World, WebWorld -from BaseClasses import Region, Entrance, Item, Tutorial, ItemClassification +from . import Constants from .Options import minecraft_options -from ..AutoWorld import World, WebWorld +from .Structures import shuffle_structures +from .ItemPool import build_item_pool, get_junk_item_names +from .Rules import set_rules client_version = 9 @@ -47,7 +46,16 @@ class MinecraftWebWorld(WebWorld): ["Albinum"] ) - tutorials = [setup, setup_es, setup_sv] + setup_fr = Tutorial( + setup.tutorial_name, + setup.description, + "Français", + "minecraft_fr.md", + "minecraft/fr", + ["TheLynk"] + ) + + tutorials = [setup, setup_es, setup_sv, setup_fr] class MinecraftWorld(World): @@ -62,13 +70,13 @@ class MinecraftWorld(World): topology_present = True web = MinecraftWebWorld() - item_name_to_id = {name: data.code for name, data in item_table.items()} - location_name_to_id = {name: data.id for name, data in advancement_table.items()} + item_name_to_id = Constants.item_name_to_id + location_name_to_id = Constants.location_name_to_id data_version = 7 - def _get_mc_data(self): - exits = [connection[0] for connection in default_connections] + def _get_mc_data(self) -> Dict[str, Any]: + exits = [connection[0] for connection in Constants.region_info["default_connections"]] return { 'world_seed': self.multiworld.per_slot_randoms[self.player].getrandbits(32), 'seed_name': self.multiworld.seed_name, @@ -87,74 +95,70 @@ class MinecraftWorld(World): 'race': self.multiworld.is_race, } - def generate_basic(self): + def create_item(self, name: str) -> Item: + item_class = ItemClassification.filler + if name in Constants.item_info["progression_items"]: + item_class = ItemClassification.progression + elif name in Constants.item_info["useful_items"]: + item_class = ItemClassification.useful + elif name in Constants.item_info["trap_items"]: + item_class = ItemClassification.trap - # Generate item pool - itempool = [] - junk_pool = junk_weights.copy() - # Add all required progression items - for (name, num) in required_items.items(): - itempool += [name] * num - # Add structure compasses if desired - if self.multiworld.structure_compasses[self.player]: - structures = [connection[1] for connection in default_connections] - for struct_name in structures: - itempool.append(f"Structure Compass ({struct_name})") - # Add dragon egg shards - if self.multiworld.egg_shards_required[self.player] > 0: - itempool += ["Dragon Egg Shard"] * self.multiworld.egg_shards_available[self.player] - # Add bee traps if desired - bee_trap_quantity = ceil(self.multiworld.bee_traps[self.player] * (len(self.location_names) - len(itempool)) * 0.01) - itempool += ["Bee Trap"] * bee_trap_quantity - # Fill remaining items with randomly generated junk - itempool += self.multiworld.random.choices(list(junk_pool.keys()), weights=list(junk_pool.values()), k=len(self.location_names) - len(itempool)) - # Convert itempool into real items - itempool = [item for item in map(lambda name: self.create_item(name), itempool)] + return MinecraftItem(name, item_class, self.item_name_to_id.get(name, None), self.player) - # Choose locations to automatically exclude based on settings - exclusion_pool = set() - exclusion_types = ['hard', 'unreasonable'] - for key in exclusion_types: - if not getattr(self.multiworld, f"include_{key}_advancements")[self.player]: - exclusion_pool.update(exclusion_table[key]) - # For postgame advancements, check with the boss goal - exclusion_pool.update(get_postgame_advancements(self.multiworld.required_bosses[self.player].current_key)) - exclusion_rules(self.multiworld, self.player, exclusion_pool) + def create_event(self, region_name: str, event_name: str) -> None: + region = self.multiworld.get_region(region_name, self.player) + loc = MinecraftLocation(self.player, event_name, None, region) + loc.place_locked_item(self.create_event_item(event_name)) + region.locations.append(loc) - # Prefill event locations with their events - self.multiworld.get_location("Blaze Spawner", self.player).place_locked_item(self.create_item("Blaze Rods")) - self.multiworld.get_location("Ender Dragon", self.player).place_locked_item(self.create_item("Defeat Ender Dragon")) - self.multiworld.get_location("Wither", self.player).place_locked_item(self.create_item("Defeat Wither")) + def create_event_item(self, name: str) -> None: + item = self.create_item(name) + item.classification = ItemClassification.progression + return item - self.multiworld.itempool += itempool + def create_regions(self) -> None: + # Create regions + for region_name, exits in Constants.region_info["regions"]: + r = Region(region_name, self.player, self.multiworld) + for exit_name in exits: + r.exits.append(Entrance(self.player, exit_name, r)) + self.multiworld.regions.append(r) - def get_filler_item_name(self) -> str: - return self.multiworld.random.choices(list(junk_weights.keys()), weights=list(junk_weights.values()))[0] + # Bind mandatory connections + for entr_name, region_name in Constants.region_info["mandatory_connections"]: + e = self.multiworld.get_entrance(entr_name, self.player) + r = self.multiworld.get_region(region_name, self.player) + e.connect(r) - def set_rules(self): - set_advancement_rules(self.multiworld, self.player) - set_completion_rules(self.multiworld, self.player) + # Add locations + for region_name, locations in Constants.location_info["locations_by_region"].items(): + region = self.multiworld.get_region(region_name, self.player) + for loc_name in locations: + loc = MinecraftLocation(self.player, loc_name, + self.location_name_to_id.get(loc_name, None), region) + region.locations.append(loc) - def create_regions(self): - def MCRegion(region_name: str, exits=[]): - ret = Region(region_name, self.player, self.multiworld) - ret.locations = [MinecraftAdvancement(self.player, loc_name, loc_data.id, ret) - for loc_name, loc_data in advancement_table.items() - if loc_data.region == region_name] - for exit in exits: - ret.exits.append(Entrance(self.player, exit, ret)) - return ret + # Add events + self.create_event("Nether Fortress", "Blaze Rods") + self.create_event("The End", "Ender Dragon") + self.create_event("Nether Fortress", "Wither") - self.multiworld.regions += [MCRegion(*r) for r in mc_regions] - link_minecraft_structures(self.multiworld, self.player) + # Shuffle the connections + shuffle_structures(self) - def generate_output(self, output_directory: str): + def create_items(self) -> None: + self.multiworld.itempool += build_item_pool(self) + + set_rules = set_rules + + def generate_output(self, output_directory: str) -> None: data = self._get_mc_data() filename = f"AP_{self.multiworld.get_out_file_name_base(self.player)}.apmc" with open(os.path.join(output_directory, filename), 'wb') as f: f.write(b64encode(bytes(json.dumps(data), 'utf-8'))) - def fill_slot_data(self): + def fill_slot_data(self) -> dict: slot_data = self._get_mc_data() for option_name in minecraft_options: option = getattr(self.multiworld, option_name)[self.player] @@ -162,20 +166,16 @@ class MinecraftWorld(World): slot_data[option_name] = int(option.value) return slot_data - def create_item(self, name: str) -> Item: - item_data = item_table[name] - if name == "Bee Trap": - classification = ItemClassification.trap - # prevent books from going on excluded locations - elif name in ("Sharpness III Book", "Infinity Book", "Looting III Book"): - classification = ItemClassification.useful - elif item_data.progression: - classification = ItemClassification.progression - else: - classification = ItemClassification.filler - item = MinecraftItem(name, classification, item_data.code, self.player) + def get_filler_item_name(self) -> str: + return get_junk_item_names(self.multiworld.random, 1)[0] + + +class MinecraftLocation(Location): + game = "Minecraft" + +class MinecraftItem(Item): + game = "Minecraft" - return item def mc_update_output(raw_data, server, port): data = json.loads(b64decode(raw_data)) diff --git a/worlds/minecraft/data/excluded_locations.json b/worlds/minecraft/data/excluded_locations.json new file mode 100644 index 0000000000..2f6fbbba6d --- /dev/null +++ b/worlds/minecraft/data/excluded_locations.json @@ -0,0 +1,40 @@ +{ + "hard": [ + "Very Very Frightening", + "A Furious Cocktail", + "Two by Two", + "Two Birds, One Arrow", + "Arbalistic", + "Monsters Hunted", + "Beaconator", + "A Balanced Diet", + "Uneasy Alliance", + "Cover Me in Debris", + "A Complete Catalogue", + "Surge Protector", + "Sound of Music", + "Star Trader", + "When the Squad Hops into Town", + "With Our Powers Combined!" + ], + "unreasonable": [ + "How Did We Get Here?", + "Adventuring Time" + ], + "ender_dragon": [ + "Free the End", + "The Next Generation", + "The End... Again...", + "You Need a Mint", + "Monsters Hunted", + "Is It a Plane?" + ], + "wither": [ + "Withering Heights", + "Bring Home the Beacon", + "Beaconator", + "A Furious Cocktail", + "How Did We Get Here?", + "Monsters Hunted" + ] +} \ No newline at end of file diff --git a/worlds/minecraft/data/items.json b/worlds/minecraft/data/items.json new file mode 100644 index 0000000000..7d35d18aeb --- /dev/null +++ b/worlds/minecraft/data/items.json @@ -0,0 +1,128 @@ +{ + "all_items": [ + "Archery", + "Progressive Resource Crafting", + "Resource Blocks", + "Brewing", + "Enchanting", + "Bucket", + "Flint and Steel", + "Bed", + "Bottles", + "Shield", + "Fishing Rod", + "Campfire", + "Progressive Weapons", + "Progressive Tools", + "Progressive Armor", + "8 Netherite Scrap", + "8 Emeralds", + "4 Emeralds", + "Channeling Book", + "Silk Touch Book", + "Sharpness III Book", + "Piercing IV Book", + "Looting III Book", + "Infinity Book", + "4 Diamond Ore", + "16 Iron Ore", + "500 XP", + "100 XP", + "50 XP", + "3 Ender Pearls", + "4 Lapis Lazuli", + "16 Porkchops", + "8 Gold Ore", + "Rotten Flesh", + "Single Arrow", + "32 Arrows", + "Saddle", + "Structure Compass (Village)", + "Structure Compass (Pillager Outpost)", + "Structure Compass (Nether Fortress)", + "Structure Compass (Bastion Remnant)", + "Structure Compass (End City)", + "Shulker Box", + "Dragon Egg Shard", + "Spyglass", + "Lead", + "Bee Trap" + ], + "progression_items": [ + "Archery", + "Progressive Resource Crafting", + "Resource Blocks", + "Brewing", + "Enchanting", + "Bucket", + "Flint and Steel", + "Bed", + "Bottles", + "Shield", + "Fishing Rod", + "Campfire", + "Progressive Weapons", + "Progressive Tools", + "Progressive Armor", + "8 Netherite Scrap", + "Channeling Book", + "Silk Touch Book", + "Piercing IV Book", + "3 Ender Pearls", + "Saddle", + "Structure Compass (Village)", + "Structure Compass (Pillager Outpost)", + "Structure Compass (Nether Fortress)", + "Structure Compass (Bastion Remnant)", + "Structure Compass (End City)", + "Dragon Egg Shard", + "Spyglass", + "Lead" + ], + "useful_items": [ + "Sharpness III Book", + "Looting III Book", + "Infinity Book" + ], + "trap_items": [ + "Bee Trap" + ], + + "required_pool": { + "Archery": 1, + "Progressive Resource Crafting": 2, + "Brewing": 1, + "Enchanting": 1, + "Bucket": 1, + "Flint and Steel": 1, + "Bed": 1, + "Bottles": 1, + "Shield": 1, + "Fishing Rod": 1, + "Campfire": 1, + "Progressive Weapons": 3, + "Progressive Tools": 3, + "Progressive Armor": 2, + "8 Netherite Scrap": 2, + "Channeling Book": 1, + "Silk Touch Book": 1, + "Sharpness III Book": 1, + "Piercing IV Book": 1, + "Looting III Book": 1, + "Infinity Book": 1, + "3 Ender Pearls": 4, + "Saddle": 1, + "Spyglass": 1, + "Lead": 1 + }, + "junk_weights": { + "4 Emeralds": 2, + "4 Diamond Ore": 1, + "16 Iron Ore": 1, + "50 XP": 4, + "16 Porkchops": 2, + "8 Gold Ore": 1, + "Rotten Flesh": 1, + "32 Arrows": 1 + } +} \ No newline at end of file diff --git a/worlds/minecraft/data/locations.json b/worlds/minecraft/data/locations.json new file mode 100644 index 0000000000..7cd00e5851 --- /dev/null +++ b/worlds/minecraft/data/locations.json @@ -0,0 +1,250 @@ +{ + "all_locations": [ + "Who is Cutting Onions?", + "Oh Shiny", + "Suit Up", + "Very Very Frightening", + "Hot Stuff", + "Free the End", + "A Furious Cocktail", + "Best Friends Forever", + "Bring Home the Beacon", + "Not Today, Thank You", + "Isn't It Iron Pick", + "Local Brewery", + "The Next Generation", + "Fishy Business", + "Hot Tourist Destinations", + "This Boat Has Legs", + "Sniper Duel", + "Nether", + "Great View From Up Here", + "How Did We Get Here?", + "Bullseye", + "Spooky Scary Skeleton", + "Two by Two", + "Stone Age", + "Two Birds, One Arrow", + "We Need to Go Deeper", + "Who's the Pillager Now?", + "Getting an Upgrade", + "Tactical Fishing", + "Zombie Doctor", + "The City at the End of the Game", + "Ice Bucket Challenge", + "Remote Getaway", + "Into Fire", + "War Pigs", + "Take Aim", + "Total Beelocation", + "Arbalistic", + "The End... Again...", + "Acquire Hardware", + "Not Quite \"Nine\" Lives", + "Cover Me With Diamonds", + "Sky's the Limit", + "Hired Help", + "Return to Sender", + "Sweet Dreams", + "You Need a Mint", + "Adventure", + "Monsters Hunted", + "Enchanter", + "Voluntary Exile", + "Eye Spy", + "The End", + "Serious Dedication", + "Postmortal", + "Monster Hunter", + "Adventuring Time", + "A Seedy Place", + "Those Were the Days", + "Hero of the Village", + "Hidden in the Depths", + "Beaconator", + "Withering Heights", + "A Balanced Diet", + "Subspace Bubble", + "Husbandry", + "Country Lode, Take Me Home", + "Bee Our Guest", + "What a Deal!", + "Uneasy Alliance", + "Diamonds!", + "A Terrible Fortress", + "A Throwaway Joke", + "Minecraft", + "Sticky Situation", + "Ol' Betsy", + "Cover Me in Debris", + "The End?", + "The Parrots and the Bats", + "A Complete Catalogue", + "Getting Wood", + "Time to Mine!", + "Hot Topic", + "Bake Bread", + "The Lie", + "On a Rail", + "Time to Strike!", + "Cow Tipper", + "When Pigs Fly", + "Overkill", + "Librarian", + "Overpowered", + "Wax On", + "Wax Off", + "The Cutest Predator", + "The Healing Power of Friendship", + "Is It a Bird?", + "Is It a Balloon?", + "Is It a Plane?", + "Surge Protector", + "Light as a Rabbit", + "Glow and Behold!", + "Whatever Floats Your Goat!", + "Caves & Cliffs", + "Feels like home", + "Sound of Music", + "Star Trader", + "Birthday Song", + "Bukkit Bukkit", + "It Spreads", + "Sneak 100", + "When the Squad Hops into Town", + "With Our Powers Combined!", + "You've Got a Friend in Me" + ], + "locations_by_region": { + "Overworld": [ + "Who is Cutting Onions?", + "Oh Shiny", + "Suit Up", + "Very Very Frightening", + "Hot Stuff", + "Best Friends Forever", + "Not Today, Thank You", + "Isn't It Iron Pick", + "Fishy Business", + "Sniper Duel", + "Bullseye", + "Stone Age", + "Two Birds, One Arrow", + "Getting an Upgrade", + "Tactical Fishing", + "Zombie Doctor", + "Ice Bucket Challenge", + "Take Aim", + "Total Beelocation", + "Arbalistic", + "Acquire Hardware", + "Cover Me With Diamonds", + "Hired Help", + "Sweet Dreams", + "Adventure", + "Monsters Hunted", + "Enchanter", + "Eye Spy", + "Monster Hunter", + "Adventuring Time", + "A Seedy Place", + "Husbandry", + "Bee Our Guest", + "Diamonds!", + "A Throwaway Joke", + "Minecraft", + "Sticky Situation", + "Ol' Betsy", + "The Parrots and the Bats", + "Getting Wood", + "Time to Mine!", + "Hot Topic", + "Bake Bread", + "The Lie", + "On a Rail", + "Time to Strike!", + "Cow Tipper", + "When Pigs Fly", + "Librarian", + "Wax On", + "Wax Off", + "The Cutest Predator", + "The Healing Power of Friendship", + "Is It a Bird?", + "Surge Protector", + "Light as a Rabbit", + "Glow and Behold!", + "Whatever Floats Your Goat!", + "Caves & Cliffs", + "Sound of Music", + "Bukkit Bukkit", + "It Spreads", + "Sneak 100", + "When the Squad Hops into Town" + ], + "The Nether": [ + "Hot Tourist Destinations", + "This Boat Has Legs", + "Nether", + "Two by Two", + "We Need to Go Deeper", + "Not Quite \"Nine\" Lives", + "Return to Sender", + "Serious Dedication", + "Hidden in the Depths", + "Subspace Bubble", + "Country Lode, Take Me Home", + "Uneasy Alliance", + "Cover Me in Debris", + "Is It a Balloon?", + "Feels like home", + "With Our Powers Combined!" + ], + "The End": [ + "Free the End", + "The Next Generation", + "Remote Getaway", + "The End... Again...", + "You Need a Mint", + "The End", + "The End?", + "Is It a Plane?" + ], + "Village": [ + "Postmortal", + "Hero of the Village", + "A Balanced Diet", + "What a Deal!", + "A Complete Catalogue", + "Star Trader" + ], + "Nether Fortress": [ + "A Furious Cocktail", + "Bring Home the Beacon", + "Local Brewery", + "How Did We Get Here?", + "Spooky Scary Skeleton", + "Into Fire", + "Beaconator", + "Withering Heights", + "A Terrible Fortress", + "Overkill" + ], + "Pillager Outpost": [ + "Who's the Pillager Now?", + "Voluntary Exile", + "Birthday Song", + "You've Got a Friend in Me" + ], + "Bastion Remnant": [ + "War Pigs", + "Those Were the Days", + "Overpowered" + ], + "End City": [ + "Great View From Up Here", + "The City at the End of the Game", + "Sky's the Limit" + ] + } +} \ No newline at end of file diff --git a/worlds/minecraft/data/regions.json b/worlds/minecraft/data/regions.json new file mode 100644 index 0000000000..c9e51e4829 --- /dev/null +++ b/worlds/minecraft/data/regions.json @@ -0,0 +1,28 @@ +{ + "regions": [ + ["Menu", ["New World"]], + ["Overworld", ["Nether Portal", "End Portal", "Overworld Structure 1", "Overworld Structure 2"]], + ["The Nether", ["Nether Structure 1", "Nether Structure 2"]], + ["The End", ["The End Structure"]], + ["Village", []], + ["Pillager Outpost", []], + ["Nether Fortress", []], + ["Bastion Remnant", []], + ["End City", []] + ], + "mandatory_connections": [ + ["New World", "Overworld"], + ["Nether Portal", "The Nether"], + ["End Portal", "The End"] + ], + "default_connections": [ + ["Overworld Structure 1", "Village"], + ["Overworld Structure 2", "Pillager Outpost"], + ["Nether Structure 1", "Nether Fortress"], + ["Nether Structure 2", "Bastion Remnant"], + ["The End Structure", "End City"] + ], + "illegal_connections": { + "Nether Fortress": ["The End Structure"] + } +} \ No newline at end of file diff --git a/worlds/minecraft/docs/minecraft_fr.md b/worlds/minecraft/docs/minecraft_fr.md new file mode 100644 index 0000000000..e25febba42 --- /dev/null +++ b/worlds/minecraft/docs/minecraft_fr.md @@ -0,0 +1,74 @@ +# Guide de configuration du randomiseur Minecraft + +## Logiciel requis + +- Minecraft Java Edition à partir de + la [page de la boutique Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-edition) +- Archipelago depuis la [page des versions d'Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) + - (sÊlectionnez `Minecraft Client` lors de l'installation.) + +## Configuration de votre fichier YAML + +### Qu'est-ce qu'un fichier YAML et pourquoi en ai-je besoin ? + +Voir le guide sur la configuration d'un YAML de base lors de la configuration d'Archipelago +guide : [Guide de configuration de base de Multiworld](/tutorial/Archipelago/setup/en) + +### OÚ puis-je obtenir un fichier YAML ? + +Vous pouvez personnaliser vos paramètres Minecraft en allant sur la [page des paramètres de joueur](/games/Minecraft/player-settings) + +## Rejoindre une partie MultiWorld + +### Obtenez votre fichier de donnÊes Minecraft + +**Un seul fichier yaml doit ÃĒtre soumis par monde minecraft, quel que soit le nombre de joueurs qui y jouent.** + +Lorsque vous rejoignez un jeu multimonde, il vous sera demandÊ de fournir votre fichier YAML à l'hÊbergeur. Une fois cela fait, +l'hÊbergeur vous fournira soit un lien pour tÊlÊcharger votre fichier de donnÊes, soit un fichier zip contenant les donnÊes de chacun +des dossiers. Votre fichier de donnÊes doit avoir une extension `.apmc`. + +Double-cliquez sur votre fichier `.apmc` pour que le client Minecraft lance automatiquement le serveur forge installÊ. Assurez-vous de +laissez cette fenÃĒtre ouverte car il s'agit de votre console serveur. + +### Connectez-vous au multiserveur + +Ouvrez Minecraft, accÊdez à "Multijoueur> Connexion directe" et rejoignez l'adresse du serveur "localhost". + +Si vous utilisez le site Web pour hÊberger le jeu, il devrait se connecter automatiquement au serveur AP sans avoir besoin de `/connect` + +sinon, une fois que vous ÃĒtes dans le jeu, tapez `/connect (Port) (Password)` oÚ `` est l'adresse du +Serveur Archipelago. `(Port)` n'est requis que si le serveur Archipelago n'utilise pas le port par dÊfaut 38281. Notez qu'il n'y a pas de deux-points entre `` et `(Port)` mais un espace. +`(Mot de passe)` n'est requis que si le serveur Archipelago que vous utilisez a un mot de passe dÊfini. + +### Jouer le jeu + +Lorsque la console vous indique que vous avez rejoint la salle, vous ÃĒtes prÃĒt. FÊlicitations pour avoir rejoint avec succès un +jeu multimonde ! À ce stade, tous les joueurs minecraft supplÊmentaires peuvent se connecter à votre serveur forge. Pour commencer le jeu une fois +que tout le monde est prÃĒt utilisez la commande `/start`. + +## Installation non Windows + +Le client Minecraft installera forge et le mod pour d'autres systèmes d'exploitation, mais Java doit ÃĒtre fourni par l' +utilisateur. Rendez-vous sur [minecraft_versions.json sur le MC AP GitHub](https://raw.githubusercontent.com/KonoTyran/Minecraft_AP_Randomizer/master/versions/minecraft_versions.json) +pour voir quelle version de Java est requise. Les nouvelles installations utiliseront par dÊfaut la version "release" la plus ÊlevÊe. +- Installez le JDK Amazon Corretto correspondant + - voir les [Liens d'installation manuelle du logiciel](#manual-installation-software-links) + - ou gestionnaire de paquets fourni par votre OS/distribution +- Ouvrez votre `host.yaml` et ajoutez le chemin vers votre Java sous la clÊ `minecraft_options` + - ` java : "chemin/vers/java-xx-amazon-corretto/bin/java"` +- ExÊcutez le client Minecraft et sÊlectionnez votre fichier .apmc + +## Installation manuelle complète + +Il est fortement recommandÊ d'utiliser le programme d'installation d'Archipelago pour gÊrer l'installation du serveur forge pour vous. +Le support ne sera pas fourni pour ceux qui souhaitent installer manuellement forge. Pour ceux d'entre vous qui savent comment faire et qui souhaitent le faire, +les liens suivants sont les versions des logiciels que nous utilisons. + +### Liens d'installation manuelle du logiciel + +- [Page de tÊlÊchargement de Minecraft Forge] (https://files.minecraftforge.net/net/minecraftforge/forge/) +- [Page des versions du mod Minecraft Archipelago Randomizer] (https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases) + - **NE PAS INSTALLER CECI SUR VOTRE CLIENT** +- [Amazon Corretto](https://docs.aws.amazon.com/corretto/) + - choisissez la version correspondante et sÊlectionnez "TÊlÊchargements" sur la gauche \ No newline at end of file diff --git a/worlds/minecraft/test/TestAdvancements.py b/worlds/minecraft/test/TestAdvancements.py index 5fc64f76bf..321aef1af9 100644 --- a/worlds/minecraft/test/TestAdvancements.py +++ b/worlds/minecraft/test/TestAdvancements.py @@ -1,10 +1,14 @@ -from .TestMinecraft import TestMinecraft +from . import MCTestBase # Format: # [location, expected_result, given_items, [excluded_items]] # Every advancement has its own test, named by its internal ID number. -class TestAdvancements(TestMinecraft): +class TestAdvancements(MCTestBase): + options = { + "shuffle_structures": False, + "structure_compasses": False + } def test_42000(self): self.run_location_tests([ @@ -1278,3 +1282,129 @@ class TestAdvancements(TestMinecraft): ["Whatever Floats Your Goat!", True, ["Progressive Weapons", "Progressive Resource Crafting"]], ["Whatever Floats Your Goat!", True, ["Progressive Weapons", "Campfire"]], ]) + + # bucket, iron pick + def test_42103(self): + self.run_location_tests([ + ["Caves & Cliffs", False, []], + ["Caves & Cliffs", False, [], ["Bucket"]], + ["Caves & Cliffs", False, [], ["Progressive Tools"]], + ["Caves & Cliffs", False, [], ["Progressive Resource Crafting"]], + ["Caves & Cliffs", True, ["Progressive Resource Crafting", "Progressive Tools", "Progressive Tools", "Bucket"]], + ]) + + # bucket, fishing rod, saddle, combat + def test_42104(self): + self.run_location_tests([ + ["Feels like home", False, []], + ["Feels like home", False, [], ['Progressive Resource Crafting']], + ["Feels like home", False, [], ['Progressive Tools']], + ["Feels like home", False, [], ['Progressive Weapons']], + ["Feels like home", False, [], ['Progressive Armor', 'Shield']], + ["Feels like home", False, [], ['Fishing Rod']], + ["Feels like home", False, [], ['Saddle']], + ["Feels like home", False, [], ['Bucket']], + ["Feels like home", False, [], ['Flint and Steel']], + ["Feels like home", True, ['Saddle', 'Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Progressive Weapons', 'Progressive Armor', 'Fishing Rod']], + ["Feels like home", True, ['Saddle', 'Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Progressive Weapons', 'Shield', 'Fishing Rod']], + ]) + + # iron pick, combat + def test_42105(self): + self.run_location_tests([ + ["Sound of Music", False, []], + ["Sound of Music", False, [], ["Progressive Tools"]], + ["Sound of Music", False, [], ["Progressive Resource Crafting"]], + ["Sound of Music", False, [], ["Progressive Weapons"]], + ["Sound of Music", False, [], ["Progressive Armor", "Shield"]], + ["Sound of Music", True, ["Progressive Tools", "Progressive Tools", "Progressive Resource Crafting", "Progressive Weapons", "Progressive Armor"]], + ["Sound of Music", True, ["Progressive Tools", "Progressive Tools", "Progressive Resource Crafting", "Progressive Weapons", "Shield"]], + ]) + + # bucket, nether, villager + def test_42106(self): + self.run_location_tests([ + ["Star Trader", False, []], + ["Star Trader", False, [], ["Bucket"]], + ["Star Trader", False, [], ["Flint and Steel"]], + ["Star Trader", False, [], ["Progressive Tools"]], + ["Star Trader", False, [], ["Progressive Resource Crafting"]], + ["Star Trader", False, [], ["Progressive Weapons"]], + ["Star Trader", True, ["Bucket", "Flint and Steel", "Progressive Tools", "Progressive Resource Crafting", "Progressive Weapons"]], + ]) + + # bucket, redstone -> iron pick, pillager outpost -> adventure + def test_42107(self): + self.run_location_tests([ + ["Birthday Song", False, []], + ["Birthday Song", False, [], ["Bucket"]], + ["Birthday Song", False, [], ["Progressive Tools"]], + ["Birthday Song", False, [], ["Progressive Weapons"]], + ["Birthday Song", False, [], ["Progressive Resource Crafting"]], + ["Birthday Song", True, ["Progressive Resource Crafting", "Progressive Tools", "Progressive Tools", "Progressive Weapons", "Bucket"]], + ]) + + # bucket, adventure + def test_42108(self): + self.run_location_tests([ + ["Bukkit Bukkit", False, []], + ["Bukkit Bukkit", False, [], ["Bucket"]], + ["Bukkit Bukkit", False, [], ["Progressive Tools"]], + ["Bukkit Bukkit", False, [], ["Progressive Weapons"]], + ["Bukkit Bukkit", False, [], ["Progressive Resource Crafting"]], + ["Bukkit Bukkit", True, ["Bucket", "Progressive Tools", "Progressive Weapons", "Progressive Resource Crafting"]], + ]) + + # iron pick, adventure + def test_42109(self): + self.run_location_tests([ + ["It Spreads", False, []], + ["It Spreads", False, [], ["Progressive Tools"]], + ["It Spreads", False, [], ["Progressive Weapons"]], + ["It Spreads", False, [], ["Progressive Resource Crafting"]], + ["It Spreads", True, ["Progressive Tools", "Progressive Tools", "Progressive Weapons", "Progressive Resource Crafting"]], + ]) + + # iron pick, adventure + def test_42110(self): + self.run_location_tests([ + ["Sneak 100", False, []], + ["Sneak 100", False, [], ["Progressive Tools"]], + ["Sneak 100", False, [], ["Progressive Weapons"]], + ["Sneak 100", False, [], ["Progressive Resource Crafting"]], + ["Sneak 100", True, ["Progressive Tools", "Progressive Tools", "Progressive Weapons", "Progressive Resource Crafting"]], + ]) + + # adventure, lead + def test_42111(self): + self.run_location_tests([ + ["When the Squad Hops into Town", False, []], + ["When the Squad Hops into Town", False, [], ["Progressive Weapons"]], + ["When the Squad Hops into Town", False, [], ["Campfire", "Progressive Resource Crafting"]], + ["When the Squad Hops into Town", False, [], ["Lead"]], + ["When the Squad Hops into Town", True, ["Progressive Weapons", "Lead", "Campfire"]], + ["When the Squad Hops into Town", True, ["Progressive Weapons", "Lead", "Progressive Resource Crafting"]], + ]) + + # adventure, lead, nether + def test_42112(self): + self.run_location_tests([ + ["With Our Powers Combined!", False, []], + ["With Our Powers Combined!", False, [], ["Lead"]], + ["With Our Powers Combined!", False, [], ["Bucket", "Progressive Tools"]], + ["With Our Powers Combined!", False, [], ["Flint and Steel"]], + ["With Our Powers Combined!", False, [], ["Progressive Weapons"]], + ["With Our Powers Combined!", False, [], ["Progressive Resource Crafting"]], + ["With Our Powers Combined!", True, ["Lead", "Progressive Weapons", "Progressive Resource Crafting", "Flint and Steel", "Progressive Tools", "Bucket"]], + ["With Our Powers Combined!", True, ["Lead", "Progressive Weapons", "Progressive Resource Crafting", "Flint and Steel", "Progressive Tools", "Progressive Tools", "Progressive Tools"]], + ]) + + # pillager outpost -> adventure + def test_42113(self): + self.run_location_tests([ + ["You've Got a Friend in Me", False, []], + ["You've Got a Friend in Me", False, [], ["Progressive Weapons"]], + ["You've Got a Friend in Me", False, [], ["Campfire", "Progressive Resource Crafting"]], + ["You've Got a Friend in Me", True, ["Progressive Weapons", "Campfire"]], + ["You've Got a Friend in Me", True, ["Progressive Weapons", "Progressive Resource Crafting"]], + ]) diff --git a/worlds/minecraft/test/TestDataLoad.py b/worlds/minecraft/test/TestDataLoad.py new file mode 100644 index 0000000000..c14eef071b --- /dev/null +++ b/worlds/minecraft/test/TestDataLoad.py @@ -0,0 +1,60 @@ +import unittest + +from .. import Constants + +class TestDataLoad(unittest.TestCase): + + def test_item_data(self): + item_info = Constants.item_info + + # All items in sub-tables are in all_items + all_items: set = set(item_info['all_items']) + assert set(item_info['progression_items']) <= all_items + assert set(item_info['useful_items']) <= all_items + assert set(item_info['trap_items']) <= all_items + assert set(item_info['required_pool'].keys()) <= all_items + assert set(item_info['junk_weights'].keys()) <= all_items + + # No overlapping ids (because of bee trap stuff) + all_ids: set = set(Constants.item_name_to_id.values()) + assert len(all_items) == len(all_ids) + + def test_location_data(self): + location_info = Constants.location_info + exclusion_info = Constants.exclusion_info + + # Every location has a region and every region's locations are in all_locations + all_locations: set = set(location_info['all_locations']) + all_locs_2: set = set() + for v in location_info['locations_by_region'].values(): + all_locs_2.update(v) + assert all_locations == all_locs_2 + + # All exclusions are locations + for v in exclusion_info.values(): + assert set(v) <= all_locations + + def test_region_data(self): + region_info = Constants.region_info + + # Every entrance and region in mandatory/default/illegal connections is a real entrance and region + all_regions = set() + all_entrances = set() + for v in region_info['regions']: + assert isinstance(v[0], str) + assert isinstance(v[1], list) + all_regions.add(v[0]) + all_entrances.update(v[1]) + + for v in region_info['mandatory_connections']: + assert v[0] in all_entrances + assert v[1] in all_regions + + for v in region_info['default_connections']: + assert v[0] in all_entrances + assert v[1] in all_regions + + for k, v in region_info['illegal_connections'].items(): + assert k in all_regions + assert set(v) <= all_entrances + diff --git a/worlds/minecraft/test/TestEntrances.py b/worlds/minecraft/test/TestEntrances.py index 8e80a1353a..946eb23d63 100644 --- a/worlds/minecraft/test/TestEntrances.py +++ b/worlds/minecraft/test/TestEntrances.py @@ -1,7 +1,11 @@ -from .TestMinecraft import TestMinecraft +from . import MCTestBase -class TestEntrances(TestMinecraft): +class TestEntrances(MCTestBase): + options = { + "shuffle_structures": False, + "structure_compasses": False + } def testPortals(self): self.run_entrance_tests([ diff --git a/worlds/minecraft/test/TestMinecraft.py b/worlds/minecraft/test/TestMinecraft.py deleted file mode 100644 index dc5c81c031..0000000000 --- a/worlds/minecraft/test/TestMinecraft.py +++ /dev/null @@ -1,68 +0,0 @@ -from test.TestBase import TestBase -from BaseClasses import MultiWorld, ItemClassification -from worlds import AutoWorld -from worlds.minecraft import MinecraftWorld -from worlds.minecraft.Items import MinecraftItem, item_table -from Options import Toggle -from worlds.minecraft.Options import AdvancementGoal, EggShardsRequired, EggShardsAvailable, BossGoal, BeeTraps, \ - ShuffleStructures, CombatDifficulty - - -# Converts the name of an item into an item object -def MCItemFactory(items, player: int): - ret = [] - singleton = False - if isinstance(items, str): - items = [items] - singleton = True - for item in items: - if item in item_table: - ret.append(MinecraftItem( - item, ItemClassification.progression if item_table[item].progression else ItemClassification.filler, - item_table[item].code, player - )) - else: - raise Exception(f"Unknown item {item}") - - if singleton: - return ret[0] - return ret - - -class TestMinecraft(TestBase): - - def setUp(self): - self.multiworld = MultiWorld(1) - self.multiworld.game[1] = "Minecraft" - self.multiworld.worlds[1] = MinecraftWorld(self.multiworld, 1) - exclusion_pools = ['hard', 'unreasonable', 'postgame'] - for pool in exclusion_pools: - setattr(self.multiworld, f"include_{pool}_advancements", {1: False}) - setattr(self.multiworld, "advancement_goal", {1: AdvancementGoal(30)}) - setattr(self.multiworld, "egg_shards_required", {1: EggShardsRequired(0)}) - setattr(self.multiworld, "egg_shards_available", {1: EggShardsAvailable(0)}) - setattr(self.multiworld, "required_bosses", {1: BossGoal(1)}) # ender dragon - setattr(self.multiworld, "shuffle_structures", {1: ShuffleStructures(False)}) - setattr(self.multiworld, "bee_traps", {1: BeeTraps(0)}) - setattr(self.multiworld, "combat_difficulty", {1: CombatDifficulty(1)}) # normal - setattr(self.multiworld, "structure_compasses", {1: Toggle(False)}) - setattr(self.multiworld, "death_link", {1: Toggle(False)}) - AutoWorld.call_single(self.multiworld, "create_regions", 1) - AutoWorld.call_single(self.multiworld, "generate_basic", 1) - AutoWorld.call_single(self.multiworld, "set_rules", 1) - - def _get_items(self, item_pool, all_except): - if all_except and len(all_except) > 0: - items = self.multiworld.itempool[:] - items = [item for item in items if - item.name not in all_except and not ("Bottle" in item.name and "AnyBottle" in all_except)] - items.extend(MCItemFactory(item_pool[0], 1)) - else: - items = MCItemFactory(item_pool[0], 1) - return self.get_state(items) - - def _get_items_partial(self, item_pool, missing_item): - new_items = item_pool[0].copy() - new_items.remove(missing_item) - items = MCItemFactory(new_items, 1) - return self.get_state(items) diff --git a/worlds/minecraft/test/TestOptions.py b/worlds/minecraft/test/TestOptions.py new file mode 100644 index 0000000000..668ed500e8 --- /dev/null +++ b/worlds/minecraft/test/TestOptions.py @@ -0,0 +1,49 @@ +from . import MCTestBase +from ..Constants import region_info +from ..Options import minecraft_options + +from BaseClasses import ItemClassification + +class AdvancementTestBase(MCTestBase): + options = { + "advancement_goal": minecraft_options["advancement_goal"].range_end + } + # beatability test implicit + +class ShardTestBase(MCTestBase): + options = { + "egg_shards_required": minecraft_options["egg_shards_required"].range_end, + "egg_shards_available": minecraft_options["egg_shards_available"].range_end + } + + # check that itempool is not overfilled with shards + def test_itempool(self): + assert len(self.multiworld.get_unfilled_locations()) == len(self.multiworld.itempool) + +class CompassTestBase(MCTestBase): + def test_compasses_in_pool(self): + structures = [x[1] for x in region_info["default_connections"]] + itempool_str = {item.name for item in self.multiworld.itempool} + for struct in structures: + assert f"Structure Compass ({struct})" in itempool_str + +class NoBeeTestBase(MCTestBase): + options = { + "bee_traps": 0 + } + + # With no bees, there are no traps in the pool + def test_bees(self): + for item in self.multiworld.itempool: + assert item.classification != ItemClassification.trap + + +class AllBeeTestBase(MCTestBase): + options = { + "bee_traps": 100 + } + + # With max bees, there are no filler items, only bee traps + def test_bees(self): + for item in self.multiworld.itempool: + assert item.classification != ItemClassification.filler diff --git a/worlds/minecraft/test/__init__.py b/worlds/minecraft/test/__init__.py index e69de29bb2..acf9b79491 100644 --- a/worlds/minecraft/test/__init__.py +++ b/worlds/minecraft/test/__init__.py @@ -0,0 +1,33 @@ +from test.TestBase import TestBase, WorldTestBase +from .. import MinecraftWorld + + +class MCTestBase(WorldTestBase, TestBase): + game = "Minecraft" + player: int = 1 + + def _create_items(self, items, player): + singleton = False + if isinstance(items, str): + items = [items] + singleton = True + ret = [self.multiworld.worlds[player].create_item(item) for item in items] + if singleton: + return ret[0] + return ret + + def _get_items(self, item_pool, all_except): + if all_except and len(all_except) > 0: + items = self.multiworld.itempool[:] + items = [item for item in items if item.name not in all_except] + items.extend(self._create_items(item_pool[0], 1)) + else: + items = self._create_items(item_pool[0], 1) + return self.get_state(items) + + def _get_items_partial(self, item_pool, missing_item): + new_items = item_pool[0].copy() + new_items.remove(missing_item) + items = self._create_items(new_items, 1) + return self.get_state(items) + diff --git a/worlds/oot/HintList.py b/worlds/oot/HintList.py index 556e165184..b0f20858e7 100644 --- a/worlds/oot/HintList.py +++ b/worlds/oot/HintList.py @@ -50,7 +50,7 @@ def getHint(item, clearer_hint=False): return Hint(item, clearText, hintType) else: return Hint(item, textOptions, hintType) - elif type(item) is str: + elif isinstance(item, str): return Hint(item, item, 'generic') else: # is an Item return Hint(item.name, item.hint_text, 'item') diff --git a/worlds/oot/Options.py b/worlds/oot/Options.py index 205938263c..35b477ae58 100644 --- a/worlds/oot/Options.py +++ b/worlds/oot/Options.py @@ -1045,6 +1045,11 @@ class AdultTradeStart(OptionSet): "Claim Check", } + def __init__(self, value: typing.Iterable[str]): + if not value: + value = self.default + super().__init__(value) + itempool_options: typing.Dict[str, type(Option)] = { "item_pool_value": ItemPoolValue, diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index 20b3ccb02d..cae67e1e65 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -85,7 +85,16 @@ class OOTWeb(WebWorld): setup.authors ) - tutorials = [setup, setup_es] + setup_fr = Tutorial( + setup.tutorial_name, + setup.description, + "Français", + "setup_fr.md", + "setup/fr", + ["TheLynk"] + ) + + tutorials = [setup, setup_es, setup_fr] class OOTWorld(World): diff --git a/worlds/oot/docs/setup_fr.md b/worlds/oot/docs/setup_fr.md new file mode 100644 index 0000000000..6248f8c44b --- /dev/null +++ b/worlds/oot/docs/setup_fr.md @@ -0,0 +1,422 @@ +# Guide d'installation Archipelago pour Ocarina of Time + +## Important + +Comme nous utilisons Bizhawk, ce guide ne s'applique qu'aux systèmes Windows et Linux. + +## Logiciel requis + +- Bizhawk : [Bizhawk sort de TASVideos] (https://tasvideos.org/BizHawk/ReleaseHistory) + - Les versions 2.3.1 et ultÊrieures sont prises en charge. La version 2.7 est recommandÊe pour la stabilitÊ. + - Des instructions d'installation dÊtaillÊes pour Bizhawk peuvent ÃĒtre trouvÊes sur le lien ci-dessus. + - Les utilisateurs Windows doivent d'abord exÊcuter le programme d'installation prereq, qui peut Êgalement ÃĒtre trouvÊ sur le lien ci-dessus. +- Le client Archipelago intÊgrÊ, qui peut ÃĒtre installÊ [ici](https://github.com/ArchipelagoMW/Archipelago/releases) + (sÊlectionnez `Ocarina of Time Client` lors de l'installation). +- Une ROM Ocarina of Time v1.0. + +## Configuration de Bizhawk + +Une fois Bizhawk installÊ, ouvrez Bizhawk et modifiez les paramètres suivants : + +- Allez dans Config > Personnaliser. Basculez vers l'onglet AvancÊ, puis basculez le Lua Core de "NLua+KopiLua" vers + "Interface Lua+Lua". RedÊmarrez ensuite Bizhawk. Ceci est nÊcessaire pour que le script Lua fonctionne correctement. + **REMARQUE : MÃĒme si "Lua+LuaInterface" est dÊjà sÊlectionnÊ, basculez entre les deux options et resÊlectionnez-le. Nouvelles installations** + ** des versions plus rÊcentes de Bizhawk ont tendance à afficher "Lua+LuaInterface" comme option sÊlectionnÊe par dÊfaut mais se chargent toujours ** + **"NLua+KopiLua" jusqu'à ce que cette Êtape soit terminÊe.** +- Sous Config > Personnaliser > AvancÊ, assurez-vous que la case pour AutoSaveRAM est cochÊe et cliquez sur le bouton 5s. + Cela rÊduit la possibilitÊ de perdre des donnÊes de sauvegarde en cas de plantage de l'Êmulateur. +- Sous Config > Personnaliser, cochez les cases "ExÊcuter en arrière-plan" et "Accepter la saisie en arrière-plan". Cela vous permettra de + continuer à jouer en arrière-plan, mÃĒme si une autre fenÃĒtre est sÊlectionnÊe. +- Sous Config> Raccourcis clavier, de nombreux raccourcis clavier sont rÊpertoriÊs, dont beaucoup sont liÊs aux touches communes du clavier. Vous voudrez probablement + dÊsactiver la plupart d'entre eux, ce que vous pouvez faire rapidement en utilisant `Esc`. +- Si vous jouez avec une manette, lorsque vous liez les commandes, dÊsactivez "P1 A Up", "P1 A Down", "P1 A Left" et "P1 A Right" + car ceux-ci interfèrent avec la visÊe s'ils sont liÊs. DÊfinissez l'entrÊe directionnelle à l'aide de l'onglet Analogique à la place. +- Sous N64, activez "Utiliser l'emplacement d'extension". Ceci est nÊcessaire pour que les sauvegardes fonctionnent. + (Le menu N64 n'apparaÃŽt qu'après le chargement d'une ROM.) + +Il est fortement recommandÊ d'associer les extensions de rom N64 (\*.n64, \*.z64) au Bizhawk que nous venons d'installer. +Pour ce faire, nous devons simplement rechercher n'importe quelle rom N64 que nous possÊdons, faire un clic droit et sÊlectionner "Ouvrir avec ...", dÊpliez +la liste qui apparaÃŽt et sÊlectionnez l'option du bas "Rechercher une autre application", puis naviguez jusqu'au dossier Bizhawk +et sÊlectionnez EmuHawk.exe. + +Un guide de configuration Bizhawk alternatif ainsi que divers conseils de dÊpannage peuvent ÃĒtre trouvÊs +[ici](https://wiki.ootrandomizer.com/index.php?title=Bizhawk). + +## Configuration de votre fichier YAML + +### Qu'est-ce qu'un fichier YAML et pourquoi en ai-je besoin ? + +Votre fichier YAML contient un ensemble d'options de configuration qui fournissent au gÊnÊrateur des informations sur la façon dont il doit +gÊnÊrer votre jeu. Chaque joueur d'un multimonde fournira son propre fichier YAML. Cette configuration permet à chaque joueur de profiter +d'une expÊrience personnalisÊe à leur goÃģt, et diffÊrents joueurs dans le mÃĒme multimonde peuvent tous avoir des options diffÊrentes. + +### OÚ puis-je obtenir un fichier YAML ? + +Un yaml OoT de base ressemblera à ceci. Il y a beaucoup d'options cosmÊtiques qui ont ÊtÊ supprimÊes pour le plaisir de ce +tutoriel, si vous voulez voir une liste complète, tÊlÊchargez Archipelago depuis +la [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) et recherchez l'exemple de fichier dans +le dossier "Lecteurs". + +``` yaml +description: Modèle par dÊfaut d'Ocarina of Time # UtilisÊ pour dÊcrire votre yaml. Utile si vous avez plusieurs fichiers +# Votre nom dans le jeu. Les espaces seront remplacÊs par des underscores et il y a une limite de 16 caractères +name: VotreNom +game: + Ocarina of Time: 1 +requires: + version: 0.1.7 # Version d'Archipelago requise pour que ce yaml fonctionne comme prÊvu. +# Options partagÊes prises en charge par tous les jeux : +accessibility: + items: 0 # Garantit que vous pourrez acquÊrir tous les articles, mais vous ne pourrez peut-ÃĒtre pas accÊder à tous les emplacements + locations: 50 # Garantit que vous pourrez accÊder à tous les emplacements, et donc à tous les articles + none: 0 # Garantit seulement que le jeu est battable. Vous ne pourrez peut-ÃĒtre pas accÊder à tous les emplacements ou acquÊrir tous les objets +progression_balancing: # Un système pour rÊduire le BK, comme dans les pÊriodes oÚ vous ne pouvez rien faire, en dÊplaçant vos ÊlÊments dans une sphère d'accès antÊrieure + 0: 0 # Choisissez un nombre infÊrieur si cela ne vous dÊrange pas d'avoir un multimonde plus long, ou si vous pouvez glitch / faire du hors logique. + 25: 0 + 50: 50 # Faites en sorte que vous ayez probablement des choses à faire. + 99: 0 # Obtenez les ÊlÊments importants tôt et restez en tÃĒte de la progression. +Ocarina of Time: + logic_rules: # dÊfinit la logique utilisÊe pour le gÊnÊrateur. + glitchless: 50 + glitched: 0 + no_logic: 0 + logic_no_night_tokens_without_suns_song: # Les skulltulas nocturnes nÊcessiteront logiquement le Chant du soleil. + false: 50 + true: 0 + open_forest: # DÊfinissez l'Êtat de la forÃĒt de Kokiri et du chemin vers l'arbre Mojo. + open: 50 + closed_deku: 0 + closed: 0 + open_kakariko: # DÊfinit l'Êtat de la porte du village de Kakariko. + open: 50 + zelda: 0 + closed: 0 + open_door_of_time: # Ouvre la Porte du Temps par dÊfaut, sans le Chant du Temps. + false: 0 + true: 50 + zora_fountain: # DÊfinit l'Êtat du roi Zora, bloquant le chemin vers la fontaine de Zora. + open: 0 + adult: 0 + closed: 50 + gerudo_fortress: # DÊfinit les conditions d'accès à la forteresse Gerudo. + normal: 0 + fast: 50 + open: 0 + bridge: # DÊfinit les exigences pour le pont arc-en-ciel. + open: 0 + vanilla: 0 + stones: 0 + medallions: 50 + dungeons: 0 + tokens: 0 + trials: # DÊfinit le nombre d'Êpreuves requises dans le ChÃĸteau de Ganon. + # vous pouvez ajouter des valeurs supplÊmentaires entre minimum et maximum + 0: 50 # valeur minimale + 6: 0 # valeur maximale + random: 0 + random-low: 0 + random-higt: 0 + starting_age: # Choisissez l'Ãĸge auquel Link commencera. + child: 50 + adult: 0 + triforce_hunt: # Rassemblez des morceaux de la Triforce dispersÊs dans le monde entier pour terminer le jeu. + false: 50 + true: 0 + triforce_goal: # Nombre de pièces Triforce nÊcessaires pour terminer le jeu. Nombre total placÊ dÊterminÊ par le paramètre Item Pool. + # vous pouvez ajouter des valeurs supplÊmentaires entre minimum et maximum + 1: 0 # valeur minimale + 50: 0 # valeur maximale + random: 0 + random-low: 0 + random-higt: 0 + 20: 50 + bombchus_in_logic: # Les Bombchus sont correctement pris en compte dans la logique. Le premier pack trouvÊ aura 20 chus ; Kokiri Shop et Bazaar vendent des recharges ; bombchus ouvre Bombchu Bowling. + false: 50 + true: 0 + bridge_stones: # DÊfinissez le nombre de pierres spirituelles requises pour le pont arc-en-ciel. + # vous pouvez ajouter des valeurs supplÊmentaires entre minimum et maximum + 0: 0 # valeur minimale + 3: 50 # valeur maximale + random: 0 + random-low: 0 + random-high: 0 + bridge_medallions: # DÊfinissez le nombre de mÊdaillons requis pour le pont arc-en-ciel. + # vous pouvez ajouter des valeurs supplÊmentaires entre minimum et maximum + 0: 0 # valeur minimale + 6: 50 # valeur maximale + random: 0 + random-low: 0 + random-high: 0 + bridge_rewards: # DÊfinissez le nombre de rÊcompenses de donjon requises pour le pont arc-en-ciel. + # vous pouvez ajouter des valeurs supplÊmentaires entre minimum et maximum + 0: 0 # valeur minimale + 9: 50 # valeur maximale + random: 0 + random-low: 0 + random-high: 0 + bridge_tokens: # DÊfinissez le nombre de jetons Gold Skulltula requis pour le pont arc-en-ciel. + # vous pouvez ajouter des valeurs supplÊmentaires entre minimum et maximum + 0: 0 # valeur minimale + 100: 50 # valeur maximale + random: 0 + random-low: 0 + random-high: 0 + shuffle_mapcompass: # Contrôle oÚ mÊlanger les cartes et boussoles des donjons. + remove: 0 + startwith: 50 + vanilla: 0 + dungeon: 0 + overworld: 0 + any_dungeon: 0 + keysanity: 0 + shuffle_smallkeys: # Contrôle oÚ mÊlanger les petites clÊs de donjon. + remove: 0 + vanilla: 0 + dungeon: 50 + overworld: 0 + any_dungeon: 0 + keysanity: 0 + shuffle_hideoutkeys: # Contrôle oÚ mÊlanger les petites clÊs de la Forteresse Gerudo. + vanilla: 50 + overworld: 0 + any_dungeon: 0 + keysanity: 0 + shuffle_bosskeys: # Contrôle oÚ mÊlanger les clÊs du boss, à l'exception de la clÊ du boss du chÃĸteau de Ganon. + remove: 0 + vanilla: 0 + dungeon: 50 + overworld: 0 + any_dungeon: 0 + keysanity: 0 + shuffle_ganon_bosskey: # Contrôle oÚ mÊlanger la clÊ du patron du chÃĸteau de Ganon. + remove: 50 + vanilla: 0 + dungeon: 0 + overworld: 0 + any_dungeon: 0 + keysanity: 0 + on_lacs: 0 + enhance_map_compass: # La carte indique si un donjon est vanille ou MQ. La boussole indique quelle est la rÊcompense du donjon. + false: 50 + true: 0 + lacs_condition: # DÊfinissez les exigences pour la cinÊmatique de la Flèche lumineuse dans le Temple du temps. + vanilla: 50 + stones: 0 + medallions: 0 + dungeons: 0 + tokens: 0 + lacs_stones: # DÊfinissez le nombre de pierres spirituelles requises pour le LACS. + # vous pouvez ajouter des valeurs supplÊmentaires entre minimum et maximum + 0: 0 # valeur minimale + 3: 50 # valeur maximale + random: 0 + random-low: 0 + random-high: 0 + lacs_medallions: # DÊfinissez le nombre de mÊdaillons requis pour LACS. + # vous pouvez ajouter des valeurs supplÊmentaires entre minimum et maximum + 0: 0 # valeur minimale + 6: 50 # valeur maximale + random: 0 + random-low: 0 + random-high: 0 + lacs_rewards: # DÊfinissez le nombre de rÊcompenses de donjon requises pour LACS. + # vous pouvez ajouter des valeurs supplÊmentaires entre minimum et maximum + 0: 0 # valeur minimale + 9: 50 # valeur maximale + random: 0 + random-low: 0 + random-high: 0 + lacs_tokens: # DÊfinissez le nombre de jetons Gold Skulltula requis pour le LACS. + # vous pouvez ajouter des valeurs supplÊmentaires entre minimum et maximum + 0: 0 # valeur minimale + 100: 50 # valeur maximale + random: 0 + random-low: 0 + random-high: 0 + shuffle_song_items: # DÊfinit oÚ les chansons peuvent apparaÃŽtre. + song: 50 + dungeon: 0 + any: 0 + shopsanity: # Randomise le contenu de la boutique. RÊglez sur "off" pour ne pas mÊlanger les magasins ; "0" mÊlange les magasins mais ne n'autorise pas les articles multimonde dans les magasins. + 0: 0 + 1: 0 + 2: 0 + 3: 0 + 4: 0 + random_value: 0 + off: 50 + tokensanity : # les rÊcompenses en jetons des Skulltulas dorÊes sont mÊlangÊes dans la rÊserve. + off: 50 + dungeons: 0 + overworld: 0 + all: 0 + shuffle_scrubs: # MÊlangez les articles vendus par Business Scrubs et fixez les prix. + off: 50 + low: 0 + regular: 0 + random_prices: 0 + shuffle_cows: # les vaches donnent des objets lorsque la chanson d'Epona est jouÊe. + false: 50 + true: 0 + shuffle_kokiri_sword: # MÊlangez l'ÊpÊe Kokiri dans la rÊserve d'objets. + false: 50 + true: 0 + shuffle_ocarinas: # MÊlangez l'Ocarina des fÊes et l'Ocarina du temps dans la rÊserve d'objets. + false: 50 + true: 0 + shuffle_weird_egg: # MÊlangez l'œuf bizarre de Malon au chÃĸteau d'Hyrule. + false: 50 + true: 0 + shuffle_gerudo_card: # MÊlangez la carte de membre Gerudo dans la rÊserve d'objets. + false: 50 + true: 0 + shuffle_beans: # Ajoute un paquet de 10 haricots au pool d'objets et change le vendeur de haricots pour qu'il vende un objet pour 60 roupies. + false: 50 + true: 0 + shuffle_medigoron_carpet_salesman: # MÊlangez les objets vendus par Medigoron et le vendeur de tapis Haunted Wasteland. + false: 50 + true: 0 + skip_child_zelda: # le jeu commence avec la lettre de Zelda, l'objet de la berceuse de Zelda et les ÊvÊnements pertinents dÊjà terminÊs. + false: 50 + true: 0 + no_escape_sequence: # Ignore la sÊquence d'effondrement de la tour entre les combats de Ganondorf et de Ganon. + false: 50 + true: 0 + no_guard_stealth: # Le vide sanitaire du chÃĸteau d'Hyrule passe directement à Zelda. + false: 50 + true: 0 + no_epona_race: # Epona peut toujours ÃĒtre invoquÊe avec Epona's Song. + false: 50 + true: 0 + skip_some_minigame_phases: # Dampe Race et Horseback Archery donnent les deux rÊcompenses si la deuxième condition est remplie lors de la première tentative. + false: 50 + true: 0 + complete_mask_quest: # Tous les masques sont immÊdiatement disponibles à l'emprunt dans la boutique Happy Mask. + false: 50 + true: 0 + useful_cutscenes: # RÊactive la cinÊmatique Poe dans le Temple de la forÃĒt, Darunia dans le Temple du feu et l'introduction de Twinrova. Surtout utile pour les pÊpins. + false: 50 + true: 0 + fast_chests: # Toutes les animations des coffres sont rapides. Si dÊsactivÊ, les ÊlÊments principaux ont une animation lente. + false: 50 + true: 0 + free_scarecrow: # Sortir l'ocarina près d'un point d'Êpouvantail fait apparaÃŽtre Pierre sans avoir besoin de la chanson. + false: 50 + true: 0 + fast_bunny_hood: # Bunny Hood vous permet de vous dÊplacer 1,5 fois plus vite comme dans Majora's Mask. + false: 50 + true: 0 + chicken_count: # Contrôle le nombre de Cuccos pour qu'Anju donne un objet en tant qu'enfant. + \# vous pouvez ajouter des valeurs supplÊmentaires entre le minimum et le maximum + 0: 0 # valeur minimale + 7: 50 # valeur maximale + random: 0 + random-low: 0 + random-high: 0 + hints: # les pierres à potins peuvent donner des indices sur l'emplacement des objets. + none: 0 + mask: 0 + agony: 0 + always: 50 + hint_dist: # Choisissez la distribution d'astuces à utiliser. Affecte la frÊquence des indices forts, quels ÊlÊments sont toujours indiquÊs, etc. + balanced: 50 + ddr: 0 + league: 0 + mw2: 0 + scrubs: 0 + strong: 0 + tournament: 0 + useless: 0 + very_strong: 0 + text_shuffle: # Randomise le texte dans le jeu pour un effet comique. + none: 50 + except_hints: 0 + complete: 0 + damage_multiplier: # contrôle la quantitÊ de dÊgÃĸts subis par Link. + half: 0 + normal: 50 + double: 0 + quadruple: 0 + ohko: 0 + no_collectible_hearts: # les cœurs ne tomberont pas des ennemis ou des objets. + false: 50 + true: 0 + starting_tod: # Changer l'heure de dÊbut de la journÊe. + default: 50 + sunrise: 0 + morning: 0 + noon: 0 + afternoon: 0 + sunset: 0 + evening: 0 + midnight: 0 + witching_hour: 0 + start_with_consumables: # DÊmarrez le jeu avec des Deku Sticks et des Deku Nuts pleins. + false: 50 + true: 0 + start_with_rupees: # Commencez avec un portefeuille plein. Les mises à niveau de portefeuille rempliront Êgalement votre portefeuille. + false: 50 + true: 0 + item_pool_value: # modifie le nombre d'objets disponibles dans le jeu. + plentiful: 0 + balanced: 50 + scarce: 0 + minimal: 0 + junk_ice_traps: # Ajoute des pièges à glace au pool d'objets. + off: 0 + normal: 50 + on: 0 + mayhem: 0 + onslaught: 0 + ice_trap_appearance: # modifie l'apparence des pièges à glace en tant qu'ÊlÊments autonomes. + major_only: 50 + junk_only: 0 + anything: 0 + logic_earliest_adult_trade: # premier ÊlÊment pouvant apparaÃŽtre dans la sÊquence d'Êchange pour adultes. + pocket_egg: 0 + pocket_cucco: 0 + cojiro: 0 + odd_mushroom: 0 + poachers_saw: 0 + broken_sword: 0 + prescription: 50 + eyeball_frog: 0 + eyedrops: 0 + claim_check: 0 + logic_latest_adult_trade: # Dernier ÊlÊment pouvant apparaÃŽtre dans la sÊquence d'Êchange pour adultes. + pocket_egg: 0 + pocket_cucco: 0 + cojiro: 0 + odd_mushroom: 0 + poachers_saw: 0 + broken_sword: 0 + prescription: 0 + eyeball_frog: 0 + eyedrops: 0 + claim_check: 50 + +``` + +## Rejoindre une partie MultiWorld + +### Obtenez votre fichier de correctif OOT + +Lorsque vous rejoignez un jeu multimonde, il vous sera demandÊ de fournir votre fichier YAML à l'hÊbergeur. Une fois que c'est Fini, +l'hÊbergeur vous fournira soit un lien pour tÊlÊcharger votre fichier de donnÊes, soit un fichier zip contenant les donnÊes de chacun +des dossiers. Votre fichier de donnÊes doit avoir une extension `.apz5`. + +Double-cliquez sur votre fichier `.apz5` pour dÊmarrer votre client et dÊmarrer le processus de patch ROM. Une fois le processus terminÊ +(cela peut prendre un certain temps), le client et l'Êmulateur seront lancÊs automatiquement (si vous avez associÊ l'extension +à l'Êmulateur comme recommandÊ). + +### Connectez-vous au multiserveur + +Une fois le client et l'Êmulateur dÊmarrÊs, vous devez les connecter. Dans l'Êmulateur, cliquez sur "Outils" +menu et sÊlectionnez "Console Lua". Cliquez sur le bouton du dossier ou appuyez sur Ctrl+O pour ouvrir un script Lua. + +AccÊdez à votre dossier d'installation Archipelago et ouvrez `data/lua/OOT/oot_connector.lua`. + +Pour connecter le client au multiserveur, mettez simplement `:` dans le champ de texte en haut et appuyez sur EntrÊe (si le +le serveur utilise un mot de passe, saisissez dans le champ de texte infÊrieur `/connect : [mot de passe]`) + +Vous ÃĒtes maintenant prÃĒt à commencer votre aventure à Hyrule. \ No newline at end of file diff --git a/worlds/oribf/__init__.py b/worlds/oribf/__init__.py index 02350917a3..854025a8ed 100644 --- a/worlds/oribf/__init__.py +++ b/worlds/oribf/__init__.py @@ -13,6 +13,7 @@ class OriBlindForest(World): game: str = "Ori and the Blind Forest" topology_present = True + data_version = 1 item_name_to_id = item_table location_name_to_id = lookup_name_to_id diff --git a/worlds/overcooked2/Logic.py b/worlds/overcooked2/Logic.py index 52bff89d21..b86dc86d3f 100644 --- a/worlds/overcooked2/Logic.py +++ b/worlds/overcooked2/Logic.py @@ -1,22 +1,17 @@ from BaseClasses import CollectionState -from .Overcooked2Levels import Overcooked2GenericLevel, Overcooked2Dlc, Overcooked2Level +from .Overcooked2Levels import Overcooked2GenericLevel, Overcooked2Dlc, Overcooked2Level, OverworldRegion, overworld_region_by_level from typing import Dict from random import Random - def has_requirements_for_level_access(state: CollectionState, level_name: str, previous_level_completed_event_name: str, - required_star_count: int, player: int) -> bool: - # Check if the ramps in the overworld are set correctly - if level_name in ramp_logic: - (ramp_reqs, level_reqs) = ramp_logic[level_name] + required_star_count: int, allow_ramp_tricks: bool, player: int) -> bool: - for req in level_reqs: - if not state.has(req + " Level Complete", player): - return False # This level needs another to be beaten first - - for req in ramp_reqs: - if not state.has(req + " Ramp", player): - return False # The player doesn't have the pre-requisite ramp button + # Must have correct ramp buttons and pre-requisite levels, or tricks to sequence break + overworld_region = overworld_region_by_level[level_name] + overworld_logic = overworld_region_logic[overworld_region] + visited = list() + if not overworld_logic(state, player, allow_ramp_tricks, visited): + return False # Kevin Levels Need to have the corresponding items if level_name.startswith("K"): @@ -81,8 +76,9 @@ def is_item_progression(item_name, level_mapping, include_kevin): if item_name.endswith("Emote"): return False - if "Kevin" in item_name or "Ramp" in item_name: - return True # always progression + for item_identifier in ["Kevin", "Ramp", "Dash"]: + if item_identifier in item_name: + return True # These things are always progression because they can have overworld implications def item_in_logic(shortname, _item_name): for star in range(0, 3): @@ -214,28 +210,128 @@ def is_completable_no_items(level: Overcooked2GenericLevel) -> bool: return len(exclusive) == 0 and len(additive) == 0 +def can_reach_main(state: CollectionState, player: int, allow_tricks: bool, visited: list) -> bool: + if OverworldRegion.main in visited: + return False + visited.append(OverworldRegion.main) -# If key missing, doesn't require a ramp to access (or the logic is handled by a preceeding level) -# -# If empty, a ramp is required to access, but the ramp button is garunteed accessible -# -# If populated, ramp(s) are required to access and the button requires all levels in the -# list to be compelted before it can be pressed -# -ramp_logic = { - "1-5": (["Yellow"], []), - "2-2": (["Green"], []), - "3-1": (["Blue"], []), - "5-2": (["Purple"], []), - "6-1": (["Pink"], []), - "6-2": (["Red", "Purple"], ["5-1"]), # 5-1 spawns blue button, blue button gets you to red button - "Kevin-1": (["Dark Green"], []), - "Kevin-7": (["Purple"], ["5-1"]), # 5-1 spawns blue button, - # press blue button, - # climb blue ramp, - # jump the gap, - # climb wood ramps - "Kevin-8": (["Red", "Blue"], ["5-1", "6-2"]), # Same as above, but 6-2 spawns the ramp to K8 + return True + +def can_reach_yellow_island(state: CollectionState, player: int, allow_tricks: bool, visited: list) -> bool: + if OverworldRegion.yellow_island in visited: + return False + visited.append(OverworldRegion.yellow_island) + + return state.has("Yellow Ramp", player) + +def can_reach_dark_green_mountain(state: CollectionState, player: int, allow_tricks: bool, visited: list) -> bool: + if OverworldRegion.dark_green_mountain in visited: + return False + visited.append(OverworldRegion.dark_green_mountain) + + return state.has_all({"Dark Green Ramp", "Kevin-1"}, player) + +def can_reach_out_of_bounds(state: CollectionState, player: int, allow_tricks: bool, visited: list) -> bool: + if OverworldRegion.out_of_bounds in visited: + return False + visited.append(OverworldRegion.out_of_bounds) + + return allow_tricks and state.has("Progressive Dash", player) and can_reach_dark_green_mountain(state, player, allow_tricks, visited) + +def can_reach_stonehenge_mountain(state: CollectionState, player: int, allow_tricks: bool, visited: list) -> bool: + if OverworldRegion.stonehenge_mountain in visited: + return False + visited.append(OverworldRegion.stonehenge_mountain) + + if state.has("Blue Ramp", player): + return True + + if can_reach_out_of_bounds(state, player, allow_tricks, visited): + return True + + return False + +def can_reach_sky_shelf(state: CollectionState, player: int, allow_tricks: bool, visited: list) -> bool: + if OverworldRegion.sky_shelf in visited: + return False + visited.append(OverworldRegion.sky_shelf) + + if state.has("Green Ramp", player): + return True + + if state.has_all({"5-1 Level Complete", "Purple Ramp"}, player): + return True + + if allow_tricks and can_reach_pink_island(state, player, allow_tricks, visited) and state.has("Progressive Dash", player): + return True + + if can_reach_tip_of_the_map(state, player, allow_tricks, visited): + return True + + return False + +def can_reach_pink_island(state: CollectionState, player: int, allow_tricks: bool, visited: list) -> bool: + if OverworldRegion.pink_island in visited: + return False + visited.append(OverworldRegion.pink_island) + + if state.has("Pink Ramp", player): + return True + + if allow_tricks and state.has("Progressive Dash", player) and can_reach_sky_shelf(state, player, allow_tricks, visited): + return True + + return False + +def can_reach_tip_of_the_map(state: CollectionState, player: int, allow_tricks: bool, visited: list) -> bool: + if OverworldRegion.tip_of_the_map in visited: + return False + visited.append(OverworldRegion.tip_of_the_map) + + if state.has_all({"5-1 Level Complete", "Purple Ramp"}, player): + return True + + if can_reach_out_of_bounds(state, player, allow_tricks, visited): + return True + + if allow_tricks and can_reach_sky_shelf(state, player, allow_tricks, visited): + return True + + return False + +def can_reach_mars_shelf(state: CollectionState, player: int, allow_tricks: bool, visited: list) -> bool: + if OverworldRegion.mars_shelf in visited: + return False + visited.append(OverworldRegion.mars_shelf) + + tip_of_the_map = can_reach_tip_of_the_map(state, player, allow_tricks, visited) + + if tip_of_the_map and allow_tricks: + return True + + if tip_of_the_map and state.has_all({"6-1 Level Complete", "Red Ramp"}, player): + return True + + return False + +def can_reach_kevin_eight_island(state: CollectionState, player: int, allow_tricks: bool, visited: list) -> bool: + if OverworldRegion.kevin_eight_island in visited: + return False + visited.append(OverworldRegion.kevin_eight_island) + + return can_reach_mars_shelf(state, player, allow_tricks, visited) + + +overworld_region_logic = { + OverworldRegion.main : can_reach_main , + OverworldRegion.yellow_island : can_reach_yellow_island , + OverworldRegion.sky_shelf : can_reach_sky_shelf , + OverworldRegion.stonehenge_mountain: can_reach_stonehenge_mountain, + OverworldRegion.tip_of_the_map : can_reach_tip_of_the_map , + OverworldRegion.pink_island : can_reach_pink_island , + OverworldRegion.mars_shelf : can_reach_mars_shelf , + OverworldRegion.dark_green_mountain: can_reach_dark_green_mountain, + OverworldRegion.kevin_eight_island : can_reach_kevin_eight_island , } horde_logic = { # Additive diff --git a/worlds/overcooked2/Options.py b/worlds/overcooked2/Options.py index 400796af59..cb4e43d25d 100644 --- a/worlds/overcooked2/Options.py +++ b/worlds/overcooked2/Options.py @@ -1,6 +1,6 @@ from enum import IntEnum from typing import TypedDict -from Options import DefaultOnToggle, Range, Choice +from Options import Toggle, DefaultOnToggle, Range, Choice class LocationBalancingMode(IntEnum): @@ -21,8 +21,14 @@ class OC2OnToggle(DefaultOnToggle): return bool(self.value) +class OC2Toggle(Toggle): + @property + def result(self) -> bool: + return bool(self.value) + + class LocationBalancing(Choice): - """Location balancing affects the density of progression items found in your world relative to other wordlds. This setting changes nothing for solo games. + """Location balancing affects the density of progression items found in your world relative to other worlds. This setting changes nothing for solo games. - Disabled: Location density in your world can fluctuate greatly depending on the settings of other players. In extreme cases, your world may be entirely populated with filler items @@ -36,9 +42,13 @@ class LocationBalancing(Choice): option_full = LocationBalancingMode.full.value default = LocationBalancingMode.compromise.value +class RampTricks(OC2Toggle): + """If enabled, generated games may require sequence breaks on the overworld map. This includes crossing small gaps and escaping out of bounds.""" + display_name = "Overworld Tricks" + class DeathLink(Choice): - """DeathLink is an opt-in feature for Multiworlds where individual death events are propogated to all games with DeathLink enabled. + """DeathLink is an opt-in feature for Multiworlds where individual death events are propagated to all games with DeathLink enabled. - Disabled: Death will behave as it does in the original game. @@ -66,7 +76,7 @@ class AlwaysPreserveCookingProgress(OC2OnToggle): display_name = "Preserve Cooking/Mixing Progress" -class DisplayLeaderboardScores(OC2OnToggle): +class DisplayLeaderboardScores(OC2Toggle): """Modifies the Overworld map to fetch and display the current world records for each level. Press number keys 1-4 to view leaderboard scores for that number of players.""" display_name = "Display Leaderboard Scores" @@ -78,7 +88,7 @@ class ShuffleLevelOrder(OC2OnToggle): class IncludeHordeLevels(OC2OnToggle): - """Includes "Horde Defence" levels in the pool of possible kitchens when Shuffle Level Order is enabled. Also adds + """Includes "Horde Defense" levels in the pool of possible kitchens when Shuffle Level Order is enabled. Also adds two horde-specific items into the item pool.""" display_name = "Include Horde Levels" @@ -109,7 +119,7 @@ class ShorterLevelDuration(OC2OnToggle): class ShortHordeLevels(OC2OnToggle): """Modifies horde levels to contain roughly 1/3rd fewer waves than in the original game. - The kitchen's health is sacled appropriately to preserve the same approximate difficulty.""" + The kitchen's health is scaled appropriately to preserve the same approximate difficulty.""" display_name = "Shorter Horde Levels" @@ -153,6 +163,7 @@ class StarThresholdScale(Range): overcooked_options = { # generator options "location_balancing": LocationBalancing, + "ramp_tricks": RampTricks, # deathlink "deathlink": DeathLink, diff --git a/worlds/overcooked2/Overcooked2Levels.py b/worlds/overcooked2/Overcooked2Levels.py index 007be13c9e..816e1e514a 100644 --- a/worlds/overcooked2/Overcooked2Levels.py +++ b/worlds/overcooked2/Overcooked2Levels.py @@ -372,3 +372,62 @@ level_id_to_shortname = { (Overcooked2Dlc.SEASONAL , 30 ): "Moon 1-4" , (Overcooked2Dlc.SEASONAL , 31 ): "Moon 1-5" , } + +class OverworldRegion(IntEnum): + main = 0 + yellow_island = 1 + sky_shelf = 2 + stonehenge_mountain = 3 + tip_of_the_map = 4 + pink_island = 5 + mars_shelf = 6 + dark_green_mountain = 7 + kevin_eight_island = 8 + out_of_bounds = 9 + +overworld_region_by_level = { + "1-1": OverworldRegion.main, + "1-2": OverworldRegion.main, + "1-3": OverworldRegion.main, + "1-4": OverworldRegion.main, + "1-5": OverworldRegion.yellow_island, + "1-6": OverworldRegion.yellow_island, + "2-1": OverworldRegion.main, + "2-2": OverworldRegion.sky_shelf, + "2-3": OverworldRegion.sky_shelf, + "2-4": OverworldRegion.main, + "2-5": OverworldRegion.main, + "2-6": OverworldRegion.main, + "3-1": OverworldRegion.stonehenge_mountain, + "3-2": OverworldRegion.stonehenge_mountain, + "3-3": OverworldRegion.stonehenge_mountain, + "3-4": OverworldRegion.stonehenge_mountain, + "3-5": OverworldRegion.stonehenge_mountain, + "3-6": OverworldRegion.main, + "4-1": OverworldRegion.main, + "4-2": OverworldRegion.main, + "4-3": OverworldRegion.main, + "4-4": OverworldRegion.main, + "4-5": OverworldRegion.main, + "4-6": OverworldRegion.main, + "5-1": OverworldRegion.main, + "5-2": OverworldRegion.sky_shelf, + "5-3": OverworldRegion.main, + "5-4": OverworldRegion.tip_of_the_map, + "5-5": OverworldRegion.tip_of_the_map, + "5-6": OverworldRegion.tip_of_the_map, + "6-1": OverworldRegion.pink_island, + "6-2": OverworldRegion.tip_of_the_map, + "6-3": OverworldRegion.tip_of_the_map, + "6-4": OverworldRegion.sky_shelf, + "6-5": OverworldRegion.mars_shelf, + "6-6": OverworldRegion.mars_shelf, + "Kevin-1": OverworldRegion.dark_green_mountain, + "Kevin-2": OverworldRegion.main, + "Kevin-3": OverworldRegion.main, + "Kevin-4": OverworldRegion.main, + "Kevin-5": OverworldRegion.main, + "Kevin-6": OverworldRegion.main, + "Kevin-7": OverworldRegion.tip_of_the_map, + "Kevin-8": OverworldRegion.kevin_eight_island, +} diff --git a/worlds/overcooked2/__init__.py b/worlds/overcooked2/__init__.py index 63d87648e1..d28fc23947 100644 --- a/worlds/overcooked2/__init__.py +++ b/worlds/overcooked2/__init__.py @@ -249,8 +249,9 @@ class Overcooked2World(World): self.level_mapping = None def set_location_priority(self) -> None: + priority_locations = self.get_priority_locations() for level in Overcooked2Level(): - if level.level_id in self.get_priority_locations(): + if level.level_id in priority_locations: location: Location = self.multiworld.get_location(level.location_name_item, self.player) location.progress_type = LocationProgressType.PRIORITY @@ -322,7 +323,7 @@ class Overcooked2World(World): level_access_rule: Callable[[CollectionState], bool] = \ lambda state, level_name=level.level_name, previous_level_completed_event_name=previous_level_completed_event_name, required_star_count=required_star_count: \ - has_requirements_for_level_access(state, level_name, previous_level_completed_event_name, required_star_count, self.player) + has_requirements_for_level_access(state, level_name, previous_level_completed_event_name, required_star_count, self.options["RampTricks"], self.player) self.connect_regions("Overworld", level.level_name, level_access_rule) # Level --> Overworld diff --git a/worlds/overcooked2/docs/setup_en.md b/worlds/overcooked2/docs/setup_en.md index d724f02f7f..de7bbb5bc8 100644 --- a/worlds/overcooked2/docs/setup_en.md +++ b/worlds/overcooked2/docs/setup_en.md @@ -19,9 +19,9 @@ ## Overview -*OC2-Modding* is a general purpose modding framework which doubles as an Archipelago MultiWorld Client. It works by using Harmony to inject custom code into the game at runtime, so none of the orignal game files need to be modified in any way. +*OC2-Modding* is a general purpose modding framework which doubles as an Archipelago MultiWorld Client. It works by using Harmony to inject custom code into the game at runtime, so none of the original game files need to be modified in any way. -When connecting to an Archipelago session using the in-game login screen, a modfile containing all relevant game modifications is automatically downloaded and applied. +When connecting to an Archipelago session using the in-game login screen, a mod file containing all relevant game modifications is automatically downloaded and applied. From this point, the game will communicate with the Archipelago service directly to manage sending/receiving items. Notifications of important events will appear through an in-game console at the top of the screen. @@ -82,3 +82,13 @@ To completely remove *OC2-Modding*, navigate to your game's installation folder Since the goal of randomizer isn't necessarily to achieve new personal high scores, players may find themselves waiting for a level timer to expire once they've met their objective. A new feature called *Auto-Complete* has been added to automatically complete levels once a target star count has been achieved. To enable *Auto-Complete*, press the **Show** button near the top of your screen to expand the modding controls. Then, repeatedly press the **Auto-Complete** button until it shows the desired setting. + +## Overworld Sequence Breaking + +In the world's settings, there is an option called "Overworld Tricks" which allows the generator to make games which require doing tricks with the food truck to complete. This includes: + +- Dashing across gaps + +- "Wiggling" up ledges + +- Going out of bounds [See Video](https://youtu.be/VdOGhi6XPu4) diff --git a/worlds/overcooked2/test/TestOvercooked2.py b/worlds/overcooked2/test/TestOvercooked2.py index a6b5a4dcde..4cb12d9d9b 100644 --- a/worlds/overcooked2/test/TestOvercooked2.py +++ b/worlds/overcooked2/test/TestOvercooked2.py @@ -1,10 +1,12 @@ import unittest - from random import Random +from worlds.AutoWorld import AutoWorldRegister +from test.general import setup_solo_multiworld + from worlds.overcooked2.Items import * -from worlds.overcooked2.Overcooked2Levels import Overcooked2Dlc, Overcooked2Level, level_id_to_shortname, ITEMS_TO_EXCLUDE_IF_NO_DLC -from worlds.overcooked2.Logic import level_logic, level_shuffle_factory +from worlds.overcooked2.Overcooked2Levels import Overcooked2Dlc, Overcooked2Level, OverworldRegion, overworld_region_by_level, level_id_to_shortname, ITEMS_TO_EXCLUDE_IF_NO_DLC +from worlds.overcooked2.Logic import level_logic, overworld_region_logic, level_shuffle_factory from worlds.overcooked2.Locations import oc2_location_name_to_id @@ -170,3 +172,43 @@ class Overcooked2Test(unittest.TestCase): count += 1 self.assertEqual(count, len(level_id_range), f"Number of levels in {dlc.name} has discrepancy between level_id range and directory") + + def testOverworldRegion(self): + # OverworldRegion + # overworld_region_by_level + # overworld_region_logic + + # Test for duplicates + regions_list = [x for x in OverworldRegion] + regions_set = set(regions_list) + self.assertEqual(len(regions_list), len(regions_set), f"Duplicate values in OverworldRegion") + + # Test all levels represented + shortnames = [level.as_generic_level.shortname for level in Overcooked2Level()] + for shortname in shortnames: + if " " in shortname: + shortname = shortname.split(" ")[1] + shortname = shortname.replace("K-", "Kevin-") + self.assertIn(shortname, overworld_region_by_level) + + for region in overworld_region_by_level.values(): + # Test all regions valid + self.assertIn(region, regions_list) + + # Test Region Coverage + self.assertIn(region, overworld_region_logic) + + # Test all regions valid + for region in overworld_region_logic: + self.assertIn(region, regions_set) + + self.assertIn("Overcooked! 2", AutoWorldRegister.world_types.keys()) + world_type = AutoWorldRegister.world_types["Overcooked! 2"] + world = setup_solo_multiworld(world_type) + state = world.get_all_state(False) + + # Test region logic + for logic in overworld_region_logic.values(): + for allow_tricks in [False, True]: + result = logic(state, 1, allow_tricks, list()) + self.assertIn(result, [False, True]) diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index abe309222e..b223568ff0 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -15,7 +15,7 @@ from .options import pokemon_rb_options from .rom_addresses import rom_addresses from .text import encode_text from .rom import generate_output, get_base_rom_bytes, get_base_rom_path, process_pokemon_data, process_wild_pokemon,\ - process_static_pokemon + process_static_pokemon, process_move_data from .rules import set_rules import worlds.pokemon_rb.poke_data as poke_data @@ -40,13 +40,14 @@ class PokemonRedBlueWorld(World): game = "Pokemon Red and Blue" option_definitions = pokemon_rb_options - data_version = 5 - required_client_version = (0, 3, 7) + data_version = 7 + required_client_version = (0, 3, 9) topology_present = False item_name_to_id = {name: data.id for name, data in item_table.items()} - location_name_to_id = {location.name: location.address for location in location_data if location.type == "Item"} + location_name_to_id = {location.name: location.address for location in location_data if location.type == "Item" + and location.address is not None} item_name_groups = item_groups web = PokemonWebWorld() @@ -58,11 +59,14 @@ class PokemonRedBlueWorld(World): self.extra_badges = {} self.type_chart = None self.local_poke_data = None + self.local_move_data = None + self.local_tms = None self.learnsets = None self.trainer_name = None self.rival_name = None self.type_chart = None self.traps = None + self.trade_mons = {} @classmethod def stage_assert_generate(cls, multiworld: MultiWorld): @@ -94,6 +98,12 @@ class PokemonRedBlueWorld(World): if len(self.multiworld.player_name[self.player].encode()) > 16: raise Exception(f"Player name too long for {self.multiworld.get_player_name(self.player)}. Player name cannot exceed 16 bytes for PokÊmon Red and Blue.") + if (self.multiworld.dexsanity[self.player] and self.multiworld.accessibility[self.player] == "locations" + and (self.multiworld.catch_em_all[self.player] != "all_pokemon" + or self.multiworld.randomize_wild_pokemon[self.player] == "vanilla" + or self.multiworld.randomize_legendary_pokemon[self.player] != "any")): + self.multiworld.accessibility[self.player] = self.multiworld.accessibility[self.player].from_text("items") + if self.multiworld.badges_needed_for_hm_moves[self.player].value >= 2: badges_to_add = ["Marsh Badge", "Volcano Badge", "Earth Badge"] if self.multiworld.badges_needed_for_hm_moves[self.player].value == 3: @@ -107,6 +117,7 @@ class PokemonRedBlueWorld(World): for badge in badges_to_add: self.extra_badges[hm_moves.pop()] = badge + process_move_data(self) process_pokemon_data(self) if self.multiworld.randomize_type_chart[self.player] == "vanilla": @@ -171,15 +182,22 @@ class PokemonRedBlueWorld(World): # damage being reduced by 1 which leads to a "not very effective" message appearing due to my changes # to the way effectiveness messages are generated. self.type_chart = sorted(chart, key=lambda matchup: -matchup[2]) + self.multiworld.early_items[self.player]["Exp. All"] = 1 def create_items(self) -> None: start_inventory = self.multiworld.start_inventory[self.player].value.copy() if self.multiworld.randomize_pokedex[self.player] == "start_with": start_inventory["Pokedex"] = 1 self.multiworld.push_precollected(self.create_item("Pokedex")) + locations = [location for location in location_data if location.type == "Item"] item_pool = [] + combined_traps = (self.multiworld.poison_trap_weight[self.player].value + + self.multiworld.fire_trap_weight[self.player].value + + self.multiworld.paralyze_trap_weight[self.player].value + + self.multiworld.ice_trap_weight[self.player].value) for location in locations: + event = location.event if not location.inclusion(self.multiworld, self.player): continue if location.original_item in self.multiworld.start_inventory[self.player].value and \ @@ -188,13 +206,22 @@ class PokemonRedBlueWorld(World): item = self.create_filler() elif location.original_item is None: item = self.create_filler() + elif location.original_item == "Pokedex": + if self.multiworld.randomize_pokedex[self.player] == "vanilla": + self.multiworld.get_location(location.name, self.player).event = True + event = True + item = self.create_item("Pokedex") + elif location.original_item.startswith("TM"): + if self.multiworld.randomize_tm_moves[self.player]: + item = self.create_item(location.original_item.split(" ")[0]) + else: + item = self.create_item(location.original_item) else: item = self.create_item(location.original_item) - combined_traps = self.multiworld.poison_trap_weight[self.player].value + self.multiworld.fire_trap_weight[self.player].value + self.multiworld.paralyze_trap_weight[self.player].value + self.multiworld.ice_trap_weight[self.player].value if (item.classification == ItemClassification.filler and self.multiworld.random.randint(1, 100) <= self.multiworld.trap_percentage[self.player].value and combined_traps != 0): item = self.create_item(self.select_trap()) - if location.event: + if event: self.multiworld.get_location(location.name, self.player).place_locked_item(item) elif "Badge" not in item.name or self.multiworld.badgesanity[self.player].value: item_pool.append(item) @@ -204,12 +231,69 @@ class PokemonRedBlueWorld(World): self.multiworld.itempool += item_pool def pre_fill(self) -> None: - process_wild_pokemon(self) process_static_pokemon(self) + pokemon_locs = [location.name for location in location_data if location.type != "Item"] + for location in self.multiworld.get_locations(self.player): + if location.name in pokemon_locs: + location.show_in_spoiler = False - if self.multiworld.old_man[self.player].value == 1: + def intervene(move): + accessible_slots = [loc for loc in self.multiworld.get_reachable_locations(test_state, self.player) if loc.type == "Wild Encounter"] + move_bit = pow(2, poke_data.hm_moves.index(move) + 2) + viable_mons = [mon for mon in self.local_poke_data if self.local_poke_data[mon]["tms"][6] & move_bit] + placed_mons = [slot.item.name for slot in accessible_slots] + # this sort method doesn't seem to work if you reference the same list being sorted in the lambda + placed_mons_copy = placed_mons.copy() + placed_mons.sort(key=lambda i: placed_mons_copy.count(i)) + placed_mon = placed_mons.pop() + if self.multiworld.area_1_to_1_mapping[self.player]: + zone = " - ".join(placed_mon.split(" - ")[:-1]) + replace_slots = [slot for slot in accessible_slots if slot.name.startswith(zone) and slot.item.name == + placed_mon] + else: + replace_slots = [self.multiworld.random.choice([slot for slot in accessible_slots if slot.item.name == + placed_mon])] + replace_mon = self.multiworld.random.choice(viable_mons) + for replace_slot in replace_slots: + replace_slot.item = self.create_item(replace_mon) + last_intervene = None + while True: + intervene_move = None + test_state = self.multiworld.get_all_state(False) + if not self.multiworld.badgesanity[self.player]: + for badge in ["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Soul Badge", + "Marsh Badge", "Volcano Badge", "Earth Badge"]: + test_state.collect(self.create_item(badge)) + if not test_state.pokemon_rb_can_surf(self.player): + intervene_move = "Surf" + if not test_state.pokemon_rb_can_strength(self.player): + intervene_move = "Strength" + # cut may not be needed if accessibility is minimal, unless you need all 8 badges and badgesanity is off, + # as you will require cut to access celadon gyn + if (self.multiworld.accessibility[self.player] != "minimal" or ((not + self.multiworld.badgesanity[self.player]) and max(self.multiworld.elite_four_condition[self.player], + self.multiworld.victory_road_condition[self.player]) > 7)): + if not test_state.pokemon_rb_can_cut(self.player): + intervene_move = "Cut" + if (self.multiworld.accessibility[self.player].current_key != "minimal" and + (self.multiworld.trainersanity[self.player] or self.multiworld.extra_key_items[self.player])): + if not test_state.pokemon_rb_can_flash(self.player): + intervene_move = "Flash" + if intervene_move: + if intervene_move == last_intervene: + raise Exception(f"Caught in infinite loop attempting to ensure {intervene_move} is available to player {self.player}") + intervene(intervene_move) + last_intervene = intervene_move + else: + break + + if self.multiworld.old_man[self.player] == "early_parcel": self.multiworld.local_early_items[self.player]["Oak's Parcel"] = 1 + if self.multiworld.dexsanity[self.player]: + for location in [self.multiworld.get_location(f"Pokedex - {mon}", self.player) + for mon in poke_data.pokemon_data.keys()]: + add_item_rule(location, lambda item: item.name != "Oak's Parcel" or item.player != self.player) if not self.multiworld.badgesanity[self.player].value: self.multiworld.non_local_items[self.player].value -= self.item_name_groups["Badges"] @@ -236,46 +320,41 @@ class PokemonRedBlueWorld(World): else: raise FillError(f"Failed to place badges for player {self.player}") - locs = [self.multiworld.get_location("Fossil - Choice A", self.player), - self.multiworld.get_location("Fossil - Choice B", self.player)] - for loc in locs: - add_item_rule(loc, lambda i: i.advancement or i.name in self.item_name_groups["Unique"] - or i.name == "Master Ball") + # Place local items in some locations to prevent save-scumming. Also Oak's PC to prevent an "AP Item" from + # entering the player's inventory. + + locs = {self.multiworld.get_location("Fossil - Choice A", self.player), + self.multiworld.get_location("Fossil - Choice B", self.player)} + + if self.multiworld.dexsanity[self.player]: + for mon in ([" ".join(self.multiworld.get_location( + f"Pallet Town - Starter {i}", self.player).item.name.split(" ")[1:]) for i in range(1, 4)] + + [" ".join(self.multiworld.get_location( + f"Fighting Dojo - Gift {i}", self.player).item.name.split(" ")[1:]) for i in range(1, 3)]): + loc = self.multiworld.get_location(f"Pokedex - {mon}", self.player) + if loc.item is None: + locs.add(loc) loc = self.multiworld.get_location("Pallet Town - Player's PC", self.player) if loc.item is None: - locs.append(loc) + locs.add(loc) - for loc in locs: + for loc in sorted(locs): unplaced_items = [] if loc.name in self.multiworld.priority_locations[self.player].value: add_item_rule(loc, lambda i: i.advancement) for item in reversed(self.multiworld.itempool): if item.player == self.player and loc.can_fill(self.multiworld.state, item, False): self.multiworld.itempool.remove(item) - state = sweep_from_pool(self.multiworld.state, self.multiworld.itempool + unplaced_items) - if state.can_reach(loc, "Location", self.player): + if item.advancement: + state = sweep_from_pool(self.multiworld.state, self.multiworld.itempool + unplaced_items) + if (not item.advancement) or state.can_reach(loc, "Location", self.player): loc.place_locked_item(item) break else: unplaced_items.append(item) self.multiworld.itempool += unplaced_items - intervene = False - test_state = self.multiworld.get_all_state(False) - if not test_state.pokemon_rb_can_surf(self.player) or not test_state.pokemon_rb_can_strength(self.player): - intervene = True - elif self.multiworld.accessibility[self.player].current_key != "minimal": - if not test_state.pokemon_rb_can_cut(self.player) or not test_state.pokemon_rb_can_flash(self.player): - intervene = True - if intervene: - # the way this is handled will be improved significantly in the future when I add options to - # let you choose the exact weights for HM compatibility - logging.warning( - f"HM-compatible PokÊmon possibly missing, placing Mew on Route 1 for player {self.player}") - loc = self.multiworld.get_location("Route 1 - Wild Pokemon - 1", self.player) - loc.item = self.create_item("Mew") - def create_regions(self): if self.multiworld.free_fly_location[self.player].value: if self.multiworld.old_man[self.player].value == 0: @@ -316,6 +395,12 @@ class PokemonRedBlueWorld(World): spoiler_handle.write(f"\n\nType matchups ({self.multiworld.player_name[self.player]}):\n\n") for matchup in self.type_chart: spoiler_handle.write(f"{matchup[0]} deals {matchup[2] * 10}% damage to {matchup[1]}\n") + spoiler_handle.write(f"\n\nPokÊmon locations ({self.multiworld.player_name[self.player]}):\n\n") + pokemon_locs = [location.name for location in location_data if location.type != "Item"] + for location in self.multiworld.get_locations(self.player): + if location.name in pokemon_locs: + spoiler_handle.write(location.name + ": " + location.item.name + "\n") + def get_filler_item_name(self) -> str: combined_traps = self.multiworld.poison_trap_weight[self.player].value + self.multiworld.fire_trap_weight[self.player].value + self.multiworld.paralyze_trap_weight[self.player].value + self.multiworld.ice_trap_weight[self.player].value @@ -335,6 +420,21 @@ class PokemonRedBlueWorld(World): self.traps += ["Ice Trap"] * self.multiworld.ice_trap_weight[self.player].value return self.multiworld.random.choice(self.traps) + def extend_hint_information(self, hint_data): + if self.multiworld.dexsanity[self.player]: + hint_data[self.player] = {} + mon_locations = {mon: set() for mon in poke_data.pokemon_data.keys()} + for loc in location_data: #self.multiworld.get_locations(self.player): + if loc.type in ["Wild Encounter", "Static Pokemon", "Legendary Pokemon", "Missable Pokemon"]: + mon = self.multiworld.get_location(loc.name, self.player).item.name + if mon.startswith("Static ") or mon.startswith("Missable "): + mon = " ".join(mon.split(" ")[1:]) + mon_locations[mon].add(loc.name.split(" -")[0]) + for mon in mon_locations: + if mon_locations[mon]: + hint_data[self.player][self.multiworld.get_location(f"Pokedex - {mon}", self.player).address] = \ + ", ".join(mon_locations[mon]) + def fill_slot_data(self) -> dict: return { "second_fossil_check_condition": self.multiworld.second_fossil_check_condition[self.player].value, @@ -357,7 +457,8 @@ class PokemonRedBlueWorld(World): "type_chart": self.type_chart, "randomize_pokedex": self.multiworld.randomize_pokedex[self.player].value, "trainersanity": self.multiworld.trainersanity[self.player].value, - "death_link": self.multiworld.death_link[self.player].value + "death_link": self.multiworld.death_link[self.player].value, + "prizesanity": self.multiworld.prizesanity[self.player].value } diff --git a/worlds/pokemon_rb/basepatch_blue.bsdiff4 b/worlds/pokemon_rb/basepatch_blue.bsdiff4 index 6932762f4d..895c5ddfe1 100644 Binary files a/worlds/pokemon_rb/basepatch_blue.bsdiff4 and b/worlds/pokemon_rb/basepatch_blue.bsdiff4 differ diff --git a/worlds/pokemon_rb/basepatch_red.bsdiff4 b/worlds/pokemon_rb/basepatch_red.bsdiff4 index cd380e1f4f..1d69c48241 100644 Binary files a/worlds/pokemon_rb/basepatch_red.bsdiff4 and b/worlds/pokemon_rb/basepatch_red.bsdiff4 differ diff --git a/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md b/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md index b857f234b0..556da20309 100644 --- a/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md +++ b/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md @@ -33,6 +33,8 @@ fossil scientist. This may require reviving a number of fossils, depending on yo * If the Old Man is blocking your way through Viridian City, you do not have Oak's Parcel in your inventory, and you've exhausted your money and PokÊ Balls, you can get a free PokÊ Ball from your mom. * HM moves can be overwritten if you have the HM for it in your bag. +* The NPC on the left behind the Celadon Game Corner counter will sell 1500 coins at once instead of giving information +about the Prize Corner ## What items and locations get shuffled? diff --git a/worlds/pokemon_rb/items.py b/worlds/pokemon_rb/items.py index 8afde91957..b30480ed3d 100644 --- a/worlds/pokemon_rb/items.py +++ b/worlds/pokemon_rb/items.py @@ -65,7 +65,7 @@ item_table = { "Super Repel": ItemData(56, ItemClassification.filler, ["Consumables"]), "Max Repel": ItemData(57, ItemClassification.filler, ["Consumables"]), "Dire Hit": ItemData(58, ItemClassification.filler, ["Consumables", "Battle Items"]), - #"Coin": ItemData(59, ItemClassification.filler), + "10 Coins": ItemData(59, ItemClassification.filler, ["Coins"]), "Fresh Water": ItemData(60, ItemClassification.filler, ["Consumables", "Vending Machine Drinks"]), "Soda Pop": ItemData(61, ItemClassification.filler, ["Consumables", "Vending Machine Drinks"]), "Lemonade": ItemData(62, ItemClassification.filler, ["Consumables", "Vending Machine Drinks"]), @@ -103,6 +103,8 @@ item_table = { "Paralyze Trap": ItemData(95, ItemClassification.trap, ["Traps"]), "Ice Trap": ItemData(96, ItemClassification.trap, ["Traps"]), "Fire Trap": ItemData(97, ItemClassification.trap, ["Traps"]), + "20 Coins": ItemData(98, ItemClassification.filler, ["Coins"]), + "100 Coins": ItemData(99, ItemClassification.filler, ["Coins"]), "HM01 Cut": ItemData(196, ItemClassification.progression, ["Unique", "HMs"]), "HM02 Fly": ItemData(197, ItemClassification.progression, ["Unique", "HMs"]), "HM03 Surf": ItemData(198, ItemClassification.progression, ["Unique", "HMs"]), @@ -119,7 +121,7 @@ item_table = { "TM09 Take Down": ItemData(209, ItemClassification.useful, ["Unique", "TMs"]), "TM10 Double Edge": ItemData(210, ItemClassification.useful, ["Unique", "TMs"]), "TM11 Bubble Beam": ItemData(211, ItemClassification.useful, ["Unique", "TMs"]), - "TM12 Water Gun": ItemData(212, ItemClassification.useful, ["Unique", "TMs"]), + "TM12 Water Gun": ItemData(212, ItemClassification.filler, ["Unique", "TMs"]), "TM13 Ice Beam": ItemData(213, ItemClassification.useful, ["Unique", "TMs"]), "TM14 Blizzard": ItemData(214, ItemClassification.useful, ["Unique", "TMs"]), "TM15 Hyper Beam": ItemData(215, ItemClassification.useful, ["Unique", "TMs"]), @@ -163,6 +165,10 @@ item_table = { "Silph Co Liberated": ItemData(None, ItemClassification.progression, []), "Become Champion": ItemData(None, ItemClassification.progression, []) } + +item_table.update({f"TM{str(i).zfill(2)}": ItemData(i + 456, ItemClassification.filler, ["Unique", "TMs"]) + for i in range(1, 51)}) + item_table.update( {pokemon: ItemData(None, ItemClassification.progression, []) for pokemon in pokemon_data.keys()} ) diff --git a/worlds/pokemon_rb/locations.py b/worlds/pokemon_rb/locations.py index 418ce9a9f2..a1b64e12e5 100644 --- a/worlds/pokemon_rb/locations.py +++ b/worlds/pokemon_rb/locations.py @@ -1,6 +1,8 @@ from BaseClasses import Location from .rom_addresses import rom_addresses +from .poke_data import pokemon_data + loc_id_start = 172000000 @@ -8,6 +10,10 @@ def trainersanity(multiworld, player): return multiworld.trainersanity[player] +def dexsanity(multiworld, player): + return multiworld.dexsanity[player] + + def hidden_items(multiworld, player): return multiworld.randomize_hidden_items[player].value > 0 @@ -20,14 +26,13 @@ def extra_key_items(multiworld, player): return multiworld.extra_key_items[player] -def pokedex(multiworld, player): - return multiworld.randomize_pokedex[player].value > 0 - - def always_on(multiworld, player): return True +def prizesanity(multiworld, player): + return multiworld.prizesanity[player] + class LocationData: @@ -72,6 +77,13 @@ class Rod: self.flag = flag +class DexSanityFlag: + def __init__(self, flag): + self.byte = int(flag / 8) + self.bit = flag % 8 + self.flag = flag + + location_data = [ LocationData("Vermilion City", "Fishing Guru", "Old Rod", rom_addresses["Rod_Vermilion_City_Fishing_Guru"], Rod(3)), @@ -119,7 +131,7 @@ location_data = [ LocationData("Celadon City", "Gambling Addict", "Coin Case", rom_addresses["Event_Gambling_Addict"], EventFlag(480)), LocationData("Celadon Gym", "Erika 2", "TM21 Mega Drain", rom_addresses["Event_Celadon_Gym"], EventFlag(424)), - LocationData("Silph Co 11F", "Silph Co President", "Master Ball", rom_addresses["Event_Silph_Co_President"], + LocationData("Silph Co 11F", "Silph Co President (Card Key)", "Master Ball", rom_addresses["Event_Silph_Co_President"], EventFlag(1933)), LocationData("Silph Co 2F", "Woman", "TM36 Self-Destruct", rom_addresses["Event_Scared_Woman"], EventFlag(1791)), @@ -374,7 +386,7 @@ location_data = [ LocationData("Seafoam Islands B4F", "Hidden Item Corner Island", "Ultra Ball", rom_addresses['Hidden_Item_Seafoam_Islands_B4F'], Hidden(26), inclusion=hidden_items), LocationData("Pokemon Mansion 1F", "Hidden Item Block Near Entrance Carpet", "Moon Stone", rom_addresses['Hidden_Item_Pokemon_Mansion_1F'], Hidden(27), inclusion=hidden_items), LocationData("Pokemon Mansion 3F", "Hidden Item Behind Burglar", "Max Revive", rom_addresses['Hidden_Item_Pokemon_Mansion_3F'], Hidden(28), inclusion=hidden_items), - LocationData("Route 23", "Hidden Item Rocks Before Final Guard", "Full Restore", rom_addresses['Hidden_Item_Route_23_1'], Hidden(29), inclusion=hidden_items), + LocationData("Route 23", "Hidden Item Rocks Before Victory Road", "Full Restore", rom_addresses['Hidden_Item_Route_23_1'], Hidden(29), inclusion=hidden_items), LocationData("Route 23", "Hidden Item East Bush After Water", "Ultra Ball", rom_addresses['Hidden_Item_Route_23_2'], Hidden(30), inclusion=hidden_items), LocationData("Route 23", "Hidden Item On Island", "Max Ether", rom_addresses['Hidden_Item_Route_23_3'], Hidden(31), inclusion=hidden_items), LocationData("Victory Road 2F", "Hidden Item Rock Before Moltres", "Ultra Ball", rom_addresses['Hidden_Item_Victory_Road_2F_1'], Hidden(32), inclusion=hidden_items), @@ -400,7 +412,8 @@ location_data = [ LocationData("Cerulean City", "Hidden Item Gym Badge Guy's Backyard", "Rare Candy", rom_addresses['Hidden_Item_Cerulean_City'], Hidden(52), inclusion=hidden_items), LocationData("Route 4", "Hidden Item Plateau East Of Mt Moon", "Great Ball", rom_addresses['Hidden_Item_Route_4'], Hidden(53), inclusion=hidden_items), - LocationData("Pallet Town", "Oak's Parcel Reward", "Pokedex", rom_addresses["Event_Pokedex"], EventFlag(0x38), inclusion=pokedex), + + LocationData("Pallet Town", "Oak's Parcel Reward", "Pokedex", rom_addresses["Event_Pokedex"], EventFlag(0x38)), LocationData("Pokemon Mansion 1F", "Scientist", None, rom_addresses["Trainersanity_EVENT_BEAT_MANSION_1_TRAINER_0_ITEM"], EventFlag(376), inclusion=trainersanity), LocationData("Pokemon Mansion 2F", "Burglar", None, rom_addresses["Trainersanity_EVENT_BEAT_MANSION_2_TRAINER_0_ITEM"], EventFlag(43), inclusion=trainersanity), @@ -712,6 +725,37 @@ location_data = [ LocationData("Indigo Plateau", "Bruno", None, rom_addresses["Trainersanity_EVENT_BEAT_BRUNOS_ROOM_TRAINER_0_ITEM"], EventFlag(20), inclusion=trainersanity), LocationData("Indigo Plateau", "Agatha", None, rom_addresses["Trainersanity_EVENT_BEAT_AGATHAS_ROOM_TRAINER_0_ITEM"], EventFlag(19), inclusion=trainersanity), LocationData("Indigo Plateau", "Lance", None, rom_addresses["Trainersanity_EVENT_BEAT_LANCES_ROOM_TRAINER_0_ITEM"], EventFlag(18), inclusion=trainersanity), + LocationData("Cinnabar Gym", "Burglar 1", None, rom_addresses["Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_A_ITEM"], EventFlag(374), inclusion=trainersanity), + LocationData("Cinnabar Gym", "Super Nerd 1", None, rom_addresses["Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_B_ITEM"], EventFlag(373), inclusion=trainersanity), + LocationData("Cinnabar Gym", "Super Nerd 2", None, rom_addresses["Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_2_ITEM"], EventFlag(372), inclusion=trainersanity), + LocationData("Cinnabar Gym", "Burglar 2", None, rom_addresses["Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_3_ITEM"], EventFlag(371), inclusion=trainersanity), + LocationData("Cinnabar Gym", "Super Nerd 3", None, rom_addresses["Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_4_ITEM"], EventFlag(370), inclusion=trainersanity), + LocationData("Cinnabar Gym", "Super Nerd 4", None, rom_addresses["Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_5_ITEM"], EventFlag(369), inclusion=trainersanity), + LocationData("Cinnabar Gym", "Super Nerd 5", None, rom_addresses["Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_6_ITEM"], EventFlag(368), inclusion=trainersanity), + + LocationData("Celadon Prize Corner", "Item Prize 1", "TM23 Dragon Rage", rom_addresses["Prize_Item_A"], EventFlag(0x69a), inclusion=prizesanity), + LocationData("Celadon Prize Corner", "Item Prize 2", "TM15 Hyper Beam", rom_addresses["Prize_Item_B"], EventFlag(0x69B), inclusion=prizesanity), + LocationData("Celadon Prize Corner", "Item Prize 3", "TM50 Substitute", rom_addresses["Prize_Item_C"], EventFlag(0x69C), inclusion=prizesanity), + + LocationData("Celadon Game Corner", "West Gambler's Gift (Coin Case)", "10 Coins", rom_addresses["Event_Game_Corner_Gift_A"], EventFlag(0x1ba)), + LocationData("Celadon Game Corner", "Center Gambler's Gift (Coin Case)", "20 Coins", rom_addresses["Event_Game_Corner_Gift_C"], EventFlag(0x1bc)), + LocationData("Celadon Game Corner", "East Gambler's Gift (Coin Case)", "20 Coins", rom_addresses["Event_Game_Corner_Gift_B"], EventFlag(0x1bb)), + + LocationData("Celadon Game Corner", "Hidden Item Northwest By Counter (Coin Case)", "10 Coins", rom_addresses["Hidden_Item_Game_Corner_1"], Hidden(54), inclusion=hidden_items), + LocationData("Celadon Game Corner", "Hidden Item Southwest Corner (Coin Case)", "10 Coins", rom_addresses["Hidden_Item_Game_Corner_2"], Hidden(55), inclusion=hidden_items), + LocationData("Celadon Game Corner", "Hidden Item Near Rumor Man (Coin Case)", "20 Coins", rom_addresses["Hidden_Item_Game_Corner_3"], Hidden(56), inclusion=hidden_items), + LocationData("Celadon Game Corner", "Hidden Item Near Speculating Woman (Coin Case)", "10 Coins", rom_addresses["Hidden_Item_Game_Corner_4"], Hidden(57), inclusion=hidden_items), + LocationData("Celadon Game Corner", "Hidden Item Near West Gifting Gambler (Coin Case)", "10 Coins", rom_addresses["Hidden_Item_Game_Corner_5"], Hidden(58), inclusion=hidden_items), + LocationData("Celadon Game Corner", "Hidden Item Near Wonderful Time Woman (Coin Case)", "20 Coins", rom_addresses["Hidden_Item_Game_Corner_6"], Hidden(59), inclusion=hidden_items), + LocationData("Celadon Game Corner", "Hidden Item Near Failing Gym Information Guy (Coin Case)", "10 Coins", rom_addresses["Hidden_Item_Game_Corner_7"], Hidden(60), inclusion=hidden_items), + LocationData("Celadon Game Corner", "Hidden Item Near East Gifting Gambler (Coin Case)", "10 Coins", rom_addresses["Hidden_Item_Game_Corner_8"], Hidden(61), inclusion=hidden_items), + LocationData("Celadon Game Corner", "Hidden Item Near Hooked Guy (Coin Case)", "10 Coins", rom_addresses["Hidden_Item_Game_Corner_9"], Hidden(62), inclusion=hidden_items), + LocationData("Celadon Game Corner", "Hidden Item at End of Horizontal Machine Row (Coin Case)", "20 Coins", rom_addresses["Hidden_Item_Game_Corner_10"], Hidden(63), inclusion=hidden_items), + LocationData("Celadon Game Corner", "Hidden Item in Front of Horizontal Machine Row (Coin Case)", "100 Coins", rom_addresses["Hidden_Item_Game_Corner_11"], Hidden(64), inclusion=hidden_items), + + *[LocationData("Pokedex", mon, ball, rom_addresses["Dexsanity_Items"] + i, DexSanityFlag(i), type="Item", + inclusion=dexsanity) for (mon, i, ball) in zip(pokemon_data.keys(), range(0, 152), + ["Poke Ball", "Great Ball", "Ultra Ball"]* 51)], LocationData("Indigo Plateau", "Become Champion", "Become Champion", event=True), LocationData("Pokemon Tower 7F", "Fuji Saved", "Fuji Saved", event=True), @@ -1965,6 +2009,25 @@ location_data = [ LocationData("Cinnabar Island", "Dome Fossil Pokemon", "Kabuto", rom_addresses["Gift_Kabuto"], None, event=True, type="Static Pokemon"), + LocationData("Route 2 East", "Marcel Trade", "Mr Mime", rom_addresses["Trade_Marcel"] + 1, None, event=True, + type="Static Pokemon"), + LocationData("Underground Tunnel North-South", "Spot Trade", "Nidoran F", rom_addresses["Trade_Spot"] + 1, None, + event=True, type="Static Pokemon"), + LocationData("Route 11", "Terry Trade", "Nidorina", rom_addresses["Trade_Terry"] + 1, None, event=True, + type="Static Pokemon"), + LocationData("Route 18", "Marc Trade", "Lickitung", rom_addresses["Trade_Marc"] + 1, None, event=True, + type="Static Pokemon"), + LocationData("Cinnabar Island", "Sailor Trade", "Seel", rom_addresses["Trade_Sailor"] + 1, None, event=True, + type="Static Pokemon"), + LocationData("Cinnabar Island", "Crinkles Trade", "Tangela", rom_addresses["Trade_Crinkles"] + 1, None, + event=True, type="Static Pokemon"), + LocationData("Cinnabar Island", "Doris Trade", "Electrode", rom_addresses["Trade_Doris"] + 1, None, + event=True, type="Static Pokemon"), + LocationData("Vermilion City", "Dux Trade", "Farfetchd", rom_addresses["Trade_Dux"] + 1, None, event=True, + type="Static Pokemon"), + LocationData("Cerulean City", "Lola Trade", "Jynx", rom_addresses["Trade_Lola"] + 1, None, event=True, + type="Static Pokemon"), + # not counted for logic currently. Could perhaps make static encounters resettable in the future? LocationData("Power Plant", "Fake Pokeball Battle 1", "Voltorb", rom_addresses["Static_Encounter_Voltorb_A"], None, event=True, type="Missable Pokemon"), @@ -2043,20 +2106,24 @@ location_data = [ ] -for i, location in enumerate(location_data): + + +i = 0 +for location in location_data: if location.event or location.rom_address is None: location.address = None else: location.address = loc_id_start + i - + i += 1 class PokemonRBLocation(Location): game = "Pokemon Red and Blue" - def __init__(self, player, name, address, rom_address): + def __init__(self, player, name, address, rom_address, type): super(PokemonRBLocation, self).__init__( player, name, address ) - self.rom_address = rom_address \ No newline at end of file + self.rom_address = rom_address + self.type = type diff --git a/worlds/pokemon_rb/logic.py b/worlds/pokemon_rb/logic.py index 70e825c2b5..8425bcdb4b 100644 --- a/worlds/pokemon_rb/logic.py +++ b/worlds/pokemon_rb/logic.py @@ -45,14 +45,13 @@ class PokemonLogic(LogicMixin): ["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Soul Badge", "Marsh Badge", "Volcano Badge", "Earth Badge", "Bicycle", "Silph Scope", "Item Finder", "Super Rod", "Good Rod", "Old Rod", "Lift Key", "Card Key", "Town Map", "Coin Case", "S.S. Ticket", "Secret Key", - "Mansion Key", "Safari Pass", "Plant Key", "Hideout Key", "HM01 Cut", "HM02 Fly", "HM03 Surf", - "HM04 Strength", "HM05 Flash"] if self.has(item, player)]) >= count + "Poke Flute", "Mansion Key", "Safari Pass", "Plant Key", "Hideout Key", "HM01 Cut", "HM02 Fly", + "HM03 Surf", "HM04 Strength", "HM05 Flash"] if self.has(item, player)]) >= count def pokemon_rb_can_pass_guards(self, player): if self.multiworld.tea[player].value: return self.has("Tea", player) else: - # this could just be "True", but you never know what weird options I might introduce later ;) return self.can_reach("Celadon City - Counter Man", "Location", player) def pokemon_rb_has_badges(self, count, player): @@ -60,13 +59,8 @@ class PokemonLogic(LogicMixin): "Soul Badge", "Volcano Badge", "Earth Badge"] if self.has(item, player)]) >= count def pokemon_rb_oaks_aide(self, count, player): - if self.multiworld.randomize_pokedex[player].value > 0: - if not self.has("Pokedex", player): - return False - else: - if not self.has("Oak's Parcel", player): - return False - return self.pokemon_rb_has_pokemon(count, player) + return ((not self.multiworld.require_pokedex[player] or self.has("Pokedex", player)) + and self.pokemon_rb_has_pokemon(count, player)) def pokemon_rb_has_pokemon(self, count, player): obtained_pokemon = set() diff --git a/worlds/pokemon_rb/options.py b/worlds/pokemon_rb/options.py index ae51c47b32..f33cb566f9 100644 --- a/worlds/pokemon_rb/options.py +++ b/worlds/pokemon_rb/options.py @@ -65,7 +65,7 @@ class CeruleanCaveCondition(Range): If extra_key_items is turned on, the number chosen will be increased by 4.""" display_name = "Cerulean Cave Condition" range_start = 0 - range_end = 25 + range_end = 26 default = 20 @@ -155,6 +155,12 @@ class RandomizeHiddenItems(Choice): default = 0 +class PrizeSanity(Toggle): + """Shuffles the TM prizes at the Celadon Prize Corner into the item pool.""" + display_name = "Prizesanity" + default = 0 + + class TrainerSanity(Toggle): """Add a location check to every trainer in the game, which can be obtained by talking to a trainer after defeating them. Does not affect gym leaders and some scripted event battles (including all Rival, Giovanni, and @@ -163,12 +169,45 @@ class TrainerSanity(Toggle): default = 0 +class RequirePokedex(Toggle): + """Require the Pokedex to obtain items from Oak's Aides or from Dexsanity checks.""" + display_name = "Require Pokedex" + default = 1 + + +class AllPokemonSeen(Toggle): + """Start with all Pokemon "seen" in your Pokedex. This allows you to see where Pokemon can be encountered in the + wild. Pokemon found by fishing or in the Cerulean Cave are not displayed.""" + default = 0 + display_name = "All Pokemon Seen" + + +class DexSanity(Toggle): + """Adds a location check for each Pokemon flagged "Owned" on your Pokedex. If accessibility is set to `locations` + and randomize_wild_pokemon is off, catch_em_all is not `all_pokemon` or randomize_legendary_pokemon is not `any`, + accessibility will be forced to `items` instead, as not all Dexsanity locations can be guaranteed to be considered + reachable in logic. + If Pokedex is required, the items for Pokemon acquired before acquiring the Pokedex can be found by talking to + Professor Oak or evaluating the Pokedex via Oak's PC.""" + display_name = "Dexsanity" + default = 0 + + class FreeFlyLocation(Toggle): """One random fly destination will be unlocked by default.""" display_name = "Free Fly Location" default = 1 +class RandomizeRockTunnel(Toggle): + """Randomize the layout of Rock Tunnel. This is highly experimental, if you encounter any issues (items or trainers + unreachable, trainers walking over walls, inability to reach end of tunnel, anything looking strange) to + Alchav#8826 in the Archipelago Discord (directly or in #pkmn-red-blue) along with the seed number found on the + signs outside the tunnel.""" + display_name = "Randomize Rock Tunnel" + default = 0 + + class OaksAidRt2(Range): """Number of Pokemon registered in the Pokedex required to receive the item from Oak's Aide on Route 2. Vanilla is 10.""" @@ -229,6 +268,13 @@ class RandomizeWildPokemon(Choice): option_completely_random = 4 +class Area1To1Mapping(Toggle): + """When randomizing wild Pokemon, for each zone, all instances of a particular Pokemon will be replaced with the + same Pokemon, resulting in fewer Pokemon in each area.""" + default = 1 + display_name = "Area 1-to-1 Mapping" + + class RandomizeStarterPokemon(Choice): """Randomize the starter Pokemon choices.""" display_name = "Randomize Starter Pokemon" @@ -334,6 +380,13 @@ class MinimumCatchRate(Range): default = 3 +class MoveBalancing(Toggle): + """All one-hit-KO moves and fixed-damage moves become normal damaging moves. + Blizzard, and moves that cause sleep have their accuracy reduced.""" + display_name = "Move Balancing" + default = 0 + + class RandomizePokemonMovesets(Choice): """Randomize the moves learned by Pokemon. prefer_types will prefer moves that match the type of the Pokemon.""" display_name = "Randomize Pokemon Movesets" @@ -343,6 +396,12 @@ class RandomizePokemonMovesets(Choice): default = 0 +class ConfineTranstormToDitto(Toggle): + """Regardless of moveset randomization, will keep Ditto's first move as Transform no others will learn it. + If an enemy Pokemon uses transform before you catch it, it will permanently change to Ditto after capture.""" + display_name = "Confine Transform to Ditto" + default = 1 + class StartWithFourMoves(Toggle): """If movesets are randomized, this will give all Pokemon 4 starting moves.""" display_name = "Start With Four Moves" @@ -356,30 +415,62 @@ class SameTypeAttackBonus(Toggle): default = 1 -class TMCompatibility(Choice): - """Randomize which Pokemon can learn each TM. prefer_types: 90% chance if Pokemon's type matches the move, - 50% chance if move is Normal type and the Pokemon is not, and 25% chance otherwise. Pokemon will retain the same - TM compatibility when they evolve if the evolved form has the same type(s). Mew will always be able to learn - every TM.""" - display_name = "TM Compatibility" - default = 0 - option_vanilla = 0 - option_prefer_types = 1 - option_completely_random = 2 - option_full_compatibility = 3 +class RandomizeTMMoves(Toggle): + """Randomize the moves taught by TMs. + All TM items will be flagged as 'filler' items regardless of how good the move they teach are.""" + display_name = "Randomize TM Moves" -class HMCompatibility(Choice): - """Randomize which Pokemon can learn each HM. prefer_types: 100% chance if Pokemon's type matches the move, - 75% chance if move is Normal type and the Pokemon is not, and 25% chance otherwise. Pokemon will retain the same - HM compatibility when they evolve if the evolved form has the same type(s). Mew will always be able to learn - every HM.""" - display_name = "HM Compatibility" - default = 0 - option_vanilla = 0 - option_prefer_types = 1 - option_completely_random = 2 - option_full_compatibility = 3 +class TMHMCompatibility(SpecialRange): + range_start = -1 + range_end = 100 + special_range_names = { + "vanilla": -1, + "none": 0, + "full": 100 + } + default = -1 + + +class TMSameTypeCompatibility(TMHMCompatibility): + """Chance of each TM being usable on each Pokemon whose type matches the move.""" + display_name = "TM Same-Type Compatibility" + + +class TMNormalTypeCompatibility(TMHMCompatibility): + """Chance of each TM being usable on each Pokemon if the move is Normal type and the Pokemon is not.""" + display_name = "TM Normal-Type Compatibility" + + +class TMOtherTypeCompatibility(TMHMCompatibility): + """Chance of each TM being usable on each Pokemon if the move a type other than Normal or one of the Pokemon's types.""" + display_name = "TM Other-Type Compatibility" + + +class HMSameTypeCompatibility(TMHMCompatibility): + """Chance of each HM being usable on each Pokemon whose type matches the move. + At least one Pokemon will always be able to learn the moves needed to meet your accessibility requirements.""" + display_name = "HM Same-Type Compatibility" + + +class HMNormalTypeCompatibility(TMHMCompatibility): + """Chance of each HM being usable on each Pokemon if the move is Normal type and the Pokemon is not. + At least one Pokemon will always be able to learn the moves needed to meet your accessibility requirements.""" + display_name = "HM Normal-Type Compatibility" + + +class HMOtherTypeCompatibility(TMHMCompatibility): + """Chance of each HM being usable on each Pokemon if the move a type other than Normal or one of the Pokemon's types. + At least one Pokemon will always be able to learn the moves needed to meet your accessibility requirements.""" + display_name = "HM Other-Type Compatibility" + + +class InheritTMHMCompatibility(Toggle): + """If on, evolved Pokemon will inherit their pre-evolved form's TM and HM compatibilities. + They will then roll the above set chances again at a 50% lower rate for all TMs and HMs their predecessor could not + learn, unless the evolved form has additional or different types, then moves of those new types will be rolled + at the full set chance.""" + display_name = "Inherit TM/HM Compatibility" class RandomizePokemonTypes(Choice): @@ -543,6 +634,17 @@ class IceTrapWeight(TrapWeight): default = 0 +class RandomizePokemonPalettes(Choice): + """Modify palettes of Pokemon. Primary Type will set Pokemons' palettes based on their primary type, Follow + Evolutions will randomize palettes but palettes will remain the same through evolutions (except Eeveelutions), + Completely Random will randomize all Pokemons' palettes individually""" + display_name = "Randomize Pokemon Palettes" + option_vanilla = 0 + option_primary_type = 1 + option_follow_evolutions = 2 + option_completely_random = 3 + + pokemon_rb_options = { "game_version": GameVersion, "trainer_name": TrainerName, @@ -561,16 +663,22 @@ pokemon_rb_options = { "extra_strength_boulders": ExtraStrengthBoulders, "require_item_finder": RequireItemFinder, "randomize_hidden_items": RandomizeHiddenItems, + "prizesanity": PrizeSanity, "trainersanity": TrainerSanity, - "badges_needed_for_hm_moves": BadgesNeededForHMMoves, - "free_fly_location": FreeFlyLocation, + "require_pokedex": RequirePokedex, + "all_pokemon_seen": AllPokemonSeen, + "dexsanity": DexSanity, "oaks_aide_rt_2": OaksAidRt2, "oaks_aide_rt_11": OaksAidRt11, "oaks_aide_rt_15": OaksAidRt15, + "badges_needed_for_hm_moves": BadgesNeededForHMMoves, + "free_fly_location": FreeFlyLocation, + "randomize_rock_tunnel": RandomizeRockTunnel, "blind_trainers": BlindTrainers, "minimum_steps_between_encounters": MinimumStepsBetweenEncounters, "exp_modifier": ExpModifier, "randomize_wild_pokemon": RandomizeWildPokemon, + "area_1_to_1_mapping": Area1To1Mapping, "randomize_starter_pokemon": RandomizeStarterPokemon, "randomize_static_pokemon": RandomizeStaticPokemon, "randomize_legendary_pokemon": RandomizeLegendaryPokemon, @@ -580,11 +688,19 @@ pokemon_rb_options = { "minimum_catch_rate": MinimumCatchRate, "randomize_trainer_parties": RandomizeTrainerParties, "trainer_legendaries": TrainerLegendaries, + "move_balancing": MoveBalancing, "randomize_pokemon_movesets": RandomizePokemonMovesets, + "confine_transform_to_ditto": ConfineTranstormToDitto, "start_with_four_moves": StartWithFourMoves, "same_type_attack_bonus": SameTypeAttackBonus, - "tm_compatibility": TMCompatibility, - "hm_compatibility": HMCompatibility, + "randomize_tm_moves": RandomizeTMMoves, + "tm_same_type_compatibility": TMSameTypeCompatibility, + "tm_normal_type_compatibility": TMNormalTypeCompatibility, + "tm_other_type_compatibility": TMOtherTypeCompatibility, + "hm_same_type_compatibility": HMSameTypeCompatibility, + "hm_normal_type_compatibility": HMNormalTypeCompatibility, + "hm_other_type_compatibility": HMOtherTypeCompatibility, + "inherit_tm_hm_compatibility": InheritTMHMCompatibility, "randomize_pokemon_types": RandomizePokemonTypes, "secondary_type_chance": SecondaryTypeChance, "randomize_type_chart": RandomizeTypeChart, @@ -604,5 +720,6 @@ pokemon_rb_options = { "fire_trap_weight": FireTrapWeight, "paralyze_trap_weight": ParalyzeTrapWeight, "ice_trap_weight": IceTrapWeight, + "randomize_pokemon_palettes": RandomizePokemonPalettes, "death_link": DeathLink } diff --git a/worlds/pokemon_rb/poke_data.py b/worlds/pokemon_rb/poke_data.py index 691db1c46e..6218c70aa6 100644 --- a/worlds/pokemon_rb/poke_data.py +++ b/worlds/pokemon_rb/poke_data.py @@ -1006,172 +1006,172 @@ learnsets = { } moves = { - 'No Move': {'id': 0, 'power': 0, 'type': 'Typeless', 'accuracy': 0, 'pp': 0}, - 'Pound': {'id': 1, 'power': 40, 'type': 'Normal', 'accuracy': 100, 'pp': 35}, - 'Karate Chop': {'id': 2, 'power': 50, 'type': 'Normal', 'accuracy': 100, 'pp': 25}, - 'Doubleslap': {'id': 3, 'power': 15, 'type': 'Normal', 'accuracy': 85, 'pp': 10}, - 'Comet Punch': {'id': 4, 'power': 18, 'type': 'Normal', 'accuracy': 85, 'pp': 15}, - 'Mega Punch': {'id': 5, 'power': 80, 'type': 'Normal', 'accuracy': 85, 'pp': 20}, - 'Pay Day': {'id': 6, 'power': 40, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, - 'Fire Punch': {'id': 7, 'power': 75, 'type': 'Fire', 'accuracy': 100, 'pp': 15}, - 'Ice Punch': {'id': 8, 'power': 75, 'type': 'Ice', 'accuracy': 100, 'pp': 15}, - 'Thunderpunch': {'id': 9, 'power': 75, 'type': 'Electric', 'accuracy': 100, 'pp': 15}, - 'Scratch': {'id': 10, 'power': 40, 'type': 'Normal', 'accuracy': 100, 'pp': 35}, - 'Vicegrip': {'id': 11, 'power': 55, 'type': 'Normal', 'accuracy': 100, 'pp': 30}, - 'Guillotine': {'id': 12, 'power': 1, 'type': 'Normal', 'accuracy': 30, 'pp': 5}, - 'Razor Wind': {'id': 13, 'power': 80, 'type': 'Normal', 'accuracy': 75, 'pp': 10}, - 'Swords Dance': {'id': 14, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30}, - 'Cut': {'id': 15, 'power': 50, 'type': 'Normal', 'accuracy': 95, 'pp': 30}, - 'Gust': {'id': 16, 'power': 40, 'type': 'Normal', 'accuracy': 100, 'pp': 35}, - 'Wing Attack': {'id': 17, 'power': 35, 'type': 'Flying', 'accuracy': 100, 'pp': 35}, - 'Whirlwind': {'id': 18, 'power': 0, 'type': 'Normal', 'accuracy': 85, 'pp': 20}, - 'Fly': {'id': 19, 'power': 70, 'type': 'Flying', 'accuracy': 95, 'pp': 15}, - 'Bind': {'id': 20, 'power': 15, 'type': 'Normal', 'accuracy': 75, 'pp': 20}, - 'Slam': {'id': 21, 'power': 80, 'type': 'Normal', 'accuracy': 75, 'pp': 20}, - 'Vine Whip': {'id': 22, 'power': 35, 'type': 'Grass', 'accuracy': 100, 'pp': 10}, - 'Stomp': {'id': 23, 'power': 65, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, - 'Double Kick': {'id': 24, 'power': 30, 'type': 'Fighting', 'accuracy': 100, 'pp': 30}, - 'Mega Kick': {'id': 25, 'power': 120, 'type': 'Normal', 'accuracy': 75, 'pp': 5}, - 'Jump Kick': {'id': 26, 'power': 70, 'type': 'Fighting', 'accuracy': 95, 'pp': 25}, - 'Rolling Kick': {'id': 27, 'power': 60, 'type': 'Fighting', 'accuracy': 85, 'pp': 15}, - 'Sand Attack': {'id': 28, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 15}, - 'Headbutt': {'id': 29, 'power': 70, 'type': 'Normal', 'accuracy': 100, 'pp': 15}, - 'Horn Attack': {'id': 30, 'power': 65, 'type': 'Normal', 'accuracy': 100, 'pp': 25}, - 'Fury Attack': {'id': 31, 'power': 15, 'type': 'Normal', 'accuracy': 85, 'pp': 20}, - 'Horn Drill': {'id': 32, 'power': 1, 'type': 'Normal', 'accuracy': 30, 'pp': 5}, - 'Tackle': {'id': 33, 'power': 35, 'type': 'Normal', 'accuracy': 95, 'pp': 35}, - 'Body Slam': {'id': 34, 'power': 85, 'type': 'Normal', 'accuracy': 100, 'pp': 15}, - 'Wrap': {'id': 35, 'power': 15, 'type': 'Normal', 'accuracy': 85, 'pp': 20}, - 'Take Down': {'id': 36, 'power': 90, 'type': 'Normal', 'accuracy': 85, 'pp': 20}, - 'Thrash': {'id': 37, 'power': 90, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, - 'Double Edge': {'id': 38, 'power': 100, 'type': 'Normal', 'accuracy': 100, 'pp': 15}, - 'Tail Whip': {'id': 39, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30}, - 'Poison Sting': {'id': 40, 'power': 15, 'type': 'Poison', 'accuracy': 100, 'pp': 35}, - 'Twineedle': {'id': 41, 'power': 25, 'type': 'Bug', 'accuracy': 100, 'pp': 20}, - 'Pin Missile': {'id': 42, 'power': 14, 'type': 'Bug', 'accuracy': 85, 'pp': 20}, - 'Leer': {'id': 43, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30}, - 'Bite': {'id': 44, 'power': 60, 'type': 'Normal', 'accuracy': 100, 'pp': 25}, - 'Growl': {'id': 45, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 40}, - 'Roar': {'id': 46, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, - 'Sing': {'id': 47, 'power': 0, 'type': 'Normal', 'accuracy': 55, 'pp': 15}, - 'Supersonic': {'id': 48, 'power': 0, 'type': 'Normal', 'accuracy': 55, 'pp': 20}, - 'Sonicboom': {'id': 49, 'power': 1, 'type': 'Normal', 'accuracy': 90, 'pp': 20}, - 'Disable': {'id': 50, 'power': 0, 'type': 'Normal', 'accuracy': 55, 'pp': 20}, - 'Acid': {'id': 51, 'power': 40, 'type': 'Poison', 'accuracy': 100, 'pp': 30}, - 'Ember': {'id': 52, 'power': 40, 'type': 'Fire', 'accuracy': 100, 'pp': 25}, - 'Flamethrower': {'id': 53, 'power': 95, 'type': 'Fire', 'accuracy': 100, 'pp': 15}, - 'Mist': {'id': 54, 'power': 0, 'type': 'Ice', 'accuracy': 100, 'pp': 30}, - 'Water Gun': {'id': 55, 'power': 40, 'type': 'Water', 'accuracy': 100, 'pp': 25}, - 'Hydro Pump': {'id': 56, 'power': 120, 'type': 'Water', 'accuracy': 80, 'pp': 5}, - 'Surf': {'id': 57, 'power': 95, 'type': 'Water', 'accuracy': 100, 'pp': 15}, - 'Ice Beam': {'id': 58, 'power': 95, 'type': 'Ice', 'accuracy': 100, 'pp': 10}, - 'Blizzard': {'id': 59, 'power': 120, 'type': 'Ice', 'accuracy': 90, 'pp': 5}, - 'Psybeam': {'id': 60, 'power': 65, 'type': 'Psychic', 'accuracy': 100, 'pp': 20}, - 'Bubblebeam': {'id': 61, 'power': 65, 'type': 'Water', 'accuracy': 100, 'pp': 20}, - 'Aurora Beam': {'id': 62, 'power': 65, 'type': 'Ice', 'accuracy': 100, 'pp': 20}, - 'Hyper Beam': {'id': 63, 'power': 150, 'type': 'Normal', 'accuracy': 90, 'pp': 5}, - 'Peck': {'id': 64, 'power': 35, 'type': 'Flying', 'accuracy': 100, 'pp': 35}, - 'Drill Peck': {'id': 65, 'power': 80, 'type': 'Flying', 'accuracy': 100, 'pp': 20}, - 'Submission': {'id': 66, 'power': 80, 'type': 'Fighting', 'accuracy': 80, 'pp': 25}, - 'Low Kick': {'id': 67, 'power': 50, 'type': 'Fighting', 'accuracy': 90, 'pp': 20}, - 'Counter': {'id': 68, 'power': 1, 'type': 'Fighting', 'accuracy': 100, 'pp': 20}, - 'Seismic Toss': {'id': 69, 'power': 1, 'type': 'Fighting', 'accuracy': 100, 'pp': 20}, - 'Strength': {'id': 70, 'power': 80, 'type': 'Normal', 'accuracy': 100, 'pp': 15}, - 'Absorb': {'id': 71, 'power': 20, 'type': 'Grass', 'accuracy': 100, 'pp': 20}, - 'Mega Drain': {'id': 72, 'power': 40, 'type': 'Grass', 'accuracy': 100, 'pp': 10}, - 'Leech Seed': {'id': 73, 'power': 0, 'type': 'Grass', 'accuracy': 90, 'pp': 10}, - 'Growth': {'id': 74, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 40}, - 'Razor Leaf': {'id': 75, 'power': 55, 'type': 'Grass', 'accuracy': 95, 'pp': 25}, - 'Solarbeam': {'id': 76, 'power': 120, 'type': 'Grass', 'accuracy': 100, 'pp': 10}, - 'Poisonpowder': {'id': 77, 'power': 0, 'type': 'Poison', 'accuracy': 75, 'pp': 35}, - 'Stun Spore': {'id': 78, 'power': 0, 'type': 'Grass', 'accuracy': 75, 'pp': 30}, - 'Sleep Powder': {'id': 79, 'power': 0, 'type': 'Grass', 'accuracy': 75, 'pp': 15}, - 'Petal Dance': {'id': 80, 'power': 70, 'type': 'Grass', 'accuracy': 100, 'pp': 20}, - 'String Shot': {'id': 81, 'power': 0, 'type': 'Bug', 'accuracy': 95, 'pp': 40}, - 'Dragon Rage': {'id': 82, 'power': 1, 'type': 'Dragon', 'accuracy': 100, 'pp': 10}, - 'Fire Spin': {'id': 83, 'power': 15, 'type': 'Fire', 'accuracy': 70, 'pp': 15}, - 'Thundershock': {'id': 84, 'power': 40, 'type': 'Electric', 'accuracy': 100, 'pp': 30}, - 'Thunderbolt': {'id': 85, 'power': 95, 'type': 'Electric', 'accuracy': 100, 'pp': 15}, - 'Thunder Wave': {'id': 86, 'power': 0, 'type': 'Electric', 'accuracy': 100, 'pp': 20}, - 'Thunder': {'id': 87, 'power': 120, 'type': 'Electric', 'accuracy': 70, 'pp': 10}, - 'Rock Throw': {'id': 88, 'power': 50, 'type': 'Rock', 'accuracy': 65, 'pp': 15}, - 'Earthquake': {'id': 89, 'power': 100, 'type': 'Ground', 'accuracy': 100, 'pp': 10}, - 'Fissure': {'id': 90, 'power': 1, 'type': 'Ground', 'accuracy': 30, 'pp': 5}, - 'Dig': {'id': 91, 'power': 100, 'type': 'Ground', 'accuracy': 100, 'pp': 10}, - 'Toxic': {'id': 92, 'power': 0, 'type': 'Poison', 'accuracy': 85, 'pp': 10}, - 'Confusion': {'id': 93, 'power': 50, 'type': 'Psychic', 'accuracy': 100, 'pp': 25}, - 'Psychic': {'id': 94, 'power': 90, 'type': 'Psychic', 'accuracy': 100, 'pp': 10}, - 'Hypnosis': {'id': 95, 'power': 0, 'type': 'Psychic', 'accuracy': 60, 'pp': 20}, - 'Meditate': {'id': 96, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 40}, - 'Agility': {'id': 97, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 30}, - 'Quick Attack': {'id': 98, 'power': 40, 'type': 'Normal', 'accuracy': 100, 'pp': 30}, - 'Rage': {'id': 99, 'power': 20, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, - 'Teleport': {'id': 100, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 20}, - 'Night Shade': {'id': 101, 'power': 0, 'type': 'Ghost', 'accuracy': 100, 'pp': 15}, - 'Mimic': {'id': 102, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 10}, - 'Screech': {'id': 103, 'power': 0, 'type': 'Normal', 'accuracy': 85, 'pp': 40}, - 'Double Team': {'id': 104, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 15}, - 'Recover': {'id': 105, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, - 'Harden': {'id': 106, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30}, - 'Minimize': {'id': 107, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, - 'Smokescreen': {'id': 108, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, - 'Confuse Ray': {'id': 109, 'power': 0, 'type': 'Ghost', 'accuracy': 100, 'pp': 10}, - 'Withdraw': {'id': 110, 'power': 0, 'type': 'Water', 'accuracy': 100, 'pp': 40}, - 'Defense Curl': {'id': 111, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 40}, - 'Barrier': {'id': 112, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 30}, - 'Light Screen': {'id': 113, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 30}, - 'Haze': {'id': 114, 'power': 0, 'type': 'Ice', 'accuracy': 100, 'pp': 30}, - 'Reflect': {'id': 115, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 20}, - 'Focus Energy': {'id': 116, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30}, - 'Bide': {'id': 117, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 10}, - 'Metronome': {'id': 118, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 10}, - 'Mirror Move': {'id': 119, 'power': 0, 'type': 'Flying', 'accuracy': 100, 'pp': 20}, - 'Selfdestruct': {'id': 120, 'power': 130, 'type': 'Normal', 'accuracy': 100, 'pp': 5}, - 'Egg Bomb': {'id': 121, 'power': 100, 'type': 'Normal', 'accuracy': 75, 'pp': 10}, - 'Lick': {'id': 122, 'power': 20, 'type': 'Ghost', 'accuracy': 100, 'pp': 30}, - 'Smog': {'id': 123, 'power': 20, 'type': 'Poison', 'accuracy': 70, 'pp': 20}, - 'Sludge': {'id': 124, 'power': 65, 'type': 'Poison', 'accuracy': 100, 'pp': 20}, - 'Bone Club': {'id': 125, 'power': 65, 'type': 'Ground', 'accuracy': 85, 'pp': 20}, - 'Fire Blast': {'id': 126, 'power': 120, 'type': 'Fire', 'accuracy': 85, 'pp': 5}, - 'Waterfall': {'id': 127, 'power': 80, 'type': 'Water', 'accuracy': 100, 'pp': 15}, - 'Clamp': {'id': 128, 'power': 35, 'type': 'Water', 'accuracy': 75, 'pp': 10}, - 'Swift': {'id': 129, 'power': 60, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, - 'Skull Bash': {'id': 130, 'power': 100, 'type': 'Normal', 'accuracy': 100, 'pp': 15}, - 'Spike Cannon': {'id': 131, 'power': 20, 'type': 'Normal', 'accuracy': 100, 'pp': 15}, - 'Constrict': {'id': 132, 'power': 10, 'type': 'Normal', 'accuracy': 100, 'pp': 35}, - 'Amnesia': {'id': 133, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 20}, - 'Kinesis': {'id': 134, 'power': 0, 'type': 'Psychic', 'accuracy': 80, 'pp': 15}, - 'Softboiled': {'id': 135, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 10}, - 'Hi Jump Kick': {'id': 136, 'power': 85, 'type': 'Fighting', 'accuracy': 90, 'pp': 20}, - 'Glare': {'id': 137, 'power': 0, 'type': 'Normal', 'accuracy': 75, 'pp': 30}, - 'Dream Eater': {'id': 138, 'power': 100, 'type': 'Psychic', 'accuracy': 100, 'pp': 15}, - 'Poison Gas': {'id': 139, 'power': 0, 'type': 'Poison', 'accuracy': 55, 'pp': 40}, - 'Barrage': {'id': 140, 'power': 15, 'type': 'Normal', 'accuracy': 85, 'pp': 20}, - 'Leech Life': {'id': 141, 'power': 20, 'type': 'Bug', 'accuracy': 100, 'pp': 15}, - 'Lovely Kiss': {'id': 142, 'power': 0, 'type': 'Normal', 'accuracy': 75, 'pp': 10}, - 'Sky Attack': {'id': 143, 'power': 140, 'type': 'Flying', 'accuracy': 90, 'pp': 5}, - 'Transform': {'id': 144, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 10}, - 'Bubble': {'id': 145, 'power': 20, 'type': 'Water', 'accuracy': 100, 'pp': 30}, - 'Dizzy Punch': {'id': 146, 'power': 70, 'type': 'Normal', 'accuracy': 100, 'pp': 10}, - 'Spore': {'id': 147, 'power': 0, 'type': 'Grass', 'accuracy': 100, 'pp': 15}, - 'Flash': {'id': 148, 'power': 0, 'type': 'Normal', 'accuracy': 70, 'pp': 20}, - 'Psywave': {'id': 149, 'power': 1, 'type': 'Psychic', 'accuracy': 80, 'pp': 15}, - 'Splash': {'id': 150, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 40}, - 'Acid Armor': {'id': 151, 'power': 0, 'type': 'Poison', 'accuracy': 100, 'pp': 40}, - 'Crabhammer': {'id': 152, 'power': 90, 'type': 'Water', 'accuracy': 85, 'pp': 10}, - 'Explosion': {'id': 153, 'power': 170, 'type': 'Normal', 'accuracy': 100, 'pp': 5}, - 'Fury Swipes': {'id': 154, 'power': 18, 'type': 'Normal', 'accuracy': 80, 'pp': 15}, - 'Bonemerang': {'id': 155, 'power': 50, 'type': 'Ground', 'accuracy': 90, 'pp': 10}, - 'Rest': {'id': 156, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 10}, - 'Rock Slide': {'id': 157, 'power': 75, 'type': 'Rock', 'accuracy': 90, 'pp': 10}, - 'Hyper Fang': {'id': 158, 'power': 80, 'type': 'Normal', 'accuracy': 90, 'pp': 15}, - 'Sharpen': {'id': 159, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30}, - 'Conversion': {'id': 160, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30}, - 'Tri Attack': {'id': 161, 'power': 80, 'type': 'Normal', 'accuracy': 100, 'pp': 10}, - 'Super Fang': {'id': 162, 'power': 1, 'type': 'Normal', 'accuracy': 90, 'pp': 10}, - 'Slash': {'id': 163, 'power': 70, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, - 'Substitute': {'id': 164, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 10}, - #'Struggle': {'id': 165, 'power': 50, 'type': 'Struggle_Type', 'accuracy': 100, 'pp': 10} + 'No Move': {'id': 0, 'power': 0, 'type': 'Typeless', 'accuracy': 0, 'pp': 0, 'effect': 0}, + 'Pound': {'id': 1, 'power': 40, 'type': 'Normal', 'accuracy': 100, 'pp': 35, 'effect': 0}, + 'Karate Chop': {'id': 2, 'power': 50, 'type': 'Normal', 'accuracy': 100, 'pp': 25, 'effect': 0}, + 'Doubleslap': {'id': 3, 'power': 15, 'type': 'Normal', 'accuracy': 85, 'pp': 10, 'effect': 29}, + 'Comet Punch': {'id': 4, 'power': 18, 'type': 'Normal', 'accuracy': 85, 'pp': 15, 'effect': 29}, + 'Mega Punch': {'id': 5, 'power': 80, 'type': 'Normal', 'accuracy': 85, 'pp': 20, 'effect': 0}, + 'Pay Day': {'id': 6, 'power': 40, 'type': 'Normal', 'accuracy': 100, 'pp': 20, 'effect': 16}, + 'Fire Punch': {'id': 7, 'power': 75, 'type': 'Fire', 'accuracy': 100, 'pp': 15, 'effect': 4}, + 'Ice Punch': {'id': 8, 'power': 75, 'type': 'Ice', 'accuracy': 100, 'pp': 15, 'effect': 5}, + 'Thunderpunch': {'id': 9, 'power': 75, 'type': 'Electric', 'accuracy': 100, 'pp': 15, 'effect': 6}, + 'Scratch': {'id': 10, 'power': 40, 'type': 'Normal', 'accuracy': 100, 'pp': 35, 'effect': 0}, + 'Vicegrip': {'id': 11, 'power': 55, 'type': 'Normal', 'accuracy': 100, 'pp': 30, 'effect': 0}, + 'Guillotine': {'id': 12, 'power': 1, 'type': 'Normal', 'accuracy': 30, 'pp': 5, 'effect': 38}, + 'Razor Wind': {'id': 13, 'power': 80, 'type': 'Normal', 'accuracy': 75, 'pp': 10, 'effect': 39}, + 'Swords Dance': {'id': 14, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30, 'effect': 50}, + 'Cut': {'id': 15, 'power': 50, 'type': 'Normal', 'accuracy': 95, 'pp': 30, 'effect': 0}, + 'Gust': {'id': 16, 'power': 40, 'type': 'Normal', 'accuracy': 100, 'pp': 35, 'effect': 0}, + 'Wing Attack': {'id': 17, 'power': 35, 'type': 'Flying', 'accuracy': 100, 'pp': 35, 'effect': 0}, + 'Whirlwind': {'id': 18, 'power': 0, 'type': 'Normal', 'accuracy': 85, 'pp': 20, 'effect': 28}, + 'Fly': {'id': 19, 'power': 70, 'type': 'Flying', 'accuracy': 95, 'pp': 15, 'effect': 43}, + 'Bind': {'id': 20, 'power': 15, 'type': 'Normal', 'accuracy': 75, 'pp': 20, 'effect': 42}, + 'Slam': {'id': 21, 'power': 80, 'type': 'Normal', 'accuracy': 75, 'pp': 20, 'effect': 0}, + 'Vine Whip': {'id': 22, 'power': 35, 'type': 'Grass', 'accuracy': 100, 'pp': 10, 'effect': 0}, + 'Stomp': {'id': 23, 'power': 65, 'type': 'Normal', 'accuracy': 100, 'pp': 20, 'effect': 37}, + 'Double Kick': {'id': 24, 'power': 30, 'type': 'Fighting', 'accuracy': 100, 'pp': 30, 'effect': 44}, + 'Mega Kick': {'id': 25, 'power': 120, 'type': 'Normal', 'accuracy': 75, 'pp': 5, 'effect': 0}, + 'Jump Kick': {'id': 26, 'power': 70, 'type': 'Fighting', 'accuracy': 95, 'pp': 25, 'effect': 45}, + 'Rolling Kick': {'id': 27, 'power': 60, 'type': 'Fighting', 'accuracy': 85, 'pp': 15, 'effect': 37}, + 'Sand Attack': {'id': 28, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 15, 'effect': 22}, + 'Headbutt': {'id': 29, 'power': 70, 'type': 'Normal', 'accuracy': 100, 'pp': 15, 'effect': 37}, + 'Horn Attack': {'id': 30, 'power': 65, 'type': 'Normal', 'accuracy': 100, 'pp': 25, 'effect': 0}, + 'Fury Attack': {'id': 31, 'power': 15, 'type': 'Normal', 'accuracy': 85, 'pp': 20, 'effect': 29}, + 'Horn Drill': {'id': 32, 'power': 1, 'type': 'Normal', 'accuracy': 30, 'pp': 5, 'effect': 38}, + 'Tackle': {'id': 33, 'power': 35, 'type': 'Normal', 'accuracy': 95, 'pp': 35, 'effect': 0}, + 'Body Slam': {'id': 34, 'power': 85, 'type': 'Normal', 'accuracy': 100, 'pp': 15, 'effect': 36}, + 'Wrap': {'id': 35, 'power': 15, 'type': 'Normal', 'accuracy': 85, 'pp': 20, 'effect': 42}, + 'Take Down': {'id': 36, 'power': 90, 'type': 'Normal', 'accuracy': 85, 'pp': 20, 'effect': 48}, + 'Thrash': {'id': 37, 'power': 90, 'type': 'Normal', 'accuracy': 100, 'pp': 20, 'effect': 27}, + 'Double Edge': {'id': 38, 'power': 100, 'type': 'Normal', 'accuracy': 100, 'pp': 15, 'effect': 48}, + 'Tail Whip': {'id': 39, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30, 'effect': 19}, + 'Poison Sting': {'id': 40, 'power': 15, 'type': 'Poison', 'accuracy': 100, 'pp': 35, 'effect': 2}, + 'Twineedle': {'id': 41, 'power': 25, 'type': 'Bug', 'accuracy': 100, 'pp': 20, 'effect': 77}, + 'Pin Missile': {'id': 42, 'power': 14, 'type': 'Bug', 'accuracy': 85, 'pp': 20, 'effect': 29}, + 'Leer': {'id': 43, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30, 'effect': 19}, + 'Bite': {'id': 44, 'power': 60, 'type': 'Normal', 'accuracy': 100, 'pp': 25, 'effect': 31}, + 'Growl': {'id': 45, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 40, 'effect': 18}, + 'Roar': {'id': 46, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 20, 'effect': 28}, + 'Sing': {'id': 47, 'power': 0, 'type': 'Normal', 'accuracy': 55, 'pp': 15, 'effect': 32}, + 'Supersonic': {'id': 48, 'power': 0, 'type': 'Normal', 'accuracy': 55, 'pp': 20, 'effect': 49}, + 'Sonicboom': {'id': 49, 'power': 1, 'type': 'Normal', 'accuracy': 90, 'pp': 20, 'effect': 41}, + 'Disable': {'id': 50, 'power': 0, 'type': 'Normal', 'accuracy': 55, 'pp': 20, 'effect': 86}, + 'Acid': {'id': 51, 'power': 40, 'type': 'Poison', 'accuracy': 100, 'pp': 30, 'effect': 69}, + 'Ember': {'id': 52, 'power': 40, 'type': 'Fire', 'accuracy': 100, 'pp': 25, 'effect': 4}, + 'Flamethrower': {'id': 53, 'power': 95, 'type': 'Fire', 'accuracy': 100, 'pp': 15, 'effect': 4}, + 'Mist': {'id': 54, 'power': 0, 'type': 'Ice', 'accuracy': 100, 'pp': 30, 'effect': 46}, + 'Water Gun': {'id': 55, 'power': 40, 'type': 'Water', 'accuracy': 100, 'pp': 25, 'effect': 0}, + 'Hydro Pump': {'id': 56, 'power': 120, 'type': 'Water', 'accuracy': 80, 'pp': 5, 'effect': 0}, + 'Surf': {'id': 57, 'power': 95, 'type': 'Water', 'accuracy': 100, 'pp': 15, 'effect': 0}, + 'Ice Beam': {'id': 58, 'power': 95, 'type': 'Ice', 'accuracy': 100, 'pp': 10, 'effect': 5}, + 'Blizzard': {'id': 59, 'power': 120, 'type': 'Ice', 'accuracy': 90, 'pp': 5, 'effect': 5}, + 'Psybeam': {'id': 60, 'power': 65, 'type': 'Psychic', 'accuracy': 100, 'pp': 20, 'effect': 76}, + 'Bubblebeam': {'id': 61, 'power': 65, 'type': 'Water', 'accuracy': 100, 'pp': 20, 'effect': 70}, + 'Aurora Beam': {'id': 62, 'power': 65, 'type': 'Ice', 'accuracy': 100, 'pp': 20, 'effect': 68}, + 'Hyper Beam': {'id': 63, 'power': 150, 'type': 'Normal', 'accuracy': 90, 'pp': 5, 'effect': 80}, + 'Peck': {'id': 64, 'power': 35, 'type': 'Flying', 'accuracy': 100, 'pp': 35, 'effect': 0}, + 'Drill Peck': {'id': 65, 'power': 80, 'type': 'Flying', 'accuracy': 100, 'pp': 20, 'effect': 0}, + 'Submission': {'id': 66, 'power': 80, 'type': 'Fighting', 'accuracy': 80, 'pp': 25, 'effect': 48}, + 'Low Kick': {'id': 67, 'power': 50, 'type': 'Fighting', 'accuracy': 90, 'pp': 20, 'effect': 37}, + 'Counter': {'id': 68, 'power': 1, 'type': 'Fighting', 'accuracy': 100, 'pp': 20, 'effect': 0}, + 'Seismic Toss': {'id': 69, 'power': 1, 'type': 'Fighting', 'accuracy': 100, 'pp': 20, 'effect': 41}, + 'Strength': {'id': 70, 'power': 80, 'type': 'Normal', 'accuracy': 100, 'pp': 15, 'effect': 0}, + 'Absorb': {'id': 71, 'power': 20, 'type': 'Grass', 'accuracy': 100, 'pp': 20, 'effect': 3}, + 'Mega Drain': {'id': 72, 'power': 40, 'type': 'Grass', 'accuracy': 100, 'pp': 10, 'effect': 3}, + 'Leech Seed': {'id': 73, 'power': 0, 'type': 'Grass', 'accuracy': 90, 'pp': 10, 'effect': 84}, + 'Growth': {'id': 74, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 40, 'effect': 13}, + 'Razor Leaf': {'id': 75, 'power': 55, 'type': 'Grass', 'accuracy': 95, 'pp': 25, 'effect': 0}, + 'Solarbeam': {'id': 76, 'power': 120, 'type': 'Grass', 'accuracy': 100, 'pp': 10, 'effect': 39}, + 'Poisonpowder': {'id': 77, 'power': 0, 'type': 'Poison', 'accuracy': 75, 'pp': 35, 'effect': 66}, + 'Stun Spore': {'id': 78, 'power': 0, 'type': 'Grass', 'accuracy': 75, 'pp': 30, 'effect': 67}, + 'Sleep Powder': {'id': 79, 'power': 0, 'type': 'Grass', 'accuracy': 75, 'pp': 15, 'effect': 32}, + 'Petal Dance': {'id': 80, 'power': 70, 'type': 'Grass', 'accuracy': 100, 'pp': 20, 'effect': 27}, + 'String Shot': {'id': 81, 'power': 0, 'type': 'Bug', 'accuracy': 95, 'pp': 40, 'effect': 20}, + 'Dragon Rage': {'id': 82, 'power': 1, 'type': 'Dragon', 'accuracy': 100, 'pp': 10, 'effect': 41}, + 'Fire Spin': {'id': 83, 'power': 15, 'type': 'Fire', 'accuracy': 70, 'pp': 15, 'effect': 42}, + 'Thundershock': {'id': 84, 'power': 40, 'type': 'Electric', 'accuracy': 100, 'pp': 30, 'effect': 6}, + 'Thunderbolt': {'id': 85, 'power': 95, 'type': 'Electric', 'accuracy': 100, 'pp': 15, 'effect': 6}, + 'Thunder Wave': {'id': 86, 'power': 0, 'type': 'Electric', 'accuracy': 100, 'pp': 20, 'effect': 67}, + 'Thunder': {'id': 87, 'power': 120, 'type': 'Electric', 'accuracy': 70, 'pp': 10, 'effect': 6}, + 'Rock Throw': {'id': 88, 'power': 50, 'type': 'Rock', 'accuracy': 65, 'pp': 15, 'effect': 0}, + 'Earthquake': {'id': 89, 'power': 100, 'type': 'Ground', 'accuracy': 100, 'pp': 10, 'effect': 0}, + 'Fissure': {'id': 90, 'power': 1, 'type': 'Ground', 'accuracy': 30, 'pp': 5, 'effect': 38}, + 'Dig': {'id': 91, 'power': 100, 'type': 'Ground', 'accuracy': 100, 'pp': 10, 'effect': 39}, + 'Toxic': {'id': 92, 'power': 0, 'type': 'Poison', 'accuracy': 85, 'pp': 10, 'effect': 66}, + 'Confusion': {'id': 93, 'power': 50, 'type': 'Psychic', 'accuracy': 100, 'pp': 25, 'effect': 76}, + 'Psychic': {'id': 94, 'power': 90, 'type': 'Psychic', 'accuracy': 100, 'pp': 10, 'effect': 71}, + 'Hypnosis': {'id': 95, 'power': 0, 'type': 'Psychic', 'accuracy': 60, 'pp': 20, 'effect': 32}, + 'Meditate': {'id': 96, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 40, 'effect': 10}, + 'Agility': {'id': 97, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 30, 'effect': 52}, + 'Quick Attack': {'id': 98, 'power': 40, 'type': 'Normal', 'accuracy': 100, 'pp': 30, 'effect': 0}, + 'Rage': {'id': 99, 'power': 20, 'type': 'Normal', 'accuracy': 100, 'pp': 20, 'effect': 81}, + 'Teleport': {'id': 100, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 20, 'effect': 28}, + 'Night Shade': {'id': 101, 'power': 0, 'type': 'Ghost', 'accuracy': 100, 'pp': 15, 'effect': 41}, + 'Mimic': {'id': 102, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 10, 'effect': 82}, + 'Screech': {'id': 103, 'power': 0, 'type': 'Normal', 'accuracy': 85, 'pp': 40, 'effect': 59}, + 'Double Team': {'id': 104, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 15, 'effect': 15}, + 'Recover': {'id': 105, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 20, 'effect': 56}, + 'Harden': {'id': 106, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30, 'effect': 11}, + 'Minimize': {'id': 107, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 20, 'effect': 15}, + 'Smokescreen': {'id': 108, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 20, 'effect': 22}, + 'Confuse Ray': {'id': 109, 'power': 0, 'type': 'Ghost', 'accuracy': 100, 'pp': 10, 'effect': 49}, + 'Withdraw': {'id': 110, 'power': 0, 'type': 'Water', 'accuracy': 100, 'pp': 40, 'effect': 11}, + 'Defense Curl': {'id': 111, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 40, 'effect': 11}, + 'Barrier': {'id': 112, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 30, 'effect': 51}, + 'Light Screen': {'id': 113, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 30, 'effect': 64}, + 'Haze': {'id': 114, 'power': 0, 'type': 'Ice', 'accuracy': 100, 'pp': 30, 'effect': 25}, + 'Reflect': {'id': 115, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 20, 'effect': 65}, + 'Focus Energy': {'id': 116, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30, 'effect': 47}, + 'Bide': {'id': 117, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 10, 'effect': 26}, + 'Metronome': {'id': 118, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 10, 'effect': 83}, + 'Mirror Move': {'id': 119, 'power': 0, 'type': 'Flying', 'accuracy': 100, 'pp': 20, 'effect': 9}, + 'Selfdestruct': {'id': 120, 'power': 130, 'type': 'Normal', 'accuracy': 100, 'pp': 5, 'effect': 7}, + 'Egg Bomb': {'id': 121, 'power': 100, 'type': 'Normal', 'accuracy': 75, 'pp': 10, 'effect': 0}, + 'Lick': {'id': 122, 'power': 20, 'type': 'Ghost', 'accuracy': 100, 'pp': 30, 'effect': 36}, + 'Smog': {'id': 123, 'power': 20, 'type': 'Poison', 'accuracy': 70, 'pp': 20, 'effect': 33}, + 'Sludge': {'id': 124, 'power': 65, 'type': 'Poison', 'accuracy': 100, 'pp': 20, 'effect': 33}, + 'Bone Club': {'id': 125, 'power': 65, 'type': 'Ground', 'accuracy': 85, 'pp': 20, 'effect': 31}, + 'Fire Blast': {'id': 126, 'power': 120, 'type': 'Fire', 'accuracy': 85, 'pp': 5, 'effect': 34}, + 'Waterfall': {'id': 127, 'power': 80, 'type': 'Water', 'accuracy': 100, 'pp': 15, 'effect': 0}, + 'Clamp': {'id': 128, 'power': 35, 'type': 'Water', 'accuracy': 75, 'pp': 10, 'effect': 42}, + 'Swift': {'id': 129, 'power': 60, 'type': 'Normal', 'accuracy': 100, 'pp': 20, 'effect': 17}, + 'Skull Bash': {'id': 130, 'power': 100, 'type': 'Normal', 'accuracy': 100, 'pp': 15, 'effect': 39}, + 'Spike Cannon': {'id': 131, 'power': 20, 'type': 'Normal', 'accuracy': 100, 'pp': 15, 'effect': 29}, + 'Constrict': {'id': 132, 'power': 10, 'type': 'Normal', 'accuracy': 100, 'pp': 35, 'effect': 70}, + 'Amnesia': {'id': 133, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 20, 'effect': 53}, + 'Kinesis': {'id': 134, 'power': 0, 'type': 'Psychic', 'accuracy': 80, 'pp': 15, 'effect': 22}, + 'Softboiled': {'id': 135, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 10, 'effect': 56}, + 'Hi Jump Kick': {'id': 136, 'power': 85, 'type': 'Fighting', 'accuracy': 90, 'pp': 20, 'effect': 45}, + 'Glare': {'id': 137, 'power': 0, 'type': 'Normal', 'accuracy': 75, 'pp': 30, 'effect': 67}, + 'Dream Eater': {'id': 138, 'power': 100, 'type': 'Psychic', 'accuracy': 100, 'pp': 15, 'effect': 8}, + 'Poison Gas': {'id': 139, 'power': 0, 'type': 'Poison', 'accuracy': 55, 'pp': 40, 'effect': 66}, + 'Barrage': {'id': 140, 'power': 15, 'type': 'Normal', 'accuracy': 85, 'pp': 20, 'effect': 29}, + 'Leech Life': {'id': 141, 'power': 20, 'type': 'Bug', 'accuracy': 100, 'pp': 15, 'effect': 3}, + 'Lovely Kiss': {'id': 142, 'power': 0, 'type': 'Normal', 'accuracy': 75, 'pp': 10, 'effect': 32}, + 'Sky Attack': {'id': 143, 'power': 140, 'type': 'Flying', 'accuracy': 90, 'pp': 5, 'effect': 39}, + 'Transform': {'id': 144, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 10, 'effect': 57}, + 'Bubble': {'id': 145, 'power': 20, 'type': 'Water', 'accuracy': 100, 'pp': 30, 'effect': 70}, + 'Dizzy Punch': {'id': 146, 'power': 70, 'type': 'Normal', 'accuracy': 100, 'pp': 10, 'effect': 0}, + 'Spore': {'id': 147, 'power': 0, 'type': 'Grass', 'accuracy': 100, 'pp': 15, 'effect': 32}, + 'Flash': {'id': 148, 'power': 0, 'type': 'Normal', 'accuracy': 70, 'pp': 20, 'effect': 22}, + 'Psywave': {'id': 149, 'power': 1, 'type': 'Psychic', 'accuracy': 80, 'pp': 15, 'effect': 41}, + 'Splash': {'id': 150, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 40, 'effect': 85}, + 'Acid Armor': {'id': 151, 'power': 0, 'type': 'Poison', 'accuracy': 100, 'pp': 40, 'effect': 51}, + 'Crabhammer': {'id': 152, 'power': 90, 'type': 'Water', 'accuracy': 85, 'pp': 10, 'effect': 0}, + 'Explosion': {'id': 153, 'power': 170, 'type': 'Normal', 'accuracy': 100, 'pp': 5, 'effect': 7}, + 'Fury Swipes': {'id': 154, 'power': 18, 'type': 'Normal', 'accuracy': 80, 'pp': 15, 'effect': 29}, + 'Bonemerang': {'id': 155, 'power': 50, 'type': 'Ground', 'accuracy': 90, 'pp': 10, 'effect': 44}, + 'Rest': {'id': 156, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 10, 'effect': 56}, + 'Rock Slide': {'id': 157, 'power': 75, 'type': 'Rock', 'accuracy': 90, 'pp': 10, 'effect': 0}, + 'Hyper Fang': {'id': 158, 'power': 80, 'type': 'Normal', 'accuracy': 90, 'pp': 15, 'effect': 31}, + 'Sharpen': {'id': 159, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30, 'effect': 10}, + 'Conversion': {'id': 160, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30, 'effect': 24}, + 'Tri Attack': {'id': 161, 'power': 80, 'type': 'Normal', 'accuracy': 100, 'pp': 10, 'effect': 0}, + 'Super Fang': {'id': 162, 'power': 1, 'type': 'Normal', 'accuracy': 90, 'pp': 10, 'effect': 40}, + 'Slash': {'id': 163, 'power': 70, 'type': 'Normal', 'accuracy': 100, 'pp': 20, 'effect': 0}, + 'Substitute': {'id': 164, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 10, 'effect': 79} + #'Struggle': {'id': 165, 'power': 50, 'type': 'Struggle_Type', 'accuracy': 100, 'pp': 10, 'effect': 48} } encounter_tables = {'Wild_Super_Rod_A': 2, 'Wild_Super_Rod_B': 2, 'Wild_Super_Rod_C': 3, 'Wild_Super_Rod_D': 2, @@ -1204,6 +1204,29 @@ tm_moves = [ 'Selfdestruct', 'Egg Bomb', 'Fire Blast', 'Swift', 'Skull Bash', 'Softboiled', 'Dream Eater', 'Sky Attack', 'Rest', 'Thunder Wave', 'Psywave', 'Explosion', 'Rock Slide', 'Tri Attack', 'Substitute' ] +#['No Move', 'Pound', 'Karate Chop', 'Doubleslap', 'Comet Punch', 'Fire Punch', 'Ice Punch', 'Thunderpunch', 'Scratch', +# 'Vicegrip', 'Guillotine', 'Cut', 'Gust', 'Wing Attack', 'Fly', 'Bind', 'Slam', 'Vine Whip', 'Stomp', 'Double Kick', 'Jump Kick', +# 'Rolling Kick', 'Sand Attack', 'Headbutt', 'Horn Attack', 'Fury Attack', 'Tackle', 'Wrap', 'Thrash', 'Tail Whip', 'Poison Sting', +# 'Twineedle', 'Pin Missile', 'Leer', 'Bite', 'Growl', 'Roar', 'Sing', 'Supersonic', 'Sonicboom', 'Disable', 'Acid', 'Ember', 'Flamethrower', +# 'Mist', 'Hydro Pump', 'Surf', 'Psybeam', 'Aurora Beam', 'Peck', 'Drill Peck', 'Low Kick', 'Strength', 'Absorb', 'Leech Seed', 'Growth', +# 'Razor Leaf', 'Poisonpowder', 'Stun Spore', 'Sleep Powder', 'Petal Dance', 'String Shot', 'Fire Spin', 'Thundershock', 'Rock Throw', 'Confusion', +# 'Hypnosis', 'Meditate', 'Agility', 'Quick Attack', 'Night Shade', 'Screech', 'Recover', 'Harden', 'Minimize', 'Smokescreen', 'Confuse Ray', 'Withdraw', +# 'Defense Curl', 'Barrier', 'Light Screen', 'Haze', 'Focus Energy', 'Mirror Move', 'Lick', 'Smog', 'Sludge', 'Bone Club', 'Waterfall', 'Clamp', 'Spike Cannon', +# 'Constrict', 'Amnesia', 'Kinesis', 'Hi Jump Kick', 'Glare', 'Poison Gas', 'Barrage', 'Leech Life', 'Lovely Kiss', 'Transform', 'Bubble', 'Dizzy Punch', 'Spore', 'Flash', +# 'Splash', 'Acid Armor', 'Crabhammer', 'Fury Swipes', 'Bonemerang', 'Hyper Fang', 'Sharpen', 'Conversion', 'Super Fang', 'Slash'] + +# print([i for i in list(moves.keys()) if i not in tm_moves]) +# filler_moves = [ +# "Razor Wind", "Whirlwind", "Counter", "Teleport", "Bide", "Skull Bash", "Sky Attack", "Psywave", +# "Pound", "Karate Chop", "Doubleslap", "Comet Punch", "Scratch", "Vicegrip", "Gust", "Wing Attack", "Bind", +# "Vine Whip", "Sand Attack", "Fury Attack", "Tackle", "Wrap", "Tail Whip", "Poison Sting", "Twineedle", +# "Leer", "Growl", "Roar", "Sing", "Supersonic", "Sonicboom", "Disable", "Acid", "Ember", "Mist", "Peck", "Absorb", +# "Growth", "Poisonpowder", "String Shot", "Meditate", "Agility", "Screech", "Double Team", "Harden", "Minimize", +# "Smokescreen", "Confuse Ray", "Withdraw", "Defense Curl", "Barrier", "Light Screen", "Haze", "Reflect", +# "Focus Energy", "Lick", "Smog", "Clamp", "Spike Cannon", "Constrict" +# +# ] + first_stage_pokemon = [pokemon for pokemon in pokemon_data.keys() if pokemon not in evolves_from] legendary_pokemon = ["Articuno", "Zapdos", "Moltres", "Mewtwo", "Mew"] diff --git a/worlds/pokemon_rb/regions.py b/worlds/pokemon_rb/regions.py index 674d24d148..98dbb3af8f 100644 --- a/worlds/pokemon_rb/regions.py +++ b/worlds/pokemon_rb/regions.py @@ -23,11 +23,12 @@ def create_regions(multiworld: MultiWorld, player: int): locations_per_region.setdefault(location.region, []) if location.inclusion(multiworld, player): locations_per_region[location.region].append(PokemonRBLocation(player, location.name, location.address, - location.rom_address)) + location.rom_address, location.type)) regions = [ create_region(multiworld, player, "Menu", locations_per_region), create_region(multiworld, player, "Anywhere", locations_per_region), create_region(multiworld, player, "Fossil", locations_per_region), + create_region(multiworld, player, "Pokedex", locations_per_region), create_region(multiworld, player, "Pallet Town", locations_per_region), create_region(multiworld, player, "Route 1", locations_per_region), create_region(multiworld, player, "Viridian City", locations_per_region), @@ -88,6 +89,7 @@ def create_regions(multiworld: MultiWorld, player: int): create_region(multiworld, player, "Route 8", locations_per_region), create_region(multiworld, player, "Route 8 Grass", locations_per_region), create_region(multiworld, player, "Celadon City", locations_per_region), + create_region(multiworld, player, "Celadon Game Corner", locations_per_region), create_region(multiworld, player, "Celadon Prize Corner", locations_per_region), create_region(multiworld, player, "Celadon Gym", locations_per_region), create_region(multiworld, player, "Route 16", locations_per_region), @@ -148,6 +150,7 @@ def create_regions(multiworld: MultiWorld, player: int): multiworld.regions += regions connect(multiworld, player, "Menu", "Anywhere", one_way=True) connect(multiworld, player, "Menu", "Pallet Town", one_way=True) + connect(multiworld, player, "Menu", "Pokedex", one_way=True) connect(multiworld, player, "Menu", "Fossil", lambda state: state.pokemon_rb_fossil_checks( state.multiworld.second_fossil_check_condition[player].value, player), one_way=True) connect(multiworld, player, "Pallet Town", "Route 1") @@ -220,6 +223,7 @@ def create_regions(multiworld: MultiWorld, player: int): connect(multiworld, player, "Route 8", "Route 8 Grass", lambda state: state.pokemon_rb_can_cut(player), one_way=True) connect(multiworld, player, "Route 7", "Celadon City") connect(multiworld, player, "Celadon City", "Celadon Gym", lambda state: state.pokemon_rb_can_cut(player), one_way=True) + connect(multiworld, player, "Celadon City", "Celadon Game Corner") connect(multiworld, player, "Celadon City", "Celadon Prize Corner") connect(multiworld, player, "Celadon City", "Route 16") connect(multiworld, player, "Route 16", "Route 16 North", lambda state: state.pokemon_rb_can_cut(player), one_way=True) diff --git a/worlds/pokemon_rb/rock_tunnel.py b/worlds/pokemon_rb/rock_tunnel.py new file mode 100644 index 0000000000..3a70709eb0 --- /dev/null +++ b/worlds/pokemon_rb/rock_tunnel.py @@ -0,0 +1,294 @@ +from .rom_addresses import rom_addresses + +disallowed1F = [[2, 2], [3, 2], [1, 8], [2, 8], [7, 7], [8, 7], [10, 4], [11, 4], [11, 12], + [11, 13], [16, 10], [17, 10], [18, 10], [16, 12], [17, 12], [18, 12]] +disallowed2F = [[16, 2], [17, 2], [18, 2], [15, 5], [15, 6], [10, 10], [11, 10], [12, 10], [7, 14], [8, 14], [1, 15], + [13, 15], [13, 16], [1, 12], [1, 10], [3, 5], [3, 6], [5, 6], [5, 7], [5, 8], [1, 2], [1, 3], [1, 4], + [11, 1]] + + +def randomize_rock_tunnel(data, random): + + seed = random.randint(0, 999999999999999999) + random.seed(seed) + + map1f = [] + map2f = [] + + address = rom_addresses["Map_Rock_Tunnel1F"] + for y in range(0, 18): + row = [] + for x in range(0, 20): + row.append(data[address]) + address += 1 + map1f.append(row) + + address = rom_addresses["Map_Rock_TunnelB1F"] + for y in range(0, 18): + row = [] + for x in range(0, 20): + row.append(data[address]) + address += 1 + map2f.append(row) + + current_map = map1f + + def floor(x, y): + current_map[y][x] = 1 + + def wide(x, y): + current_map[y][x] = 32 + current_map[y][x + 1] = 34 + + def tall(x, y): + current_map[y][x] = 23 + current_map[y + 1][x] = 31 + + def single(x, y): + current_map[y][x] = 2 + + # 0 = top left, 1 = middle, 2 = top right, 3 = bottom right + entrance_c = random.choice([0, 1, 2]) + exit_c = [0, 1, 3] + if entrance_c == 2: + exit_c.remove(1) + else: + exit_c.remove(entrance_c) + exit_c = random.choice(exit_c) + remaining = [i for i in [0, 1, 2, 3] if i not in [entrance_c, exit_c]] + + if entrance_c == 0: + floor(6, 3) + floor(6, 4) + tall(random.randint(8, 10), 2) + wide(4, random.randint(5, 7)) + wide(1, random.choice([5, 6, 7, 9])) + elif entrance_c == 1: + if remaining == [0, 2] or random.randint(0, 1): + tall(random.randint(8, 10), 2) + floor(7, 4) + floor(8, 4) + else: + tall(random.randint(11, 12), 5) + floor(9, 5) + floor(9, 6) + elif entrance_c == 2: + floor(16, 2) + floor(16, 3) + if remaining == [1, 3]: + wide(17, 4) + else: + tall(random.randint(11, 17), random.choice([2, 5])) + + if exit_c == 0: + r = random.sample([0, 1, 2], 2) + if 0 in r: + floor(1, 11) + floor(2, 11) + if 1 in r: + floor(3, 11) + floor(4, 11) + if 2 in r: + floor(5, 11) + floor(6, 11) + elif exit_c == 1 or (exit_c == 3 and entrance_c == 0): + r = random.sample([1, 3, 5, 7], random.randint(1, 2)) + for i in r: + floor(i, 11) + floor(i + 1, 11) + if exit_c != 3: + tall(random.choice([9, 10, 12]), 12) + + # 0 = top left, 1 = middle, 2 = top right, 3 = bottom right + # [0, 1] [0, 2] [1, 2] [1, 3], [2, 3] + if remaining[0] == 1: + floor(9, 5) + floor(9, 6) + + if remaining == [0, 2]: + if random.randint(0, 1): + tall(9, 4) + floor(9, 6) + floor(9, 7) + else: + floor(10, 7) + floor(11, 7) + + if remaining == [1, 2]: + floor(16, 2) + floor(16, 3) + tall(random.randint(11, 17), random.choice([2, 5])) + if remaining in [[1, 3], [2, 3]]: + r = round(random.triangular(0, 3, 0)) + floor(12 + (r * 2), 7) + if r < 3: + floor(13 + (r * 2), 7) + if remaining == [1, 3]: + wide(10, random.choice([3, 5])) + + if remaining != [0, 1] and exit_c != 1: + wide(7, 6) + + if entrance_c != 0: + if random.randint(0, 1): + wide(4, random.randint(4, 7)) + else: + wide(1, random.choice([5, 6, 7, 9])) + + current_map = map2f + + if 3 in remaining: + c = random.choice([entrance_c, exit_c]) + else: + c = random.choice(remaining) + + # 0 = top right, 1 = middle, 2 = bottom right, 3 = top left + if c in [0, 1]: + if random.randint(0, 2): + tall(random.choice([2, 4]), 5) + r = random.choice([1, 3, 7, 9, 11]) + floor(3 if r < 11 else random.randint(1, 2), r) + floor(3 if r < 11 else random.randint(1, 2), r + 1) + if random.randint(0, 2): + tall(random.randint(6, 7), 7) + r = random.choice([1, 3, 5, 9]) + floor(6, r) + floor(6, r + 1) + if random.randint(0, 2): + wide(7, 15) + r = random.randint(0, 4) + if r == 0: + floor(9, 14) + floor(10, 14) + elif r == 1: + floor(11, 14) + floor(12, 14) + elif r == 2: + floor(13, 13) + floor(13, 14) + elif r == 3: + floor(13, 11) + floor(13, 12) + elif r == 4: + floor(13, 10) + floor(14, 10) + if c == 0: + tall(random.randint(9, 10), 5) + if random.randint(0, 1): + floor(10, 7) + floor(11, 7) + tall(random.randint(12, 17), 8) + else: + floor(12, 5) + floor(12, 6) + wide(13, random.randint(4, 5)) + wide(17, random.randint(3, 5)) + r = random.choice([1, 3]) + floor(12, r) + floor(12, + 1) + + elif c == 2: + r = random.randint(0, 6) + if r == 0: + floor(12, 1) + floor(12, 2) + elif r == 1: + floor(12, 3) + floor(12, 4) + elif r == 2: + floor(12, 5) + floor(12, 6) + elif r == 3: + floor(10, 7) + floor(11, 7) + elif r == 4: + floor(9, 7) + floor(9, 8) + elif r == 5: + floor(9, 9) + floor(9, 10) + elif r == 6: + floor(8, 11) + floor(9, 11) + if r < 2 or (r in [2, 3] and random.randint(0, 1)): + wide(7, random.randint(6, 7)) + elif r in [2, 3]: + tall(random.randint(9, 10), 5) + else: + tall(random.randint(6, 7), 7) + r = random.randint(r, 6) + if r == 0: + #early block + wide(13, random.randint(2, 5)) + tall(random.randint(14, 15), 1) + elif r == 1: + if random.randint(0, 1): + tall(16, 5) + tall(random.choice([14, 15, 17]), 1) + else: + wide(16, random.randint(6,8)) + single(18, 7) + elif r == 2: + tall(random.randint(12, 16), 8) + elif r == 3: + wide(10, 9) + single(12, 9) + elif r == 4: + wide(10, random.randint(11, 12)) + single(12, random.randint(11, 12)) + elif r == 5: + tall(random.randint(8, 10), 12) + elif r == 6: + wide(7, 15) + r = random.randint(r, 6) + if r == 6: + #late open + r2 = random.randint(0, 2) + floor(1 + (r2 * 2), 14) + floor(2 + (r2 * 2), 14) + elif r == 5: + floor(6, 12) + floor(6, 13) + elif r == 4: + if random.randint(0, 1): + floor(6, 11) + floor(7, 11) + else: + floor(8, 11) + floor(9, 11) + elif r == 3: + floor(9, 9) + floor(9, 10) + elif r < 3: + single(9, 7) + floor(9, 8) + + def check_addable_block(check_map, disallowed): + if check_map[y][x] == 1 and [x, y] not in disallowed: + i = 0 + for xx in range(x-1, x+2): + for yy in range(y-1, y+2): + if check_map[yy][xx] == 1: + i += 1 + if i >= 8: + single(x, y) + + for _ in range(100): + y = random.randint(1, 16) + x = random.randint(1, 18) + current_map = map1f + check_addable_block(map1f, disallowed1F) + current_map = map2f + check_addable_block(map2f, disallowed2F) + + address = rom_addresses["Map_Rock_Tunnel1F"] + for y in map1f: + for x in y: + data[address] = x + address += 1 + address = rom_addresses["Map_Rock_TunnelB1F"] + for y in map2f: + for x in y: + data[address] = x + address += 1 + return seed \ No newline at end of file diff --git a/worlds/pokemon_rb/rom.py b/worlds/pokemon_rb/rom.py index 9dbc3a8b83..76f51318dc 100644 --- a/worlds/pokemon_rb/rom.py +++ b/worlds/pokemon_rb/rom.py @@ -8,6 +8,7 @@ from .text import encode_text from .rom_addresses import rom_addresses from .locations import location_data from .items import item_table +from .rock_tunnel import randomize_rock_tunnel import worlds.pokemon_rb.poke_data as poke_data @@ -28,15 +29,15 @@ def filter_moves(moves, type, random): return ret -def get_move(moves, chances, random, starting_move=False): +def get_move(local_move_data, moves, chances, random, starting_move=False): type = choose_forced_type(chances, random) filtered_moves = filter_moves(moves, type, random) for move in filtered_moves: - if poke_data.moves[move]["accuracy"] > 80 and poke_data.moves[move]["power"] > 0 or not starting_move: + if local_move_data[move]["accuracy"] > 80 and local_move_data[move]["power"] > 0 or not starting_move: moves.remove(move) return move else: - return get_move(moves, [], random, starting_move) + return get_move(local_move_data, moves, [], random, starting_move) def get_encounter_slots(self): @@ -75,6 +76,42 @@ def randomize_pokemon(self, mon, mons_list, randomize_type, random): return mon +def set_mon_palettes(self, random, data): + if self.multiworld.randomize_pokemon_palettes[self.player] == "vanilla": + return + pallet_map = { + "Poison": 0x0F, + "Normal": 0x10, + "Ice": 0x11, + "Fire": 0x12, + "Water": 0x13, + "Ghost": 0x14, + "Ground": 0x15, + "Grass": 0x16, + "Psychic": 0x17, + "Electric": 0x18, + "Rock": 0x19, + "Dragon": 0x1F, + "Flying": 0x20, + "Fighting": 0x21, + "Bug": 0x22 + } + palettes = [] + for mon in poke_data.pokemon_data: + if self.multiworld.randomize_pokemon_palettes[self.player] == "primary_type": + pallet = pallet_map[self.local_poke_data[mon]["type1"]] + elif (self.multiworld.randomize_pokemon_palettes[self.player] == "follow_evolutions" and mon in + poke_data.evolves_from and poke_data.evolves_from[mon] != "Eevee"): + pallet = palettes[-1] + else: # completely_random or follow_evolutions and it is not an evolved form (except eeveelutions) + pallet = random.choice(list(pallet_map.values())) + palettes.append(pallet) + address = rom_addresses["Mon_Palettes"] + for pallet in palettes: + data[address] = pallet + address += 1 + + def process_trainer_data(self, data, random): mons_list = [pokemon for pokemon in poke_data.pokemon_data.keys() if pokemon not in poke_data.legendary_pokemon or self.multiworld.trainer_legendaries[self.player].value] @@ -163,6 +200,7 @@ def process_static_pokemon(self): randomize_type, self.multiworld.random)) location.place_locked_item(mon) + chosen_mons = set() for slot in starter_slots: location = self.multiworld.get_location(slot.name, self.player) randomize_type = self.multiworld.randomize_starter_pokemon[self.player].value @@ -170,9 +208,13 @@ def process_static_pokemon(self): if not randomize_type: location.place_locked_item(self.create_item(slot_type + " " + slot.original_item)) else: - location.place_locked_item(self.create_item(slot_type + " " + - randomize_pokemon(self, slot.original_item, mons_list, randomize_type, - self.multiworld.random))) + mon = self.create_item(slot_type + " " + randomize_pokemon(self, slot.original_item, mons_list, + randomize_type, self.multiworld.random)) + while mon.name in chosen_mons: + mon = self.create_item(slot_type + " " + randomize_pokemon(self, slot.original_item, mons_list, + randomize_type, self.multiworld.random)) + chosen_mons.add(mon.name) + location.place_locked_item(mon) def process_wild_pokemon(self): @@ -180,27 +222,36 @@ def process_wild_pokemon(self): encounter_slots = get_encounter_slots(self) placed_mons = {pokemon: 0 for pokemon in poke_data.pokemon_data.keys()} + zone_mapping = {} if self.multiworld.randomize_wild_pokemon[self.player].value: mons_list = [pokemon for pokemon in poke_data.pokemon_data.keys() if pokemon not in poke_data.legendary_pokemon or self.multiworld.randomize_legendary_pokemon[self.player].value == 3] self.multiworld.random.shuffle(encounter_slots) locations = [] for slot in encounter_slots: - mon = randomize_pokemon(self, slot.original_item, mons_list, - self.multiworld.randomize_wild_pokemon[self.player].value, self.multiworld.random) + location = self.multiworld.get_location(slot.name, self.player) + zone = " - ".join(location.name.split(" - ")[:-1]) + if zone not in zone_mapping: + zone_mapping[zone] = {} + original_mon = slot.original_item + if self.multiworld.area_1_to_1_mapping[self.player] and original_mon in zone_mapping[zone]: + mon = zone_mapping[zone][original_mon] + else: + mon = randomize_pokemon(self, original_mon, mons_list, + self.multiworld.randomize_wild_pokemon[self.player].value, self.multiworld.random) # if static Pokemon are not randomized, we make sure nothing on Pokemon Tower 6F is a Marowak # if static Pokemon are randomized we deal with that during static encounter randomization while (self.multiworld.randomize_static_pokemon[self.player].value == 0 and mon == "Marowak" and "Pokemon Tower 6F" in slot.name): # to account for the possibility that only one ground type Pokemon exists, match only stats for this fix - mon = randomize_pokemon(self, slot.original_item, mons_list, 2, self.multiworld.random) + mon = randomize_pokemon(self, original_mon, mons_list, 2, self.multiworld.random) placed_mons[mon] += 1 - location = self.multiworld.get_location(slot.name, self.player) location.item = self.create_item(mon) location.event = True location.locked = True location.item.location = location locations.append(location) + zone_mapping[zone][original_mon] = mon mons_to_add = [] remaining_pokemon = [pokemon for pokemon in poke_data.pokemon_data.keys() if placed_mons[pokemon] == 0 and @@ -223,22 +274,46 @@ def process_wild_pokemon(self): for mon in mons_to_add: stat_base = get_base_stat_total(mon) candidate_locations = get_encounter_slots(self) - if self.multiworld.randomize_wild_pokemon[self.player].value in [1, 3]: - candidate_locations = [slot for slot in candidate_locations if any([poke_data.pokemon_data[slot.original_item][ - "type1"] in [self.local_poke_data[mon]["type1"], self.local_poke_data[mon]["type2"]], - poke_data.pokemon_data[slot.original_item]["type2"] in [self.local_poke_data[mon]["type1"], - self.local_poke_data[mon]["type2"]]])] - if not candidate_locations: - candidate_locations = get_encounter_slots(self) + if self.multiworld.randomize_wild_pokemon[self.player].current_key in ["match_base_stats", "match_types_and_base_stats"]: + candidate_locations.sort(key=lambda slot: abs(get_base_stat_total(slot.original_item) - stat_base)) + if self.multiworld.randomize_wild_pokemon[self.player].current_key in ["match_types", "match_types_and_base_stats"]: + candidate_locations.sort(key=lambda slot: not any([poke_data.pokemon_data[slot.original_item]["type1"] in + [self.local_poke_data[mon]["type1"], self.local_poke_data[mon]["type2"]], + poke_data.pokemon_data[slot.original_item]["type2"] in + [self.local_poke_data[mon]["type1"], self.local_poke_data[mon]["type2"]]])) candidate_locations = [self.multiworld.get_location(location.name, self.player) for location in candidate_locations] - candidate_locations.sort(key=lambda slot: abs(get_base_stat_total(slot.item.name) - stat_base)) for location in candidate_locations: - if placed_mons[location.item.name] > 1 or location.item.name not in poke_data.first_stage_pokemon: - placed_mons[location.item.name] -= 1 - location.item = self.create_item(mon) - location.item.location = location + zone = " - ".join(location.name.split(" - ")[:-1]) + if self.multiworld.catch_em_all[self.player] == "all_pokemon" and self.multiworld.area_1_to_1_mapping[self.player]: + if not [self.multiworld.get_location(l.name, self.player) for l in get_encounter_slots(self) + if (not l.name.startswith(zone)) and + self.multiworld.get_location(l.name, self.player).item.name == location.item.name]: + continue + if self.multiworld.catch_em_all[self.player] == "first_stage" and self.multiworld.area_1_to_1_mapping[self.player]: + if not [self.multiworld.get_location(l.name, self.player) for l in get_encounter_slots(self) + if (not l.name.startswith(zone)) and + self.multiworld.get_location(l.name, self.player).item.name == location.item.name and l.name + not in poke_data.evolves_from]: + continue + + if placed_mons[location.item.name] < 2 and (location.item.name in poke_data.first_stage_pokemon + or self.multiworld.catch_em_all[self.player]): + continue + + if self.multiworld.area_1_to_1_mapping[self.player]: + place_locations = [place_location for place_location in candidate_locations if + place_location.name.startswith(zone) and + place_location.item.name == location.item.name] + else: + place_locations = [location] + for place_location in place_locations: + placed_mons[place_location.item.name] -= 1 + place_location.item = self.create_item(mon) + place_location.item.location = place_location placed_mons[mon] += 1 - break + break + else: + raise Exception else: for slot in encounter_slots: @@ -250,10 +325,41 @@ def process_wild_pokemon(self): placed_mons[location.item.name] += 1 +def process_move_data(self): + self.local_move_data = deepcopy(poke_data.moves) + if self.multiworld.move_balancing[self.player]: + self.local_move_data["Sing"]["accuracy"] = 30 + self.local_move_data["Sleep Powder"]["accuracy"] = 40 + self.local_move_data["Spore"]["accuracy"] = 50 + self.local_move_data["Sonicboom"]["effect"] = 0 + self.local_move_data["Sonicboom"]["power"] = 50 + self.local_move_data["Dragon Rage"]["effect"] = 0 + self.local_move_data["Dragon Rage"]["power"] = 80 + self.local_move_data["Horn Drill"]["effect"] = 0 + self.local_move_data["Horn Drill"]["power"] = 70 + self.local_move_data["Horn Drill"]["accuracy"] = 90 + self.local_move_data["Guillotine"]["effect"] = 0 + self.local_move_data["Guillotine"]["power"] = 70 + self.local_move_data["Guillotine"]["accuracy"] = 90 + self.local_move_data["Fissure"]["effect"] = 0 + self.local_move_data["Fissure"]["power"] = 70 + self.local_move_data["Fissure"]["accuracy"] = 90 + self.local_move_data["Blizzard"]["accuracy"] = 70 + if self.multiworld.randomize_tm_moves[self.player]: + self.local_tms = self.multiworld.random.sample([move for move in poke_data.moves.keys() if move not in + ["No Move"] + poke_data.hm_moves], 50) + else: + self.local_tms = poke_data.tm_moves.copy() + + def process_pokemon_data(self): local_poke_data = deepcopy(poke_data.pokemon_data) learnsets = deepcopy(poke_data.learnsets) + tms_hms = self.local_tms + poke_data.hm_moves + + + compat_hms = set() for mon, mon_data in local_poke_data.items(): if self.multiworld.randomize_pokemon_stats[self.player].value == 1: @@ -265,18 +371,21 @@ def process_pokemon_data(self): mon_data["spd"] = stats[3] mon_data["spc"] = stats[4] elif self.multiworld.randomize_pokemon_stats[self.player].value == 2: - old_stats = mon_data["hp"] + mon_data["atk"] + mon_data["def"] + mon_data["spd"] + mon_data["spc"] - 5 - stats = [1, 1, 1, 1, 1] - while old_stats > 0: - stat = self.multiworld.random.randint(0, 4) - if stats[stat] < 255: - old_stats -= 1 - stats[stat] += 1 - mon_data["hp"] = stats[0] - mon_data["atk"] = stats[1] - mon_data["def"] = stats[2] - mon_data["spd"] = stats[3] - mon_data["spc"] = stats[4] + first_run = True + while (mon_data["hp"] > 255 or mon_data["atk"] > 255 or mon_data["def"] > 255 or mon_data["spd"] > 255 + or mon_data["spc"] > 255 or first_run): + first_run = False + total_stats = mon_data["hp"] + mon_data["atk"] + mon_data["def"] + mon_data["spd"] + mon_data["spc"] - 60 + dist = [self.multiworld.random.randint(1, 101) / 100, self.multiworld.random.randint(1, 101) / 100, + self.multiworld.random.randint(1, 101) / 100, self.multiworld.random.randint(1, 101) / 100, + self.multiworld.random.randint(1, 101) / 100] + total_dist = sum(dist) + + mon_data["hp"] = int(round(dist[0] / total_dist * total_stats) + 20) + mon_data["atk"] = int(round(dist[1] / total_dist * total_stats) + 10) + mon_data["def"] = int(round(dist[2] / total_dist * total_stats) + 10) + mon_data["spd"] = int(round(dist[3] / total_dist * total_stats) + 10) + mon_data["spc"] = int(round(dist[4] / total_dist * total_stats) + 10) if self.multiworld.randomize_pokemon_types[self.player].value: if self.multiworld.randomize_pokemon_types[self.player].value == 1 and mon in poke_data.evolves_from: type1 = local_poke_data[poke_data.evolves_from[mon]]["type1"] @@ -318,46 +427,237 @@ def process_pokemon_data(self): moves = list(poke_data.moves.keys()) for move in ["No Move"] + poke_data.hm_moves: moves.remove(move) - mon_data["start move 1"] = get_move(moves, chances, self.multiworld.random, True) - for i in range(2, 5): - if mon_data[f"start move {i}"] != "No Move" or self.multiworld.start_with_four_moves[ - self.player].value == 1: - mon_data[f"start move {i}"] = get_move(moves, chances, self.multiworld.random) + if self.multiworld.confine_transform_to_ditto[self.player]: + moves.remove("Transform") + if self.multiworld.start_with_four_moves[self.player]: + num_moves = 4 + else: + num_moves = len([i for i in [mon_data["start move 1"], mon_data["start move 2"], + mon_data["start move 3"], mon_data["start move 4"]] if i != "No Move"]) if mon in learnsets: - for move_num in range(0, len(learnsets[mon])): - learnsets[mon][move_num] = get_move(moves, chances, self.multiworld.random) + num_moves += len(learnsets[mon]) + non_power_moves = [] + learnsets[mon] = [] + for i in range(num_moves): + if i == 0 and mon == "Ditto" and self.multiworld.confine_transform_to_ditto[self.player]: + move = "Transform" + else: + move = get_move(self.local_move_data, moves, chances, self.multiworld.random) + while move == "Transform" and self.multiworld.confine_transform_to_ditto[self.player]: + move = get_move(self.local_move_data, moves, chances, self.multiworld.random) + if self.local_move_data[move]["power"] < 5: + non_power_moves.append(move) + else: + learnsets[mon].append(move) + learnsets[mon].sort(key=lambda move: self.local_move_data[move]["power"]) + if learnsets[mon]: + for move in non_power_moves: + learnsets[mon].insert(self.multiworld.random.randint(1, len(learnsets[mon])), move) + else: + learnsets[mon] = non_power_moves + for i in range(1, 5): + if mon_data[f"start move {i}"] != "No Move" or self.multiworld.start_with_four_moves[self.player]: + mon_data[f"start move {i}"] = learnsets[mon].pop(0) + if self.multiworld.randomize_pokemon_catch_rates[self.player].value: mon_data["catch rate"] = self.multiworld.random.randint(self.multiworld.minimum_catch_rate[self.player], 255) else: mon_data["catch rate"] = max(self.multiworld.minimum_catch_rate[self.player], mon_data["catch rate"]) - if mon != "Mew": - tms_hms = poke_data.tm_moves + poke_data.hm_moves - for flag, tm_move in enumerate(tms_hms): - if ((mon in poke_data.evolves_from.keys() and mon_data["type1"] == - local_poke_data[poke_data.evolves_from[mon]]["type1"] and mon_data["type2"] == - local_poke_data[poke_data.evolves_from[mon]]["type2"]) and ( - (flag < 50 and self.multiworld.tm_compatibility[self.player].value in [1, 2]) or ( - flag >= 51 and self.multiworld.hm_compatibility[self.player].value in [1, 2]))): - bit = 1 if local_poke_data[poke_data.evolves_from[mon]]["tms"][int(flag / 8)] & 1 << (flag % 8) else 0 - elif (flag < 50 and self.multiworld.tm_compatibility[self.player].value == 1) or (flag >= 50 and self.multiworld.hm_compatibility[self.player].value == 1): - type_match = poke_data.moves[tm_move]["type"] in [mon_data["type1"], mon_data["type2"]] - bit = int(self.multiworld.random.randint(1, 100) < [[90, 50, 25], [100, 75, 25]][flag >= 50][0 if type_match else 1 if poke_data.moves[tm_move]["type"] == "Normal" else 2]) - elif (flag < 50 and self.multiworld.tm_compatibility[self.player].value == 2) or (flag >= 50 and self.multiworld.hm_compatibility[self.player].value == 2): - bit = self.multiworld.random.randint(0, 1) - elif (flag < 50 and self.multiworld.tm_compatibility[self.player].value == 3) or (flag >= 50 and self.multiworld.hm_compatibility[self.player].value == 3): + def roll_tm_compat(roll_move): + if self.local_move_data[roll_move]["type"] in [mon_data["type1"], mon_data["type2"]]: + if roll_move in poke_data.hm_moves: + if self.multiworld.hm_same_type_compatibility[self.player].value == -1: + return mon_data["tms"][int(flag / 8)] + r = self.multiworld.random.randint(1, 100) <= self.multiworld.hm_same_type_compatibility[self.player].value + if r and mon not in poke_data.legendary_pokemon: + compat_hms.add(roll_move) + return r + else: + if self.multiworld.tm_same_type_compatibility[self.player].value == -1: + return mon_data["tms"][int(flag / 8)] + return self.multiworld.random.randint(1, 100) <= self.multiworld.tm_same_type_compatibility[self.player].value + elif self.local_move_data[roll_move]["type"] == "Normal" and "Normal" not in [mon_data["type1"], mon_data["type2"]]: + if roll_move in poke_data.hm_moves: + if self.multiworld.hm_normal_type_compatibility[self.player].value == -1: + return mon_data["tms"][int(flag / 8)] + r = self.multiworld.random.randint(1, 100) <= self.multiworld.hm_normal_type_compatibility[self.player].value + if r and mon not in poke_data.legendary_pokemon: + compat_hms.add(roll_move) + return r + else: + if self.multiworld.tm_normal_type_compatibility[self.player].value == -1: + return mon_data["tms"][int(flag / 8)] + return self.multiworld.random.randint(1, 100) <= self.multiworld.tm_normal_type_compatibility[self.player].value + else: + if roll_move in poke_data.hm_moves: + if self.multiworld.hm_other_type_compatibility[self.player].value == -1: + return mon_data["tms"][int(flag / 8)] + r = self.multiworld.random.randint(1, 100) <= self.multiworld.hm_other_type_compatibility[self.player].value + if r and mon not in poke_data.legendary_pokemon: + compat_hms.add(roll_move) + return r + else: + if self.multiworld.tm_other_type_compatibility[self.player].value == -1: + return mon_data["tms"][int(flag / 8)] + return self.multiworld.random.randint(1, 100) <= self.multiworld.tm_other_type_compatibility[self.player].value + + + for flag, tm_move in enumerate(tms_hms): + if mon in poke_data.evolves_from.keys() and self.multiworld.inherit_tm_hm_compatibility[self.player]: + + if local_poke_data[poke_data.evolves_from[mon]]["tms"][int(flag / 8)] & 1 << (flag % 8): + # always inherit learnable tms/hms bit = 1 else: - continue - if bit: - mon_data["tms"][int(flag / 8)] |= 1 << (flag % 8) - else: - mon_data["tms"][int(flag / 8)] &= ~(1 << (flag % 8)) + if self.local_move_data[tm_move]["type"] in [mon_data["type1"], mon_data["type2"]] and \ + self.local_move_data[tm_move]["type"] not in [ + local_poke_data[poke_data.evolves_from[mon]]["type1"], + local_poke_data[poke_data.evolves_from[mon]]["type2"]]: + # the tm/hm is for a move whose type matches current mon, but not pre-evolved form + # so this gets full chance roll + bit = roll_tm_compat(tm_move) + # otherwise 50% reduced chance to add compatibility over pre-evolved form + elif self.multiworld.random.randint(1, 100) > 50 and roll_tm_compat(tm_move): + bit = 1 + else: + bit = 0 + else: + bit = roll_tm_compat(tm_move) + if bit: + mon_data["tms"][int(flag / 8)] |= 1 << (flag % 8) + else: + mon_data["tms"][int(flag / 8)] &= ~(1 << (flag % 8)) + + hm_verify = ["Surf", "Strength"] + if self.multiworld.accessibility[self.player] != "minimal" or ((not + self.multiworld.badgesanity[self.player]) and max(self.multiworld.elite_four_condition[self.player], + self.multiworld.victory_road_condition[self.player]) > 7): + hm_verify += ["Cut"] + if self.multiworld.accessibility[self.player] != "minimal" and (self.multiworld.trainersanity[self.player] or + self.multiworld.extra_key_items[self.player]): + hm_verify += ["Flash"] + + for hm_move in hm_verify: + if hm_move not in compat_hms: + mon = self.multiworld.random.choice([mon for mon in poke_data.pokemon_data if mon not in + poke_data.legendary_pokemon]) + flag = tms_hms.index(hm_move) + local_poke_data[mon]["tms"][int(flag / 8)] |= 1 << (flag % 8) self.local_poke_data = local_poke_data self.learnsets = learnsets +def write_quizzes(self, data, random): + + def get_quiz(q, a): + if q == 0: + r = random.randint(0, 3) + if r == 0: + mon = self.trade_mons["Trade_Dux"] + text = "A woman inVermilion City" + elif r == 1: + mon = self.trade_mons["Trade_Lola"] + text = "A man inCerulean City" + elif r == 2: + mon = self.trade_mons["Trade_Marcel"] + text = "Someone on Route 2" + elif r == 3: + mon = self.trade_mons["Trade_Spot"] + text = "Someone on Route 5" + if not a: + answers.append(0) + old_mon = mon + while old_mon == mon: + mon = random.choice(list(poke_data.pokemon_data.keys())) + + return encode_text(f"{text}was looking for{mon}?") + elif q == 1: + for location in self.multiworld.get_filled_locations(): + if location.item.name == "Secret Key" and location.item.player == self.player: + break + player_name = self.multiworld.player_name[location.player] + if not a: + if len(self.multiworld.player_name) > 1: + old_name = player_name + while old_name == player_name: + player_name = random.choice(list(self.multiworld.player_name.values())) + else: + return encode_text("You're playingin a multiworldwith otherplayers?") + if player_name == self.multiworld.player_name[self.player]: + player_name = "yourself" + player_name = encode_text(player_name, force=True, safety=True) + return encode_text(f"The Secret Key wasfound by") + player_name + encode_text("") + elif q == 2: + if a: + return encode_text(f"#mon ispronouncedPo-kay-mon?") + else: + if random.randint(0, 1): + return encode_text(f"#mon ispronouncedPo-key-mon?") + else: + return encode_text(f"#mon ispronouncedPo-kuh-mon?") + elif q == 3: + starters = [" ".join(self.multiworld.get_location( + f"Pallet Town - Starter {i}", self.player).item.name.split(" ")[1:]) for i in range(1, 4)] + mon = random.choice(starters) + nots = random.choice(range(8, 16, 2)) + if random.randint(0, 1): + while mon in starters: + mon = random.choice(list(poke_data.pokemon_data.keys())) + if a: + nots += 1 + elif not a: + nots += 1 + text = f"{mon} was" + while nots > 0: + i = random.randint(1, min(4, nots)) + text += ("not " * i) + "" + nots -= i + text += "a starter choice?" + return encode_text(text) + elif q == 4: + if a: + tm_text = self.local_tms[27] + else: + if self.multiworld.randomize_tm_moves[self.player]: + wrong_tms = self.local_tms.copy() + wrong_tms.pop(27) + tm_text = random.choice(wrong_tms) + else: + tm_text = "TOMBSTONER" + return encode_text(f"TM28 contains{tm_text.upper()}?") + elif q == 5: + i = 8 + while not a and i in [1, 8]: + i = random.randint(0, 99999999) + return encode_text(f"There are {i}certified #MONLEAGUE BADGEs?") + elif q == 6: + i = 2 + while not a and i in [1, 2]: + i = random.randint(0, 99) + return encode_text(f"POLIWAG evolves {i}times?") + elif q == 7: + entity = "Motor Carrier" + if not a: + entity = random.choice(["Driver", "Shipper"]) + return encode_text("Title 49 of theU.S. Code ofFederalRegulations part397.67 states" + f"that the{entity}is responsiblefor planningroutes when" + "hazardousmaterials aretransported?") + + answers = [random.randint(0, 1), random.randint(0, 1), random.randint(0, 1), + random.randint(0, 1), random.randint(0, 1), random.randint(0, 1)] + + questions = random.sample((range(0, 8)), 6) + question_texts = [] + for i, question in enumerate(questions): + question_texts.append(get_quiz(question, answers[i])) + + for i, quiz in enumerate(["A", "B", "C", "D", "E", "F"]): + data[rom_addresses[f"Quiz_Answer_{quiz}"]] = int(not answers[i]) << 4 | (i + 1) + write_bytes(data, question_texts[i], rom_addresses[f"Text_Quiz_{quiz}"]) + + def generate_output(self, output_directory: str): random = self.multiworld.per_slot_randoms[self.player] game_version = self.multiworld.game_version[self.player].current_key @@ -384,10 +684,33 @@ def generate_output(self, output_directory: str): elif " ".join(location.item.name.split()[1:]) in poke_data.pokemon_data.keys(): data[address] = poke_data.pokemon_data[" ".join(location.item.name.split()[1:])]["id"] else: - data[address] = self.item_name_to_id[location.item.name] - 172000000 + item_id = self.item_name_to_id[location.item.name] - 172000000 + if item_id > 255: + item_id -= 256 + data[address] = item_id else: data[location.rom_address] = 0x2C # AP Item + + def set_trade_mon(address, loc): + mon = self.multiworld.get_location(loc, self.player).item.name + data[rom_addresses[address]] = poke_data.pokemon_data[mon]["id"] + self.trade_mons[address] = mon + + if game_version == "red": + set_trade_mon("Trade_Terry", "Safari Zone Center - Wild Pokemon - 5") + set_trade_mon("Trade_Spot", "Safari Zone East - Wild Pokemon - 1") + else: + set_trade_mon("Trade_Terry", "Safari Zone Center - Wild Pokemon - 7") + set_trade_mon("Trade_Spot", "Safari Zone East - Wild Pokemon - 7") + set_trade_mon("Trade_Marcel", "Route 24 - Wild Pokemon - 6") + set_trade_mon("Trade_Sailor", "Pokemon Mansion 1F - Wild Pokemon - 3") + set_trade_mon("Trade_Dux", "Route 3 - Wild Pokemon - 2") + set_trade_mon("Trade_Marc", "Route 23 - Super Rod Pokemon - 1") + set_trade_mon("Trade_Lola", "Route 10 - Super Rod Pokemon - 1") + set_trade_mon("Trade_Doris", "Cerulean Cave 1F - Wild Pokemon - 9") + set_trade_mon("Trade_Crinkles", "Route 12 - Wild Pokemon - 4") + data[rom_addresses['Fly_Location']] = self.fly_map_code if self.multiworld.tea[self.player].value: @@ -421,6 +744,15 @@ def generate_output(self, output_directory: str): if self.multiworld.old_man[self.player].value == 2: data[rom_addresses['Option_Old_Man']] = 0x11 data[rom_addresses['Option_Old_Man_Lying']] = 0x15 + if self.multiworld.require_pokedex[self.player]: + data[rom_addresses["Require_Pokedex_A"]] = 1 + data[rom_addresses["Require_Pokedex_B"]] = 1 + data[rom_addresses["Require_Pokedex_C"]] = 1 + if self.multiworld.dexsanity[self.player]: + data[rom_addresses["Option_Dexsanity_A"]] = 1 + data[rom_addresses["Option_Dexsanity_B"]] = 1 + if self.multiworld.all_pokemon_seen[self.player]: + data[rom_addresses["Option_Pokedex_Seen"]] = 1 money = str(self.multiworld.starting_money[self.player].value).zfill(6) data[rom_addresses["Starting_Money_High"]] = int(money[:2], 16) data[rom_addresses["Starting_Money_Middle"]] = int(money[2:4], 16) @@ -433,6 +765,7 @@ def generate_output(self, output_directory: str): write_bytes(data, encode_text( " ".join(self.multiworld.get_location("Route 3 - Pokemon For Sale", self.player).item.name.upper().split()[1:])), rom_addresses["Text_Magikarp_Salesman"]) + write_quizzes(self, data, random) if self.multiworld.badges_needed_for_hm_moves[self.player].value == 0: for hm_move in poke_data.hm_moves: @@ -492,10 +825,10 @@ def generate_output(self, output_directory: str): data[address + 17] = poke_data.moves[self.local_poke_data[mon]["start move 3"]]["id"] data[address + 18] = poke_data.moves[self.local_poke_data[mon]["start move 4"]]["id"] write_bytes(data, self.local_poke_data[mon]["tms"], address + 20) - if mon in self.learnsets: - address = rom_addresses["Learnset_" + mon.replace(" ", "")] - for i, move in enumerate(self.learnsets[mon]): - data[(address + 1) + i * 2] = poke_data.moves[move]["id"] + if mon in self.learnsets and self.learnsets[mon]: + address = rom_addresses["Learnset_" + mon.replace(" ", "")] + for i, move in enumerate(self.learnsets[mon]): + data[(address + 1) + i * 2] = poke_data.moves[move]["id"] data[rom_addresses["Option_Aide_Rt2"]] = self.multiworld.oaks_aide_rt_2[self.player].value data[rom_addresses["Option_Aide_Rt11"]] = self.multiworld.oaks_aide_rt_11[self.player].value @@ -507,8 +840,8 @@ def generate_output(self, output_directory: str): if self.multiworld.reusable_tms[self.player].value: data[rom_addresses["Option_Reusable_TMs"]] = 0xC9 - data[rom_addresses["Option_Trainersanity"]] = self.multiworld.trainersanity[self.player].value - data[rom_addresses["Option_Trainersanity2"]] = self.multiworld.trainersanity[self.player].value + for i in range(1, 10): + data[rom_addresses[f"Option_Trainersanity{i}"]] = self.multiworld.trainersanity[self.player].value data[rom_addresses["Option_Always_Half_STAB"]] = int(not self.multiworld.same_type_attack_bonus[self.player].value) @@ -532,8 +865,23 @@ def generate_output(self, output_directory: str): if data[rom_addresses["Start_Inventory"] + item.code - 172000000] < 255: data[rom_addresses["Start_Inventory"] + item.code - 172000000] += 1 + set_mon_palettes(self, random, data) process_trainer_data(self, data, random) + for move_data in self.local_move_data.values(): + if move_data["id"] == 0: + continue + address = rom_addresses["Move_Data"] + ((move_data["id"] - 1) * 6) + write_bytes(data, bytearray([move_data["id"], move_data["effect"], move_data["power"], + poke_data.type_ids[move_data["type"]], round(move_data["accuracy"] * 2.55), move_data["pp"]]), address) + + TM_IDs = bytearray([poke_data.moves[move]["id"] for move in self.local_tms]) + write_bytes(data, TM_IDs, rom_addresses["TM_Moves"]) + + if self.multiworld.randomize_rock_tunnel[self.player]: + seed = randomize_rock_tunnel(data, random) + write_bytes(data, encode_text(f"SEED: {seed}"), rom_addresses["Text_Rock_Tunnel_Sign"]) + mons = [mon["id"] for mon in poke_data.pokemon_data.values()] random.shuffle(mons) data[rom_addresses['Title_Mon_First']] = mons.pop() @@ -564,7 +912,7 @@ def generate_output(self, output_directory: str): else: write_bytes(data, self.rival_name, rom_addresses['Rival_Name']) - data[0xFF00] = 1 # client compatibility version + data[0xFF00] = 2 # client compatibility version write_bytes(data, self.multiworld.seed_name.encode(), 0xFFDB) write_bytes(data, self.multiworld.player_name[self.player].encode(), 0xFFF0) @@ -608,7 +956,7 @@ def get_base_rom_path(game_version: str) -> str: options = Utils.get_options() file_name = options["pokemon_rb_options"][f"{game_version}_rom_file"] if not os.path.exists(file_name): - file_name = Utils.local_path(file_name) + file_name = Utils.user_path(file_name) return file_name diff --git a/worlds/pokemon_rb/rom_addresses.py b/worlds/pokemon_rb/rom_addresses.py index 30c38fa240..eb9785ee63 100644 --- a/worlds/pokemon_rb/rom_addresses.py +++ b/worlds/pokemon_rb/rom_addresses.py @@ -1,7 +1,7 @@ rom_addresses = { "Option_Encounter_Minimum_Steps": 0x3c3, - "Option_Blind_Trainers": 0x30fc, - "Option_Trainersanity": 0x318c, + "Option_Blind_Trainers": 0x30e2, + "Option_Trainersanity1": 0x3172, "Option_Lose_Money": 0x40d4, "Base_Stats_Mew": 0x4260, "Title_Mon_First": 0x4373, @@ -9,94 +9,95 @@ rom_addresses = { "Player_Name": 0x456e, "Rival_Name": 0x4576, "Price_Master_Ball": 0x45d0, - "Title_Seed": 0x5e3a, - "Title_Slot_Name": 0x5e5a, - "PC_Item": 0x6228, - "PC_Item_Quantity": 0x622d, - "Options": 0x623d, - "Fly_Location": 0x6242, - "Skip_Player_Name": 0x625b, - "Skip_Rival_Name": 0x6269, - "Option_Old_Man": 0xcafc, - "Option_Old_Man_Lying": 0xcaff, - "Option_Boulders": 0xcda5, - "Option_Rock_Tunnel_Extra_Items": 0xcdae, - "Wild_Route1": 0xd108, - "Wild_Route2": 0xd11e, - "Wild_Route22": 0xd134, - "Wild_ViridianForest": 0xd14a, - "Wild_Route3": 0xd160, - "Wild_MtMoon1F": 0xd176, - "Wild_MtMoonB1F": 0xd18c, - "Wild_MtMoonB2F": 0xd1a2, - "Wild_Route4": 0xd1b8, - "Wild_Route24": 0xd1ce, - "Wild_Route25": 0xd1e4, - "Wild_Route9": 0xd1fa, - "Wild_Route5": 0xd210, - "Wild_Route6": 0xd226, - "Wild_Route11": 0xd23c, - "Wild_RockTunnel1F": 0xd252, - "Wild_RockTunnelB1F": 0xd268, - "Wild_Route10": 0xd27e, - "Wild_Route12": 0xd294, - "Wild_Route8": 0xd2aa, - "Wild_Route7": 0xd2c0, - "Wild_PokemonTower3F": 0xd2da, - "Wild_PokemonTower4F": 0xd2f0, - "Wild_PokemonTower5F": 0xd306, - "Wild_PokemonTower6F": 0xd31c, - "Wild_PokemonTower7F": 0xd332, - "Wild_Route13": 0xd348, - "Wild_Route14": 0xd35e, - "Wild_Route15": 0xd374, - "Wild_Route16": 0xd38a, - "Wild_Route17": 0xd3a0, - "Wild_Route18": 0xd3b6, - "Wild_SafariZoneCenter": 0xd3cc, - "Wild_SafariZoneEast": 0xd3e2, - "Wild_SafariZoneNorth": 0xd3f8, - "Wild_SafariZoneWest": 0xd40e, - "Wild_SeaRoutes": 0xd425, - "Wild_SeafoamIslands1F": 0xd43a, - "Wild_SeafoamIslandsB1F": 0xd450, - "Wild_SeafoamIslandsB2F": 0xd466, - "Wild_SeafoamIslandsB3F": 0xd47c, - "Wild_SeafoamIslandsB4F": 0xd492, - "Wild_PokemonMansion1F": 0xd4a8, - "Wild_PokemonMansion2F": 0xd4be, - "Wild_PokemonMansion3F": 0xd4d4, - "Wild_PokemonMansionB1F": 0xd4ea, - "Wild_Route21": 0xd500, - "Wild_Surf_Route21": 0xd515, - "Wild_CeruleanCave1F": 0xd52a, - "Wild_CeruleanCave2F": 0xd540, - "Wild_CeruleanCaveB1F": 0xd556, - "Wild_PowerPlant": 0xd56c, - "Wild_Route23": 0xd582, - "Wild_VictoryRoad2F": 0xd598, - "Wild_VictoryRoad3F": 0xd5ae, - "Wild_VictoryRoad1F": 0xd5c4, - "Wild_DiglettsCave": 0xd5da, - "Ghost_Battle5": 0xd730, - "HM_Surf_Badge_a": 0xda1e, - "HM_Surf_Badge_b": 0xda23, - "Wild_Old_Rod": 0xe320, - "Wild_Good_Rod": 0xe34d, - "Option_Reusable_TMs": 0xe619, - "Wild_Super_Rod_A": 0xea4e, - "Wild_Super_Rod_B": 0xea53, - "Wild_Super_Rod_C": 0xea58, - "Wild_Super_Rod_D": 0xea5f, - "Wild_Super_Rod_E": 0xea64, - "Wild_Super_Rod_F": 0xea69, - "Wild_Super_Rod_G": 0xea72, - "Wild_Super_Rod_H": 0xea7b, - "Wild_Super_Rod_I": 0xea84, - "Wild_Super_Rod_J": 0xea8d, - "Starting_Money_High": 0xf957, - "Starting_Money_Middle": 0xf95a, - "Starting_Money_Low": 0xf95d, + "Title_Seed": 0x5e57, + "Title_Slot_Name": 0x5e77, + "PC_Item": 0x6245, + "PC_Item_Quantity": 0x624a, + "Options": 0x625a, + "Fly_Location": 0x625f, + "Skip_Player_Name": 0x6278, + "Skip_Rival_Name": 0x6286, + "Option_Old_Man": 0xcb05, + "Option_Old_Man_Lying": 0xcb08, + "Option_Boulders": 0xcdae, + "Option_Rock_Tunnel_Extra_Items": 0xcdb7, + "Wild_Route1": 0xd111, + "Wild_Route2": 0xd127, + "Wild_Route22": 0xd13d, + "Wild_ViridianForest": 0xd153, + "Wild_Route3": 0xd169, + "Wild_MtMoon1F": 0xd17f, + "Wild_MtMoonB1F": 0xd195, + "Wild_MtMoonB2F": 0xd1ab, + "Wild_Route4": 0xd1c1, + "Wild_Route24": 0xd1d7, + "Wild_Route25": 0xd1ed, + "Wild_Route9": 0xd203, + "Wild_Route5": 0xd219, + "Wild_Route6": 0xd22f, + "Wild_Route11": 0xd245, + "Wild_RockTunnel1F": 0xd25b, + "Wild_RockTunnelB1F": 0xd271, + "Wild_Route10": 0xd287, + "Wild_Route12": 0xd29d, + "Wild_Route8": 0xd2b3, + "Wild_Route7": 0xd2c9, + "Wild_PokemonTower3F": 0xd2e3, + "Wild_PokemonTower4F": 0xd2f9, + "Wild_PokemonTower5F": 0xd30f, + "Wild_PokemonTower6F": 0xd325, + "Wild_PokemonTower7F": 0xd33b, + "Wild_Route13": 0xd351, + "Wild_Route14": 0xd367, + "Wild_Route15": 0xd37d, + "Wild_Route16": 0xd393, + "Wild_Route17": 0xd3a9, + "Wild_Route18": 0xd3bf, + "Wild_SafariZoneCenter": 0xd3d5, + "Wild_SafariZoneEast": 0xd3eb, + "Wild_SafariZoneNorth": 0xd401, + "Wild_SafariZoneWest": 0xd417, + "Wild_SeaRoutes": 0xd42e, + "Wild_SeafoamIslands1F": 0xd443, + "Wild_SeafoamIslandsB1F": 0xd459, + "Wild_SeafoamIslandsB2F": 0xd46f, + "Wild_SeafoamIslandsB3F": 0xd485, + "Wild_SeafoamIslandsB4F": 0xd49b, + "Wild_PokemonMansion1F": 0xd4b1, + "Wild_PokemonMansion2F": 0xd4c7, + "Wild_PokemonMansion3F": 0xd4dd, + "Wild_PokemonMansionB1F": 0xd4f3, + "Wild_Route21": 0xd509, + "Wild_Surf_Route21": 0xd51e, + "Wild_CeruleanCave1F": 0xd533, + "Wild_CeruleanCave2F": 0xd549, + "Wild_CeruleanCaveB1F": 0xd55f, + "Wild_PowerPlant": 0xd575, + "Wild_Route23": 0xd58b, + "Wild_VictoryRoad2F": 0xd5a1, + "Wild_VictoryRoad3F": 0xd5b7, + "Wild_VictoryRoad1F": 0xd5cd, + "Wild_DiglettsCave": 0xd5e3, + "Ghost_Battle5": 0xd739, + "HM_Surf_Badge_a": 0xda2f, + "HM_Surf_Badge_b": 0xda34, + "Wild_Old_Rod": 0xe331, + "Wild_Good_Rod": 0xe35e, + "Option_Reusable_TMs": 0xe62a, + "Wild_Super_Rod_A": 0xea5f, + "Wild_Super_Rod_B": 0xea64, + "Wild_Super_Rod_C": 0xea69, + "Wild_Super_Rod_D": 0xea70, + "Wild_Super_Rod_E": 0xea75, + "Wild_Super_Rod_F": 0xea7a, + "Wild_Super_Rod_G": 0xea83, + "Wild_Super_Rod_H": 0xea8c, + "Wild_Super_Rod_I": 0xea95, + "Wild_Super_Rod_J": 0xea9e, + "Starting_Money_High": 0xf968, + "Starting_Money_Middle": 0xf96b, + "Starting_Money_Low": 0xf96e, + "Option_Pokedex_Seen": 0xf989, "HM_Fly_Badge_a": 0x1318e, "HM_Fly_Badge_b": 0x13193, "HM_Cut_Badge_a": 0x131c4, @@ -105,35 +106,36 @@ rom_addresses = { "HM_Strength_Badge_b": 0x131f9, "HM_Flash_Badge_a": 0x13208, "HM_Flash_Badge_b": 0x1320d, + "TM_Moves": 0x1376c, "Encounter_Chances": 0x13911, "Option_Viridian_Gym_Badges": 0x1901d, "Event_Sleepy_Guy": 0x191bc, "Starter2_K": 0x195a8, "Starter3_K": 0x195b0, "Event_Rocket_Thief": 0x196cc, - "Option_Cerulean_Cave_Condition": 0x1986c, - "Event_Stranded_Man": 0x19b1f, - "Event_Rivals_Sister": 0x19cf2, - "Option_Pokemon_League_Badges": 0x19e0f, - "Shop10": 0x19ee6, - "Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_0_ITEM": 0x1a03a, - "Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_1_ITEM": 0x1a048, - "Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_2_ITEM": 0x1a056, - "Missable_Silph_Co_4F_Item_1": 0x1a0fe, - "Missable_Silph_Co_4F_Item_2": 0x1a105, - "Missable_Silph_Co_4F_Item_3": 0x1a10c, - "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_0_ITEM": 0x1a264, - "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_1_ITEM": 0x1a272, - "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_2_ITEM": 0x1a280, - "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_3_ITEM": 0x1a28e, - "Missable_Silph_Co_5F_Item_1": 0x1a366, - "Missable_Silph_Co_5F_Item_2": 0x1a36d, - "Missable_Silph_Co_5F_Item_3": 0x1a374, - "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_0_ITEM": 0x1a4a4, - "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_1_ITEM": 0x1a4b2, - "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_2_ITEM": 0x1a4c0, - "Missable_Silph_Co_6F_Item_1": 0x1a5e2, - "Missable_Silph_Co_6F_Item_2": 0x1a5e9, + "Option_Cerulean_Cave_Condition": 0x19875, + "Event_Stranded_Man": 0x19b28, + "Event_Rivals_Sister": 0x19cfb, + "Option_Pokemon_League_Badges": 0x19e18, + "Shop10": 0x19eef, + "Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_0_ITEM": 0x1a043, + "Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_1_ITEM": 0x1a051, + "Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_2_ITEM": 0x1a05f, + "Missable_Silph_Co_4F_Item_1": 0x1a107, + "Missable_Silph_Co_4F_Item_2": 0x1a10e, + "Missable_Silph_Co_4F_Item_3": 0x1a115, + "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_0_ITEM": 0x1a26d, + "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_1_ITEM": 0x1a27b, + "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_2_ITEM": 0x1a289, + "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_3_ITEM": 0x1a297, + "Missable_Silph_Co_5F_Item_1": 0x1a36f, + "Missable_Silph_Co_5F_Item_2": 0x1a376, + "Missable_Silph_Co_5F_Item_3": 0x1a37d, + "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_0_ITEM": 0x1a4ad, + "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_1_ITEM": 0x1a4bb, + "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_2_ITEM": 0x1a4c9, + "Missable_Silph_Co_6F_Item_1": 0x1a5eb, + "Missable_Silph_Co_6F_Item_2": 0x1a5f2, "Event_Free_Sample": 0x1cad6, "Starter1_F": 0x1cca2, "Starter2_F": 0x1cca6, @@ -145,49 +147,50 @@ rom_addresses = { "Starter2_I": 0x1d0fa, "Starter1_D": 0x1d101, "Starter3_D": 0x1d10b, - "Starter2_E": 0x1d2e5, - "Starter3_E": 0x1d2ed, - "Event_Pokedex": 0x1d351, - "Event_Oaks_Gift": 0x1d381, - "Event_Pokemart_Quest": 0x1d579, - "Shop1": 0x1d5a3, - "Event_Bicycle_Shop": 0x1d83d, - "Text_Bicycle": 0x1d8d0, - "Event_Fuji": 0x1da05, - "Trainersanity_EVENT_BEAT_MEW_ITEM": 0x1dc58, - "Static_Encounter_Mew": 0x1dc88, - "Gift_Eevee": 0x1dd01, - "Shop7": 0x1dd53, - "Event_Mr_Psychic": 0x1de30, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_0_ITEM": 0x1e32b, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_1_ITEM": 0x1e339, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_2_ITEM": 0x1e347, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_3_ITEM": 0x1e355, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_4_ITEM": 0x1e363, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_5_ITEM": 0x1e371, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_6_ITEM": 0x1e37f, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_7_ITEM": 0x1e38d, - "Trainersanity_EVENT_BEAT_ZAPDOS_ITEM": 0x1e39b, - "Static_Encounter_Voltorb_A": 0x1e40a, - "Static_Encounter_Voltorb_B": 0x1e412, - "Static_Encounter_Voltorb_C": 0x1e41a, - "Static_Encounter_Electrode_A": 0x1e422, - "Static_Encounter_Voltorb_D": 0x1e42a, - "Static_Encounter_Voltorb_E": 0x1e432, - "Static_Encounter_Electrode_B": 0x1e43a, - "Static_Encounter_Voltorb_F": 0x1e442, - "Static_Encounter_Zapdos": 0x1e44a, - "Missable_Power_Plant_Item_1": 0x1e452, - "Missable_Power_Plant_Item_2": 0x1e459, - "Missable_Power_Plant_Item_3": 0x1e460, - "Missable_Power_Plant_Item_4": 0x1e467, - "Missable_Power_Plant_Item_5": 0x1e46e, - "Event_Rt16_House_Woman": 0x1e647, - "Option_Victory_Road_Badges": 0x1e718, - "Event_Bill": 0x1e949, + "Starter2_E": 0x1d300, + "Starter3_E": 0x1d308, + "Event_Pokedex": 0x1d36c, + "Event_Oaks_Gift": 0x1d39c, + "Event_Pokemart_Quest": 0x1d594, + "Shop1": 0x1d5be, + "Event_Bicycle_Shop": 0x1d858, + "Text_Bicycle": 0x1d8eb, + "Event_Fuji": 0x1da20, + "Trainersanity_EVENT_BEAT_MEW_ITEM": 0x1dc73, + "Static_Encounter_Mew": 0x1dca3, + "Gift_Eevee": 0x1dd1c, + "Shop7": 0x1dd6e, + "Event_Mr_Psychic": 0x1de4b, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_0_ITEM": 0x1e346, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_1_ITEM": 0x1e354, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_2_ITEM": 0x1e362, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_3_ITEM": 0x1e370, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_4_ITEM": 0x1e37e, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_5_ITEM": 0x1e38c, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_6_ITEM": 0x1e39a, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_7_ITEM": 0x1e3a8, + "Trainersanity_EVENT_BEAT_ZAPDOS_ITEM": 0x1e3b6, + "Static_Encounter_Voltorb_A": 0x1e425, + "Static_Encounter_Voltorb_B": 0x1e42d, + "Static_Encounter_Voltorb_C": 0x1e435, + "Static_Encounter_Electrode_A": 0x1e43d, + "Static_Encounter_Voltorb_D": 0x1e445, + "Static_Encounter_Voltorb_E": 0x1e44d, + "Static_Encounter_Electrode_B": 0x1e455, + "Static_Encounter_Voltorb_F": 0x1e45d, + "Static_Encounter_Zapdos": 0x1e465, + "Missable_Power_Plant_Item_1": 0x1e46d, + "Missable_Power_Plant_Item_2": 0x1e474, + "Missable_Power_Plant_Item_3": 0x1e47b, + "Missable_Power_Plant_Item_4": 0x1e482, + "Missable_Power_Plant_Item_5": 0x1e489, + "Event_Rt16_House_Woman": 0x1e662, + "Option_Victory_Road_Badges": 0x1e733, + "Event_Bill": 0x1e964, "Starter1_O": 0x372b0, "Starter2_O": 0x372b4, "Starter3_O": 0x372b8, + "Move_Data": 0x38000, "Base_Stats": 0x383de, "Starter3_C": 0x39cf2, "Starter1_C": 0x39cf8, @@ -217,320 +220,347 @@ rom_addresses = { "Rival_Starter3_H": 0x3a4ab, "Rival_Starter1_H": 0x3a4b9, "Trainer_Data_End": 0x3a52e, - "Learnset_Rhydon": 0x3b1d9, - "Learnset_Kangaskhan": 0x3b1e7, - "Learnset_NidoranM": 0x3b1f6, - "Learnset_Clefairy": 0x3b208, - "Learnset_Spearow": 0x3b219, - "Learnset_Voltorb": 0x3b228, - "Learnset_Nidoking": 0x3b234, - "Learnset_Slowbro": 0x3b23c, - "Learnset_Ivysaur": 0x3b24f, - "Learnset_Exeggutor": 0x3b25f, - "Learnset_Lickitung": 0x3b263, - "Learnset_Exeggcute": 0x3b273, - "Learnset_Grimer": 0x3b284, - "Learnset_Gengar": 0x3b292, - "Learnset_NidoranF": 0x3b29b, - "Learnset_Nidoqueen": 0x3b2a9, - "Learnset_Cubone": 0x3b2b4, - "Learnset_Rhyhorn": 0x3b2c3, - "Learnset_Lapras": 0x3b2d1, - "Learnset_Mew": 0x3b2e1, - "Learnset_Gyarados": 0x3b2eb, - "Learnset_Shellder": 0x3b2fb, - "Learnset_Tentacool": 0x3b30a, - "Learnset_Gastly": 0x3b31f, - "Learnset_Scyther": 0x3b325, - "Learnset_Staryu": 0x3b337, - "Learnset_Blastoise": 0x3b347, - "Learnset_Pinsir": 0x3b355, - "Learnset_Tangela": 0x3b363, - "Learnset_Growlithe": 0x3b379, - "Learnset_Onix": 0x3b385, - "Learnset_Fearow": 0x3b391, - "Learnset_Pidgey": 0x3b3a0, - "Learnset_Slowpoke": 0x3b3b1, - "Learnset_Kadabra": 0x3b3c9, - "Learnset_Graveler": 0x3b3e1, - "Learnset_Chansey": 0x3b3ef, - "Learnset_Machoke": 0x3b407, - "Learnset_MrMime": 0x3b413, - "Learnset_Hitmonlee": 0x3b41f, - "Learnset_Hitmonchan": 0x3b42b, - "Learnset_Arbok": 0x3b437, - "Learnset_Parasect": 0x3b443, - "Learnset_Psyduck": 0x3b452, - "Learnset_Drowzee": 0x3b461, - "Learnset_Golem": 0x3b46f, - "Learnset_Magmar": 0x3b47f, - "Learnset_Electabuzz": 0x3b48f, - "Learnset_Magneton": 0x3b49b, - "Learnset_Koffing": 0x3b4ac, - "Learnset_Mankey": 0x3b4bd, - "Learnset_Seel": 0x3b4cc, - "Learnset_Diglett": 0x3b4db, - "Learnset_Tauros": 0x3b4e7, - "Learnset_Farfetchd": 0x3b4f9, - "Learnset_Venonat": 0x3b508, - "Learnset_Dragonite": 0x3b516, - "Learnset_Doduo": 0x3b52b, - "Learnset_Poliwag": 0x3b53c, - "Learnset_Jynx": 0x3b54a, - "Learnset_Moltres": 0x3b558, - "Learnset_Articuno": 0x3b560, - "Learnset_Zapdos": 0x3b568, - "Learnset_Meowth": 0x3b575, - "Learnset_Krabby": 0x3b584, - "Learnset_Vulpix": 0x3b59a, - "Learnset_Pikachu": 0x3b5ac, - "Learnset_Dratini": 0x3b5c1, - "Learnset_Dragonair": 0x3b5d0, - "Learnset_Kabuto": 0x3b5df, - "Learnset_Kabutops": 0x3b5e9, - "Learnset_Horsea": 0x3b5f6, - "Learnset_Seadra": 0x3b602, - "Learnset_Sandshrew": 0x3b615, - "Learnset_Sandslash": 0x3b621, - "Learnset_Omanyte": 0x3b630, - "Learnset_Omastar": 0x3b63a, - "Learnset_Jigglypuff": 0x3b648, - "Learnset_Eevee": 0x3b666, - "Learnset_Flareon": 0x3b670, - "Learnset_Jolteon": 0x3b682, - "Learnset_Vaporeon": 0x3b694, - "Learnset_Machop": 0x3b6a9, - "Learnset_Zubat": 0x3b6b8, - "Learnset_Ekans": 0x3b6c7, - "Learnset_Paras": 0x3b6d6, - "Learnset_Poliwhirl": 0x3b6e6, - "Learnset_Poliwrath": 0x3b6f4, - "Learnset_Beedrill": 0x3b704, - "Learnset_Dodrio": 0x3b714, - "Learnset_Primeape": 0x3b722, - "Learnset_Dugtrio": 0x3b72e, - "Learnset_Venomoth": 0x3b73a, - "Learnset_Dewgong": 0x3b748, - "Learnset_Butterfree": 0x3b762, - "Learnset_Machamp": 0x3b772, - "Learnset_Golduck": 0x3b780, - "Learnset_Hypno": 0x3b78c, - "Learnset_Golbat": 0x3b79a, - "Learnset_Mewtwo": 0x3b7a6, - "Learnset_Snorlax": 0x3b7b2, - "Learnset_Magikarp": 0x3b7bf, - "Learnset_Muk": 0x3b7c7, - "Learnset_Kingler": 0x3b7d7, - "Learnset_Cloyster": 0x3b7e3, - "Learnset_Electrode": 0x3b7e9, - "Learnset_Weezing": 0x3b7f7, - "Learnset_Persian": 0x3b803, - "Learnset_Marowak": 0x3b80f, - "Learnset_Haunter": 0x3b827, - "Learnset_Alakazam": 0x3b832, - "Learnset_Pidgeotto": 0x3b843, - "Learnset_Pidgeot": 0x3b851, - "Learnset_Bulbasaur": 0x3b864, - "Learnset_Venusaur": 0x3b874, - "Learnset_Tentacruel": 0x3b884, - "Learnset_Goldeen": 0x3b89b, - "Learnset_Seaking": 0x3b8a9, - "Learnset_Ponyta": 0x3b8c2, - "Learnset_Rapidash": 0x3b8d0, - "Learnset_Rattata": 0x3b8e1, - "Learnset_Raticate": 0x3b8eb, - "Learnset_Nidorino": 0x3b8f9, - "Learnset_Nidorina": 0x3b90b, - "Learnset_Geodude": 0x3b91c, - "Learnset_Porygon": 0x3b92a, - "Learnset_Aerodactyl": 0x3b934, - "Learnset_Magnemite": 0x3b942, - "Learnset_Charmander": 0x3b957, - "Learnset_Squirtle": 0x3b968, - "Learnset_Charmeleon": 0x3b979, - "Learnset_Wartortle": 0x3b98a, - "Learnset_Charizard": 0x3b998, - "Learnset_Oddish": 0x3b9b1, - "Learnset_Gloom": 0x3b9c3, - "Learnset_Vileplume": 0x3b9d1, - "Learnset_Bellsprout": 0x3b9dc, - "Learnset_Weepinbell": 0x3b9f0, - "Learnset_Victreebel": 0x3ba00, + "Learnset_Rhydon": 0x3b1e1, + "Learnset_Kangaskhan": 0x3b1ef, + "Learnset_NidoranM": 0x3b1fe, + "Learnset_Clefairy": 0x3b210, + "Learnset_Spearow": 0x3b221, + "Learnset_Voltorb": 0x3b230, + "Learnset_Nidoking": 0x3b23c, + "Learnset_Slowbro": 0x3b244, + "Learnset_Ivysaur": 0x3b257, + "Learnset_Exeggutor": 0x3b267, + "Learnset_Lickitung": 0x3b26b, + "Learnset_Exeggcute": 0x3b27b, + "Learnset_Grimer": 0x3b28c, + "Learnset_Gengar": 0x3b29a, + "Learnset_NidoranF": 0x3b2a3, + "Learnset_Nidoqueen": 0x3b2b1, + "Learnset_Cubone": 0x3b2bc, + "Learnset_Rhyhorn": 0x3b2cb, + "Learnset_Lapras": 0x3b2d9, + "Learnset_Mew": 0x3b2e9, + "Learnset_Gyarados": 0x3b2f3, + "Learnset_Shellder": 0x3b303, + "Learnset_Tentacool": 0x3b312, + "Learnset_Gastly": 0x3b327, + "Learnset_Scyther": 0x3b32d, + "Learnset_Staryu": 0x3b33f, + "Learnset_Blastoise": 0x3b34f, + "Learnset_Pinsir": 0x3b35d, + "Learnset_Tangela": 0x3b36b, + "Learnset_Growlithe": 0x3b381, + "Learnset_Onix": 0x3b38d, + "Learnset_Fearow": 0x3b399, + "Learnset_Pidgey": 0x3b3a8, + "Learnset_Slowpoke": 0x3b3b9, + "Learnset_Kadabra": 0x3b3d1, + "Learnset_Graveler": 0x3b3e9, + "Learnset_Chansey": 0x3b3f7, + "Learnset_Machoke": 0x3b40f, + "Learnset_MrMime": 0x3b41b, + "Learnset_Hitmonlee": 0x3b427, + "Learnset_Hitmonchan": 0x3b433, + "Learnset_Arbok": 0x3b43f, + "Learnset_Parasect": 0x3b44b, + "Learnset_Psyduck": 0x3b45a, + "Learnset_Drowzee": 0x3b469, + "Learnset_Golem": 0x3b477, + "Learnset_Magmar": 0x3b487, + "Learnset_Electabuzz": 0x3b497, + "Learnset_Magneton": 0x3b4a3, + "Learnset_Koffing": 0x3b4b4, + "Learnset_Mankey": 0x3b4c5, + "Learnset_Seel": 0x3b4d4, + "Learnset_Diglett": 0x3b4e3, + "Learnset_Tauros": 0x3b4ef, + "Learnset_Farfetchd": 0x3b501, + "Learnset_Venonat": 0x3b510, + "Learnset_Dragonite": 0x3b51e, + "Learnset_Doduo": 0x3b533, + "Learnset_Poliwag": 0x3b544, + "Learnset_Jynx": 0x3b552, + "Learnset_Moltres": 0x3b560, + "Learnset_Articuno": 0x3b568, + "Learnset_Zapdos": 0x3b570, + "Learnset_Meowth": 0x3b57d, + "Learnset_Krabby": 0x3b58c, + "Learnset_Vulpix": 0x3b5a2, + "Learnset_Pikachu": 0x3b5b4, + "Learnset_Dratini": 0x3b5c9, + "Learnset_Dragonair": 0x3b5d8, + "Learnset_Kabuto": 0x3b5e7, + "Learnset_Kabutops": 0x3b5f1, + "Learnset_Horsea": 0x3b5fe, + "Learnset_Seadra": 0x3b60a, + "Learnset_Sandshrew": 0x3b61d, + "Learnset_Sandslash": 0x3b629, + "Learnset_Omanyte": 0x3b638, + "Learnset_Omastar": 0x3b642, + "Learnset_Jigglypuff": 0x3b650, + "Learnset_Eevee": 0x3b66e, + "Learnset_Flareon": 0x3b678, + "Learnset_Jolteon": 0x3b68a, + "Learnset_Vaporeon": 0x3b69c, + "Learnset_Machop": 0x3b6b1, + "Learnset_Zubat": 0x3b6c0, + "Learnset_Ekans": 0x3b6cf, + "Learnset_Paras": 0x3b6de, + "Learnset_Poliwhirl": 0x3b6ee, + "Learnset_Poliwrath": 0x3b6fc, + "Learnset_Beedrill": 0x3b70c, + "Learnset_Dodrio": 0x3b71c, + "Learnset_Primeape": 0x3b72a, + "Learnset_Dugtrio": 0x3b736, + "Learnset_Venomoth": 0x3b742, + "Learnset_Dewgong": 0x3b750, + "Learnset_Butterfree": 0x3b76a, + "Learnset_Machamp": 0x3b77a, + "Learnset_Golduck": 0x3b788, + "Learnset_Hypno": 0x3b794, + "Learnset_Golbat": 0x3b7a2, + "Learnset_Mewtwo": 0x3b7ae, + "Learnset_Snorlax": 0x3b7ba, + "Learnset_Magikarp": 0x3b7c7, + "Learnset_Muk": 0x3b7cf, + "Learnset_Kingler": 0x3b7df, + "Learnset_Cloyster": 0x3b7eb, + "Learnset_Electrode": 0x3b7f1, + "Learnset_Weezing": 0x3b7ff, + "Learnset_Persian": 0x3b80b, + "Learnset_Marowak": 0x3b817, + "Learnset_Haunter": 0x3b82f, + "Learnset_Alakazam": 0x3b83a, + "Learnset_Pidgeotto": 0x3b84b, + "Learnset_Pidgeot": 0x3b859, + "Learnset_Bulbasaur": 0x3b86c, + "Learnset_Venusaur": 0x3b87c, + "Learnset_Tentacruel": 0x3b88c, + "Learnset_Goldeen": 0x3b8a3, + "Learnset_Seaking": 0x3b8b1, + "Learnset_Ponyta": 0x3b8ca, + "Learnset_Rapidash": 0x3b8d8, + "Learnset_Rattata": 0x3b8e9, + "Learnset_Raticate": 0x3b8f3, + "Learnset_Nidorino": 0x3b901, + "Learnset_Nidorina": 0x3b913, + "Learnset_Geodude": 0x3b924, + "Learnset_Porygon": 0x3b932, + "Learnset_Aerodactyl": 0x3b93c, + "Learnset_Magnemite": 0x3b94b, + "Learnset_Charmander": 0x3b960, + "Learnset_Squirtle": 0x3b971, + "Learnset_Charmeleon": 0x3b982, + "Learnset_Wartortle": 0x3b993, + "Learnset_Charizard": 0x3b9a1, + "Learnset_Oddish": 0x3b9ba, + "Learnset_Gloom": 0x3b9cc, + "Learnset_Vileplume": 0x3b9da, + "Learnset_Bellsprout": 0x3b9e5, + "Learnset_Weepinbell": 0x3b9f9, + "Learnset_Victreebel": 0x3ba09, "Option_Always_Half_STAB": 0x3e3fb, "Type_Chart": 0x3e4ee, "Ghost_Battle3": 0x3f1be, - "Trainersanity_EVENT_BEAT_MANSION_1_TRAINER_0_ITEM": 0x44341, - "Missable_Pokemon_Mansion_1F_Item_1": 0x443d8, - "Missable_Pokemon_Mansion_1F_Item_2": 0x443df, - "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_1_TRAINER_0_ITEM": 0x44514, - "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_1_TRAINER_1_ITEM": 0x44522, - "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_1_TRAINER_2_ITEM": 0x44530, - "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_1_TRAINER_3_ITEM": 0x4453e, - "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_1_TRAINER_4_ITEM": 0x4454c, - "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_1_TRAINER_5_ITEM": 0x4455a, - "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_1_TRAINER_6_ITEM": 0x44568, - "Map_Rock_TunnelF": 0x44686, - "Trainersanity_EVENT_BEAT_VICTORY_ROAD_3_TRAINER_0_ITEM": 0x44a55, - "Trainersanity_EVENT_BEAT_VICTORY_ROAD_3_TRAINER_1_ITEM": 0x44a63, - "Trainersanity_EVENT_BEAT_VICTORY_ROAD_3_TRAINER_2_ITEM": 0x44a71, - "Trainersanity_EVENT_BEAT_VICTORY_ROAD_3_TRAINER_3_ITEM": 0x44a7f, - "Missable_Victory_Road_3F_Item_1": 0x44b1f, - "Missable_Victory_Road_3F_Item_2": 0x44b26, - "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_1_TRAINER_0_ITEM": 0x44c47, - "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_1_TRAINER_1_ITEM": 0x44c55, - "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_1_TRAINER_2_ITEM": 0x44c63, - "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_1_TRAINER_3_ITEM": 0x44c71, - "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_1_TRAINER_4_ITEM": 0x44c7f, - "Missable_Rocket_Hideout_B1F_Item_1": 0x44d4f, - "Missable_Rocket_Hideout_B1F_Item_2": 0x44d56, - "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_2_TRAINER_0_ITEM": 0x45100, - "Missable_Rocket_Hideout_B2F_Item_1": 0x45141, - "Missable_Rocket_Hideout_B2F_Item_2": 0x45148, - "Missable_Rocket_Hideout_B2F_Item_3": 0x4514f, - "Missable_Rocket_Hideout_B2F_Item_4": 0x45156, - "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_3_TRAINER_0_ITEM": 0x45333, - "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_3_TRAINER_1_ITEM": 0x45341, - "Missable_Rocket_Hideout_B3F_Item_1": 0x45397, - "Missable_Rocket_Hideout_B3F_Item_2": 0x4539e, - "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_4_TRAINER_0_ITEM": 0x4554a, - "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_4_TRAINER_1_ITEM": 0x45558, - "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_4_TRAINER_2_ITEM": 0x45566, - "Missable_Rocket_Hideout_B4F_Item_1": 0x45655, - "Missable_Rocket_Hideout_B4F_Item_2": 0x4565c, - "Missable_Rocket_Hideout_B4F_Item_3": 0x45663, - "Missable_Rocket_Hideout_B4F_Item_4": 0x4566a, - "Missable_Rocket_Hideout_B4F_Item_5": 0x45671, - "Missable_Safari_Zone_East_Item_1": 0x458e0, - "Missable_Safari_Zone_East_Item_2": 0x458e7, - "Missable_Safari_Zone_East_Item_3": 0x458ee, - "Missable_Safari_Zone_East_Item_4": 0x458f5, - "Missable_Safari_Zone_North_Item_1": 0x45a40, - "Missable_Safari_Zone_North_Item_2": 0x45a47, - "Missable_Safari_Zone_Center_Item": 0x45c27, - "Missable_Cerulean_Cave_2F_Item_1": 0x45e64, - "Missable_Cerulean_Cave_2F_Item_2": 0x45e6b, - "Missable_Cerulean_Cave_2F_Item_3": 0x45e72, - "Trainersanity_EVENT_BEAT_MEWTWO_ITEM": 0x45f4a, - "Static_Encounter_Mewtwo": 0x45f74, - "Missable_Cerulean_Cave_B1F_Item_1": 0x45f7c, - "Missable_Cerulean_Cave_B1F_Item_2": 0x45f83, - "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_2_TRAINER_0_ITEM": 0x46059, - "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_2_TRAINER_1_ITEM": 0x46067, - "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_2_TRAINER_2_ITEM": 0x46075, - "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_2_TRAINER_3_ITEM": 0x46083, - "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_2_TRAINER_4_ITEM": 0x46091, - "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_2_TRAINER_5_ITEM": 0x4609f, - "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_2_TRAINER_6_ITEM": 0x460ad, - "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_2_TRAINER_7_ITEM": 0x460bb, - "Missable_Rock_Tunnel_B1F_Item_1": 0x461df, - "Missable_Rock_Tunnel_B1F_Item_2": 0x461e6, - "Missable_Rock_Tunnel_B1F_Item_3": 0x461ed, - "Missable_Rock_Tunnel_B1F_Item_4": 0x461f4, - "Trainersanity_EVENT_BEAT_ARTICUNO_ITEM": 0x468f7, - "Static_Encounter_Articuno": 0x4694e, - "Hidden_Item_Viridian_Forest_1": 0x46eaf, - "Hidden_Item_Viridian_Forest_2": 0x46eb5, - "Hidden_Item_MtMoonB2F_1": 0x46ebc, - "Hidden_Item_MtMoonB2F_2": 0x46ec2, - "Hidden_Item_Route_25_1": 0x46ed6, - "Hidden_Item_Route_25_2": 0x46edc, - "Hidden_Item_Route_9": 0x46ee3, - "Hidden_Item_SS_Anne_Kitchen": 0x46ef6, - "Hidden_Item_SS_Anne_B1F": 0x46efd, - "Hidden_Item_Route_10_1": 0x46f04, - "Hidden_Item_Route_10_2": 0x46f0a, - "Hidden_Item_Rocket_Hideout_B1F": 0x46f11, - "Hidden_Item_Rocket_Hideout_B3F": 0x46f18, - "Hidden_Item_Rocket_Hideout_B4F": 0x46f1f, - "Hidden_Item_Pokemon_Tower_5F": 0x46f33, - "Hidden_Item_Route_13_1": 0x46f3a, - "Hidden_Item_Route_13_2": 0x46f40, - "Hidden_Item_Safari_Zone_West": 0x46f4e, - "Hidden_Item_Silph_Co_5F": 0x46f55, - "Hidden_Item_Silph_Co_9F": 0x46f5c, - "Hidden_Item_Copycats_House": 0x46f63, - "Hidden_Item_Cerulean_Cave_1F": 0x46f6a, - "Hidden_Item_Cerulean_Cave_B1F": 0x46f71, - "Hidden_Item_Power_Plant_1": 0x46f78, - "Hidden_Item_Power_Plant_2": 0x46f7e, - "Hidden_Item_Seafoam_Islands_B2F": 0x46f85, - "Hidden_Item_Seafoam_Islands_B4F": 0x46f8c, - "Hidden_Item_Pokemon_Mansion_1F": 0x46f93, - "Hidden_Item_Pokemon_Mansion_3F": 0x46fa7, - "Hidden_Item_Pokemon_Mansion_B1F": 0x46fb4, - "Hidden_Item_Route_23_1": 0x46fc7, - "Hidden_Item_Route_23_2": 0x46fcd, - "Hidden_Item_Route_23_3": 0x46fd3, - "Hidden_Item_Victory_Road_2F_1": 0x46fda, - "Hidden_Item_Victory_Road_2F_2": 0x46fe0, - "Hidden_Item_Unused_6F": 0x46fe7, - "Hidden_Item_Viridian_City": 0x46ff5, - "Hidden_Item_Route_11": 0x470a2, - "Hidden_Item_Route_12": 0x470a9, - "Hidden_Item_Route_17_1": 0x470b7, - "Hidden_Item_Route_17_2": 0x470bd, - "Hidden_Item_Route_17_3": 0x470c3, - "Hidden_Item_Route_17_4": 0x470c9, - "Hidden_Item_Route_17_5": 0x470cf, - "Hidden_Item_Underground_Path_NS_1": 0x470d6, - "Hidden_Item_Underground_Path_NS_2": 0x470dc, - "Hidden_Item_Underground_Path_WE_1": 0x470e3, - "Hidden_Item_Underground_Path_WE_2": 0x470e9, - "Hidden_Item_Celadon_City": 0x470f0, - "Hidden_Item_Seafoam_Islands_B3F": 0x470f7, - "Hidden_Item_Vermilion_City": 0x470fe, - "Hidden_Item_Cerulean_City": 0x47105, - "Hidden_Item_Route_4": 0x4710c, + "Dexsanity_Items": 0x44254, + "Option_Dexsanity_A": 0x44301, + "Require_Pokedex_B": 0x44305, + "Option_Dexsanity_B": 0x44362, + "Require_Pokedex_C": 0x44366, + "Trainersanity_EVENT_BEAT_MANSION_1_TRAINER_0_ITEM": 0x4454b, + "Missable_Pokemon_Mansion_1F_Item_1": 0x445e2, + "Missable_Pokemon_Mansion_1F_Item_2": 0x445e9, + "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_1_TRAINER_0_ITEM": 0x4471e, + "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_1_TRAINER_1_ITEM": 0x4472c, + "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_1_TRAINER_2_ITEM": 0x4473a, + "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_1_TRAINER_3_ITEM": 0x44748, + "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_1_TRAINER_4_ITEM": 0x44756, + "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_1_TRAINER_5_ITEM": 0x44764, + "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_1_TRAINER_6_ITEM": 0x44772, + "Map_Rock_Tunnel1F": 0x4488f, + "Trainersanity_EVENT_BEAT_VICTORY_ROAD_3_TRAINER_0_ITEM": 0x44c5f, + "Trainersanity_EVENT_BEAT_VICTORY_ROAD_3_TRAINER_1_ITEM": 0x44c6d, + "Trainersanity_EVENT_BEAT_VICTORY_ROAD_3_TRAINER_2_ITEM": 0x44c7b, + "Trainersanity_EVENT_BEAT_VICTORY_ROAD_3_TRAINER_3_ITEM": 0x44c89, + "Missable_Victory_Road_3F_Item_1": 0x44d29, + "Missable_Victory_Road_3F_Item_2": 0x44d30, + "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_1_TRAINER_0_ITEM": 0x44e51, + "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_1_TRAINER_1_ITEM": 0x44e5f, + "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_1_TRAINER_2_ITEM": 0x44e6d, + "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_1_TRAINER_3_ITEM": 0x44e7b, + "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_1_TRAINER_4_ITEM": 0x44e89, + "Missable_Rocket_Hideout_B1F_Item_1": 0x44f59, + "Missable_Rocket_Hideout_B1F_Item_2": 0x44f60, + "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_2_TRAINER_0_ITEM": 0x4530a, + "Missable_Rocket_Hideout_B2F_Item_1": 0x4534b, + "Missable_Rocket_Hideout_B2F_Item_2": 0x45352, + "Missable_Rocket_Hideout_B2F_Item_3": 0x45359, + "Missable_Rocket_Hideout_B2F_Item_4": 0x45360, + "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_3_TRAINER_0_ITEM": 0x4553d, + "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_3_TRAINER_1_ITEM": 0x4554b, + "Missable_Rocket_Hideout_B3F_Item_1": 0x455a1, + "Missable_Rocket_Hideout_B3F_Item_2": 0x455a8, + "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_4_TRAINER_0_ITEM": 0x45754, + "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_4_TRAINER_1_ITEM": 0x45762, + "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_4_TRAINER_2_ITEM": 0x45770, + "Missable_Rocket_Hideout_B4F_Item_1": 0x4585f, + "Missable_Rocket_Hideout_B4F_Item_2": 0x45866, + "Missable_Rocket_Hideout_B4F_Item_3": 0x4586d, + "Missable_Rocket_Hideout_B4F_Item_4": 0x45874, + "Missable_Rocket_Hideout_B4F_Item_5": 0x4587b, + "Missable_Safari_Zone_East_Item_1": 0x45aea, + "Missable_Safari_Zone_East_Item_2": 0x45af1, + "Missable_Safari_Zone_East_Item_3": 0x45af8, + "Missable_Safari_Zone_East_Item_4": 0x45aff, + "Missable_Safari_Zone_North_Item_1": 0x45c4a, + "Missable_Safari_Zone_North_Item_2": 0x45c51, + "Missable_Safari_Zone_Center_Item": 0x45e31, + "Missable_Cerulean_Cave_2F_Item_1": 0x4606e, + "Missable_Cerulean_Cave_2F_Item_2": 0x46075, + "Missable_Cerulean_Cave_2F_Item_3": 0x4607c, + "Trainersanity_EVENT_BEAT_MEWTWO_ITEM": 0x46154, + "Static_Encounter_Mewtwo": 0x4617e, + "Missable_Cerulean_Cave_B1F_Item_1": 0x46186, + "Missable_Cerulean_Cave_B1F_Item_2": 0x4618d, + "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_2_TRAINER_0_ITEM": 0x46263, + "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_2_TRAINER_1_ITEM": 0x46271, + "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_2_TRAINER_2_ITEM": 0x4627f, + "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_2_TRAINER_3_ITEM": 0x4628d, + "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_2_TRAINER_4_ITEM": 0x4629b, + "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_2_TRAINER_5_ITEM": 0x462a9, + "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_2_TRAINER_6_ITEM": 0x462b7, + "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_2_TRAINER_7_ITEM": 0x462c5, + "Missable_Rock_Tunnel_B1F_Item_1": 0x463e9, + "Missable_Rock_Tunnel_B1F_Item_2": 0x463f0, + "Missable_Rock_Tunnel_B1F_Item_3": 0x463f7, + "Missable_Rock_Tunnel_B1F_Item_4": 0x463fe, + "Map_Rock_TunnelB1F": 0x4640f, + "Trainersanity_EVENT_BEAT_ARTICUNO_ITEM": 0x46b01, + "Static_Encounter_Articuno": 0x46b58, + "Hidden_Item_Game_Corner_1": 0x46ff0, + "Hidden_Item_Game_Corner_2": 0x46ff6, + "Hidden_Item_Game_Corner_3": 0x46ffc, + "Hidden_Item_Game_Corner_4": 0x47002, + "Hidden_Item_Game_Corner_5": 0x47008, + "Hidden_Item_Game_Corner_6": 0x4700e, + "Hidden_Item_Game_Corner_7": 0x47014, + "Hidden_Item_Game_Corner_8": 0x4701a, + "Hidden_Item_Game_Corner_9": 0x47020, + "Hidden_Item_Game_Corner_10": 0x47026, + "Hidden_Item_Game_Corner_11": 0x4702c, + "Quiz_Answer_A": 0x47060, + "Quiz_Answer_B": 0x47066, + "Quiz_Answer_C": 0x4706c, + "Quiz_Answer_D": 0x47072, + "Quiz_Answer_E": 0x47078, + "Quiz_Answer_F": 0x4707e, + "Hidden_Item_Viridian_Forest_1": 0x470b3, + "Hidden_Item_Viridian_Forest_2": 0x470b9, + "Hidden_Item_MtMoonB2F_1": 0x470c0, + "Hidden_Item_MtMoonB2F_2": 0x470c6, + "Hidden_Item_Route_25_1": 0x470da, + "Hidden_Item_Route_25_2": 0x470e0, + "Hidden_Item_Route_9": 0x470e7, + "Hidden_Item_SS_Anne_Kitchen": 0x470fa, + "Hidden_Item_SS_Anne_B1F": 0x47101, + "Hidden_Item_Route_10_1": 0x47108, + "Hidden_Item_Route_10_2": 0x4710e, + "Hidden_Item_Rocket_Hideout_B1F": 0x47115, + "Hidden_Item_Rocket_Hideout_B3F": 0x4711c, + "Hidden_Item_Rocket_Hideout_B4F": 0x47123, + "Hidden_Item_Pokemon_Tower_5F": 0x47137, + "Hidden_Item_Route_13_1": 0x4713e, + "Hidden_Item_Route_13_2": 0x47144, + "Hidden_Item_Safari_Zone_West": 0x47152, + "Hidden_Item_Silph_Co_5F": 0x47159, + "Hidden_Item_Silph_Co_9F": 0x47160, + "Hidden_Item_Copycats_House": 0x47167, + "Hidden_Item_Cerulean_Cave_1F": 0x4716e, + "Hidden_Item_Cerulean_Cave_B1F": 0x47175, + "Hidden_Item_Power_Plant_1": 0x4717c, + "Hidden_Item_Power_Plant_2": 0x47182, + "Hidden_Item_Seafoam_Islands_B2F": 0x47189, + "Hidden_Item_Seafoam_Islands_B4F": 0x47190, + "Hidden_Item_Pokemon_Mansion_1F": 0x47197, + "Hidden_Item_Pokemon_Mansion_3F": 0x471ab, + "Hidden_Item_Pokemon_Mansion_B1F": 0x471b8, + "Hidden_Item_Route_23_1": 0x471cb, + "Hidden_Item_Route_23_2": 0x471d1, + "Hidden_Item_Route_23_3": 0x471d7, + "Hidden_Item_Victory_Road_2F_1": 0x471de, + "Hidden_Item_Victory_Road_2F_2": 0x471e4, + "Hidden_Item_Unused_6F": 0x471eb, + "Hidden_Item_Viridian_City": 0x471f9, + "Hidden_Item_Route_11": 0x472a6, + "Hidden_Item_Route_12": 0x472ad, + "Hidden_Item_Route_17_1": 0x472bb, + "Hidden_Item_Route_17_2": 0x472c1, + "Hidden_Item_Route_17_3": 0x472c7, + "Hidden_Item_Route_17_4": 0x472cd, + "Hidden_Item_Route_17_5": 0x472d3, + "Hidden_Item_Underground_Path_NS_1": 0x472da, + "Hidden_Item_Underground_Path_NS_2": 0x472e0, + "Hidden_Item_Underground_Path_WE_1": 0x472e7, + "Hidden_Item_Underground_Path_WE_2": 0x472ed, + "Hidden_Item_Celadon_City": 0x472f4, + "Hidden_Item_Seafoam_Islands_B3F": 0x472fb, + "Hidden_Item_Vermilion_City": 0x47302, + "Hidden_Item_Cerulean_City": 0x47309, + "Hidden_Item_Route_4": 0x47310, "Event_Counter": 0x482d3, - "Event_Thirsty_Girl_Lemonade": 0x484f9, - "Event_Thirsty_Girl_Soda": 0x4851d, - "Event_Thirsty_Girl_Water": 0x48541, - "Option_Tea": 0x4871d, - "Event_Mansion_Lady": 0x4872a, - "Badge_Celadon_Gym": 0x48a1b, - "Event_Celadon_Gym": 0x48a2f, - "Trainersanity_EVENT_BEAT_CELADON_GYM_TRAINER_0_ITEM": 0x48a75, - "Trainersanity_EVENT_BEAT_CELADON_GYM_TRAINER_1_ITEM": 0x48a83, - "Trainersanity_EVENT_BEAT_CELADON_GYM_TRAINER_2_ITEM": 0x48a91, - "Trainersanity_EVENT_BEAT_CELADON_GYM_TRAINER_3_ITEM": 0x48a9f, - "Trainersanity_EVENT_BEAT_CELADON_GYM_TRAINER_4_ITEM": 0x48aad, - "Trainersanity_EVENT_BEAT_CELADON_GYM_TRAINER_5_ITEM": 0x48abb, - "Trainersanity_EVENT_BEAT_CELADON_GYM_TRAINER_6_ITEM": 0x48ac9, - "Event_Gambling_Addict": 0x492a1, - "Gift_Magikarp": 0x4943e, - "Option_Aide_Rt11": 0x4959b, - "Event_Rt11_Oaks_Aide": 0x4959f, - "Event_Mourning_Girl": 0x49699, - "Option_Aide_Rt15": 0x49784, - "Event_Rt_15_Oaks_Aide": 0x49788, - "Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_0_ITEM": 0x49b2e, - "Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_1_ITEM": 0x49b3c, - "Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_2_ITEM": 0x49b4a, - "Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_3_ITEM": 0x49b58, - "Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_4_ITEM": 0x49b66, - "Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_5_ITEM": 0x49b74, - "Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_6_ITEM": 0x49b82, - "Missable_Mt_Moon_1F_Item_1": 0x49c91, - "Missable_Mt_Moon_1F_Item_2": 0x49c98, - "Missable_Mt_Moon_1F_Item_3": 0x49c9f, - "Missable_Mt_Moon_1F_Item_4": 0x49ca6, - "Missable_Mt_Moon_1F_Item_5": 0x49cad, - "Missable_Mt_Moon_1F_Item_6": 0x49cb4, - "Trainersanity_EVENT_BEAT_MT_MOON_3_TRAINER_0_ITEM": 0x49f87, - "Trainersanity_EVENT_BEAT_MT_MOON_3_TRAINER_1_ITEM": 0x49f95, - "Trainersanity_EVENT_BEAT_MT_MOON_3_TRAINER_2_ITEM": 0x49fa3, - "Trainersanity_EVENT_BEAT_MT_MOON_3_TRAINER_3_ITEM": 0x49fb1, - "Dome_Fossil_Text": 0x4a025, - "Event_Dome_Fossil": 0x4a045, - "Helix_Fossil_Text": 0x4a081, - "Event_Helix_Fossil": 0x4a0a1, - "Missable_Mt_Moon_B2F_Item_1": 0x4a18a, - "Missable_Mt_Moon_B2F_Item_2": 0x4a191, - "Missable_Safari_Zone_West_Item_1": 0x4a373, - "Missable_Safari_Zone_West_Item_2": 0x4a37a, - "Missable_Safari_Zone_West_Item_3": 0x4a381, - "Missable_Safari_Zone_West_Item_4": 0x4a388, - "Event_Safari_Zone_Secret_House": 0x4a48d, + "Shop_Stones": 0x483e9, + "Event_Thirsty_Girl_Lemonade": 0x48501, + "Event_Thirsty_Girl_Soda": 0x48525, + "Event_Thirsty_Girl_Water": 0x48549, + "Option_Tea": 0x48725, + "Event_Mansion_Lady": 0x48732, + "Badge_Celadon_Gym": 0x48a23, + "Event_Celadon_Gym": 0x48a37, + "Trainersanity_EVENT_BEAT_CELADON_GYM_TRAINER_0_ITEM": 0x48a7d, + "Trainersanity_EVENT_BEAT_CELADON_GYM_TRAINER_1_ITEM": 0x48a8b, + "Trainersanity_EVENT_BEAT_CELADON_GYM_TRAINER_2_ITEM": 0x48a99, + "Trainersanity_EVENT_BEAT_CELADON_GYM_TRAINER_3_ITEM": 0x48aa7, + "Trainersanity_EVENT_BEAT_CELADON_GYM_TRAINER_4_ITEM": 0x48ab5, + "Trainersanity_EVENT_BEAT_CELADON_GYM_TRAINER_5_ITEM": 0x48ac3, + "Trainersanity_EVENT_BEAT_CELADON_GYM_TRAINER_6_ITEM": 0x48ad1, + "Event_Game_Corner_Gift_A": 0x48e98, + "Event_Game_Corner_Gift_C": 0x48f14, + "Event_Game_Corner_Gift_B": 0x48f63, + "Event_Gambling_Addict": 0x49306, + "Gift_Magikarp": 0x494a3, + "Option_Aide_Rt11": 0x49600, + "Event_Rt11_Oaks_Aide": 0x49604, + "Event_Mourning_Girl": 0x496fe, + "Option_Aide_Rt15": 0x497e9, + "Event_Rt_15_Oaks_Aide": 0x497ed, + "Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_0_ITEM": 0x49b93, + "Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_1_ITEM": 0x49ba1, + "Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_2_ITEM": 0x49baf, + "Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_3_ITEM": 0x49bbd, + "Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_4_ITEM": 0x49bcb, + "Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_5_ITEM": 0x49bd9, + "Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_6_ITEM": 0x49be7, + "Missable_Mt_Moon_1F_Item_1": 0x49cf6, + "Missable_Mt_Moon_1F_Item_2": 0x49cfd, + "Missable_Mt_Moon_1F_Item_3": 0x49d04, + "Missable_Mt_Moon_1F_Item_4": 0x49d0b, + "Missable_Mt_Moon_1F_Item_5": 0x49d12, + "Missable_Mt_Moon_1F_Item_6": 0x49d19, + "Trainersanity_EVENT_BEAT_MT_MOON_3_TRAINER_0_ITEM": 0x49fec, + "Trainersanity_EVENT_BEAT_MT_MOON_3_TRAINER_1_ITEM": 0x49ffa, + "Trainersanity_EVENT_BEAT_MT_MOON_3_TRAINER_2_ITEM": 0x4a008, + "Trainersanity_EVENT_BEAT_MT_MOON_3_TRAINER_3_ITEM": 0x4a016, + "Dome_Fossil_Text": 0x4a08a, + "Event_Dome_Fossil": 0x4a0aa, + "Helix_Fossil_Text": 0x4a0e6, + "Event_Helix_Fossil": 0x4a106, + "Missable_Mt_Moon_B2F_Item_1": 0x4a1ef, + "Missable_Mt_Moon_B2F_Item_2": 0x4a1f6, + "Missable_Safari_Zone_West_Item_1": 0x4a3d8, + "Missable_Safari_Zone_West_Item_2": 0x4a3df, + "Missable_Safari_Zone_West_Item_3": 0x4a3e6, + "Missable_Safari_Zone_West_Item_4": 0x4a3ed, + "Event_Safari_Zone_Secret_House": 0x4a4f2, "Missable_Route_24_Item": 0x506e6, "Missable_Route_25_Item": 0x5080b, "Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_0_ITEM": 0x50d47, @@ -606,13 +636,16 @@ rom_addresses = { "Prize_Mon_D2": 0x5288a, "Prize_Mon_E2": 0x5288b, "Prize_Mon_F2": 0x5288c, - "Prize_Mon_A": 0x529b0, - "Prize_Mon_B": 0x529b2, - "Prize_Mon_C": 0x529b4, - "Prize_Mon_D": 0x529b6, - "Prize_Mon_E": 0x529b8, - "Prize_Mon_F": 0x529ba, - "Start_Inventory": 0x52add, + "Prize_Item_A": 0x52895, + "Prize_Item_B": 0x52896, + "Prize_Item_C": 0x52897, + "Prize_Mon_A": 0x529cc, + "Prize_Mon_B": 0x529ce, + "Prize_Mon_C": 0x529d0, + "Prize_Mon_D": 0x529d2, + "Prize_Mon_E": 0x529d4, + "Prize_Mon_F": 0x529d6, + "Start_Inventory": 0x52af9, "Missable_Route_2_Item_1": 0x5404a, "Missable_Route_2_Item_2": 0x54051, "Missable_Route_4_Item": 0x543df, @@ -696,81 +729,82 @@ rom_addresses = { "Missable_Route_12_Item_2": 0x5870b, "Missable_Route_15_Item": 0x589c7, "Ghost_Battle6": 0x58df0, - "Trainersanity_EVENT_BEAT_ROUTE_6_TRAINER_0_ITEM": 0x59106, - "Trainersanity_EVENT_BEAT_ROUTE_6_TRAINER_1_ITEM": 0x59114, - "Trainersanity_EVENT_BEAT_ROUTE_6_TRAINER_2_ITEM": 0x59122, - "Trainersanity_EVENT_BEAT_ROUTE_6_TRAINER_3_ITEM": 0x59130, - "Trainersanity_EVENT_BEAT_ROUTE_6_TRAINER_4_ITEM": 0x5913e, - "Trainersanity_EVENT_BEAT_ROUTE_6_TRAINER_5_ITEM": 0x5914c, - "Trainersanity_EVENT_BEAT_ROUTE_8_TRAINER_0_ITEM": 0x5921e, - "Trainersanity_EVENT_BEAT_ROUTE_8_TRAINER_1_ITEM": 0x5922c, - "Trainersanity_EVENT_BEAT_ROUTE_8_TRAINER_2_ITEM": 0x5923a, - "Trainersanity_EVENT_BEAT_ROUTE_8_TRAINER_3_ITEM": 0x59248, - "Trainersanity_EVENT_BEAT_ROUTE_8_TRAINER_4_ITEM": 0x59256, - "Trainersanity_EVENT_BEAT_ROUTE_8_TRAINER_5_ITEM": 0x59264, - "Trainersanity_EVENT_BEAT_ROUTE_8_TRAINER_6_ITEM": 0x59272, - "Trainersanity_EVENT_BEAT_ROUTE_8_TRAINER_7_ITEM": 0x59280, - "Trainersanity_EVENT_BEAT_ROUTE_8_TRAINER_8_ITEM": 0x5928e, - "Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_0_ITEM": 0x59406, - "Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_1_ITEM": 0x59414, - "Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_2_ITEM": 0x59422, - "Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_3_ITEM": 0x59430, - "Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_4_ITEM": 0x5943e, - "Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_5_ITEM": 0x5944c, - "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_0_ITEM": 0x59533, - "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_1_ITEM": 0x59541, - "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_2_ITEM": 0x5954f, - "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_3_ITEM": 0x5955d, - "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_4_ITEM": 0x5956b, - "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_5_ITEM": 0x59579, - "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_6_ITEM": 0x59587, - "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_7_ITEM": 0x59595, - "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_8_ITEM": 0x595a3, - "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_9_ITEM": 0x595b1, - "Static_Encounter_Snorlax_A": 0x596ef, - "Trainersanity_EVENT_BEAT_ROUTE_12_TRAINER_0_ITEM": 0x5975d, - "Trainersanity_EVENT_BEAT_ROUTE_12_TRAINER_1_ITEM": 0x5976b, - "Trainersanity_EVENT_BEAT_ROUTE_12_TRAINER_2_ITEM": 0x59779, - "Trainersanity_EVENT_BEAT_ROUTE_12_TRAINER_3_ITEM": 0x59787, - "Trainersanity_EVENT_BEAT_ROUTE_12_TRAINER_4_ITEM": 0x59795, - "Trainersanity_EVENT_BEAT_ROUTE_12_TRAINER_5_ITEM": 0x597a3, - "Trainersanity_EVENT_BEAT_ROUTE_12_TRAINER_6_ITEM": 0x597b1, - "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_0_ITEM": 0x598b9, - "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_1_ITEM": 0x598c7, - "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_2_ITEM": 0x598d5, - "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_3_ITEM": 0x598e3, - "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_4_ITEM": 0x598f1, - "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_5_ITEM": 0x598ff, - "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_6_ITEM": 0x5990d, - "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_7_ITEM": 0x5991b, - "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_8_ITEM": 0x59929, - "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_9_ITEM": 0x59937, - "Static_Encounter_Snorlax_B": 0x59a51, - "Trainersanity_EVENT_BEAT_ROUTE_16_TRAINER_0_ITEM": 0x59abd, - "Trainersanity_EVENT_BEAT_ROUTE_16_TRAINER_1_ITEM": 0x59acb, - "Trainersanity_EVENT_BEAT_ROUTE_16_TRAINER_2_ITEM": 0x59ad9, - "Trainersanity_EVENT_BEAT_ROUTE_16_TRAINER_3_ITEM": 0x59ae7, - "Trainersanity_EVENT_BEAT_ROUTE_16_TRAINER_4_ITEM": 0x59af5, - "Trainersanity_EVENT_BEAT_ROUTE_16_TRAINER_5_ITEM": 0x59b03, - "Trainersanity_EVENT_BEAT_ROUTE_18_TRAINER_0_ITEM": 0x59be4, - "Trainersanity_EVENT_BEAT_ROUTE_18_TRAINER_1_ITEM": 0x59bf2, - "Trainersanity_EVENT_BEAT_ROUTE_18_TRAINER_2_ITEM": 0x59c00, - "Event_Pokemon_Fan_Club": 0x59d13, - "Trainersanity_EVENT_BEAT_SILPH_CO_2F_TRAINER_0_ITEM": 0x59e73, - "Trainersanity_EVENT_BEAT_SILPH_CO_2F_TRAINER_1_ITEM": 0x59e81, - "Trainersanity_EVENT_BEAT_SILPH_CO_2F_TRAINER_2_ITEM": 0x59e8f, - "Trainersanity_EVENT_BEAT_SILPH_CO_2F_TRAINER_3_ITEM": 0x59e9d, - "Event_Scared_Woman": 0x59eaf, - "Trainersanity_EVENT_BEAT_SILPH_CO_3F_TRAINER_0_ITEM": 0x5a0b7, - "Trainersanity_EVENT_BEAT_SILPH_CO_3F_TRAINER_1_ITEM": 0x5a0c5, - "Missable_Silph_Co_3F_Item": 0x5a15f, - "Trainersanity_EVENT_BEAT_SILPH_CO_10F_TRAINER_0_ITEM": 0x5a281, - "Trainersanity_EVENT_BEAT_SILPH_CO_10F_TRAINER_1_ITEM": 0x5a28f, - "Missable_Silph_Co_10F_Item_1": 0x5a319, - "Missable_Silph_Co_10F_Item_2": 0x5a320, - "Missable_Silph_Co_10F_Item_3": 0x5a327, - "Trainersanity_EVENT_BEAT_LANCES_ROOM_TRAINER_0_ITEM": 0x5a48a, - "Guard_Drink_List": 0x5a69f, + "Require_Pokedex_A": 0x59051, + "Trainersanity_EVENT_BEAT_ROUTE_6_TRAINER_0_ITEM": 0x5910b, + "Trainersanity_EVENT_BEAT_ROUTE_6_TRAINER_1_ITEM": 0x59119, + "Trainersanity_EVENT_BEAT_ROUTE_6_TRAINER_2_ITEM": 0x59127, + "Trainersanity_EVENT_BEAT_ROUTE_6_TRAINER_3_ITEM": 0x59135, + "Trainersanity_EVENT_BEAT_ROUTE_6_TRAINER_4_ITEM": 0x59143, + "Trainersanity_EVENT_BEAT_ROUTE_6_TRAINER_5_ITEM": 0x59151, + "Trainersanity_EVENT_BEAT_ROUTE_8_TRAINER_0_ITEM": 0x59223, + "Trainersanity_EVENT_BEAT_ROUTE_8_TRAINER_1_ITEM": 0x59231, + "Trainersanity_EVENT_BEAT_ROUTE_8_TRAINER_2_ITEM": 0x5923f, + "Trainersanity_EVENT_BEAT_ROUTE_8_TRAINER_3_ITEM": 0x5924d, + "Trainersanity_EVENT_BEAT_ROUTE_8_TRAINER_4_ITEM": 0x5925b, + "Trainersanity_EVENT_BEAT_ROUTE_8_TRAINER_5_ITEM": 0x59269, + "Trainersanity_EVENT_BEAT_ROUTE_8_TRAINER_6_ITEM": 0x59277, + "Trainersanity_EVENT_BEAT_ROUTE_8_TRAINER_7_ITEM": 0x59285, + "Trainersanity_EVENT_BEAT_ROUTE_8_TRAINER_8_ITEM": 0x59293, + "Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_0_ITEM": 0x5940d, + "Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_1_ITEM": 0x5941b, + "Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_2_ITEM": 0x59429, + "Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_3_ITEM": 0x59437, + "Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_4_ITEM": 0x59445, + "Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_5_ITEM": 0x59453, + "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_0_ITEM": 0x5953a, + "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_1_ITEM": 0x59548, + "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_2_ITEM": 0x59556, + "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_3_ITEM": 0x59564, + "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_4_ITEM": 0x59572, + "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_5_ITEM": 0x59580, + "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_6_ITEM": 0x5958e, + "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_7_ITEM": 0x5959c, + "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_8_ITEM": 0x595aa, + "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_9_ITEM": 0x595b8, + "Static_Encounter_Snorlax_A": 0x596f6, + "Trainersanity_EVENT_BEAT_ROUTE_12_TRAINER_0_ITEM": 0x59764, + "Trainersanity_EVENT_BEAT_ROUTE_12_TRAINER_1_ITEM": 0x59772, + "Trainersanity_EVENT_BEAT_ROUTE_12_TRAINER_2_ITEM": 0x59780, + "Trainersanity_EVENT_BEAT_ROUTE_12_TRAINER_3_ITEM": 0x5978e, + "Trainersanity_EVENT_BEAT_ROUTE_12_TRAINER_4_ITEM": 0x5979c, + "Trainersanity_EVENT_BEAT_ROUTE_12_TRAINER_5_ITEM": 0x597aa, + "Trainersanity_EVENT_BEAT_ROUTE_12_TRAINER_6_ITEM": 0x597b8, + "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_0_ITEM": 0x598c0, + "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_1_ITEM": 0x598ce, + "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_2_ITEM": 0x598dc, + "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_3_ITEM": 0x598ea, + "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_4_ITEM": 0x598f8, + "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_5_ITEM": 0x59906, + "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_6_ITEM": 0x59914, + "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_7_ITEM": 0x59922, + "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_8_ITEM": 0x59930, + "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_9_ITEM": 0x5993e, + "Static_Encounter_Snorlax_B": 0x59a58, + "Trainersanity_EVENT_BEAT_ROUTE_16_TRAINER_0_ITEM": 0x59ac4, + "Trainersanity_EVENT_BEAT_ROUTE_16_TRAINER_1_ITEM": 0x59ad2, + "Trainersanity_EVENT_BEAT_ROUTE_16_TRAINER_2_ITEM": 0x59ae0, + "Trainersanity_EVENT_BEAT_ROUTE_16_TRAINER_3_ITEM": 0x59aee, + "Trainersanity_EVENT_BEAT_ROUTE_16_TRAINER_4_ITEM": 0x59afc, + "Trainersanity_EVENT_BEAT_ROUTE_16_TRAINER_5_ITEM": 0x59b0a, + "Trainersanity_EVENT_BEAT_ROUTE_18_TRAINER_0_ITEM": 0x59beb, + "Trainersanity_EVENT_BEAT_ROUTE_18_TRAINER_1_ITEM": 0x59bf9, + "Trainersanity_EVENT_BEAT_ROUTE_18_TRAINER_2_ITEM": 0x59c07, + "Event_Pokemon_Fan_Club": 0x59d1a, + "Trainersanity_EVENT_BEAT_SILPH_CO_2F_TRAINER_0_ITEM": 0x59e7a, + "Trainersanity_EVENT_BEAT_SILPH_CO_2F_TRAINER_1_ITEM": 0x59e88, + "Trainersanity_EVENT_BEAT_SILPH_CO_2F_TRAINER_2_ITEM": 0x59e96, + "Trainersanity_EVENT_BEAT_SILPH_CO_2F_TRAINER_3_ITEM": 0x59ea4, + "Event_Scared_Woman": 0x59eb6, + "Trainersanity_EVENT_BEAT_SILPH_CO_3F_TRAINER_0_ITEM": 0x5a0be, + "Trainersanity_EVENT_BEAT_SILPH_CO_3F_TRAINER_1_ITEM": 0x5a0cc, + "Missable_Silph_Co_3F_Item": 0x5a166, + "Trainersanity_EVENT_BEAT_SILPH_CO_10F_TRAINER_0_ITEM": 0x5a288, + "Trainersanity_EVENT_BEAT_SILPH_CO_10F_TRAINER_1_ITEM": 0x5a296, + "Missable_Silph_Co_10F_Item_1": 0x5a320, + "Missable_Silph_Co_10F_Item_2": 0x5a327, + "Missable_Silph_Co_10F_Item_3": 0x5a32e, + "Trainersanity_EVENT_BEAT_LANCES_ROOM_TRAINER_0_ITEM": 0x5a491, + "Guard_Drink_List": 0x5a6a6, "Event_Museum": 0x5c266, "Badge_Pewter_Gym": 0x5c3ed, "Event_Pewter_Gym": 0x5c401, @@ -879,6 +913,16 @@ rom_addresses = { "Trainersanity_EVENT_BEAT_SILPH_CO_11F_TRAINER_1_ITEM": 0x6234a, "Event_Silph_Co_President": 0x6235d, "Ghost_Battle4": 0x708e1, + "Trade_Terry": 0x71b7b, + "Trade_Marcel": 0x71b89, + "Trade_Sailor": 0x71ba5, + "Trade_Dux": 0x71bb3, + "Trade_Marc": 0x71bc1, + "Trade_Lola": 0x71bcf, + "Trade_Doris": 0x71bdd, + "Trade_Crinkles": 0x71beb, + "Trade_Spot": 0x71bf9, + "Mon_Palettes": 0x725dd, "Badge_Viridian_Gym": 0x749f7, "Event_Viridian_Gym": 0x74a0b, "Trainersanity_EVENT_BEAT_VIRIDIAN_GYM_TRAINER_0_ITEM": 0x74a66, @@ -906,18 +950,39 @@ rom_addresses = { "Trainersanity_EVENT_BEAT_FUCHSIA_GYM_TRAINER_5_ITEM": 0x756d2, "Badge_Cinnabar_Gym": 0x75a06, "Event_Cinnabar_Gym": 0x75a1a, - "Event_Lab_Scientist": 0x75e43, - "Fossils_Needed_For_Second_Item": 0x75f10, - "Event_Dome_Fossil_B": 0x75f8d, - "Event_Helix_Fossil_B": 0x75fad, - "Shop8": 0x760cb, - "Starter2_N": 0x761fe, - "Starter3_N": 0x76206, - "Trainersanity_EVENT_BEAT_LORELEIS_ROOM_TRAINER_0_ITEM": 0x764ce, - "Trainersanity_EVENT_BEAT_BRUNOS_ROOM_TRAINER_0_ITEM": 0x76627, - "Trainersanity_EVENT_BEAT_AGATHAS_ROOM_TRAINER_0_ITEM": 0x76786, - "Option_Itemfinder": 0x768ff, - "Text_Magikarp_Salesman": 0x8a7fe, + "Option_Trainersanity4": 0x75af6, + "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_B_ITEM": 0x75b02, + "Option_Trainersanity3": 0x75b46, + "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_A_ITEM": 0x75b52, + "Option_Trainersanity5": 0x75bad, + "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_2_ITEM": 0x75bb9, + "Option_Trainersanity6": 0x75bfd, + "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_3_ITEM": 0x75c09, + "Option_Trainersanity7": 0x75c4d, + "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_4_ITEM": 0x75c59, + "Option_Trainersanity8": 0x75c9d, + "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_5_ITEM": 0x75ca9, + "Option_Trainersanity9": 0x75ced, + "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_6_ITEM": 0x75cf9, + "Event_Lab_Scientist": 0x75f17, + "Fossils_Needed_For_Second_Item": 0x75fe4, + "Event_Dome_Fossil_B": 0x76061, + "Event_Helix_Fossil_B": 0x76081, + "Shop8": 0x7619f, + "Starter2_N": 0x762d2, + "Starter3_N": 0x762da, + "Trainersanity_EVENT_BEAT_LORELEIS_ROOM_TRAINER_0_ITEM": 0x7663d, + "Trainersanity_EVENT_BEAT_BRUNOS_ROOM_TRAINER_0_ITEM": 0x76796, + "Trainersanity_EVENT_BEAT_AGATHAS_ROOM_TRAINER_0_ITEM": 0x768f5, + "Option_Itemfinder": 0x76a6e, + "Text_Quiz_A": 0x88806, + "Text_Quiz_B": 0x8893a, + "Text_Quiz_C": 0x88a6e, + "Text_Quiz_D": 0x88ba2, + "Text_Quiz_E": 0x88cd6, + "Text_Quiz_F": 0x88e0a, + "Text_Magikarp_Salesman": 0x8ae3f, + "Text_Rock_Tunnel_Sign": 0x8e82a, "Text_Badges_Needed": 0x92304, "Badge_Text_Boulder_Badge": 0x99010, "Badge_Text_Cascade_Badge": 0x99028, diff --git a/worlds/pokemon_rb/rules.py b/worlds/pokemon_rb/rules.py index 493a58e594..c69df00366 100644 --- a/worlds/pokemon_rb/rules.py +++ b/worlds/pokemon_rb/rules.py @@ -1,26 +1,45 @@ -from ..generic.Rules import add_item_rule, add_rule +from ..generic.Rules import add_item_rule, add_rule, location_item_name +from .items import item_groups + def set_rules(world, player): - add_item_rule(world.get_location("Pallet Town - Player's PC", player), - lambda i: i.player == player and "Badge" not in i.name and "Trap" not in i.name and - i.name != "Pokedex") + item_rules = { + "Pallet Town - Player's PC": (lambda i: i.player == player and "Badge" not in i.name and "Trap" not in i.name + and i.name != "Pokedex" and "Coins" not in i.name) + } + + if world.prizesanity[player]: + def prize_rule(i): + return i.player != player or i.name in item_groups["Unique"] + item_rules["Celadon Prize Corner - Item Prize 1"] = prize_rule + item_rules["Celadon Prize Corner - Item Prize 2"] = prize_rule + item_rules["Celadon Prize Corner - Item Prize 3"] = prize_rule + + if world.accessibility[player] != "locations": + world.get_location("Cerulean City - Bicycle Shop", player).always_allow = (lambda state, item: + item.name == "Bike Voucher" + and item.player == player) + world.get_location("Fuchsia City - Safari Zone Warden", player).always_allow = (lambda state, item: + item.name == "Gold Teeth" and + item.player == player) access_rules = { - "Pallet Town - Rival's Sister": lambda state: state.has("Oak's Parcel", player), "Pallet Town - Oak's Post-Route-22-Rival Gift": lambda state: state.has("Oak's Parcel", player), "Viridian City - Sleepy Guy": lambda state: state.pokemon_rb_can_cut(player) or state.pokemon_rb_can_surf(player), "Route 2 - Oak's Aide": lambda state: state.pokemon_rb_oaks_aide(state.multiworld.oaks_aide_rt_2[player].value + 5, player), "Pewter City - Museum": lambda state: state.pokemon_rb_can_cut(player), - "Cerulean City - Bicycle Shop": lambda state: state.has("Bike Voucher", player), + "Cerulean City - Bicycle Shop": lambda state: state.has("Bike Voucher", player) + or location_item_name(state, "Cerulean City - Bicycle Shop", player) == ("Bike Voucher", player), "Lavender Town - Mr. Fuji": lambda state: state.has("Fuji Saved", player), "Vermilion Gym - Lt. Surge 1": lambda state: state.pokemon_rb_can_cut(player or state.pokemon_rb_can_surf(player)), "Vermilion Gym - Lt. Surge 2": lambda state: state.pokemon_rb_can_cut(player or state.pokemon_rb_can_surf(player)), "Route 11 - Oak's Aide": lambda state: state.pokemon_rb_oaks_aide(state.multiworld.oaks_aide_rt_11[player].value + 5, player), "Celadon City - Stranded Man": lambda state: state.pokemon_rb_can_surf(player), - "Silph Co 11F - Silph Co President": lambda state: state.has("Card Key", player), - "Fuchsia City - Safari Zone Warden": lambda state: state.has("Gold Teeth", player), + "Silph Co 11F - Silph Co President (Card Key)": lambda state: state.has("Card Key", player), + "Fuchsia City - Safari Zone Warden": lambda state: state.has("Gold Teeth", player) + or location_item_name(state, "Fuchsia City - Safari Zone Warden", player) == ("Gold Teeth", player), "Route 12 - Island Item": lambda state: state.pokemon_rb_can_surf(player), "Route 12 - Item Behind Cuttable Tree": lambda state: state.pokemon_rb_can_cut(player), "Route 15 - Oak's Aide": lambda state: state.pokemon_rb_oaks_aide(state.multiworld.oaks_aide_rt_15[player].value + 5, player), @@ -38,6 +57,23 @@ def set_rules(world, player): "Silph Co 6F - Southwest Item (Card Key)": lambda state: state.has("Card Key", player), "Silph Co 7F - East Item (Card Key)": lambda state: state.has("Card Key", player), "Safari Zone Center - Island Item": lambda state: state.pokemon_rb_can_surf(player), + "Celadon Prize Corner - Item Prize 1": lambda state: state.has("Coin Case", player), + "Celadon Prize Corner - Item Prize 2": lambda state: state.has("Coin Case", player), + "Celadon Prize Corner - Item Prize 3": lambda state: state.has("Coin Case", player), + "Celadon Game Corner - West Gambler's Gift (Coin Case)": lambda state: state.has("Coin Case", player), + "Celadon Game Corner - Center Gambler's Gift (Coin Case)": lambda state: state.has("Coin Case", player), + "Celadon Game Corner - East Gambler's Gift (Coin Case)": lambda state: state.has("Coin Case", player), + "Celadon Game Corner - Hidden Item Northwest By Counter (Coin Case)": lambda state: state.has("Coin Case", player), + "Celadon Game Corner - Hidden Item Southwest Corner (Coin Case)": lambda state: state.has("Coin Case", player), + "Celadon Game Corner - Hidden Item Near Rumor Man (Coin Case)": lambda state: state.has("Coin Case", player), + "Celadon Game Corner - Hidden Item Near Speculating Woman (Coin Case)": lambda state: state.has("Coin Case", player), + "Celadon Game Corner - Hidden Item Near West Gifting Gambler (Coin Case)": lambda state: state.has("Coin Case", player), + "Celadon Game Corner - Hidden Item Near Wonderful Time Woman (Coin Case)": lambda state: state.has("Coin Case", player), + "Celadon Game Corner - Hidden Item Near Failing Gym Information Guy (Coin Case)": lambda state: state.has( "Coin Case", player), + "Celadon Game Corner - Hidden Item Near East Gifting Gambler (Coin Case)": lambda state: state.has("Coin Case", player), + "Celadon Game Corner - Hidden Item Near Hooked Guy (Coin Case)": lambda state: state.has("Coin Case", player), + "Celadon Game Corner - Hidden Item at End of Horizontal Machine Row (Coin Case)": lambda state: state.has("Coin Case", player), + "Celadon Game Corner - Hidden Item in Front of Horizontal Machine Row (Coin Case)": lambda state: state.has("Coin Case", player), "Silph Co 11F - Silph Co Liberated": lambda state: state.has("Card Key", player), @@ -89,6 +125,18 @@ def set_rules(world, player): "Seafoam Islands B4F - Legendary Pokemon": lambda state: state.pokemon_rb_can_strength(player), "Vermilion City - Legendary Pokemon": lambda state: state.pokemon_rb_can_surf(player) and state.has("S.S. Ticket", player), + **{f"Pokemon Tower {floor}F - Wild Pokemon - {slot}": lambda state: state.has("Silph Scope", player) for floor in range(3, 8) for slot in range(1, 11)}, + + "Route 2 - Marcel Trade": lambda state: state.can_reach("Route 24 - Wild Pokemon - 6", "Location", player), + "Underground Tunnel West-East - Spot Trade": lambda state: state.can_reach("Route 24 - Wild Pokemon - 6", "Location", player), + "Route 11 - Terry Trade": lambda state: state.can_reach("Safari Zone Center - Wild Pokemon - 5", "Location", player), + "Route 18 - Marc Trade": lambda state: state.can_reach("Route 23 - Super Rod Pokemon - 1", "Location", player), + "Cinnabar Island - Sailor Trade": lambda state: state.can_reach("Pokemon Mansion 1F - Wild Pokemon - 3", "Location", player), + "Cinnabar Island - Crinkles Trade": lambda state: state.can_reach("Route 12 - Wild Pokemon - 4", "Location", player), + "Cinnabar Island - Doris Trade": lambda state: state.can_reach("Cerulean Cave 1F - Wild Pokemon - 9", "Location", player), + "Vermilion City - Dux Trade": lambda state: state.can_reach("Route 3 - Wild Pokemon - 2", "Location", player), + "Cerulean City - Lola Trade": lambda state: state.can_reach("Route 10 - Super Rod Pokemon - 1", "Location", player), + # PokÊdex check "Pallet Town - Oak's Parcel Reward": lambda state: state.has("Oak's Parcel", player), @@ -142,7 +190,7 @@ def set_rules(world, player): "Pokemon Mansion 1F - Hidden Item Block Near Entrance Carpet": lambda state: state.pokemon_rb_can_get_hidden_items(player), "Pokemon Mansion 3F - Hidden Item Behind Burglar": lambda state: state.pokemon_rb_can_get_hidden_items(player), - "Route 23 - Hidden Item Rocks Before Final Guard": lambda state: state.pokemon_rb_can_get_hidden_items( + "Route 23 - Hidden Item Rocks Before Victory Road": lambda state: state.pokemon_rb_can_get_hidden_items( player), "Route 23 - Hidden Item East Bush After Water": lambda state: state.pokemon_rb_can_get_hidden_items( player), @@ -178,3 +226,11 @@ def set_rules(world, player): for loc in world.get_locations(player): if loc.name in access_rules: add_rule(loc, access_rules[loc.name]) + if loc.name in item_rules: + add_item_rule(loc, item_rules[loc.name]) + if loc.name.startswith("Pokedex"): + mon = loc.name.split(" - ")[1] + add_rule(loc, lambda state, i=mon: (state.has("Pokedex", player) or not + state.multiworld.require_pokedex[player]) and (state.has(i, player) + or state.has(f"Static {i}", player))) + diff --git a/worlds/pokemon_rb/text.py b/worlds/pokemon_rb/text.py index e15623d4b8..d20891d7d7 100644 --- a/worlds/pokemon_rb/text.py +++ b/worlds/pokemon_rb/text.py @@ -1,5 +1,9 @@ special_chars = { "PKMN": 0x4A, + "LINE": 0x4F, + "CONT": 0x55, + "DONE": 0x57, + "PROMPT": 0x58, "'d": 0xBB, "'l": 0xBC, "'t": 0xBE, @@ -105,7 +109,7 @@ char_map = { "9": 0xFF, } -unsafe_chars = ["@", "#", "PKMN"] +unsafe_chars = ["@", "#", "PKMN", "LINE", "DONE", "CONT", "PROMPT"] def encode_text(text: str, length: int=0, whitespace=False, force=False, safety=False): @@ -114,9 +118,9 @@ def encode_text(text: str, length: int=0, whitespace=False, force=False, safety= special = False for char in text: if char == ">": - if spec_char in unsafe_chars and safety: - raise KeyError(f"Disallowed Pokemon text special character '<{spec_char}>'") try: + if spec_char in unsafe_chars and safety: + raise KeyError(f"Disallowed Pokemon text special character '<{spec_char}>'") encoded_text.append(special_chars[spec_char]) except KeyError: if force: @@ -131,10 +135,10 @@ def encode_text(text: str, length: int=0, whitespace=False, force=False, safety= elif special is True: spec_char += char else: - if char in unsafe_chars and safety: - raise KeyError(f"Disallowed Pokemon text character '{char}'") try: encoded_text.append(char_map[char]) + if char in unsafe_chars and safety: + raise KeyError(f"Disallowed Pokemon text character '{char}'") except KeyError: if force: encoded_text.append(char_map[" "]) diff --git a/worlds/sa2b/Items.py b/worlds/sa2b/Items.py index 904a854cfa..28f71fca7a 100644 --- a/worlds/sa2b/Items.py +++ b/worlds/sa2b/Items.py @@ -79,6 +79,8 @@ trap_table = { ItemName.gravity_trap: ItemData(0xFF0034, False, True), ItemName.exposition_trap: ItemData(0xFF0035, False, True), #ItemName.darkness_trap: ItemData(0xFF0036, False, True), + + ItemName.pong_trap: ItemData(0xFF0050, False, True), } emeralds_table = { diff --git a/worlds/sa2b/Locations.py b/worlds/sa2b/Locations.py index f5831cd7ee..82fe3aa5be 100644 --- a/worlds/sa2b/Locations.py +++ b/worlds/sa2b/Locations.py @@ -504,6 +504,235 @@ beetle_location_table = { LocationName.cannon_core_beetle: 0xFF061E, } +omochao_location_table = { + LocationName.city_escape_omo_1: 0xFF0800, + LocationName.wild_canyon_omo_1: 0xFF0801, + LocationName.prison_lane_omo_1: 0xFF0802, + LocationName.metal_harbor_omo_1: 0xFF0803, + LocationName.pumpkin_hill_omo_1: 0xFF0805, + LocationName.mission_street_omo_1: 0xFF0806, + LocationName.aquatic_mine_omo_1: 0xFF0807, + LocationName.hidden_base_omo_1: 0xFF0809, + LocationName.pyramid_cave_omo_1: 0xFF080A, + LocationName.death_chamber_omo_1: 0xFF080B, + LocationName.eternal_engine_omo_1: 0xFF080C, + LocationName.meteor_herd_omo_1: 0xFF080D, + LocationName.crazy_gadget_omo_1: 0xFF080E, + LocationName.final_rush_omo_1: 0xFF080F, + + LocationName.iron_gate_omo_1: 0xFF0810, + LocationName.dry_lagoon_omo_1: 0xFF0811, + LocationName.sand_ocean_omo_1: 0xFF0812, + LocationName.radical_highway_omo_1: 0xFF0813, + LocationName.egg_quarters_omo_1: 0xFF0814, + LocationName.lost_colony_omo_1: 0xFF0815, + LocationName.weapons_bed_omo_1: 0xFF0816, + LocationName.security_hall_omo_1: 0xFF0817, + LocationName.white_jungle_omo_1: 0xFF0818, + LocationName.mad_space_omo_1: 0xFF081B, + LocationName.cosmic_wall_omo_1: 0xFF081C, + LocationName.final_chase_omo_1: 0xFF081D, + + LocationName.cannon_core_omo_1: 0xFF081E, + + LocationName.city_escape_omo_2: 0xFF0820, + LocationName.wild_canyon_omo_2: 0xFF0821, + LocationName.prison_lane_omo_2: 0xFF0822, + LocationName.metal_harbor_omo_2: 0xFF0823, + LocationName.pumpkin_hill_omo_2: 0xFF0825, + LocationName.mission_street_omo_2: 0xFF0826, + LocationName.aquatic_mine_omo_2: 0xFF0827, + LocationName.hidden_base_omo_2: 0xFF0829, + LocationName.pyramid_cave_omo_2: 0xFF082A, + LocationName.death_chamber_omo_2: 0xFF082B, + LocationName.eternal_engine_omo_2: 0xFF082C, + LocationName.meteor_herd_omo_2: 0xFF082D, + LocationName.crazy_gadget_omo_2: 0xFF082E, + LocationName.final_rush_omo_2: 0xFF082F, + + LocationName.iron_gate_omo_2: 0xFF0830, + LocationName.dry_lagoon_omo_2: 0xFF0831, + LocationName.sand_ocean_omo_2: 0xFF0832, + LocationName.radical_highway_omo_2: 0xFF0833, + LocationName.egg_quarters_omo_2: 0xFF0834, + LocationName.lost_colony_omo_2: 0xFF0835, + LocationName.weapons_bed_omo_2: 0xFF0836, + LocationName.security_hall_omo_2: 0xFF0837, + LocationName.white_jungle_omo_2: 0xFF0838, + LocationName.mad_space_omo_2: 0xFF083B, + + LocationName.cannon_core_omo_2: 0xFF083E, + + LocationName.city_escape_omo_3: 0xFF0840, + LocationName.wild_canyon_omo_3: 0xFF0841, + LocationName.prison_lane_omo_3: 0xFF0842, + LocationName.metal_harbor_omo_3: 0xFF0843, + LocationName.pumpkin_hill_omo_3: 0xFF0845, + LocationName.mission_street_omo_3: 0xFF0846, + LocationName.aquatic_mine_omo_3: 0xFF0847, + LocationName.hidden_base_omo_3: 0xFF0849, + LocationName.pyramid_cave_omo_3: 0xFF084A, + LocationName.death_chamber_omo_3: 0xFF084B, + LocationName.eternal_engine_omo_3: 0xFF084C, + LocationName.meteor_herd_omo_3: 0xFF084D, + LocationName.crazy_gadget_omo_3: 0xFF084E, + LocationName.final_rush_omo_3: 0xFF084F, + + LocationName.iron_gate_omo_3: 0xFF0850, + LocationName.dry_lagoon_omo_3: 0xFF0851, + LocationName.radical_highway_omo_3: 0xFF0853, + LocationName.egg_quarters_omo_3: 0xFF0854, + LocationName.lost_colony_omo_3: 0xFF0855, + LocationName.weapons_bed_omo_3: 0xFF0856, + LocationName.security_hall_omo_3: 0xFF0857, + LocationName.white_jungle_omo_3: 0xFF0858, + LocationName.mad_space_omo_3: 0xFF085B, + + LocationName.cannon_core_omo_3: 0xFF085E, + + LocationName.city_escape_omo_4: 0xFF0860, + LocationName.wild_canyon_omo_4: 0xFF0861, + LocationName.prison_lane_omo_4: 0xFF0862, + LocationName.metal_harbor_omo_4: 0xFF0863, + LocationName.pumpkin_hill_omo_4: 0xFF0865, + LocationName.mission_street_omo_4: 0xFF0866, + LocationName.aquatic_mine_omo_4: 0xFF0867, + LocationName.hidden_base_omo_4: 0xFF0869, + LocationName.pyramid_cave_omo_4: 0xFF086A, + LocationName.death_chamber_omo_4: 0xFF086B, + LocationName.eternal_engine_omo_4: 0xFF086C, + LocationName.crazy_gadget_omo_4: 0xFF086E, + + LocationName.iron_gate_omo_4: 0xFF0870, + LocationName.dry_lagoon_omo_4: 0xFF0871, + LocationName.radical_highway_omo_4: 0xFF0873, + LocationName.egg_quarters_omo_4: 0xFF0874, + LocationName.lost_colony_omo_4: 0xFF0875, + LocationName.security_hall_omo_4: 0xFF0877, + LocationName.white_jungle_omo_4: 0xFF0878, + LocationName.mad_space_omo_4: 0xFF087B, + + LocationName.cannon_core_omo_4: 0xFF087E, + + LocationName.city_escape_omo_5: 0xFF0880, + LocationName.wild_canyon_omo_5: 0xFF0881, + LocationName.prison_lane_omo_5: 0xFF0882, + LocationName.metal_harbor_omo_5: 0xFF0883, + LocationName.pumpkin_hill_omo_5: 0xFF0885, + LocationName.mission_street_omo_5: 0xFF0886, + LocationName.aquatic_mine_omo_5: 0xFF0887, + LocationName.death_chamber_omo_5: 0xFF088B, + LocationName.eternal_engine_omo_5: 0xFF088C, + LocationName.crazy_gadget_omo_5: 0xFF088E, + + LocationName.iron_gate_omo_5: 0xFF0890, + LocationName.dry_lagoon_omo_5: 0xFF0891, + LocationName.radical_highway_omo_5: 0xFF0893, + LocationName.egg_quarters_omo_5: 0xFF0894, + LocationName.lost_colony_omo_5: 0xFF0895, + LocationName.security_hall_omo_5: 0xFF0897, + LocationName.white_jungle_omo_5: 0xFF0898, + LocationName.mad_space_omo_5: 0xFF089B, + + LocationName.cannon_core_omo_5: 0xFF089E, + + LocationName.city_escape_omo_6: 0xFF08A0, + LocationName.wild_canyon_omo_6: 0xFF08A1, + LocationName.prison_lane_omo_6: 0xFF08A2, + LocationName.pumpkin_hill_omo_6: 0xFF08A5, + LocationName.mission_street_omo_6: 0xFF08A6, + LocationName.aquatic_mine_omo_6: 0xFF08A7, + LocationName.death_chamber_omo_6: 0xFF08AB, + LocationName.eternal_engine_omo_6: 0xFF08AC, + LocationName.crazy_gadget_omo_6: 0xFF08AE, + + LocationName.iron_gate_omo_6: 0xFF08B0, + LocationName.dry_lagoon_omo_6: 0xFF08B1, + LocationName.radical_highway_omo_6: 0xFF08B3, + LocationName.egg_quarters_omo_6: 0xFF08B4, + LocationName.lost_colony_omo_6: 0xFF08B5, + LocationName.security_hall_omo_6: 0xFF08B7, + + LocationName.cannon_core_omo_6: 0xFF08BE, + + LocationName.city_escape_omo_7: 0xFF08C0, + LocationName.wild_canyon_omo_7: 0xFF08C1, + LocationName.prison_lane_omo_7: 0xFF08C2, + LocationName.pumpkin_hill_omo_7: 0xFF08C5, + LocationName.mission_street_omo_7: 0xFF08C6, + LocationName.aquatic_mine_omo_7: 0xFF08C7, + LocationName.death_chamber_omo_7: 0xFF08CB, + LocationName.eternal_engine_omo_7: 0xFF08CC, + LocationName.crazy_gadget_omo_7: 0xFF08CE, + + LocationName.dry_lagoon_omo_7: 0xFF08D1, + LocationName.radical_highway_omo_7: 0xFF08D3, + LocationName.egg_quarters_omo_7: 0xFF08D4, + LocationName.lost_colony_omo_7: 0xFF08D5, + LocationName.security_hall_omo_7: 0xFF08D7, + + LocationName.cannon_core_omo_7: 0xFF08DE, + + LocationName.city_escape_omo_8: 0xFF08E0, + LocationName.wild_canyon_omo_8: 0xFF08E1, + LocationName.prison_lane_omo_8: 0xFF08E2, + LocationName.pumpkin_hill_omo_8: 0xFF08E5, + LocationName.mission_street_omo_8: 0xFF08E6, + LocationName.death_chamber_omo_8: 0xFF08EB, + LocationName.eternal_engine_omo_8: 0xFF08EC, + LocationName.crazy_gadget_omo_8: 0xFF08EE, + + LocationName.dry_lagoon_omo_8: 0xFF08F1, + LocationName.radical_highway_omo_8: 0xFF08F3, + LocationName.lost_colony_omo_8: 0xFF08F5, + LocationName.security_hall_omo_8: 0xFF08F7, + + LocationName.cannon_core_omo_8: 0xFF08FE, + + LocationName.city_escape_omo_9: 0xFF0900, + LocationName.wild_canyon_omo_9: 0xFF0901, + LocationName.prison_lane_omo_9: 0xFF0902, + LocationName.pumpkin_hill_omo_9: 0xFF0905, + LocationName.death_chamber_omo_9: 0xFF090B, + LocationName.eternal_engine_omo_9: 0xFF090C, + LocationName.crazy_gadget_omo_9: 0xFF090E, + + LocationName.dry_lagoon_omo_9: 0xFF0911, + LocationName.security_hall_omo_9: 0xFF0917, + + LocationName.cannon_core_omo_9: 0xFF091E, + + LocationName.city_escape_omo_10: 0xFF0920, + LocationName.wild_canyon_omo_10: 0xFF0921, + LocationName.prison_lane_omo_10: 0xFF0922, + LocationName.pumpkin_hill_omo_10: 0xFF0925, + LocationName.eternal_engine_omo_10: 0xFF092C, + LocationName.crazy_gadget_omo_10: 0xFF092E, + + LocationName.dry_lagoon_omo_10: 0xFF0931, + LocationName.security_hall_omo_10: 0xFF0937, + + LocationName.city_escape_omo_11: 0xFF0940, + LocationName.pumpkin_hill_omo_11: 0xFF0945, + LocationName.eternal_engine_omo_11: 0xFF094C, + LocationName.crazy_gadget_omo_11: 0xFF094E, + + LocationName.dry_lagoon_omo_11: 0xFF0951, + LocationName.security_hall_omo_11: 0xFF0957, + + LocationName.city_escape_omo_12: 0xFF0960, + LocationName.eternal_engine_omo_12: 0xFF096C, + LocationName.crazy_gadget_omo_12: 0xFF096E, + + LocationName.dry_lagoon_omo_12: 0xFF0971, + LocationName.security_hall_omo_12: 0xFF0977, + + LocationName.city_escape_omo_13: 0xFF0980, + LocationName.crazy_gadget_omo_13: 0xFF098E, + + LocationName.city_escape_omo_14: 0xFF09A0, +} + boss_gate_location_table = { LocationName.gate_1_boss: 0xFF0100, LocationName.gate_2_boss: 0xFF0101, @@ -530,6 +759,33 @@ chao_garden_beginner_location_table = { } chao_garden_intermediate_location_table = { + LocationName.chao_race_challenge_1: 0xFF022A, + LocationName.chao_race_challenge_2: 0xFF022B, + LocationName.chao_race_challenge_3: 0xFF022C, + LocationName.chao_race_challenge_4: 0xFF022D, + LocationName.chao_race_challenge_5: 0xFF022E, + LocationName.chao_race_challenge_6: 0xFF022F, + LocationName.chao_race_challenge_7: 0xFF0230, + LocationName.chao_race_challenge_8: 0xFF0231, + LocationName.chao_race_challenge_9: 0xFF0232, + LocationName.chao_race_challenge_10: 0xFF0233, + LocationName.chao_race_challenge_11: 0xFF0234, + LocationName.chao_race_challenge_12: 0xFF0235, + + LocationName.chao_race_hero_1: 0xFF0236, + LocationName.chao_race_hero_2: 0xFF0237, + LocationName.chao_race_hero_3: 0xFF0238, + LocationName.chao_race_hero_4: 0xFF0239, + + LocationName.chao_race_dark_1: 0xFF023A, + LocationName.chao_race_dark_2: 0xFF023B, + LocationName.chao_race_dark_3: 0xFF023C, + LocationName.chao_race_dark_4: 0xFF023D, + + LocationName.chao_standard_karate: 0xFF0301, +} + +chao_garden_expert_location_table = { LocationName.chao_race_aquamarine_1: 0xFF020C, LocationName.chao_race_aquamarine_2: 0xFF020D, LocationName.chao_race_aquamarine_3: 0xFF020E, @@ -561,37 +817,43 @@ chao_garden_intermediate_location_table = { LocationName.chao_race_diamond_4: 0xFF0228, LocationName.chao_race_diamond_5: 0xFF0229, - LocationName.chao_standard_karate: 0xFF0301, -} - -chao_garden_expert_location_table = { - LocationName.chao_race_challenge_1: 0xFF022A, - LocationName.chao_race_challenge_2: 0xFF022B, - LocationName.chao_race_challenge_3: 0xFF022C, - LocationName.chao_race_challenge_4: 0xFF022D, - LocationName.chao_race_challenge_5: 0xFF022E, - LocationName.chao_race_challenge_6: 0xFF022F, - LocationName.chao_race_challenge_7: 0xFF0230, - LocationName.chao_race_challenge_8: 0xFF0231, - LocationName.chao_race_challenge_9: 0xFF0232, - LocationName.chao_race_challenge_10: 0xFF0233, - LocationName.chao_race_challenge_11: 0xFF0234, - LocationName.chao_race_challenge_12: 0xFF0235, - - LocationName.chao_race_hero_1: 0xFF0236, - LocationName.chao_race_hero_2: 0xFF0237, - LocationName.chao_race_hero_3: 0xFF0238, - LocationName.chao_race_hero_4: 0xFF0239, - - LocationName.chao_race_dark_1: 0xFF023A, - LocationName.chao_race_dark_2: 0xFF023B, - LocationName.chao_race_dark_3: 0xFF023C, - LocationName.chao_race_dark_4: 0xFF023D, - LocationName.chao_expert_karate: 0xFF0302, LocationName.chao_super_karate: 0xFF0303, } +kart_race_beginner_location_table = { + LocationName.kart_race_beginner_sonic: 0xFF0A00, + LocationName.kart_race_beginner_tails: 0xFF0A01, + LocationName.kart_race_beginner_knuckles: 0xFF0A02, + LocationName.kart_race_beginner_shadow: 0xFF0A03, + LocationName.kart_race_beginner_eggman: 0xFF0A04, + LocationName.kart_race_beginner_rouge: 0xFF0A05, +} + +kart_race_standard_location_table = { + LocationName.kart_race_standard_sonic: 0xFF0A06, + LocationName.kart_race_standard_tails: 0xFF0A07, + LocationName.kart_race_standard_knuckles: 0xFF0A08, + LocationName.kart_race_standard_shadow: 0xFF0A09, + LocationName.kart_race_standard_eggman: 0xFF0A0A, + LocationName.kart_race_standard_rouge: 0xFF0A0B, +} + +kart_race_expert_location_table = { + LocationName.kart_race_expert_sonic: 0xFF0A0C, + LocationName.kart_race_expert_tails: 0xFF0A0D, + LocationName.kart_race_expert_knuckles: 0xFF0A0E, + LocationName.kart_race_expert_shadow: 0xFF0A0F, + LocationName.kart_race_expert_eggman: 0xFF0A10, + LocationName.kart_race_expert_rouge: 0xFF0A11, +} + +kart_race_mini_location_table = { + LocationName.kart_race_beginner: 0xFF0A12, + LocationName.kart_race_standard: 0xFF0A13, + LocationName.kart_race_expert: 0xFF0A14, +} + green_hill_location_table = { LocationName.green_hill: 0xFF001F, } @@ -605,6 +867,10 @@ final_boss_location_table = { LocationName.finalhazard: 0xFF005F, } +grand_prix_location_table = { + LocationName.grand_prix: 0xFF007F, +} + all_locations = { **mission_location_table, **upgrade_location_table, @@ -613,12 +879,18 @@ all_locations = { **pipe_location_table, **hidden_whistle_location_table, **beetle_location_table, + **omochao_location_table, **chao_garden_beginner_location_table, **chao_garden_intermediate_location_table, **chao_garden_expert_location_table, + **kart_race_beginner_location_table, + **kart_race_standard_location_table, + **kart_race_expert_location_table, + **kart_race_mini_location_table, **green_hill_location_table, **green_hill_chao_location_table, **final_boss_location_table, + **grand_prix_location_table, } boss_gate_set = [ @@ -665,62 +937,80 @@ def setup_locations(world: MultiWorld, player: int, mission_map: typing.Dict[int location_table = {} chao_location_table = {} + if world.goal[player] == 3: + if world.kart_race_checks[player] == 2: + location_table.update({**kart_race_beginner_location_table}) + location_table.update({**kart_race_standard_location_table}) + location_table.update({**kart_race_expert_location_table}) + elif world.kart_race_checks[player] == 1: + location_table.update({**kart_race_mini_location_table}) + location_table.update({**grand_prix_location_table}) + else: + for i in range(31): + mission_count = mission_count_map[i] + mission_order: typing.List[int] = mission_orders[mission_map[i]] + stage_prefix: str = stage_name_prefixes[i] - for i in range(31): - mission_count = mission_count_map[i] - mission_order: typing.List[int] = mission_orders[mission_map[i]] - stage_prefix: str = stage_name_prefixes[i] + for j in range(mission_count): + mission_number = mission_order[j] + location_name: str = stage_prefix + str(mission_number) + location_table[location_name] = mission_location_table[location_name] - for j in range(mission_count): - mission_number = mission_order[j] - location_name: str = stage_prefix + str(mission_number) - location_table[location_name] = mission_location_table[location_name] - - location_table.update({**upgrade_location_table}) - - if world.keysanity[player]: - location_table.update({**chao_key_location_table}) - - if world.whistlesanity[player].value == 1: - location_table.update({**pipe_location_table}) - elif world.whistlesanity[player].value == 2: - location_table.update({**hidden_whistle_location_table}) - elif world.whistlesanity[player].value == 3: - location_table.update({**pipe_location_table}) - location_table.update({**hidden_whistle_location_table}) - - if world.beetlesanity[player]: - location_table.update({**beetle_location_table}) - - if world.goal[player].value == 0 or world.goal[player].value == 2: - location_table.update({**final_boss_location_table}) - - if world.goal[player].value == 1 or world.goal[player].value == 2: - location_table.update({**green_hill_location_table}) + location_table.update({**upgrade_location_table}) if world.keysanity[player]: - location_table.update({**green_hill_chao_location_table}) + location_table.update({**chao_key_location_table}) - if world.chao_garden_difficulty[player].value >= 1: - chao_location_table.update({**chao_garden_beginner_location_table}) - if world.chao_garden_difficulty[player].value >= 2: - chao_location_table.update({**chao_garden_intermediate_location_table}) - if world.chao_garden_difficulty[player].value >= 3: - chao_location_table.update({**chao_garden_expert_location_table}) + if world.whistlesanity[player].value == 1: + location_table.update({**pipe_location_table}) + elif world.whistlesanity[player].value == 2: + location_table.update({**hidden_whistle_location_table}) + elif world.whistlesanity[player].value == 3: + location_table.update({**pipe_location_table}) + location_table.update({**hidden_whistle_location_table}) - for key, value in chao_location_table.items(): - if key in chao_karate_set: - if world.include_chao_karate[player]: + if world.beetlesanity[player]: + location_table.update({**beetle_location_table}) + + if world.omosanity[player]: + location_table.update({**omochao_location_table}) + + if world.kart_race_checks[player] == 2: + location_table.update({**kart_race_beginner_location_table}) + location_table.update({**kart_race_standard_location_table}) + location_table.update({**kart_race_expert_location_table}) + elif world.kart_race_checks[player] == 1: + location_table.update({**kart_race_mini_location_table}) + + if world.goal[player].value == 0 or world.goal[player].value == 2: + location_table.update({**final_boss_location_table}) + + if world.goal[player].value == 1 or world.goal[player].value == 2: + location_table.update({**green_hill_location_table}) + + if world.keysanity[player]: + location_table.update({**green_hill_chao_location_table}) + + if world.chao_garden_difficulty[player].value >= 1: + chao_location_table.update({**chao_garden_beginner_location_table}) + if world.chao_garden_difficulty[player].value >= 2: + chao_location_table.update({**chao_garden_intermediate_location_table}) + if world.chao_garden_difficulty[player].value >= 3: + chao_location_table.update({**chao_garden_expert_location_table}) + + for key, value in chao_location_table.items(): + if key in chao_karate_set: + if world.include_chao_karate[player]: + location_table[key] = value + elif key not in chao_race_prize_set: + if world.chao_race_checks[player] == "all": + location_table[key] = value + else: location_table[key] = value - elif key not in chao_race_prize_set: - if world.chao_race_checks[player] == "all": - location_table[key] = value - else: - location_table[key] = value - for x in range(len(boss_gate_set)): - if x < world.number_of_level_gates[player].value: - location_table[boss_gate_set[x]] = boss_gate_location_table[boss_gate_set[x]] + for x in range(len(boss_gate_set)): + if x < world.number_of_level_gates[player].value: + location_table[boss_gate_set[x]] = boss_gate_location_table[boss_gate_set[x]] return location_table diff --git a/worlds/sa2b/Missions.py b/worlds/sa2b/Missions.py index d9767586a6..fbff62c136 100644 --- a/worlds/sa2b/Missions.py +++ b/worlds/sa2b/Missions.py @@ -194,48 +194,52 @@ stage_name_prefixes: typing.List[str] = [ ] def get_mission_count_table(multiworld: MultiWorld, player: int): - speed_active_missions = 1 - mech_active_missions = 1 - hunt_active_missions = 1 - kart_active_missions = 1 - cannons_core_active_missions = 1 - - for i in range(2,6): - if getattr(multiworld, "speed_mission_" + str(i), None)[player]: - speed_active_missions += 1 - - if getattr(multiworld, "mech_mission_" + str(i), None)[player]: - mech_active_missions += 1 - - if getattr(multiworld, "hunt_mission_" + str(i), None)[player]: - hunt_active_missions += 1 - - if getattr(multiworld, "kart_mission_" + str(i), None)[player]: - kart_active_missions += 1 - - if getattr(multiworld, "cannons_core_mission_" + str(i), None)[player]: - cannons_core_active_missions += 1 - - speed_active_missions = min(speed_active_missions, multiworld.speed_mission_count[player].value) - mech_active_missions = min(mech_active_missions, multiworld.mech_mission_count[player].value) - hunt_active_missions = min(hunt_active_missions, multiworld.hunt_mission_count[player].value) - kart_active_missions = min(kart_active_missions, multiworld.kart_mission_count[player].value) - cannons_core_active_missions = min(cannons_core_active_missions, multiworld.cannons_core_mission_count[player].value) - - active_missions: typing.List[typing.List[int]] = [ - speed_active_missions, - mech_active_missions, - hunt_active_missions, - kart_active_missions, - cannons_core_active_missions - ] - mission_count_table: typing.Dict[int, int] = {} - for level in range(31): - level_style = level_styles[level] - level_mission_count = active_missions[level_style] - mission_count_table[level] = level_mission_count + if multiworld.goal[player] == 3: + for level in range(31): + mission_count_table[level] = 0 + else: + speed_active_missions = 1 + mech_active_missions = 1 + hunt_active_missions = 1 + kart_active_missions = 1 + cannons_core_active_missions = 1 + + for i in range(2,6): + if getattr(multiworld, "speed_mission_" + str(i), None)[player]: + speed_active_missions += 1 + + if getattr(multiworld, "mech_mission_" + str(i), None)[player]: + mech_active_missions += 1 + + if getattr(multiworld, "hunt_mission_" + str(i), None)[player]: + hunt_active_missions += 1 + + if getattr(multiworld, "kart_mission_" + str(i), None)[player]: + kart_active_missions += 1 + + if getattr(multiworld, "cannons_core_mission_" + str(i), None)[player]: + cannons_core_active_missions += 1 + + speed_active_missions = min(speed_active_missions, multiworld.speed_mission_count[player].value) + mech_active_missions = min(mech_active_missions, multiworld.mech_mission_count[player].value) + hunt_active_missions = min(hunt_active_missions, multiworld.hunt_mission_count[player].value) + kart_active_missions = min(kart_active_missions, multiworld.kart_mission_count[player].value) + cannons_core_active_missions = min(cannons_core_active_missions, multiworld.cannons_core_mission_count[player].value) + + active_missions: typing.List[typing.List[int]] = [ + speed_active_missions, + mech_active_missions, + hunt_active_missions, + kart_active_missions, + cannons_core_active_missions + ] + + for level in range(31): + level_style = level_styles[level] + level_mission_count = active_missions[level_style] + mission_count_table[level] = level_mission_count return mission_count_table @@ -243,73 +247,77 @@ def get_mission_count_table(multiworld: MultiWorld, player: int): def get_mission_table(multiworld: MultiWorld, player: int): mission_table: typing.Dict[int, int] = {} - speed_active_missions: typing.List[int] = [1] - mech_active_missions: typing.List[int] = [1] - hunt_active_missions: typing.List[int] = [1] - kart_active_missions: typing.List[int] = [1] - cannons_core_active_missions: typing.List[int] = [1] + if multiworld.goal[player] == 3: + for level in range(31): + mission_table[level] = 0 + else: + speed_active_missions: typing.List[int] = [1] + mech_active_missions: typing.List[int] = [1] + hunt_active_missions: typing.List[int] = [1] + kart_active_missions: typing.List[int] = [1] + cannons_core_active_missions: typing.List[int] = [1] - # Add included missions - for i in range(2,6): - if getattr(multiworld, "speed_mission_" + str(i), None)[player]: - speed_active_missions.append(i) - - if getattr(multiworld, "mech_mission_" + str(i), None)[player]: - mech_active_missions.append(i) - - if getattr(multiworld, "hunt_mission_" + str(i), None)[player]: - hunt_active_missions.append(i) - - if getattr(multiworld, "kart_mission_" + str(i), None)[player]: - kart_active_missions.append(i) - - if getattr(multiworld, "cannons_core_mission_" + str(i), None)[player]: - cannons_core_active_missions.append(i) - - active_missions: typing.List[typing.List[int]] = [ - speed_active_missions, - mech_active_missions, - hunt_active_missions, - kart_active_missions, - cannons_core_active_missions - ] - - for level in range(31): - level_style = level_styles[level] - - level_active_missions: typing.List[int] = copy.deepcopy(active_missions[level_style]) - level_chosen_missions: typing.List[int] = [] - - # The first mission must be M1, M2, or M4 - first_mission = 1 - - if multiworld.mission_shuffle[player]: - first_mission = multiworld.random.choice([mission for mission in level_active_missions if mission in [1, 2, 3, 4]]) - - level_active_missions.remove(first_mission) - - # Place Active Missions in the chosen mission list - for mission in level_active_missions: - if mission not in level_chosen_missions: - level_chosen_missions.append(mission) - - if multiworld.mission_shuffle[player]: - multiworld.random.shuffle(level_chosen_missions) - - level_chosen_missions.insert(0, first_mission) - - # Fill in the non-included missions + # Add included missions for i in range(2,6): - if i not in level_chosen_missions: - level_chosen_missions.append(i) + if getattr(multiworld, "speed_mission_" + str(i), None)[player]: + speed_active_missions.append(i) - # Determine which mission order index we have, for conveying to the mod - for i in range(len(mission_orders)): - if mission_orders[i] == level_chosen_missions: - level_mission_index = i - break + if getattr(multiworld, "mech_mission_" + str(i), None)[player]: + mech_active_missions.append(i) - mission_table[level] = level_mission_index + if getattr(multiworld, "hunt_mission_" + str(i), None)[player]: + hunt_active_missions.append(i) + + if getattr(multiworld, "kart_mission_" + str(i), None)[player]: + kart_active_missions.append(i) + + if getattr(multiworld, "cannons_core_mission_" + str(i), None)[player]: + cannons_core_active_missions.append(i) + + active_missions: typing.List[typing.List[int]] = [ + speed_active_missions, + mech_active_missions, + hunt_active_missions, + kart_active_missions, + cannons_core_active_missions + ] + + for level in range(31): + level_style = level_styles[level] + + level_active_missions: typing.List[int] = copy.deepcopy(active_missions[level_style]) + level_chosen_missions: typing.List[int] = [] + + # The first mission must be M1, M2, M3, or M4 + first_mission = 1 + + if multiworld.mission_shuffle[player]: + first_mission = multiworld.random.choice([mission for mission in level_active_missions if mission in [1, 2, 3, 4]]) + + level_active_missions.remove(first_mission) + + # Place Active Missions in the chosen mission list + for mission in level_active_missions: + if mission not in level_chosen_missions: + level_chosen_missions.append(mission) + + if multiworld.mission_shuffle[player]: + multiworld.random.shuffle(level_chosen_missions) + + level_chosen_missions.insert(0, first_mission) + + # Fill in the non-included missions + for i in range(2,6): + if i not in level_chosen_missions: + level_chosen_missions.append(i) + + # Determine which mission order index we have, for conveying to the mod + for i in range(len(mission_orders)): + if mission_orders[i] == level_chosen_missions: + level_mission_index = i + break + + mission_table[level] = level_mission_index return mission_table diff --git a/worlds/sa2b/Names/ItemName.py b/worlds/sa2b/Names/ItemName.py index 270b113383..7fa34ea722 100644 --- a/worlds/sa2b/Names/ItemName.py +++ b/worlds/sa2b/Names/ItemName.py @@ -51,6 +51,7 @@ tiny_trap = "Tiny Trap" gravity_trap = "Gravity Trap" exposition_trap = "Exposition Trap" darkness_trap = "Darkness Trap" +pong_trap = "Pong Trap" white_emerald = "White Chaos Emerald" red_emerald = "Red Chaos Emerald" diff --git a/worlds/sa2b/Names/LocationName.py b/worlds/sa2b/Names/LocationName.py index 34826a1ccd..9a970bda75 100644 --- a/worlds/sa2b/Names/LocationName.py +++ b/worlds/sa2b/Names/LocationName.py @@ -16,6 +16,20 @@ city_escape_hidden_2 = "City Escape - Hidden 2" city_escape_hidden_3 = "City Escape - Hidden 3" city_escape_hidden_4 = "City Escape - Hidden 4" city_escape_hidden_5 = "City Escape - Hidden 5" +city_escape_omo_1 = "City Escape - Omochao 1" +city_escape_omo_2 = "City Escape - Omochao 2" +city_escape_omo_3 = "City Escape - Omochao 3" +city_escape_omo_4 = "City Escape - Omochao 4" +city_escape_omo_5 = "City Escape - Omochao 5" +city_escape_omo_6 = "City Escape - Omochao 6" +city_escape_omo_7 = "City Escape - Omochao 7" +city_escape_omo_8 = "City Escape - Omochao 8" +city_escape_omo_9 = "City Escape - Omochao 9" +city_escape_omo_10 = "City Escape - Omochao 10" +city_escape_omo_11 = "City Escape - Omochao 11" +city_escape_omo_12 = "City Escape - Omochao 12" +city_escape_omo_13 = "City Escape - Omochao 13" +city_escape_omo_14 = "City Escape - Omochao 14" city_escape_beetle = "City Escape - Gold Beetle" city_escape_upgrade = "City Escape - Upgrade" metal_harbor_1 = "Metal Harbor - 1" @@ -27,6 +41,11 @@ metal_harbor_chao_1 = "Metal Harbor - Chao Key 1" metal_harbor_chao_2 = "Metal Harbor - Chao Key 2" metal_harbor_chao_3 = "Metal Harbor - Chao Key 3" metal_harbor_pipe_1 = "Metal Harbor - Pipe 1" +metal_harbor_omo_1 = "Metal Harbor - Omochao 1" +metal_harbor_omo_2 = "Metal Harbor - Omochao 2" +metal_harbor_omo_3 = "Metal Harbor - Omochao 3" +metal_harbor_omo_4 = "Metal Harbor - Omochao 4" +metal_harbor_omo_5 = "Metal Harbor - Omochao 5" metal_harbor_beetle = "Metal Harbor - Gold Beetle" metal_harbor_upgrade = "Metal Harbor - Upgrade" green_forest_1 = "Green Forest - 1" @@ -57,6 +76,10 @@ pyramid_cave_pipe_1 = "Pyramid Cave - Pipe 1" pyramid_cave_pipe_2 = "Pyramid Cave - Pipe 2" pyramid_cave_pipe_3 = "Pyramid Cave - Pipe 3" pyramid_cave_pipe_4 = "Pyramid Cave - Pipe 4" +pyramid_cave_omo_1 = "Pyramid Cave - Omochao 1" +pyramid_cave_omo_2 = "Pyramid Cave - Omochao 2" +pyramid_cave_omo_3 = "Pyramid Cave - Omochao 3" +pyramid_cave_omo_4 = "Pyramid Cave - Omochao 4" pyramid_cave_beetle = "Pyramid Cave - Gold Beetle" pyramid_cave_upgrade = "Pyramid Cave - Upgrade" crazy_gadget_1 = "Crazy Gadget - 1" @@ -72,6 +95,19 @@ crazy_gadget_pipe_2 = "Crazy Gadget - Pipe 2" crazy_gadget_pipe_3 = "Crazy Gadget - Pipe 3" crazy_gadget_pipe_4 = "Crazy Gadget - Pipe 4" crazy_gadget_hidden_1 = "Crazy Gadget - Hidden 1" +crazy_gadget_omo_1 = "Crazy Gadget - Omochao 1" +crazy_gadget_omo_2 = "Crazy Gadget - Omochao 2" +crazy_gadget_omo_3 = "Crazy Gadget - Omochao 3" +crazy_gadget_omo_4 = "Crazy Gadget - Omochao 4" +crazy_gadget_omo_5 = "Crazy Gadget - Omochao 5" +crazy_gadget_omo_6 = "Crazy Gadget - Omochao 6" +crazy_gadget_omo_7 = "Crazy Gadget - Omochao 7" +crazy_gadget_omo_8 = "Crazy Gadget - Omochao 8" +crazy_gadget_omo_9 = "Crazy Gadget - Omochao 9" +crazy_gadget_omo_10 = "Crazy Gadget - Omochao 10" +crazy_gadget_omo_11 = "Crazy Gadget - Omochao 11" +crazy_gadget_omo_12 = "Crazy Gadget - Omochao 12" +crazy_gadget_omo_13 = "Crazy Gadget - Omochao 13" crazy_gadget_beetle = "Crazy Gadget - Gold Beetle" crazy_gadget_upgrade = "Crazy Gadget - Upgrade" final_rush_1 = "Final Rush - 1" @@ -84,6 +120,9 @@ final_rush_chao_2 = "Final Rush - Chao Key 2" final_rush_chao_3 = "Final Rush - Chao Key 3" final_rush_pipe_1 = "Final Rush - Pipe 1" final_rush_pipe_2 = "Final Rush - Pipe 2" +final_rush_omo_1 = "Final Rush - Omochao 1" +final_rush_omo_2 = "Final Rush - Omochao 2" +final_rush_omo_3 = "Final Rush - Omochao 3" final_rush_beetle = "Final Rush - Gold Beetle" final_rush_upgrade = "Final Rush - Upgrade" @@ -102,6 +141,16 @@ prison_lane_pipe_3 = "Prison Lane - Pipe 3" prison_lane_hidden_1 = "Prison Lane - Hidden 1" prison_lane_hidden_2 = "Prison Lane - Hidden 2" prison_lane_hidden_3 = "Prison Lane - Hidden 3" +prison_lane_omo_1 = "Prison Lane - Omochao 1" +prison_lane_omo_2 = "Prison Lane - Omochao 2" +prison_lane_omo_3 = "Prison Lane - Omochao 3" +prison_lane_omo_4 = "Prison Lane - Omochao 4" +prison_lane_omo_5 = "Prison Lane - Omochao 5" +prison_lane_omo_6 = "Prison Lane - Omochao 6" +prison_lane_omo_7 = "Prison Lane - Omochao 7" +prison_lane_omo_8 = "Prison Lane - Omochao 8" +prison_lane_omo_9 = "Prison Lane - Omochao 9" +prison_lane_omo_10 = "Prison Lane - Omochao 10" prison_lane_beetle = "Prison Lane - Gold Beetle" prison_lane_upgrade = "Prison Lane - Upgrade" mission_street_1 = "Mission Street - 1" @@ -119,6 +168,14 @@ mission_street_hidden_1 = "Mission Street - Hidden 1" mission_street_hidden_2 = "Mission Street - Hidden 2" mission_street_hidden_3 = "Mission Street - Hidden 3" mission_street_hidden_4 = "Mission Street - Hidden 4" +mission_street_omo_1 = "Mission Street - Omochao 1" +mission_street_omo_2 = "Mission Street - Omochao 2" +mission_street_omo_3 = "Mission Street - Omochao 3" +mission_street_omo_4 = "Mission Street - Omochao 4" +mission_street_omo_5 = "Mission Street - Omochao 5" +mission_street_omo_6 = "Mission Street - Omochao 6" +mission_street_omo_7 = "Mission Street - Omochao 7" +mission_street_omo_8 = "Mission Street - Omochao 8" mission_street_beetle = "Mission Street - Gold Beetle" mission_street_upgrade = "Mission Street - Upgrade" route_101_1 = "Route 101 - 1" @@ -138,6 +195,10 @@ hidden_base_pipe_2 = "Hidden Base - Pipe 2" hidden_base_pipe_3 = "Hidden Base - Pipe 3" hidden_base_pipe_4 = "Hidden Base - Pipe 4" hidden_base_pipe_5 = "Hidden Base - Pipe 5" +hidden_base_omo_1 = "Hidden Base - Omochao 1" +hidden_base_omo_2 = "Hidden Base - Omochao 2" +hidden_base_omo_3 = "Hidden Base - Omochao 3" +hidden_base_omo_4 = "Hidden Base - Omochao 4" hidden_base_beetle = "Hidden Base - Gold Beetle" hidden_base_upgrade = "Hidden Base - Upgrade" eternal_engine_1 = "Eternal Engine - 1" @@ -153,6 +214,18 @@ eternal_engine_pipe_2 = "Eternal Engine - Pipe 2" eternal_engine_pipe_3 = "Eternal Engine - Pipe 3" eternal_engine_pipe_4 = "Eternal Engine - Pipe 4" eternal_engine_pipe_5 = "Eternal Engine - Pipe 5" +eternal_engine_omo_1 = "Eternal Engine - Omochao 1" +eternal_engine_omo_2 = "Eternal Engine - Omochao 2" +eternal_engine_omo_3 = "Eternal Engine - Omochao 3" +eternal_engine_omo_4 = "Eternal Engine - Omochao 4" +eternal_engine_omo_5 = "Eternal Engine - Omochao 5" +eternal_engine_omo_6 = "Eternal Engine - Omochao 6" +eternal_engine_omo_7 = "Eternal Engine - Omochao 7" +eternal_engine_omo_8 = "Eternal Engine - Omochao 8" +eternal_engine_omo_9 = "Eternal Engine - Omochao 9" +eternal_engine_omo_10 = "Eternal Engine - Omochao 10" +eternal_engine_omo_11 = "Eternal Engine - Omochao 11" +eternal_engine_omo_12 = "Eternal Engine - Omochao 12" eternal_engine_beetle = "Eternal Engine - Gold Beetle" eternal_engine_upgrade = "Eternal Engine - Upgrade" @@ -168,6 +241,16 @@ wild_canyon_chao_3 = "Wild Canyon - Chao Key 3" wild_canyon_pipe_1 = "Wild Canyon - Pipe 1" wild_canyon_pipe_2 = "Wild Canyon - Pipe 2" wild_canyon_pipe_3 = "Wild Canyon - Pipe 3" +wild_canyon_omo_1 = "Wild Canyon - Omochao 1" +wild_canyon_omo_2 = "Wild Canyon - Omochao 2" +wild_canyon_omo_3 = "Wild Canyon - Omochao 3" +wild_canyon_omo_4 = "Wild Canyon - Omochao 4" +wild_canyon_omo_5 = "Wild Canyon - Omochao 5" +wild_canyon_omo_6 = "Wild Canyon - Omochao 6" +wild_canyon_omo_7 = "Wild Canyon - Omochao 7" +wild_canyon_omo_8 = "Wild Canyon - Omochao 8" +wild_canyon_omo_9 = "Wild Canyon - Omochao 9" +wild_canyon_omo_10 = "Wild Canyon - Omochao 10" wild_canyon_beetle = "Wild Canyon - Gold Beetle" wild_canyon_upgrade = "Wild Canyon - Upgrade" pumpkin_hill_1 = "Pumpkin Hill - 1" @@ -180,6 +263,17 @@ pumpkin_hill_chao_2 = "Pumpkin Hill - Chao Key 2" pumpkin_hill_chao_3 = "Pumpkin Hill - Chao Key 3" pumpkin_hill_pipe_1 = "Pumpkin Hill - Pipe 1" pumpkin_hill_hidden_1 = "Pumpkin Hill - Hidden 1" +pumpkin_hill_omo_1 = "Pumpkin Hill - Omochao 1" +pumpkin_hill_omo_2 = "Pumpkin Hill - Omochao 2" +pumpkin_hill_omo_3 = "Pumpkin Hill - Omochao 3" +pumpkin_hill_omo_4 = "Pumpkin Hill - Omochao 4" +pumpkin_hill_omo_5 = "Pumpkin Hill - Omochao 5" +pumpkin_hill_omo_6 = "Pumpkin Hill - Omochao 6" +pumpkin_hill_omo_7 = "Pumpkin Hill - Omochao 7" +pumpkin_hill_omo_8 = "Pumpkin Hill - Omochao 8" +pumpkin_hill_omo_9 = "Pumpkin Hill - Omochao 9" +pumpkin_hill_omo_10 = "Pumpkin Hill - Omochao 10" +pumpkin_hill_omo_11 = "Pumpkin Hill - Omochao 11" pumpkin_hill_upgrade = "Pumpkin Hill - Upgrade" aquatic_mine_1 = "Aquatic Mine - 1" aquatic_mine_2 = "Aquatic Mine - 2" @@ -192,6 +286,13 @@ aquatic_mine_chao_3 = "Aquatic Mine - Chao Key 3" aquatic_mine_pipe_1 = "Aquatic Mine - Pipe 1" aquatic_mine_pipe_2 = "Aquatic Mine - Pipe 2" aquatic_mine_pipe_3 = "Aquatic Mine - Pipe 3" +aquatic_mine_omo_1 = "Aquatic Mine - Omochao 1" +aquatic_mine_omo_2 = "Aquatic Mine - Omochao 2" +aquatic_mine_omo_3 = "Aquatic Mine - Omochao 3" +aquatic_mine_omo_4 = "Aquatic Mine - Omochao 4" +aquatic_mine_omo_5 = "Aquatic Mine - Omochao 5" +aquatic_mine_omo_6 = "Aquatic Mine - Omochao 6" +aquatic_mine_omo_7 = "Aquatic Mine - Omochao 7" aquatic_mine_beetle = "Aquatic Mine - Gold Beetle" aquatic_mine_upgrade = "Aquatic Mine - Upgrade" death_chamber_1 = "Death Chamber - 1" @@ -207,6 +308,15 @@ death_chamber_pipe_2 = "Death Chamber - Pipe 2" death_chamber_pipe_3 = "Death Chamber - Pipe 3" death_chamber_hidden_1 = "Death Chamber - Hidden 1" death_chamber_hidden_2 = "Death Chamber - Hidden 2" +death_chamber_omo_1 = "Death Chamber - Omochao 1" +death_chamber_omo_2 = "Death Chamber - Omochao 2" +death_chamber_omo_3 = "Death Chamber - Omochao 3" +death_chamber_omo_4 = "Death Chamber - Omochao 4" +death_chamber_omo_5 = "Death Chamber - Omochao 5" +death_chamber_omo_6 = "Death Chamber - Omochao 6" +death_chamber_omo_7 = "Death Chamber - Omochao 7" +death_chamber_omo_8 = "Death Chamber - Omochao 8" +death_chamber_omo_9 = "Death Chamber - Omochao 9" death_chamber_beetle = "Death Chamber - Gold Beetle" death_chamber_upgrade = "Death Chamber - Upgrade" meteor_herd_1 = "Meteor Herd - 1" @@ -220,6 +330,9 @@ meteor_herd_chao_3 = "Meteor Herd - Chao Key 3" meteor_herd_pipe_1 = "Meteor Herd - Pipe 1" meteor_herd_pipe_2 = "Meteor Herd - Pipe 2" meteor_herd_pipe_3 = "Meteor Herd - Pipe 3" +meteor_herd_omo_1 = "Meteor Herd - Omochao 1" +meteor_herd_omo_2 = "Meteor Herd - Omochao 2" +meteor_herd_omo_3 = "Meteor Herd - Omochao 3" meteor_herd_beetle = "Meteor Herd - Gold Beetle" meteor_herd_upgrade = "Meteor Herd - Upgrade" @@ -239,6 +352,14 @@ radical_highway_pipe_3 = "Radical Highway - Pipe 3" radical_highway_hidden_1 = "Radical Highway - Hidden 1" radical_highway_hidden_2 = "Radical Highway - Hidden 2" radical_highway_hidden_3 = "Radical Highway - Hidden 3" +radical_highway_omo_1 = "Radical Highway - Omochao 1" +radical_highway_omo_2 = "Radical Highway - Omochao 2" +radical_highway_omo_3 = "Radical Highway - Omochao 3" +radical_highway_omo_4 = "Radical Highway - Omochao 4" +radical_highway_omo_5 = "Radical Highway - Omochao 5" +radical_highway_omo_6 = "Radical Highway - Omochao 6" +radical_highway_omo_7 = "Radical Highway - Omochao 7" +radical_highway_omo_8 = "Radical Highway - Omochao 8" radical_highway_beetle = "Radical Highway - Gold Beetle" radical_highway_upgrade = "Radical Highway - Upgrade" white_jungle_1 = "White Jungle - 1" @@ -256,6 +377,11 @@ white_jungle_pipe_4 = "White Jungle - Pipe 4" white_jungle_hidden_1 = "White Jungle - Hidden 1" white_jungle_hidden_2 = "White Jungle - Hidden 2" white_jungle_hidden_3 = "White Jungle - Hidden 3" +white_jungle_omo_1 = "White Jungle - Omochao 1" +white_jungle_omo_2 = "White Jungle - Omochao 2" +white_jungle_omo_3 = "White Jungle - Omochao 3" +white_jungle_omo_4 = "White Jungle - Omochao 4" +white_jungle_omo_5 = "White Jungle - Omochao 5" white_jungle_beetle = "White Jungle - Gold Beetle" white_jungle_upgrade = "White Jungle - Upgrade" sky_rail_1 = "Sky Rail - 1" @@ -285,6 +411,7 @@ final_chase_chao_3 = "Final Chase - Chao Key 3" final_chase_pipe_1 = "Final Chase - Pipe 1" final_chase_pipe_2 = "Final Chase - Pipe 2" final_chase_pipe_3 = "Final Chase - Pipe 3" +final_chase_omo_1 = "Final Chase - Omochao 1" final_chase_beetle = "Final Chase - Gold Beetle" final_chase_upgrade = "Final Chase - Upgrade" @@ -302,6 +429,12 @@ iron_gate_pipe_2 = "Iron Gate - Pipe 2" iron_gate_pipe_3 = "Iron Gate - Pipe 3" iron_gate_pipe_4 = "Iron Gate - Pipe 4" iron_gate_pipe_5 = "Iron Gate - Pipe 5" +iron_gate_omo_1 = "Iron Gate - Omochao 1" +iron_gate_omo_2 = "Iron Gate - Omochao 2" +iron_gate_omo_3 = "Iron Gate - Omochao 3" +iron_gate_omo_4 = "Iron Gate - Omochao 4" +iron_gate_omo_5 = "Iron Gate - Omochao 5" +iron_gate_omo_6 = "Iron Gate - Omochao 6" iron_gate_beetle = "Iron Gate - Gold Beetle" iron_gate_upgrade = "Iron Gate - Upgrade" sand_ocean_1 = "Sand Ocean - 1" @@ -317,6 +450,8 @@ sand_ocean_pipe_2 = "Sand Ocean - Pipe 2" sand_ocean_pipe_3 = "Sand Ocean - Pipe 3" sand_ocean_pipe_4 = "Sand Ocean - Pipe 4" sand_ocean_pipe_5 = "Sand Ocean - Pipe 5" +sand_ocean_omo_1 = "Sand Ocean - Omochao 1" +sand_ocean_omo_2 = "Sand Ocean - Omochao 2" sand_ocean_beetle = "Sand Ocean - Gold Beetle" sand_ocean_upgrade = "Sand Ocean - Upgrade" lost_colony_1 = "Lost Colony - 1" @@ -330,6 +465,14 @@ lost_colony_chao_3 = "Lost Colony - Chao Key 3" lost_colony_pipe_1 = "Lost Colony - Pipe 1" lost_colony_pipe_2 = "Lost Colony - Pipe 2" lost_colony_hidden_1 = "Lost Colony - Hidden 1" +lost_colony_omo_1 = "Lost Colony - Omochao 1" +lost_colony_omo_2 = "Lost Colony - Omochao 2" +lost_colony_omo_3 = "Lost Colony - Omochao 3" +lost_colony_omo_4 = "Lost Colony - Omochao 4" +lost_colony_omo_5 = "Lost Colony - Omochao 5" +lost_colony_omo_6 = "Lost Colony - Omochao 6" +lost_colony_omo_7 = "Lost Colony - Omochao 7" +lost_colony_omo_8 = "Lost Colony - Omochao 8" lost_colony_beetle = "Lost Colony - Gold Beetle" lost_colony_upgrade = "Lost Colony - Upgrade" weapons_bed_1 = "Weapons Bed - 1" @@ -345,6 +488,9 @@ weapons_bed_pipe_2 = "Weapons Bed - Pipe 2" weapons_bed_pipe_3 = "Weapons Bed - Pipe 3" weapons_bed_pipe_4 = "Weapons Bed - Pipe 4" weapons_bed_pipe_5 = "Weapons Bed - Pipe 5" +weapons_bed_omo_1 = "Weapons Bed - Omochao 1" +weapons_bed_omo_2 = "Weapons Bed - Omochao 2" +weapons_bed_omo_3 = "Weapons Bed - Omochao 3" weapons_bed_upgrade = "Weapons Bed - Upgrade" cosmic_wall_1 = "Cosmic Wall - 1" cosmic_wall_2 = "Cosmic Wall - 2" @@ -359,6 +505,7 @@ cosmic_wall_pipe_2 = "Cosmic Wall - Pipe 2" cosmic_wall_pipe_3 = "Cosmic Wall - Pipe 3" cosmic_wall_pipe_4 = "Cosmic Wall - Pipe 4" cosmic_wall_pipe_5 = "Cosmic Wall - Pipe 5" +cosmic_wall_omo_1 = "Cosmic Wall - Omochao 1" cosmic_wall_beetle = "Cosmic Wall - Gold Beetle" cosmic_wall_upgrade = "Cosmic Wall - Upgrade" @@ -373,6 +520,18 @@ dry_lagoon_chao_2 = "Dry Lagoon - Chao Key 2" dry_lagoon_chao_3 = "Dry Lagoon - Chao Key 3" dry_lagoon_pipe_1 = "Dry Lagoon - Pipe 1" dry_lagoon_hidden_1 = "Dry Lagoon - Hidden 1" +dry_lagoon_omo_1 = "Dry Lagoon - Omochao 1" +dry_lagoon_omo_2 = "Dry Lagoon - Omochao 2" +dry_lagoon_omo_3 = "Dry Lagoon - Omochao 3" +dry_lagoon_omo_4 = "Dry Lagoon - Omochao 4" +dry_lagoon_omo_5 = "Dry Lagoon - Omochao 5" +dry_lagoon_omo_6 = "Dry Lagoon - Omochao 6" +dry_lagoon_omo_7 = "Dry Lagoon - Omochao 7" +dry_lagoon_omo_8 = "Dry Lagoon - Omochao 8" +dry_lagoon_omo_9 = "Dry Lagoon - Omochao 9" +dry_lagoon_omo_10 = "Dry Lagoon - Omochao 10" +dry_lagoon_omo_11 = "Dry Lagoon - Omochao 11" +dry_lagoon_omo_12 = "Dry Lagoon - Omochao 12" dry_lagoon_beetle = "Dry Lagoon - Gold Beetle" dry_lagoon_upgrade = "Dry Lagoon - Upgrade" egg_quarters_1 = "Egg Quarters - 1" @@ -387,6 +546,13 @@ egg_quarters_pipe_1 = "Egg Quarters - Pipe 1" egg_quarters_pipe_2 = "Egg Quarters - Pipe 2" egg_quarters_hidden_1 = "Egg Quarters - Hidden 1" egg_quarters_hidden_2 = "Egg Quarters - Hidden 2" +egg_quarters_omo_1 = "Egg Quarters - Omochao 1" +egg_quarters_omo_2 = "Egg Quarters - Omochao 2" +egg_quarters_omo_3 = "Egg Quarters - Omochao 3" +egg_quarters_omo_4 = "Egg Quarters - Omochao 4" +egg_quarters_omo_5 = "Egg Quarters - Omochao 5" +egg_quarters_omo_6 = "Egg Quarters - Omochao 6" +egg_quarters_omo_7 = "Egg Quarters - Omochao 7" egg_quarters_beetle = "Egg Quarters - Gold Beetle" egg_quarters_upgrade = "Egg Quarters - Upgrade" security_hall_1 = "Security Hall - 1" @@ -399,6 +565,18 @@ security_hall_chao_2 = "Security Hall - Chao Key 2" security_hall_chao_3 = "Security Hall - Chao Key 3" security_hall_pipe_1 = "Security Hall - Pipe 1" security_hall_hidden_1 = "Security Hall - Hidden 1" +security_hall_omo_1 = "Security Hall - Omochao 1" +security_hall_omo_2 = "Security Hall - Omochao 2" +security_hall_omo_3 = "Security Hall - Omochao 3" +security_hall_omo_4 = "Security Hall - Omochao 4" +security_hall_omo_5 = "Security Hall - Omochao 5" +security_hall_omo_6 = "Security Hall - Omochao 6" +security_hall_omo_7 = "Security Hall - Omochao 7" +security_hall_omo_8 = "Security Hall - Omochao 8" +security_hall_omo_9 = "Security Hall - Omochao 9" +security_hall_omo_10 = "Security Hall - Omochao 10" +security_hall_omo_11 = "Security Hall - Omochao 11" +security_hall_omo_12 = "Security Hall - Omochao 12" security_hall_beetle = "Security Hall - Gold Beetle" security_hall_upgrade = "Security Hall - Upgrade" route_280_1 = "Route 280 - 1" @@ -418,6 +596,11 @@ mad_space_pipe_1 = "Mad Space - Pipe 1" mad_space_pipe_2 = "Mad Space - Pipe 2" mad_space_pipe_3 = "Mad Space - Pipe 3" mad_space_pipe_4 = "Mad Space - Pipe 4" +mad_space_omo_1 = "Mad Space - Omochao 1" +mad_space_omo_2 = "Mad Space - Omochao 2" +mad_space_omo_3 = "Mad Space - Omochao 3" +mad_space_omo_4 = "Mad Space - Omochao 4" +mad_space_omo_5 = "Mad Space - Omochao 5" mad_space_beetle = "Mad Space - Gold Beetle" mad_space_upgrade = "Mad Space - Upgrade" @@ -435,6 +618,15 @@ cannon_core_pipe_2 = "Cannon Core - Pipe 2" cannon_core_pipe_3 = "Cannon Core - Pipe 3" cannon_core_pipe_4 = "Cannon Core - Pipe 4" cannon_core_pipe_5 = "Cannon Core - Pipe 5" +cannon_core_omo_1 = "Cannon Core - Omochao 1" +cannon_core_omo_2 = "Cannon Core - Omochao 2" +cannon_core_omo_3 = "Cannon Core - Omochao 3" +cannon_core_omo_4 = "Cannon Core - Omochao 4" +cannon_core_omo_5 = "Cannon Core - Omochao 5" +cannon_core_omo_6 = "Cannon Core - Omochao 6" +cannon_core_omo_7 = "Cannon Core - Omochao 7" +cannon_core_omo_8 = "Cannon Core - Omochao 8" +cannon_core_omo_9 = "Cannon Core - Omochao 9" cannon_core_hidden_1 = "Cannon Core - Hidden 1" cannon_core_beetle = "Cannon Core - Gold Beetle" @@ -518,6 +710,30 @@ chao_standard_karate = "Chao Karate - Standard" chao_expert_karate = "Chao Karate - Expert" chao_super_karate = "Chao Karate - Super" +# Kart Race Definitions +kart_race_beginner_sonic = "Kart Race - Beginner - Sonic" +kart_race_beginner_tails = "Kart Race - Beginner - Tails" +kart_race_beginner_knuckles = "Kart Race - Beginner - Knuckles" +kart_race_beginner_shadow = "Kart Race - Beginner - Shadow" +kart_race_beginner_eggman = "Kart Race - Beginner - Eggman" +kart_race_beginner_rouge = "Kart Race - Beginner - Rouge" +kart_race_standard_sonic = "Kart Race - Standard - Sonic" +kart_race_standard_tails = "Kart Race - Standard - Tails" +kart_race_standard_knuckles = "Kart Race - Standard - Knuckles" +kart_race_standard_shadow = "Kart Race - Standard - Shadow" +kart_race_standard_eggman = "Kart Race - Standard - Eggman" +kart_race_standard_rouge = "Kart Race - Standard - Rouge" +kart_race_expert_sonic = "Kart Race - Expert - Sonic" +kart_race_expert_tails = "Kart Race - Expert - Tails" +kart_race_expert_knuckles = "Kart Race - Expert - Knuckles" +kart_race_expert_shadow = "Kart Race - Expert - Shadow" +kart_race_expert_eggman = "Kart Race - Expert - Eggman" +kart_race_expert_rouge = "Kart Race - Expert - Rouge" + +kart_race_beginner = "Kart Race - Beginner" +kart_race_standard = "Kart Race - Standard" +kart_race_expert = "Kart Race - Expert" + # Other Definitions green_hill = "Green Hill" green_hill_chao_1 = "Green Hill - Chao Key 1" @@ -582,6 +798,13 @@ biolizard_region = "Biolizard" green_hill_region = "Green Hill" +grand_prix = "Grand Prix" +grand_prix_region = "Grand Prix" + chao_garden_beginner_region = "Chao Garden - Beginner" chao_garden_intermediate_region = "Chao Garden - Intermediate" chao_garden_expert_region = "Chao Garden - Expert" + +kart_race_beginner_region = "Kart Race - Beginner" +kart_race_standard_region = "Kart Race - Intermediate" +kart_race_expert_region = "Kart Race - Expert" diff --git a/worlds/sa2b/Options.py b/worlds/sa2b/Options.py index 4724d30a5f..f4dfd833c4 100644 --- a/worlds/sa2b/Options.py +++ b/worlds/sa2b/Options.py @@ -9,11 +9,13 @@ class Goal(Choice): Biolizard: Finish Cannon's Core and defeat the Biolizard and Finalhazard Chaos Emerald Hunt: Find the Seven Chaos Emeralds and reach Green Hill Zone Finalhazard Chaos Emerald Hunt: Find the Seven Chaos Emeralds and reach Green Hill Zone, then defeat Finalhazard + Grand Prix: Win every race in Kart Race Mode (all standard levels are disabled) """ display_name = "Goal" option_biolizard = 0 option_chaos_emerald_hunt = 1 option_finalhazard_chaos_emerald_hunt = 2 + option_grand_prix = 3 default = 0 @@ -84,6 +86,24 @@ class DarknessTrapWeight(BaseTrapWeight): display_name = "Darkness Trap Weight" +class PongTrapWeight(BaseTrapWeight): + """ + Likelihood of receiving a trap which forces you to play a Pong minigame + """ + display_name = "Pong Trap Weight" + + +class MinigameTrapDifficulty(Choice): + """ + How difficult any Minigame-style traps are + """ + display_name = "Minigame Trap Difficulty" + option_easy = 0 + option_medium = 1 + option_hard = 2 + default = 1 + + class JunkFillPercentage(Range): """ Replace a percentage of non-required emblems in the item pool with random junk items @@ -150,6 +170,27 @@ class Beetlesanity(Toggle): display_name = "Beetlesanity" +class Omosanity(Toggle): + """ + Determines whether activating Omochao grants checks + """ + display_name = "Omosanity" + + +class KartRaceChecks(Choice): + """ + Determines whether Kart Race Mode grants checks + None: No Kart Races grant checks + Mini: Each Kart Race difficulty must be beaten only once + Full: Every Character must separately beat each Kart Race difficulty + """ + display_name = "Kart Race Checks" + option_none = 0 + option_mini = 1 + option_full = 2 + default = 0 + + class EmblemPercentageForCannonsCore(Range): """ Allows logic to gate the final mission behind a number of Emblems @@ -440,6 +481,27 @@ class CannonsCoreMission5(DefaultOnToggle): display_name = "Cannon's Core Mission 5" +class RingLoss(Choice): + """ + How taking damage is handled + Classic: You lose all of your rings when hit + Modern: You lose 20 rings when hit + OHKO: You die immediately when hit (NOTE: Some Hard Logic tricks may require damage boosts!) + """ + display_name = "Ring Loss" + option_classic = 0 + option_modern = 1 + option_ohko = 2 + default = 0 + + @classmethod + def get_option_name(cls, value) -> str: + if cls.auto_display_name and value == 2: + return cls.name_lookup[value].upper() + else: + return cls.name_lookup[value] + + class SADXMusic(Choice): """ Whether the randomizer will include Sonic Adventure DX Music in the music pool @@ -515,6 +577,8 @@ sa2b_options: typing.Dict[str, type(Option)] = { "keysanity": Keysanity, "whistlesanity": Whistlesanity, "beetlesanity": Beetlesanity, + "omosanity": Omosanity, + "kart_race_checks": KartRaceChecks, "required_rank": RequiredRank, "emblem_percentage_for_cannons_core": EmblemPercentageForCannonsCore, "required_cannons_core_missions": RequiredCannonsCoreMissions, @@ -533,6 +597,9 @@ sa2b_options: typing.Dict[str, type(Option)] = { "gravity_trap_weight": GravityTrapWeight, "exposition_trap_weight": ExpositionTrapWeight, #"darkness_trap_weight": DarknessTrapWeight, + "pong_trap_weight": PongTrapWeight, + "minigame_trap_difficulty": MinigameTrapDifficulty, + "ring_loss": RingLoss, "sadx_music": SADXMusic, "music_shuffle": MusicShuffle, "narrator": Narrator, diff --git a/worlds/sa2b/Regions.py b/worlds/sa2b/Regions.py index a5e75b4073..b18fa9ec78 100644 --- a/worlds/sa2b/Regions.py +++ b/worlds/sa2b/Regions.py @@ -134,6 +134,20 @@ def create_regions(world, player: int, active_locations): LocationName.city_escape_hidden_3, LocationName.city_escape_hidden_4, LocationName.city_escape_hidden_5, + LocationName.city_escape_omo_1, + LocationName.city_escape_omo_2, + LocationName.city_escape_omo_3, + LocationName.city_escape_omo_4, + LocationName.city_escape_omo_5, + LocationName.city_escape_omo_6, + LocationName.city_escape_omo_7, + LocationName.city_escape_omo_8, + LocationName.city_escape_omo_9, + LocationName.city_escape_omo_10, + LocationName.city_escape_omo_11, + LocationName.city_escape_omo_12, + LocationName.city_escape_omo_13, + LocationName.city_escape_omo_14, LocationName.city_escape_beetle, LocationName.city_escape_upgrade, ] @@ -150,6 +164,11 @@ def create_regions(world, player: int, active_locations): LocationName.metal_harbor_chao_2, LocationName.metal_harbor_chao_3, LocationName.metal_harbor_pipe_1, + LocationName.metal_harbor_omo_1, + LocationName.metal_harbor_omo_2, + LocationName.metal_harbor_omo_3, + LocationName.metal_harbor_omo_4, + LocationName.metal_harbor_omo_5, LocationName.metal_harbor_beetle, LocationName.metal_harbor_upgrade, ] @@ -190,6 +209,10 @@ def create_regions(world, player: int, active_locations): LocationName.pyramid_cave_pipe_2, LocationName.pyramid_cave_pipe_3, LocationName.pyramid_cave_pipe_4, + LocationName.pyramid_cave_omo_1, + LocationName.pyramid_cave_omo_2, + LocationName.pyramid_cave_omo_3, + LocationName.pyramid_cave_omo_4, LocationName.pyramid_cave_beetle, LocationName.pyramid_cave_upgrade, ] @@ -210,6 +233,19 @@ def create_regions(world, player: int, active_locations): LocationName.crazy_gadget_pipe_3, LocationName.crazy_gadget_pipe_4, LocationName.crazy_gadget_hidden_1, + LocationName.crazy_gadget_omo_1, + LocationName.crazy_gadget_omo_2, + LocationName.crazy_gadget_omo_3, + LocationName.crazy_gadget_omo_4, + LocationName.crazy_gadget_omo_5, + LocationName.crazy_gadget_omo_6, + LocationName.crazy_gadget_omo_7, + LocationName.crazy_gadget_omo_8, + LocationName.crazy_gadget_omo_9, + LocationName.crazy_gadget_omo_10, + LocationName.crazy_gadget_omo_11, + LocationName.crazy_gadget_omo_12, + LocationName.crazy_gadget_omo_13, LocationName.crazy_gadget_beetle, LocationName.crazy_gadget_upgrade, ] @@ -227,6 +263,9 @@ def create_regions(world, player: int, active_locations): LocationName.final_rush_chao_3, LocationName.final_rush_pipe_1, LocationName.final_rush_pipe_2, + LocationName.final_rush_omo_1, + LocationName.final_rush_omo_2, + LocationName.final_rush_omo_3, LocationName.final_rush_beetle, LocationName.final_rush_upgrade, ] @@ -248,6 +287,16 @@ def create_regions(world, player: int, active_locations): LocationName.prison_lane_hidden_1, LocationName.prison_lane_hidden_2, LocationName.prison_lane_hidden_3, + LocationName.prison_lane_omo_1, + LocationName.prison_lane_omo_2, + LocationName.prison_lane_omo_3, + LocationName.prison_lane_omo_4, + LocationName.prison_lane_omo_5, + LocationName.prison_lane_omo_6, + LocationName.prison_lane_omo_7, + LocationName.prison_lane_omo_8, + LocationName.prison_lane_omo_9, + LocationName.prison_lane_omo_10, LocationName.prison_lane_beetle, LocationName.prison_lane_upgrade, ] @@ -270,6 +319,14 @@ def create_regions(world, player: int, active_locations): LocationName.mission_street_hidden_2, LocationName.mission_street_hidden_3, LocationName.mission_street_hidden_4, + LocationName.mission_street_omo_1, + LocationName.mission_street_omo_2, + LocationName.mission_street_omo_3, + LocationName.mission_street_omo_4, + LocationName.mission_street_omo_5, + LocationName.mission_street_omo_6, + LocationName.mission_street_omo_7, + LocationName.mission_street_omo_8, LocationName.mission_street_beetle, LocationName.mission_street_upgrade, ] @@ -299,6 +356,10 @@ def create_regions(world, player: int, active_locations): LocationName.hidden_base_pipe_3, LocationName.hidden_base_pipe_4, LocationName.hidden_base_pipe_5, + LocationName.hidden_base_omo_1, + LocationName.hidden_base_omo_2, + LocationName.hidden_base_omo_3, + LocationName.hidden_base_omo_4, LocationName.hidden_base_beetle, LocationName.hidden_base_upgrade, ] @@ -319,6 +380,18 @@ def create_regions(world, player: int, active_locations): LocationName.eternal_engine_pipe_3, LocationName.eternal_engine_pipe_4, LocationName.eternal_engine_pipe_5, + LocationName.eternal_engine_omo_1, + LocationName.eternal_engine_omo_2, + LocationName.eternal_engine_omo_3, + LocationName.eternal_engine_omo_4, + LocationName.eternal_engine_omo_5, + LocationName.eternal_engine_omo_6, + LocationName.eternal_engine_omo_7, + LocationName.eternal_engine_omo_8, + LocationName.eternal_engine_omo_9, + LocationName.eternal_engine_omo_10, + LocationName.eternal_engine_omo_11, + LocationName.eternal_engine_omo_12, LocationName.eternal_engine_beetle, LocationName.eternal_engine_upgrade, ] @@ -337,6 +410,16 @@ def create_regions(world, player: int, active_locations): LocationName.wild_canyon_pipe_1, LocationName.wild_canyon_pipe_2, LocationName.wild_canyon_pipe_3, + LocationName.wild_canyon_omo_1, + LocationName.wild_canyon_omo_2, + LocationName.wild_canyon_omo_3, + LocationName.wild_canyon_omo_4, + LocationName.wild_canyon_omo_5, + LocationName.wild_canyon_omo_6, + LocationName.wild_canyon_omo_7, + LocationName.wild_canyon_omo_8, + LocationName.wild_canyon_omo_9, + LocationName.wild_canyon_omo_10, LocationName.wild_canyon_beetle, LocationName.wild_canyon_upgrade, ] @@ -354,6 +437,17 @@ def create_regions(world, player: int, active_locations): LocationName.pumpkin_hill_chao_3, LocationName.pumpkin_hill_pipe_1, LocationName.pumpkin_hill_hidden_1, + LocationName.pumpkin_hill_omo_1, + LocationName.pumpkin_hill_omo_2, + LocationName.pumpkin_hill_omo_3, + LocationName.pumpkin_hill_omo_4, + LocationName.pumpkin_hill_omo_5, + LocationName.pumpkin_hill_omo_6, + LocationName.pumpkin_hill_omo_7, + LocationName.pumpkin_hill_omo_8, + LocationName.pumpkin_hill_omo_9, + LocationName.pumpkin_hill_omo_10, + LocationName.pumpkin_hill_omo_11, LocationName.pumpkin_hill_upgrade, ] pumpkin_hill_region = create_region(world, player, active_locations, LocationName.pumpkin_hill_region, @@ -371,6 +465,13 @@ def create_regions(world, player: int, active_locations): LocationName.aquatic_mine_pipe_1, LocationName.aquatic_mine_pipe_2, LocationName.aquatic_mine_pipe_3, + LocationName.aquatic_mine_omo_1, + LocationName.aquatic_mine_omo_2, + LocationName.aquatic_mine_omo_3, + LocationName.aquatic_mine_omo_4, + LocationName.aquatic_mine_omo_5, + LocationName.aquatic_mine_omo_6, + LocationName.aquatic_mine_omo_7, LocationName.aquatic_mine_beetle, LocationName.aquatic_mine_upgrade, ] @@ -391,6 +492,15 @@ def create_regions(world, player: int, active_locations): LocationName.death_chamber_pipe_3, LocationName.death_chamber_hidden_1, LocationName.death_chamber_hidden_2, + LocationName.death_chamber_omo_1, + LocationName.death_chamber_omo_2, + LocationName.death_chamber_omo_3, + LocationName.death_chamber_omo_4, + LocationName.death_chamber_omo_5, + LocationName.death_chamber_omo_6, + LocationName.death_chamber_omo_7, + LocationName.death_chamber_omo_8, + LocationName.death_chamber_omo_9, LocationName.death_chamber_beetle, LocationName.death_chamber_upgrade, ] @@ -409,6 +519,9 @@ def create_regions(world, player: int, active_locations): LocationName.meteor_herd_pipe_1, LocationName.meteor_herd_pipe_2, LocationName.meteor_herd_pipe_3, + LocationName.meteor_herd_omo_1, + LocationName.meteor_herd_omo_2, + LocationName.meteor_herd_omo_3, LocationName.meteor_herd_beetle, LocationName.meteor_herd_upgrade, ] @@ -430,6 +543,14 @@ def create_regions(world, player: int, active_locations): LocationName.radical_highway_hidden_1, LocationName.radical_highway_hidden_2, LocationName.radical_highway_hidden_3, + LocationName.radical_highway_omo_1, + LocationName.radical_highway_omo_2, + LocationName.radical_highway_omo_3, + LocationName.radical_highway_omo_4, + LocationName.radical_highway_omo_5, + LocationName.radical_highway_omo_6, + LocationName.radical_highway_omo_7, + LocationName.radical_highway_omo_8, LocationName.radical_highway_beetle, LocationName.radical_highway_upgrade, ] @@ -452,6 +573,11 @@ def create_regions(world, player: int, active_locations): LocationName.white_jungle_hidden_1, LocationName.white_jungle_hidden_2, LocationName.white_jungle_hidden_3, + LocationName.white_jungle_omo_1, + LocationName.white_jungle_omo_2, + LocationName.white_jungle_omo_3, + LocationName.white_jungle_omo_4, + LocationName.white_jungle_omo_5, LocationName.white_jungle_beetle, LocationName.white_jungle_upgrade, ] @@ -491,6 +617,7 @@ def create_regions(world, player: int, active_locations): LocationName.final_chase_pipe_1, LocationName.final_chase_pipe_2, LocationName.final_chase_pipe_3, + LocationName.final_chase_omo_1, LocationName.final_chase_beetle, LocationName.final_chase_upgrade, ] @@ -511,6 +638,12 @@ def create_regions(world, player: int, active_locations): LocationName.iron_gate_pipe_3, LocationName.iron_gate_pipe_4, LocationName.iron_gate_pipe_5, + LocationName.iron_gate_omo_1, + LocationName.iron_gate_omo_2, + LocationName.iron_gate_omo_3, + LocationName.iron_gate_omo_4, + LocationName.iron_gate_omo_5, + LocationName.iron_gate_omo_6, LocationName.iron_gate_beetle, LocationName.iron_gate_upgrade, ] @@ -531,6 +664,8 @@ def create_regions(world, player: int, active_locations): LocationName.sand_ocean_pipe_3, LocationName.sand_ocean_pipe_4, LocationName.sand_ocean_pipe_5, + LocationName.sand_ocean_omo_1, + LocationName.sand_ocean_omo_2, LocationName.sand_ocean_beetle, LocationName.sand_ocean_upgrade, ] @@ -549,6 +684,14 @@ def create_regions(world, player: int, active_locations): LocationName.lost_colony_pipe_1, LocationName.lost_colony_pipe_2, LocationName.lost_colony_hidden_1, + LocationName.lost_colony_omo_1, + LocationName.lost_colony_omo_2, + LocationName.lost_colony_omo_3, + LocationName.lost_colony_omo_4, + LocationName.lost_colony_omo_5, + LocationName.lost_colony_omo_6, + LocationName.lost_colony_omo_7, + LocationName.lost_colony_omo_8, LocationName.lost_colony_beetle, LocationName.lost_colony_upgrade, ] @@ -569,6 +712,9 @@ def create_regions(world, player: int, active_locations): LocationName.weapons_bed_pipe_3, LocationName.weapons_bed_pipe_4, LocationName.weapons_bed_pipe_5, + LocationName.weapons_bed_omo_1, + LocationName.weapons_bed_omo_2, + LocationName.weapons_bed_omo_3, LocationName.weapons_bed_upgrade, ] weapons_bed_region = create_region(world, player, active_locations, LocationName.weapons_bed_region, @@ -588,6 +734,7 @@ def create_regions(world, player: int, active_locations): LocationName.cosmic_wall_pipe_3, LocationName.cosmic_wall_pipe_4, LocationName.cosmic_wall_pipe_5, + LocationName.cosmic_wall_omo_1, LocationName.cosmic_wall_beetle, LocationName.cosmic_wall_upgrade, ] @@ -605,6 +752,18 @@ def create_regions(world, player: int, active_locations): LocationName.dry_lagoon_chao_3, LocationName.dry_lagoon_pipe_1, LocationName.dry_lagoon_hidden_1, + LocationName.dry_lagoon_omo_1, + LocationName.dry_lagoon_omo_2, + LocationName.dry_lagoon_omo_3, + LocationName.dry_lagoon_omo_4, + LocationName.dry_lagoon_omo_5, + LocationName.dry_lagoon_omo_6, + LocationName.dry_lagoon_omo_7, + LocationName.dry_lagoon_omo_8, + LocationName.dry_lagoon_omo_9, + LocationName.dry_lagoon_omo_10, + LocationName.dry_lagoon_omo_11, + LocationName.dry_lagoon_omo_12, LocationName.dry_lagoon_beetle, LocationName.dry_lagoon_upgrade, ] @@ -624,6 +783,13 @@ def create_regions(world, player: int, active_locations): LocationName.egg_quarters_pipe_2, LocationName.egg_quarters_hidden_1, LocationName.egg_quarters_hidden_2, + LocationName.egg_quarters_omo_1, + LocationName.egg_quarters_omo_2, + LocationName.egg_quarters_omo_3, + LocationName.egg_quarters_omo_4, + LocationName.egg_quarters_omo_5, + LocationName.egg_quarters_omo_6, + LocationName.egg_quarters_omo_7, LocationName.egg_quarters_beetle, LocationName.egg_quarters_upgrade, ] @@ -641,6 +807,18 @@ def create_regions(world, player: int, active_locations): LocationName.security_hall_chao_3, LocationName.security_hall_pipe_1, LocationName.security_hall_hidden_1, + LocationName.security_hall_omo_1, + LocationName.security_hall_omo_2, + LocationName.security_hall_omo_3, + LocationName.security_hall_omo_4, + LocationName.security_hall_omo_5, + LocationName.security_hall_omo_6, + LocationName.security_hall_omo_7, + LocationName.security_hall_omo_8, + LocationName.security_hall_omo_9, + LocationName.security_hall_omo_10, + LocationName.security_hall_omo_11, + LocationName.security_hall_omo_12, LocationName.security_hall_beetle, LocationName.security_hall_upgrade, ] @@ -670,6 +848,11 @@ def create_regions(world, player: int, active_locations): LocationName.mad_space_pipe_2, LocationName.mad_space_pipe_3, LocationName.mad_space_pipe_4, + LocationName.mad_space_omo_1, + LocationName.mad_space_omo_2, + LocationName.mad_space_omo_3, + LocationName.mad_space_omo_4, + LocationName.mad_space_omo_5, LocationName.mad_space_beetle, LocationName.mad_space_upgrade, ] @@ -691,6 +874,15 @@ def create_regions(world, player: int, active_locations): LocationName.cannon_core_pipe_4, LocationName.cannon_core_pipe_5, LocationName.cannon_core_hidden_1, + LocationName.cannon_core_omo_1, + LocationName.cannon_core_omo_2, + LocationName.cannon_core_omo_3, + LocationName.cannon_core_omo_4, + LocationName.cannon_core_omo_5, + LocationName.cannon_core_omo_6, + LocationName.cannon_core_omo_7, + LocationName.cannon_core_omo_8, + LocationName.cannon_core_omo_9, LocationName.cannon_core_beetle, ] cannon_core_region = create_region(world, player, active_locations, LocationName.cannon_core_region, @@ -782,6 +974,59 @@ def create_regions(world, player: int, active_locations): chao_garden_expert_region = create_region(world, player, active_locations, LocationName.chao_garden_expert_region, chao_garden_expert_region_locations) + kart_race_beginner_region_locations = [] + if world.kart_race_checks[player] == 2: + kart_race_beginner_region_locations.extend([ + LocationName.kart_race_beginner_sonic, + LocationName.kart_race_beginner_tails, + LocationName.kart_race_beginner_knuckles, + LocationName.kart_race_beginner_shadow, + LocationName.kart_race_beginner_eggman, + LocationName.kart_race_beginner_rouge, + ]) + if world.kart_race_checks[player] == 1: + kart_race_beginner_region_locations.append(LocationName.kart_race_beginner) + kart_race_beginner_region = create_region(world, player, active_locations, LocationName.kart_race_beginner_region, + kart_race_beginner_region_locations) + + kart_race_standard_region_locations = [] + if world.kart_race_checks[player] == 2: + kart_race_standard_region_locations.extend([ + LocationName.kart_race_standard_sonic, + LocationName.kart_race_standard_tails, + LocationName.kart_race_standard_knuckles, + LocationName.kart_race_standard_shadow, + LocationName.kart_race_standard_eggman, + LocationName.kart_race_standard_rouge, + ]) + if world.kart_race_checks[player] == 1: + kart_race_standard_region_locations.append(LocationName.kart_race_standard) + kart_race_standard_region = create_region(world, player, active_locations, LocationName.kart_race_standard_region, + kart_race_standard_region_locations) + + kart_race_expert_region_locations = [] + if world.kart_race_checks[player] == 2: + kart_race_expert_region_locations.extend([ + LocationName.kart_race_expert_sonic, + LocationName.kart_race_expert_tails, + LocationName.kart_race_expert_knuckles, + LocationName.kart_race_expert_shadow, + LocationName.kart_race_expert_eggman, + LocationName.kart_race_expert_rouge, + ]) + if world.kart_race_checks[player] == 1: + kart_race_expert_region_locations.append(LocationName.kart_race_expert) + kart_race_expert_region = create_region(world, player, active_locations, LocationName.kart_race_expert_region, + kart_race_expert_region_locations) + + if world.goal[player] == 3: + grand_prix_region_locations = [ + LocationName.grand_prix, + ] + grand_prix_region = create_region(world, player, active_locations, LocationName.grand_prix_region, + grand_prix_region_locations) + world.regions += [grand_prix_region] + if world.goal[player] == 0 or world.goal[player] == 2: biolizard_region_locations = [ LocationName.finalhazard, @@ -838,6 +1083,9 @@ def create_regions(world, player: int, active_locations): chao_garden_beginner_region, chao_garden_intermediate_region, chao_garden_expert_region, + kart_race_beginner_region, + kart_race_standard_region, + kart_race_expert_region, ] @@ -867,6 +1115,8 @@ def connect_regions(world, player, gates: typing.List[LevelGate], cannon_core_em state.has(ItemName.blue_emerald, player))) if world.goal[player] == 2: connect(world, player, names, LocationName.green_hill_region, LocationName.biolizard_region) + elif world.goal[player] == 3: + connect(world, player, names, LocationName.kart_race_expert_region, LocationName.grand_prix_region) for i in range(len(gates[0].gate_levels)): connect(world, player, names, LocationName.gate_0_region, shuffleable_regions[gates[0].gate_levels[i]]) @@ -941,27 +1191,51 @@ def connect_regions(world, player, gates: typing.List[LevelGate], cannon_core_em connect(world, player, names, LocationName.gate_0_region, LocationName.chao_garden_beginner_region) connect(world, player, names, LocationName.gate_0_region, LocationName.chao_garden_intermediate_region) connect(world, player, names, LocationName.gate_0_region, LocationName.chao_garden_expert_region) + + connect(world, player, names, LocationName.gate_0_region, LocationName.kart_race_beginner_region) + connect(world, player, names, LocationName.gate_0_region, LocationName.kart_race_standard_region) + connect(world, player, names, LocationName.gate_0_region, LocationName.kart_race_expert_region) elif gates_len == 2: connect(world, player, names, LocationName.gate_0_region, LocationName.chao_garden_beginner_region) connect(world, player, names, LocationName.gate_0_region, LocationName.chao_garden_intermediate_region) connect(world, player, names, LocationName.gate_1_region, LocationName.chao_garden_expert_region) + + connect(world, player, names, LocationName.gate_0_region, LocationName.kart_race_beginner_region) + connect(world, player, names, LocationName.gate_0_region, LocationName.kart_race_standard_region) + connect(world, player, names, LocationName.gate_1_region, LocationName.kart_race_expert_region) elif gates_len == 3: connect(world, player, names, LocationName.gate_0_region, LocationName.chao_garden_beginner_region) connect(world, player, names, LocationName.gate_1_region, LocationName.chao_garden_intermediate_region) connect(world, player, names, LocationName.gate_2_region, LocationName.chao_garden_expert_region) + + connect(world, player, names, LocationName.gate_0_region, LocationName.kart_race_beginner_region) + connect(world, player, names, LocationName.gate_1_region, LocationName.kart_race_standard_region) + connect(world, player, names, LocationName.gate_2_region, LocationName.kart_race_expert_region) elif gates_len == 4: connect(world, player, names, LocationName.gate_0_region, LocationName.chao_garden_beginner_region) connect(world, player, names, LocationName.gate_1_region, LocationName.chao_garden_intermediate_region) connect(world, player, names, LocationName.gate_3_region, LocationName.chao_garden_expert_region) + + connect(world, player, names, LocationName.gate_0_region, LocationName.kart_race_beginner_region) + connect(world, player, names, LocationName.gate_1_region, LocationName.kart_race_standard_region) + connect(world, player, names, LocationName.gate_3_region, LocationName.kart_race_expert_region) elif gates_len == 5: connect(world, player, names, LocationName.gate_1_region, LocationName.chao_garden_beginner_region) connect(world, player, names, LocationName.gate_2_region, LocationName.chao_garden_intermediate_region) connect(world, player, names, LocationName.gate_3_region, LocationName.chao_garden_expert_region) + + connect(world, player, names, LocationName.gate_1_region, LocationName.kart_race_beginner_region) + connect(world, player, names, LocationName.gate_2_region, LocationName.kart_race_standard_region) + connect(world, player, names, LocationName.gate_3_region, LocationName.kart_race_expert_region) elif gates_len >= 6: connect(world, player, names, LocationName.gate_1_region, LocationName.chao_garden_beginner_region) connect(world, player, names, LocationName.gate_2_region, LocationName.chao_garden_intermediate_region) connect(world, player, names, LocationName.gate_4_region, LocationName.chao_garden_expert_region) + connect(world, player, names, LocationName.gate_1_region, LocationName.kart_race_beginner_region) + connect(world, player, names, LocationName.gate_2_region, LocationName.kart_race_standard_region) + connect(world, player, names, LocationName.gate_4_region, LocationName.kart_race_expert_region) + def create_region(world: MultiWorld, player: int, active_locations, name: str, locations=None): ret = Region(name, player, world) diff --git a/worlds/sa2b/Rules.py b/worlds/sa2b/Rules.py index a0590ba29c..ba3c8862c6 100644 --- a/worlds/sa2b/Rules.py +++ b/worlds/sa2b/Rules.py @@ -3,8 +3,8 @@ import typing from BaseClasses import MultiWorld from .Names import LocationName, ItemName from .Locations import boss_gate_set -from ..AutoWorld import LogicMixin -from ..generic.Rules import add_rule, set_rule, CollectionRule +from worlds.AutoWorld import LogicMixin +from worlds.generic.Rules import add_rule, set_rule, CollectionRule from .GateBosses import boss_has_requirement from .Missions import stage_name_prefixes, mission_orders @@ -619,6 +619,174 @@ def set_mission_upgrade_rules_standard(world: MultiWorld, player: int): lambda state: state.has(ItemName.tails_booster, player) and state.has(ItemName.eggman_jet_engine, player)) + # Omochao Upgrade Requirements + if world.omosanity[player]: + add_rule(world.get_location(LocationName.eternal_engine_omo_1, player), + lambda state: state.has(ItemName.tails_booster, player)) + + add_rule(world.get_location(LocationName.hidden_base_omo_2, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.pyramid_cave_omo_2, player), + lambda state: state.has(ItemName.sonic_bounce_bracelet, player)) + add_rule(world.get_location(LocationName.death_chamber_omo_2, player), + lambda state: state.has(ItemName.knuckles_hammer_gloves, player)) + add_rule(world.get_location(LocationName.eternal_engine_omo_2, player), + lambda state: state.has(ItemName.tails_booster, player)) + + add_rule(world.get_location(LocationName.radical_highway_omo_2, player), + lambda state: state.has(ItemName.shadow_air_shoes, player)) + add_rule(world.get_location(LocationName.weapons_bed_omo_2, player), + lambda state: state.has(ItemName.eggman_jet_engine, player)) + + add_rule(world.get_location(LocationName.mission_street_omo_3, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.hidden_base_omo_3, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.pyramid_cave_omo_3, player), + lambda state: state.has(ItemName.sonic_bounce_bracelet, player)) + add_rule(world.get_location(LocationName.eternal_engine_omo_3, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.final_rush_omo_3, player), + lambda state: state.has(ItemName.sonic_bounce_bracelet, player)) + + add_rule(world.get_location(LocationName.weapons_bed_omo_3, player), + lambda state: state.has(ItemName.eggman_jet_engine, player) and + state.has(ItemName.eggman_large_cannon, player)) + + add_rule(world.get_location(LocationName.metal_harbor_omo_4, player), + lambda state: state.has(ItemName.sonic_light_shoes, player)) + add_rule(world.get_location(LocationName.mission_street_omo_4, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.hidden_base_omo_4, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.pyramid_cave_omo_4, player), + lambda state: state.has(ItemName.sonic_bounce_bracelet, player)) + add_rule(world.get_location(LocationName.death_chamber_omo_4, player), + lambda state: state.has(ItemName.knuckles_hammer_gloves, player)) + add_rule(world.get_location(LocationName.eternal_engine_omo_4, player), + lambda state: state.has(ItemName.tails_booster, player)) + + add_rule(world.get_location(LocationName.mad_space_omo_4, player), + lambda state: state.has(ItemName.rouge_iron_boots, player)) + + add_rule(world.get_location(LocationName.cannon_core_omo_4, player), + lambda state: state.has(ItemName.tails_booster, player)) + + add_rule(world.get_location(LocationName.metal_harbor_omo_5, player), + lambda state: state.has(ItemName.sonic_light_shoes, player)) + add_rule(world.get_location(LocationName.mission_street_omo_5, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.death_chamber_omo_5, player), + lambda state: state.has(ItemName.knuckles_hammer_gloves, player)) + add_rule(world.get_location(LocationName.eternal_engine_omo_5, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.crazy_gadget_omo_5, player), + lambda state: state.has(ItemName.sonic_bounce_bracelet, player)) + + add_rule(world.get_location(LocationName.white_jungle_omo_5, player), + lambda state: state.has(ItemName.shadow_air_shoes, player)) + add_rule(world.get_location(LocationName.mad_space_omo_5, player), + lambda state: state.has(ItemName.rouge_iron_boots, player)) + + add_rule(world.get_location(LocationName.cannon_core_omo_5, player), + lambda state: state.has(ItemName.tails_booster, player) and + state.has(ItemName.eggman_jet_engine, player)) + + add_rule(world.get_location(LocationName.mission_street_omo_6, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.death_chamber_omo_6, player), + lambda state: state.has(ItemName.knuckles_hammer_gloves, player)) + add_rule(world.get_location(LocationName.eternal_engine_omo_6, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.crazy_gadget_omo_6, player), + lambda state: state.has(ItemName.sonic_bounce_bracelet, player)) + + add_rule(world.get_location(LocationName.lost_colony_omo_6, player), + lambda state: state.has(ItemName.eggman_jet_engine, player)) + + add_rule(world.get_location(LocationName.cannon_core_omo_6, player), + lambda state: state.has(ItemName.tails_booster, player) and + state.has(ItemName.eggman_jet_engine, player)) + + add_rule(world.get_location(LocationName.mission_street_omo_7, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.death_chamber_omo_7, player), + lambda state: state.has(ItemName.knuckles_shovel_claws, player) and + state.has(ItemName.knuckles_hammer_gloves, player)) + add_rule(world.get_location(LocationName.eternal_engine_omo_7, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.crazy_gadget_omo_7, player), + lambda state: state.has(ItemName.sonic_bounce_bracelet, player)) + + add_rule(world.get_location(LocationName.lost_colony_omo_7, player), + lambda state: state.has(ItemName.eggman_jet_engine, player)) + + add_rule(world.get_location(LocationName.cannon_core_omo_7, player), + lambda state: state.has(ItemName.tails_booster, player) and + state.has(ItemName.eggman_jet_engine, player) and + state.has(ItemName.knuckles_hammer_gloves, player) and + state.has(ItemName.knuckles_air_necklace, player)) + + add_rule(world.get_location(LocationName.mission_street_omo_8, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.death_chamber_omo_8, player), + lambda state: state.has(ItemName.knuckles_hammer_gloves, player)) + add_rule(world.get_location(LocationName.eternal_engine_omo_8, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.crazy_gadget_omo_8, player), + lambda state: state.has(ItemName.sonic_bounce_bracelet, player)) + + add_rule(world.get_location(LocationName.lost_colony_omo_8, player), + lambda state: state.has(ItemName.eggman_jet_engine, player)) + add_rule(world.get_location(LocationName.security_hall_omo_8, player), + lambda state: state.has(ItemName.rouge_mystic_melody, player) and + state.has(ItemName.rouge_iron_boots, player)) + + add_rule(world.get_location(LocationName.cannon_core_omo_8, player), + lambda state: state.has(ItemName.tails_booster, player) and + state.has(ItemName.eggman_jet_engine, player) and + state.has(ItemName.knuckles_hammer_gloves, player) and + state.has(ItemName.knuckles_air_necklace, player)) + + add_rule(world.get_location(LocationName.death_chamber_omo_9, player), + lambda state: state.has(ItemName.knuckles_mystic_melody, player) and + state.has(ItemName.knuckles_hammer_gloves, player)) + add_rule(world.get_location(LocationName.eternal_engine_omo_9, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.crazy_gadget_omo_9, player), + lambda state: state.has(ItemName.sonic_bounce_bracelet, player)) + + add_rule(world.get_location(LocationName.cannon_core_omo_9, player), + lambda state: state.has(ItemName.tails_booster, player) and + state.has(ItemName.eggman_jet_engine, player) and + state.has(ItemName.knuckles_hammer_gloves, player) and + state.has(ItemName.knuckles_air_necklace, player)) + + add_rule(world.get_location(LocationName.eternal_engine_omo_10, player), + lambda state: state.has(ItemName.tails_booster, player) and + state.has(ItemName.tails_bazooka, player)) + add_rule(world.get_location(LocationName.crazy_gadget_omo_10, player), + lambda state: state.has(ItemName.sonic_bounce_bracelet, player)) + + add_rule(world.get_location(LocationName.eternal_engine_omo_11, player), + lambda state: state.has(ItemName.tails_booster, player) and + state.has(ItemName.tails_bazooka, player)) + add_rule(world.get_location(LocationName.crazy_gadget_omo_11, player), + lambda state: state.has(ItemName.sonic_bounce_bracelet, player)) + + add_rule(world.get_location(LocationName.eternal_engine_omo_12, player), + lambda state: state.has(ItemName.tails_booster, player) and + state.has(ItemName.tails_bazooka, player)) + add_rule(world.get_location(LocationName.crazy_gadget_omo_12, player), + lambda state: state.has(ItemName.sonic_light_shoes, player) and + state.has(ItemName.sonic_bounce_bracelet, player) and + state.has(ItemName.sonic_flame_ring, player)) + + add_rule(world.get_location(LocationName.crazy_gadget_omo_13, player), + lambda state: state.has(ItemName.sonic_light_shoes, player) and + state.has(ItemName.sonic_bounce_bracelet, player) and + state.has(ItemName.sonic_flame_ring, player)) + # Gold Beetle Upgrade Requirements if world.beetlesanity[player]: add_rule(world.get_location(LocationName.mission_street_beetle, player), @@ -715,9 +883,6 @@ def set_mission_upgrade_rules_hard(world: MultiWorld, player: int): lambda state: state.has(ItemName.tails_booster, player)) # Mission 3 Upgrade Requirements - add_rule_safe(world, LocationName.city_escape_3, player, - lambda state: state.has(ItemName.sonic_bounce_bracelet, player) or - state.has(ItemName.sonic_mystic_melody, player)) add_rule_safe(world, LocationName.wild_canyon_3, player, lambda state: state.has(ItemName.knuckles_shovel_claws, player)) add_rule_safe(world, LocationName.prison_lane_3, player, @@ -752,9 +917,7 @@ def set_mission_upgrade_rules_hard(world: MultiWorld, player: int): add_rule_safe(world, LocationName.sand_ocean_3, player, lambda state: state.has(ItemName.eggman_jet_engine, player)) add_rule_safe(world, LocationName.egg_quarters_3, player, - lambda state: state.has(ItemName.rouge_mystic_melody, player) and - state.has(ItemName.rouge_pick_nails, player) and - state.has(ItemName.rouge_iron_boots, player)) + lambda state: state.has(ItemName.rouge_mystic_melody, player)) add_rule_safe(world, LocationName.lost_colony_3, player, lambda state: state.has(ItemName.eggman_mystic_melody, player) and state.has(ItemName.eggman_jet_engine, player)) @@ -845,8 +1008,6 @@ def set_mission_upgrade_rules_hard(world: MultiWorld, player: int): lambda state: state.has(ItemName.eggman_jet_engine, player)) add_rule_safe(world, LocationName.security_hall_5, player, lambda state: state.has(ItemName.rouge_treasure_scope, player)) - add_rule_safe(world, LocationName.mad_space_5, player, - lambda state: state.has(ItemName.rouge_iron_boots, player)) add_rule_safe(world, LocationName.cosmic_wall_5, player, lambda state: state.has(ItemName.eggman_jet_engine, player)) @@ -968,8 +1129,6 @@ def set_mission_upgrade_rules_hard(world: MultiWorld, player: int): add_rule(world.get_location(LocationName.cosmic_wall_pipe_1, player), lambda state: state.has(ItemName.eggman_jet_engine, player)) - add_rule(world.get_location(LocationName.mission_street_pipe_2, player), - lambda state: state.has(ItemName.tails_booster, player)) add_rule(world.get_location(LocationName.hidden_base_pipe_2, player), lambda state: state.has(ItemName.tails_booster, player)) add_rule(world.get_location(LocationName.death_chamber_pipe_2, player), @@ -997,9 +1156,6 @@ def set_mission_upgrade_rules_hard(world: MultiWorld, player: int): state.has(ItemName.knuckles_hammer_gloves, player)) add_rule(world.get_location(LocationName.eternal_engine_pipe_3, player), lambda state: state.has(ItemName.tails_booster, player)) - add_rule(world.get_location(LocationName.crazy_gadget_pipe_3, player), - lambda state: state.has(ItemName.sonic_bounce_bracelet, player) or - state.has(ItemName.sonic_mystic_melody, player)) add_rule(world.get_location(LocationName.weapons_bed_pipe_3, player), lambda state: state.has(ItemName.eggman_jet_engine, player)) @@ -1056,6 +1212,125 @@ def set_mission_upgrade_rules_hard(world: MultiWorld, player: int): add_rule(world.get_location(LocationName.cannon_core_hidden_1, player), lambda state: state.has(ItemName.tails_booster, player)) + # Omochao Upgrade Requirements + if world.omosanity[player]: + add_rule(world.get_location(LocationName.eternal_engine_omo_1, player), + lambda state: state.has(ItemName.tails_booster, player)) + + add_rule(world.get_location(LocationName.hidden_base_omo_2, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.death_chamber_omo_2, player), + lambda state: state.has(ItemName.knuckles_hammer_gloves, player)) + add_rule(world.get_location(LocationName.eternal_engine_omo_2, player), + lambda state: state.has(ItemName.tails_booster, player)) + + add_rule(world.get_location(LocationName.weapons_bed_omo_2, player), + lambda state: state.has(ItemName.eggman_jet_engine, player) or + state.has(ItemName.eggman_large_cannon, player)) + + add_rule(world.get_location(LocationName.hidden_base_omo_3, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.eternal_engine_omo_3, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.final_rush_omo_3, player), + lambda state: state.has(ItemName.sonic_bounce_bracelet, player)) + + add_rule(world.get_location(LocationName.weapons_bed_omo_3, player), + lambda state: state.has(ItemName.eggman_jet_engine, player)) + + add_rule(world.get_location(LocationName.hidden_base_omo_4, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.death_chamber_omo_4, player), + lambda state: state.has(ItemName.knuckles_hammer_gloves, player)) + add_rule(world.get_location(LocationName.eternal_engine_omo_4, player), + lambda state: state.has(ItemName.tails_booster, player)) + + add_rule(world.get_location(LocationName.cannon_core_omo_4, player), + lambda state: state.has(ItemName.tails_booster, player)) + + add_rule(world.get_location(LocationName.mission_street_omo_5, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.death_chamber_omo_5, player), + lambda state: state.has(ItemName.knuckles_hammer_gloves, player)) + add_rule(world.get_location(LocationName.eternal_engine_omo_5, player), + lambda state: state.has(ItemName.tails_booster, player)) + + add_rule(world.get_location(LocationName.cannon_core_omo_5, player), + lambda state: state.has(ItemName.tails_booster, player)) + + add_rule(world.get_location(LocationName.mission_street_omo_6, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.death_chamber_omo_6, player), + lambda state: state.has(ItemName.knuckles_hammer_gloves, player)) + add_rule(world.get_location(LocationName.eternal_engine_omo_6, player), + lambda state: state.has(ItemName.tails_booster, player)) + + add_rule(world.get_location(LocationName.lost_colony_omo_6, player), + lambda state: state.has(ItemName.eggman_jet_engine, player)) + + add_rule(world.get_location(LocationName.cannon_core_omo_6, player), + lambda state: state.has(ItemName.tails_booster, player)) + + add_rule(world.get_location(LocationName.mission_street_omo_7, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.death_chamber_omo_7, player), + lambda state: state.has(ItemName.knuckles_shovel_claws, player) and + state.has(ItemName.knuckles_hammer_gloves, player)) + add_rule(world.get_location(LocationName.eternal_engine_omo_7, player), + lambda state: state.has(ItemName.tails_booster, player)) + + add_rule(world.get_location(LocationName.lost_colony_omo_7, player), + lambda state: state.has(ItemName.eggman_jet_engine, player)) + + add_rule(world.get_location(LocationName.cannon_core_omo_7, player), + lambda state: state.has(ItemName.tails_booster, player) and + state.has(ItemName.knuckles_hammer_gloves, player)) + + add_rule(world.get_location(LocationName.mission_street_omo_8, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.death_chamber_omo_8, player), + lambda state: state.has(ItemName.knuckles_hammer_gloves, player)) + add_rule(world.get_location(LocationName.eternal_engine_omo_8, player), + lambda state: state.has(ItemName.tails_booster, player)) + + add_rule(world.get_location(LocationName.lost_colony_omo_8, player), + lambda state: state.has(ItemName.eggman_jet_engine, player)) + add_rule(world.get_location(LocationName.security_hall_omo_8, player), + lambda state: state.has(ItemName.rouge_iron_boots, player)) + + add_rule(world.get_location(LocationName.cannon_core_omo_8, player), + lambda state: state.has(ItemName.tails_booster, player) and + state.has(ItemName.knuckles_hammer_gloves, player)) + + add_rule(world.get_location(LocationName.death_chamber_omo_9, player), + lambda state: state.has(ItemName.knuckles_mystic_melody, player) and + state.has(ItemName.knuckles_hammer_gloves, player)) + add_rule(world.get_location(LocationName.eternal_engine_omo_9, player), + lambda state: state.has(ItemName.tails_booster, player)) + + add_rule(world.get_location(LocationName.cannon_core_omo_9, player), + lambda state: state.has(ItemName.tails_booster, player) and + state.has(ItemName.knuckles_hammer_gloves, player)) + + add_rule(world.get_location(LocationName.eternal_engine_omo_10, player), + lambda state: state.has(ItemName.tails_booster, player) and + state.has(ItemName.tails_bazooka, player)) + + add_rule(world.get_location(LocationName.eternal_engine_omo_11, player), + lambda state: state.has(ItemName.tails_booster, player) and + state.has(ItemName.tails_bazooka, player)) + + add_rule(world.get_location(LocationName.eternal_engine_omo_12, player), + lambda state: state.has(ItemName.tails_booster, player) and + state.has(ItemName.tails_bazooka, player)) + add_rule(world.get_location(LocationName.crazy_gadget_omo_12, player), + lambda state: state.has(ItemName.sonic_light_shoes, player) and + state.has(ItemName.sonic_flame_ring, player)) + + add_rule(world.get_location(LocationName.crazy_gadget_omo_13, player), + lambda state: state.has(ItemName.sonic_light_shoes, player) and + state.has(ItemName.sonic_flame_ring, player)) + # Gold Beetle Upgrade Requirements if world.beetlesanity[player]: add_rule(world.get_location(LocationName.hidden_base_beetle, player), @@ -1096,11 +1371,12 @@ def set_rules(world: MultiWorld, player: int, gate_bosses: typing.Dict[int, int] # Mission Progression Rules (Mission 1 begets Mission 2, etc.) set_mission_progress_rules(world, player, mission_map, mission_count_map) - # Upgrade Requirements for each mission location - if world.logic_difficulty[player].value == 0: - set_mission_upgrade_rules_standard(world, player) - elif world.logic_difficulty[player].value == 1: - set_mission_upgrade_rules_hard(world, player) + if world.goal[player].value != 3: + # Upgrade Requirements for each mission location + if world.logic_difficulty[player].value == 0: + set_mission_upgrade_rules_standard(world, player) + elif world.logic_difficulty[player].value == 1: + set_mission_upgrade_rules_hard(world, player) # Upgrade Requirements for each boss gate set_boss_gate_rules(world, player, gate_bosses) diff --git a/worlds/sa2b/__init__.py b/worlds/sa2b/__init__.py index dad7503e0e..d81ec58328 100644 --- a/worlds/sa2b/__init__.py +++ b/worlds/sa2b/__init__.py @@ -9,7 +9,7 @@ from .Regions import create_regions, shuffleable_regions, connect_regions, Level gate_0_blacklist_regions from .Rules import set_rules from .Names import ItemName, LocationName -from ..AutoWorld import WebWorld, World +from worlds.AutoWorld import WebWorld, World from .GateBosses import get_gate_bosses, get_boss_name from .Missions import get_mission_table, get_mission_count_table, get_first_and_last_cannons_core_missions import Patch @@ -52,7 +52,7 @@ class SA2BWorld(World): game: str = "Sonic Adventure 2 Battle" option_definitions = sa2b_options topology_present = False - data_version = 4 + data_version = 5 item_name_groups = item_groups item_name_to_id = {name: data.code for name, data in item_table.items()} @@ -71,17 +71,21 @@ class SA2BWorld(World): def _get_slot_data(self): return { - "ModVersion": 200, + "ModVersion": 201, "Goal": self.multiworld.goal[self.player].value, "MusicMap": self.music_map, "MissionMap": self.mission_map, "MissionCountMap": self.mission_count_map, "MusicShuffle": self.multiworld.music_shuffle[self.player].value, "Narrator": self.multiworld.narrator[self.player].value, + "MinigameTrapDifficulty": self.multiworld.minigame_trap_difficulty[self.player].value, + "RingLoss": self.multiworld.ring_loss[self.player].value, "RequiredRank": self.multiworld.required_rank[self.player].value, "ChaoKeys": self.multiworld.keysanity[self.player].value, "Whistlesanity": self.multiworld.whistlesanity[self.player].value, "GoldBeetles": self.multiworld.beetlesanity[self.player].value, + "OmochaoChecks": self.multiworld.omosanity[self.player].value, + "KartRaceChecks": self.multiworld.kart_race_checks[self.player].value, "ChaoRaceChecks": self.multiworld.chao_race_checks[self.player].value, "ChaoGardenDifficulty": self.multiworld.chao_garden_difficulty[self.player].value, "DeathLink": self.multiworld.death_link[self.player].value, @@ -145,13 +149,45 @@ class SA2BWorld(World): return levels_per_gate def generate_early(self): - self.gate_bosses = get_gate_bosses(self.multiworld, self.player) + if self.multiworld.goal[self.player].value == 3: + # Turn off everything else for Grand Prix goal + self.multiworld.number_of_level_gates[self.player].value = 0 + self.multiworld.emblem_percentage_for_cannons_core[self.player].value = 0 + self.multiworld.junk_fill_percentage[self.player].value = 100 + self.multiworld.trap_fill_percentage[self.player].value = 100 + self.multiworld.omochao_trap_weight[self.player].value = 0 + self.multiworld.timestop_trap_weight[self.player].value = 0 + self.multiworld.confusion_trap_weight[self.player].value = 0 + self.multiworld.tiny_trap_weight[self.player].value = 0 + self.multiworld.gravity_trap_weight[self.player].value = 0 - def generate_basic(self): + valid_trap_weights = self.multiworld.exposition_trap_weight[self.player].value + self.multiworld.pong_trap_weight[self.player].value + + if valid_trap_weights == 0: + self.multiworld.exposition_trap_weight[self.player].value = 4 + self.multiworld.pong_trap_weight[self.player].value = 4 + + if self.multiworld.kart_race_checks[self.player].value == 0: + self.multiworld.kart_race_checks[self.player].value = 2 + + self.gate_bosses = {} + else: + self.gate_bosses = get_gate_bosses(self.multiworld, self.player) + + def create_regions(self): + self.mission_map = get_mission_table(self.multiworld, self.player) + self.mission_count_map = get_mission_count_table(self.multiworld, self.player) + + self.location_table = setup_locations(self.multiworld, self.player, self.mission_map, self.mission_count_map) + create_regions(self.multiworld, self.player, self.location_table) + + # Not Generate Basic if self.multiworld.goal[self.player].value == 0 or self.multiworld.goal[self.player].value == 2: self.multiworld.get_location(LocationName.finalhazard, self.player).place_locked_item(self.create_item(ItemName.maria)) elif self.multiworld.goal[self.player].value == 1: self.multiworld.get_location(LocationName.green_hill, self.player).place_locked_item(self.create_item(ItemName.maria)) + elif self.multiworld.goal[self.player].value == 3: + self.multiworld.get_location(LocationName.grand_prix, self.player).place_locked_item(self.create_item(ItemName.maria)) itempool: typing.List[SA2BItem] = [] @@ -159,18 +195,19 @@ class SA2BWorld(World): total_required_locations = len(self.location_table) total_required_locations -= 1; # Locked Victory Location - # Fill item pool with all required items - for item in {**upgrades_table}: - itempool += self._create_items(item) - - if self.multiworld.goal[self.player].value == 1 or self.multiworld.goal[self.player].value == 2: - # Some flavor of Chaos Emerald Hunt - for item in {**emeralds_table}: + if self.multiworld.goal[self.player].value != 3: + # Fill item pool with all required items + for item in {**upgrades_table}: itempool += self._create_items(item) - # Cap at 180 Emblems + if self.multiworld.goal[self.player].value == 1 or self.multiworld.goal[self.player].value == 2: + # Some flavor of Chaos Emerald Hunt + for item in {**emeralds_table}: + itempool += self._create_items(item) + + # Cap at 250 Emblems raw_emblem_count = total_required_locations - len(itempool) - total_emblem_count = min(raw_emblem_count, 180) + total_emblem_count = min(raw_emblem_count, 250) extra_junk_count = raw_emblem_count - total_emblem_count self.emblems_for_cannons_core = math.floor( @@ -234,6 +271,7 @@ class SA2BWorld(World): trap_weights += ([ItemName.gravity_trap] * self.multiworld.gravity_trap_weight[self.player].value) trap_weights += ([ItemName.exposition_trap] * self.multiworld.exposition_trap_weight[self.player].value) #trap_weights += ([ItemName.darkness_trap] * self.multiworld.darkness_trap_weight[self.player].value) + trap_weights += ([ItemName.pong_trap] * self.multiworld.pong_trap_weight[self.player].value) junk_count += extra_junk_count trap_count = 0 if (len(trap_weights) == 0) else math.ceil(junk_count * (self.multiworld.trap_fill_percentage[self.player].value / 100.0)) @@ -309,13 +347,6 @@ class SA2BWorld(World): self.music_map = dict(zip(musiclist_o, musiclist_s)) - def create_regions(self): - self.mission_map = get_mission_table(self.multiworld, self.player) - self.mission_count_map = get_mission_count_table(self.multiworld, self.player) - - self.location_table = setup_locations(self.multiworld, self.player, self.mission_map, self.mission_count_map) - create_regions(self.multiworld, self.player, self.location_table) - def create_item(self, name: str, force_non_progression=False) -> Item: data = item_table[name] diff --git a/worlds/sa2b/docs/en_Sonic Adventure 2 Battle.md b/worlds/sa2b/docs/en_Sonic Adventure 2 Battle.md index 89ff80e9c4..3b608808ae 100644 --- a/worlds/sa2b/docs/en_Sonic Adventure 2 Battle.md +++ b/worlds/sa2b/docs/en_Sonic Adventure 2 Battle.md @@ -10,11 +10,11 @@ The randomizer shuffles emblems and upgrade items into the AP item pool. The sto ## What is the goal of Sonic Adventure 2: Battle when randomized? -If the Biolizard goal is selected, the objective is to unlock and complete the required number of Cannon's Core Missions, then complete the Biolizard boss fight. If the Emerald Hunt goal is selected, the objective is to find the seven Chaos Emeralds then complete Green Hill Zone and optionally default Final Hazard. +If the Biolizard goal is selected, the objective is to unlock and complete the required number of Cannon's Core Missions, then complete the Biolizard boss fight. If the Emerald Hunt goal is selected, the objective is to find the seven Chaos Emeralds then complete Green Hill Zone and optionally default Final Hazard. If the Grand Prix goal is selected, the objective is to complete all levels in the kart race mode. ## What items and locations get shuffled? -All 30 story stages leading up to Cannon's Core will be shuffled and can be optionally placed behind emblem requirement gates. Mission order can be shuffled for each stage. Chao keys, animal pipes, hidden whistle spots, and gold beetles can be added as additional locations to check in each stage. Chao Garden emblems can optionally be added to the randomizer. All emblems from the selected mission range and all 28 character upgrade items get shuffled. At most 180 emblems will be added to the item pool, after which remaining items added will be random collectables (rings, shields, etc). Traps can also be optionally included. +All 30 story stages leading up to Cannon's Core will be shuffled and can be optionally placed behind emblem requirement gates. Mission order can be shuffled for each stage. Chao keys, animal pipes, hidden whistle spots, omochao, and gold beetles can be added as additional locations to check in each stage. Chao Garden emblems can optionally be added to the randomizer. All emblems from the selected mission range and all 28 character upgrade items get shuffled. At most 250 emblems will be added to the item pool, after which remaining items added will be random collectables (rings, shields, etc). Traps can also be optionally included. ## Which items can be in another player's world? diff --git a/worlds/sc2wol/Items.py b/worlds/sc2wol/Items.py index aae83f5031..9776e4fed1 100644 --- a/worlds/sc2wol/Items.py +++ b/worlds/sc2wol/Items.py @@ -182,9 +182,11 @@ filler_items: typing.Tuple[str, ...] = ( '+15 Starting Vespene' ) +# Defense rating table +# Commented defense ratings are handled in LogicMixin defense_ratings = { "Siege Tank": 5, - "Maelstrom Rounds": 2, + # "Maelstrom Rounds": 2, "Planetary Fortress": 3, # Bunker w/ Marine/Marauder: 3, "Perdition Turret": 2, @@ -193,7 +195,7 @@ defense_ratings = { } zerg_defense_ratings = { "Perdition Turret": 2, - # Bunker w/ Firebat: 2 + # Bunker w/ Firebat: 2, "Hive Mind Emulator": 3, "Psi Disruptor": 3 } diff --git a/worlds/sc2wol/LogicMixin.py b/worlds/sc2wol/LogicMixin.py index dac9d856e7..c803835f63 100644 --- a/worlds/sc2wol/LogicMixin.py +++ b/worlds/sc2wol/LogicMixin.py @@ -17,10 +17,12 @@ class SC2WoLLogic(LogicMixin): or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has('Wraith', player) def _sc2wol_has_competent_anti_air(self, multiworld: MultiWorld, player: int) -> bool: - return self.has_any({'Marine', 'Goliath'}, player) or self._sc2wol_has_air_anti_air(multiworld, player) + return self.has('Goliath', player) \ + or self.has('Marine', player) and self.has_any({'Medic', 'Medivac'}, player) \ + or self._sc2wol_has_air_anti_air(multiworld, player) def _sc2wol_has_anti_air(self, multiworld: MultiWorld, player: int) -> bool: - return self.has_any({'Missile Turret', 'Thor', 'War Pigs', 'Spartan Company', "Hel's Angel", 'Battlecruiser', 'Wraith'}, player) \ + return self.has_any({'Missile Turret', 'Thor', 'War Pigs', 'Spartan Company', "Hel's Angel", 'Battlecruiser', 'Marine', 'Wraith'}, player) \ or self._sc2wol_has_competent_anti_air(multiworld, player) \ or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has_any({'Ghost', 'Spectre'}, player) @@ -28,6 +30,8 @@ class SC2WoLLogic(LogicMixin): defense_score = sum((defense_ratings[item] for item in defense_ratings if self.has(item, player))) if self.has_any({'Marine', 'Marauder'}, player) and self.has('Bunker', player): defense_score += 3 + if self.has_all({'Siege Tank', 'Maelstrom Rounds'}, player): + defense_score += 2 if zerg_enemy: defense_score += sum((zerg_defense_ratings[item] for item in zerg_defense_ratings if self.has(item, player))) if self.has('Firebat', player) and self.has('Bunker', player): diff --git a/worlds/sc2wol/MissionTables.py b/worlds/sc2wol/MissionTables.py index d926ea6251..6db9354768 100644 --- a/worlds/sc2wol/MissionTables.py +++ b/worlds/sc2wol/MissionTables.py @@ -1,7 +1,5 @@ -from typing import NamedTuple, Dict, List, Set - -from BaseClasses import MultiWorld -from .Options import get_option_value +from typing import NamedTuple, Dict, List +from enum import IntEnum no_build_regions_list = ["Liberation Day", "Breakout", "Ghost of a Chance", "Piercing the Shroud", "Whispers of Doom", "Belly of the Beast"] @@ -13,6 +11,14 @@ hard_regions_list = ["Maw of the Void", "Engine of Destruction", "In Utter Darkn "Shatter the Sky"] +class MissionPools(IntEnum): + STARTER = 0 + EASY = 1 + MEDIUM = 2 + HARD = 3 + FINAL = 4 + + class MissionInfo(NamedTuple): id: int required_world: List[int] @@ -23,119 +29,119 @@ class MissionInfo(NamedTuple): class FillMission(NamedTuple): - type: str + type: int connect_to: List[int] # -1 connects to Menu category: str number: int = 0 # number of worlds need beaten completion_critical: bool = False # missions needed to beat game or_requirements: bool = False # true if the requirements should be or-ed instead of and-ed - relegate: bool = False # true if this is a slot no build missions should be relegated to. + removal_priority: int = 0 # how many missions missing from the pool required to remove this mission vanilla_shuffle_order = [ - FillMission("no_build", [-1], "Mar Sara", completion_critical=True), - FillMission("easy", [0], "Mar Sara", completion_critical=True), - FillMission("easy", [1], "Mar Sara", completion_critical=True), - FillMission("easy", [2], "Colonist"), - FillMission("medium", [3], "Colonist"), - FillMission("hard", [4], "Colonist", number=7), - FillMission("hard", [4], "Colonist", number=7, relegate=True), - FillMission("easy", [2], "Artifact", completion_critical=True), - FillMission("medium", [7], "Artifact", number=8, completion_critical=True), - FillMission("hard", [8], "Artifact", number=11, completion_critical=True), - FillMission("hard", [9], "Artifact", number=14, completion_critical=True), - FillMission("hard", [10], "Artifact", completion_critical=True), - FillMission("medium", [2], "Covert", number=4), - FillMission("medium", [12], "Covert"), - FillMission("hard", [13], "Covert", number=8, relegate=True), - FillMission("hard", [13], "Covert", number=8, relegate=True), - FillMission("medium", [2], "Rebellion", number=6), - FillMission("hard", [16], "Rebellion"), - FillMission("hard", [17], "Rebellion"), - FillMission("hard", [18], "Rebellion"), - FillMission("hard", [19], "Rebellion", relegate=True), - FillMission("medium", [8], "Prophecy"), - FillMission("hard", [21], "Prophecy"), - FillMission("hard", [22], "Prophecy"), - FillMission("hard", [23], "Prophecy", relegate=True), - FillMission("hard", [11], "Char", completion_critical=True), - FillMission("hard", [25], "Char", completion_critical=True), - FillMission("hard", [25], "Char", completion_critical=True), - FillMission("all_in", [26, 27], "Char", completion_critical=True, or_requirements=True) + FillMission(MissionPools.STARTER, [-1], "Mar Sara", completion_critical=True), + FillMission(MissionPools.EASY, [0], "Mar Sara", completion_critical=True), + FillMission(MissionPools.EASY, [1], "Mar Sara", completion_critical=True), + FillMission(MissionPools.EASY, [2], "Colonist"), + FillMission(MissionPools.MEDIUM, [3], "Colonist"), + FillMission(MissionPools.HARD, [4], "Colonist", number=7), + FillMission(MissionPools.HARD, [4], "Colonist", number=7, removal_priority=1), + FillMission(MissionPools.EASY, [2], "Artifact", completion_critical=True), + FillMission(MissionPools.MEDIUM, [7], "Artifact", number=8, completion_critical=True), + FillMission(MissionPools.HARD, [8], "Artifact", number=11, completion_critical=True), + FillMission(MissionPools.HARD, [9], "Artifact", number=14, completion_critical=True), + FillMission(MissionPools.HARD, [10], "Artifact", completion_critical=True), + FillMission(MissionPools.MEDIUM, [2], "Covert", number=4), + FillMission(MissionPools.MEDIUM, [12], "Covert"), + FillMission(MissionPools.HARD, [13], "Covert", number=8, removal_priority=3), + FillMission(MissionPools.HARD, [13], "Covert", number=8, removal_priority=2), + FillMission(MissionPools.MEDIUM, [2], "Rebellion", number=6), + FillMission(MissionPools.HARD, [16], "Rebellion"), + FillMission(MissionPools.HARD, [17], "Rebellion"), + FillMission(MissionPools.HARD, [18], "Rebellion"), + FillMission(MissionPools.HARD, [19], "Rebellion", removal_priority=5), + FillMission(MissionPools.MEDIUM, [8], "Prophecy", removal_priority=9), + FillMission(MissionPools.HARD, [21], "Prophecy", removal_priority=8), + FillMission(MissionPools.HARD, [22], "Prophecy", removal_priority=7), + FillMission(MissionPools.HARD, [23], "Prophecy", removal_priority=6), + FillMission(MissionPools.HARD, [11], "Char", completion_critical=True), + FillMission(MissionPools.HARD, [25], "Char", completion_critical=True, removal_priority=4), + FillMission(MissionPools.HARD, [25], "Char", completion_critical=True), + FillMission(MissionPools.FINAL, [26, 27], "Char", completion_critical=True, or_requirements=True) ] mini_campaign_order = [ - FillMission("no_build", [-1], "Mar Sara", completion_critical=True), - FillMission("easy", [0], "Colonist"), - FillMission("medium", [1], "Colonist"), - FillMission("medium", [0], "Artifact", completion_critical=True), - FillMission("medium", [3], "Artifact", number=4, completion_critical=True), - FillMission("hard", [4], "Artifact", number=8, completion_critical=True), - FillMission("medium", [0], "Covert", number=2), - FillMission("hard", [6], "Covert"), - FillMission("medium", [0], "Rebellion", number=3), - FillMission("hard", [8], "Rebellion"), - FillMission("medium", [4], "Prophecy"), - FillMission("hard", [10], "Prophecy"), - FillMission("hard", [5], "Char", completion_critical=True), - FillMission("hard", [5], "Char", completion_critical=True), - FillMission("all_in", [12, 13], "Char", completion_critical=True, or_requirements=True) + FillMission(MissionPools.STARTER, [-1], "Mar Sara", completion_critical=True), + FillMission(MissionPools.EASY, [0], "Colonist"), + FillMission(MissionPools.MEDIUM, [1], "Colonist"), + FillMission(MissionPools.EASY, [0], "Artifact", completion_critical=True), + FillMission(MissionPools.MEDIUM, [3], "Artifact", number=4, completion_critical=True), + FillMission(MissionPools.HARD, [4], "Artifact", number=8, completion_critical=True), + FillMission(MissionPools.MEDIUM, [0], "Covert", number=2), + FillMission(MissionPools.HARD, [6], "Covert"), + FillMission(MissionPools.MEDIUM, [0], "Rebellion", number=3), + FillMission(MissionPools.HARD, [8], "Rebellion"), + FillMission(MissionPools.MEDIUM, [4], "Prophecy"), + FillMission(MissionPools.HARD, [10], "Prophecy"), + FillMission(MissionPools.HARD, [5], "Char", completion_critical=True), + FillMission(MissionPools.HARD, [5], "Char", completion_critical=True), + FillMission(MissionPools.FINAL, [12, 13], "Char", completion_critical=True, or_requirements=True) ] gauntlet_order = [ - FillMission("no_build", [-1], "I", completion_critical=True), - FillMission("easy", [0], "II", completion_critical=True), - FillMission("medium", [1], "III", completion_critical=True), - FillMission("medium", [2], "IV", completion_critical=True), - FillMission("hard", [3], "V", completion_critical=True), - FillMission("hard", [4], "VI", completion_critical=True), - FillMission("all_in", [5], "Final", completion_critical=True) + FillMission(MissionPools.STARTER, [-1], "I", completion_critical=True), + FillMission(MissionPools.EASY, [0], "II", completion_critical=True), + FillMission(MissionPools.EASY, [1], "III", completion_critical=True), + FillMission(MissionPools.MEDIUM, [2], "IV", completion_critical=True), + FillMission(MissionPools.MEDIUM, [3], "V", completion_critical=True), + FillMission(MissionPools.HARD, [4], "VI", completion_critical=True), + FillMission(MissionPools.FINAL, [5], "Final", completion_critical=True) ] grid_order = [ - FillMission("no_build", [-1], "_1"), - FillMission("medium", [0], "_1"), - FillMission("medium", [1, 6, 3], "_1", or_requirements=True), - FillMission("hard", [2, 7], "_1", or_requirements=True), - FillMission("easy", [0], "_2"), - FillMission("medium", [1, 4], "_2", or_requirements=True), - FillMission("hard", [2, 5, 10, 7], "_2", or_requirements=True), - FillMission("hard", [3, 6, 11], "_2", or_requirements=True), - FillMission("medium", [4, 9, 12], "_3", or_requirements=True), - FillMission("hard", [5, 8, 10, 13], "_3", or_requirements=True), - FillMission("hard", [6, 9, 11, 14], "_3", or_requirements=True), - FillMission("hard", [7, 10], "_3", or_requirements=True), - FillMission("hard", [8, 13], "_4", or_requirements=True), - FillMission("hard", [9, 12, 14], "_4", or_requirements=True), - FillMission("hard", [10, 13], "_4", or_requirements=True), - FillMission("all_in", [11, 14], "_4", or_requirements=True) + FillMission(MissionPools.STARTER, [-1], "_1"), + FillMission(MissionPools.EASY, [0], "_1"), + FillMission(MissionPools.MEDIUM, [1, 6, 3], "_1", or_requirements=True), + FillMission(MissionPools.HARD, [2, 7], "_1", or_requirements=True), + FillMission(MissionPools.EASY, [0], "_2"), + FillMission(MissionPools.MEDIUM, [1, 4], "_2", or_requirements=True), + FillMission(MissionPools.HARD, [2, 5, 10, 7], "_2", or_requirements=True), + FillMission(MissionPools.HARD, [3, 6, 11], "_2", or_requirements=True), + FillMission(MissionPools.MEDIUM, [4, 9, 12], "_3", or_requirements=True), + FillMission(MissionPools.HARD, [5, 8, 10, 13], "_3", or_requirements=True), + FillMission(MissionPools.HARD, [6, 9, 11, 14], "_3", or_requirements=True), + FillMission(MissionPools.HARD, [7, 10], "_3", or_requirements=True), + FillMission(MissionPools.HARD, [8, 13], "_4", or_requirements=True), + FillMission(MissionPools.HARD, [9, 12, 14], "_4", or_requirements=True), + FillMission(MissionPools.HARD, [10, 13], "_4", or_requirements=True), + FillMission(MissionPools.FINAL, [11, 14], "_4", or_requirements=True) ] mini_grid_order = [ - FillMission("no_build", [-1], "_1"), - FillMission("medium", [0], "_1"), - FillMission("medium", [1, 5], "_1", or_requirements=True), - FillMission("easy", [0], "_2"), - FillMission("medium", [1, 3], "_2", or_requirements=True), - FillMission("hard", [2, 4], "_2", or_requirements=True), - FillMission("medium", [3, 7], "_3", or_requirements=True), - FillMission("hard", [4, 6], "_3", or_requirements=True), - FillMission("all_in", [5, 7], "_3", or_requirements=True) + FillMission(MissionPools.STARTER, [-1], "_1"), + FillMission(MissionPools.EASY, [0], "_1"), + FillMission(MissionPools.MEDIUM, [1, 5], "_1", or_requirements=True), + FillMission(MissionPools.EASY, [0], "_2"), + FillMission(MissionPools.MEDIUM, [1, 3], "_2", or_requirements=True), + FillMission(MissionPools.HARD, [2, 4], "_2", or_requirements=True), + FillMission(MissionPools.MEDIUM, [3, 7], "_3", or_requirements=True), + FillMission(MissionPools.HARD, [4, 6], "_3", or_requirements=True), + FillMission(MissionPools.FINAL, [5, 7], "_3", or_requirements=True) ] blitz_order = [ - FillMission("no_build", [-1], "I"), - FillMission("easy", [-1], "I"), - FillMission("medium", [0, 1], "II", number=1, or_requirements=True), - FillMission("medium", [0, 1], "II", number=1, or_requirements=True), - FillMission("medium", [0, 1], "III", number=2, or_requirements=True), - FillMission("medium", [0, 1], "III", number=2, or_requirements=True), - FillMission("hard", [0, 1], "IV", number=3, or_requirements=True), - FillMission("hard", [0, 1], "IV", number=3, or_requirements=True), - FillMission("hard", [0, 1], "V", number=4, or_requirements=True), - FillMission("hard", [0, 1], "V", number=4, or_requirements=True), - FillMission("hard", [0, 1], "Final", number=5, or_requirements=True), - FillMission("all_in", [0, 1], "Final", number=5, or_requirements=True) + FillMission(MissionPools.STARTER, [-1], "I"), + FillMission(MissionPools.EASY, [-1], "I"), + FillMission(MissionPools.MEDIUM, [0, 1], "II", number=1, or_requirements=True), + FillMission(MissionPools.MEDIUM, [0, 1], "II", number=1, or_requirements=True), + FillMission(MissionPools.MEDIUM, [0, 1], "III", number=2, or_requirements=True), + FillMission(MissionPools.MEDIUM, [0, 1], "III", number=2, or_requirements=True), + FillMission(MissionPools.HARD, [0, 1], "IV", number=3, or_requirements=True), + FillMission(MissionPools.HARD, [0, 1], "IV", number=3, or_requirements=True), + FillMission(MissionPools.HARD, [0, 1], "V", number=4, or_requirements=True), + FillMission(MissionPools.HARD, [0, 1], "V", number=4, or_requirements=True), + FillMission(MissionPools.HARD, [0, 1], "Final", number=5, or_requirements=True), + FillMission(MissionPools.FINAL, [0, 1], "Final", number=5, or_requirements=True) ] mission_orders = [vanilla_shuffle_order, vanilla_shuffle_order, mini_campaign_order, grid_order, mini_grid_order, blitz_order, gauntlet_order] @@ -176,40 +182,21 @@ vanilla_mission_req_table = { lookup_id_to_mission: Dict[int, str] = { data.id: mission_name for mission_name, data in vanilla_mission_req_table.items() if data.id} -no_build_starting_mission_locations = { +starting_mission_locations = { "Liberation Day": "Liberation Day: Victory", "Breakout": "Breakout: Victory", "Ghost of a Chance": "Ghost of a Chance: Victory", "Piercing the Shroud": "Piercing the Shroud: Victory", "Whispers of Doom": "Whispers of Doom: Victory", "Belly of the Beast": "Belly of the Beast: Victory", -} - -build_starting_mission_locations = { "Zero Hour": "Zero Hour: First Group Rescued", "Evacuation": "Evacuation: First Chysalis", - "Devil's Playground": "Devil's Playground: Tosh's Miners" -} - -advanced_starting_mission_locations = { + "Devil's Playground": "Devil's Playground: Tosh's Miners", "Smash and Grab": "Smash and Grab: First Relic", "The Great Train Robbery": "The Great Train Robbery: North Defiler" } -def get_starting_mission_locations(multiworld: MultiWorld, player: int) -> Set[str]: - if get_option_value(multiworld, player, 'shuffle_no_build') or get_option_value(multiworld, player, 'mission_order') < 2: - # Always start with a no-build mission unless explicitly relegating them - # Vanilla and Vanilla Shuffled always start with a no-build even when relegated - return no_build_starting_mission_locations - elif get_option_value(multiworld, player, 'required_tactics') > 0: - # Advanced Tactics/No Logic add more starting missions to the pool - return {**build_starting_mission_locations, **advanced_starting_mission_locations} - else: - # Standard starting missions when relegate is on - return build_starting_mission_locations - - alt_final_mission_locations = { "Maw of the Void": "Maw of the Void: Victory", "Engine of Destruction": "Engine of Destruction: Victory", diff --git a/worlds/sc2wol/Options.py b/worlds/sc2wol/Options.py index 4526328f53..4f2032d662 100644 --- a/worlds/sc2wol/Options.py +++ b/worlds/sc2wol/Options.py @@ -1,6 +1,7 @@ -from typing import Dict +from typing import Dict, FrozenSet, Union from BaseClasses import MultiWorld from Options import Choice, Option, Toggle, DefaultOnToggle, ItemSet, OptionSet, Range +from .MissionTables import vanilla_mission_req_table class GameDifficulty(Choice): @@ -110,6 +111,7 @@ class ExcludedMissions(OptionSet): Only applies on shortened mission orders. It may be impossible to build a valid campaign if too many missions are excluded.""" display_name = "Excluded Missions" + valid_keys = {mission_name for mission_name in vanilla_mission_req_table.keys() if mission_name != 'All-In'} # noinspection PyTypeChecker @@ -130,19 +132,10 @@ sc2wol_options: Dict[str, Option] = { } -def get_option_value(multiworld: MultiWorld, player: int, name: str) -> int: - option = getattr(multiworld, name, None) +def get_option_value(multiworld: MultiWorld, player: int, name: str) -> Union[int, FrozenSet]: + if multiworld is None: + return sc2wol_options[name].default - if option is None: - return 0 + player_option = getattr(multiworld, name)[player] - return int(option[player].value) - - -def get_option_set_value(multiworld: MultiWorld, player: int, name: str) -> set: - option = getattr(multiworld, name, None) - - if option is None: - return set() - - return option[player].value + return player_option.value diff --git a/worlds/sc2wol/PoolFilter.py b/worlds/sc2wol/PoolFilter.py index c4aa1098bb..16cc51f243 100644 --- a/worlds/sc2wol/PoolFilter.py +++ b/worlds/sc2wol/PoolFilter.py @@ -2,8 +2,8 @@ from typing import Callable, Dict, List, Set from BaseClasses import MultiWorld, ItemClassification, Item, Location from .Items import item_table from .MissionTables import no_build_regions_list, easy_regions_list, medium_regions_list, hard_regions_list,\ - mission_orders, get_starting_mission_locations, MissionInfo, vanilla_mission_req_table, alt_final_mission_locations -from .Options import get_option_value, get_option_set_value + mission_orders, MissionInfo, alt_final_mission_locations, MissionPools +from .Options import get_option_value from .LogicMixin import SC2WoLLogic # Items with associated upgrades @@ -21,34 +21,33 @@ STARPORT_UNITS = {"Medivac", "Wraith", "Viking", "Banshee", "Battlecruiser", "He PROTOSS_REGIONS = {"A Sinister Turn", "Echoes of the Future", "In Utter Darkness"} -def filter_missions(multiworld: MultiWorld, player: int) -> Dict[str, List[str]]: +def filter_missions(multiworld: MultiWorld, player: int) -> Dict[int, List[str]]: """ Returns a semi-randomly pruned tuple of no-build, easy, medium, and hard mission sets """ mission_order_type = get_option_value(multiworld, player, "mission_order") + shuffle_no_build = get_option_value(multiworld, player, "shuffle_no_build") shuffle_protoss = get_option_value(multiworld, player, "shuffle_protoss") - excluded_missions = set(get_option_set_value(multiworld, player, "excluded_missions")) - invalid_mission_names = excluded_missions.difference(vanilla_mission_req_table.keys()) - if invalid_mission_names: - raise Exception("Error in locked_missions - the following are not valid mission names: " + ", ".join(invalid_mission_names)) + excluded_missions = get_option_value(multiworld, player, "excluded_missions") mission_count = len(mission_orders[mission_order_type]) - 1 - # Vanilla and Vanilla Shuffled use the entire mission pool - if mission_count == 28: - return { - "no_build": no_build_regions_list[:], - "easy": easy_regions_list[:], - "medium": medium_regions_list[:], - "hard": hard_regions_list[:], - "all_in": ["All-In"] - } - - mission_pools = [ - [], - easy_regions_list, - medium_regions_list, - hard_regions_list - ] + mission_pools = { + MissionPools.STARTER: no_build_regions_list[:], + MissionPools.EASY: easy_regions_list[:], + MissionPools.MEDIUM: medium_regions_list[:], + MissionPools.HARD: hard_regions_list[:], + MissionPools.FINAL: [] + } + if mission_order_type == 0: + # Vanilla uses the entire mission pool + mission_pools[MissionPools.FINAL] = ['All-In'] + return mission_pools + elif mission_order_type == 1: + # Vanilla Shuffled ignores the player-provided excluded missions + excluded_missions = set() + # Omitting No-Build missions if not shuffling no-build + if not shuffle_no_build: + excluded_missions = excluded_missions.union(no_build_regions_list) # Omitting Protoss missions if not shuffling protoss if not shuffle_protoss: excluded_missions = excluded_missions.union(PROTOSS_REGIONS) @@ -58,46 +57,35 @@ def filter_missions(multiworld: MultiWorld, player: int) -> Dict[str, List[str]] excluded_missions.add(final_mission) else: final_mission = 'All-In' - # Yaml settings determine which missions can be placed in the first slot - mission_pools[0] = [mission for mission in get_starting_mission_locations(multiworld, player).keys() if mission not in excluded_missions] - # Removing the new no-build missions from their original sets - for i in range(1, len(mission_pools)): - mission_pools[i] = [mission for mission in mission_pools[i] if mission not in excluded_missions.union(mission_pools[0])] - # If the first mission is a build mission, there may not be enough locations to reach Outbreak as a second mission + # Excluding missions + for difficulty, mission_pool in mission_pools.items(): + mission_pools[difficulty] = [mission for mission in mission_pool if mission not in excluded_missions] + mission_pools[MissionPools.FINAL].append(final_mission) + # Mission pool changes on Build-Only if not get_option_value(multiworld, player, 'shuffle_no_build'): - # Swapping Outbreak and The Great Train Robbery - if "Outbreak" in mission_pools[1]: - mission_pools[1].remove("Outbreak") - mission_pools[2].append("Outbreak") - if "The Great Train Robbery" in mission_pools[2]: - mission_pools[2].remove("The Great Train Robbery") - mission_pools[1].append("The Great Train Robbery") - # Removing random missions from each difficulty set in a cycle - set_cycle = 0 - current_count = sum(len(mission_pool) for mission_pool in mission_pools) + def move_mission(mission_name, current_pool, new_pool): + if mission_name in mission_pools[current_pool]: + mission_pools[current_pool].remove(mission_name) + mission_pools[new_pool].append(mission_name) + # Replacing No Build missions with Easy missions + move_mission("Zero Hour", MissionPools.EASY, MissionPools.STARTER) + move_mission("Evacuation", MissionPools.EASY, MissionPools.STARTER) + move_mission("Devil's Playground", MissionPools.EASY, MissionPools.STARTER) + # Pushing Outbreak to Normal, as it cannot be placed as the second mission on Build-Only + move_mission("Outbreak", MissionPools.EASY, MissionPools.MEDIUM) + # Pushing extra Normal missions to Easy + move_mission("The Great Train Robbery", MissionPools.MEDIUM, MissionPools.EASY) + move_mission("Echoes of the Future", MissionPools.MEDIUM, MissionPools.EASY) + move_mission("Cutthroat", MissionPools.MEDIUM, MissionPools.EASY) + # Additional changes on Advanced Tactics + if get_option_value(multiworld, player, "required_tactics") > 0: + move_mission("The Great Train Robbery", MissionPools.EASY, MissionPools.STARTER) + move_mission("Smash and Grab", MissionPools.EASY, MissionPools.STARTER) + move_mission("Moebius Factor", MissionPools.MEDIUM, MissionPools.EASY) + move_mission("Welcome to the Jungle", MissionPools.MEDIUM, MissionPools.EASY) + move_mission("Engine of Destruction", MissionPools.HARD, MissionPools.MEDIUM) - if current_count < mission_count: - raise Exception("Not enough missions available to fill the campaign on current settings. Please exclude fewer missions.") - while current_count > mission_count: - if set_cycle == 4: - set_cycle = 0 - # Must contain at least one mission per set - mission_pool = mission_pools[set_cycle] - if len(mission_pool) <= 1: - if all(len(mission_pool) <= 1 for mission_pool in mission_pools): - raise Exception("Not enough missions available to fill the campaign on current settings. Please exclude fewer missions.") - else: - mission_pool.remove(multiworld.random.choice(mission_pool)) - current_count -= 1 - set_cycle += 1 - - return { - "no_build": mission_pools[0], - "easy": mission_pools[1], - "medium": mission_pools[2], - "hard": mission_pools[3], - "all_in": [final_mission] - } + return mission_pools def get_item_upgrades(inventory: List[Item], parent_item: Item or str): @@ -135,7 +123,21 @@ class ValidInventory: requirements = mission_requirements cascade_keys = self.cascade_removal_map.keys() units_always_have_upgrades = get_option_value(self.multiworld, self.player, "units_always_have_upgrades") - if self.min_units_per_structure > 0: + + # Locking associated items for items that have already been placed when units_always_have_upgrades is on + if units_always_have_upgrades: + existing_items = self.existing_items[:] + while existing_items: + existing_item = existing_items.pop() + items_to_lock = self.cascade_removal_map.get(existing_item, [existing_item]) + for item in items_to_lock: + if item in inventory: + inventory.remove(item) + locked_items.append(item) + if item in existing_items: + existing_items.remove(item) + + if self.min_units_per_structure > 0 and self.has_units_per_structure(): requirements.append(lambda state: state.has_units_per_structure()) def attempt_removal(item: Item) -> bool: @@ -151,6 +153,10 @@ class ValidInventory: return False return True + # Determining if the full-size inventory can complete campaign + if not all(requirement(self) for requirement in requirements): + raise Exception("Too many items excluded - campaign is impossible to complete.") + while len(inventory) + len(locked_items) > inventory_size: if len(inventory) == 0: raise Exception("Reduced item pool generation failed - not enough locations available to place items.") diff --git a/worlds/sc2wol/Regions.py b/worlds/sc2wol/Regions.py index bcf6434aa5..033636662b 100644 --- a/worlds/sc2wol/Regions.py +++ b/worlds/sc2wol/Regions.py @@ -2,7 +2,7 @@ from typing import List, Set, Dict, Tuple, Optional, Callable from BaseClasses import MultiWorld, Region, Entrance, Location from .Locations import LocationData from .Options import get_option_value -from .MissionTables import MissionInfo, mission_orders, vanilla_mission_req_table, alt_final_mission_locations +from .MissionTables import MissionInfo, mission_orders, vanilla_mission_req_table, alt_final_mission_locations, MissionPools from .PoolFilter import filter_missions @@ -14,34 +14,18 @@ def create_regions(multiworld: MultiWorld, player: int, locations: Tuple[Locatio mission_order = mission_orders[mission_order_type] mission_pools = filter_missions(multiworld, player) - final_mission = mission_pools['all_in'][0] - used_regions = [mission for mission_pool in mission_pools.values() for mission in mission_pool] regions = [create_region(multiworld, player, locations_per_region, location_cache, "Menu")] - for region_name in used_regions: - regions.append(create_region(multiworld, player, locations_per_region, location_cache, region_name)) - # Changing the completion condition for alternate final missions into an event - if final_mission != 'All-In': - final_location = alt_final_mission_locations[final_mission] - # Final location should be near the end of the cache - for i in range(len(location_cache) - 1, -1, -1): - if location_cache[i].name == final_location: - location_cache[i].locked = True - location_cache[i].event = True - location_cache[i].address = None - break - else: - final_location = 'All-In: Victory' - - if __debug__: - if mission_order_type in (0, 1): - throwIfAnyLocationIsNotAssignedToARegion(regions, locations_per_region.keys()) - - multiworld.regions += regions names: Dict[str, int] = {} if mission_order_type == 0: + + # Generating all regions and locations + for region_name in vanilla_mission_req_table.keys(): + regions.append(create_region(multiworld, player, locations_per_region, location_cache, region_name)) + multiworld.regions += regions + connect(multiworld, player, names, 'Menu', 'Liberation Day'), connect(multiworld, player, names, 'Liberation Day', 'The Outlaws', lambda state: state.has("Beat Liberation Day", player)), @@ -110,31 +94,32 @@ def create_regions(multiworld: MultiWorld, player: int, locations: Tuple[Locatio lambda state: state.has('Beat Gates of Hell', player) and ( state.has('Beat Shatter the Sky', player) or state.has('Beat Belly of the Beast', player))) - return vanilla_mission_req_table, 29, final_location + return vanilla_mission_req_table, 29, 'All-In: Victory' else: missions = [] + remove_prophecy = mission_order_type == 1 and not get_option_value(multiworld, player, "shuffle_protoss") + + final_mission = mission_pools[MissionPools.FINAL][0] + + # Determining if missions must be removed + mission_pool_size = sum(len(mission_pool) for mission_pool in mission_pools.values()) + removals = len(mission_order) - mission_pool_size + # Removing entire Prophecy chain on vanilla shuffled when not shuffling protoss + if remove_prophecy: + removals -= 4 + # Initial fill out of mission list and marking all-in mission for mission in mission_order: - if mission is None: + # Removing extra missions if mission pool is too small + if 0 < mission.removal_priority <= removals or mission.category == 'Prophecy' and remove_prophecy: missions.append(None) - elif mission.type == "all_in": + elif mission.type == MissionPools.FINAL: missions.append(final_mission) - elif mission.relegate and not get_option_value(multiworld, player, "shuffle_no_build"): - missions.append("no_build") else: missions.append(mission.type) - # Place Protoss Missions if we are not using ShuffleProtoss and are in Vanilla Shuffled - if get_option_value(multiworld, player, "shuffle_protoss") == 0 and mission_order_type == 1: - missions[22] = "A Sinister Turn" - mission_pools['medium'].remove("A Sinister Turn") - missions[23] = "Echoes of the Future" - mission_pools['medium'].remove("Echoes of the Future") - missions[24] = "In Utter Darkness" - mission_pools['hard'].remove("In Utter Darkness") - no_build_slots = [] easy_slots = [] medium_slots = [] @@ -144,79 +129,108 @@ def create_regions(multiworld: MultiWorld, player: int, locations: Tuple[Locatio for i in range(len(missions)): if missions[i] is None: continue - if missions[i] == "no_build": + if missions[i] == MissionPools.STARTER: no_build_slots.append(i) - elif missions[i] == "easy": + elif missions[i] == MissionPools.EASY: easy_slots.append(i) - elif missions[i] == "medium": + elif missions[i] == MissionPools.MEDIUM: medium_slots.append(i) - elif missions[i] == "hard": + elif missions[i] == MissionPools.HARD: hard_slots.append(i) # Add no_build missions to the pool and fill in no_build slots - missions_to_add = mission_pools['no_build'] + missions_to_add = mission_pools[MissionPools.STARTER] + if len(no_build_slots) > len(missions_to_add): + raise Exception("There are no valid No-Build missions. Please exclude fewer missions.") for slot in no_build_slots: filler = multiworld.random.randint(0, len(missions_to_add) - 1) missions[slot] = missions_to_add.pop(filler) # Add easy missions into pool and fill in easy slots - missions_to_add = missions_to_add + mission_pools['easy'] + missions_to_add = missions_to_add + mission_pools[MissionPools.EASY] + if len(easy_slots) > len(missions_to_add): + raise Exception("There are not enough Easy missions to fill the campaign. Please exclude fewer missions.") for slot in easy_slots: filler = multiworld.random.randint(0, len(missions_to_add) - 1) missions[slot] = missions_to_add.pop(filler) # Add medium missions into pool and fill in medium slots - missions_to_add = missions_to_add + mission_pools['medium'] + missions_to_add = missions_to_add + mission_pools[MissionPools.MEDIUM] + if len(medium_slots) > len(missions_to_add): + raise Exception("There are not enough Easy and Medium missions to fill the campaign. Please exclude fewer missions.") for slot in medium_slots: filler = multiworld.random.randint(0, len(missions_to_add) - 1) missions[slot] = missions_to_add.pop(filler) # Add hard missions into pool and fill in hard slots - missions_to_add = missions_to_add + mission_pools['hard'] + missions_to_add = missions_to_add + mission_pools[MissionPools.HARD] + if len(hard_slots) > len(missions_to_add): + raise Exception("There are not enough missions to fill the campaign. Please exclude fewer missions.") for slot in hard_slots: filler = multiworld.random.randint(0, len(missions_to_add) - 1) missions[slot] = missions_to_add.pop(filler) + # Generating regions and locations from selected missions + for region_name in missions: + regions.append(create_region(multiworld, player, locations_per_region, location_cache, region_name)) + multiworld.regions += regions + + # Mapping original mission slots to shifted mission slots when missions are removed + slot_map = [] + slot_offset = 0 + for position, mission in enumerate(missions): + slot_map.append(position - slot_offset + 1) + if mission is None: + slot_offset += 1 + # Loop through missions to create requirements table and connect regions # TODO: Handle 'and' connections mission_req_table = {} - for i in range(len(missions)): + + for i, mission in enumerate(missions): + if mission is None: + continue connections = [] for connection in mission_order[i].connect_to: + required_mission = missions[connection] if connection == -1: - connect(multiworld, player, names, "Menu", missions[i]) + connect(multiworld, player, names, "Menu", mission) + elif required_mission is None: + continue else: - connect(multiworld, player, names, missions[connection], missions[i], + connect(multiworld, player, names, required_mission, mission, (lambda name, missions_req: (lambda state: state.has(f"Beat {name}", player) and state._sc2wol_cleared_missions(multiworld, player, missions_req))) (missions[connection], mission_order[i].number)) - connections.append(connection + 1) + connections.append(slot_map[connection]) - mission_req_table.update({missions[i]: MissionInfo( - vanilla_mission_req_table[missions[i]].id, connections, mission_order[i].category, + mission_req_table.update({mission: MissionInfo( + vanilla_mission_req_table[mission].id, connections, mission_order[i].category, number=mission_order[i].number, completion_critical=mission_order[i].completion_critical, or_requirements=mission_order[i].or_requirements)}) final_mission_id = vanilla_mission_req_table[final_mission].id - return mission_req_table, final_mission_id, final_mission + ': Victory' + # Changing the completion condition for alternate final missions into an event + if final_mission != 'All-In': + final_location = alt_final_mission_locations[final_mission] + # Final location should be near the end of the cache + for i in range(len(location_cache) - 1, -1, -1): + if location_cache[i].name == final_location: + location_cache[i].locked = True + location_cache[i].event = True + location_cache[i].address = None + break + else: + final_location = 'All-In: Victory' -def throwIfAnyLocationIsNotAssignedToARegion(regions: List[Region], regionNames: Set[str]): - existingRegions = set() - - for region in regions: - existingRegions.add(region.name) - - if (regionNames - existingRegions): - raise Exception("Starcraft: the following regions are used in locations: {}, but no such region exists".format( - regionNames - existingRegions)) - + return mission_req_table, final_mission_id, final_location def create_location(player: int, location_data: LocationData, region: Region, location_cache: List[Location]) -> Location: diff --git a/worlds/sc2wol/__init__.py b/worlds/sc2wol/__init__.py index 878f3882dc..60de200804 100644 --- a/worlds/sc2wol/__init__.py +++ b/worlds/sc2wol/__init__.py @@ -7,10 +7,10 @@ from .Items import StarcraftWoLItem, item_table, filler_items, item_name_groups, get_basic_units from .Locations import get_locations from .Regions import create_regions -from .Options import sc2wol_options, get_option_value, get_option_set_value +from .Options import sc2wol_options, get_option_value from .LogicMixin import SC2WoLLogic from .PoolFilter import filter_missions, filter_items, get_item_upgrades -from .MissionTables import get_starting_mission_locations, MissionInfo +from .MissionTables import starting_mission_locations, MissionInfo class Starcraft2WoLWebWorld(WebWorld): @@ -137,7 +137,6 @@ def assign_starter_items(multiworld: MultiWorld, player: int, excluded_items: Se # The first world should also be the starting world first_mission = list(multiworld.worlds[player].mission_req_table)[0] - starting_mission_locations = get_starting_mission_locations(multiworld, player) if first_mission in starting_mission_locations: first_location = starting_mission_locations[first_mission] elif first_mission == "In Utter Darkness": @@ -174,7 +173,7 @@ def get_item_pool(multiworld: MultiWorld, player: int, mission_req_table: Dict[s locked_items = [] # YAML items - yaml_locked_items = get_option_set_value(multiworld, player, 'locked_items') + yaml_locked_items = get_option_value(multiworld, player, 'locked_items') for name, data in item_table.items(): if name not in excluded_items: diff --git a/worlds/sm/Regions.py b/worlds/sm/Regions.py index 966366e62f..f8ef908889 100644 --- a/worlds/sm/Regions.py +++ b/worlds/sm/Regions.py @@ -1,8 +1,8 @@ def create_regions(self, world, player: int): from . import create_region from BaseClasses import Entrance - from logic.logic import Logic - from graph.vanilla.graph_locations import locationsDict + from worlds.sm.variaRandomizer.logic.logic import Logic + from worlds.sm.variaRandomizer.graph.vanilla.graph_locations import locationsDict regions = [] for accessPoint in Logic.accessPoints: diff --git a/worlds/sm/Rules.py b/worlds/sm/Rules.py index 54468a40f2..bce9247342 100644 --- a/worlds/sm/Rules.py +++ b/worlds/sm/Rules.py @@ -1,10 +1,7 @@ from ..generic.Rules import set_rule, add_rule -from graph.vanilla.graph_locations import locationsDict -from graph.graph_utils import vanillaTransitions, getAccessPoint -from logic.logic import Logic -from rom.rom_patches import RomPatches -from utils.doorsmanager import DoorsManager +from worlds.sm.variaRandomizer.graph.vanilla.graph_locations import locationsDict +from worlds.sm.variaRandomizer.logic.logic import Logic def evalSMBool(smbool, maxDiff): return smbool.bool == True and smbool.difficulty <= maxDiff diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 43edf35c56..255551c267 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -7,8 +7,6 @@ import threading import base64 from typing import Any, Dict, Iterable, List, Set, TextIO, TypedDict -from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils - logger = logging.getLogger("Super Metroid") from .Regions import create_regions @@ -21,16 +19,17 @@ import Utils from BaseClasses import Region, Entrance, Location, MultiWorld, Item, ItemClassification, CollectionState, Tutorial from ..AutoWorld import World, AutoLogicRegister, WebWorld -from logic.smboolmanager import SMBoolManager -from graph.vanilla.graph_locations import locationsDict -from graph.graph_utils import getAccessPoint -from rando.ItemLocContainer import ItemLocation -from rando.Items import ItemManager -from utils.parameters import * -from logic.logic import Logic -from randomizer import VariaRandomizer -from utils.doorsmanager import DoorsManager -from rom.rom_patches import RomPatches +from worlds.sm.variaRandomizer.logic.smboolmanager import SMBoolManager +from worlds.sm.variaRandomizer.graph.vanilla.graph_locations import locationsDict +from worlds.sm.variaRandomizer.graph.graph_utils import getAccessPoint +from worlds.sm.variaRandomizer.rando.ItemLocContainer import ItemLocation +from worlds.sm.variaRandomizer.rando.Items import ItemManager +from worlds.sm.variaRandomizer.utils.parameters import * +from worlds.sm.variaRandomizer.logic.logic import Logic +from worlds.sm.variaRandomizer.randomizer import VariaRandomizer +from worlds.sm.variaRandomizer.utils.doorsmanager import DoorsManager +from worlds.sm.variaRandomizer.rom.rom_patches import RomPatches +from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils class SMCollectionState(metaclass=AutoLogicRegister): diff --git a/worlds/sm/docs/multiworld_en.md b/worlds/sm/docs/multiworld_en.md index 826be60188..77ec660dfe 100644 --- a/worlds/sm/docs/multiworld_en.md +++ b/worlds/sm/docs/multiworld_en.md @@ -100,7 +100,7 @@ the lua you are using in your file explorer and copy the `socket.dll` to the bas Once you have changed the loaded core, you must restart BizHawk. 2. Load your ROM file if it hasn't already been loaded. 3. Click on the Tools menu and click on **Lua Console** -4. Click the button to open a new Lua script. +4. Click the Open Folder icon that says `Open Script` via the tooltip on mouse hover, or click the Script Menu then `Open Script...`, or press `Ctrl-O`. 5. Select the `Connector.lua` file included with your client - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the emulator is 64-bit or 32-bit. Please note the most recent versions of BizHawk are 64-bit only. diff --git a/worlds/sm/variaRandomizer/LICENSE b/worlds/sm/variaRandomizer/LICENSE index 9cecc1d466..421c53a33b 100644 --- a/worlds/sm/variaRandomizer/LICENSE +++ b/worlds/sm/variaRandomizer/LICENSE @@ -1,674 +1,21 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - {one line to give the program's name and a brief idea of what it does.} - Copyright (C) {year} {name of author} - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - {project} Copyright (C) {year} {fullname} - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. +MIT License + +Copyright (c) 2022 dude & flo and others + +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. diff --git a/worlds/sm/variaRandomizer/__init__.py b/worlds/sm/variaRandomizer/__init__.py index 6aed1dfd5a..e69de29bb2 100644 --- a/worlds/sm/variaRandomizer/__init__.py +++ b/worlds/sm/variaRandomizer/__init__.py @@ -1,3 +0,0 @@ -import sys -import os -sys.path.append(os.path.dirname(os.path.abspath(__file__))) \ No newline at end of file diff --git a/worlds/sm/variaRandomizer/graph/graph.py b/worlds/sm/variaRandomizer/graph/graph.py index bcbf138123..bf9af48dc9 100644 --- a/worlds/sm/variaRandomizer/graph/graph.py +++ b/worlds/sm/variaRandomizer/graph/graph.py @@ -1,9 +1,9 @@ import copy, logging from operator import attrgetter -import utils.log -from logic.smbool import SMBool, smboolFalse -from utils.parameters import infinity -from logic.helpers import Bosses +from worlds.sm.variaRandomizer.utils import log +from worlds.sm.variaRandomizer.logic.smbool import SMBool, smboolFalse +from worlds.sm.variaRandomizer.utils.parameters import infinity +from worlds.sm.variaRandomizer.logic.helpers import Bosses class Path(object): __slots__ = ( 'path', 'pdiff', 'distance' ) @@ -106,7 +106,7 @@ class AccessGraph(object): 'availAccessPoints' ) def __init__(self, accessPointList, transitions, dotFile=None): - self.log = utils.log.get('Graph') + self.log = log.get('Graph') self.accessPoints = {} self.InterAreaTransitions = [] self.EscapeAttributes = { diff --git a/worlds/sm/variaRandomizer/graph/graph_utils.py b/worlds/sm/variaRandomizer/graph/graph_utils.py index 75d494a65b..e147da5e57 100644 --- a/worlds/sm/variaRandomizer/graph/graph_utils.py +++ b/worlds/sm/variaRandomizer/graph/graph_utils.py @@ -1,10 +1,10 @@ import copy import random -from logic.logic import Logic -from utils.parameters import Knows -from graph.location import locationsDict -from rom.rom import snes_to_pc -import utils.log +from worlds.sm.variaRandomizer.logic.logic import Logic +from worlds.sm.variaRandomizer.utils.parameters import Knows +from worlds.sm.variaRandomizer.graph.location import locationsDict +from worlds.sm.variaRandomizer.rom.rom import snes_to_pc +from worlds.sm.variaRandomizer.utils import log # order expected by ROM patches graphAreas = [ @@ -89,7 +89,7 @@ def getAccessPoint(apName, apList=None): return next(ap for ap in apList if ap.Name == apName) class GraphUtils: - log = utils.log.get('GraphUtils') + log = log.get('GraphUtils') def getStartAccessPointNames(): return [ap.Name for ap in Logic.accessPoints if ap.Start is not None] diff --git a/worlds/sm/variaRandomizer/graph/location.py b/worlds/sm/variaRandomizer/graph/location.py index f60158c1db..92eb1ccbca 100644 --- a/worlds/sm/variaRandomizer/graph/location.py +++ b/worlds/sm/variaRandomizer/graph/location.py @@ -1,4 +1,4 @@ -from utils.parameters import infinity +from worlds.sm.variaRandomizer.utils.parameters import infinity import copy class Location: diff --git a/worlds/sm/variaRandomizer/graph/vanilla/graph_access.py b/worlds/sm/variaRandomizer/graph/vanilla/graph_access.py index b74b69026e..279f249e86 100644 --- a/worlds/sm/variaRandomizer/graph/vanilla/graph_access.py +++ b/worlds/sm/variaRandomizer/graph/vanilla/graph_access.py @@ -1,9 +1,9 @@ -from graph.graph import AccessPoint -from utils.parameters import Settings -from rom.rom_patches import RomPatches -from logic.smbool import SMBool -from logic.helpers import Bosses -from logic.cache import Cache +from worlds.sm.variaRandomizer.graph.graph import AccessPoint +from worlds.sm.variaRandomizer.utils.parameters import Settings +from worlds.sm.variaRandomizer.rom.rom_patches import RomPatches +from worlds.sm.variaRandomizer.logic.smbool import SMBool +from worlds.sm.variaRandomizer.logic.helpers import Bosses +from worlds.sm.variaRandomizer.logic.cache import Cache # all access points and traverse functions accessPoints = [ diff --git a/worlds/sm/variaRandomizer/graph/vanilla/graph_helpers.py b/worlds/sm/variaRandomizer/graph/vanilla/graph_helpers.py index f189a47606..41ffe51192 100644 --- a/worlds/sm/variaRandomizer/graph/vanilla/graph_helpers.py +++ b/worlds/sm/variaRandomizer/graph/vanilla/graph_helpers.py @@ -1,11 +1,11 @@ from math import ceil -from logic.smbool import SMBool -from logic.helpers import Helpers, Bosses -from logic.cache import Cache -from rom.rom_patches import RomPatches -from graph.graph_utils import getAccessPoint -from utils.parameters import Settings +from worlds.sm.variaRandomizer.logic.smbool import SMBool +from worlds.sm.variaRandomizer.logic.helpers import Helpers, Bosses +from worlds.sm.variaRandomizer.logic.cache import Cache +from worlds.sm.variaRandomizer.rom.rom_patches import RomPatches +from worlds.sm.variaRandomizer.graph.graph_utils import getAccessPoint +from worlds.sm.variaRandomizer.utils.parameters import Settings class HelpersGraph(Helpers): def __init__(self, smbm): diff --git a/worlds/sm/variaRandomizer/graph/vanilla/graph_locations.py b/worlds/sm/variaRandomizer/graph/vanilla/graph_locations.py index 671368e831..62eaf3c0fe 100644 --- a/worlds/sm/variaRandomizer/graph/vanilla/graph_locations.py +++ b/worlds/sm/variaRandomizer/graph/vanilla/graph_locations.py @@ -1,8 +1,8 @@ -from logic.helpers import Bosses -from utils.parameters import Settings -from rom.rom_patches import RomPatches -from logic.smbool import SMBool -from graph.location import locationsDict +from worlds.sm.variaRandomizer.logic.helpers import Bosses +from worlds.sm.variaRandomizer.utils.parameters import Settings +from worlds.sm.variaRandomizer.rom.rom_patches import RomPatches +from worlds.sm.variaRandomizer.logic.smbool import SMBool +from worlds.sm.variaRandomizer.graph.location import locationsDict locationsDict["Energy Tank, Gauntlet"].AccessFrom = { 'Landing Site': lambda sm: SMBool(True) diff --git a/worlds/sm/variaRandomizer/logic/helpers.py b/worlds/sm/variaRandomizer/logic/helpers.py index 4df4665770..3f8720d84f 100644 --- a/worlds/sm/variaRandomizer/logic/helpers.py +++ b/worlds/sm/variaRandomizer/logic/helpers.py @@ -1,11 +1,11 @@ import math -from logic.cache import Cache -from logic.smbool import SMBool, smboolFalse -from utils.parameters import Settings, easy, medium, diff2text -from rom.rom_patches import RomPatches -from utils.utils import normalizeRounding +from worlds.sm.variaRandomizer.logic.cache import Cache +from worlds.sm.variaRandomizer.logic.smbool import SMBool, smboolFalse +from worlds.sm.variaRandomizer.utils.parameters import Settings, easy, medium, diff2text +from worlds.sm.variaRandomizer.rom.rom_patches import RomPatches +from worlds.sm.variaRandomizer.utils.utils import normalizeRounding class Helpers(object): diff --git a/worlds/sm/variaRandomizer/logic/logic.py b/worlds/sm/variaRandomizer/logic/logic.py index 5d47932b78..6ce20406b9 100644 --- a/worlds/sm/variaRandomizer/logic/logic.py +++ b/worlds/sm/variaRandomizer/logic/logic.py @@ -4,20 +4,20 @@ class Logic(object): @staticmethod def factory(implementation): if implementation == 'vanilla': - from graph.vanilla.graph_helpers import HelpersGraph - from graph.vanilla.graph_access import accessPoints - from graph.vanilla.graph_locations import locations - from graph.vanilla.graph_locations import LocationsHelper + from worlds.sm.variaRandomizer.graph.vanilla.graph_helpers import HelpersGraph + from worlds.sm.variaRandomizer.graph.vanilla.graph_access import accessPoints + from worlds.sm.variaRandomizer.graph.vanilla.graph_locations import locations + from worlds.sm.variaRandomizer.graph.vanilla.graph_locations import LocationsHelper Logic.locations = locations Logic.accessPoints = accessPoints Logic.HelpersGraph = HelpersGraph Logic.patches = implementation Logic.LocationsHelper = LocationsHelper elif implementation == 'rotation': - from graph.rotation.graph_helpers import HelpersGraph - from graph.rotation.graph_access import accessPoints - from graph.rotation.graph_locations import locations - from graph.rotation.graph_locations import LocationsHelper + from worlds.sm.variaRandomizer.graph.rotation.graph_helpers import HelpersGraph + from worlds.sm.variaRandomizer.graph.rotation.graph_access import accessPoints + from worlds.sm.variaRandomizer.graph.rotation.graph_locations import locations + from worlds.sm.variaRandomizer.graph.rotation.graph_locations import LocationsHelper Logic.locations = locations Logic.accessPoints = accessPoints Logic.HelpersGraph = HelpersGraph diff --git a/worlds/sm/variaRandomizer/logic/smboolmanager.py b/worlds/sm/variaRandomizer/logic/smboolmanager.py index c09455fdaa..a351e163fa 100644 --- a/worlds/sm/variaRandomizer/logic/smboolmanager.py +++ b/worlds/sm/variaRandomizer/logic/smboolmanager.py @@ -1,11 +1,11 @@ # object to handle the smbools and optimize them -from logic.cache import Cache -from logic.smbool import SMBool, smboolFalse -from logic.helpers import Bosses -from logic.logic import Logic -from utils.doorsmanager import DoorsManager -from utils.parameters import Knows, isKnows +from worlds.sm.variaRandomizer.logic.cache import Cache +from worlds.sm.variaRandomizer.logic.smbool import SMBool, smboolFalse +from worlds.sm.variaRandomizer.logic.helpers import Bosses +from worlds.sm.variaRandomizer.logic.logic import Logic +from worlds.sm.variaRandomizer.utils.doorsmanager import DoorsManager +from worlds.sm.variaRandomizer.utils.parameters import Knows, isKnows import logging import sys diff --git a/worlds/sm/variaRandomizer/patches/patchaccess.py b/worlds/sm/variaRandomizer/patches/patchaccess.py index bce2d48658..9ed8317294 100644 --- a/worlds/sm/variaRandomizer/patches/patchaccess.py +++ b/worlds/sm/variaRandomizer/patches/patchaccess.py @@ -1,7 +1,7 @@ import os, importlib -from logic.logic import Logic -from patches.common.patches import patches, additional_PLMs -from utils.parameters import appDir +from worlds.sm.variaRandomizer.logic.logic import Logic +from worlds.sm.variaRandomizer.patches.common.patches import patches, additional_PLMs +from worlds.sm.variaRandomizer.utils.parameters import appDir class PatchAccess(object): def __init__(self): @@ -16,12 +16,12 @@ class PatchAccess(object): # load dict patches self.dictPatches = patches - logicPatches = importlib.import_module("patches.{}.patches".format(Logic.patches)).patches + logicPatches = importlib.import_module("worlds.sm.variaRandomizer.patches.{}.patches".format(Logic.patches)).patches self.dictPatches.update(logicPatches) # load additional PLMs self.additionalPLMs = additional_PLMs - logicPLMs = importlib.import_module("patches.{}.patches".format(Logic.patches)).additional_PLMs + logicPLMs = importlib.import_module("worlds.sm.variaRandomizer.patches.{}.patches".format(Logic.patches)).additional_PLMs self.additionalPLMs.update(logicPLMs) def getPatchPath(self, patch): diff --git a/worlds/sm/variaRandomizer/rando/Choice.py b/worlds/sm/variaRandomizer/rando/Choice.py index 3cb5667755..e200448b43 100644 --- a/worlds/sm/variaRandomizer/rando/Choice.py +++ b/worlds/sm/variaRandomizer/rando/Choice.py @@ -1,13 +1,14 @@ -import utils.log, random -from utils.utils import getRangeDict, chooseFromRange -from rando.ItemLocContainer import ItemLocation +import random +from worlds.sm.variaRandomizer.utils import log +from worlds.sm.variaRandomizer.utils.utils import getRangeDict, chooseFromRange +from worlds.sm.variaRandomizer.rando.ItemLocContainer import ItemLocation # helper object to choose item/loc class Choice(object): def __init__(self, restrictions): self.restrictions = restrictions self.settings = restrictions.settings - self.log = utils.log.get("Choice") + self.log = log.get("Choice") # args are return from RandoServices.getPossiblePlacements # return itemLoc dict, or None if no possible choice diff --git a/worlds/sm/variaRandomizer/rando/Filler.py b/worlds/sm/variaRandomizer/rando/Filler.py index 3408aee057..733e7cdbbb 100644 --- a/worlds/sm/variaRandomizer/rando/Filler.py +++ b/worlds/sm/variaRandomizer/rando/Filler.py @@ -1,14 +1,14 @@ -import utils.log, copy, time, random - -from logic.cache import RequestCache -from rando.RandoServices import RandoServices -from rando.Choice import ItemThenLocChoice -from rando.RandoServices import ComebackCheckType -from rando.ItemLocContainer import ItemLocation, getItemLocationsStr -from utils.parameters import infinity -from logic.helpers import diffValue2txt -from graph.graph_utils import GraphUtils +import copy, time, random +from worlds.sm.variaRandomizer.utils import log +from worlds.sm.variaRandomizer.logic.cache import RequestCache +from worlds.sm.variaRandomizer.rando.RandoServices import RandoServices +from worlds.sm.variaRandomizer.rando.Choice import ItemThenLocChoice +from worlds.sm.variaRandomizer.rando.RandoServices import ComebackCheckType +from worlds.sm.variaRandomizer.rando.ItemLocContainer import ItemLocation, getItemLocationsStr +from worlds.sm.variaRandomizer.utils.parameters import infinity +from worlds.sm.variaRandomizer.logic.helpers import diffValue2txt +from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils # base class for fillers. a filler responsibility is to fill a given # ItemLocContainer while a certain condition is fulfilled (usually @@ -25,7 +25,7 @@ class Filler(object): self.endDate = endDate self.baseContainer = emptyContainer self.maxDiff = self.settings.maxDiff - self.log = utils.log.get('Filler') + self.log = log.get('Filler') # reinit algo state def initFiller(self): diff --git a/worlds/sm/variaRandomizer/rando/GraphBuilder.py b/worlds/sm/variaRandomizer/rando/GraphBuilder.py index 3670550e16..6eeb1d865c 100644 --- a/worlds/sm/variaRandomizer/rando/GraphBuilder.py +++ b/worlds/sm/variaRandomizer/rando/GraphBuilder.py @@ -1,9 +1,9 @@ -import utils.log, random, copy - -from graph.graph_utils import GraphUtils, vanillaTransitions, vanillaBossesTransitions, escapeSource, escapeTargets -from logic.logic import Logic -from graph.graph import AccessGraphRando as AccessGraph +import random, copy +from worlds.sm.variaRandomizer.utils import log +from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils, vanillaTransitions, vanillaBossesTransitions, escapeSource, escapeTargets +from worlds.sm.variaRandomizer.logic.logic import Logic +from worlds.sm.variaRandomizer.graph.graph import AccessGraphRando as AccessGraph # creates graph and handles randomized escape class GraphBuilder(object): @@ -13,7 +13,7 @@ class GraphBuilder(object): self.bossRando = graphSettings.bossRando self.escapeRando = graphSettings.escapeRando self.minimizerN = graphSettings.minimizerN - self.log = utils.log.get('GraphBuilder') + self.log = log.get('GraphBuilder') # builds everything but escape transitions def createGraph(self): diff --git a/worlds/sm/variaRandomizer/rando/ItemLocContainer.py b/worlds/sm/variaRandomizer/rando/ItemLocContainer.py index 1ab43355c1..859fe5503f 100644 --- a/worlds/sm/variaRandomizer/rando/ItemLocContainer.py +++ b/worlds/sm/variaRandomizer/rando/ItemLocContainer.py @@ -1,8 +1,8 @@ -import copy, utils.log - -from logic.smbool import SMBool, smboolFalse -from logic.smboolmanager import SMBoolManager +import copy +from worlds.sm.variaRandomizer.utils import log +from worlds.sm.variaRandomizer.logic.smbool import SMBool, smboolFalse +from worlds.sm.variaRandomizer.logic.smboolmanager import SMBoolManager from collections import Counter class ItemLocation(object): @@ -58,7 +58,7 @@ class ItemLocContainer(object): self.itemPool = itemPool self.itemPoolBackup = None self.unrestrictedItems = set() - self.log = utils.log.get('ItemLocContainer') + self.log = log.get('ItemLocContainer') self.checkConsistency() def checkConsistency(self): diff --git a/worlds/sm/variaRandomizer/rando/Items.py b/worlds/sm/variaRandomizer/rando/Items.py index b7dacca40e..6c4d35119e 100644 --- a/worlds/sm/variaRandomizer/rando/Items.py +++ b/worlds/sm/variaRandomizer/rando/Items.py @@ -1,5 +1,6 @@ -from utils.utils import randGaussBounds, getRangeDict, chooseFromRange -import utils.log, logging, copy, random +from worlds.sm.variaRandomizer.utils.utils import randGaussBounds, getRangeDict, chooseFromRange +from worlds.sm.variaRandomizer.utils import log +import logging, copy, random class Item: __slots__ = ( 'Category', 'Class', 'Name', 'Code', 'Type', 'BeamBits', 'ItemBits', 'Id' ) @@ -392,7 +393,7 @@ class ItemPoolGenerator(object): self.maxItems = 105 # 100 item locs and 5 bosses self.maxEnergy = 18 # 14E, 4R self.maxDiff = maxDiff - self.log = utils.log.get('ItemPool') + self.log = log.get('ItemPool') def isUltraSparseNoTanks(self): # if low stuff botwoon is not known there is a hard energy req of one tank, even diff --git a/worlds/sm/variaRandomizer/rando/RandoExec.py b/worlds/sm/variaRandomizer/rando/RandoExec.py index bf440f044c..9ff6fc2d99 100644 --- a/worlds/sm/variaRandomizer/rando/RandoExec.py +++ b/worlds/sm/variaRandomizer/rando/RandoExec.py @@ -1,15 +1,16 @@ -import sys, random, time, utils.log +import sys, random, time -from logic.logic import Logic -from graph.graph_utils import GraphUtils, getAccessPoint -from rando.Restrictions import Restrictions -from rando.RandoServices import RandoServices -from rando.GraphBuilder import GraphBuilder -from rando.RandoSetup import RandoSetup -from rando.Items import ItemManager -from rando.ItemLocContainer import ItemLocation -from utils.vcr import VCR -from utils.doorsmanager import DoorsManager +from worlds.sm.variaRandomizer.utils import log +from worlds.sm.variaRandomizer.logic.logic import Logic +from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils, getAccessPoint +from worlds.sm.variaRandomizer.rando.Restrictions import Restrictions +from worlds.sm.variaRandomizer.rando.RandoServices import RandoServices +from worlds.sm.variaRandomizer.rando.GraphBuilder import GraphBuilder +from worlds.sm.variaRandomizer.rando.RandoSetup import RandoSetup +from worlds.sm.variaRandomizer.rando.Items import ItemManager +from worlds.sm.variaRandomizer.rando.ItemLocContainer import ItemLocation +from worlds.sm.variaRandomizer.utils.vcr import VCR +from worlds.sm.variaRandomizer.utils.doorsmanager import DoorsManager # entry point for rando execution ("randomize" method) class RandoExec(object): @@ -19,7 +20,7 @@ class RandoExec(object): self.vcr = vcr self.randoSettings = randoSettings self.graphSettings = graphSettings - self.log = utils.log.get('RandoExec') + self.log = log.get('RandoExec') self.player = player # processes settings to : diff --git a/worlds/sm/variaRandomizer/rando/RandoServices.py b/worlds/sm/variaRandomizer/rando/RandoServices.py index bcb076d120..6ea86c9e4a 100644 --- a/worlds/sm/variaRandomizer/rando/RandoServices.py +++ b/worlds/sm/variaRandomizer/rando/RandoServices.py @@ -1,9 +1,10 @@ -import utils.log, copy, random, sys, logging +import copy, random, sys, logging, os from enum import Enum, unique -from utils.parameters import infinity -from rando.ItemLocContainer import getLocListStr, getItemListStr, getItemLocStr, ItemLocation -from logic.helpers import Bosses +from worlds.sm.variaRandomizer.utils import log +from worlds.sm.variaRandomizer.utils.parameters import infinity +from worlds.sm.variaRandomizer.rando.ItemLocContainer import getLocListStr, getItemListStr, getItemLocStr, ItemLocation +from worlds.sm.variaRandomizer.logic.helpers import Bosses # used to specify whether we want to come back from locations @unique @@ -23,7 +24,7 @@ class RandoServices(object): self.settings = restrictions.settings self.areaGraph = graph self.cache = cache - self.log = utils.log.get('RandoServices') + self.log = log.get('RandoServices') # collect an item/loc with logic in a container from a given AP # return new AP diff --git a/worlds/sm/variaRandomizer/rando/RandoSettings.py b/worlds/sm/variaRandomizer/rando/RandoSettings.py index a2ce5908ed..030b14fff2 100644 --- a/worlds/sm/variaRandomizer/rando/RandoSettings.py +++ b/worlds/sm/variaRandomizer/rando/RandoSettings.py @@ -1,9 +1,9 @@ import sys, random from collections import defaultdict -from rando.Items import ItemManager -from utils.utils import getRangeDict, chooseFromRange -from rando.ItemLocContainer import ItemLocation +from worlds.sm.variaRandomizer.rando.Items import ItemManager +from worlds.sm.variaRandomizer.utils.utils import getRangeDict, chooseFromRange +from worlds.sm.variaRandomizer.rando.ItemLocContainer import ItemLocation # Holder for settings and a few utility functions related to them # (especially for plando/rando). diff --git a/worlds/sm/variaRandomizer/rando/RandoSetup.py b/worlds/sm/variaRandomizer/rando/RandoSetup.py index 0a73ad1dd0..c82802f8c1 100644 --- a/worlds/sm/variaRandomizer/rando/RandoSetup.py +++ b/worlds/sm/variaRandomizer/rando/RandoSetup.py @@ -1,14 +1,15 @@ -import copy, utils.log, random +import copy, random -from utils.utils import randGaussBounds -from logic.smbool import SMBool, smboolFalse -from logic.smboolmanager import SMBoolManager -from logic.helpers import Bosses -from graph.graph_utils import getAccessPoint, GraphUtils -from rando.Filler import FrontFiller -from rando.ItemLocContainer import ItemLocContainer, getLocListStr, ItemLocation, getItemListStr -from rando.Restrictions import Restrictions -from utils.parameters import infinity +from worlds.sm.variaRandomizer.utils import log +from worlds.sm.variaRandomizer.utils.utils import randGaussBounds +from worlds.sm.variaRandomizer.logic.smbool import SMBool, smboolFalse +from worlds.sm.variaRandomizer.logic.smboolmanager import SMBoolManager +from worlds.sm.variaRandomizer.logic.helpers import Bosses +from worlds.sm.variaRandomizer.graph.graph_utils import getAccessPoint, GraphUtils +from worlds.sm.variaRandomizer.rando.Filler import FrontFiller +from worlds.sm.variaRandomizer.rando.ItemLocContainer import ItemLocContainer, getLocListStr, ItemLocation, getItemListStr +from worlds.sm.variaRandomizer.rando.Restrictions import Restrictions +from worlds.sm.variaRandomizer.utils.parameters import infinity # checks init conditions for the randomizer: processes super fun settings, graph, start location, special restrictions # the entry point is createItemLocContainer @@ -49,7 +50,7 @@ class RandoSetup(object): # we have to use item manager only once, otherwise pool will change self.itemManager.createItemPool(exclude) self.basePool = self.itemManager.getItemPool()[:] - self.log = utils.log.get('RandoSetup') + self.log = log.get('RandoSetup') if len(locations) != len(self.locations): self.log.debug("inaccessible locations :"+getLocListStr([loc for loc in locations if loc not in self.locations])) diff --git a/worlds/sm/variaRandomizer/rando/Restrictions.py b/worlds/sm/variaRandomizer/rando/Restrictions.py index 2f932ecce2..953eb2ef06 100644 --- a/worlds/sm/variaRandomizer/rando/Restrictions.py +++ b/worlds/sm/variaRandomizer/rando/Restrictions.py @@ -1,13 +1,13 @@ -import copy, random, utils.log - -from graph.graph_utils import getAccessPoint -from rando.ItemLocContainer import getLocListStr +import copy, random +from worlds.sm.variaRandomizer.utils import log +from worlds.sm.variaRandomizer.graph.graph_utils import getAccessPoint +from worlds.sm.variaRandomizer.rando.ItemLocContainer import getLocListStr # Holds settings related to item placement restrictions. # canPlaceAtLocation is the main entry point here class Restrictions(object): def __init__(self, settings): - self.log = utils.log.get('Restrictions') + self.log = log.get('Restrictions') self.settings = settings # Item split : Major, Chozo, Full, Scavenger self.split = settings.restrictions['MajorMinor'] diff --git a/worlds/sm/variaRandomizer/randomizer.py b/worlds/sm/variaRandomizer/randomizer.py index 854e76b3db..6a2c33ca4e 100644 --- a/worlds/sm/variaRandomizer/randomizer.py +++ b/worlds/sm/variaRandomizer/randomizer.py @@ -3,19 +3,18 @@ from Utils import output_path import argparse, os.path, json, sys, shutil, random, copy, requests -from rando.RandoSettings import RandoSettings, GraphSettings -from rando.RandoExec import RandoExec -from graph.graph_utils import vanillaTransitions, vanillaBossesTransitions, GraphUtils, getAccessPoint -from utils.parameters import Knows, Controller, easy, medium, hard, harder, hardcore, mania, infinity, text2diff, diff2text, appDir -from rom.rom_patches import RomPatches -from rom.rompatcher import RomPatcher -from utils.utils import PresetLoader, loadRandoPreset, getDefaultMultiValues, getPresetDir -from utils.version import displayedVersion -from logic.smbool import SMBool -from utils.doorsmanager import DoorsManager -from logic.logic import Logic +from worlds.sm.variaRandomizer.rando.RandoSettings import RandoSettings, GraphSettings +from worlds.sm.variaRandomizer.rando.RandoExec import RandoExec +from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils, getAccessPoint +from worlds.sm.variaRandomizer.utils.parameters import Controller, easy, medium, hard, harder, hardcore, mania, infinity, text2diff, appDir +from worlds.sm.variaRandomizer.rom.rom_patches import RomPatches +from worlds.sm.variaRandomizer.rom.rompatcher import RomPatcher +from worlds.sm.variaRandomizer.utils.utils import PresetLoader, loadRandoPreset, getDefaultMultiValues, getPresetDir +from worlds.sm.variaRandomizer.utils.version import displayedVersion +from worlds.sm.variaRandomizer.utils.doorsmanager import DoorsManager +from worlds.sm.variaRandomizer.logic.logic import Logic -import utils.log +from worlds.sm.variaRandomizer.utils import log from worlds.sm.Options import StartLocation # we need to know the logic before doing anything else @@ -304,8 +303,8 @@ class VariaRandomizer: print("plandoRando param requires output param") sys.exit(-1) - utils.log.init(args.debug) - logger = utils.log.get('Rando') + log.init(args.debug) + logger = log.get('Rando') Logic.factory(args.logic) diff --git a/worlds/sm/variaRandomizer/rom/ips.py b/worlds/sm/variaRandomizer/rom/ips.py index dcce44a2cb..34a41e2ecf 100644 --- a/worlds/sm/variaRandomizer/rom/ips.py +++ b/worlds/sm/variaRandomizer/rom/ips.py @@ -1,6 +1,6 @@ import itertools -from utils.utils import range_union +from worlds.sm.variaRandomizer.utils.utils import range_union # adapted from ips-util for python 3.2 (https://pypi.org/project/ips-util/) class IPS_Patch(object): diff --git a/worlds/sm/variaRandomizer/rom/rom.py b/worlds/sm/variaRandomizer/rom/rom.py index 52b1fd0460..7b1cf06ffc 100644 --- a/worlds/sm/variaRandomizer/rom/rom.py +++ b/worlds/sm/variaRandomizer/rom/rom.py @@ -1,6 +1,6 @@ import base64 -from rom.ips import IPS_Patch +from worlds.sm.variaRandomizer.rom.ips import IPS_Patch def pc_to_snes(pcaddress): snesaddress=(((pcaddress<<1)&0x7F0000)|(pcaddress&0x7FFF)|0x8000)|0x800000 diff --git a/worlds/sm/variaRandomizer/rom/rom_patches.py b/worlds/sm/variaRandomizer/rom/rom_patches.py index 9fe68c9f9a..26e8a84e94 100644 --- a/worlds/sm/variaRandomizer/rom/rom_patches.py +++ b/worlds/sm/variaRandomizer/rom/rom_patches.py @@ -1,4 +1,4 @@ -from logic.smbool import SMBool +from worlds.sm.variaRandomizer.logic.smbool import SMBool # layout patches added by randomizers class RomPatches: diff --git a/worlds/sm/variaRandomizer/rom/rompatcher.py b/worlds/sm/variaRandomizer/rom/rompatcher.py index 9dae1cea87..85cd3bf312 100644 --- a/worlds/sm/variaRandomizer/rom/rompatcher.py +++ b/worlds/sm/variaRandomizer/rom/rompatcher.py @@ -1,14 +1,13 @@ import os, random, re - -from rando.Items import ItemManager -from rom.ips import IPS_Patch -from utils.doorsmanager import DoorsManager -from graph.graph_utils import GraphUtils, getAccessPoint, locIdsByAreaAddresses -from logic.logic import Logic -from rom.rom import RealROM, snes_to_pc -from patches.patchaccess import PatchAccess -from utils.parameters import appDir -import utils.log +from worlds.sm.variaRandomizer.rando.Items import ItemManager +from worlds.sm.variaRandomizer.rom.ips import IPS_Patch +from worlds.sm.variaRandomizer.utils.doorsmanager import DoorsManager +from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils, getAccessPoint, locIdsByAreaAddresses +from worlds.sm.variaRandomizer.logic.logic import Logic +from worlds.sm.variaRandomizer.rom.rom import RealROM, snes_to_pc, pc_to_snes +from worlds.sm.variaRandomizer.patches.patchaccess import PatchAccess +from worlds.sm.variaRandomizer.utils.parameters import appDir +from worlds.sm.variaRandomizer.utils import log def getWord(w): return (w & 0x00FF, (w & 0xFF00) >> 8) @@ -57,7 +56,7 @@ class RomPatcher: } def __init__(self, romFileName=None, magic=None, plando=False, player=0): - self.log = utils.log.get('RomPatcher') + self.log = log.get('RomPatcher') self.romFileName = romFileName self.race = None self.romFile = RealROM(romFileName) diff --git a/worlds/sm/variaRandomizer/utils/doorsmanager.py b/worlds/sm/variaRandomizer/utils/doorsmanager.py index bb2f5f6121..c8feacdbab 100644 --- a/worlds/sm/variaRandomizer/utils/doorsmanager.py +++ b/worlds/sm/variaRandomizer/utils/doorsmanager.py @@ -1,10 +1,11 @@ import random import copy -from logic.smbool import SMBool -from rom.rom_patches import RomPatches -import utils.log, logging +from worlds.sm.variaRandomizer.logic.smbool import SMBool +from worlds.sm.variaRandomizer.rom.rom_patches import RomPatches +import logging -LOG = utils.log.get('DoorsManager') +from worlds.sm.variaRandomizer.utils import log +LOG = log.get('DoorsManager') colorsList = ['red', 'green', 'yellow', 'wave', 'spazer', 'plasma', 'ice'] # 1/15 chance to have the door set to grey diff --git a/worlds/sm/variaRandomizer/utils/parameters.py b/worlds/sm/variaRandomizer/utils/parameters.py index 0f7b62c6b9..6bae03b465 100644 --- a/worlds/sm/variaRandomizer/utils/parameters.py +++ b/worlds/sm/variaRandomizer/utils/parameters.py @@ -1,4 +1,4 @@ -from logic.smbool import SMBool +from worlds.sm.variaRandomizer.logic.smbool import SMBool import os import sys from pathlib import Path diff --git a/worlds/sm/variaRandomizer/utils/utils.py b/worlds/sm/variaRandomizer/utils/utils.py index 402c629919..ba43d710a3 100644 --- a/worlds/sm/variaRandomizer/utils/utils.py +++ b/worlds/sm/variaRandomizer/utils/utils.py @@ -1,8 +1,8 @@ import os, json, re, random -from utils.parameters import Knows, Settings, Controller, isKnows, isSettings, isButton -from utils.parameters import easy, medium, hard, harder, hardcore, mania, text2diff -from logic.smbool import SMBool +from worlds.sm.variaRandomizer.utils.parameters import Knows, Settings, Controller, isKnows, isSettings, isButton +from worlds.sm.variaRandomizer.utils.parameters import easy, medium, hard, harder, hardcore, mania, text2diff +from worlds.sm.variaRandomizer.logic.smbool import SMBool def isStdPreset(preset): return preset in ['newbie', 'casual', 'regular', 'veteran', 'expert', 'master', 'samus', 'solution', 'Season_Races', 'SMRAT2021'] @@ -264,7 +264,7 @@ class PresetLoaderDict(PresetLoader): super(PresetLoaderDict, self).__init__() def getDefaultMultiValues(): - from graph.graph_utils import GraphUtils + from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils defaultMultiValues = { 'startLocation': GraphUtils.getStartAccessPointNames(), 'majorsSplit': ['Full', 'FullWithHUD', 'Major', 'Chozo', 'Scavenger'], diff --git a/worlds/sm64ex/docs/setup_en.md b/worlds/sm64ex/docs/setup_en.md index acf9432fe5..1ff8b5e938 100644 --- a/worlds/sm64ex/docs/setup_en.md +++ b/worlds/sm64ex/docs/setup_en.md @@ -67,6 +67,62 @@ Failing to use a new file may make some locations unavailable. However, this can To play offline, first generate a seed on the game's settings page. Create a room and download the `.apsm64ex` file, and start the game with the `--sm64ap_file "path/to/FileName"` launch argument. +# Optional: Using Batch Files to play offline and MultiWorld games + +As an alternative to launching the game with sm64pclauncher, it is also possible to launch the completed build with the use of Windows batch files. This has the added benefit of streamlining the join process so that manual editing of connection info is not needed for each new game. However, you'll need to be somewhat comfortable with creating and using batch files. + +IMPORTANT NOTE: The remainder of this section uses copy-and-paste code that assumes you're using the US version. If you instead use the Japanese version, you'll need to edit the EXE name accordingly by changing "sm64.us.f3dex2e.exe" to "sm64.jp.f3dex2e.exe". + +### Making an offline.bat for launching offline patch files + +Open Notepad. Paste in the following text: `start sm64.us.f3dex2e.exe --sm64ap_file %1` + +Go to File > Save As... + +Navigate to the folder you selected for your SM64 build when you followed the Build guide for SM64PCLauncher earlier. Once there, navigate further into `build` and then `us_pc`. This folder should be the same folder that `sm64.us.f3dex2e.exe` resides in. + +Make the file name `"offline.bat"` . THE QUOTE MARKS ARE IMPORTANT! Otherwise, it will create a text file instead ("offline.bat.txt"), which won't work as a batch file. + +Now you should have a file called `offline.bat` with a gear icon in the same folder as your "sm64.us.f3dex2e.exe". Right click `offline.bat` and choose `Send To > Desktop (Create Shortcut)`. +- If the icon for this file is a notepad rather than a gear, you saved it as a .txt file on accident. To fix this, change the file extension to .bat. + +From now on, whenever you start an offline, single-player game, just download the `.apsm64ex` patch file from the Generator, then drag-and-drop that onto `offline.bat` to open the game and start playing. + +NOTE: When playing offline patch files, a `.save` file is created in the same directory as your patch file, which contains your save data for that seed. Don't delete it until you're done with that seed. + +### Making an online.bat for launching online Multiworld games + +These steps are very similar. You will be making a batch file in the same location as before. However, the text you put into this batch file is different, and you will not drag patch files onto it. + +Use the same steps as before to open Notepad and paste in the following: + +`set /p port="Enter port number of room - "` + +`set /p slot="Enter slot name - "` + +`start sm64.us.f3dex2e.exe --sm64ap_name "%slot%" --sm64ap_ip archipelago.gg:%port%` + +Save this file as `"online.bat"`, then create a shortcut by following the same steps as before. + +To use this batch file, double-click it. A window will open. Type the five-digit port number of the room you wish to join, then type your slot name. +- The port number is provided on the room page. The game host should share this page with all players. +- The slot name is whatever you typed in the "Name" field when creating a config file. All slot names are visible on the room page. + +Once you provide those two bits of information, the game will open. If the info is correct, when the game starts, you will see "Connected to Archipelago" on the bottom of your screen, and you will be able to enter the castle. +- If you don't see this text and crash upon entering the castle, try again. Double-check the port number and slot name; even a single typo will cause your connection to fail. + +### Addendum - Deleting old saves + +Loading an old Mario save alongside a new seed is a bad idea, as it can cause locked doors and castle secret stars to already be unlocked / obtained. You should avoid opening a save that says "Stars x 0" as opposed to one that simply says "New". + +You can manually delete these old saves in-game before starting a new game, but that can be tedious. With a small edit to the batch files, you can delete these old saves automatically. Just add the line `del %AppData%\sm64ex\*.bin` to the batch file, above the `start` command. For example, here is `offline.bat` with the additional line: + +`del %AppData%\sm64ex\*.bin` + +`start sm64.us.f3dex2e.exe --sm64ap_file %1` + +This extra line deletes any previous save data before opening the game. Don't worry about lost stars or checks - the AP server (or in the case of offline, the `.save` file) keeps track of your star count, unlocked keys/caps/cannons, and which locations have already been checked, so you won't have to redo them. At worst you'll have to rewatch the door unlocking animations, and catch the rabbit Mips twice for his first star again if you haven't yet collected the second one. + ## Installation Troubleshooting Start the game from the command line to view helpful messages regarding SM64EX. diff --git a/worlds/smw/Options.py b/worlds/smw/Options.py index a9416b633d..60135896c8 100644 --- a/worlds/smw/Options.py +++ b/worlds/smw/Options.py @@ -88,7 +88,7 @@ class BowserCastleRooms(Choice): class BossShuffle(Choice): """ - How the rooms of Bowser's Castle Front Door behave + How bosses are shuffled None: Bosses are not shuffled Simple: Four Reznors and the seven Koopalings are shuffled around Full: Each boss location gets a fully random boss diff --git a/worlds/smw/Rom.py b/worlds/smw/Rom.py index 5a4e9b5352..ffd8923786 100644 --- a/worlds/smw/Rom.py +++ b/worlds/smw/Rom.py @@ -971,5 +971,5 @@ def get_base_rom_path(file_name: str = "") -> str: if not file_name: file_name = options["smw_options"]["rom_file"] if not os.path.exists(file_name): - file_name = Utils.local_path(file_name) + file_name = Utils.user_path(file_name) return file_name diff --git a/worlds/smw/docs/setup_en.md b/worlds/smw/docs/setup_en.md index 5f600c33f0..a8f6759227 100644 --- a/worlds/smw/docs/setup_en.md +++ b/worlds/smw/docs/setup_en.md @@ -90,7 +90,7 @@ the lua you are using in your file explorer and copy the `socket.dll` to the bas Once you have changed the loaded core, you must restart BizHawk. 2. Load your ROM file if it hasn't already been loaded. 3. Click on the Tools menu and click on **Lua Console** -4. Click the button to open a new Lua script. +4. Click the Open Folder icon that says `Open Script` via the tooltip on mouse hover, or click the Script Menu then `Open Script...`, or press `Ctrl-O`. 5. Select the `Connector.lua` file included with your client - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the emulator is 64-bit or 32-bit. Please note the most recent versions of BizHawk are 64-bit only. diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index c14cbe35d2..0c4ca08dcb 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -509,7 +509,7 @@ class SMZ3World(World): return self.smz3DungeonItems else: return [] - + def post_fill(self): # some small or big keys (those always_allow) can be unreachable in-game # while logic still collects some of them (probably to simulate the player collecting pot keys in the logic), some others don't @@ -524,7 +524,7 @@ class SMZ3World(World): loc.item.classification = ItemClassification.filler loc.item.item.Progression = False loc.item.location.event = False - self.unreachable.append(loc) + self.unreachable.append(loc) def get_filler_item_name(self) -> str: return self.multiworld.random.choice(self.junkItemsNames) diff --git a/worlds/smz3/data/zsm.ips b/worlds/smz3/data/zsm.ips index f2ecadf8ac..fff36d95d1 100644 Binary files a/worlds/smz3/data/zsm.ips and b/worlds/smz3/data/zsm.ips differ diff --git a/worlds/smz3/docs/multiworld_en.md b/worlds/smz3/docs/multiworld_en.md index 735be9d519..f375cd55e1 100644 --- a/worlds/smz3/docs/multiworld_en.md +++ b/worlds/smz3/docs/multiworld_en.md @@ -98,7 +98,7 @@ the lua you are using in your file explorer and copy the `socket.dll` to the bas Once you have changed the loaded core, you must restart BizHawk. 2. Load your ROM file if it hasn't already been loaded. 3. Click on the Tools menu and click on **Lua Console** -4. Click the button to open a new Lua script. +4. Click the Open Folder icon that says `Open Script` via the tooltip on mouse hover, or click the Script Menu then `Open Script...`, or press `Ctrl-O`. 5. Select the `Connector.lua` file included with your client - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the emulator is 64-bit or 32-bit. Please note the most recent versions of BizHawk are 64-bit only. diff --git a/worlds/soe/docs/multiworld_en.md b/worlds/soe/docs/multiworld_en.md index 115fe5ea1a..fcaf339f7b 100644 --- a/worlds/soe/docs/multiworld_en.md +++ b/worlds/soe/docs/multiworld_en.md @@ -85,7 +85,7 @@ you may be prompted to allow it to communicate through the Windows Firewall. Once you have changed the loaded core, you must restart BizHawk. 2. Load your ROM file if it hasn't already been loaded. 3. Click on the Tools menu and click on **Lua Console** -4. Click the button to open a new Lua script. +4. Click the Open Folder icon that says `Open Script` via the tooltip on mouse hover, or click the Script Menu then `Open Script...`, or press `Ctrl-O`. 5. Select any `Connector.lua` file from your SNI installation ##### bsnes-plus-nwa diff --git a/worlds/spire/Options.py b/worlds/spire/Options.py index 1711e12deb..76cbc4cf37 100644 --- a/worlds/spire/Options.py +++ b/worlds/spire/Options.py @@ -1,15 +1,34 @@ import typing -from Options import Choice, Option, Range, Toggle +from Options import TextChoice, Option, Range, Toggle -class Character(Choice): - """Pick What Character you wish to play with.""" +class Character(TextChoice): + """Enter the internal ID of the character to use. + + if you don't know the exact ID to enter with the mod installed go to + `Mods -> Archipelago Multi-world -> config` to view a list of installed modded character IDs. + + the downfall characters will only work if you have downfall installed. + + Spire Take the Wheel will have your client pick a random character from the list of all your installed characters + including custom ones. + + if the chosen character mod is not installed it will default back to 'The Ironclad' + """ display_name = "Character" - option_ironclad = 0 - option_silent = 1 - option_defect = 2 - option_watcher = 3 - default = 0 + option_The_Ironclad = 0 + option_The_Silent = 1 + option_The_Defect = 2 + option_The_Watcher = 3 + option_The_Hermit = 4 + option_The_Slime_Boss = 5 + option_The_Guardian = 6 + option_The_Hexaghost = 7 + option_The_Champ = 8 + option_The_Gremlins = 9 + option_The_Automaton = 10 + option_The_Snecko = 11 + option_spire_take_the_wheel = 12 class Ascension(Range): @@ -20,10 +39,17 @@ class Ascension(Range): default = 0 -class HeartRun(Toggle): - """Whether or not you will need to collect the 3 keys and enter the final act to - complete the game. The Heart does not need to be defeated.""" - display_name = "Heart Run" +class FinalAct(Toggle): + """Whether you will need to collect the 3 keys and beat the final act to complete the game.""" + display_name = "Final Act" + option_true = 1 + option_false = 0 + default = 0 + + +class Downfall(Toggle): + """When Downfall is Installed this will switch the played mode to Downfall""" + display_name = "Downfall" option_true = 1 option_false = 0 default = 0 @@ -32,5 +58,6 @@ class HeartRun(Toggle): spire_options: typing.Dict[str, type(Option)] = { "character": Character, "ascension": Ascension, - "heart_run": HeartRun + "final_act": FinalAct, + "downfall": Downfall, } diff --git a/worlds/spire/__init__.py b/worlds/spire/__init__.py index 5a7ed19ecf..0695d18700 100644 --- a/worlds/spire/__init__.py +++ b/worlds/spire/__init__.py @@ -32,18 +32,11 @@ class SpireWorld(World): topology_present = False data_version = 1 web = SpireWeb() + required_client_version = (0, 3, 7) item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = location_table - def _get_slot_data(self): - return { - 'seed': "".join(self.multiworld.per_slot_randoms[self.player].choice(string.ascii_letters) for i in range(16)), - 'character': self.multiworld.character[self.player], - 'ascension': self.multiworld.ascension[self.player], - 'heart_run': self.multiworld.heart_run[self.player] - } - def generate_basic(self): # Fill out our pool with our items from item_pool, assuming 1 item if not present in item_pool pool = [] @@ -63,7 +56,6 @@ class SpireWorld(World): if self.multiworld.logic[self.player] != 'no logic': self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) - def set_rules(self): set_rules(self.multiworld, self.player) @@ -74,10 +66,12 @@ class SpireWorld(World): create_regions(self.multiworld, self.player) def fill_slot_data(self) -> dict: - slot_data = self._get_slot_data() + slot_data = { + 'seed': "".join(self.multiworld.per_slot_randoms[self.player].choice(string.ascii_letters) for i in range(16)) + } for option_name in spire_options: option = getattr(self.multiworld, option_name)[self.player] - slot_data[option_name] = int(option.value) + slot_data[option_name] = option.value return slot_data def get_filler_item_name(self) -> str: diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index 306a3ec7e0..79c057fcbe 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -42,7 +42,8 @@ class StardewWebWorld(WebWorld): class StardewValleyWorld(World): """ - Stardew Valley farming simulator game where the objective is basically to spend the least possible time on your farm. + Stardew Valley is an open-ended country-life RPG. You can farm, fish, mine, fight, complete quests, + befriend villagers, and uncover dark secrets. """ game = "Stardew Valley" option_definitions = stardew_valley_options diff --git a/worlds/stardew_valley/docs/setup_en.md b/worlds/stardew_valley/docs/setup_en.md index 7ac9c8a814..30d5f3da2d 100644 --- a/worlds/stardew_valley/docs/setup_en.md +++ b/worlds/stardew_valley/docs/setup_en.md @@ -23,7 +23,7 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) ### Where do I get a YAML file? -You can customize your settings by visiting the [Stardew Valley Player Settings Page](/games/Stardew Valley/player-settings) +You can customize your settings by visiting the [Stardew Valley Player Settings Page](../player-settings) ## Joining a MultiWorld Game diff --git a/worlds/stardew_valley/logic.py b/worlds/stardew_valley/logic.py index 79b2b63ddf..85a5bb08d4 100644 --- a/worlds/stardew_valley/logic.py +++ b/worlds/stardew_valley/logic.py @@ -946,11 +946,16 @@ class StardewLogic: return region_rule & season_rule & difficulty_rule def can_catch_every_fish(self) -> StardewRule: - rules = [self.has_skill_level("Fishing", 10), self.received("Progressive Fishing Rod", 4)] + rules = [self.has_skill_level("Fishing", 10), self.has_max_fishing_rod()] for fish in all_fish_items: rules.append(self.can_catch_fish(fish)) return _And(rules) + def has_max_fishing_rod(self) -> StardewRule: + if self.options[options.ToolProgression] == options.ToolProgression.option_progressive: + return self.received("Progressive Fishing Rod", 4) + return self.can_get_fishing_xp() + def can_cook(self) -> StardewRule: return self.has_house(1) or self.has_skill_level("Foraging", 9) diff --git a/worlds/stardew_valley/test/TestOptions.py b/worlds/stardew_valley/test/TestOptions.py new file mode 100644 index 0000000000..063d9c2be9 --- /dev/null +++ b/worlds/stardew_valley/test/TestOptions.py @@ -0,0 +1,8 @@ +from worlds.stardew_valley.test import SVTestBase + + +class TestMasterAnglerVanillaTools(SVTestBase): + options = { + "goal": "master_angler", + "tool_progression": "vanilla", + } diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py index 1ddf037641..c9a8c74667 100644 --- a/worlds/stardew_valley/test/__init__.py +++ b/worlds/stardew_valley/test/__init__.py @@ -11,4 +11,10 @@ class SVTestBase(WorldTestBase): def world_setup(self, *args, **kwargs): super().world_setup(*args, **kwargs) - self.world = self.multiworld.worlds[self.player] + if self.constructed: + self.world = self.multiworld.worlds[self.player] + + @property + def run_default_tests(self) -> bool: + # world_setup is overridden, so it'd always run default tests when importing SVTestBase + return type(self) is not SVTestBase and super().run_default_tests diff --git a/worlds/subnautica/Items.py b/worlds/subnautica/Items.py index a5ccc1fb59..600e1d1996 100644 --- a/worlds/subnautica/Items.py +++ b/worlds/subnautica/Items.py @@ -40,8 +40,8 @@ item_table: Dict[int, ItemDict] = { 'tech_type': 'CyclopsThermalReactorModule'}, 35007: {'classification': ItemClassification.filler, 'count': 1, - 'name': 'Stillsuit', - 'tech_type': 'WaterFiltrationSuitFragment'}, + 'name': 'Water Filtration Suit', + 'tech_type': 'WaterFiltrationSuit'}, 35008: {'classification': ItemClassification.progression, 'count': 1, 'name': 'Alien Containment', @@ -223,7 +223,7 @@ item_table: Dict[int, ItemDict] = { 'name': 'Observatory', 'tech_type': 'BaseObservatory'}, 35053: {'classification': ItemClassification.progression, - 'count': 2, + 'count': 1, 'name': 'Multipurpose Room', 'tech_type': 'BaseRoom'}, 35054: {'classification': ItemClassification.useful, @@ -338,12 +338,11 @@ item_table: Dict[int, ItemDict] = { 'count': 1, 'name': 'Ultra High Capacity Tank', 'tech_type': 'HighCapacityTank'}, - # these currently unlock through some special sauce in Subnautica, unlike any established other - # keeping here for later 35082: {'classification': ItemClassification.progression, - 'count': 0, + 'count': 1, 'name': 'Large Room', 'tech_type': 'BaseLargeRoom'}, + # awarded with their rooms, keeping that as-is as they're cosmetic 35083: {'classification': ItemClassification.filler, 'count': 0, 'name': 'Large Room Glass Dome', @@ -360,6 +359,18 @@ item_table: Dict[int, ItemDict] = { 'count': 0, 'name': 'Partition Door', 'tech_type': 'BasePartitionDoor'}, + # new items that the mod implements + + # Awards all furniture as a bundle + 35100: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Furniture', + 'tech_type': 'Furniture'}, + # Awards all farming blueprints as a bundle + 35101: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Farming', + 'tech_type': 'Farming'}, } advancement_item_names: Set[str] = set() @@ -372,8 +383,15 @@ for item_id, item_data in item_table.items(): else: non_advancement_item_names.add(item_name) +group_items: Dict[int, Set[int]] = { + 35100: {35025, 35047, 35048, 35056, 35057, 35058, 35059, 35060, 35061, 35062, 35063, 35064, 35065, 35067, 35068, + 35069, 35070, 35073, 35074}, + 35101: {35049, 35050, 35051, 35071, 35072, 35074} +} + if False: # turn to True to export for Subnautica mod from .Locations import location_table + from NetUtils import encode itemcount = sum(item_data["count"] for item_data in item_table.values()) assert itemcount == len(location_table), f"{itemcount} != {len(location_table)}" payload = {item_id: item_data["tech_type"] for item_id, item_data in item_table.items()} @@ -381,3 +399,5 @@ if False: # turn to True to export for Subnautica mod with open("items.json", "w") as f: json.dump(payload, f) + with open("group_items.json", "w") as f: + f.write(encode(group_items)) diff --git a/worlds/subnautica/Locations.py b/worlds/subnautica/Locations.py index da92f8d481..e0a33966f0 100644 --- a/worlds/subnautica/Locations.py +++ b/worlds/subnautica/Locations.py @@ -580,9 +580,14 @@ if False: # turn to True to export for Subnautica mod with open("locations.json", "w") as f: json.dump(payload, f) - def radiated(pos: Vector): - aurora_dist = math.sqrt((pos["x"] - 1038.0) ** 2 + (pos["y"] - -3.4) ** 2 + (pos["y"] - -163.1) ** 2) + # copy-paste from Rules + def is_radiated(x: float, y: float, z: float) -> bool: + aurora_dist = math.sqrt((x - 1038.0) ** 2 + y ** 2 + (z - -163.1) ** 2) return aurora_dist < 950 + # end of copy-paste + + def radiated(pos: Vector): + return is_radiated(pos["x"], pos["y"], pos["z"]) def far_away(pos: Vector): return (pos["x"] ** 2 + pos["z"] ** 2) > (800 ** 2) diff --git a/worlds/subnautica/Options.py b/worlds/subnautica/Options.py index 03834cdbba..91c4866142 100644 --- a/worlds/subnautica/Options.py +++ b/worlds/subnautica/Options.py @@ -35,14 +35,6 @@ class EarlySeaglide(DefaultOnToggle): display_name = "Early Seaglide" -class ItemPool(Choice): - """Valuable item pool leaves all filler items in their vanilla locations and - creates random duplicates of important items into freed spots.""" - display_name = "Item Pool" - option_standard = 0 - option_valuable = 1 - - class Goal(Choice): """Goal to complete. Launch: Leave the planet. @@ -108,7 +100,6 @@ class SubnauticaDeathLink(DeathLink): options = { "swim_rule": SwimRule, "early_seaglide": EarlySeaglide, - "item_pool": ItemPool, "goal": Goal, "creature_scans": CreatureScans, "creature_scan_logic": AggressiveScanLogic, diff --git a/worlds/subnautica/Rules.py b/worlds/subnautica/Rules.py index 48db25a815..793c85be41 100644 --- a/worlds/subnautica/Rules.py +++ b/worlds/subnautica/Rules.py @@ -221,6 +221,11 @@ def get_max_depth(state: "CollectionState", player: int): ) +def is_radiated(x: float, y: float, z: float) -> bool: + aurora_dist = math.sqrt((x - 1038.0) ** 2 + y ** 2 + (z - -163.1) ** 2) + return aurora_dist < 950 + + def can_access_location(state: "CollectionState", player: int, loc: LocationDict) -> bool: need_laser_cutter = loc.get("need_laser_cutter", False) if need_laser_cutter and not has_laser_cutter(state, player): @@ -235,8 +240,7 @@ def can_access_location(state: "CollectionState", player: int, loc: LocationDict pos_y = pos["y"] pos_z = pos["z"] - aurora_dist = math.sqrt((pos_x - 1038.0) ** 2 + (pos_y - -3.4) ** 2 + (pos_z - -163.1) ** 2) - need_radiation_suit = aurora_dist < 950 + need_radiation_suit = is_radiated(pos_x, pos_y, pos_z) if need_radiation_suit and not state.has("Radiation Suit", player): return False diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index 53c04fb3d8..bc1e4f696c 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -1,4 +1,7 @@ +from __future__ import annotations + import logging +import itertools from typing import List, Dict, Any from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification @@ -7,7 +10,7 @@ from . import Items from . import Locations from . import Creatures from . import Options -from .Items import item_table +from .Items import item_table, group_items from .Rules import set_rules logger = logging.getLogger("Subnautica") @@ -42,7 +45,7 @@ class SubnauticaWorld(World): option_definitions = Options.options data_version = 9 - required_client_version = (0, 3, 8) + required_client_version = (0, 3, 9) creatures_to_scan: List[str] @@ -84,37 +87,41 @@ class SubnauticaWorld(World): def create_items(self): # Generate item pool - pool = [] + pool: List[SubnauticaItem] = [] extras = self.multiworld.creature_scans[self.player].value - valuable = self.multiworld.item_pool[self.player] == Options.ItemPool.option_valuable - for item in item_table.values(): - for i in range(item["count"]): - subnautica_item = self.create_item(item["name"]) - if item["name"] == "Neptune Launch Platform": - self.multiworld.get_location("Aurora - Captain Data Terminal", self.player).place_locked_item( - subnautica_item) - elif valuable and ItemClassification.filler == item["classification"]: - extras += 1 - else: - pool.append(subnautica_item) - for item_name in self.multiworld.random.choices(sorted(Items.advancement_item_names - {"Neptune Launch Platform"}), - k=extras): + grouped = set(itertools.chain.from_iterable(group_items.values())) + + for item_id, item in item_table.items(): + if item_id in grouped: + extras += item["count"] + else: + for i in range(item["count"]): + subnautica_item = self.create_item(item["name"]) + if item["name"] == "Neptune Launch Platform": + self.multiworld.get_location("Aurora - Captain Data Terminal", self.player).place_locked_item( + subnautica_item) + else: + pool.append(subnautica_item) + + group_amount: int = 3 + assert len(group_items) * group_amount <= extras + for name in ("Furniture", "Farming"): + for _ in range(group_amount): + pool.append(self.create_item(name)) + extras -= group_amount + + for item_name in self.multiworld.random.choices( + sorted(Items.advancement_item_names - {"Neptune Launch Platform"}), k=extras): item = self.create_item(item_name) - item.classification = ItemClassification.filler # as it's an extra, just fast-fill it somewhere pool.append(item) self.multiworld.itempool += pool def fill_slot_data(self) -> Dict[str, Any]: goal: Options.Goal = self.multiworld.goal[self.player] - item_pool: Options.ItemPool = self.multiworld.item_pool[self.player] swim_rule: Options.SwimRule = self.multiworld.swim_rule[self.player] vanilla_tech: List[str] = [] - if item_pool == Options.ItemPool.option_valuable: - for item in Items.item_table.values(): - if item["classification"] == ItemClassification.filler: - vanilla_tech.append(item["tech_type"]) slot_data: Dict[str, Any] = { "goal": goal.current_key, @@ -126,7 +133,7 @@ class SubnauticaWorld(World): return slot_data - def create_item(self, name: str) -> Item: + def create_item(self, name: str) -> SubnauticaItem: item_id: int = self.item_name_to_id[name] return SubnauticaItem(name, diff --git a/worlds/timespinner/Items.py b/worlds/timespinner/Items.py index add8beabf5..45c67c2547 100644 --- a/worlds/timespinner/Items.py +++ b/worlds/timespinner/Items.py @@ -239,6 +239,22 @@ starter_spells: Tuple[str, ...] = ( 'Corruption' ) +# weighted +starter_progression_items: Tuple[str, ...] = ( + 'Talaria Attachment', + 'Talaria Attachment', + 'Succubus Hairpin', + 'Succubus Hairpin', + 'Timespinner Wheel', + 'Timespinner Wheel', + 'Twin Pyramid Key', + 'Celestial Sash', + 'Lightwall', + 'Modern Warp Beacon', + 'Timeworn Warp Beacon', + 'Mysterious Warp Beacon' +) + filler_items: Tuple[str, ...] = ( 'Potion', 'Ether', diff --git a/worlds/timespinner/Locations.py b/worlds/timespinner/Locations.py index 960444acb8..70c76b8638 100644 --- a/worlds/timespinner/Locations.py +++ b/worlds/timespinner/Locations.py @@ -14,7 +14,7 @@ class LocationData(NamedTuple): rule: Callable[[CollectionState], bool] = lambda state: True -def get_locations(world: Optional[MultiWorld], player: Optional[int], +def get_location_datas(world: Optional[MultiWorld], player: Optional[int], precalculated_weights: PreCalculatedWeights) -> Tuple[LocationData, ...]: flooded: PreCalculatedWeights = precalculated_weights @@ -41,8 +41,8 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int], LocationData('Upper lake desolation', 'Lake Desolation (Upper): Double jump cave floor', 1337013), LocationData('Upper lake desolation', 'Lake Desolation (Upper): Sparrow chest', 1337014), LocationData('Upper lake desolation', 'Lake Desolation (Upper): Crash site pedestal', 1337015), - LocationData('Upper lake desolation', 'Lake Desolation (Upper): Crash site chest 1', 1337016, lambda state: state.has_all({'Killed Maw'}, player)), - LocationData('Upper lake desolation', 'Lake Desolation (Upper): Crash site chest 2', 1337017, lambda state: state.has_all({'Killed Maw'}, player)), + LocationData('Upper lake desolation', 'Lake Desolation (Upper): Crash site chest 1', 1337016, lambda state: state.has('Killed Maw', player)), + LocationData('Upper lake desolation', 'Lake Desolation (Upper): Crash site chest 2', 1337017, lambda state: state.has('Killed Maw', player)), LocationData('Eastern lake desolation', 'Lake Desolation: Kitty Boss', 1337018), LocationData('Library', 'Library: Basement', 1337019), LocationData('Library', 'Library: Warp gate', 1337020), @@ -77,7 +77,7 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int], LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Secret room', 1337049, logic.can_break_walls), LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Bottom left room', 1337050), LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Last chance before Xarion', 1337051, logic.has_doublejump), - LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Xarion', 1337052, lambda state: state.has('Water Mask', player) if flooded.flood_xarion else True), + LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Xarion', 1337052, lambda state: not flooded.flood_xarion or state.has('Water Mask', player)), LocationData('Sealed Caves (Sirens)', 'Sealed Caves (Sirens): Water hook', 1337053, lambda state: state.has('Water Mask', player)), LocationData('Sealed Caves (Sirens)', 'Sealed Caves (Sirens): Siren room underwater right', 1337054, lambda state: state.has('Water Mask', player)), LocationData('Sealed Caves (Sirens)', 'Sealed Caves (Sirens): Siren room underwater left', 1337055, lambda state: state.has('Water Mask', player)), @@ -125,7 +125,7 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int], LocationData('Forest', 'Forest: Waterfall chest 1', 1337094, lambda state: state.has('Water Mask', player)), LocationData('Forest', 'Forest: Waterfall chest 2', 1337095, lambda state: state.has('Water Mask', player)), LocationData('Forest', 'Forest: Batcave', 1337096), - LocationData('Forest', 'Castle Ramparts: In the moat', 1337097, lambda state: state.has('Water Mask', player) if flooded.flood_moat else True), + LocationData('Forest', 'Castle Ramparts: In the moat', 1337097, lambda state: not flooded.flood_moat or state.has('Water Mask', player)), LocationData('Left Side forest Caves', 'Forest: Before Serene single bat cave', 1337098), LocationData('Upper Lake Serene', 'Lake Serene (Upper): Rat nest', 1337099), LocationData('Upper Lake Serene', 'Lake Serene (Upper): Double jump cave platform', 1337100, logic.has_doublejump), @@ -139,22 +139,22 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int], LocationData('Lower Lake Serene', 'Lake Serene (Lower): Under the eels', 1337106), LocationData('Lower Lake Serene', 'Lake Serene (Lower): Water spikes room', 1337107), LocationData('Lower Lake Serene', 'Lake Serene (Lower): Underwater secret', 1337108, logic.can_break_walls), - LocationData('Lower Lake Serene', 'Lake Serene (Lower): T chest', 1337109, lambda state: logic.has_doublejump_of_npc(state) if flooded.dry_lake_serene else True), + LocationData('Lower Lake Serene', 'Lake Serene (Lower): T chest', 1337109, lambda state: not flooded.dry_lake_serene or logic.has_doublejump_of_npc(state)), LocationData('Lower Lake Serene', 'Lake Serene (Lower): Past the eels', 1337110), - LocationData('Lower Lake Serene', 'Lake Serene (Lower): Underwater pedestal', 1337111, lambda state: logic.has_doublejump(state) if flooded.dry_lake_serene else True), - LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Shroom jump room', 1337112, lambda state: logic.has_doublejump(state) if not flooded.flood_maw else True), - LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Secret room', 1337113, lambda state: logic.can_break_walls(state) and (state.has('Water Mask', player) if flooded.flood_maw else True)), - LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Bottom left room', 1337114, lambda state: state.has('Water Mask', player) if flooded.flood_maw else True), + LocationData('Lower Lake Serene', 'Lake Serene (Lower): Underwater pedestal', 1337111, lambda state: not flooded.dry_lake_serene or logic.has_doublejump(state)), + LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Shroom jump room', 1337112, lambda state: not flooded.flood_maw or logic.has_doublejump(state)), + LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Secret room', 1337113, lambda state: logic.can_break_walls(state) and (not flooded.flood_maw or state.has('Water Mask', player))), + LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Bottom left room', 1337114, lambda state: not flooded.flood_maw or state.has('Water Mask', player)), LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Single shroom room', 1337115), LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 1', 1337116, lambda state: logic.has_forwarddash_doublejump(state) or flooded.flood_maw), LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 2', 1337117, lambda state: logic.has_forwarddash_doublejump(state) or flooded.flood_maw), LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 3', 1337118, lambda state: logic.has_forwarddash_doublejump(state) or flooded.flood_maw), LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 4', 1337119, lambda state: logic.has_forwarddash_doublejump(state) or flooded.flood_maw), - LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Pedestal', 1337120, lambda state: state.has('Water Mask', player) if flooded.flood_maw else True), + LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Pedestal', 1337120, lambda state: not flooded.flood_maw or state.has('Water Mask', player)), LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Last chance before Maw', 1337121, lambda state: state.has('Water Mask', player) if flooded.flood_maw else logic.has_doublejump(state)), - LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Plasma Crystal', 1337173, lambda state: state.has_any({'Gas Mask', 'Talaria Attachment'}, player) and (state.has('Water Mask', player) if flooded.flood_maw else True)), - LocationData('Caves of Banishment (Maw)', 'Killed Maw', EventId, lambda state: state.has('Gas Mask', player) and (state.has('Water Mask', player) if flooded.flood_maw else True)), - LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Mineshaft', 1337122, lambda state: state.has_any({'Gas Mask', 'Talaria Attachment'}, player) and (state.has('Water Mask', player) if flooded.flood_maw else True)), + LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Plasma Crystal', 1337173, lambda state: state.has_any({'Gas Mask', 'Talaria Attachment'}, player) and (not flooded.flood_maw or state.has('Water Mask', player))), + LocationData('Caves of Banishment (Maw)', 'Killed Maw', EventId, lambda state: state.has('Gas Mask', player) and (not flooded.flood_maw or state.has('Water Mask', player))), + LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Mineshaft', 1337122, lambda state: state.has_any({'Gas Mask', 'Talaria Attachment'}, player) and (not flooded.flood_maw or state.has('Water Mask', player))), LocationData('Caves of Banishment (Sirens)', 'Caves of Banishment (Sirens): Wyvern room', 1337123), LocationData('Caves of Banishment (Sirens)', 'Caves of Banishment (Sirens): Siren room above water chest', 1337124), LocationData('Caves of Banishment (Sirens)', 'Caves of Banishment (Sirens): Siren room underwater left chest', 1337125, lambda state: state.has('Water Mask', player)), @@ -179,11 +179,11 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int], LocationData('Castle Keep', 'Castle Keep: Royal guard tiny room', 1337141, lambda state: logic.has_doublejump(state) or logic.has_fastjump_on_npc(state)), LocationData('Royal towers (lower)', 'Royal Towers: Floor secret', 1337142, lambda state: logic.has_doublejump(state) and logic.can_break_walls(state)), LocationData('Royal towers', 'Royal Towers: Pre-climb gap', 1337143), - LocationData('Royal towers', 'Royal Towers: Long balcony', 1337144, lambda state: state.has('Water Mask', player) if flooded.flood_courtyard else True), - LocationData('Royal towers', 'Royal Towers: Past bottom struggle juggle', 1337145, lambda state: logic.has_doublejump_of_npc(state) if not flooded.flood_courtyard else True), + LocationData('Royal towers', 'Royal Towers: Long balcony', 1337144, lambda state: not flooded.flood_courtyard or state.has('Water Mask', player)), + LocationData('Royal towers', 'Royal Towers: Past bottom struggle juggle', 1337145, lambda state: flooded.flood_courtyard or logic.has_doublejump_of_npc(state)), LocationData('Royal towers', 'Royal Towers: Bottom struggle juggle', 1337146, logic.has_doublejump_of_npc), LocationData('Royal towers (upper)', 'Royal Towers: Top struggle juggle', 1337147, logic.has_doublejump_of_npc), - LocationData('Royal towers (upper)', 'Royal Towers: No struggle required', 1337148, logic.has_doublejump_of_npc), + LocationData('Royal towers (upper)', 'Royal Towers: No struggle required', 1337148), LocationData('Royal towers', 'Royal Towers: Right tower freebie', 1337149), LocationData('Royal towers (upper)', 'Royal Towers: Left tower small balcony', 1337150), LocationData('Royal towers (upper)', 'Royal Towers: Left tower royal guard', 1337151), @@ -196,10 +196,10 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int], # Ancient pyramid locations LocationData('Ancient Pyramid (entrance)', 'Ancient Pyramid: Why not it\'s right there', 1337246), LocationData('Ancient Pyramid (left)', 'Ancient Pyramid: Conviction guarded room', 1337247), - LocationData('Ancient Pyramid (left)', 'Ancient Pyramid: Pit secret room', 1337248, lambda state: logic.can_break_walls(state) and (state.has('Water Mask', player) if flooded.flood_pyramid_shaft else True)), - LocationData('Ancient Pyramid (left)', 'Ancient Pyramid: Regret chest', 1337249, lambda state: logic.can_break_walls(state) and (state.has('Water Mask', player) if flooded.flood_pyramid_shaft else True)), - LocationData('Ancient Pyramid (right)', 'Ancient Pyramid: Nightmare Door chest', 1337236, lambda state: state.has('Water Mask', player) if flooded.flood_pyramid_back else True), - LocationData('Ancient Pyramid (right)', 'Killed Nightmare', EventId, lambda state: state.has_all({'Timespinner Wheel', 'Timespinner Spindle', 'Timespinner Gear 1', 'Timespinner Gear 2', 'Timespinner Gear 3'}, player) and (state.has('Water Mask', player) if flooded.flood_pyramid_back else True)) + LocationData('Ancient Pyramid (left)', 'Ancient Pyramid: Pit secret room', 1337248, lambda state: logic.can_break_walls(state) and (not flooded.flood_pyramid_shaft or state.has('Water Mask', player))), + LocationData('Ancient Pyramid (left)', 'Ancient Pyramid: Regret chest', 1337249, lambda state: logic.can_break_walls(state) and (not flooded.flood_pyramid_shaft or state.has('Water Mask', player))), + LocationData('Ancient Pyramid (right)', 'Ancient Pyramid: Nightmare Door chest', 1337236, lambda state: not flooded.flood_pyramid_back or state.has('Water Mask', player)), + LocationData('Ancient Pyramid (right)', 'Killed Nightmare', EventId, lambda state: state.has_all({'Timespinner Wheel', 'Timespinner Spindle', 'Timespinner Gear 1', 'Timespinner Gear 2', 'Timespinner Gear 3'}, player) and (not flooded.flood_pyramid_back or state.has('Water Mask', player))) ] # 1337156 - 1337170 Downloads @@ -244,15 +244,15 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int], LocationData('Emperors tower', 'Emperor\'s Tower: Memory - Way Up There (Final Circle)', 1337187, logic.has_doublejump_of_npc), LocationData('Forest', 'Forest: Journal - Rats (Lachiem Expedition)', 1337188), LocationData('Forest', 'Forest: Journal - Bat Jump Ledge (Peace Treaty)', 1337189, lambda state: logic.has_doublejump_of_npc(state) or logic.has_forwarddash_doublejump(state) or logic.has_fastjump_on_npc(state)), - LocationData('Forest', 'Forest: Journal - Floating in Moat (Prime Edicts)', 1337190, lambda state: state.has('Water Mask', player) if flooded.flood_moat else True), + LocationData('Forest', 'Forest: Journal - Floating in Moat (Prime Edicts)', 1337190, lambda state: not flooded.flood_moat or state.has('Water Mask', player)), LocationData('Castle Ramparts', 'Castle Ramparts: Journal - Archer + Knight (Declaration of Independence)', 1337191), LocationData('Castle Keep', 'Castle Keep: Journal - Under the Twins (Letter of Reference)', 1337192), LocationData('Castle Basement', 'Castle Basement: Journal - Castle Loop Giantess (Political Advice)', 1337193), LocationData('Royal towers (lower)', 'Royal Towers: Journal - Aelana\'s Room (Diplomatic Missive)', 1337194, logic.has_pink), LocationData('Royal towers (upper)', 'Royal Towers: Journal - Top Struggle Juggle Base (War of the Sisters)', 1337195), LocationData('Royal towers (upper)', 'Royal Towers: Journal - Aelana Boss (Stained Letter)', 1337196), - LocationData('Royal towers', 'Royal Towers: Journal - Near Bottom Struggle Juggle (Mission Findings)', 1337197, lambda state: logic.has_doublejump_of_npc(state) if not flooded.flood_courtyard else True), - LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Journal - Lower Left Caves (Naivety)', 1337198, lambda state: state.has('Water Mask', player) if flooded.flood_maw else True) + LocationData('Royal towers', 'Royal Towers: Journal - Near Bottom Struggle Juggle (Mission Findings)', 1337197, lambda state: flooded.flood_courtyard or logic.has_doublejump_of_npc(state)), + LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Journal - Lower Left Caves (Naivety)', 1337198, lambda state: not flooded.flood_maw or state.has('Water Mask', player)) ) # 1337199 - 1337236 Reserved for future use diff --git a/worlds/timespinner/Options.py b/worlds/timespinner/Options.py index 0448e93dc4..5f4d230688 100644 --- a/worlds/timespinner/Options.py +++ b/worlds/timespinner/Options.py @@ -357,7 +357,7 @@ class RisingTidesOverrides(OptionDict): "CastleBasement": { "Dry": 66, "Flooded": 17, "FloodedWithSavePointAvailable": 17 }, "CastleCourtyard": { "Dry": 67, "Flooded": 33 }, "LakeDesolation": { "Dry": 67, "Flooded": 33 }, - "LakeSerene": { "Dry": 67, "Flooded": 33 }, + "LakeSerene": { "Dry": 33, "Flooded": 67 }, } diff --git a/worlds/timespinner/PreCalculatedWeights.py b/worlds/timespinner/PreCalculatedWeights.py index 8501ef73a0..64243e25ed 100644 --- a/worlds/timespinner/PreCalculatedWeights.py +++ b/worlds/timespinner/PreCalculatedWeights.py @@ -1,7 +1,6 @@ from typing import Tuple, Dict, Union from BaseClasses import MultiWorld -from .Options import is_option_enabled, get_option_value - +from .Options import timespinner_options, is_option_enabled, get_option_value class PreCalculatedWeights: pyramid_keys_unlock: str @@ -21,24 +20,37 @@ class PreCalculatedWeights: dry_lake_serene: bool def __init__(self, world: MultiWorld, player: int): - weights_overrrides: Dict[str, Union[str, Dict[str, int]]] = self.get_flood_weights_overrides(world, player) + if world and is_option_enabled(world, player, "RisingTides"): + weights_overrrides: Dict[str, Union[str, Dict[str, int]]] = self.get_flood_weights_overrides(world, player) - self.flood_basement, self.flood_basement_high = \ - self.roll_flood_setting_with_available_save(world, player, weights_overrrides, "CastleBasement") - self.flood_xarion = self.roll_flood_setting(world, player, weights_overrrides, "Xarion") - self.flood_maw = self.roll_flood_setting(world, player, weights_overrrides, "Maw") - self.flood_pyramid_shaft = self.roll_flood_setting(world, player, weights_overrrides, "AncientPyramidShaft") - self.flood_pyramid_back = self.roll_flood_setting(world, player, weights_overrrides, "Sandman") - self.flood_moat = self.roll_flood_setting(world, player, weights_overrrides, "CastleMoat") - self.flood_courtyard = self.roll_flood_setting(world, player, weights_overrrides, "CastleCourtyard") - self.flood_lake_desolation = self.roll_flood_setting(world, player, weights_overrrides, "LakeDesolation") - self.dry_lake_serene = self.roll_flood_setting(world, player, weights_overrrides, "LakeSerene") + self.flood_basement, self.flood_basement_high = \ + self.roll_flood_setting(world, player, weights_overrrides, "CastleBasement") + self.flood_xarion, _ = self.roll_flood_setting(world, player, weights_overrrides, "Xarion") + self.flood_maw, _ = self.roll_flood_setting(world, player, weights_overrrides, "Maw") + self.flood_pyramid_shaft, _ = self.roll_flood_setting(world, player, weights_overrrides, "AncientPyramidShaft") + self.flood_pyramid_back, _ = self.roll_flood_setting(world, player, weights_overrrides, "Sandman") + self.flood_moat, _ = self.roll_flood_setting(world, player, weights_overrrides, "CastleMoat") + self.flood_courtyard, _ = self.roll_flood_setting(world, player, weights_overrrides, "CastleCourtyard") + self.flood_lake_desolation, _ = self.roll_flood_setting(world, player, weights_overrrides, "LakeDesolation") + flood_lake_serene, _ = self.roll_flood_setting(world, player, weights_overrrides, "LakeSerene") + self.dry_lake_serene = not flood_lake_serene + else: + self.flood_basement = False + self.flood_basement_high = False + self.flood_xarion = False + self.flood_maw = False + self.flood_pyramid_shaft = False + self.flood_pyramid_back = False + self.flood_moat = False + self.flood_courtyard = False + self.flood_lake_desolation = False + self.dry_lake_serene = False self.pyramid_keys_unlock, self.present_key_unlock, self.past_key_unlock, self.time_key_unlock = \ - self.get_pyramid_keys_unlock(world, player, self.flood_maw) + self.get_pyramid_keys_unlocks(world, player, self.flood_maw) - - def get_pyramid_keys_unlock(self, world: MultiWorld, player: int, is_maw_flooded: bool) -> Tuple[str, str, str, str]: + @staticmethod + def get_pyramid_keys_unlocks(world: MultiWorld, player: int, is_maw_flooded: bool) -> Tuple[str, str, str, str]: present_teleportation_gates: Tuple[str, ...] = ( "GateKittyBoss", "GateLeftLibrary", @@ -87,37 +99,26 @@ class PreCalculatedWeights: ) @staticmethod - def get_flood_weights_overrides( world: MultiWorld, player: int) -> Dict[str, Union[str, Dict[str, int]]]: + def get_flood_weights_overrides(world: MultiWorld, player: int) -> Dict[str, Union[str, Dict[str, int]]]: weights_overrides_option: Union[int, Dict[str, Union[str, Dict[str, int]]]] = \ get_option_value(world, player, "RisingTidesOverrides") - if weights_overrides_option == 0: - return {} + default_weights: Dict[str, Dict[str, int]] = timespinner_options["RisingTidesOverrides"].default + + if not weights_overrides_option: + weights_overrides_option = default_weights else: - return weights_overrides_option + for key, weights in default_weights.items(): + if not key in weights_overrides_option: + weights_overrides_option[key] = weights + + return weights_overrides_option @staticmethod - def roll_flood_setting(world: MultiWorld, player: int, weights: Dict[str, Union[Dict[str, int], str]], key: str) -> bool: - if not world or not is_option_enabled(world, player, "RisingTides"): - return False + def roll_flood_setting(world: MultiWorld, player: int, + all_weights: Dict[str, Union[Dict[str, int], str]], key: str) -> Tuple[bool, bool]: - weights = weights[key] if key in weights else { "Dry": 67, "Flooded": 33 } - - if isinstance(weights, dict): - result: str = world.random.choices(list(weights.keys()), weights=list(map(int, weights.values())))[0] - else: - result: str = weights - - return result == "Flooded" - - @staticmethod - def roll_flood_setting_with_available_save(world: MultiWorld, player: int, - weights: Dict[str, Union[Dict[str, int], str]], key: str) -> Tuple[bool, bool]: - - if not world or not is_option_enabled(world, player, "RisingTides"): - return False, False - - weights = weights[key] if key in weights else {"Dry": 66, "Flooded": 17, "FloodedWithSavePointAvailable": 17} + weights: Union[Dict[str, int], str] = all_weights[key] if isinstance(weights, dict): result: str = world.random.choices(list(weights.keys()), weights=list(map(int, weights.values())))[0] @@ -127,6 +128,6 @@ class PreCalculatedWeights: if result == "Dry": return False, False elif result == "Flooded": - return True, False - elif result == "FloodedWithSavePointAvailable": return True, True + elif result == "FloodedWithSavePointAvailable": + return True, False diff --git a/worlds/timespinner/Regions.py b/worlds/timespinner/Regions.py index ab8ee97ac6..905cae867e 100644 --- a/worlds/timespinner/Regions.py +++ b/worlds/timespinner/Regions.py @@ -1,64 +1,64 @@ from typing import List, Set, Dict, Tuple, Optional, Callable from BaseClasses import CollectionState, MultiWorld, Region, Entrance, Location from .Options import is_option_enabled -from .Locations import LocationData +from .Locations import LocationData, get_location_datas from .PreCalculatedWeights import PreCalculatedWeights from .LogicExtensions import TimespinnerLogic -def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData, ...], location_cache: List[Location], - precalculated_weights: PreCalculatedWeights): +def create_regions_and_locations(world: MultiWorld, player: int, precalculated_weights: PreCalculatedWeights): + locationn_datas: Tuple[LocationData] = get_location_datas(world, player, precalculated_weights) - locations_per_region = get_locations_per_region(locations) + locations_per_region: Dict[str, List[LocationData]] = split_location_datas_per_region(locationn_datas) regions = [ - create_region(world, player, locations_per_region, location_cache, 'Menu'), - create_region(world, player, locations_per_region, location_cache, 'Tutorial'), - create_region(world, player, locations_per_region, location_cache, 'Lake desolation'), - create_region(world, player, locations_per_region, location_cache, 'Upper lake desolation'), - create_region(world, player, locations_per_region, location_cache, 'Lower lake desolation'), - create_region(world, player, locations_per_region, location_cache, 'Eastern lake desolation'), - create_region(world, player, locations_per_region, location_cache, 'Library'), - create_region(world, player, locations_per_region, location_cache, 'Library top'), - create_region(world, player, locations_per_region, location_cache, 'Varndagroth tower left'), - create_region(world, player, locations_per_region, location_cache, 'Varndagroth tower right (upper)'), - create_region(world, player, locations_per_region, location_cache, 'Varndagroth tower right (lower)'), - create_region(world, player, locations_per_region, location_cache, 'Varndagroth tower right (elevator)'), - create_region(world, player, locations_per_region, location_cache, 'Sealed Caves (Sirens)'), - create_region(world, player, locations_per_region, location_cache, 'Military Fortress'), - create_region(world, player, locations_per_region, location_cache, 'Military Fortress (hangar)'), - create_region(world, player, locations_per_region, location_cache, 'The lab'), - create_region(world, player, locations_per_region, location_cache, 'The lab (power off)'), - create_region(world, player, locations_per_region, location_cache, 'The lab (upper)'), - create_region(world, player, locations_per_region, location_cache, 'Emperors tower'), - create_region(world, player, locations_per_region, location_cache, 'Skeleton Shaft'), - create_region(world, player, locations_per_region, location_cache, 'Sealed Caves (upper)'), - create_region(world, player, locations_per_region, location_cache, 'Sealed Caves (Xarion)'), - create_region(world, player, locations_per_region, location_cache, 'Refugee Camp'), - create_region(world, player, locations_per_region, location_cache, 'Forest'), - create_region(world, player, locations_per_region, location_cache, 'Left Side forest Caves'), - create_region(world, player, locations_per_region, location_cache, 'Upper Lake Serene'), - create_region(world, player, locations_per_region, location_cache, 'Lower Lake Serene'), - create_region(world, player, locations_per_region, location_cache, 'Caves of Banishment (upper)'), - create_region(world, player, locations_per_region, location_cache, 'Caves of Banishment (Maw)'), - create_region(world, player, locations_per_region, location_cache, 'Caves of Banishment (Sirens)'), - create_region(world, player, locations_per_region, location_cache, 'Castle Ramparts'), - create_region(world, player, locations_per_region, location_cache, 'Castle Keep'), - create_region(world, player, locations_per_region, location_cache, 'Castle Basement'), - create_region(world, player, locations_per_region, location_cache, 'Royal towers (lower)'), - create_region(world, player, locations_per_region, location_cache, 'Royal towers'), - create_region(world, player, locations_per_region, location_cache, 'Royal towers (upper)'), - create_region(world, player, locations_per_region, location_cache, 'Temporal Gyre'), - create_region(world, player, locations_per_region, location_cache, 'Ancient Pyramid (entrance)'), - create_region(world, player, locations_per_region, location_cache, 'Ancient Pyramid (left)'), - create_region(world, player, locations_per_region, location_cache, 'Ancient Pyramid (right)'), - create_region(world, player, locations_per_region, location_cache, 'Space time continuum') + create_region(world, player, locations_per_region, 'Menu'), + create_region(world, player, locations_per_region, 'Tutorial'), + create_region(world, player, locations_per_region, 'Lake desolation'), + create_region(world, player, locations_per_region, 'Upper lake desolation'), + create_region(world, player, locations_per_region, 'Lower lake desolation'), + create_region(world, player, locations_per_region, 'Eastern lake desolation'), + create_region(world, player, locations_per_region, 'Library'), + create_region(world, player, locations_per_region, 'Library top'), + create_region(world, player, locations_per_region, 'Varndagroth tower left'), + create_region(world, player, locations_per_region, 'Varndagroth tower right (upper)'), + create_region(world, player, locations_per_region, 'Varndagroth tower right (lower)'), + create_region(world, player, locations_per_region, 'Varndagroth tower right (elevator)'), + create_region(world, player, locations_per_region, 'Sealed Caves (Sirens)'), + create_region(world, player, locations_per_region, 'Military Fortress'), + create_region(world, player, locations_per_region, 'Military Fortress (hangar)'), + create_region(world, player, locations_per_region, 'The lab'), + create_region(world, player, locations_per_region, 'The lab (power off)'), + create_region(world, player, locations_per_region, 'The lab (upper)'), + create_region(world, player, locations_per_region, 'Emperors tower'), + create_region(world, player, locations_per_region, 'Skeleton Shaft'), + create_region(world, player, locations_per_region, 'Sealed Caves (upper)'), + create_region(world, player, locations_per_region, 'Sealed Caves (Xarion)'), + create_region(world, player, locations_per_region, 'Refugee Camp'), + create_region(world, player, locations_per_region, 'Forest'), + create_region(world, player, locations_per_region, 'Left Side forest Caves'), + create_region(world, player, locations_per_region, 'Upper Lake Serene'), + create_region(world, player, locations_per_region, 'Lower Lake Serene'), + create_region(world, player, locations_per_region, 'Caves of Banishment (upper)'), + create_region(world, player, locations_per_region, 'Caves of Banishment (Maw)'), + create_region(world, player, locations_per_region, 'Caves of Banishment (Sirens)'), + create_region(world, player, locations_per_region, 'Castle Ramparts'), + create_region(world, player, locations_per_region, 'Castle Keep'), + create_region(world, player, locations_per_region, 'Castle Basement'), + create_region(world, player, locations_per_region, 'Royal towers (lower)'), + create_region(world, player, locations_per_region, 'Royal towers'), + create_region(world, player, locations_per_region, 'Royal towers (upper)'), + create_region(world, player, locations_per_region, 'Temporal Gyre'), + create_region(world, player, locations_per_region, 'Ancient Pyramid (entrance)'), + create_region(world, player, locations_per_region, 'Ancient Pyramid (left)'), + create_region(world, player, locations_per_region, 'Ancient Pyramid (right)'), + create_region(world, player, locations_per_region, 'Space time continuum') ] if is_option_enabled(world, player, "GyreArchives"): regions.extend([ - create_region(world, player, locations_per_region, location_cache, 'Ravenlord\'s Lair'), - create_region(world, player, locations_per_region, location_cache, 'Ifrit\'s Lair'), + create_region(world, player, locations_per_region, 'Ravenlord\'s Lair'), + create_region(world, player, locations_per_region, 'Ifrit\'s Lair'), ]) if __debug__: @@ -70,127 +70,126 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData flooded: PreCalculatedWeights = precalculated_weights logic = TimespinnerLogic(world, player, precalculated_weights) - names: Dict[str, int] = {} - connect(world, player, names, 'Lake desolation', 'Lower lake desolation', lambda state: logic.has_timestop(state) or state.has('Talaria Attachment', player) or flooded.flood_lake_desolation) - connect(world, player, names, 'Lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player)) - connect(world, player, names, 'Lake desolation', 'Skeleton Shaft', lambda state: logic.has_doublejump(state) or flooded.flood_lake_desolation) - connect(world, player, names, 'Lake desolation', 'Space time continuum', logic.has_teleport) - connect(world, player, names, 'Upper lake desolation', 'Lake desolation') - connect(world, player, names, 'Upper lake desolation', 'Eastern lake desolation') - connect(world, player, names, 'Lower lake desolation', 'Lake desolation') - connect(world, player, names, 'Lower lake desolation', 'Eastern lake desolation') - connect(world, player, names, 'Eastern lake desolation', 'Space time continuum', logic.has_teleport) - connect(world, player, names, 'Eastern lake desolation', 'Library') - connect(world, player, names, 'Eastern lake desolation', 'Lower lake desolation') - connect(world, player, names, 'Eastern lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player)) - connect(world, player, names, 'Library', 'Eastern lake desolation') - connect(world, player, names, 'Library', 'Library top', lambda state: logic.has_doublejump(state) or state.has('Talaria Attachment', player)) - connect(world, player, names, 'Library', 'Varndagroth tower left', logic.has_keycard_D) - connect(world, player, names, 'Library', 'Space time continuum', logic.has_teleport) - connect(world, player, names, 'Library top', 'Library') - connect(world, player, names, 'Varndagroth tower left', 'Library') - connect(world, player, names, 'Varndagroth tower left', 'Varndagroth tower right (upper)', logic.has_keycard_C) - connect(world, player, names, 'Varndagroth tower left', 'Varndagroth tower right (lower)', logic.has_keycard_B) - connect(world, player, names, 'Varndagroth tower left', 'Sealed Caves (Sirens)', lambda state: logic.has_keycard_B(state) and state.has('Elevator Keycard', player)) - connect(world, player, names, 'Varndagroth tower left', 'Refugee Camp', lambda state: state.has('Timespinner Wheel', player) and state.has('Timespinner Spindle', player)) - connect(world, player, names, 'Varndagroth tower right (upper)', 'Varndagroth tower left') - connect(world, player, names, 'Varndagroth tower right (upper)', 'Varndagroth tower right (elevator)', lambda state: state.has('Elevator Keycard', player)) - connect(world, player, names, 'Varndagroth tower right (elevator)', 'Varndagroth tower right (upper)') - connect(world, player, names, 'Varndagroth tower right (elevator)', 'Varndagroth tower right (lower)') - connect(world, player, names, 'Varndagroth tower right (lower)', 'Varndagroth tower left', logic.has_keycard_B) - connect(world, player, names, 'Varndagroth tower right (lower)', 'Varndagroth tower right (elevator)', lambda state: state.has('Elevator Keycard', player)) - connect(world, player, names, 'Varndagroth tower right (lower)', 'Sealed Caves (Sirens)', lambda state: logic.has_keycard_B(state) and state.has('Elevator Keycard', player)) - connect(world, player, names, 'Varndagroth tower right (lower)', 'Military Fortress', logic.can_kill_all_3_bosses) - connect(world, player, names, 'Varndagroth tower right (lower)', 'Space time continuum', logic.has_teleport) - connect(world, player, names, 'Sealed Caves (Sirens)', 'Varndagroth tower left', lambda state: state.has('Elevator Keycard', player)) - connect(world, player, names, 'Sealed Caves (Sirens)', 'Varndagroth tower right (lower)', lambda state: state.has('Elevator Keycard', player)) - connect(world, player, names, 'Sealed Caves (Sirens)', 'Space time continuum', logic.has_teleport) - connect(world, player, names, 'Military Fortress', 'Varndagroth tower right (lower)', logic.can_kill_all_3_bosses) - connect(world, player, names, 'Military Fortress', 'Temporal Gyre', lambda state: state.has('Timespinner Wheel', player)) - connect(world, player, names, 'Military Fortress', 'Military Fortress (hangar)', logic.has_doublejump) - connect(world, player, names, 'Military Fortress (hangar)', 'Military Fortress') - connect(world, player, names, 'Military Fortress (hangar)', 'The lab', lambda state: logic.has_keycard_B(state) and logic.has_doublejump(state)) - connect(world, player, names, 'Temporal Gyre', 'Military Fortress') - connect(world, player, names, 'The lab', 'Military Fortress') - connect(world, player, names, 'The lab', 'The lab (power off)', logic.has_doublejump_of_npc) - connect(world, player, names, 'The lab (power off)', 'The lab') - connect(world, player, names, 'The lab (power off)', 'The lab (upper)', logic.has_forwarddash_doublejump) - connect(world, player, names, 'The lab (upper)', 'The lab (power off)') - connect(world, player, names, 'The lab (upper)', 'Emperors tower', logic.has_forwarddash_doublejump) - connect(world, player, names, 'The lab (upper)', 'Ancient Pyramid (entrance)', lambda state: state.has_all({'Timespinner Wheel', 'Timespinner Spindle', 'Timespinner Gear 1', 'Timespinner Gear 2', 'Timespinner Gear 3'}, player)) - connect(world, player, names, 'Emperors tower', 'The lab (upper)') - connect(world, player, names, 'Skeleton Shaft', 'Lake desolation') - connect(world, player, names, 'Skeleton Shaft', 'Sealed Caves (upper)', logic.has_keycard_A) - connect(world, player, names, 'Skeleton Shaft', 'Space time continuum', logic.has_teleport) - connect(world, player, names, 'Sealed Caves (upper)', 'Skeleton Shaft') - connect(world, player, names, 'Sealed Caves (upper)', 'Sealed Caves (Xarion)', lambda state: logic.has_teleport(state) or logic.has_doublejump(state)) - connect(world, player, names, 'Sealed Caves (Xarion)', 'Sealed Caves (upper)', logic.has_doublejump) - connect(world, player, names, 'Sealed Caves (Xarion)', 'Space time continuum', logic.has_teleport) - connect(world, player, names, 'Refugee Camp', 'Forest') - #connect(world, player, names, 'Refugee Camp', 'Library', lambda state: not is_option_enabled(world, player, "Inverted")) - connect(world, player, names, 'Refugee Camp', 'Space time continuum', logic.has_teleport) - connect(world, player, names, 'Forest', 'Refugee Camp') - connect(world, player, names, 'Forest', 'Left Side forest Caves', lambda state: state.has('Talaria Attachment', player) or logic.has_timestop(state)) - connect(world, player, names, 'Forest', 'Caves of Banishment (Sirens)') - connect(world, player, names, 'Forest', 'Castle Ramparts') - connect(world, player, names, 'Left Side forest Caves', 'Forest') - connect(world, player, names, 'Left Side forest Caves', 'Upper Lake Serene', logic.has_timestop) - connect(world, player, names, 'Left Side forest Caves', 'Lower Lake Serene', lambda state: state.has('Water Mask', player) or flooded.dry_lake_serene) - connect(world, player, names, 'Left Side forest Caves', 'Space time continuum', logic.has_teleport) - connect(world, player, names, 'Upper Lake Serene', 'Left Side forest Caves') - connect(world, player, names, 'Upper Lake Serene', 'Lower Lake Serene', lambda state: state.has('Water Mask', player)) - connect(world, player, names, 'Lower Lake Serene', 'Upper Lake Serene') - connect(world, player, names, 'Lower Lake Serene', 'Left Side forest Caves') - connect(world, player, names, 'Lower Lake Serene', 'Caves of Banishment (upper)') - connect(world, player, names, 'Caves of Banishment (upper)', 'Upper Lake Serene', lambda state: state.has('Water Mask', player) or flooded.dry_lake_serene) - connect(world, player, names, 'Caves of Banishment (upper)', 'Caves of Banishment (Maw)', lambda state: logic.has_doublejump(state) or state.has_any({'Gas Mask', 'Twin Pyramid Key'}, player)) - connect(world, player, names, 'Caves of Banishment (upper)', 'Space time continuum', logic.has_teleport) - connect(world, player, names, 'Caves of Banishment (Maw)', 'Caves of Banishment (upper)', lambda state: logic.has_doublejump(state) if not flooded.flood_maw else state.has('Water Mask', player)) - connect(world, player, names, 'Caves of Banishment (Maw)', 'Caves of Banishment (Sirens)', lambda state: state.has('Gas Mask', player)) - connect(world, player, names, 'Caves of Banishment (Maw)', 'Space time continuum', logic.has_teleport) - connect(world, player, names, 'Caves of Banishment (Sirens)', 'Forest') - connect(world, player, names, 'Castle Ramparts', 'Forest') - connect(world, player, names, 'Castle Ramparts', 'Castle Keep') - connect(world, player, names, 'Castle Ramparts', 'Space time continuum', logic.has_teleport) - connect(world, player, names, 'Castle Keep', 'Castle Ramparts') - connect(world, player, names, 'Castle Keep', 'Castle Basement', lambda state: state.has('Water Mask', player) or not flooded.flood_basement) - connect(world, player, names, 'Castle Keep', 'Royal towers (lower)', logic.has_doublejump) - connect(world, player, names, 'Castle Keep', 'Space time continuum', logic.has_teleport) - connect(world, player, names, 'Royal towers (lower)', 'Castle Keep') - connect(world, player, names, 'Royal towers (lower)', 'Royal towers', lambda state: state.has('Timespinner Wheel', player) or logic.has_forwarddash_doublejump(state)) - connect(world, player, names, 'Royal towers (lower)', 'Space time continuum', logic.has_teleport) - connect(world, player, names, 'Royal towers', 'Royal towers (lower)') - connect(world, player, names, 'Royal towers', 'Royal towers (upper)', logic.has_doublejump) - connect(world, player, names, 'Royal towers (upper)', 'Royal towers') - #connect(world, player, names, 'Ancient Pyramid (entrance)', 'The lab (upper)', lambda state: not is_option_enabled(world, player, "EnterSandman")) - connect(world, player, names, 'Ancient Pyramid (entrance)', 'Ancient Pyramid (left)', logic.has_doublejump) - connect(world, player, names, 'Ancient Pyramid (left)', 'Ancient Pyramid (entrance)') - connect(world, player, names, 'Ancient Pyramid (left)', 'Ancient Pyramid (right)', lambda state: logic.has_upwarddash(state) or flooded.flood_pyramid_shaft) - connect(world, player, names, 'Ancient Pyramid (right)', 'Ancient Pyramid (left)', lambda state: logic.has_upwarddash(state) or flooded.flood_pyramid_shaft) - connect(world, player, names, 'Space time continuum', 'Lake desolation', lambda state: logic.can_teleport_to(state, "Present", "GateLakeDesolation")) - connect(world, player, names, 'Space time continuum', 'Lower lake desolation', lambda state: logic.can_teleport_to(state, "Present", "GateKittyBoss")) - connect(world, player, names, 'Space time continuum', 'Library', lambda state: logic.can_teleport_to(state, "Present", "GateLeftLibrary")) - connect(world, player, names, 'Space time continuum', 'Varndagroth tower right (lower)', lambda state: logic.can_teleport_to(state, "Present", "GateMilitaryGate")) - connect(world, player, names, 'Space time continuum', 'Skeleton Shaft', lambda state: logic.can_teleport_to(state, "Present", "GateSealedCaves")) - connect(world, player, names, 'Space time continuum', 'Sealed Caves (Sirens)', lambda state: logic.can_teleport_to(state, "Present", "GateSealedSirensCave")) - connect(world, player, names, 'Space time continuum', 'Upper Lake Serene', lambda state: logic.can_teleport_to(state, "Past", "GateLakeSereneLeft")) - connect(world, player, names, 'Space time continuum', 'Left Side forest Caves', lambda state: logic.can_teleport_to(state, "Past", "GateLakeSereneRight")) - connect(world, player, names, 'Space time continuum', 'Refugee Camp', lambda state: logic.can_teleport_to(state, "Past", "GateAccessToPast")) - connect(world, player, names, 'Space time continuum', 'Castle Ramparts', lambda state: logic.can_teleport_to(state, "Past", "GateCastleRamparts")) - connect(world, player, names, 'Space time continuum', 'Castle Keep', lambda state: logic.can_teleport_to(state, "Past", "GateCastleKeep")) - connect(world, player, names, 'Space time continuum', 'Royal towers (lower)', lambda state: logic.can_teleport_to(state, "Past", "GateRoyalTowers")) - connect(world, player, names, 'Space time continuum', 'Caves of Banishment (Maw)', lambda state: logic.can_teleport_to(state, "Past", "GateMaw")) - connect(world, player, names, 'Space time continuum', 'Caves of Banishment (upper)', lambda state: logic.can_teleport_to(state, "Past", "GateCavesOfBanishment")) - connect(world, player, names, 'Space time continuum', 'Ancient Pyramid (entrance)', lambda state: logic.can_teleport_to(state, "Time", "GateGyre") or (not is_option_enabled(world, player, "UnchainedKeys") and is_option_enabled(world, player, "EnterSandman"))) - connect(world, player, names, 'Space time continuum', 'Ancient Pyramid (left)', lambda state: logic.can_teleport_to(state, "Time", "GateLeftPyramid")) - connect(world, player, names, 'Space time continuum', 'Ancient Pyramid (right)', lambda state: logic.can_teleport_to(state, "Time", "GateRightPyramid")) + connect(world, player, 'Lake desolation', 'Lower lake desolation', lambda state: logic.has_timestop(state) or state.has('Talaria Attachment', player) or flooded.flood_lake_desolation) + connect(world, player, 'Lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player)) + connect(world, player, 'Lake desolation', 'Skeleton Shaft', lambda state: logic.has_doublejump(state) or flooded.flood_lake_desolation) + connect(world, player, 'Lake desolation', 'Space time continuum', logic.has_teleport) + connect(world, player, 'Upper lake desolation', 'Lake desolation') + connect(world, player, 'Upper lake desolation', 'Eastern lake desolation') + connect(world, player, 'Lower lake desolation', 'Lake desolation') + connect(world, player, 'Lower lake desolation', 'Eastern lake desolation') + connect(world, player, 'Eastern lake desolation', 'Space time continuum', logic.has_teleport) + connect(world, player, 'Eastern lake desolation', 'Library') + connect(world, player, 'Eastern lake desolation', 'Lower lake desolation') + connect(world, player, 'Eastern lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player)) + connect(world, player, 'Library', 'Eastern lake desolation') + connect(world, player, 'Library', 'Library top', lambda state: logic.has_doublejump(state) or state.has('Talaria Attachment', player)) + connect(world, player, 'Library', 'Varndagroth tower left', logic.has_keycard_D) + connect(world, player, 'Library', 'Space time continuum', logic.has_teleport) + connect(world, player, 'Library top', 'Library') + connect(world, player, 'Varndagroth tower left', 'Library') + connect(world, player, 'Varndagroth tower left', 'Varndagroth tower right (upper)', logic.has_keycard_C) + connect(world, player, 'Varndagroth tower left', 'Varndagroth tower right (lower)', logic.has_keycard_B) + connect(world, player, 'Varndagroth tower left', 'Sealed Caves (Sirens)', lambda state: logic.has_keycard_B(state) and state.has('Elevator Keycard', player)) + connect(world, player, 'Varndagroth tower left', 'Refugee Camp', lambda state: state.has('Timespinner Wheel', player) and state.has('Timespinner Spindle', player)) + connect(world, player, 'Varndagroth tower right (upper)', 'Varndagroth tower left') + connect(world, player, 'Varndagroth tower right (upper)', 'Varndagroth tower right (elevator)', lambda state: state.has('Elevator Keycard', player)) + connect(world, player, 'Varndagroth tower right (elevator)', 'Varndagroth tower right (upper)') + connect(world, player, 'Varndagroth tower right (elevator)', 'Varndagroth tower right (lower)') + connect(world, player, 'Varndagroth tower right (lower)', 'Varndagroth tower left', logic.has_keycard_B) + connect(world, player, 'Varndagroth tower right (lower)', 'Varndagroth tower right (elevator)', lambda state: state.has('Elevator Keycard', player)) + connect(world, player, 'Varndagroth tower right (lower)', 'Sealed Caves (Sirens)', lambda state: logic.has_keycard_B(state) and state.has('Elevator Keycard', player)) + connect(world, player, 'Varndagroth tower right (lower)', 'Military Fortress', logic.can_kill_all_3_bosses) + connect(world, player, 'Varndagroth tower right (lower)', 'Space time continuum', logic.has_teleport) + connect(world, player, 'Sealed Caves (Sirens)', 'Varndagroth tower left', lambda state: state.has('Elevator Keycard', player)) + connect(world, player, 'Sealed Caves (Sirens)', 'Varndagroth tower right (lower)', lambda state: state.has('Elevator Keycard', player)) + connect(world, player, 'Sealed Caves (Sirens)', 'Space time continuum', logic.has_teleport) + connect(world, player, 'Military Fortress', 'Varndagroth tower right (lower)', logic.can_kill_all_3_bosses) + connect(world, player, 'Military Fortress', 'Temporal Gyre', lambda state: state.has('Timespinner Wheel', player)) + connect(world, player, 'Military Fortress', 'Military Fortress (hangar)', logic.has_doublejump) + connect(world, player, 'Military Fortress (hangar)', 'Military Fortress') + connect(world, player, 'Military Fortress (hangar)', 'The lab', lambda state: logic.has_keycard_B(state) and logic.has_doublejump(state)) + connect(world, player, 'Temporal Gyre', 'Military Fortress') + connect(world, player, 'The lab', 'Military Fortress') + connect(world, player, 'The lab', 'The lab (power off)', logic.has_doublejump_of_npc) + connect(world, player, 'The lab (power off)', 'The lab') + connect(world, player, 'The lab (power off)', 'The lab (upper)', logic.has_forwarddash_doublejump) + connect(world, player, 'The lab (upper)', 'The lab (power off)') + connect(world, player, 'The lab (upper)', 'Emperors tower', logic.has_forwarddash_doublejump) + connect(world, player, 'The lab (upper)', 'Ancient Pyramid (entrance)', lambda state: state.has_all({'Timespinner Wheel', 'Timespinner Spindle', 'Timespinner Gear 1', 'Timespinner Gear 2', 'Timespinner Gear 3'}, player)) + connect(world, player, 'Emperors tower', 'The lab (upper)') + connect(world, player, 'Skeleton Shaft', 'Lake desolation') + connect(world, player, 'Skeleton Shaft', 'Sealed Caves (upper)', logic.has_keycard_A) + connect(world, player, 'Skeleton Shaft', 'Space time continuum', logic.has_teleport) + connect(world, player, 'Sealed Caves (upper)', 'Skeleton Shaft') + connect(world, player, 'Sealed Caves (upper)', 'Sealed Caves (Xarion)', lambda state: logic.has_teleport(state) or logic.has_doublejump(state)) + connect(world, player, 'Sealed Caves (Xarion)', 'Sealed Caves (upper)', logic.has_doublejump) + connect(world, player, 'Sealed Caves (Xarion)', 'Space time continuum', logic.has_teleport) + connect(world, player, 'Refugee Camp', 'Forest') + #connect(world, player, 'Refugee Camp', 'Library', lambda state: not is_option_enabled(world, player, "Inverted")) + connect(world, player, 'Refugee Camp', 'Space time continuum', logic.has_teleport) + connect(world, player, 'Forest', 'Refugee Camp') + connect(world, player, 'Forest', 'Left Side forest Caves', lambda state: state.has('Talaria Attachment', player) or logic.has_timestop(state)) + connect(world, player, 'Forest', 'Caves of Banishment (Sirens)') + connect(world, player, 'Forest', 'Castle Ramparts') + connect(world, player, 'Left Side forest Caves', 'Forest') + connect(world, player, 'Left Side forest Caves', 'Upper Lake Serene', logic.has_timestop) + connect(world, player, 'Left Side forest Caves', 'Lower Lake Serene', lambda state: state.has('Water Mask', player) or flooded.dry_lake_serene) + connect(world, player, 'Left Side forest Caves', 'Space time continuum', logic.has_teleport) + connect(world, player, 'Upper Lake Serene', 'Left Side forest Caves') + connect(world, player, 'Upper Lake Serene', 'Lower Lake Serene', lambda state: state.has('Water Mask', player) or flooded.dry_lake_serene) + connect(world, player, 'Lower Lake Serene', 'Upper Lake Serene') + connect(world, player, 'Lower Lake Serene', 'Left Side forest Caves') + connect(world, player, 'Lower Lake Serene', 'Caves of Banishment (upper)', lambda state: not flooded.dry_lake_serene or logic.has_doublejump(state)) + connect(world, player, 'Caves of Banishment (upper)', 'Upper Lake Serene', lambda state: state.has('Water Mask', player) or flooded.dry_lake_serene) + connect(world, player, 'Caves of Banishment (upper)', 'Caves of Banishment (Maw)', lambda state: logic.has_doublejump(state) or state.has_any({'Gas Mask', 'Talaria Attachment'} or logic.has_teleport(state), player)) + connect(world, player, 'Caves of Banishment (upper)', 'Space time continuum', logic.has_teleport) + connect(world, player, 'Caves of Banishment (Maw)', 'Caves of Banishment (upper)', lambda state: logic.has_doublejump(state) if not flooded.flood_maw else state.has('Water Mask', player)) + connect(world, player, 'Caves of Banishment (Maw)', 'Caves of Banishment (Sirens)', lambda state: state.has_any({'Gas Mask', 'Talaria Attachment'}, player) ) + connect(world, player, 'Caves of Banishment (Maw)', 'Space time continuum', logic.has_teleport) + connect(world, player, 'Caves of Banishment (Sirens)', 'Forest') + connect(world, player, 'Castle Ramparts', 'Forest') + connect(world, player, 'Castle Ramparts', 'Castle Keep') + connect(world, player, 'Castle Ramparts', 'Space time continuum', logic.has_teleport) + connect(world, player, 'Castle Keep', 'Castle Ramparts') + connect(world, player, 'Castle Keep', 'Castle Basement', lambda state: state.has('Water Mask', player) or not flooded.flood_basement) + connect(world, player, 'Castle Keep', 'Royal towers (lower)', logic.has_doublejump) + connect(world, player, 'Castle Keep', 'Space time continuum', logic.has_teleport) + connect(world, player, 'Royal towers (lower)', 'Castle Keep') + connect(world, player, 'Royal towers (lower)', 'Royal towers', lambda state: state.has('Timespinner Wheel', player) or logic.has_forwarddash_doublejump(state)) + connect(world, player, 'Royal towers (lower)', 'Space time continuum', logic.has_teleport) + connect(world, player, 'Royal towers', 'Royal towers (lower)') + connect(world, player, 'Royal towers', 'Royal towers (upper)', logic.has_doublejump) + connect(world, player, 'Royal towers (upper)', 'Royal towers') + #connect(world, player, 'Ancient Pyramid (entrance)', 'The lab (upper)', lambda state: not is_option_enabled(world, player, "EnterSandman")) + connect(world, player, 'Ancient Pyramid (entrance)', 'Ancient Pyramid (left)', logic.has_doublejump) + connect(world, player, 'Ancient Pyramid (left)', 'Ancient Pyramid (entrance)') + connect(world, player, 'Ancient Pyramid (left)', 'Ancient Pyramid (right)', lambda state: logic.has_upwarddash(state) or flooded.flood_pyramid_shaft) + connect(world, player, 'Ancient Pyramid (right)', 'Ancient Pyramid (left)', lambda state: logic.has_upwarddash(state) or flooded.flood_pyramid_shaft) + connect(world, player, 'Space time continuum', 'Lake desolation', lambda state: logic.can_teleport_to(state, "Present", "GateLakeDesolation")) + connect(world, player, 'Space time continuum', 'Lower lake desolation', lambda state: logic.can_teleport_to(state, "Present", "GateKittyBoss")) + connect(world, player, 'Space time continuum', 'Library', lambda state: logic.can_teleport_to(state, "Present", "GateLeftLibrary")) + connect(world, player, 'Space time continuum', 'Varndagroth tower right (lower)', lambda state: logic.can_teleport_to(state, "Present", "GateMilitaryGate")) + connect(world, player, 'Space time continuum', 'Skeleton Shaft', lambda state: logic.can_teleport_to(state, "Present", "GateSealedCaves")) + connect(world, player, 'Space time continuum', 'Sealed Caves (Sirens)', lambda state: logic.can_teleport_to(state, "Present", "GateSealedSirensCave")) + connect(world, player, 'Space time continuum', 'Upper Lake Serene', lambda state: logic.can_teleport_to(state, "Past", "GateLakeSereneLeft")) + connect(world, player, 'Space time continuum', 'Left Side forest Caves', lambda state: logic.can_teleport_to(state, "Past", "GateLakeSereneRight")) + connect(world, player, 'Space time continuum', 'Refugee Camp', lambda state: logic.can_teleport_to(state, "Past", "GateAccessToPast")) + connect(world, player, 'Space time continuum', 'Castle Ramparts', lambda state: logic.can_teleport_to(state, "Past", "GateCastleRamparts")) + connect(world, player, 'Space time continuum', 'Castle Keep', lambda state: logic.can_teleport_to(state, "Past", "GateCastleKeep")) + connect(world, player, 'Space time continuum', 'Royal towers (lower)', lambda state: logic.can_teleport_to(state, "Past", "GateRoyalTowers")) + connect(world, player, 'Space time continuum', 'Caves of Banishment (Maw)', lambda state: logic.can_teleport_to(state, "Past", "GateMaw")) + connect(world, player, 'Space time continuum', 'Caves of Banishment (upper)', lambda state: logic.can_teleport_to(state, "Past", "GateCavesOfBanishment")) + connect(world, player, 'Space time continuum', 'Ancient Pyramid (entrance)', lambda state: logic.can_teleport_to(state, "Time", "GateGyre") or (not is_option_enabled(world, player, "UnchainedKeys") and is_option_enabled(world, player, "EnterSandman"))) + connect(world, player, 'Space time continuum', 'Ancient Pyramid (left)', lambda state: logic.can_teleport_to(state, "Time", "GateLeftPyramid")) + connect(world, player, 'Space time continuum', 'Ancient Pyramid (right)', lambda state: logic.can_teleport_to(state, "Time", "GateRightPyramid")) if is_option_enabled(world, player, "GyreArchives"): - connect(world, player, names, 'The lab (upper)', 'Ravenlord\'s Lair', lambda state: state.has('Merchant Crow', player)) - connect(world, player, names, 'Ravenlord\'s Lair', 'The lab (upper)') - connect(world, player, names, 'Library top', 'Ifrit\'s Lair', lambda state: state.has('Kobo', player) and state.can_reach('Refugee Camp', 'Region', player)) - connect(world, player, names, 'Ifrit\'s Lair', 'Library top') + connect(world, player, 'The lab (upper)', 'Ravenlord\'s Lair', lambda state: state.has('Merchant Crow', player)) + connect(world, player, 'Ravenlord\'s Lair', 'The lab (upper)') + connect(world, player, 'Library top', 'Ifrit\'s Lair', lambda state: state.has('Kobo', player) and state.can_reach('Refugee Camp', 'Region', player)) + connect(world, player, 'Ifrit\'s Lair', 'Library top') def throwIfAnyLocationIsNotAssignedToARegion(regions: List[Region], regionNames: Set[str]): @@ -203,7 +202,7 @@ def throwIfAnyLocationIsNotAssignedToARegion(regions: List[Region], regionNames: raise Exception("Timespinner: the following regions are used in locations: {}, but no such region exists".format(regionNames - existingRegions)) -def create_location(player: int, location_data: LocationData, region: Region, location_cache: List[Location]) -> Location: +def create_location(player: int, location_data: LocationData, region: Region) -> Location: location = Location(player, location_data.name, location_data.code, region) location.access_rule = location_data.rule @@ -211,17 +210,15 @@ def create_location(player: int, location_data: LocationData, region: Region, lo location.event = True location.locked = True - location_cache.append(location) - return location -def create_region(world: MultiWorld, player: int, locations_per_region: Dict[str, List[LocationData]], location_cache: List[Location], name: str) -> Region: +def create_region(world: MultiWorld, player: int, locations_per_region: Dict[str, List[LocationData]], name: str) -> Region: region = Region(name, player, world) if name in locations_per_region: for location_data in locations_per_region[name]: - location = create_location(player, location_data, region, location_cache) + location = create_location(player, location_data, region) region.locations.append(location) return region @@ -250,19 +247,13 @@ def connectStartingRegion(world: MultiWorld, player: int): space_time_continuum.exits.append(teleport_back_to_start) -def connect(world: MultiWorld, player: int, used_names: Dict[str, int], source: str, target: str, +def connect(world: MultiWorld, player: int, source: str, target: str, rule: Optional[Callable[[CollectionState], bool]] = None): + sourceRegion = world.get_region(source, player) targetRegion = world.get_region(target, player) - if target not in used_names: - used_names[target] = 1 - name = target - else: - used_names[target] += 1 - name = target + (' ' * used_names[target]) - - connection = Entrance(player, name, sourceRegion) + connection = Entrance(player, "", sourceRegion) if rule: connection.access_rule = rule @@ -271,7 +262,7 @@ def connect(world: MultiWorld, player: int, used_names: Dict[str, int], source: connection.connect(targetRegion) -def get_locations_per_region(locations: Tuple[LocationData, ...]) -> Dict[str, List[LocationData]]: +def split_location_datas_per_region(locations: Tuple[LocationData, ...]) -> Dict[str, List[LocationData]]: per_region: Dict[str, List[LocationData]] = {} for location in locations: diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index cb52459b52..de1d58e961 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -1,10 +1,11 @@ -from typing import Dict, List, Set, Tuple, TextIO -from BaseClasses import Item, MultiWorld, Location, Tutorial, ItemClassification -from .Items import get_item_names_per_category, item_table, starter_melee_weapons, starter_spells, filler_items -from .Locations import get_locations, EventId +from typing import Dict, List, Set, Tuple, TextIO, Union +from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification +from .Items import get_item_names_per_category +from .Items import item_table, starter_melee_weapons, starter_spells, filler_items, starter_progression_items +from .Locations import get_location_datas, EventId from .Options import is_option_enabled, get_option_value, timespinner_options from .PreCalculatedWeights import PreCalculatedWeights -from .Regions import create_regions +from .Regions import create_regions_and_locations from worlds.AutoWorld import World, WebWorld class TimespinnerWebWorld(WebWorld): @@ -29,7 +30,6 @@ class TimespinnerWebWorld(WebWorld): tutorials = [setup, setup_de] - class TimespinnerWorld(World): """ Timespinner is a beautiful metroidvania inspired by classic 90s action-platformers. @@ -44,21 +44,16 @@ class TimespinnerWorld(World): required_client_version = (0, 3, 7) item_name_to_id = {name: data.code for name, data in item_table.items()} - location_name_to_id = {location.name: location.code for location in get_locations(None, None, None)} + location_name_to_id = {location.name: location.code for location in get_location_datas(None, None, None)} item_name_groups = get_item_names_per_category() - locked_locations: List[str] - location_cache: List[Location] precalculated_weights: PreCalculatedWeights def __init__(self, world: MultiWorld, player: int): super().__init__(world, player) - - self.locked_locations = [] - self.location_cache = [] self.precalculated_weights = PreCalculatedWeights(world, player) - def generate_early(self): + def generate_early(self) -> None: # in generate_early the start_inventory isnt copied over to precollected_items yet, so we can still modify the options directly if self.multiworld.start_inventory[self.player].value.pop('Meyef', 0) > 0: self.multiworld.StartWithMeyef[self.player].value = self.multiworld.StartWithMeyef[self.player].option_true @@ -67,44 +62,28 @@ class TimespinnerWorld(World): if self.multiworld.start_inventory[self.player].value.pop('Jewelry Box', 0) > 0: self.multiworld.StartWithJewelryBox[self.player].value = self.multiworld.StartWithJewelryBox[self.player].option_true - def create_regions(self): - locations = get_locations(self.multiworld, self.player, self.precalculated_weights) - create_regions(self.multiworld, self.player, locations, self.location_cache, self.precalculated_weights) + def create_regions(self) -> None: + create_regions_and_locations(self.multiworld, self.player, self.precalculated_weights) - def create_item(self, name: str) -> Item: - return create_item_with_correct_settings(self.multiworld, self.player, name) + def create_items(self) -> None: + self.create_and_assign_event_items() - def get_filler_item_name(self) -> str: - trap_chance: int = get_option_value(self.multiworld, self.player, "TrapChance") - enabled_traps: List[str] = get_option_value(self.multiworld, self.player, "Traps") + excluded_items: Set[str] = self.get_excluded_items() - if self.multiworld.random.random() < (trap_chance / 100) and enabled_traps: - return self.multiworld.random.choice(enabled_traps) - else: - return self.multiworld.random.choice(filler_items) + self.assign_starter_items(excluded_items) + self.place_first_progression_item(excluded_items) - def set_rules(self): - setup_events(self.player, self.locked_locations, self.location_cache) + self.multiworld.itempool += self.get_item_pool(excluded_items) + def set_rules(self) -> None: final_boss: str - if is_option_enabled(self.multiworld, self.player, "DadPercent"): + if self.is_option_enabled("DadPercent"): final_boss = "Killed Emperor" else: final_boss = "Killed Nightmare" self.multiworld.completion_condition[self.player] = lambda state: state.has(final_boss, self.player) - def generate_basic(self): - excluded_items: Set[str] = get_excluded_items(self, self.multiworld, self.player) - - assign_starter_items(self.multiworld, self.player, excluded_items, self.locked_locations) - - pool = get_item_pool(self.multiworld, self.player, excluded_items) - - fill_item_pool_with_dummy_items(self, self.multiworld, self.player, self.locked_locations, self.location_cache, pool) - - self.multiworld.itempool += pool - def fill_slot_data(self) -> Dict[str, object]: slot_data: Dict[str, object] = {} @@ -112,12 +91,12 @@ class TimespinnerWorld(World): for option_name in timespinner_options: if (option_name not in ap_specific_settings): - slot_data[option_name] = get_option_value(self.multiworld, self.player, option_name) + slot_data[option_name] = self.get_option_value(option_name) slot_data["StinkyMaw"] = True slot_data["ProgressiveVerticalMovement"] = False slot_data["ProgressiveKeycards"] = False - slot_data["PersonalItems"] = get_personal_items(self.player, self.location_cache) + slot_data["PersonalItems"] = self.get_personal_items() slot_data["PyramidKeysGate"] = self.precalculated_weights.pyramid_keys_unlock slot_data["PresentGate"] = self.precalculated_weights.present_key_unlock slot_data["PastGate"] = self.precalculated_weights.past_key_unlock @@ -135,17 +114,17 @@ class TimespinnerWorld(World): return slot_data - def write_spoiler_header(self, spoiler_handle: TextIO): - if is_option_enabled(self.multiworld, self.player, "UnchainedKeys"): + def write_spoiler_header(self, spoiler_handle: TextIO) -> None: + if self.is_option_enabled("UnchainedKeys"): spoiler_handle.write(f'Modern Warp Beacon unlock: {self.precalculated_weights.present_key_unlock}\n') spoiler_handle.write(f'Timeworn Warp Beacon unlock: {self.precalculated_weights.past_key_unlock}\n') - if is_option_enabled(self.multiworld, self.player, "EnterSandman"): + if self.is_option_enabled("EnterSandman"): spoiler_handle.write(f'Mysterious Warp Beacon unlock: {self.precalculated_weights.time_key_unlock}\n') else: spoiler_handle.write(f'Twin Pyramid Keys unlock: {self.precalculated_weights.pyramid_keys_unlock}\n') - if is_option_enabled(self.multiworld, self.player, "RisingTides"): + if self.is_option_enabled("RisingTides"): flooded_areas: List[str] = [] if self.precalculated_weights.flood_basement: @@ -167,8 +146,8 @@ class TimespinnerWorld(World): flooded_areas.append("Castle Courtyard") if self.precalculated_weights.flood_lake_desolation: flooded_areas.append("Lake Desolation") - if self.precalculated_weights.dry_lake_serene: - flooded_areas.append("Dry Lake Serene") + if not self.precalculated_weights.dry_lake_serene: + flooded_areas.append("Lake Serene") if len(flooded_areas) == 0: flooded_areas_string: str = "None" @@ -177,133 +156,154 @@ class TimespinnerWorld(World): spoiler_handle.write(f'Flooded Areas: {flooded_areas_string}\n') + def create_item(self, name: str) -> Item: + data = item_table[name] -def get_excluded_items(self: TimespinnerWorld, world: MultiWorld, player: int) -> Set[str]: - excluded_items: Set[str] = set() - - if is_option_enabled(world, player, "StartWithJewelryBox"): - excluded_items.add('Jewelry Box') - if is_option_enabled(world, player, "StartWithMeyef"): - excluded_items.add('Meyef') - if is_option_enabled(world, player, "QuickSeed"): - excluded_items.add('Talaria Attachment') - - if is_option_enabled(world, player, "UnchainedKeys"): - excluded_items.add('Twin Pyramid Key') - - if not is_option_enabled(world, player, "EnterSandman"): - excluded_items.add('Mysterious Warp Beacon') - else: - excluded_items.add('Timeworn Warp Beacon') - excluded_items.add('Modern Warp Beacon') - excluded_items.add('Mysterious Warp Beacon') - - for item in world.precollected_items[player]: - if item.name not in self.item_name_groups['UseItem']: - excluded_items.add(item.name) - - return excluded_items - - -def assign_starter_items(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]): - non_local_items = world.non_local_items[player].value - - local_starter_melee_weapons = tuple(item for item in starter_melee_weapons if item not in non_local_items) - if not local_starter_melee_weapons: - if 'Plasma Orb' in non_local_items: - raise Exception("Atleast one melee orb must be local") + if data.useful: + classification = ItemClassification.useful + elif data.progression: + classification = ItemClassification.progression + elif data.trap: + classification = ItemClassification.trap else: - local_starter_melee_weapons = ('Plasma Orb',) + classification = ItemClassification.filler + + item = Item(name, classification, data.code, self.player) - local_starter_spells = tuple(item for item in starter_spells if item not in non_local_items) - if not local_starter_spells: - if 'Lightwall' in non_local_items: - raise Exception("Atleast one spell must be local") - else: - local_starter_spells = ('Lightwall',) + if not item.advancement: + return item - assign_starter_item(world, player, excluded_items, locked_locations, 'Tutorial: Yo Momma 1', local_starter_melee_weapons) - assign_starter_item(world, player, excluded_items, locked_locations, 'Tutorial: Yo Momma 2', local_starter_spells) + if (name == 'Tablet' or name == 'Library Keycard V') and not self.is_option_enabled("DownloadableItems"): + item.classification = ItemClassification.filler + elif name == 'Oculus Ring' and not self.is_option_enabled("EyeSpy"): + item.classification = ItemClassification.filler + elif (name == 'Kobo' or name == 'Merchant Crow') and not self.is_option_enabled("GyreArchives"): + item.classification = ItemClassification.filler + elif name in {"Timeworn Warp Beacon", "Modern Warp Beacon", "Mysterious Warp Beacon"} \ + and not self.is_option_enabled("UnchainedKeys"): + item.classification = ItemClassification.filler - -def assign_starter_item(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str], - location: str, item_list: Tuple[str, ...]): - - item_name = world.random.choice(item_list) - - excluded_items.add(item_name) - - item = create_item_with_correct_settings(world, player, item_name) - - world.get_location(location, player).place_locked_item(item) - - locked_locations.append(location) - - -def get_item_pool(world: MultiWorld, player: int, excluded_items: Set[str]) -> List[Item]: - pool: List[Item] = [] - - for name, data in item_table.items(): - if name not in excluded_items: - for _ in range(data.count): - item = create_item_with_correct_settings(world, player, name) - pool.append(item) - - return pool - - -def fill_item_pool_with_dummy_items(self: TimespinnerWorld, world: MultiWorld, player: int, locked_locations: List[str], - location_cache: List[Location], pool: List[Item]): - for _ in range(len(location_cache) - len(locked_locations) - len(pool)): - item = create_item_with_correct_settings(world, player, self.get_filler_item_name()) - pool.append(item) - - -def create_item_with_correct_settings(world: MultiWorld, player: int, name: str) -> Item: - data = item_table[name] - - if data.useful: - classification = ItemClassification.useful - elif data.progression: - classification = ItemClassification.progression - elif data.trap: - classification = ItemClassification.trap - else: - classification = ItemClassification.filler - - item = Item(name, classification, data.code, player) - - if not item.advancement: return item - if (name == 'Tablet' or name == 'Library Keycard V') and not is_option_enabled(world, player, "DownloadableItems"): - item.classification = ItemClassification.filler - elif name == 'Oculus Ring' and not is_option_enabled(world, player, "EyeSpy"): - item.classification = ItemClassification.filler - elif (name == 'Kobo' or name == 'Merchant Crow') and not is_option_enabled(world, player, "GyreArchives"): - item.classification = ItemClassification.filler - elif name in {"Timeworn Warp Beacon", "Modern Warp Beacon", "Mysterious Warp Beacon"} \ - and not is_option_enabled(world, player, "UnchainedKeys"): - item.classification = ItemClassification.filler + def get_filler_item_name(self) -> str: + trap_chance: int = self.get_option_value("TrapChance") + enabled_traps: List[str] = self.get_option_value("Traps") - return item + if self.multiworld.random.random() < (trap_chance / 100) and enabled_traps: + return self.multiworld.random.choice(enabled_traps) + else: + return self.multiworld.random.choice(filler_items) + def get_excluded_items(self) -> Set[str]: + excluded_items: Set[str] = set() -def setup_events(player: int, locked_locations: List[str], location_cache: List[Location]): - for location in location_cache: - if location.address == EventId: - item = Item(location.name, ItemClassification.progression, EventId, player) + if self.is_option_enabled("StartWithJewelryBox"): + excluded_items.add('Jewelry Box') + if self.is_option_enabled("StartWithMeyef"): + excluded_items.add('Meyef') + if self.is_option_enabled("QuickSeed"): + excluded_items.add('Talaria Attachment') - locked_locations.append(location.name) + if self.is_option_enabled("UnchainedKeys"): + excluded_items.add('Twin Pyramid Key') - location.place_locked_item(item) + if not self.is_option_enabled("EnterSandman"): + excluded_items.add('Mysterious Warp Beacon') + else: + excluded_items.add('Timeworn Warp Beacon') + excluded_items.add('Modern Warp Beacon') + excluded_items.add('Mysterious Warp Beacon') + for item in self.multiworld.precollected_items[self.player]: + if item.name not in self.item_name_groups['UseItem']: + excluded_items.add(item.name) -def get_personal_items(player: int, locations: List[Location]) -> Dict[int, int]: - personal_items: Dict[int, int] = {} + return excluded_items - for location in locations: - if location.address and location.item and location.item.code and location.item.player == player: - personal_items[location.address] = location.item.code + def assign_starter_items(self, excluded_items: Set[str]) -> None: + non_local_items: Set[str] = self.multiworld.non_local_items[self.player].value - return personal_items + local_starter_melee_weapons = tuple(item for item in starter_melee_weapons if item not in non_local_items) + if not local_starter_melee_weapons: + if 'Plasma Orb' in non_local_items: + raise Exception("Atleast one melee orb must be local") + else: + local_starter_melee_weapons = ('Plasma Orb',) + + local_starter_spells = tuple(item for item in starter_spells if item not in non_local_items) + if not local_starter_spells: + if 'Lightwall' in non_local_items: + raise Exception("Atleast one spell must be local") + else: + local_starter_spells = ('Lightwall',) + + self.assign_starter_item(excluded_items, 'Tutorial: Yo Momma 1', local_starter_melee_weapons) + self.assign_starter_item(excluded_items, 'Tutorial: Yo Momma 2', local_starter_spells) + + def assign_starter_item(self, excluded_items: Set[str], location: str, item_list: Tuple[str, ...]) -> None: + item_name = self.multiworld.random.choice(item_list) + + self.place_locked_item(excluded_items, location, item_name) + + def place_first_progression_item(self, excluded_items: Set[str]) -> None: + if self.is_option_enabled("QuickSeed") or self.is_option_enabled("Inverted") \ + or self.precalculated_weights.flood_lake_desolation: + return + + for item in self.multiworld.precollected_items[self.player]: + if item.name in starter_progression_items and not item.name in excluded_items: + return + + local_starter_progression_items = tuple( + item for item in starter_progression_items + if item not in excluded_items and item not in self.multiworld.non_local_items[self.player].value) + + if not local_starter_progression_items: + return + + progression_item = self.multiworld.random.choice(local_starter_progression_items) + + self.multiworld.local_early_items[self.player][progression_item] = 1 + + def place_locked_item(self, excluded_items: Set[str], location: str, item: str) -> None: + excluded_items.add(item) + + item = self.create_item(item) + + self.multiworld.get_location(location, self.player).place_locked_item(item) + + def get_item_pool(self, excluded_items: Set[str]) -> List[Item]: + pool: List[Item] = [] + + for name, data in item_table.items(): + if name not in excluded_items: + for _ in range(data.count): + item = self.create_item(name) + pool.append(item) + + for _ in range(len(self.multiworld.get_unfilled_locations(self.player)) - len(pool)): + item = self.create_item(self.get_filler_item_name()) + pool.append(item) + + return pool + + def create_and_assign_event_items(self) -> None: + for location in self.multiworld.get_locations(self.player): + if location.address == EventId: + item = Item(location.name, ItemClassification.progression, EventId, self.player) + location.place_locked_item(item) + + def get_personal_items(self) -> Dict[int, int]: + personal_items: Dict[int, int] = {} + + for location in self.multiworld.get_locations(self.player): + if location.address and location.item and location.item.code and location.item.player == self.player: + personal_items[location.address] = location.item.code + + return personal_items + + def is_option_enabled(self, option: str) -> bool: + return is_option_enabled(self.multiworld, self.player, option) + + def get_option_value(self, option: str) -> Union[int, Dict, List]: + return get_option_value(self.multiworld, self.player, option) diff --git a/worlds/tloz/ItemPool.py b/worlds/tloz/ItemPool.py new file mode 100644 index 0000000000..9dcf1b7aef --- /dev/null +++ b/worlds/tloz/ItemPool.py @@ -0,0 +1,147 @@ +from BaseClasses import ItemClassification +from .Locations import level_locations, all_level_locations, standard_level_locations, shop_locations +from .Options import TriforceLocations, StartingPosition + +# Swords are in starting_weapons +overworld_items = { + "Letter": 1, + "Power Bracelet": 1, + "Heart Container": 1, + "Sword": 1 +} + +# Bomb, Arrow, 1 Small Key and Red Water of Life are in guaranteed_shop_items +shop_items = { + "Magical Shield": 3, + "Food": 2, + "Small Key": 1, + "Candle": 1, + "Recovery Heart": 1, + "Blue Ring": 1, + "Water of Life (Blue)": 1 +} + +# Magical Rod and Red Candle are in starting_weapons, Triforce Fragments are added in its section of get_pool_core +major_dungeon_items = { + "Heart Container": 8, + "Bow": 1, + "Boomerang": 1, + "Magical Boomerang": 1, + "Raft": 1, + "Stepladder": 1, + "Recorder": 1, + "Magical Key": 1, + "Book of Magic": 1, + "Silver Arrow": 1, + "Red Ring": 1 +} + +minor_dungeon_items = { + "Bomb": 23, + "Small Key": 45, + "Five Rupees": 17 +} + +take_any_items = { + "Heart Container": 4 +} + +# Map/Compasses: 18 +# Reasoning: Adding some variety to the vanilla game. + +map_compass_replacements = { + "Fairy": 6, + "Clock": 3, + "Water of Life (Red)": 1, + "Water of Life (Blue)": 2, + "Bomb": 2, + "Small Key": 2, + "Five Rupees": 2 +} +basic_pool = { + item: overworld_items.get(item, 0) + shop_items.get(item, 0) + + major_dungeon_items.get(item, 0) + map_compass_replacements.get(item, 0) + for item in set(overworld_items) | set(shop_items) | set(major_dungeon_items) | set(map_compass_replacements) +} + +starting_weapons = ["Sword", "White Sword", "Magical Sword", "Magical Rod", "Red Candle"] +guaranteed_shop_items = ["Small Key", "Bomb", "Water of Life (Red)", "Arrow"] +starting_weapon_locations = ["Starting Sword Cave", "Letter Cave", "Armos Knights"] +dangerous_weapon_locations = [ + "Level 1 Compass", "Level 2 Bomb Drop (Keese)", "Level 3 Key Drop (Zols Entrance)", "Level 3 Compass"] + +def generate_itempool(tlozworld): + (pool, placed_items) = get_pool_core(tlozworld) + tlozworld.multiworld.itempool.extend([tlozworld.multiworld.create_item(item, tlozworld.player) for item in pool]) + for (location_name, item) in placed_items.items(): + location = tlozworld.multiworld.get_location(location_name, tlozworld.player) + location.place_locked_item(tlozworld.multiworld.create_item(item, tlozworld.player)) + if item == "Bomb": + location.item.classification = ItemClassification.progression + +def get_pool_core(world): + random = world.multiworld.random + + pool = [] + placed_items = {} + minor_items = dict(minor_dungeon_items) + + # Guaranteed Shop Items + reserved_store_slots = random.sample(shop_locations[0:9], 4) + for location, item in zip(reserved_store_slots, guaranteed_shop_items): + placed_items[location] = item + + # Starting Weapon + starting_weapon = random.choice(starting_weapons) + if world.multiworld.StartingPosition[world.player] == StartingPosition.option_safe: + placed_items[starting_weapon_locations[0]] = starting_weapon + elif world.multiworld.StartingPosition[world.player] in \ + [StartingPosition.option_unsafe, StartingPosition.option_dangerous]: + if world.multiworld.StartingPosition[world.player] == StartingPosition.option_dangerous: + for location in dangerous_weapon_locations: + if world.multiworld.ExpandedPool[world.player] or "Drop" not in location: + starting_weapon_locations.append(location) + placed_items[random.choice(starting_weapon_locations)] = starting_weapon + else: + pool.append(starting_weapon) + for other_weapons in starting_weapons: + if other_weapons != starting_weapon: + pool.append(other_weapons) + + # Triforce Fragments + fragment = "Triforce Fragment" + if world.multiworld.ExpandedPool[world.player]: + possible_level_locations = [location for location in all_level_locations + if location not in level_locations[8]] + else: + possible_level_locations = [location for location in standard_level_locations + if location not in level_locations[8]] + for level in range(1, 9): + if world.multiworld.TriforceLocations[world.player] == TriforceLocations.option_vanilla: + placed_items[f"Level {level} Triforce"] = fragment + elif world.multiworld.TriforceLocations[world.player] == TriforceLocations.option_dungeons: + placed_items[possible_level_locations.pop(random.randint(0, len(possible_level_locations) - 1))] = fragment + else: + pool.append(fragment) + + # Level 9 junk fill + if world.multiworld.ExpandedPool[world.player] > 0: + spots = random.sample(level_locations[8], len(level_locations[8]) // 2) + for spot in spots: + junk = random.choice(list(minor_items.keys())) + placed_items[spot] = junk + minor_items[junk] -= 1 + + # Finish Pool + final_pool = basic_pool + if world.multiworld.ExpandedPool[world.player]: + final_pool = { + item: basic_pool.get(item, 0) + minor_items.get(item, 0) + take_any_items.get(item, 0) + for item in set(basic_pool) | set(minor_items) | set(take_any_items) + } + final_pool["Five Rupees"] -= 1 + for item in final_pool.keys(): + for i in range(0, final_pool[item]): + pool.append(item) + + return pool, placed_items diff --git a/worlds/tloz/Items.py b/worlds/tloz/Items.py new file mode 100644 index 0000000000..d896d11d77 --- /dev/null +++ b/worlds/tloz/Items.py @@ -0,0 +1,147 @@ +from BaseClasses import ItemClassification +import typing +from typing import Dict + +progression = ItemClassification.progression +filler = ItemClassification.filler +useful = ItemClassification.useful +trap = ItemClassification.trap + + +class ItemData(typing.NamedTuple): + code: typing.Optional[int] + classification: ItemClassification + + +item_table: Dict[str, ItemData] = { + "Boomerang": ItemData(100, useful), + "Bow": ItemData(101, progression), + "Magical Boomerang": ItemData(102, useful), + "Raft": ItemData(103, progression), + "Stepladder": ItemData(104, progression), + "Recorder": ItemData(105, progression), + "Magical Rod": ItemData(106, progression), + "Red Candle": ItemData(107, progression), + "Book of Magic": ItemData(108, progression), + "Magical Key": ItemData(109, useful), + "Red Ring": ItemData(110, useful), + "Silver Arrow": ItemData(111, progression), + "Sword": ItemData(112, progression), + "White Sword": ItemData(113, progression), + "Magical Sword": ItemData(114, progression), + "Heart Container": ItemData(115, progression), + "Letter": ItemData(116, progression), + "Magical Shield": ItemData(117, useful), + "Candle": ItemData(118, progression), + "Arrow": ItemData(119, progression), + "Food": ItemData(120, progression), + "Water of Life (Blue)": ItemData(121, useful), + "Water of Life (Red)": ItemData(122, useful), + "Blue Ring": ItemData(123, useful), + "Triforce Fragment": ItemData(124, progression), + "Power Bracelet": ItemData(125, useful), + "Small Key": ItemData(126, filler), + "Bomb": ItemData(127, filler), + "Recovery Heart": ItemData(128, filler), + "Five Rupees": ItemData(129, filler), + "Rupee": ItemData(130, filler), + "Clock": ItemData(131, filler), + "Fairy": ItemData(132, filler) + +} + +item_game_ids = { + "Bomb": 0x00, + "Sword": 0x01, + "White Sword": 0x02, + "Magical Sword": 0x03, + "Food": 0x04, + "Recorder": 0x05, + "Candle": 0x06, + "Red Candle": 0x07, + "Arrow": 0x08, + "Silver Arrow": 0x09, + "Bow": 0x0A, + "Magical Key": 0x0B, + "Raft": 0x0C, + "Stepladder": 0x0D, + "Five Rupees": 0x0F, + "Magical Rod": 0x10, + "Book of Magic": 0x11, + "Blue Ring": 0x12, + "Red Ring": 0x13, + "Power Bracelet": 0x14, + "Letter": 0x15, + "Small Key": 0x19, + "Heart Container": 0x1A, + "Triforce Fragment": 0x1B, + "Magical Shield": 0x1C, + "Boomerang": 0x1D, + "Magical Boomerang": 0x1E, + "Water of Life (Blue)": 0x1F, + "Water of Life (Red)": 0x20, + "Recovery Heart": 0x22, + "Rupee": 0x18, + "Clock": 0x21, + "Fairy": 0x23 +} + +# Item prices are going to get a bit of a writeup here, because these are some seemingly arbitrary +# design decisions and future contributors may want to know how these were arrived at. + +# First, I based everything off of the Blue Ring. Since the Red Ring is twice as good as the Blue Ring, +# logic dictates it should cost twice as much. Since you can't make something cost 500 rupees, the only +# solution was to halve the price of the Blue Ring. Correspondingly, everything else sold in shops was +# also cut in half. + +# Then, I decided on a factor for swords. Since each sword does double the damage of its predecessor, each +# one should be at least double. Since the sword saves so much time when upgraded (as, unlike other items, +# you don't need to switch to it), I wanted a bit of a premium on upgrades. Thus, a 4x multiplier was chosen, +# allowing the basic Sword to stay cheap while making the Magical Sword be a hefty upgrade you'll +# feel the price of. + +# Since arrows do the same amount of damage as the White Sword and silver arrows are the same with the Magical Sword. +# they were given corresponding costs. + +# Utility items were based on the prices of the shield, keys, and food. Broadly useful utility items should cost more, +# while limited use utility items should cost less. After eyeballing those, a few editorial decisions were made as +# deliberate thumbs on the scale of game balance. Those exceptions will be noted below. In general, prices were chosen +# based on how a player would feel spending that amount of money as opposed to how useful an item actually is. + +item_prices = { + "Bomb": 10, + "Sword": 10, + "White Sword": 40, + "Magical Sword": 160, + "Food": 30, + "Recorder": 45, + "Candle": 30, + "Red Candle": 60, + "Arrow": 40, + "Silver Arrow": 160, + "Bow": 40, + "Magical Key": 250, # Replacing all small keys commands a high premium + "Raft": 80, + "Stepladder": 80, + "Five Rupees": 255, # This could cost anything above 5 Rupees and be fine, but 255 is the funniest + "Magical Rod": 100, # White Sword with forever beams should cost at least more than the White Sword itself + "Book of Magic": 60, + "Blue Ring": 125, + "Red Ring": 250, + "Power Bracelet": 25, + "Letter": 20, + "Small Key": 40, + "Heart Container": 80, + "Triforce Fragment": 200, # Since I couldn't make Zelda 1 track shop purchases, this is how to discourage repeat + # Triforce purchases. The punishment for endless Rupee grinding to avoid searching out + # Triforce pieces is that you're doing endless Rupee grinding to avoid playing the game + "Magical Shield": 45, + "Boomerang": 5, + "Magical Boomerang": 20, + "Water of Life (Blue)": 20, + "Water of Life (Red)": 34, + "Recovery Heart": 5, + "Rupee": 50, + "Clock": 0, + "Fairy": 10 +} diff --git a/worlds/tloz/Locations.py b/worlds/tloz/Locations.py new file mode 100644 index 0000000000..3e46c43833 --- /dev/null +++ b/worlds/tloz/Locations.py @@ -0,0 +1,343 @@ +from . import Rom + +major_locations = [ + "Starting Sword Cave", + "White Sword Pond", + "Magical Sword Grave", + "Take Any Item Left", + "Take Any Item Middle", + "Take Any Item Right", + "Armos Knights", + "Ocean Heart Container", + "Letter Cave", +] + +level_locations = [ + [ + "Level 1 Item (Bow)", "Level 1 Item (Boomerang)", "Level 1 Map", "Level 1 Compass", "Level 1 Boss", + "Level 1 Triforce", "Level 1 Key Drop (Keese Entrance)", "Level 1 Key Drop (Stalfos Middle)", + "Level 1 Key Drop (Moblins)", "Level 1 Key Drop (Stalfos Water)", + "Level 1 Key Drop (Stalfos Entrance)", "Level 1 Key Drop (Wallmasters)", + ], + [ + "Level 2 Item (Magical Boomerang)", "Level 2 Map", "Level 2 Compass", "Level 2 Boss", "Level 2 Triforce", + "Level 2 Key Drop (Ropes West)", "Level 2 Key Drop (Moldorms)", + "Level 2 Key Drop (Ropes Middle)", "Level 2 Key Drop (Ropes Entrance)", + "Level 2 Bomb Drop (Keese)", "Level 2 Bomb Drop (Moblins)", + "Level 2 Rupee Drop (Gels)", + ], + [ + "Level 3 Item (Raft)", "Level 3 Map", "Level 3 Compass", "Level 3 Boss", "Level 3 Triforce", + "Level 3 Key Drop (Zols and Keese West)", "Level 3 Key Drop (Keese North)", + "Level 3 Key Drop (Zols Central)", "Level 3 Key Drop (Zols South)", + "Level 3 Key Drop (Zols Entrance)", "Level 3 Bomb Drop (Darknuts West)", + "Level 3 Bomb Drop (Keese Corridor)", "Level 3 Bomb Drop (Darknuts Central)", + "Level 3 Rupee Drop (Zols and Keese East)" + ], + [ + "Level 4 Item (Stepladder)", "Level 4 Map", "Level 4 Compass", "Level 4 Boss", "Level 4 Triforce", + "Level 4 Key Drop (Keese Entrance)", "Level 4 Key Drop (Keese Central)", + "Level 4 Key Drop (Zols)", "Level 4 Key Drop (Keese North)", + ], + [ + "Level 5 Item (Recorder)", "Level 5 Map", "Level 5 Compass", "Level 5 Boss", "Level 5 Triforce", + "Level 5 Key Drop (Keese North)", "Level 5 Key Drop (Gibdos North)", + "Level 5 Key Drop (Gibdos Central)", "Level 5 Key Drop (Pols Voice Entrance)", + "Level 5 Key Drop (Gibdos Entrance)", "Level 5 Key Drop (Gibdos, Keese, and Pols Voice)", + "Level 5 Key Drop (Zols)", "Level 5 Bomb Drop (Gibdos)", + "Level 5 Bomb Drop (Dodongos)", "Level 5 Rupee Drop (Zols)", + ], + [ + "Level 6 Item (Magical Rod)", "Level 6 Map", "Level 6 Compass", "Level 6 Boss", "Level 6 Triforce", + "Level 6 Key Drop (Wizzrobes Entrance)", "Level 6 Key Drop (Keese)", + "Level 6 Key Drop (Wizzrobes North Island)", "Level 6 Key Drop (Wizzrobes North Stream)", + "Level 6 Key Drop (Vires)", "Level 6 Bomb Drop (Wizzrobes)", + "Level 6 Rupee Drop (Wizzrobes)" + ], + [ + "Level 7 Item (Red Candle)", "Level 7 Map", "Level 7 Compass", "Level 7 Boss", "Level 7 Triforce", + "Level 7 Key Drop (Ropes)", "Level 7 Key Drop (Goriyas)", "Level 7 Key Drop (Stalfos)", + "Level 7 Key Drop (Moldorms)", "Level 7 Bomb Drop (Goriyas South)", "Level 7 Bomb Drop (Keese and Spikes)", + "Level 7 Bomb Drop (Moldorms South)", "Level 7 Bomb Drop (Moldorms North)", + "Level 7 Bomb Drop (Goriyas North)", "Level 7 Bomb Drop (Dodongos)", + "Level 7 Bomb Drop (Digdogger)", "Level 7 Rupee Drop (Goriyas Central)", + "Level 7 Rupee Drop (Dodongos)", "Level 7 Rupee Drop (Goriyas North)", + ], + [ + "Level 8 Item (Magical Key)", "Level 8 Map", "Level 8 Compass", "Level 8 Item (Book of Magic)", "Level 8 Boss", + "Level 8 Triforce", "Level 8 Key Drop (Darknuts West)", + "Level 8 Key Drop (Darknuts Far West)", "Level 8 Key Drop (Pols Voice South)", + "Level 8 Key Drop (Pols Voice and Keese)", "Level 8 Key Drop (Darknuts Central)", + "Level 8 Key Drop (Keese and Zols Entrance)", "Level 8 Bomb Drop (Darknuts North)", + "Level 8 Bomb Drop (Darknuts East)", "Level 8 Bomb Drop (Pols Voice North)", + "Level 8 Rupee Drop (Manhandla Entrance West)", "Level 8 Rupee Drop (Manhandla Entrance North)", + "Level 8 Rupee Drop (Darknuts and Gibdos)", + ], + [ + "Level 9 Item (Silver Arrow)", "Level 9 Item (Red Ring)", + "Level 9 Map", "Level 9 Compass", + "Level 9 Key Drop (Patra Southwest)", "Level 9 Key Drop (Like Likes and Zols East)", + "Level 9 Key Drop (Wizzrobes and Bubbles East)", "Level 9 Key Drop (Wizzrobes East Island)", + "Level 9 Bomb Drop (Blue Lanmolas)", "Level 9 Bomb Drop (Gels Lake)", + "Level 9 Bomb Drop (Like Likes and Zols Corridor)", "Level 9 Bomb Drop (Patra Northeast)", + "Level 9 Bomb Drop (Vires)", "Level 9 Rupee Drop (Wizzrobes West Island)", + "Level 9 Rupee Drop (Red Lanmolas)", "Level 9 Rupee Drop (Keese Southwest)", + "Level 9 Rupee Drop (Keese Central Island)", "Level 9 Rupee Drop (Wizzrobes Central)", + "Level 9 Rupee Drop (Wizzrobes North Island)", "Level 9 Rupee Drop (Gels East)" + ] +] + +all_level_locations = [location for level in level_locations for location in level] + +standard_level_locations = [location for level in level_locations for location in level if "Drop" not in location] + +shop_locations = [ + "Arrow Shop Item Left", "Arrow Shop Item Middle", "Arrow Shop Item Right", + "Candle Shop Item Left", "Candle Shop Item Middle", "Candle Shop Item Right", + "Blue Ring Shop Item Left", "Blue Ring Shop Item Middle", "Blue Ring Shop Item Right", + "Shield Shop Item Left", "Shield Shop Item Middle", "Shield Shop Item Right", + "Potion Shop Item Left", "Potion Shop Item Middle", "Potion Shop Item Right" +] + +food_locations = [ + "Level 7 Map", "Level 7 Boss", "Level 7 Triforce", "Level 7 Key Drop (Goriyas)", + "Level 7 Bomb Drop (Moldorms North)", "Level 7 Bomb Drop (Goriyas North)", + "Level 7 Bomb Drop (Dodongos)", "Level 7 Rupee Drop (Goriyas North)" +] + +floor_location_game_offsets_early = { + "Level 1 Item (Bow)": 0x7F, + "Level 1 Item (Boomerang)": 0x44, + "Level 1 Map": 0x43, + "Level 1 Compass": 0x54, + "Level 1 Boss": 0x35, + "Level 1 Triforce": 0x36, + "Level 1 Key Drop (Keese Entrance)": 0x72, + "Level 1 Key Drop (Moblins)": 0x23, + "Level 1 Key Drop (Stalfos Water)": 0x33, + "Level 1 Key Drop (Stalfos Entrance)": 0x74, + "Level 1 Key Drop (Stalfos Middle)": 0x53, + "Level 1 Key Drop (Wallmasters)": 0x45, + "Level 2 Item (Magical Boomerang)": 0x4F, + "Level 2 Map": 0x5F, + "Level 2 Compass": 0x6F, + "Level 2 Boss": 0x0E, + "Level 2 Triforce": 0x0D, + "Level 2 Key Drop (Ropes West)": 0x6C, + "Level 2 Key Drop (Moldorms)": 0x3E, + "Level 2 Key Drop (Ropes Middle)": 0x4E, + "Level 2 Key Drop (Ropes Entrance)": 0x7E, + "Level 2 Bomb Drop (Keese)": 0x3F, + "Level 2 Bomb Drop (Moblins)": 0x1E, + "Level 2 Rupee Drop (Gels)": 0x2F, + "Level 3 Item (Raft)": 0x0F, + "Level 3 Map": 0x4C, + "Level 3 Compass": 0x5A, + "Level 3 Boss": 0x4D, + "Level 3 Triforce": 0x3D, + "Level 3 Key Drop (Zols and Keese West)": 0x49, + "Level 3 Key Drop (Keese North)": 0x2A, + "Level 3 Key Drop (Zols Central)": 0x4B, + "Level 3 Key Drop (Zols South)": 0x6B, + "Level 3 Key Drop (Zols Entrance)": 0x7B, + "Level 3 Bomb Drop (Darknuts West)": 0x69, + "Level 3 Bomb Drop (Keese Corridor)": 0x4A, + "Level 3 Bomb Drop (Darknuts Central)": 0x5B, + "Level 3 Rupee Drop (Zols and Keese East)": 0x5D, + "Level 4 Item (Stepladder)": 0x60, + "Level 4 Map": 0x21, + "Level 4 Compass": 0x62, + "Level 4 Boss": 0x13, + "Level 4 Triforce": 0x03, + "Level 4 Key Drop (Keese Entrance)": 0x70, + "Level 4 Key Drop (Keese Central)": 0x51, + "Level 4 Key Drop (Zols)": 0x40, + "Level 4 Key Drop (Keese North)": 0x01, + "Level 5 Item (Recorder)": 0x04, + "Level 5 Map": 0x46, + "Level 5 Compass": 0x37, + "Level 5 Boss": 0x24, + "Level 5 Triforce": 0x14, + "Level 5 Key Drop (Keese North)": 0x16, + "Level 5 Key Drop (Gibdos North)": 0x26, + "Level 5 Key Drop (Gibdos Central)": 0x47, + "Level 5 Key Drop (Pols Voice Entrance)": 0x77, + "Level 5 Key Drop (Gibdos Entrance)": 0x66, + "Level 5 Key Drop (Gibdos, Keese, and Pols Voice)": 0x27, + "Level 5 Key Drop (Zols)": 0x55, + "Level 5 Bomb Drop (Gibdos)": 0x65, + "Level 5 Bomb Drop (Dodongos)": 0x56, + "Level 5 Rupee Drop (Zols)": 0x57, + "Level 6 Item (Magical Rod)": 0x75, + "Level 6 Map": 0x19, + "Level 6 Compass": 0x68, + "Level 6 Boss": 0x1C, + "Level 6 Triforce": 0x0C, + "Level 6 Key Drop (Wizzrobes Entrance)": 0x7A, + "Level 6 Key Drop (Keese)": 0x58, + "Level 6 Key Drop (Wizzrobes North Island)": 0x29, + "Level 6 Key Drop (Wizzrobes North Stream)": 0x1A, + "Level 6 Key Drop (Vires)": 0x2D, + "Level 6 Bomb Drop (Wizzrobes)": 0x3C, + "Level 6 Rupee Drop (Wizzrobes)": 0x28 +} + +floor_location_game_ids_early = {} +for key, value in floor_location_game_offsets_early.items(): + floor_location_game_ids_early[key] = value + Rom.first_quest_dungeon_items_early + +floor_location_game_offsets_late = { + "Level 7 Item (Red Candle)": 0x4A, + "Level 7 Map": 0x18, + "Level 7 Compass": 0x5A, + "Level 7 Boss": 0x2A, + "Level 7 Triforce": 0x2B, + "Level 7 Key Drop (Ropes)": 0x78, + "Level 7 Key Drop (Goriyas)": 0x0A, + "Level 7 Key Drop (Stalfos)": 0x6D, + "Level 7 Key Drop (Moldorms)": 0x3A, + "Level 7 Bomb Drop (Goriyas South)": 0x69, + "Level 7 Bomb Drop (Keese and Spikes)": 0x68, + "Level 7 Bomb Drop (Moldorms South)": 0x7A, + "Level 7 Bomb Drop (Moldorms North)": 0x0B, + "Level 7 Bomb Drop (Goriyas North)": 0x1B, + "Level 7 Bomb Drop (Dodongos)": 0x0C, + "Level 7 Bomb Drop (Digdogger)": 0x6C, + "Level 7 Rupee Drop (Goriyas Central)": 0x38, + "Level 7 Rupee Drop (Dodongos)": 0x58, + "Level 7 Rupee Drop (Goriyas North)": 0x09, + "Level 8 Item (Magical Key)": 0x0F, + "Level 8 Item (Book of Magic)": 0x6F, + "Level 8 Map": 0x2E, + "Level 8 Compass": 0x5F, + "Level 8 Boss": 0x3C, + "Level 8 Triforce": 0x2C, + "Level 8 Key Drop (Darknuts West)": 0x5C, + "Level 8 Key Drop (Darknuts Far West)": 0x4B, + "Level 8 Key Drop (Pols Voice South)": 0x4C, + "Level 8 Key Drop (Pols Voice and Keese)": 0x5D, + "Level 8 Key Drop (Darknuts Central)": 0x5E, + "Level 8 Key Drop (Keese and Zols Entrance)": 0x7F, + "Level 8 Bomb Drop (Darknuts North)": 0x0E, + "Level 8 Bomb Drop (Darknuts East)": 0x3F, + "Level 8 Bomb Drop (Pols Voice North)": 0x1D, + "Level 8 Rupee Drop (Manhandla Entrance West)": 0x7D, + "Level 8 Rupee Drop (Manhandla Entrance North)": 0x6E, + "Level 8 Rupee Drop (Darknuts and Gibdos)": 0x4E, + "Level 9 Item (Silver Arrow)": 0x4F, + "Level 9 Item (Red Ring)": 0x00, + "Level 9 Map": 0x27, + "Level 9 Compass": 0x35, + "Level 9 Key Drop (Patra Southwest)": 0x61, + "Level 9 Key Drop (Like Likes and Zols East)": 0x56, + "Level 9 Key Drop (Wizzrobes and Bubbles East)": 0x47, + "Level 9 Key Drop (Wizzrobes East Island)": 0x57, + "Level 9 Bomb Drop (Blue Lanmolas)": 0x11, + "Level 9 Bomb Drop (Gels Lake)": 0x23, + "Level 9 Bomb Drop (Like Likes and Zols Corridor)": 0x25, + "Level 9 Bomb Drop (Patra Northeast)": 0x16, + "Level 9 Bomb Drop (Vires)": 0x37, + "Level 9 Rupee Drop (Wizzrobes West Island)": 0x40, + "Level 9 Rupee Drop (Red Lanmolas)": 0x12, + "Level 9 Rupee Drop (Keese Southwest)": 0x62, + "Level 9 Rupee Drop (Keese Central Island)": 0x34, + "Level 9 Rupee Drop (Wizzrobes Central)": 0x44, + "Level 9 Rupee Drop (Wizzrobes North Island)": 0x15, + "Level 9 Rupee Drop (Gels East)": 0x26 +} + +floor_location_game_ids_late = {} +for key, value in floor_location_game_offsets_late.items(): + floor_location_game_ids_late[key] = value + Rom.first_quest_dungeon_items_late + +dungeon_items = {**floor_location_game_ids_early, **floor_location_game_ids_late} + +shop_location_ids = { + "Arrow Shop Item Left": 0x18637, + "Arrow Shop Item Middle": 0x18638, + "Arrow Shop Item Right": 0x18639, + "Candle Shop Item Left": 0x1863A, + "Candle Shop Item Middle": 0x1863B, + "Candle Shop Item Right": 0x1863C, + "Shield Shop Item Left": 0x1863D, + "Shield Shop Item Middle": 0x1863E, + "Shield Shop Item Right": 0x1863F, + "Blue Ring Shop Item Left": 0x18640, + "Blue Ring Shop Item Middle": 0x18641, + "Blue Ring Shop Item Right": 0x18642, + "Potion Shop Item Left": 0x1862E, + "Potion Shop Item Middle": 0x1862F, + "Potion Shop Item Right": 0x18630 +} + +shop_price_location_ids = { + "Arrow Shop Item Left": 0x18673, + "Arrow Shop Item Middle": 0x18674, + "Arrow Shop Item Right": 0x18675, + "Candle Shop Item Left": 0x18676, + "Candle Shop Item Middle": 0x18677, + "Candle Shop Item Right": 0x18678, + "Shield Shop Item Left": 0x18679, + "Shield Shop Item Middle": 0x1867A, + "Shield Shop Item Right": 0x1867B, + "Blue Ring Shop Item Left": 0x1867C, + "Blue Ring Shop Item Middle": 0x1867D, + "Blue Ring Shop Item Right": 0x1867E, + "Potion Shop Item Left": 0x1866A, + "Potion Shop Item Middle": 0x1866B, + "Potion Shop Item Right": 0x1866C +} + +secret_money_ids = { + "Secret Money 1": 0x18680, + "Secret Money 2": 0x18683, + "Secret Money 3": 0x18686 +} + +major_location_ids = { + "Starting Sword Cave": 0x18611, + "White Sword Pond": 0x18617, + "Magical Sword Grave": 0x1861A, + "Letter Cave": 0x18629, + "Take Any Item Left": 0x18613, + "Take Any Item Middle": 0x18614, + "Take Any Item Right": 0x18615, + "Armos Knights": 0x10D05, + "Ocean Heart Container": 0x1789A +} + +major_location_offsets = { + "Starting Sword Cave": 0x77, + "White Sword Pond": 0x0A, + "Magical Sword Grave": 0x21, + "Letter Cave": 0x0E, + # "Take Any Item Left": 0x7B, + # "Take Any Item Middle": 0x2C, + # "Take Any Item Right": 0x47, + "Armos Knights": 0x24, + "Ocean Heart Container": 0x5F +} + +overworld_locations = [ + "Starting Sword Cave", + "White Sword Pond", + "Magical Sword Grave", + "Letter Cave", + "Armos Knights", + "Ocean Heart Container" +] + +underworld1_locations = [*floor_location_game_offsets_early.keys()] + +underworld2_locations = [*floor_location_game_offsets_late.keys()] + +#cave_locations = ["Take Any Item Left", "Take Any Item Middle", "Take Any Item Right"] + [*shop_locations] + +location_table_base = [x for x in major_locations] + \ + [y for y in all_level_locations] + \ + [z for z in shop_locations] +location_table = {} +for i, location in enumerate(location_table_base): + location_table[location] = i + +location_ids = {**dungeon_items, **shop_location_ids, **major_location_ids} diff --git a/worlds/tloz/Options.py b/worlds/tloz/Options.py new file mode 100644 index 0000000000..96bd3e296d --- /dev/null +++ b/worlds/tloz/Options.py @@ -0,0 +1,40 @@ +import typing +from Options import Option, DefaultOnToggle, Choice + + +class ExpandedPool(DefaultOnToggle): + """Puts room clear drops and take any caves into the pool of items and locations.""" + display_name = "Expanded Item Pool" + + +class TriforceLocations(Choice): + """Where Triforce fragments can be located. Note that Triforce pieces + obtained in a dungeon will heal and warp you out, while overworld Triforce pieces obtained will appear to have + no immediate effect. This is normal.""" + display_name = "Triforce Locations" + option_vanilla = 0 + option_dungeons = 1 + option_anywhere = 2 + + +class StartingPosition(Choice): + """How easy is the start of the game. + Safe means a weapon is guaranteed in Starting Sword Cave. + Unsafe means that a weapon is guaranteed between Starting Sword Cave, Letter Cave, and Armos Knight. + Dangerous adds these level locations to the unsafe pool (if they exist): +# Level 1 Compass, Level 2 Bomb Drop (Keese), Level 3 Key Drop (Zols Entrance), Level 3 Compass + Very Dangerous is the same as dangerous except it doesn't guarantee a weapon. It will only mean progression + will be there in single player seeds. In multi worlds, however, this means all bets are off and after checking + the dangerous spots, you could be stuck until someone sends you a weapon""" + display_name = "Starting Position" + option_safe = 0 + option_unsafe = 1 + option_dangerous = 2 + option_very_dangerous = 3 + + +tloz_options: typing.Dict[str, type(Option)] = { + "ExpandedPool": ExpandedPool, + "TriforceLocations": TriforceLocations, + "StartingPosition": StartingPosition +} diff --git a/worlds/tloz/Rom.py b/worlds/tloz/Rom.py new file mode 100644 index 0000000000..0be96d664a --- /dev/null +++ b/worlds/tloz/Rom.py @@ -0,0 +1,78 @@ +import zlib +import os + +import Utils +from Patch import APDeltaPatch + +NA10CHECKSUM = 'D7AE93DF' +ROM_PLAYER_LIMIT = 65535 +ROM_NAME = 0x10 +bit_positions = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80] +candle_shop = bit_positions[5] +arrow_shop = bit_positions[4] +potion_shop = bit_positions[1] +shield_shop = bit_positions[6] +ring_shop = bit_positions[7] +take_any = bit_positions[2] +first_quest_dungeon_items_early = 0x18910 +first_quest_dungeon_items_late = 0x18C10 +game_mode = 0x12 +sword = 0x0657 +bombs = 0x0658 +arrow = 0x0659 +bow = 0x065A +candle = 0x065B +recorder = 0x065C +food = 0x065D +potion = 0x065E +magical_rod = 0x065F +raft = 0x0660 +book_of_magic = 0x0661 +ring = 0x0662 +stepladder = 0x0663 +magical_key = 0x0664 +power_bracelet = 0x0665 +letter = 0x0666 +heart_containers = 0x066F +triforce_fragments = 0x0671 +boomerang = 0x0674 +magical_boomerang = 0x0675 +magical_shield = 0x0676 +rupees_to_add = 0x067D + + + + +class TLoZDeltaPatch(APDeltaPatch): + checksum = NA10CHECKSUM + hash = NA10CHECKSUM + game = "The Legend of Zelda" + patch_file_ending = ".aptloz" + result_file_ending = ".nes" + + @classmethod + def get_source_data(cls) -> bytes: + return get_base_rom_bytes() + + +def get_base_rom_bytes(file_name: str = "") -> bytes: + base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None) + if not base_rom_bytes: + file_name = get_base_rom_path(file_name) + base_rom_bytes = bytes(Utils.read_snes_rom(open(file_name, "rb"))) + + basechecksum = str(hex(zlib.crc32(base_rom_bytes))).upper()[2:] + if NA10CHECKSUM != basechecksum: + raise Exception('Supplied Base Rom does not match known CRC-32 for NA (1.0) release. ' + 'Get the correct game and version, then dump it') + get_base_rom_bytes.base_rom_bytes = base_rom_bytes + return base_rom_bytes + + +def get_base_rom_path(file_name: str = "") -> str: + options = Utils.get_options() + if not file_name: + file_name = options["tloz_options"]["rom_file"] + if not os.path.exists(file_name): + file_name = Utils.user_path(file_name) + return file_name diff --git a/worlds/tloz/Rules.py b/worlds/tloz/Rules.py new file mode 100644 index 0000000000..1e66e5a849 --- /dev/null +++ b/worlds/tloz/Rules.py @@ -0,0 +1,150 @@ +from typing import TYPE_CHECKING + +from ..generic.Rules import add_rule +from .Locations import food_locations, shop_locations +from .ItemPool import dangerous_weapon_locations +from .Options import StartingPosition + +if TYPE_CHECKING: + from . import TLoZWorld + +def set_rules(tloz_world: "TLoZWorld"): + player = tloz_world.player + world = tloz_world.multiworld + + # Boss events for a nicer spoiler log play through + for level in range(1, 9): + boss = world.get_location(f"Level {level} Boss", player) + boss_event = world.get_location(f"Level {level} Boss Status", player) + status = tloz_world.create_event(f"Boss {level} Defeated") + boss_event.place_locked_item(status) + add_rule(boss_event, lambda state, b=boss: state.can_reach(b, "Location", player)) + + # No dungeons without weapons except for the dangerous weapon locations if we're dangerous, no unsafe dungeons + for i, level in enumerate(tloz_world.levels[1:10]): + for location in level.locations: + if world.StartingPosition[player] < StartingPosition.option_dangerous \ + or location.name not in dangerous_weapon_locations: + add_rule(world.get_location(location.name, player), + lambda state: state.has_group("weapons", player)) + if i > 0: # Don't need an extra heart for Level 1 + add_rule(world.get_location(location.name, player), + lambda state, hearts=i: state.has("Heart Container", player, hearts) or + (state.has("Blue Ring", player) and + state.has("Heart Container", player, int(hearts / 2))) or + (state.has("Red Ring", player) and + state.has("Heart Container", player, int(hearts / 4)))) + if "Pols Voice" in location.name: # This enemy needs specific weapons + add_rule(world.get_location(location.name, player), + lambda state: state.has_group("swords", player) or state.has("Bow", player)) + + # No requiring anything in a shop until we can farm for money + for location in shop_locations: + add_rule(world.get_location(location, player), + lambda state: state.has_group("weapons", player)) + + # Everything from 4 on up has dark rooms + for level in tloz_world.levels[4:]: + for location in level.locations: + add_rule(world.get_location(location.name, player), + lambda state: state.has_group("candles", player) + or (state.has("Magical Rod", player) and state.has("Book", player))) + + # Everything from 5 on up has gaps + for level in tloz_world.levels[5:]: + for location in level.locations: + add_rule(world.get_location(location.name, player), + lambda state: state.has("Stepladder", player)) + + add_rule(world.get_location("Level 5 Boss", player), + lambda state: state.has("Recorder", player)) + + add_rule(world.get_location("Level 6 Boss", player), + lambda state: state.has("Bow", player) and state.has_group("arrows", player)) + + add_rule(world.get_location("Level 7 Item (Red Candle)", player), + lambda state: state.has("Recorder", player)) + add_rule(world.get_location("Level 7 Boss", player), + lambda state: state.has("Recorder", player)) + if world.ExpandedPool[player]: + add_rule(world.get_location("Level 7 Key Drop (Stalfos)", player), + lambda state: state.has("Recorder", player)) + add_rule(world.get_location("Level 7 Bomb Drop (Digdogger)", player), + lambda state: state.has("Recorder", player)) + add_rule(world.get_location("Level 7 Rupee Drop (Dodongos)", player), + lambda state: state.has("Recorder", player)) + + for location in food_locations: + if world.ExpandedPool[player] or "Drop" not in location: + add_rule(world.get_location(location, player), + lambda state: state.has("Food", player)) + + add_rule(world.get_location("Level 8 Item (Magical Key)", player), + lambda state: state.has("Bow", player) and state.has_group("arrows", player)) + if world.ExpandedPool[player]: + add_rule(world.get_location("Level 8 Bomb Drop (Darknuts North)", player), + lambda state: state.has("Bow", player) and state.has_group("arrows", player)) + + for location in tloz_world.levels[9].locations: + add_rule(world.get_location(location.name, player), + lambda state: state.has("Triforce Fragment", player, 8) and + state.has_group("swords", player)) + + # Yes we are looping this range again for Triforce locations. No I can't add it to the boss event loop + for level in range(1, 9): + add_rule(world.get_location(f"Level {level} Triforce", player), + lambda state, l=level: state.has(f"Boss {l} Defeated", player)) + + # Sword, raft, and ladder spots + add_rule(world.get_location("White Sword Pond", player), + lambda state: state.has("Heart Container", player, 2)) + add_rule(world.get_location("Magical Sword Grave", player), + lambda state: state.has("Heart Container", player, 9)) + + stepladder_locations = ["Ocean Heart Container", "Level 4 Triforce", "Level 4 Boss", "Level 4 Map"] + stepladder_locations_expanded = ["Level 4 Key Drop (Keese North)"] + for location in stepladder_locations: + add_rule(world.get_location(location, player), + lambda state: state.has("Stepladder", player)) + if world.ExpandedPool[player]: + for location in stepladder_locations_expanded: + add_rule(world.get_location(location, player), + lambda state: state.has("Stepladder", player)) + + # Don't allow Take Any Items until we can actually get in one + if world.ExpandedPool[player]: + add_rule(world.get_location("Take Any Item Left", player), + lambda state: state.has_group("candles", player) or + state.has("Raft", player)) + add_rule(world.get_location("Take Any Item Middle", player), + lambda state: state.has_group("candles", player) or + state.has("Raft", player)) + add_rule(world.get_location("Take Any Item Right", player), + lambda state: state.has_group("candles", player) or + state.has("Raft", player)) + for location in tloz_world.levels[4].locations: + add_rule(world.get_location(location.name, player), + lambda state: state.has("Raft", player) or state.has("Recorder", player)) + for location in tloz_world.levels[7].locations: + add_rule(world.get_location(location.name, player), + lambda state: state.has("Recorder", player)) + for location in tloz_world.levels[8].locations: + add_rule(world.get_location(location.name, player), + lambda state: state.has("Bow", player)) + + add_rule(world.get_location("Potion Shop Item Left", player), + lambda state: state.has("Letter", player)) + add_rule(world.get_location("Potion Shop Item Middle", player), + lambda state: state.has("Letter", player)) + add_rule(world.get_location("Potion Shop Item Right", player), + lambda state: state.has("Letter", player)) + + add_rule(world.get_location("Shield Shop Item Left", player), + lambda state: state.has_group("candles", player) or + state.has("Bomb", player)) + add_rule(world.get_location("Shield Shop Item Middle", player), + lambda state: state.has_group("candles", player) or + state.has("Bomb", player)) + add_rule(world.get_location("Shield Shop Item Right", player), + lambda state: state.has_group("candles", player) or + state.has("Bomb", player)) \ No newline at end of file diff --git a/worlds/tloz/__init__.py b/worlds/tloz/__init__.py new file mode 100644 index 0000000000..311215c157 --- /dev/null +++ b/worlds/tloz/__init__.py @@ -0,0 +1,317 @@ +import logging +import os +import threading +import pkgutil +from typing import NamedTuple, Union, Dict, Any + +import bsdiff4 + +import Utils +from BaseClasses import Item, Location, Region, Entrance, MultiWorld, ItemClassification, Tutorial +from .ItemPool import generate_itempool, starting_weapons, dangerous_weapon_locations +from .Items import item_table, item_prices, item_game_ids +from .Locations import location_table, level_locations, major_locations, shop_locations, all_level_locations, \ + standard_level_locations, shop_price_location_ids, secret_money_ids, location_ids, food_locations +from .Options import tloz_options +from .Rom import TLoZDeltaPatch, get_base_rom_path, first_quest_dungeon_items_early, first_quest_dungeon_items_late +from .Rules import set_rules +from worlds.AutoWorld import World, WebWorld +from worlds.generic.Rules import add_rule + + +class TLoZWeb(WebWorld): + theme = "stone" + setup = Tutorial( + "Multiworld Setup Tutorial", + "A guide to setting up The Legend of Zelda for Archipelago on your computer.", + "English", + "multiworld_en.md", + "multiworld/en", + ["Rosalie and Figment"] + ) + + tutorials = [setup] + + +class TLoZWorld(World): + """ + The Legend of Zelda needs almost no introduction. Gather the eight fragments of the + Triforce of Wisdom, enter Death Mountain, defeat Ganon, and rescue Princess Zelda. + This randomizer shuffles all the items in the game around, leading to a new adventure + every time. + """ + option_definitions = tloz_options + game = "The Legend of Zelda" + topology_present = False + data_version = 1 + base_id = 7000 + web = TLoZWeb() + + item_name_to_id = {name: data.code for name, data in item_table.items()} + location_name_to_id = location_table + + item_name_groups = { + 'weapons': starting_weapons, + 'swords': { + "Sword", "White Sword", "Magical Sword" + }, + "candles": { + "Candle", "Red Candle" + }, + "arrows": { + "Arrow", "Silver Arrow" + } + } + + for k, v in item_name_to_id.items(): + item_name_to_id[k] = v + base_id + + for k, v in location_name_to_id.items(): + if v is not None: + location_name_to_id[k] = v + base_id + + def __init__(self, world: MultiWorld, player: int): + super().__init__(world, player) + self.generator_in_use = threading.Event() + self.rom_name_available_event = threading.Event() + self.levels = None + self.filler_items = None + + def create_item(self, name: str): + return TLoZItem(name, item_table[name].classification, self.item_name_to_id[name], self.player) + + def create_event(self, event: str): + return TLoZItem(event, ItemClassification.progression, None, self.player) + + def create_location(self, name, id, parent, event=False): + return_location = TLoZLocation(self.player, name, id, parent) + return_location.event = event + return return_location + + def create_regions(self): + menu = Region("Menu", self.player, self.multiworld) + overworld = Region("Overworld", self.player, self.multiworld) + self.levels = [None] # Yes I'm making a one-indexed array in a zero-indexed language. I hate me too. + for i in range(1, 10): + level = Region(f"Level {i}", self.player, self.multiworld) + self.levels.append(level) + new_entrance = Entrance(self.player, f"Level {i}", overworld) + new_entrance.connect(level) + overworld.exits.append(new_entrance) + self.multiworld.regions.append(level) + + for i, level in enumerate(level_locations): + for location in level: + if self.multiworld.ExpandedPool[self.player] or "Drop" not in location: + self.levels[i + 1].locations.append( + self.create_location(location, self.location_name_to_id[location], self.levels[i + 1])) + + for level in range(1, 9): + boss_event = self.create_location(f"Level {level} Boss Status", None, + self.multiworld.get_region(f"Level {level}", self.player), + True) + boss_event.show_in_spoiler = False + self.levels[level].locations.append(boss_event) + + for location in major_locations: + if self.multiworld.ExpandedPool[self.player] or "Take Any" not in location: + overworld.locations.append( + self.create_location(location, self.location_name_to_id[location], overworld)) + + for location in shop_locations: + overworld.locations.append( + self.create_location(location, self.location_name_to_id[location], overworld)) + + ganon = self.create_location("Ganon", None, self.multiworld.get_region("Level 9", self.player)) + zelda = self.create_location("Zelda", None, self.multiworld.get_region("Level 9", self.player)) + ganon.show_in_spoiler = False + zelda.show_in_spoiler = False + self.levels[9].locations.append(ganon) + self.levels[9].locations.append(zelda) + begin_game = Entrance(self.player, "Begin Game", menu) + menu.exits.append(begin_game) + begin_game.connect(overworld) + self.multiworld.regions.append(menu) + self.multiworld.regions.append(overworld) + + + def create_items(self): + # refer to ItemPool.py + generate_itempool(self) + + # refer to Rules.py + set_rules = set_rules + + def generate_basic(self): + ganon = self.multiworld.get_location("Ganon", self.player) + ganon.place_locked_item(self.create_event("Triforce of Power")) + add_rule(ganon, lambda state: state.has("Silver Arrow", self.player) and state.has("Bow", self.player)) + + self.multiworld.get_location("Zelda", self.player).place_locked_item(self.create_event("Rescued Zelda!")) + add_rule(self.multiworld.get_location("Zelda", self.player), + lambda state: ganon in state.locations_checked) + self.multiworld.completion_condition[self.player] = lambda state: state.has("Rescued Zelda!", self.player) + + def apply_base_patch(self, rom): + # The base patch source is on a different repo, so here's the summary of changes: + # Remove Triforce check for recorder, so you can always warp. + # Remove level check for Triforce Fragments (and maps and compasses, but this won't matter) + # Replace some code with a jump to free space + # Check if we're picking up a Triforce Fragment. If so, increment the local count + # In either case, we do the instructions we overwrote with the jump and then return to normal flow + # Remove map/compass check so they're always on + # Removing a bit from the boss roars flags, so we can have more dungeon items. This allows us to + # go past 0x1F items for dungeon items. + base_patch_location = os.path.dirname(__file__) + "/z1_base_patch.bsdiff4" + with open(base_patch_location, "rb") as base_patch: + rom_data = bsdiff4.patch(rom.read(), base_patch.read()) + rom_data = bytearray(rom_data) + # Set every item to the new nothing value, but keep room flags. Type 2 boss roars should + # become type 1 boss roars, so we at least keep the sound of roaring where it should be. + for i in range(0, 0x7F): + item = rom_data[first_quest_dungeon_items_early + i] + if item & 0b00100000: + rom_data[first_quest_dungeon_items_early + i] = item & 0b11011111 + rom_data[first_quest_dungeon_items_early + i] = item | 0b01000000 + if item & 0b00011111 == 0b00000011: # Change all Item 03s to Item 3F, the proper "nothing" + rom_data[first_quest_dungeon_items_early + i] = item | 0b00111111 + + item = rom_data[first_quest_dungeon_items_late + i] + if item & 0b00100000: + rom_data[first_quest_dungeon_items_late + i] = item & 0b11011111 + rom_data[first_quest_dungeon_items_late + i] = item | 0b01000000 + if item & 0b00011111 == 0b00000011: + rom_data[first_quest_dungeon_items_late + i] = item | 0b00111111 + return rom_data + + def apply_randomizer(self): + with open(get_base_rom_path(), 'rb') as rom: + rom_data = self.apply_base_patch(rom) + # Write each location's new data in + for location in self.multiworld.get_filled_locations(self.player): + # Zelda and Ganon aren't real locations + if location.name == "Ganon" or location.name == "Zelda": + continue + + # Neither are boss defeat events + if "Status" in location.name: + continue + + item = location.item.name + # Remote items are always going to look like Rupees. + if location.item.player != self.player: + item = "Rupee" + + item_id = item_game_ids[item] + location_id = location_ids[location.name] + + # Shop prices need to be set + if location.name in shop_locations: + if location.name[-5:] == "Right": + # Final item in stores has bit 6 and 7 set. It's what marks the cave a shop. + item_id = item_id | 0b11000000 + price_location = shop_price_location_ids[location.name] + item_price = item_prices[item] + if item == "Rupee": + item_class = location.item.classification + if item_class == ItemClassification.progression: + item_price = item_price * 2 + elif item_class == ItemClassification.useful: + item_price = item_price // 2 + elif item_class == ItemClassification.filler: + item_price = item_price // 2 + elif item_class == ItemClassification.trap: + item_price = item_price * 2 + rom_data[price_location] = item_price + if location.name == "Take Any Item Right": + # Same story as above: bit 6 is what makes this a Take Any cave + item_id = item_id | 0b01000000 + rom_data[location_id] = item_id + + # We shuffle the tiers of rupee caves. Caves that shared a value before still will. + secret_caves = self.multiworld.per_slot_randoms[self.player].sample(sorted(secret_money_ids), 3) + secret_cave_money_amounts = [20, 50, 100] + for i, amount in enumerate(secret_cave_money_amounts): + # Giving approximately double the money to keep grinding down + amount = amount * self.multiworld.per_slot_randoms[self.player].triangular(1.5, 2.5) + secret_cave_money_amounts[i] = int(amount) + for i, cave in enumerate(secret_caves): + rom_data[secret_money_ids[cave]] = secret_cave_money_amounts[i] + return rom_data + + def generate_output(self, output_directory: str): + try: + patched_rom = self.apply_randomizer() + outfilebase = 'AP_' + self.multiworld.seed_name + outfilepname = f'_P{self.player}' + outfilepname += f"_{self.multiworld.get_file_safe_player_name(self.player).replace(' ', '_')}" + outputFilename = os.path.join(output_directory, f'{outfilebase}{outfilepname}.nes') + self.rom_name_text = f'LOZ{Utils.__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed:11}\0' + self.romName = bytearray(self.rom_name_text, 'utf8')[:0x20] + self.romName.extend([0] * (0x20 - len(self.romName))) + self.rom_name = self.romName + patched_rom[0x10:0x30] = self.romName + self.playerName = bytearray(self.multiworld.player_name[self.player], 'utf8')[:0x20] + self.playerName.extend([0] * (0x20 - len(self.playerName))) + patched_rom[0x30:0x50] = self.playerName + patched_filename = os.path.join(output_directory, outputFilename) + with open(patched_filename, 'wb') as patched_rom_file: + patched_rom_file.write(patched_rom) + patch = TLoZDeltaPatch(os.path.splitext(outputFilename)[0] + TLoZDeltaPatch.patch_file_ending, + player=self.player, + player_name=self.multiworld.player_name[self.player], + patched_path=outputFilename) + patch.write() + os.unlink(patched_filename) + finally: + self.rom_name_available_event.set() + + def modify_multidata(self, multidata: dict): + import base64 + self.rom_name_available_event.wait() + new_name = base64.b64encode(bytes(self.rom_name)).decode() + multidata["connect_names"][new_name] = multidata["connect_names"][self.multiworld.player_name[self.player]] + + def get_filler_item_name(self) -> str: + if self.filler_items is None: + self.filler_items = [item for item in item_table if item_table[item].classification == ItemClassification.filler] + return self.multiworld.random.choice(self.filler_items) + + def fill_slot_data(self) -> Dict[str, Any]: + if self.multiworld.ExpandedPool[self.player]: + take_any_left = self.multiworld.get_location("Take Any Item Left", self.player).item + take_any_middle = self.multiworld.get_location("Take Any Item Middle", self.player).item + take_any_right = self.multiworld.get_location("Take Any Item Right", self.player).item + if take_any_left.player == self.player: + take_any_left = take_any_left.code + else: + take_any_left = -1 + if take_any_middle.player == self.player: + take_any_middle = take_any_middle.code + else: + take_any_middle = -1 + if take_any_right.player == self.player: + take_any_right = take_any_right.code + else: + take_any_right = -1 + + slot_data = { + "TakeAnyLeft": take_any_left, + "TakeAnyMiddle": take_any_middle, + "TakeAnyRight": take_any_right + } + else: + slot_data = { + "TakeAnyLeft": -1, + "TakeAnyMiddle": -1, + "TakeAnyRight": -1 + } + return slot_data + + +class TLoZItem(Item): + game = 'The Legend of Zelda' + + +class TLoZLocation(Location): + game = 'The Legend of Zelda' \ No newline at end of file diff --git a/worlds/tloz/docs/en_The Legend of Zelda.md b/worlds/tloz/docs/en_The Legend of Zelda.md new file mode 100644 index 0000000000..e443c9b953 --- /dev/null +++ b/worlds/tloz/docs/en_The Legend of Zelda.md @@ -0,0 +1,43 @@ +# The Legend of Zelda (NES) + +## Where is the settings page? + +The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +config file. + +## What does randomization do to this game? + +All acquirable pickups (except maps and compasses) are shuffled among each other. Logic is in place to ensure both +that the game is still completable, and that players aren't forced to enter dungeons under-geared. + +Shops can contain any item in the game, with prices added for the items unavailable in stores. Rupee caves are worth +more while shops cost less, making shop routing and money management important without requiring mindless grinding. + +## What items and locations get shuffled? + +In general, all item pickups in the game. More formally: + +- Every inventory item. +- Every item found in the five kinds of shops. +- Optionally, Triforce Fragments can be shuffled to be within dungeons, or anywhere. +- Optionally, enemy-held items and dungeon floor items can be included in the shuffle, along with their slots +- Maps and compasses have been replaced with bonus items, including Clocks and Fairies. + +## What items from The Legend of Zelda can appear in other players' worlds? + +All items can appear in other players' worlds. + +## What does another world's item look like in The Legend of Zelda? + +All local items appear as normal. All remote items, no matter the game they originate from, will take on the appearance +of a single Rupee. These single Rupees will have variable prices in shops: progression and trap items will cost more, +filler and useful items will cost less, and uncategorized items will be in the middle. + +## Are there any other changes made? + +- The map and compass for each dungeon start already acquired, and other items can be found in their place. +- The Recorder will warp you between all eight levels regardless of Triforce count + - It's possible for this to be your route to level 4! +- Pressing Select will cycle through your inventory. +- Shop purchases are tracked within sessions, indicated by the item being elevated from its normal position. +- What slots from a Take Any Cave have been chosen are similarly tracked. \ No newline at end of file diff --git a/worlds/tloz/docs/multiworld_en.md b/worlds/tloz/docs/multiworld_en.md new file mode 100644 index 0000000000..031d4eee87 --- /dev/null +++ b/worlds/tloz/docs/multiworld_en.md @@ -0,0 +1,109 @@ +# The Legend of Zelda (NES) Multiworld Setup Guide + +## Required Software + +- The Zelda1Client + - Bundled with Archipelago: [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) +- The BizHawk emulator. Versions 2.3.1 and higher are supported. Version 2.7 is recommended + - [BizHawk Official Website](http://tasvideos.org/BizHawk.html) + +## Optional Software + +- [Map Tracker](https://github.com/Br00ty/tloz_brooty/releases/latest) + - Used alongside [Poptracker](https://github.com/black-sliver/PopTracker) to keep track of what items/checks you've gotten. Uses auto-tracking by connecting to the Archipelago server. + +## Installation Procedures + +1. Download and install the latest version of Archipelago. + - On Windows, download Setup.Archipelago..exe and run it. +2. Assign Bizhawk version 2.3.1 or higher as your default program for launching `.nes` files. + - Extract your Bizhawk folder to your Desktop, or somewhere you will remember. Below are optional additional steps + for loading ROMs more conveniently. + 1. Right-click on a ROM file and select **Open with...** + 2. Check the box next to **Always use this app to open .nes files**. + 3. Scroll to the bottom of the list and click the grey text **Look for another App on this PC**. + 4. Browse for `EmuHawk.exe` located inside your Bizhawk folder (from step 1) and click **Open**. + +## Create a Config (.yaml) File + +### What is a config file and why do I need one? + +See the guide on setting up a basic YAML at the Archipelago setup +guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) + +### Where do I get a config file? + +The Player Settings page on the website allows you to configure your personal settings and export a config file from +them. Player settings page: [The Legend of Zelda Player Settings Page](/games/The%20Legend%20of%20Zelda/player-settings) + +### Verifying your config file + +If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML +validator page: [YAML Validation page](/mysterycheck) + +## Generating a Single-Player Game + +1. Navigate to the Player Settings page, configure your options, and click the "Generate Game" button. + - Player Settings page: [The Legend of Zelda Player Settings Page](/games/The%20Legend%20of%20Zelda/player-settings) +2. You will be presented with a "Seed Info" page. +3. Click the "Create New Room" link. +4. You will be presented with a server page, from which you can download your patch file. +5. Double-click on your patch file, and the Zelda 1 Client will launch automatically, create your ROM from the + patch file, and open your emulator for you. +6. Since this is a single-player game, you will no longer need the client, so feel free to close it. + +## Joining a MultiWorld Game + +### Obtain your patch file and create your ROM + +When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that is done, +the host will provide you with either a link to download your patch file, or with a zip file containing everyone's patch +files. Your patch file should have a `.aptloz` extension. + +Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically launch the +client, and will also create your ROM in the same place as your patch file. + + +## Running the Client Program and Connecting to the Server + +Once the Archipelago server has been hosted: + +1. Navigate to your Archipelago install folder and run `ArchipelagoZelda1Client.exe`. +2. Notice the `/connect command` on the server hosting page. (It should look like `/connect archipelago.gg:*****` + where ***** are numbers) +3. Type the connect command into the client OR add the port to the pre-populated address on the top bar (it should + already say `archipelago.gg`) and click `connect`. + +### Running Your Game and Connecting to the Client Program + +1. Open Bizhawk 2.3.1 or higher and load your ROM OR click your ROM file if it is already associated with the + extension `*.nes`. +2. Click on the Tools menu and click on **Lua Console**. +3. Click the folder button to open a new Lua script. (CTL-O or **Script** -> **Open Script**) +4. Navigate to the location you installed Archipelago to. Open `data/lua/TLOZ/tloz_connector.lua`. + 1. If it gives a `NLua.Exceptions.LuaScriptException: .\socket.lua:13: module 'socket.core' not found:` exception + close your emulator entirely, restart it and re-run these steps. + 2. If it says `Must use a version of bizhawk 2.3.1 or higher`, double-check your Bizhawk version by clicking ** + Help** -> **About**. + +## Play the game + +When the client shows both NES and server are connected, you are good to go. You can check the connection status of the +NES at any time by running `/nes`. + +### Other Client Commands + +All other commands may be found on the [Archipelago Server and Client Commands Guide.](/tutorial/Archipelago/commands/en) +. + +## Known Issues + +- Triforce Fragments and Heart Containers may be purchased multiple times. It is up to you if you wish to take advantage +of this; logic will not account for or require purchasing any slot more than once. Remote items, no matter what they +are, will always only be sent once. +- Obtaining a remote item will move the location of any existing item in that room. Should this make an item +inaccessible, simply exit and re-enter the room. This can be used to obtain the Ocean Heart Container item without the +stepladder; logic does not account for this. +- Whether you've purchased from a shop is tracked via Archipelago between sessions: if you revisit a single player game, +none of your shop pruchase statuses will be remembered. If you want them to be, connect to the client and server like +you would in a multiplayer game. diff --git a/worlds/tloz/requirements.txt b/worlds/tloz/requirements.txt new file mode 100644 index 0000000000..d1f50ea5e9 --- /dev/null +++ b/worlds/tloz/requirements.txt @@ -0,0 +1 @@ +bsdiff4>=1.2.2 \ No newline at end of file diff --git a/worlds/tloz/z1_base_patch.bsdiff4 b/worlds/tloz/z1_base_patch.bsdiff4 new file mode 100644 index 0000000000..1231c86994 Binary files /dev/null and b/worlds/tloz/z1_base_patch.bsdiff4 differ diff --git a/worlds/wargroove/data/save/campaign-c40a6e5b0cdf86ddac03b276691c483d.cmp b/worlds/wargroove/data/save/campaign-c40a6e5b0cdf86ddac03b276691c483d.cmp index cf0c5304cd..8525595c65 100644 Binary files a/worlds/wargroove/data/save/campaign-c40a6e5b0cdf86ddac03b276691c483d.cmp and b/worlds/wargroove/data/save/campaign-c40a6e5b0cdf86ddac03b276691c483d.cmp differ diff --git a/worlds/wargroove/data/save/campaign-c40a6e5b0cdf86ddac03b276691c483d.cmp.bak b/worlds/wargroove/data/save/campaign-c40a6e5b0cdf86ddac03b276691c483d.cmp.bak index 5eb2c244e6..815d57726f 100644 Binary files a/worlds/wargroove/data/save/campaign-c40a6e5b0cdf86ddac03b276691c483d.cmp.bak and b/worlds/wargroove/data/save/campaign-c40a6e5b0cdf86ddac03b276691c483d.cmp.bak differ diff --git a/worlds/witness/Options.py b/worlds/witness/Options.py index e4e7e33faa..2047eb9ca6 100644 --- a/worlds/witness/Options.py +++ b/worlds/witness/Options.py @@ -158,6 +158,12 @@ class HintAmount(Range): default = 10 +class DeathLink(Toggle): + """If on: Whenever you fail a puzzle (with some exceptions), everyone who is also on Death Link dies. + The effect of a "death" in The Witness is a Power Surge.""" + display_name = "Death Link" + + the_witness_options: Dict[str, type] = { "puzzle_randomization": PuzzleRandomization, "shuffle_symbols": ShuffleSymbols, @@ -176,6 +182,7 @@ the_witness_options: Dict[str, type] = { "trap_percentage": TrapPercentage, "puzzle_skip_amount": PuzzleSkipAmount, "hint_amount": HintAmount, + "death_link": DeathLink, } diff --git a/worlds/witness/WitnessLogic.txt b/worlds/witness/WitnessLogic.txt index 308cba385f..329cdf3ce8 100644 --- a/worlds/witness/WitnessLogic.txt +++ b/worlds/witness/WitnessLogic.txt @@ -14,7 +14,7 @@ Tutorial (Tutorial) - Outside Tutorial - 0x03629: 158005 - 0x0A3B5 (Back Left) - True - True 158006 - 0x0A3B2 (Back Right) - True - True 158007 - 0x03629 (Gate Open) - 0x002C2 & 0x0A3B5 & 0x0A3B2 - True -158008 - 0x03505 (Gate Close) - 0x2FAF6 - True +158008 - 0x03505 (Gate Close) - 0x2FAF6 & 0x03629 - True 158009 - 0x0C335 (Pillar) - True - Triangles 158010 - 0x0C373 (Patio Floor) - 0x0C335 - Dots 159512 - 0x33530 (Cloud EP) - True - True @@ -411,7 +411,7 @@ Keep Tower (Keep) - Keep - 0x04F8F: 158206 - 0x0361B (Tower Shortcut Panel) - True - True Door - 0x04F8F (Tower Shortcut) - 0x0361B 158704 - 0x0360E (Laser Panel Hedges) - 0x01A0F & 0x019E7 & 0x019DC & 0x00139 - True -158705 - 0x03317 (Laser Panel Pressure Plates) - 0x01D3F - Shapers & Black/White Squares & Colored Squares & Stars & Stars + Same Colored Symbol & Dots +158705 - 0x03317 (Laser Panel Pressure Plates) - 0x033EA & 0x01BE9 & 0x01CD3 & 0x01D3F - Shapers & Black/White Squares & Colored Squares & Stars & Stars + Same Colored Symbol & Dots Laser - 0x014BB (Laser) - 0x0360E | 0x03317 159240 - 0x033BE (Pressure Plates 1 EP) - 0x033EA - True 159241 - 0x033BF (Pressure Plates 2 EP) - 0x01BE9 - True @@ -909,7 +909,7 @@ Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near - 158430 - 0x09FD8 (Near Row 5) - 0x09FD7 - Colored Squares & Symmetry & Colored Dots Door - 0x09FFB (Staircase Near) - 0x09FD8 -Mountain Floor 2 Blue Bridge (Mountain Floor 2) - Mountain Floor 2 Beyond Bridge - TrueOneWay - Mountain Floor 2 At Door - TrueOneWay: +Mountain Floor 2 Blue Bridge (Mountain Floor 2) - Mountain Floor 2 Beyond Bridge - TrueOneWay - Mountain Floor 2 At Door - 0x09ED8: Mountain Floor 2 At Door (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EDD: Door - 0x09EDD (Elevator Room Entry) - 0x09ED8 & 0x09E86 @@ -1113,8 +1113,9 @@ Obelisks (EPs) - Entry - True: 159735 - 0xFFE35 (River Obelisk Side 6) - 0x035CB & 0x035CF - True 159740 - 0xFFE40 (Quarry Obelisk Side 1) - 0x28A7B & 0x005F6 & 0x00859 & 0x17CB9 & 0x28A4A - True 159741 - 0xFFE41 (Quarry Obelisk Side 2) - 0x334B6 & 0x00614 & 0x0069D & 0x28A4C - True -159742 - 0xFFE42 (Quarry Obelisk Side 3) - 0x289CF & 0x289D1 & 0x33692 - True -159743 - 0xFFE43 (Quarry Obelisk Side 4) - 0x03E77 & 0x03E7C - True +159742 - 0xFFE42 (Quarry Obelisk Side 3) - 0x289CF & 0x289D1 - True +159743 - 0xFFE43 (Quarry Obelisk Side 4) - 0x33692 - True +159744 - 0xFFE44 (Quarry Obelisk Side 5) - 0x03E77 & 0x03E7C - True 159750 - 0xFFE50 (Town Obelisk Side 1) - 0x035C7 - True 159751 - 0xFFE51 (Town Obelisk Side 2) - 0x01848 & 0x03D06 & 0x33530 & 0x33600 & 0x28A2F & 0x28A37 & 0x334A3 & 0x3352F - True 159752 - 0xFFE52 (Town Obelisk Side 3) - 0x33857 & 0x33879 & 0x03C19 - True diff --git a/worlds/witness/WitnessLogicExpert.txt b/worlds/witness/WitnessLogicExpert.txt index f100a3095b..c6cc59605a 100644 --- a/worlds/witness/WitnessLogicExpert.txt +++ b/worlds/witness/WitnessLogicExpert.txt @@ -14,7 +14,7 @@ Tutorial (Tutorial) - Outside Tutorial - True: 158005 - 0x0A3B5 (Back Left) - True - Dots & Full Dots 158006 - 0x0A3B2 (Back Right) - True - Dots & Full Dots 158007 - 0x03629 (Gate Open) - 0x002C2 - Symmetry & Dots -158008 - 0x03505 (Gate Close) - 0x2FAF6 - False +158008 - 0x03505 (Gate Close) - 0x2FAF6 & 0x03629 - False 158009 - 0x0C335 (Pillar) - True - Triangles 158010 - 0x0C373 (Patio Floor) - 0x0C335 - Dots 159512 - 0x33530 (Cloud EP) - True - True @@ -270,7 +270,7 @@ Door - 0x0368A (Stairs) - 0x03677 159413 - 0x00614 (Lift EP) - 0x275FF & 0x03675 - True Quarry Boathouse (Quarry Boathouse) - Quarry - True - Quarry Boathouse Upper Front - 0x03852 - Quarry Boathouse Behind Staircase - 0x2769B: -158146 - 0x034D4 (Intro Left) - True - Stars & Eraser +158146 - 0x034D4 (Intro Left) - True - Stars & Stars + Same Colored Symbol & Eraser 158147 - 0x021D5 (Intro Right) - True - Shapers & Eraser 158148 - 0x03852 (Ramp Height Control) - 0x034D4 & 0x021D5 - Rotated Shapers 158166 - 0x17CA6 (Boat Spawn) - True - Boat @@ -411,7 +411,7 @@ Keep Tower (Keep) - Keep - 0x04F8F: 158206 - 0x0361B (Tower Shortcut Panel) - True - True Door - 0x04F8F (Tower Shortcut) - 0x0361B 158704 - 0x0360E (Laser Panel Hedges) - 0x01A0F & 0x019E7 & 0x019DC & 0x00139 - True -158705 - 0x03317 (Laser Panel Pressure Plates) - 0x01BE9 - Shapers & Rotated Shapers & Triangles & Stars & Stars + Same Colored Symbol & Colored Squares & Black/White Squares +158705 - 0x03317 (Laser Panel Pressure Plates) - 0x033EA & 0x01BE9 & 0x01CD3 & 0x01D3F - Shapers & Rotated Shapers & Triangles & Stars & Stars + Same Colored Symbol & Colored Squares & Black/White Squares Laser - 0x014BB (Laser) - 0x0360E | 0x03317 159240 - 0x033BE (Pressure Plates 1 EP) - 0x033EA - True 159241 - 0x033BF (Pressure Plates 2 EP) - 0x01BE9 - True @@ -909,7 +909,7 @@ Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near - 158430 - 0x09FD8 (Near Row 5) - 0x09FD7 - Stars & Stars + Same Colored Symbol & Rotated Shapers & Eraser Door - 0x09FFB (Staircase Near) - 0x09FD8 -Mountain Floor 2 Blue Bridge (Mountain Floor 2) - Mountain Floor 2 Beyond Bridge - TrueOneWay - Mountain Floor 2 At Door - TrueOneWay: +Mountain Floor 2 Blue Bridge (Mountain Floor 2) - Mountain Floor 2 Beyond Bridge - TrueOneWay - Mountain Floor 2 At Door - 0x09ED8: Mountain Floor 2 At Door (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EDD: Door - 0x09EDD (Elevator Room Entry) - 0x09ED8 & 0x09E86 @@ -1113,11 +1113,12 @@ Obelisks (EPs) - Entry - True: 159735 - 0xFFE35 (River Obelisk Side 6) - 0x035CB & 0x035CF - True 159740 - 0xFFE40 (Quarry Obelisk Side 1) - 0x28A7B & 0x005F6 & 0x00859 & 0x17CB9 & 0x28A4A - True 159741 - 0xFFE41 (Quarry Obelisk Side 2) - 0x334B6 & 0x00614 & 0x0069D & 0x28A4C - True -159742 - 0xFFE42 (Quarry Obelisk Side 3) - 0x289CF & 0x289D1 & 0x33692 - True -159743 - 0xFFE43 (Quarry Obelisk Side 4) - 0x03E77 & 0x03E7C - True +159742 - 0xFFE42 (Quarry Obelisk Side 3) - 0x289CF & 0x289D1 - True +159743 - 0xFFE43 (Quarry Obelisk Side 4) - 0x33692 - True +159744 - 0xFFE44 (Quarry Obelisk Side 5) - 0x03E77 & 0x03E7C - True 159750 - 0xFFE50 (Town Obelisk Side 1) - 0x035C7 - True 159751 - 0xFFE51 (Town Obelisk Side 2) - 0x01848 & 0x03D06 & 0x33530 & 0x33600 & 0x28A2F & 0x28A37 & 0x334A3 & 0x3352F - True 159752 - 0xFFE52 (Town Obelisk Side 3) - 0x33857 & 0x33879 & 0x03C19 - True 159753 - 0xFFE53 (Town Obelisk Side 4) - 0x28B30 & 0x035C9 - True 159754 - 0xFFE54 (Town Obelisk Side 5) - 0x03335 & 0x03412 & 0x038A6 & 0x038AA & 0x03E3F & 0x03E40 & 0x28B8E - True -159755 - 0xFFE55 (Town Obelisk Side 6) - 0x28B91 & 0x03BCE & 0x03BCF & 0x03BD1 & 0x339B6 & 0x33A20 & 0x33A29 & 0x33A2A & 0x33B06 - True \ No newline at end of file +159755 - 0xFFE55 (Town Obelisk Side 6) - 0x28B91 & 0x03BCE & 0x03BCF & 0x03BD1 & 0x339B6 & 0x33A20 & 0x33A29 & 0x33A2A & 0x33B06 - True diff --git a/worlds/witness/WitnessLogicVanilla.txt b/worlds/witness/WitnessLogicVanilla.txt index 294950c305..9c62cc98d8 100644 --- a/worlds/witness/WitnessLogicVanilla.txt +++ b/worlds/witness/WitnessLogicVanilla.txt @@ -14,7 +14,7 @@ Tutorial (Tutorial) - Outside Tutorial - 0x03629: 158005 - 0x0A3B5 (Back Left) - True - True 158006 - 0x0A3B2 (Back Right) - True - True 158007 - 0x03629 (Gate Open) - 0x002C2 & 0x0A3B5 & 0x0A3B2 - True -158008 - 0x03505 (Gate Close) - 0x2FAF6 - True +158008 - 0x03505 (Gate Close) - 0x2FAF6 & 0x03629 - True 158009 - 0x0C335 (Pillar) - True - Triangles - True 158010 - 0x0C373 (Patio Floor) - 0x0C335 - Dots 159512 - 0x33530 (Cloud EP) - True - True @@ -411,7 +411,7 @@ Keep Tower (Keep) - Keep - 0x04F8F: 158206 - 0x0361B (Tower Shortcut Panel) - True - True Door - 0x04F8F (Tower Shortcut) - 0x0361B 158704 - 0x0360E (Laser Panel Hedges) - 0x01A0F & 0x019E7 & 0x019DC & 0x00139 - True -158705 - 0x03317 (Laser Panel Pressure Plates) - 0x01D3F - Shapers & Black/White Squares & Rotated Shapers +158705 - 0x03317 (Laser Panel Pressure Plates) - 0x033EA & 0x01BE9 & 0x01CD3 & 0x01D3F - Shapers & Black/White Squares & Rotated Shapers Laser - 0x014BB (Laser) - 0x0360E | 0x03317 159240 - 0x033BE (Pressure Plates 1 EP) - 0x033EA - True 159241 - 0x033BF (Pressure Plates 2 EP) - 0x01BE9 - True @@ -909,7 +909,7 @@ Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near - 158430 - 0x09FD8 (Near Row 5) - 0x09FD7 - Colored Squares & Symmetry Door - 0x09FFB (Staircase Near) - 0x09FD8 -Mountain Floor 2 Blue Bridge (Mountain Floor 2) - Mountain Floor 2 Beyond Bridge - TrueOneWay - Mountain Floor 2 At Door - TrueOneWay: +Mountain Floor 2 Blue Bridge (Mountain Floor 2) - Mountain Floor 2 Beyond Bridge - TrueOneWay - Mountain Floor 2 At Door - 0x09ED8: Mountain Floor 2 At Door (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EDD: Door - 0x09EDD (Elevator Room Entry) - 0x09ED8 & 0x09E86 @@ -1113,8 +1113,9 @@ Obelisks (EPs) - Entry - True: 159735 - 0xFFE35 (River Obelisk Side 6) - 0x035CB & 0x035CF - True 159740 - 0xFFE40 (Quarry Obelisk Side 1) - 0x28A7B & 0x005F6 & 0x00859 & 0x17CB9 & 0x28A4A - True 159741 - 0xFFE41 (Quarry Obelisk Side 2) - 0x334B6 & 0x00614 & 0x0069D & 0x28A4C - True -159742 - 0xFFE42 (Quarry Obelisk Side 3) - 0x289CF & 0x289D1 & 0x33692 - True -159743 - 0xFFE43 (Quarry Obelisk Side 4) - 0x03E77 & 0x03E7C - True +159742 - 0xFFE42 (Quarry Obelisk Side 3) - 0x289CF & 0x289D1 - True +159743 - 0xFFE43 (Quarry Obelisk Side 4) - 0x33692 - True +159744 - 0xFFE44 (Quarry Obelisk Side 5) - 0x03E77 & 0x03E7C - True 159750 - 0xFFE50 (Town Obelisk Side 1) - 0x035C7 - True 159751 - 0xFFE51 (Town Obelisk Side 2) - 0x01848 & 0x03D06 & 0x33530 & 0x33600 & 0x28A2F & 0x28A37 & 0x334A3 & 0x3352F - True 159752 - 0xFFE52 (Town Obelisk Side 3) - 0x33857 & 0x33879 & 0x03C19 - True diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index 71bebb6eb1..358e063403 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -14,7 +14,7 @@ from .items import WitnessItem, StaticWitnessItems, WitnessPlayerItems from .rules import set_rules from .regions import WitnessRegions from .Options import is_option_enabled, the_witness_options, get_option_value -from .utils import best_junk_to_add_based_on_weights, get_audio_logs +from .utils import best_junk_to_add_based_on_weights, get_audio_logs, make_warning_string from logging import warning @@ -38,7 +38,7 @@ class WitnessWorld(World): """ game = "The Witness" topology_present = False - data_version = 12 + data_version = 13 static_logic = StaticWitnessLogic() static_locat = StaticWitnessLocations() @@ -52,7 +52,7 @@ class WitnessWorld(World): location_name_to_id = StaticWitnessLocations.ALL_LOCATIONS_TO_ID item_name_groups = StaticWitnessItems.ITEM_NAME_GROUPS - required_client_version = (0, 3, 8) + required_client_version = (0, 3, 9) def _get_slot_data(self): return { @@ -67,7 +67,7 @@ class WitnessWorld(World): 'progressive_item_lists': self.items.MULTI_LISTS_BY_CODE, 'obelisk_side_id_to_EPs': self.static_logic.OBELISK_SIDE_ID_TO_EP_HEXES, 'precompleted_puzzles': {int(h, 16) for h in self.player_logic.PRECOMPLETED_LOCATIONS}, - 'ep_to_name': self.static_logic.EP_ID_TO_NAME, + 'entity_to_name': self.static_logic.ENTITY_ID_TO_NAME, } def generate_early(self): @@ -93,13 +93,13 @@ class WitnessWorld(World): self.items = WitnessPlayerItems(self.locat, self.multiworld, self.player, self.player_logic) self.regio = WitnessRegions(self.locat) - def create_regions(self): - self.regio.create_regions(self.multiworld, self.player, self.player_logic) - - def generate_basic(self): self.log_ids_to_hints = dict() self.junk_items_created = {key: 0 for key in self.items.JUNK_WEIGHTS.keys()} + def create_regions(self): + self.regio.create_regions(self.multiworld, self.player, self.player_logic) + + def create_items(self): # Generate item pool pool = [] for item in self.items.ITEM_TABLE: @@ -109,22 +109,12 @@ class WitnessWorld(World): pool.append(witness_item) self.items_by_name[item] = witness_item - less_junk = 0 - - dog_check = self.multiworld.get_location( - "Town Pet the Dog", self.player - ) - - dog_check.place_locked_item(self.create_item("Puzzle Skip")) - - less_junk += 1 - for precol_item in self.multiworld.precollected_items[self.player]: if precol_item.name in self.items_by_name: # if item is in the pool, remove 1 instance. item_obj = self.items_by_name[precol_item.name] if item_obj in pool: - pool.remove(item_obj) # remove one instance of this pre-collected item if it exists + pool.remove(item_obj) # remove one instance of this pre-collected item if it exists for item in self.player_logic.STARTING_INVENTORY: self.multiworld.push_precollected(self.items_by_name[item]) @@ -132,15 +122,8 @@ class WitnessWorld(World): for item in self.items.EXTRA_AMOUNTS: for i in range(0, self.items.EXTRA_AMOUNTS[item]): - if len(pool) < len(self.locat.CHECK_LOCATION_TABLE) - len(self.locat.EVENT_LOCATION_TABLE) - less_junk: - witness_item = self.create_item(item) - pool.append(witness_item) - - # Put in junk items to fill the rest - junk_size = len(self.locat.CHECK_LOCATION_TABLE) - len(pool) - len(self.locat.EVENT_LOCATION_TABLE) - less_junk - - for i in range(0, junk_size): - pool.append(self.create_item(self.get_filler_item_name())) + witness_item = self.create_item(item) + pool.append(witness_item) # Tie Event Items to Event Locations (e.g. Laser Activations) for event_location in self.locat.EVENT_LOCATION_TABLE: @@ -150,27 +133,108 @@ class WitnessWorld(World): location_obj = self.multiworld.get_location(event_location, self.player) location_obj.place_locked_item(item_obj) - self.multiworld.itempool += pool + # Find out how much empty space there is for junk items. -1 for the "Town Pet the Dog" check + itempool_difference = len(self.locat.CHECK_LOCATION_TABLE) - len(self.locat.EVENT_LOCATION_TABLE) - 1 + itempool_difference -= len(pool) - def pre_fill(self): - # Put good item on first check if there are any of the designated "good items" in the pool + # Place two locked items: Good symbol on Tutorial Gate Open, and a Puzzle Skip on "Town Pet the Dog" good_items_in_the_game = [] + plandoed_items = set() + + for v in self.multiworld.plando_items[self.player]: + if v.get("from_pool", True): + plandoed_items.update({self.items_by_name[i] for i in v.get("items", dict()).keys() + if i in self.items_by_name}) + if "item" in v and v["item"] in self.items_by_name: + plandoed_items.add(self.items_by_name[v["item"]]) for symbol in self.items.GOOD_ITEMS: item = self.items_by_name[symbol] - if item in self.multiworld.itempool: # Only do this if the item is still in item pool (e.g. after plando) + if item in pool and item not in plandoed_items: + # for now, any item that is mentioned in any plando option, even if it's a list of items, is ineligible. + # Hopefully, in the future, plando gets resolved before create_items. + # I could also partially resolve lists myself, but this could introduce errors if not done carefully. good_items_in_the_game.append(symbol) if good_items_in_the_game: random_good_item = self.multiworld.random.choice(good_items_in_the_game) - first_check = self.multiworld.get_location( - "Tutorial Gate Open", self.player - ) item = self.items_by_name[random_good_item] - first_check.place_locked_item(item) - self.multiworld.itempool.remove(item) + if get_option_value(self.multiworld, self.player, "puzzle_randomization") == 1: + self.multiworld.local_early_items[self.player][random_good_item] = 1 + else: + first_check = self.multiworld.get_location( + "Tutorial Gate Open", self.player + ) + + first_check.place_locked_item(item) + pool.remove(item) + + dog_check = self.multiworld.get_location( + "Town Pet the Dog", self.player + ) + + dog_check.place_locked_item(self.create_item("Puzzle Skip")) + + # Fill rest of item pool with junk if there is room + if itempool_difference > 0: + for i in range(0, itempool_difference): + self.multiworld.itempool.append(self.create_item(self.get_filler_item_name())) + + # Remove junk, Functioning Brain, useful items (non-door), useful door items in that order until there is room + if itempool_difference < 0: + junk = [ + item for item in pool + if item.classification in {ItemClassification.filler, ItemClassification.trap} + and item.name != "Functioning Brain" + ] + + f_brain = [item for item in pool if item.name == "Functioning Brain"] + + usefuls = [ + item for item in pool + if item.classification == ItemClassification.useful + and item.name not in StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT + ] + + removable_doors = [ + item for item in pool + if item.classification == ItemClassification.useful + and item.name in StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT + ] + + self.multiworld.per_slot_randoms[self.player].shuffle(junk) + self.multiworld.per_slot_randoms[self.player].shuffle(usefuls) + self.multiworld.per_slot_randoms[self.player].shuffle(removable_doors) + + removed_junk = False + removed_usefuls = False + removed_doors = False + + for i in range(itempool_difference, 0): + if junk: + pool.remove(junk.pop()) + removed_junk = True + elif f_brain: + pool.remove(f_brain.pop()) + elif usefuls: + pool.remove(usefuls.pop()) + removed_usefuls = True + elif removable_doors: + pool.remove(removable_doors.pop()) + removed_doors = True + + warn = make_warning_string( + removed_junk, removed_usefuls, removed_doors, not junk, not usefuls, not removable_doors + ) + + if warn: + warning(f"This Witness world has too few locations to place all its items." + f" In order to make space, {warn} had to be removed.") + + # Finally, add the generated pool to the overall itempool + self.multiworld.itempool += pool def set_rules(self): set_rules(self.multiworld, self.player, self.player_logic, self.locat) diff --git a/worlds/witness/docs/setup_en.md b/worlds/witness/docs/setup_en.md index 8e85090c10..94a50846f9 100644 --- a/worlds/witness/docs/setup_en.md +++ b/worlds/witness/docs/setup_en.md @@ -3,7 +3,7 @@ ## Required Software - [The Witness for 64-bit Windows (e.g. Steam version)](https://store.steampowered.com/app/210970/The_Witness/) -- [The Witness Archipelago Randomizer](https://github.com/NewSoupVi/The-Witness-Randomizer-for-Archipelago/releases) +- [The Witness Archipelago Randomizer](https://github.com/NewSoupVi/The-Witness-Randomizer-for-Archipelago/releases/latest) ## Optional Software @@ -14,7 +14,7 @@ 1. Launch The Witness 2. Start a fresh save -3. Launch [The Witness Archipelago Randomizer](https://github.com/NewSoupVi/The-Witness-Randomizer-for-Archipelago) +3. Launch [The Witness Archipelago Randomizer](https://github.com/NewSoupVi/The-Witness-Randomizer-for-Archipelago/releases/latest) 4. Enter the Archipelago address, slot name and password 5. Press "Connect" 6. Enjoy! @@ -23,7 +23,7 @@ To continue an earlier game: 1. Launch The Witness 2. Load the save you last played this world on, if it's not the one you loaded into automatically -3. Launch [The Witness Archipelago Randomizer](https://github.com/NewSoupVi/The-Witness-Randomizer-for-Archipelago) +3. Launch [The Witness Archipelago Randomizer](https://github.com/NewSoupVi/The-Witness-Randomizer-for-Archipelago/releases/latest) 4. Press "Load Credentials" (or type them in manually) 5. Press "Connect" diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index baa6dd45dd..4c36c4826b 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -2,96 +2,96 @@ from BaseClasses import MultiWorld from .Options import is_option_enabled, get_option_value joke_hints = [ - ("Quaternions", "break", "my brain"), - ("Eclipse", "has nothing", "but you should do it anyway"), - ("", "Beep", ""), - ("Putting in custom subtitles", "shouldn't have been", "as hard as it was..."), - ("BK mode", "is right", "around the corner"), - ("", "You can do it!", ""), - ("", "I believe in you!", ""), - ("The person playing", "is", "cute <3"), - ("dash dot, dash dash dash", "dash, dot dot dot dot, dot dot", "dash dot, dash dash dot"), - ("When you think about it,", "there are actually a lot of", "bubbles in a stream"), - ("Never gonna give you up", "Never gonna let you down", "Never gonna run around and desert you"), - ("Thanks to", "the Archipelago developers", "for making this possible."), - ("Have you tried ChecksFinder?", "If you like puzzles,", "you might enjoy it!"), - ("Have you tried Dark Souls III?", "A tough game like this", "feels better when friends are helping you!"), - ("Have you tried Donkey Kong Country 3?", "A legendary game", "from a golden age of platformers!"), - ("Have you tried Factorio?", "Alone in an unknown multiworld.", "Sound familiar?"), - ("Have you tried Final Fantasy?", "Experience a classic game", "improved to fit modern standards!"), - ("Have you tried Hollow Knight?", "Another independent hit", "revolutionising a genre!"), - ("Have you tried A Link to the Past?", "The Archipelago game", "that started it all!"), - ("Have you tried Meritous?", "You should know that obscure games", "are often groundbreaking!"), - ("Have you tried Ocarine of Time?", "One of the biggest randomizers,", "big inspiration for this one's features!"), - ("Have you tried Raft?", "Haven't you always wanted to explore", "the ocean surrounding this island?"), - ("Have you tried Risk of Rain 2?", "I haven't either.", "But I hear it's incredible!"), - ("Have you tried Rogue Legacy?", "After solving so many puzzles", "it's the perfect way to rest your brain."), - ("Have you tried Secret of Evermore?", "I haven't either", "But I hear it's great!"), - ("Have you tried Slay the Spire?", "Experience the thrill of combat", "without needing fast fingers!"), - ("Have you tried SMZ3?", "Why play one incredible game", "when you can play 2 at once?"), - ("Have you tried Starcraft 2?", "Use strategy and management", "to crush your enemies!"), - ("Have you tried Super Mario 64?", "3-dimensional games like this", "owe everything to that game."), - ("Have you tried Super Metroid?", "A classic game", "that started a whole genre."), - ("Have you tried Timespinner?", "Everyone who plays it", "ends up loving it!"), - ("Have you tried VVVVVV?", "Experience the essence of gaming", "distilled into its purest form!"), - ("Have you tried The Witness?", "Oh. I guess you already have.", " Thanks for playing!"), - ("Have you tried Super Mario World?", "I don't think I need to tell you", "that it is beloved by many."), - ("Have you tried Overcooked 2?", "When you're done relaxing with puzzles,", - "use your energy to yell at your friends."), - ("Have you tried Zillion?", "Me neither. But it looks fun.", "So, let's try something new together?"), - ("Have you tried Hylics 2?", "Stop motion might just be", "the epitome of unique art styles."), - ("Have you tried Pokemon Red&Blue?", "A cute pet collecting game", "that fascinated an entire generation."), - ("Waiting to get your items?", "Try BK Sudoku!", "Make progress even while stuck."), - ("One day I was fascinated", "by the subject of", "generation of waves by wind"), - ("I don't like sandwiches", "Why would you think I like sandwiches?", "Have you ever seen me with a sandwich?"), - ("Where are you right now?", "I'm at soup!", "What do you mean you're at soup?"), - ("Remember to ask", "in the Archipelago Discord", "what the Functioning Brain does."), - ("", "Don't use your puzzle skips", "you might need them later"), - ("", "For an extra challenge", "Try playing blindfolded"), - ("Go to the top of the mountain", "and see if you can see", "your house"), - ("Yellow = Red + Green", "Cyan = Green + Blue", "Magenta = Red + Blue"), - ("", "Maybe that panel really is unsolvable", ""), - ("", "Did you make sure it was plugged in?", ""), - ("", "Do not look into laser with remaining eye", ""), - ("", "Try pressing Space to jump", ""), - ("The Witness is a Doom clone.", "Just replace the demons", "with puzzles"), - ("", "Test Hint please ignore", ""), - ("Shapers can never be placed", "outside the panel boundaries", "even if subtracted."), - ("", "The Keep laser panels use", "the same trick on both sides!"), - ("Can't get past a door? Try going around.", "Can't go around? Try building a", "nether portal."), - ("", "We've been trying to reach you", "about your car's extended warranty"), - ("I hate this game. I hate this game.", "I hate this game.", "-chess player Bobby Fischer"), - ("Dear Mario,", "Please come to the castle.", "I've baked a cake for you!"), - ("Have you tried waking up?", "", "Yeah, me neither."), - ("Why do they call it The Witness,", "when wit game the player view", "play of with the game."), - ("", "THE WIND FISH IN NAME ONLY", "FOR IT IS NEITHER"), - ("Like this game? Try The Wit.nes,", "Understand, INSIGHT, Taiji", "What the Witness?, and Tametsi."), - ("", "In a race", "It's survival of the Witnesst"), - ("", "This hint has been removed", "We apologize for your inconvenience."), - ("", "O-----------", ""), - ("Circle is draw", "Square is separate", "Line is win"), - ("Circle is draw", "Star is pair", "Line is win"), - ("Circle is draw", "Circle is copy", "Line is win"), - ("Circle is draw", "Dot is eat", "Line is win"), - ("Circle is start", "Walk is draw", "Line is win"), - ("Circle is start", "Line is win", "Witness is you"), - ("Can't find any items?", "Consider a relaxing boat trip", "around the island"), - ("", "Don't forget to like, comment, and subscribe", ""), - ("Ah crap, gimme a second.", "[papers rustling]", "Sorry, nothing."), - ("", "Trying to get a hint?", "Too bad."), - ("", "Here's a hint:", "Get good at the game."), - ("", "I'm still not entirely sure", "what we're witnessing here."), - ("Have you found a red page yet?", "No?", "Then have you found a blue page?"), - ( - "And here we see the Witness player,", - "seeking answers where there are none-", - "Did someone turn on the loudspeaker?" - ), - ( - "Hints suggested by:", - "IHNN, Beaker, MrPokemon11, Ember, TheM8, NewSoupVi,", - "KF, Yoshi348, Berserker, BowlinJim, oddGarrett, Pink Switch." - ), + "Quaternions break my brain", + "Eclipse has nothing, but you should do it anyway.", + "Beep", + "Putting in custom subtitles shouldn't have been as hard as it was...", + "BK mode is right around the corner.", + "You can do it!", + "I believe in you!", + "The person playing is cute. <3", + "dash dot, dash dash dash, dash, dot dot dot dot, dot dot, dash dot, dash dash dot", + "When you think about it, there are actually a lot of bubbles in a stream.", + "Never gonna give you up\nNever gonna let you down\nNever gonna run around and desert you", + "Thanks to the Archipelago developers for making this possible.", + "Have you tried ChecksFinder?\nIf you like puzzles, you might enjoy it!", + "Have you tried Dark Souls III?\nA tough game like this feels better when friends are helping you!", + "Have you tried Donkey Kong Country 3?\nA legendary game from a golden age of platformers!", + "Have you tried Factorio?\nAlone in an unknown multiworld. Sound familiar?", + "Have you tried Final Fantasy?\nExperience a classic game improved to fit modern standards!", + "Have you tried Hollow Knight?\nAnother independent hit revolutionising a genre!", + "Have you tried A Link to the Past?\nThe Archipelago game that started it all!", + "Have you tried Meritous?\nYou should know that obscure games are often groundbreaking!", + "Have you tried Ocarina of Time?\nOne of the biggest randomizers, big inspiration for this one's features!", + "Have you tried Raft?\nHaven't you always wanted to explore the ocean surrounding this island?", + "Have you tried Risk of Rain 2?\nI haven't either. But I hear it's incredible!", + "Have you tried Rogue Legacy?\nAfter solving so many puzzles it's the perfect way to rest your brain.", + "Have you tried Secret of Evermore?\nI haven't either But I hear it's great!", + "Have you tried Slay the Spire?\nExperience the thrill of combat without needing fast fingers!", + "Have you tried SMZ3?\nWhy play one incredible game when you can play 2 at once?", + "Have you tried Starcraft 2?\nUse strategy and management to crush your enemies!", + "Have you tried Super Mario 64?\n3-dimensional games like this owe everything to that game.", + "Have you tried Super Metroid?\nA classic game, yet still one of the best in the genre.", + "Have you tried Timespinner?\nEveryone who plays it ends up loving it!", + "Have you tried VVVVVV?\nExperience the essence of gaming distilled into its purest form!", + "Have you tried The Witness?\nOh. I guess you already have. Thanks for playing!", + "Have you tried Super Mario World?\nI don't think I need to tell you that it is beloved by many.", + "Have you tried Overcooked 2?\nWhen you're done relaxing with puzzles, use your energy to yell at your friends.", + "Have you tried Zillion?\nMe neither. But it looks fun. So, let's try something new together?", + "Have you tried Hylics 2?\nStop motion might just be the epitome of unique art styles.", + "Have you tried Pokemon Red&Blue?\nA cute pet collecting game that fascinated an entire generation.", + "Have you tried Lufia II?\nRoguelites are not just a 2010s phenomenon, turns out.", + "Have you tried Minecraft?\nI have recently learned this is a question that needs to be asked.", + "Have you tried Subnautica?\nIf you like this game's lonely atmosphere, I would suggest you try it.", + + "Have you tried Sonic Adventure 2?\nIf the silence on this island is getting to you, " + "there aren't many games more energetic.", + + "Waiting to get your items?\nTry BK Sudoku! Make progress even while stuck.", + "One day I was fascinated by the subject of generation of waves by wind.", + "I don't like sandwiches. Why would you think I like sandwiches? Have you ever seen me with a sandwich?", + "Where are you right now?\nI'm at soup!\nWhat do you mean you're at soup?", + "Remember to ask in the Archipelago Discord what the Functioning Brain does.", + "Don't use your puzzle skips, you might need them later.", + "For an extra challenge, try playing blindfolded.", + "Go to the top of the mountain and see if you can see your house.", + "Yellow = Red + Green\nCyan = Green + Blue\nMagenta = Red + Blue", + "Maybe that panel really is unsolvable.", + "Did you make sure it was plugged in?", + "Do not look into laser with remaining eye.", + "Try pressing Space to jump.", + "The Witness is a Doom clone.\nJust replace the demons with puzzles", + "Test Hint please ignore", + "Shapers can never be placed outside the panel boundaries, even if subtracted.", + "The Keep laser panels use the same trick on both sides!", + "Can't get past a door? Try going around. Can't go around? Try building a nether portal.", + "We've been trying to reach you about your car's extended warranty.", + "I hate this game. I hate this game. I hate this game.\n- Chess player Bobby Fischer", + "Dear Mario,\nPlease come to the castle. I've baked a cake for you!", + "Have you tried waking up?\nYeah, me neither.", + "Why do they call it The Witness, when wit game the player view play of with the game.", + "THE WIND FISH IN NAME ONLY, FOR IT IS NEITHER", + "Like this game?\nTry The Wit.nes, Understand, INSIGHT, Taiji What the Witness?, and Tametsi.", + "In a race, It's survival of the Witnesst.", + "This hint has been removed. We apologize for your inconvenience.", + "O-----------", + "Circle is draw\nSquare is separate\nLine is win", + "Circle is draw\nStar is pair\nLine is win", + "Circle is draw\nCircle is copy\nLine is win", + "Circle is draw\nDot is eat\nLine is win", + "Circle is start\nWalk is draw\nLine is win", + "Circle is start\nLine is win\nWitness is you", + "Can't find any items?\nConsider a relaxing boat trip around the island!", + "Don't forget to like, comment, and subscribe.", + "Ah crap, gimme a second.\n[papers rustling]\nSorry, nothing.", + "Trying to get a hint? Too bad.", + "Here's a hint: Get good at the game.", + "I'm still not entirely sure what we're witnessing here.", + "Have you found a red page yet? No? Then have you found a blue page?", + "And here we see the Witness player, seeking answers where there are none-\nDid someone turn on the loudspeaker?", + + "Hints suggested by:\nIHNN, Beaker, MrPokemon11, Ember, TheM8, NewSoupVi," + "KF, Yoshi348, Berserker, BowlinJim, oddGarrett, Pink Switch.", ] @@ -186,7 +186,7 @@ def make_hint_from_item(multiworld: MultiWorld, player: int, item: str): if location_obj.player != player: location_name += " (" + multiworld.get_player_name(location_obj.player) + ")" - return location_name, item, location_obj.address if(location_obj.player == player) else -1 + return location_name, item, location_obj.address if (location_obj.player == player) else -1 def make_hint_from_location(multiworld: MultiWorld, player: int, location: str): @@ -196,7 +196,7 @@ def make_hint_from_location(multiworld: MultiWorld, player: int, location: str): if item_obj.player != player: item_name += " (" + multiworld.get_player_name(item_obj.player) + ")" - return location, item_name, location_obj.address if(location_obj.player == player) else -1 + return location, item_name, location_obj.address if (location_obj.player == player) else -1 def make_hints(multiworld: MultiWorld, player: int, hint_amount: int): @@ -258,9 +258,9 @@ def make_hints(multiworld: MultiWorld, player: int, hint_amount: int): for loc, item in always_hint_pairs.items(): if item[1]: - hints.append((item[0], "can be found at", loc, item[2])) + hints.append((f"{item[0]} can be found at {loc}.", item[2])) else: - hints.append((loc, "contains", item[0], item[2])) + hints.append((f"{loc} contains {item[0]}.", item[2])) multiworld.per_slot_randoms[player].shuffle(hints) # shuffle always hint order in case of low hint amount @@ -279,9 +279,9 @@ def make_hints(multiworld: MultiWorld, player: int, hint_amount: int): del priority_hint_pairs[loc] if item[1]: - hints.append((item[0], "can be found at", loc, item[2])) + hints.append((f"{item[0]} can be found at {loc}.", item[2])) else: - hints.append((loc, "contains", item[0], item[2])) + hints.append((f"{loc} contains {item[0]}.", item[2])) continue if next_random_hint_is_item: @@ -290,10 +290,10 @@ def make_hints(multiworld: MultiWorld, player: int, hint_amount: int): continue hint = make_hint_from_item(multiworld, player, prog_items_in_this_world.pop()) - hints.append((hint[1], "can be found at", hint[0], hint[2])) + hints.append((f"{hint[1]} can be found at {hint[0]}.", hint[2])) else: hint = make_hint_from_location(multiworld, player, locations_in_this_world.pop()) - hints.append((hint[0], "contains", hint[1], hint[2])) + hints.append((f"{hint[0]} contains {hint[1]}.", hint[2])) next_random_hint_is_item = not next_random_hint_is_item @@ -301,4 +301,4 @@ def make_hints(multiworld: MultiWorld, player: int, hint_amount: int): def generate_joke_hints(multiworld: MultiWorld, player: int, amount: int): - return [(x, y, z, -1) for (x, y, z) in multiworld.per_slot_randoms[player].sample(joke_hints, amount)] + return [(x, -1) for x in multiworld.per_slot_randoms[player].sample(joke_hints, amount)] diff --git a/worlds/witness/items.py b/worlds/witness/items.py index 914c1af2c0..4beb3b0290 100644 --- a/worlds/witness/items.py +++ b/worlds/witness/items.py @@ -205,13 +205,10 @@ class WitnessPlayerItems: ] if is_option_enabled(multiworld, player, "shuffle_discarded_panels"): - if is_option_enabled(multiworld, player, "shuffle_discarded_panels"): - if get_option_value(multiworld, player, "puzzle_randomization") == 1: - self.GOOD_ITEMS.append("Arrows") - else: - self.GOOD_ITEMS.append("Triangles") - if not is_option_enabled(multiworld, player, "disable_non_randomized_puzzles"): - self.GOOD_ITEMS.append("Colored Squares") + if get_option_value(multiworld, player, "puzzle_randomization") == 1: + self.GOOD_ITEMS.append("Arrows") + else: + self.GOOD_ITEMS.append("Triangles") self.GOOD_ITEMS = [ StaticWitnessLogic.ITEMS_TO_PROGRESSIVE.get(item, item) for item in self.GOOD_ITEMS diff --git a/worlds/witness/locations.py b/worlds/witness/locations.py index 37f61646d1..f9d1012cb4 100644 --- a/worlds/witness/locations.py +++ b/worlds/witness/locations.py @@ -13,13 +13,10 @@ class StaticWitnessLocations: """ ID_START = 158000 - EXTRA_LOCATIONS = { + GENERAL_LOCATIONS = { "Tutorial Front Left", "Tutorial Back Left", "Tutorial Back Right", - } - - GENERAL_LOCATIONS = { "Tutorial Gate Open", "Outside Tutorial Vault Box", @@ -302,6 +299,7 @@ class StaticWitnessLocations: "Quarry Obelisk Side 2", "Quarry Obelisk Side 3", "Quarry Obelisk Side 4", + "Quarry Obelisk Side 5", "Town Obelisk Side 1", "Town Obelisk Side 2", "Town Obelisk Side 3", @@ -338,6 +336,7 @@ class StaticWitnessLocations: "Quarry Obelisk Side 2", "Quarry Obelisk Side 3", "Quarry Obelisk Side 4", + "Quarry Obelisk Side 5", "Town Obelisk Side 1", "Town Obelisk Side 2", "Town Obelisk Side 3", @@ -388,6 +387,9 @@ class StaticWitnessLocations: "Mountain Floor 2 Near Row 5", "Mountain Floor 2 Far Row 6", + "Mountain Floor 2 Light Bridge Controller Near", + "Mountain Floor 2 Light Bridge Controller Far", + "Mountain Bottom Floor Yellow Bridge EP", "Mountain Bottom Floor Blue Bridge EP", "Mountain Floor 2 Pink Bridge EP", diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index 34b53b62ff..3e81993dc9 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -40,7 +40,7 @@ class WitnessPlayerLogic: Panels outside of the same region will still be checked manually. """ - if panel_hex in self.COMPLETELY_DISABLED_CHECKS: + if panel_hex in self.COMPLETELY_DISABLED_CHECKS or panel_hex in self.PRECOMPLETED_LOCATIONS: return frozenset() check_obj = self.REFERENCE_LOGIC.CHECKS_BY_HEX[panel_hex] @@ -433,7 +433,6 @@ class WitnessPlayerLogic: self.ADDED_CHECKS = set() self.VICTORY_LOCATION = "0x0356B" self.EVENT_ITEM_NAMES = { - "0x01A0F": "Keep Laser Panel (Hedge Mazes) Activates", "0x09D9B": "Monastery Shutters Open", "0x193A6": "Monastery Laser Panel Activates", "0x00037": "Monastery Branch Panels Activate", @@ -442,8 +441,11 @@ class WitnessPlayerLogic: "0x00139": "Keep Hedges 1 Knowledge", "0x019DC": "Keep Hedges 2 Knowledge", "0x019E7": "Keep Hedges 3 Knowledge", - "0x01D3F": "Keep Laser Panel (Pressure Plates) Activates", - "0x01BE9": "Keep Laser Panel (Pressure Plates) Activates - Expert", + "0x01A0F": "Keep Hedges 4 Knowledge", + "0x033EA": "Pressure Plates 1 Knowledge", + "0x01BE9": "Pressure Plates 2 Knowledge", + "0x01CD3": "Pressure Plates 3 Knowledge", + "0x01D3F": "Pressure Plates 4 Knowledge", "0x09F7F": "Mountain Access", "0x0367C": "Quarry Laser Stoneworks Requirement Met", "0x009A1": "Swamp Between Bridges Far 1 Activates", @@ -492,11 +494,9 @@ class WitnessPlayerLogic: "0x17D02": "Windmill Blades Spinning", "0x0A0C9": "Cargo Box EP completable", "0x09E39": "Pink Light Bridge Extended", - "0x01CD3": "Pressure Plates 3 EP available", "0x17CC4": "Rails EP available", "0x2896A": "Bridge Underside EP available", "0x00064": "First Tunnel EP visible", - "0x033EA": "Pressure Plates 1 EP available", "0x03553": "Tutorial Video EPs availble", "0x17C79": "Bunker Door EP available", "0x275FF": "Stoneworks Light EPs available", diff --git a/worlds/witness/regions.py b/worlds/witness/regions.py index 69b0317d85..0e15cafe10 100644 --- a/worlds/witness/regions.py +++ b/worlds/witness/regions.py @@ -27,20 +27,18 @@ class WitnessRegions: ) def connect(self, world: MultiWorld, player: int, source: str, target: str, player_logic: WitnessPlayerLogic, - panel_hex_to_solve_set=frozenset({frozenset()})): + panel_hex_to_solve_set=frozenset({frozenset()}), backwards: bool = False): """ connect two regions and set the corresponding requirement """ source_region = world.get_region(source, player) target_region = world.get_region(target, player) - #print(source_region) - #print(target_region) - #print("---") + backwards = " Backwards" if backwards else "" connection = Entrance( player, - source + " to " + target, + source + " to " + target + backwards, source_region ) @@ -92,10 +90,18 @@ class WitnessRegions: self.connect(world, player, region_name, connection[0], player_logic, frozenset({frozenset()})) continue + backwards_connections = set() + for subset in connection[1]: if all({panel in player_logic.DOOR_ITEMS_BY_ID for panel in subset}): if all({reference_logic.CHECKS_BY_HEX[panel]["id"] is None for panel in subset}): - self.connect(world, player, connection[0], region_name, player_logic, frozenset({subset})) + backwards_connections.add(subset) + + if backwards_connections: + self.connect( + world, player, connection[0], region_name, player_logic, + frozenset(backwards_connections), True + ) self.connect(world, player, region_name, connection[0], player_logic, connection[1]) diff --git a/worlds/witness/settings/Disable_Unrandomized.txt b/worlds/witness/settings/Disable_Unrandomized.txt index 8f6034ccb9..dbe9caa5be 100644 --- a/worlds/witness/settings/Disable_Unrandomized.txt +++ b/worlds/witness/settings/Disable_Unrandomized.txt @@ -114,15 +114,17 @@ Disabled Locations: 0x17CAA (River Garden Entry Panel) -0x034A7 (Left Shutter EP) -0x034AD (Middle Shutter EP) -0x034AF (Right Shutter EP) -0x339B6 (Eclipse EP) - 0x03549 - True -0x33A29 (Window EP) - 0x03553 - True -0x33A2A (Door EP) - 0x03553 - True -0x33B06 (Church EP) - 0x0354E - True -0x3352F (Gate EP) -0x33600 (Patio Flowers EP) -0x035F5 (Tinted Door EP) -0x000D3 (Green Room Flowers EP) -0x33A20 (Theater Flowers EP) \ No newline at end of file +Precompleted Locations: +0x034A7 +0x034AD +0x034AF +0x339B6 +0x33A29 +0x33A2A +0x33B06 +0x3352F +0x33600 +0x035F5 +0x000D3 +0x33A20 +0x03BE2 \ No newline at end of file diff --git a/worlds/witness/settings/EP_Shuffle/EP_Easy.txt b/worlds/witness/settings/EP_Shuffle/EP_Easy.txt index 3e168b5891..939055169a 100644 --- a/worlds/witness/settings/EP_Shuffle/EP_Easy.txt +++ b/worlds/witness/settings/EP_Shuffle/EP_Easy.txt @@ -9,4 +9,6 @@ Precompleted Locations: 0x33857 0x33879 0x016B2 -0x036CE \ No newline at end of file +0x036CE +0x03B25 +0x28B2A \ No newline at end of file diff --git a/worlds/witness/static_logic.py b/worlds/witness/static_logic.py index 4311a84fa1..f395613b91 100644 --- a/worlds/witness/static_logic.py +++ b/worlds/witness/static_logic.py @@ -81,8 +81,6 @@ class StaticWitnessLogicObj: full_check_name = check_name elif "EP" in check_name: location_type = "EP" - - self.EP_ID_TO_NAME[check_hex] = full_check_name else: location_type = "General" @@ -114,6 +112,8 @@ class StaticWitnessLogicObj: "panelType": location_type } + self.ENTITY_ID_TO_NAME[check_hex] = full_check_name + self.CHECKS_BY_NAME[self.CHECKS_BY_HEX[check_hex]["checkName"]] = self.CHECKS_BY_HEX[check_hex] self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[check_hex] = requirement @@ -132,7 +132,7 @@ class StaticWitnessLogicObj: self.EP_TO_OBELISK_SIDE = dict() - self.EP_ID_TO_NAME = dict() + self.ENTITY_ID_TO_NAME = dict() self.read_logic_file(file_path) @@ -159,7 +159,7 @@ class StaticWitnessLogic: EP_TO_OBELISK_SIDE = dict() - EP_ID_TO_NAME = dict() + ENTITY_ID_TO_NAME = dict() def parse_items(self): """ @@ -235,4 +235,4 @@ class StaticWitnessLogic: self.EP_TO_OBELISK_SIDE.update(self.sigma_normal.EP_TO_OBELISK_SIDE) - self.EP_ID_TO_NAME.update(self.sigma_normal.EP_ID_TO_NAME) \ No newline at end of file + self.ENTITY_ID_TO_NAME.update(self.sigma_normal.ENTITY_ID_TO_NAME) \ No newline at end of file diff --git a/worlds/witness/utils.py b/worlds/witness/utils.py index dcb335edd9..7182545cf5 100644 --- a/worlds/witness/utils.py +++ b/worlds/witness/utils.py @@ -3,6 +3,42 @@ from Utils import cache_argsless from itertools import accumulate from typing import * from fractions import Fraction +from collections import Counter + + +def make_warning_string(any_j: bool, any_u: bool, any_d: bool, all_j: bool, all_u: bool, all_d: bool) -> str: + warning_string = "" + + if any_j: + if all_j: + warning_string += "all " + else: + warning_string += "some " + + warning_string += "junk" + + if any_u or any_d: + if warning_string: + warning_string += " and " + + if all_u: + warning_string += "all " + else: + warning_string += "some " + + warning_string += "usefuls" + + if any_d: + warning_string += ", including " + + if all_d: + warning_string += "all " + else: + warning_string += "some " + + warning_string += "non-essential door items" + + return warning_string def best_junk_to_add_based_on_weights(weights: Dict[Any, Fraction], created_junk: Dict[Any, int]): diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index 44d80cff50..241cb452a9 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -2,13 +2,12 @@ from collections import deque, Counter from contextlib import redirect_stdout import functools import threading -from typing import Any, Dict, List, Set, Tuple, Optional, cast +from typing import Any, Dict, List, Literal, Set, Tuple, Optional, cast import os import logging from BaseClasses import ItemClassification, LocationProgressType, \ MultiWorld, Item, CollectionState, Entrance, Tutorial -from Options import AssembleOptions from .logic import cs_to_zz_locs from .region import ZillionLocation, ZillionRegion from .options import ZillionStartChar, zillion_options, validate @@ -48,17 +47,17 @@ class ZillionWorld(World): game = "Zillion" web = ZillionWebWorld() - option_definitions: Dict[str, AssembleOptions] = zillion_options - topology_present: bool = True # indicate if world type has any meaningful layout/pathing + option_definitions = zillion_options + topology_present = True # indicate if world type has any meaningful layout/pathing # map names to their IDs - item_name_to_id: Dict[str, int] = _item_name_to_id - location_name_to_id: Dict[str, int] = _loc_name_to_id + item_name_to_id = _item_name_to_id + location_name_to_id = _loc_name_to_id # increment this every time something in your world's names/id mappings changes. # While this is set to 0 in *any* AutoWorld, the entire DataPackage is considered in testing mode and will be # retrieved by clients on every connection. - data_version: int = 1 + data_version = 1 logger: logging.Logger @@ -250,13 +249,13 @@ class ZillionWorld(World): if group["game"] == "Zillion": assert "item_pool" in group item_pool = group["item_pool"] - to_stay = "JJ" + to_stay: Literal['Apple', 'Champ', 'JJ'] = "JJ" if "JJ" in item_pool: assert "players" in group group_players = group["players"] start_chars = cast(Dict[int, ZillionStartChar], getattr(multiworld, "start_char")) players_start_chars = [ - (player, start_chars[player].get_current_option_name()) + (player, start_chars[player].current_option_name) for player in group_players ] start_char_counts = Counter(sc for _, sc in players_start_chars) diff --git a/worlds/zillion/logic.py b/worlds/zillion/logic.py index 204f242500..225076da09 100644 --- a/worlds/zillion/logic.py +++ b/worlds/zillion/logic.py @@ -42,6 +42,7 @@ def item_counts(cs: CollectionState, p: int) -> Tuple[Tuple[str, int], ...]: LogicCacheType = Dict[int, Tuple[_Counter[Tuple[str, int]], FrozenSet[Location]]] +""" { hash: (cs.prog_items, accessible_locations) } """ def cs_to_zz_locs(cs: CollectionState, p: int, zz_r: Randomizer, id_to_zz_item: Dict[int, Item]) -> FrozenSet[Location]: diff --git a/worlds/zillion/options.py b/worlds/zillion/options.py index 2c5a9dd8e7..6aa88f5b22 100644 --- a/worlds/zillion/options.py +++ b/worlds/zillion/options.py @@ -276,14 +276,14 @@ def validate(world: "MultiWorld", p: int) -> "Tuple[ZzOptions, Counter[str]]": skill = wo.skill[p].value jump_levels = cast(ZillionJumpLevels, wo.jump_levels[p]) - jump_option = jump_levels.get_current_option_name().lower() + jump_option = jump_levels.current_key required_level = char_to_jump["Apple"][cast(ZzVBLR, jump_option)].index(3) + 1 if skill == 0: # because of hp logic on final boss required_level = 8 gun_levels = cast(ZillionGunLevels, wo.gun_levels[p]) - gun_option = gun_levels.get_current_option_name().lower() + gun_option = gun_levels.current_key guns_required = char_to_gun["Champ"][cast(ZzVBLR, gun_option)].index(3) floppy_req = cast(ZillionFloppyReq, wo.floppy_req[p]) @@ -347,10 +347,14 @@ def validate(world: "MultiWorld", p: int) -> "Tuple[ZzOptions, Counter[str]]": # that should be all of the level requirements met + name_capitalization = { + "jj": "JJ", + "apple": "Apple", + "champ": "Champ", + } + start_char = cast(ZillionStartChar, wo.start_char[p]) - start_char_name = start_char.get_current_option_name() - if start_char_name == "Jj": - start_char_name = "JJ" + start_char_name = name_capitalization[start_char.current_key] assert start_char_name in chars start_char_name = cast(Chars, start_char_name) diff --git a/worlds/zillion/region.py b/worlds/zillion/region.py index 29ffb01d2b..cf5aa65889 100644 --- a/worlds/zillion/region.py +++ b/worlds/zillion/region.py @@ -15,7 +15,7 @@ class ZillionRegion(Region): name: str, hint: str, player: int, - multiworld: Optional[MultiWorld] = None) -> None: + multiworld: MultiWorld) -> None: super().__init__(name, player, multiworld, hint) self.zz_r = zz_r