mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-18 05:23:47 -07:00
Compare commits
2 Commits
webhost_ot
...
dock-dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 30fa0658b0 | |||
| 44a0c44036 |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -51,7 +51,7 @@ jobs:
|
||||
run: |
|
||||
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
|
||||
choco install innosetup --version=6.7.0 --allow-downgrade
|
||||
choco install innosetup --version=6.2.2 --allow-downgrade
|
||||
- name: Build
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
|
||||
143
.github/workflows/docker.yml
vendored
143
.github/workflows/docker.yml
vendored
@@ -11,144 +11,43 @@ on:
|
||||
- "!.github/workflows/**"
|
||||
- ".github/workflows/docker.yml"
|
||||
branches:
|
||||
- "main"
|
||||
- "dock-dev"
|
||||
tags:
|
||||
- "v?[0-9]+.[0-9]+.[0-9]*"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
push_to_registry:
|
||||
name: Push Docker image to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
image-name: ${{ steps.image.outputs.name }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
package-name: ${{ steps.package.outputs.name }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set lowercase image name
|
||||
id: image
|
||||
run: |
|
||||
echo "name=${GITHUB_REPOSITORY,,}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set package name
|
||||
id: package
|
||||
run: |
|
||||
echo "name=$(basename ${GITHUB_REPOSITORY,,})" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ steps.image.outputs.name }}
|
||||
tags: |
|
||||
type=ref,event=branch,enable={{is_not_default_branch}}
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,value=nightly,enable={{is_default_branch}}
|
||||
|
||||
- name: Compute final tags
|
||||
id: final-tags
|
||||
run: |
|
||||
readarray -t tags <<< "${{ steps.meta.outputs.tags }}"
|
||||
|
||||
if [[ "${{ github.ref_type }}" == "tag" ]]; then
|
||||
tag="${{ github.ref_name }}"
|
||||
if [[ "$tag" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
full_latest="${{ env.REGISTRY }}/${{ steps.image.outputs.name }}:latest"
|
||||
# Check if latest is already in tags to avoid duplicates
|
||||
if ! printf '%s\n' "${tags[@]}" | grep -q "^$full_latest$"; then
|
||||
tags+=("$full_latest")
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Set multiline output
|
||||
echo "tags<<EOF" >> $GITHUB_OUTPUT
|
||||
printf '%s\n' "${tags[@]}" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
build:
|
||||
needs: prepare
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- platform: amd64
|
||||
runner: ubuntu-latest
|
||||
suffix: amd64
|
||||
cache-scope: amd64
|
||||
- platform: arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
suffix: arm64
|
||||
cache-scope: arm64
|
||||
contents: read
|
||||
attestations: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
username: ${{ secrets.DOCKERHUB_USER }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Compute suffixed tags
|
||||
id: tags
|
||||
run: |
|
||||
readarray -t tags <<< "${{ needs.prepare.outputs.tags }}"
|
||||
suffixed=()
|
||||
for t in "${tags[@]}"; do
|
||||
suffixed+=("$t-${{ matrix.suffix }}")
|
||||
done
|
||||
echo "tags=$(IFS=','; echo "${suffixed[*]}")" >> $GITHUB_OUTPUT
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
||||
with:
|
||||
images: ubufugu/dockipelago
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
id: push
|
||||
uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/${{ matrix.platform }}
|
||||
push: true
|
||||
tags: ${{ steps.tags.outputs.tags }}
|
||||
labels: ${{ needs.prepare.outputs.labels }}
|
||||
cache-from: type=gha,scope=${{ matrix.cache-scope }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.cache-scope }}
|
||||
provenance: false
|
||||
|
||||
manifest:
|
||||
needs: [prepare, build]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create and push multi-arch manifest
|
||||
run: |
|
||||
readarray -t tag_array <<< "${{ needs.prepare.outputs.tags }}"
|
||||
|
||||
for tag in "${tag_array[@]}"; do
|
||||
docker manifest create "$tag" \
|
||||
"$tag-amd64" \
|
||||
"$tag-arm64"
|
||||
|
||||
docker manifest push "$tag"
|
||||
done
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -45,7 +45,6 @@ EnemizerCLI/
|
||||
/SNI/
|
||||
/sni-*/
|
||||
/appimagetool*
|
||||
/VC_redist.x64.exe
|
||||
/host.yaml
|
||||
/options.yaml
|
||||
/config.yaml
|
||||
|
||||
@@ -727,7 +727,6 @@ class CollectionState():
|
||||
advancements: Set[Location]
|
||||
path: Dict[Union[Region, Entrance], PathValue]
|
||||
locations_checked: Set[Location]
|
||||
"""Internal cache for Advancement Locations already checked by this CollectionState. Not for use in logic."""
|
||||
stale: Dict[int, bool]
|
||||
allow_partial_entrances: bool
|
||||
additional_init_functions: List[Callable[[CollectionState, MultiWorld], None]] = []
|
||||
|
||||
@@ -773,7 +773,7 @@ class CommonContext:
|
||||
if len(parts) == 1:
|
||||
parts = title.split(', ', 1)
|
||||
if len(parts) > 1:
|
||||
text = f"{parts[1]}\n\n{text}" if text else parts[1]
|
||||
text = parts[1] + '\n\n' + text
|
||||
title = parts[0]
|
||||
# display error
|
||||
self._messagebox = MessageBox(title, text, error=True)
|
||||
@@ -896,8 +896,6 @@ async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None)
|
||||
"May not be running Archipelago on that address or port.")
|
||||
except websockets.InvalidURI:
|
||||
ctx.handle_connection_loss("Failed to connect to the multiworld server (invalid URI)")
|
||||
except asyncio.TimeoutError:
|
||||
ctx.handle_connection_loss("Failed to connect to the multiworld server. Connection timed out.")
|
||||
except OSError:
|
||||
ctx.handle_connection_loss("Failed to connect to the multiworld server")
|
||||
except Exception:
|
||||
|
||||
1
Fill.py
1
Fill.py
@@ -280,7 +280,6 @@ def remaining_fill(multiworld: MultiWorld,
|
||||
item_to_place = itempool.pop()
|
||||
spot_to_fill: typing.Optional[Location] = None
|
||||
|
||||
# going through locations in the same order as the provided `locations` argument
|
||||
for i, location in enumerate(locations):
|
||||
if location_can_fill_item(location, item_to_place):
|
||||
# popping by index is faster than removing by content,
|
||||
|
||||
3
Main.py
3
Main.py
@@ -207,9 +207,6 @@ def main(args, seed=None, baked_server_options: dict[str, object] | None = None)
|
||||
else:
|
||||
logger.info("Progression balancing skipped.")
|
||||
|
||||
AutoWorld.call_all(multiworld, "finalize_multiworld")
|
||||
AutoWorld.call_all(multiworld, "pre_output")
|
||||
|
||||
# we're about to output using multithreading, so we're removing the global random state to prevent accidental use
|
||||
multiworld.random.passthrough = False
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ import time
|
||||
import typing
|
||||
import weakref
|
||||
import zlib
|
||||
from signal import SIGINT, SIGTERM, signal
|
||||
|
||||
import ModuleUpdate
|
||||
|
||||
@@ -497,8 +496,7 @@ class Context:
|
||||
|
||||
self.read_data = {}
|
||||
# there might be a better place to put this.
|
||||
race_mode = decoded_obj.get("race_mode", 0)
|
||||
self.read_data["race_mode"] = lambda: race_mode
|
||||
self.read_data["race_mode"] = lambda: decoded_obj.get("race_mode", 0)
|
||||
mdata_ver = decoded_obj["minimum_versions"]["server"]
|
||||
if mdata_ver > version_tuple:
|
||||
raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver}, "
|
||||
@@ -1303,13 +1301,6 @@ class CommandMeta(type):
|
||||
commands.update(base.commands)
|
||||
commands.update({command_name[5:]: method for command_name, method in attrs.items() if
|
||||
command_name.startswith("_cmd_")})
|
||||
for command_name, method in commands.items():
|
||||
# wrap async def functions so they run on default asyncio loop
|
||||
if inspect.iscoroutinefunction(method):
|
||||
def _wrapper(self, *args, _method=method, **kwargs):
|
||||
return async_start(_method(self, *args, **kwargs))
|
||||
functools.update_wrapper(_wrapper, method)
|
||||
commands[command_name] = _wrapper
|
||||
return super(CommandMeta, cls).__new__(cls, name, bases, attrs)
|
||||
|
||||
|
||||
@@ -2572,8 +2563,6 @@ async def console(ctx: Context):
|
||||
input_text = await queue.get()
|
||||
queue.task_done()
|
||||
ctx.commandprocessor(input_text)
|
||||
except asyncio.exceptions.CancelledError:
|
||||
ctx.logger.info("ConsoleTask cancelled")
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
@@ -2740,26 +2729,6 @@ async def main(args: argparse.Namespace):
|
||||
console_task = asyncio.create_task(console(ctx))
|
||||
if ctx.auto_shutdown:
|
||||
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, [console_task]))
|
||||
|
||||
def stop():
|
||||
try:
|
||||
for remove_signal in [SIGINT, SIGTERM]:
|
||||
asyncio.get_event_loop().remove_signal_handler(remove_signal)
|
||||
except NotImplementedError:
|
||||
pass
|
||||
ctx.commandprocessor._cmd_exit()
|
||||
|
||||
def shutdown(signum, frame):
|
||||
stop()
|
||||
|
||||
try:
|
||||
for sig in [SIGINT, SIGTERM]:
|
||||
asyncio.get_event_loop().add_signal_handler(sig, stop)
|
||||
except NotImplementedError:
|
||||
# add_signal_handler is only implemented for UNIX platforms
|
||||
for sig in [SIGINT, SIGTERM]:
|
||||
signal(sig, shutdown)
|
||||
|
||||
await ctx.exit_event.wait()
|
||||
console_task.cancel()
|
||||
if ctx.shutdown_task:
|
||||
|
||||
@@ -29,7 +29,7 @@ import webbrowser
|
||||
import re
|
||||
from urllib.parse import urlparse
|
||||
from worlds.AutoWorld import AutoWorldRegister, World
|
||||
from Options import (Option, Toggle, TextChoice, Choice, FreeText, NamedRange, Range, OptionSet, OptionList,
|
||||
from Options import (Option, Toggle, TextChoice, Choice, FreeText, NamedRange, Range, OptionSet, OptionList, Removed,
|
||||
OptionCounter, Visibility)
|
||||
|
||||
|
||||
@@ -318,28 +318,26 @@ class OptionsCreator(ThemedApp):
|
||||
else:
|
||||
self.show_result_snack("Name cannot be longer than 16 characters.")
|
||||
|
||||
def create_range(self, option: typing.Type[Range], name: str, bind=True):
|
||||
def create_range(self, option: typing.Type[Range], name: str):
|
||||
def update_text(range_box: VisualRange):
|
||||
self.options[name] = int(range_box.slider.value)
|
||||
range_box.tag.text = str(int(range_box.slider.value))
|
||||
return
|
||||
|
||||
box = VisualRange(option=option, name=name)
|
||||
if bind:
|
||||
box.slider.bind(value=lambda _, _1: update_text(box))
|
||||
box.slider.bind(on_touch_move=lambda _, _1: update_text(box))
|
||||
self.options[name] = option.default
|
||||
return box
|
||||
|
||||
def create_named_range(self, option: typing.Type[NamedRange], name: str):
|
||||
def set_to_custom(range_box: VisualNamedRange):
|
||||
range_box.range.tag.text = str(int(range_box.range.slider.value))
|
||||
if range_box.range.slider.value in option.special_range_names.values():
|
||||
value = next(key for key, val in option.special_range_names.items()
|
||||
if val == range_box.range.slider.value)
|
||||
self.options[name] = value
|
||||
set_button_text(box.choice, value.title())
|
||||
else:
|
||||
if (not self.options[name] == range_box.range.slider.value) \
|
||||
and (not self.options[name] in option.special_range_names or
|
||||
range_box.range.slider.value != option.special_range_names[self.options[name]]):
|
||||
# we should validate the touch here,
|
||||
# but this is much cheaper
|
||||
self.options[name] = int(range_box.range.slider.value)
|
||||
range_box.range.tag.text = str(int(range_box.range.slider.value))
|
||||
set_button_text(range_box.choice, "Custom")
|
||||
|
||||
def set_button_text(button: MDButton, text: str):
|
||||
@@ -348,7 +346,7 @@ class OptionsCreator(ThemedApp):
|
||||
def set_value(text: str, range_box: VisualNamedRange):
|
||||
range_box.range.slider.value = min(max(option.special_range_names[text.lower()], option.range_start),
|
||||
option.range_end)
|
||||
range_box.range.tag.text = str(option.special_range_names[text.lower()])
|
||||
range_box.range.tag.text = str(int(range_box.range.slider.value))
|
||||
set_button_text(range_box.choice, text)
|
||||
self.options[name] = text.lower()
|
||||
range_box.range.slider.dropdown.dismiss()
|
||||
@@ -357,18 +355,13 @@ class OptionsCreator(ThemedApp):
|
||||
# for some reason this fixes an issue causing some to not open
|
||||
box.range.slider.dropdown.open()
|
||||
|
||||
box = VisualNamedRange(option=option, name=name, range_widget=self.create_range(option, name, bind=False))
|
||||
default: int | str = option.default
|
||||
if default in option.special_range_names:
|
||||
box = VisualNamedRange(option=option, name=name, range_widget=self.create_range(option, name))
|
||||
if option.default in option.special_range_names:
|
||||
# value can get mismatched in this case
|
||||
box.range.slider.value = min(max(option.special_range_names[default], option.range_start),
|
||||
box.range.slider.value = min(max(option.special_range_names[option.default], option.range_start),
|
||||
option.range_end)
|
||||
box.range.tag.text = str(int(box.range.slider.value))
|
||||
elif default in option.special_range_names.values():
|
||||
# better visual
|
||||
default = next(key for key, val in option.special_range_names.items() if val == option.default)
|
||||
set_button_text(box.choice, default.title())
|
||||
box.range.slider.bind(value=lambda _, _2: set_to_custom(box))
|
||||
box.range.slider.bind(on_touch_move=lambda _, _2: set_to_custom(box))
|
||||
items = [
|
||||
{
|
||||
"text": choice.title(),
|
||||
@@ -378,7 +371,7 @@ class OptionsCreator(ThemedApp):
|
||||
]
|
||||
box.range.slider.dropdown = MDDropdownMenu(caller=box.choice, items=items)
|
||||
box.choice.bind(on_release=open_dropdown)
|
||||
self.options[name] = default
|
||||
self.options[name] = option.default
|
||||
return box
|
||||
|
||||
def create_free_text(self, option: typing.Type[FreeText] | typing.Type[TextChoice], name: str):
|
||||
@@ -454,12 +447,8 @@ class OptionsCreator(ThemedApp):
|
||||
valid_keys = sorted(option.valid_keys)
|
||||
if option.verify_item_name:
|
||||
valid_keys += list(world.item_name_to_id.keys())
|
||||
if option.convert_name_groups:
|
||||
valid_keys += list(world.item_name_groups.keys())
|
||||
if option.verify_location_name:
|
||||
valid_keys += list(world.location_name_to_id.keys())
|
||||
if option.convert_name_groups:
|
||||
valid_keys += list(world.location_name_groups.keys())
|
||||
|
||||
if not issubclass(option, OptionCounter):
|
||||
def apply_changes(button):
|
||||
@@ -481,6 +470,14 @@ class OptionsCreator(ThemedApp):
|
||||
dialog.scrollbox.layout.spacing = dp(5)
|
||||
dialog.scrollbox.layout.padding = [0, dp(5), 0, 0]
|
||||
|
||||
if name not in self.options:
|
||||
# convert from non-mutable to mutable
|
||||
# We use list syntax even for sets, set behavior is enforced through GUI
|
||||
if issubclass(option, OptionCounter):
|
||||
self.options[name] = deepcopy(option.default)
|
||||
else:
|
||||
self.options[name] = sorted(option.default)
|
||||
|
||||
if issubclass(option, OptionCounter):
|
||||
for value in sorted(self.options[name]):
|
||||
dialog.add_set_item(value, self.options[name].get(value, None))
|
||||
@@ -494,15 +491,6 @@ class OptionsCreator(ThemedApp):
|
||||
def create_option_set_list_counter(self, option: typing.Type[OptionList] | typing.Type[OptionSet] |
|
||||
typing.Type[OptionCounter], name: str, world: typing.Type[World]):
|
||||
main_button = MDButton(MDButtonText(text="Edit"), on_release=lambda x: self.create_popup(option, name, world))
|
||||
|
||||
if name not in self.options:
|
||||
# convert from non-mutable to mutable
|
||||
# We use list syntax even for sets, set behavior is enforced through GUI
|
||||
if issubclass(option, OptionCounter):
|
||||
self.options[name] = deepcopy(option.default)
|
||||
else:
|
||||
self.options[name] = sorted(option.default)
|
||||
|
||||
return main_button
|
||||
|
||||
def create_option(self, option: typing.Type[Option], name: str, world: typing.Type[World]) -> Widget:
|
||||
|
||||
@@ -85,7 +85,6 @@ Currently, the following games are supported:
|
||||
* APQuest
|
||||
* Satisfactory
|
||||
* EarthBound
|
||||
* Mega Man 3
|
||||
|
||||
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
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import asyncio
|
||||
import typing
|
||||
import bsdiff4
|
||||
@@ -16,9 +15,6 @@ from CommonClient import CommonContext, server_loop, \
|
||||
gui_enabled, ClientCommandProcessor, logger, get_base_parser
|
||||
from Utils import async_start
|
||||
|
||||
# Heartbeat for position sharing via bounces, in seconds
|
||||
UNDERTALE_STATUS_INTERVAL = 30.0
|
||||
UNDERTALE_ONLINE_TIMEOUT = 60.0
|
||||
|
||||
class UndertaleCommandProcessor(ClientCommandProcessor):
|
||||
def __init__(self, ctx):
|
||||
@@ -113,11 +109,6 @@ class UndertaleContext(CommonContext):
|
||||
self.completed_routes = {"pacifist": 0, "genocide": 0, "neutral": 0}
|
||||
# self.save_game_folder: files go in this path to pass data between us and the actual game
|
||||
self.save_game_folder = os.path.expandvars(r"%localappdata%/UNDERTALE")
|
||||
self.last_sent_position: typing.Optional[tuple] = None
|
||||
self.last_room: typing.Optional[str] = None
|
||||
self.last_status_write: float = 0.0
|
||||
self.other_undertale_status: dict[int, dict] = {}
|
||||
|
||||
|
||||
def patch_game(self):
|
||||
with open(Utils.user_path("Undertale", "data.win"), "rb") as f:
|
||||
@@ -228,9 +219,6 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
|
||||
await ctx.send_msgs([{"cmd": "SetNotify", "keys": [str(ctx.slot)+" RoutesDone neutral",
|
||||
str(ctx.slot)+" RoutesDone pacifist",
|
||||
str(ctx.slot)+" RoutesDone genocide"]}])
|
||||
if any(info.game == "Undertale" and slot != ctx.slot
|
||||
for slot, info in ctx.slot_info.items()):
|
||||
ctx.set_notify("undertale_room_status")
|
||||
if args["slot_data"]["only_flakes"]:
|
||||
with open(os.path.join(ctx.save_game_folder, "GenoNoChest.flag"), "w") as f:
|
||||
f.close()
|
||||
@@ -275,12 +263,6 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
|
||||
if str(ctx.slot)+" RoutesDone pacifist" in args["keys"]:
|
||||
if args["keys"][str(ctx.slot) + " RoutesDone pacifist"] is not None:
|
||||
ctx.completed_routes["pacifist"] = args["keys"][str(ctx.slot)+" RoutesDone pacifist"]
|
||||
if "undertale_room_status" in args["keys"] and args["keys"]["undertale_room_status"]:
|
||||
status = args["keys"]["undertale_room_status"]
|
||||
ctx.other_undertale_status = {
|
||||
int(key): val for key, val in status.items()
|
||||
if int(key) != ctx.slot
|
||||
}
|
||||
elif cmd == "SetReply":
|
||||
if args["value"] is not None:
|
||||
if str(ctx.slot)+" RoutesDone pacifist" == args["key"]:
|
||||
@@ -289,19 +271,17 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
|
||||
ctx.completed_routes["genocide"] = args["value"]
|
||||
elif str(ctx.slot)+" RoutesDone neutral" == args["key"]:
|
||||
ctx.completed_routes["neutral"] = args["value"]
|
||||
if args.get("key") == "undertale_room_status" and args.get("value"):
|
||||
ctx.other_undertale_status = {
|
||||
int(key): val for key, val in args["value"].items()
|
||||
if int(key) != ctx.slot
|
||||
}
|
||||
elif cmd == "ReceivedItems":
|
||||
start_index = args["index"]
|
||||
|
||||
if start_index == 0:
|
||||
ctx.items_received = []
|
||||
elif start_index != len(ctx.items_received):
|
||||
await ctx.check_locations(ctx.locations_checked)
|
||||
await ctx.send_msgs([{"cmd": "Sync"}])
|
||||
sync_msg = [{"cmd": "Sync"}]
|
||||
if ctx.locations_checked:
|
||||
sync_msg.append({"cmd": "LocationChecks",
|
||||
"locations": list(ctx.locations_checked)})
|
||||
await ctx.send_msgs(sync_msg)
|
||||
if start_index == len(ctx.items_received):
|
||||
counter = -1
|
||||
placedWeapon = 0
|
||||
@@ -388,8 +368,9 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
|
||||
f.close()
|
||||
|
||||
elif cmd == "Bounced":
|
||||
data = args.get("data", {})
|
||||
if "x" in data and "room" in data:
|
||||
tags = args.get("tags", [])
|
||||
if "Online" in tags:
|
||||
data = args.get("data", {})
|
||||
if data["player"] != ctx.slot and data["player"] is not None:
|
||||
filename = f"FRISK" + str(data["player"]) + ".playerspot"
|
||||
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
|
||||
@@ -400,63 +381,21 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
|
||||
|
||||
async def multi_watcher(ctx: UndertaleContext):
|
||||
while not ctx.exit_event.is_set():
|
||||
if "Online" in ctx.tags and any(
|
||||
info.game == "Undertale" and slot != ctx.slot
|
||||
for slot, info in ctx.slot_info.items()):
|
||||
now = time.time()
|
||||
path = ctx.save_game_folder
|
||||
for root, dirs, files in os.walk(path):
|
||||
for file in files:
|
||||
if "spots.mine" in file:
|
||||
with open(os.path.join(root, file), "r") as mine:
|
||||
this_x = mine.readline()
|
||||
this_y = mine.readline()
|
||||
this_room = mine.readline()
|
||||
this_sprite = mine.readline()
|
||||
this_frame = mine.readline()
|
||||
|
||||
if this_room != ctx.last_room or \
|
||||
now - ctx.last_status_write >= UNDERTALE_STATUS_INTERVAL:
|
||||
ctx.last_room = this_room
|
||||
ctx.last_status_write = now
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "Set",
|
||||
"key": "undertale_room_status",
|
||||
"default": {},
|
||||
"want_reply": False,
|
||||
"operations": [{"operation": "update",
|
||||
"value": {str(ctx.slot): {"room": this_room,
|
||||
"time": now}}}]
|
||||
}])
|
||||
|
||||
# If player was visible but timed out (heartbeat) or left the room, remove them.
|
||||
for slot, entry in ctx.other_undertale_status.items():
|
||||
if entry.get("room") != this_room or \
|
||||
now - entry.get("time", now) > UNDERTALE_ONLINE_TIMEOUT:
|
||||
playerspot = os.path.join(ctx.save_game_folder,
|
||||
f"FRISK{slot}.playerspot")
|
||||
if os.path.exists(playerspot):
|
||||
os.remove(playerspot)
|
||||
|
||||
current_position = (this_x, this_y, this_room, this_sprite, this_frame)
|
||||
if current_position == ctx.last_sent_position:
|
||||
continue
|
||||
|
||||
# Empty status dict = no data yet → send to bootstrap.
|
||||
online_in_room = any(
|
||||
entry.get("room") == this_room and
|
||||
now - entry.get("time", now) <= UNDERTALE_ONLINE_TIMEOUT
|
||||
for entry in ctx.other_undertale_status.values()
|
||||
)
|
||||
if ctx.other_undertale_status and not online_in_room:
|
||||
continue
|
||||
|
||||
message = [{"cmd": "Bounce", "games": ["Undertale"],
|
||||
"data": {"player": ctx.slot, "x": this_x, "y": this_y,
|
||||
"room": this_room, "spr": this_sprite,
|
||||
"frm": this_frame}}]
|
||||
await ctx.send_msgs(message)
|
||||
ctx.last_sent_position = current_position
|
||||
path = ctx.save_game_folder
|
||||
for root, dirs, files in os.walk(path):
|
||||
for file in files:
|
||||
if "spots.mine" in file and "Online" in ctx.tags:
|
||||
with open(os.path.join(root, file), "r") as mine:
|
||||
this_x = mine.readline()
|
||||
this_y = mine.readline()
|
||||
this_room = mine.readline()
|
||||
this_sprite = mine.readline()
|
||||
this_frame = mine.readline()
|
||||
mine.close()
|
||||
message = [{"cmd": "Bounce", "tags": ["Online"],
|
||||
"data": {"player": ctx.slot, "x": this_x, "y": this_y, "room": this_room,
|
||||
"spr": this_sprite, "frm": this_frame}}]
|
||||
await ctx.send_msgs(message)
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
@@ -470,9 +409,10 @@ async def game_watcher(ctx: UndertaleContext):
|
||||
for file in files:
|
||||
if ".item" in file:
|
||||
os.remove(os.path.join(root, file))
|
||||
await ctx.check_locations(ctx.locations_checked)
|
||||
await ctx.send_msgs([{"cmd": "Sync"}])
|
||||
|
||||
sync_msg = [{"cmd": "Sync"}]
|
||||
if ctx.locations_checked:
|
||||
sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)})
|
||||
await ctx.send_msgs(sync_msg)
|
||||
ctx.syncing = False
|
||||
if ctx.got_deathlink:
|
||||
ctx.got_deathlink = False
|
||||
@@ -507,7 +447,7 @@ async def game_watcher(ctx: UndertaleContext):
|
||||
for l in lines:
|
||||
sending = sending+[(int(l.rstrip('\n')))+12000]
|
||||
finally:
|
||||
await ctx.check_locations(sending)
|
||||
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": sending}])
|
||||
if "victory" in file and str(ctx.route) in file:
|
||||
victory = True
|
||||
if ".playerspot" in file and "Online" not in ctx.tags:
|
||||
|
||||
15
Utils.py
15
Utils.py
@@ -18,14 +18,11 @@ import logging
|
||||
import warnings
|
||||
|
||||
from argparse import Namespace
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from settings import Settings, get_settings
|
||||
from time import sleep
|
||||
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union, TypeGuard
|
||||
from yaml import load, load_all, dump
|
||||
from pathspec import PathSpec, GitIgnoreSpec
|
||||
from typing_extensions import deprecated
|
||||
|
||||
try:
|
||||
from yaml import CLoader as UnsafeLoader, CSafeLoader as SafeLoader, CDumper as Dumper
|
||||
@@ -318,7 +315,6 @@ def get_public_ipv6() -> str:
|
||||
return ip
|
||||
|
||||
|
||||
@deprecated("Utils.get_options() is deprecated. Use the settings API instead.")
|
||||
def get_options() -> Settings:
|
||||
deprecate("Utils.get_options() is deprecated. Use the settings API instead.")
|
||||
return get_settings()
|
||||
@@ -1007,7 +1003,6 @@ def async_start(co: Coroutine[None, None, typing.Any], name: Optional[str] = Non
|
||||
|
||||
|
||||
def deprecate(message: str, add_stacklevels: int = 0):
|
||||
"""also use typing_extensions.deprecated wherever you use this"""
|
||||
if __debug__:
|
||||
raise Exception(message)
|
||||
warnings.warn(message, stacklevel=2 + add_stacklevels)
|
||||
@@ -1072,7 +1067,6 @@ def _extend_freeze_support() -> None:
|
||||
multiprocessing.freeze_support = multiprocessing.spawn.freeze_support = _freeze_support if is_frozen() else _noop
|
||||
|
||||
|
||||
@deprecated("Use multiprocessing.freeze_support() instead")
|
||||
def freeze_support() -> None:
|
||||
"""This now only calls multiprocessing.freeze_support since we are patching freeze_support on module load."""
|
||||
import multiprocessing
|
||||
@@ -1293,15 +1287,6 @@ def is_iterable_except_str(obj: object) -> TypeGuard[typing.Iterable[typing.Any]
|
||||
return isinstance(obj, typing.Iterable)
|
||||
|
||||
|
||||
def utcnow() -> datetime:
|
||||
"""
|
||||
Implementation of Python's datetime.utcnow() function for use after deprecation.
|
||||
Needed for timezone-naive UTC datetimes stored in databases with PonyORM (upstream).
|
||||
https://ponyorm.org/ponyorm-list/2014-August/000113.html
|
||||
"""
|
||||
return datetime.now(timezone.utc).replace(tzinfo=None)
|
||||
|
||||
|
||||
class DaemonThreadPoolExecutor(concurrent.futures.ThreadPoolExecutor):
|
||||
"""
|
||||
ThreadPoolExecutor that uses daemonic threads that do not keep the program alive.
|
||||
|
||||
@@ -11,7 +11,6 @@ from pony.flask import Pony
|
||||
from werkzeug.routing import BaseConverter
|
||||
|
||||
from Utils import title_sorted, get_file_safe_name
|
||||
from .cli import CLI
|
||||
|
||||
UPLOAD_FOLDER = os.path.relpath('uploads')
|
||||
LOGS_FOLDER = os.path.relpath('logs')
|
||||
@@ -46,8 +45,6 @@ app.config["SELFGEN"] = True # application process is in charge of scheduling G
|
||||
app.config["JOB_THRESHOLD"] = 1
|
||||
# after what time in seconds should generation be aborted, freeing the queue slot. Can be set to None to disable.
|
||||
app.config["JOB_TIME"] = 600
|
||||
# maximum time in seconds since last activity for a room to be hosted
|
||||
app.config["MAX_ROOM_TIMEOUT"] = 259200
|
||||
# memory limit for generator processes in bytes
|
||||
app.config["GENERATOR_MEMORY_LIMIT"] = 4294967296
|
||||
|
||||
@@ -67,7 +64,6 @@ app.config["ASSET_RIGHTS"] = False
|
||||
|
||||
cache = Cache()
|
||||
Compress(app)
|
||||
CLI(app)
|
||||
|
||||
|
||||
def to_python(value: str) -> uuid.UUID:
|
||||
|
||||
@@ -2,20 +2,10 @@
|
||||
from typing import List, Tuple
|
||||
|
||||
from flask import Blueprint
|
||||
from flask_cors import CORS
|
||||
|
||||
from ..models import Seed, Slot
|
||||
|
||||
api_endpoints = Blueprint('api', __name__, url_prefix="/api")
|
||||
cors = CORS(api_endpoints, resources={
|
||||
r"/api/datapackage/*": {"origins": "*"},
|
||||
r"/api/datapackage": {"origins": "*"},
|
||||
r"/api/datapackage_checksum/*": {"origins": "*"},
|
||||
r"/api/room_status/*": {"origins": "*"},
|
||||
r"/api/tracker/*": {"origins": "*"},
|
||||
r"/api/static_tracker/*": {"origins": "*"},
|
||||
r"/api/slot_data_tracker/*": {"origins": "*"}
|
||||
})
|
||||
|
||||
|
||||
def get_players(seed: Seed) -> List[Tuple[str, str]]:
|
||||
|
||||
@@ -4,14 +4,14 @@ import json
|
||||
import logging
|
||||
import multiprocessing
|
||||
import typing
|
||||
from datetime import timedelta
|
||||
from datetime import timedelta, datetime
|
||||
from threading import Event, Thread
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from pony.orm import db_session, select, commit, PrimaryKey, desc
|
||||
from pony.orm import db_session, select, commit, PrimaryKey
|
||||
|
||||
from Utils import restricted_loads, utcnow
|
||||
from Utils import restricted_loads
|
||||
from .locker import Locker, AlreadyRunningException
|
||||
|
||||
_stop_event = Event()
|
||||
@@ -129,11 +129,10 @@ def autohost(config: dict):
|
||||
with db_session:
|
||||
rooms = select(
|
||||
room for room in Room if
|
||||
room.last_activity >= utcnow() - timedelta(
|
||||
seconds=config["MAX_ROOM_TIMEOUT"])).order_by(desc(Room.last_port))
|
||||
room.last_activity >= datetime.utcnow() - timedelta(days=3))
|
||||
for room in rooms:
|
||||
# we have to filter twice, as the per-room timeout can't currently be PonyORM transpiled.
|
||||
if room.last_activity >= utcnow() - timedelta(seconds=room.timeout + 5):
|
||||
if room.last_activity >= datetime.utcnow() - timedelta(seconds=room.timeout + 5):
|
||||
hosters[room.id.int % len(hosters)].start_room(room.id)
|
||||
|
||||
except AlreadyRunningException:
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
from flask import Flask
|
||||
|
||||
|
||||
class CLI:
|
||||
def __init__(self, app: Flask) -> None:
|
||||
from .stats import stats_cli
|
||||
|
||||
app.cli.add_command(stats_cli)
|
||||
@@ -1,36 +0,0 @@
|
||||
import click
|
||||
from flask.cli import AppGroup
|
||||
from pony.orm import raw_sql
|
||||
|
||||
from Utils import format_SI_prefix
|
||||
|
||||
stats_cli = AppGroup("stats")
|
||||
|
||||
|
||||
@stats_cli.command("show")
|
||||
def show() -> None:
|
||||
from pony.orm import db_session, select
|
||||
|
||||
from WebHostLib.models import GameDataPackage
|
||||
|
||||
total_games_package_count: int = 0
|
||||
total_games_package_size: int
|
||||
top_10_package_sizes: list[tuple[int, str]] = []
|
||||
|
||||
with db_session:
|
||||
data_length = raw_sql("LENGTH(data)")
|
||||
data_length_desc = raw_sql("LENGTH(data) DESC")
|
||||
data_length_sum = raw_sql("SUM(LENGTH(data))")
|
||||
total_games_package_count = GameDataPackage.select().count()
|
||||
total_games_package_size = select(data_length_sum for _ in GameDataPackage).first() # type: ignore
|
||||
top_10_package_sizes = list(
|
||||
select((data_length, dp.checksum) for dp in GameDataPackage) # type: ignore
|
||||
.order_by(lambda _, _2: data_length_desc)
|
||||
.limit(10)
|
||||
)
|
||||
|
||||
click.echo(f"Total number of games packages: {total_games_package_count}")
|
||||
click.echo(f"Total size of games packages: {format_SI_prefix(total_games_package_size, power=1024)}B")
|
||||
click.echo(f"Top {len(top_10_package_sizes)} biggest games packages:")
|
||||
for size, checksum in top_10_package_sizes:
|
||||
click.echo(f" {checksum}: {size:>8d}")
|
||||
@@ -172,7 +172,7 @@ class WebHostContext(Context):
|
||||
room.multisave = pickle.dumps(self.get_save())
|
||||
# saving only occurs on activity, so we can "abuse" this information to mark this as last_activity
|
||||
if not exit_save: # we don't want to count a shutdown as activity, which would restart the server again
|
||||
room.last_activity = Utils.utcnow()
|
||||
room.last_activity = datetime.datetime.utcnow()
|
||||
return True
|
||||
|
||||
def get_save(self) -> dict:
|
||||
@@ -367,7 +367,8 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
||||
with db_session:
|
||||
# ensure the Room does not spin up again on its own, minute of safety buffer
|
||||
room = Room.get(id=room_id)
|
||||
room.last_activity = Utils.utcnow() - datetime.timedelta(minutes=1, seconds=room.timeout)
|
||||
room.last_activity = datetime.datetime.utcnow() - \
|
||||
datetime.timedelta(minutes=1, seconds=room.timeout)
|
||||
del room
|
||||
tear_down_logging(room_id)
|
||||
logging.info(f"Shutting down room {room_id} on {name}.")
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from datetime import timedelta
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
from flask import render_template
|
||||
from pony.orm import count
|
||||
|
||||
from Utils import utcnow
|
||||
from WebHostLib import app, cache
|
||||
from .models import Room, Seed
|
||||
|
||||
@@ -11,6 +10,6 @@ from .models import Room, Seed
|
||||
@app.route('/', methods=['GET', 'POST'])
|
||||
@cache.cached(timeout=300) # cache has to appear under app route for caching to work
|
||||
def landing():
|
||||
rooms = count(room for room in Room if room.creation_time >= utcnow() - timedelta(days=7))
|
||||
seeds = count(seed for seed in Seed if seed.creation_time >= utcnow() - timedelta(days=7))
|
||||
rooms = count(room for room in Room if room.creation_time >= datetime.utcnow() - timedelta(days=7))
|
||||
seeds = count(seed for seed in Seed if seed.creation_time >= datetime.utcnow() - timedelta(days=7))
|
||||
return render_template("landing.html", rooms=rooms, seeds=seeds)
|
||||
|
||||
@@ -9,12 +9,11 @@ from flask import request, redirect, url_for, render_template, Response, session
|
||||
from pony.orm import count, commit, db_session
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
|
||||
from worlds.AutoWorld import AutoWorldRegister, World
|
||||
from . import app, cache
|
||||
from .markdown import render_markdown
|
||||
from .models import Seed, Room, Command, UUID, uuid4
|
||||
from Utils import title_sorted, utcnow
|
||||
from Utils import title_sorted
|
||||
|
||||
class WebWorldTheme(StrEnum):
|
||||
DIRT = "dirt"
|
||||
@@ -234,12 +233,11 @@ def host_room(room: UUID):
|
||||
if room is None:
|
||||
return abort(404)
|
||||
|
||||
now = utcnow()
|
||||
now = datetime.datetime.utcnow()
|
||||
# indicate that the page should reload to get the assigned port
|
||||
should_refresh = (
|
||||
(not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3))
|
||||
or room.last_activity < now - datetime.timedelta(seconds=room.timeout)
|
||||
)
|
||||
should_refresh = ((not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3))
|
||||
or room.last_activity < now - datetime.timedelta(seconds=room.timeout))
|
||||
|
||||
if now - room.last_activity > datetime.timedelta(minutes=1):
|
||||
# we only set last_activity if needed, otherwise parallel access on /room will cause an internal server error
|
||||
# due to "pony.orm.core.OptimisticCheckError: Object Room was updated outside of current transaction"
|
||||
|
||||
@@ -2,8 +2,6 @@ from datetime import datetime
|
||||
from uuid import UUID, uuid4
|
||||
from pony.orm import Database, PrimaryKey, Required, Set, Optional, buffer, LongStr
|
||||
|
||||
from Utils import utcnow
|
||||
|
||||
db = Database()
|
||||
|
||||
STATE_QUEUED = 0
|
||||
@@ -22,8 +20,8 @@ class Slot(db.Entity):
|
||||
|
||||
class Room(db.Entity):
|
||||
id = PrimaryKey(UUID, default=uuid4)
|
||||
last_activity: datetime = Required(datetime, default=lambda: utcnow(), index=True)
|
||||
creation_time: datetime = Required(datetime, default=lambda: utcnow(), index=True) # index used by landing page
|
||||
last_activity = Required(datetime, default=lambda: datetime.utcnow(), index=True)
|
||||
creation_time = Required(datetime, default=lambda: datetime.utcnow(), index=True) # index used by landing page
|
||||
owner = Required(UUID, index=True)
|
||||
commands = Set('Command')
|
||||
seed = Required('Seed', index=True)
|
||||
@@ -40,7 +38,7 @@ class Seed(db.Entity):
|
||||
rooms = Set(Room)
|
||||
multidata = Required(bytes, lazy=True)
|
||||
owner = Required(UUID, index=True)
|
||||
creation_time: datetime = Required(datetime, default=lambda: utcnow(), index=True) # index used by landing page
|
||||
creation_time = Required(datetime, default=lambda: datetime.utcnow(), index=True) # index used by landing page
|
||||
slots = Set(Slot)
|
||||
spoiler = Optional(LongStr, lazy=True)
|
||||
meta = Required(LongStr, default=lambda: "{\"race\": false}") # additional meta information/tags
|
||||
|
||||
@@ -6,7 +6,6 @@ waitress>=3.0.2
|
||||
Flask-Caching>=2.3.0
|
||||
Flask-Compress==1.18 # pkg_resources can't resolve the "backports.zstd" dependency of >1.18, breaking ModuleUpdate.py
|
||||
Flask-Limiter>=3.12
|
||||
Flask-Cors>=6.0.2
|
||||
bokeh>=3.6.3
|
||||
markupsafe>=3.0.2
|
||||
setproctitle>=1.3.5
|
||||
|
||||
@@ -10,7 +10,7 @@ from werkzeug.exceptions import abort
|
||||
|
||||
from MultiServer import Context, get_saving_second
|
||||
from NetUtils import ClientStatus, Hint, NetworkItem, NetworkSlot, SlotType
|
||||
from Utils import restricted_loads, KeyedDefaultDict, utcnow
|
||||
from Utils import restricted_loads, KeyedDefaultDict
|
||||
from . import app, cache
|
||||
from .models import GameDataPackage, Room
|
||||
|
||||
@@ -273,10 +273,9 @@ class TrackerData:
|
||||
Does not include players who have no activity recorded.
|
||||
"""
|
||||
last_activity: Dict[TeamPlayer, datetime.timedelta] = {}
|
||||
now = utcnow()
|
||||
now = datetime.datetime.utcnow()
|
||||
for (team, player), timestamp in self._multisave.get("client_activity_timers", []):
|
||||
from_timestamp = datetime.datetime.fromtimestamp(timestamp, datetime.timezone.utc).replace(tzinfo=None)
|
||||
last_activity[team, player] = now - from_timestamp
|
||||
last_activity[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp)
|
||||
|
||||
return last_activity
|
||||
|
||||
|
||||
@@ -41,8 +41,16 @@ http {
|
||||
# server_name example.com www.example.com;
|
||||
|
||||
keepalive_timeout 5;
|
||||
|
||||
|
||||
# path for static files
|
||||
root /app/WebHostLib;
|
||||
|
||||
location / {
|
||||
# checks for static file, if not found proxy to app
|
||||
try_files $uri @proxy_to_app;
|
||||
}
|
||||
|
||||
location @proxy_to_app {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $http_host;
|
||||
@@ -52,15 +60,5 @@ http {
|
||||
|
||||
proxy_pass http://app_server;
|
||||
}
|
||||
|
||||
location /static/ {
|
||||
root /app/WebHostLib/;
|
||||
autoindex off;
|
||||
}
|
||||
|
||||
location = /favicon.ico {
|
||||
alias /app/WebHostLib/static/static/favicon.ico;
|
||||
access_log off;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,9 +134,6 @@
|
||||
# Mega Man 2
|
||||
/worlds/mm2/ @Silvris
|
||||
|
||||
# Mega Man 3
|
||||
/worlds/mm3/ @Silvris
|
||||
|
||||
# MegaMan Battle Network 3
|
||||
/worlds/mmbn3/ @digiholic
|
||||
|
||||
|
||||
@@ -87,8 +87,7 @@ The world is your game integration for the Archipelago generator, webhost, and m
|
||||
information necessary for creating the items and locations to be randomized, the logic for item placement, the
|
||||
datapackage information so other game clients can recognize your game data, and documentation. Your world must be
|
||||
written as a Python package to be loaded by Archipelago. This is currently done by creating a fork of the Archipelago
|
||||
repository and creating a new world package in `/worlds/` (see [running from source](/docs/running%20from%20source.md)
|
||||
for setup).
|
||||
repository and creating a new world package in `/worlds/`.
|
||||
|
||||
The base World class can be found in [AutoWorld](/worlds/AutoWorld.py). Methods available for your world to call
|
||||
during generation can be found in [BaseClasses](/BaseClasses.py) and [Fill](/Fill.py). Some examples and documentation
|
||||
|
||||
@@ -46,8 +46,8 @@ which is the correct way to package your `.apworld` as a world developer. Do not
|
||||
|
||||
### "Build APWorlds" Launcher Component
|
||||
|
||||
In the Archipelago Launcher (on [source only](/docs/running%20from%20source.md)), there is a "Build APWorlds"
|
||||
component that will package all world folders to `.apworld`, and add `archipelago.json` manifest files to them.
|
||||
In the Archipelago Launcher, there is a "Build APWorlds" component that will package all world folders to `.apworld`,
|
||||
and add `archipelago.json` manifest files to them.
|
||||
These .apworld files will be output to `build/apworlds` (relative to the Archipelago root directory).
|
||||
The `archipelago.json` file in each .apworld will automatically include the appropriate
|
||||
`version` and `compatible_version`.
|
||||
|
||||
@@ -491,10 +491,9 @@ class MyGameWorld(World):
|
||||
base_id = 1234
|
||||
# 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 can be generated with arbitrary code during world load, but keep in mind that
|
||||
# anything expensive (e.g. parsing non-python data files) will delay world loading.
|
||||
# They can include events, but don't have to since events will be placed manually.
|
||||
# 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 = {name: id for
|
||||
id, name in enumerate(mygame_items, base_id)}
|
||||
location_name_to_id = {name: id for
|
||||
@@ -771,7 +770,6 @@ class MyGameState(LogicMixin):
|
||||
new_state.mygame_defeatable_enemies = {
|
||||
player: enemies.copy() for player, enemies in self.mygame_defeatable_enemies.items()
|
||||
}
|
||||
return new_state
|
||||
```
|
||||
|
||||
After doing this, you can now access `state.mygame_defeatable_enemies[player]` from your access rules.
|
||||
|
||||
@@ -186,20 +186,9 @@ class ERPlacementState:
|
||||
self.pairings = []
|
||||
self.world = world
|
||||
self.coupled = coupled
|
||||
self.collection_state = world.multiworld.get_all_state(False, True)
|
||||
self.entrance_lookup = entrance_lookup
|
||||
|
||||
# Construct an 'all state', similar to MultiWorld.get_all_state(), but only for the world which is having its
|
||||
# entrances randomized.
|
||||
single_player_all_state = CollectionState(world.multiworld, True)
|
||||
player = world.player
|
||||
for item in world.multiworld.itempool:
|
||||
if item.player == player:
|
||||
world.collect(single_player_all_state, item)
|
||||
for item in world.get_pre_fill_items():
|
||||
world.collect(single_player_all_state, item)
|
||||
single_player_all_state.sweep_for_advancements(world.get_locations())
|
||||
self.collection_state = single_player_all_state
|
||||
|
||||
@property
|
||||
def placed_regions(self) -> set[Region]:
|
||||
return self.collection_state.reachable_regions[self.world.player]
|
||||
@@ -237,7 +226,7 @@ class ERPlacementState:
|
||||
copied_state.blocked_connections[self.world.player].remove(source_exit)
|
||||
copied_state.blocked_connections[self.world.player].update(target_entrance.connected_region.exits)
|
||||
copied_state.update_reachable_regions(self.world.player)
|
||||
copied_state.sweep_for_advancements(self.world.get_locations())
|
||||
copied_state.sweep_for_advancements()
|
||||
# test that at there are newly reachable randomized exits that are ACTUALLY reachable
|
||||
available_randomized_exits = copied_state.blocked_connections[self.world.player]
|
||||
for _exit in available_randomized_exits:
|
||||
@@ -413,7 +402,7 @@ def randomize_entrances(
|
||||
placed_exits, paired_entrances = er_state.connect(source_exit, target_entrance)
|
||||
# propagate new connections
|
||||
er_state.collection_state.update_reachable_regions(world.player)
|
||||
er_state.collection_state.sweep_for_advancements(world.get_locations())
|
||||
er_state.collection_state.sweep_for_advancements()
|
||||
if on_connect:
|
||||
change = on_connect(er_state, placed_exits, paired_entrances)
|
||||
if change:
|
||||
|
||||
@@ -213,11 +213,6 @@ Root: HKCR; Subkey: "{#MyAppName}ebpatch"; ValueData: "Archi
|
||||
Root: HKCR; Subkey: "{#MyAppName}ebpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ebpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apmm3"; ValueData: "{#MyAppName}mm3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}mm3patch"; ValueData: "Archipelago Mega Man 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}mm3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}mm3patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: "";
|
||||
|
||||
@@ -248,7 +248,6 @@ class WorldTestBase(unittest.TestCase):
|
||||
with self.subTest("Game", game=self.game, seed=self.multiworld.seed):
|
||||
distribute_items_restrictive(self.multiworld)
|
||||
call_all(self.multiworld, "post_fill")
|
||||
call_all(self.multiworld, "finalize_multiworld")
|
||||
self.assertTrue(fulfills_accessibility(), "Collected all locations, but can't beat the game.")
|
||||
placed_items = [loc.item for loc in self.multiworld.get_locations() if loc.item and loc.item.code]
|
||||
self.assertLessEqual(len(self.multiworld.itempool), len(placed_items),
|
||||
|
||||
@@ -88,7 +88,6 @@ class TestIDs(unittest.TestCase):
|
||||
multiworld = setup_solo_multiworld(world_type)
|
||||
distribute_items_restrictive(multiworld)
|
||||
call_all(multiworld, "post_fill")
|
||||
call_all(multiworld, "finalize_multiworld")
|
||||
datapackage = world_type.get_data_package_data()
|
||||
for item_group, item_names in datapackage["item_name_groups"].items():
|
||||
self.assertIsInstance(item_group, str,
|
||||
|
||||
@@ -46,8 +46,6 @@ class TestImplemented(unittest.TestCase):
|
||||
with self.subTest(game=game_name, seed=multiworld.seed):
|
||||
distribute_items_restrictive(multiworld)
|
||||
call_all(multiworld, "post_fill")
|
||||
call_all(multiworld, "finalize_multiworld")
|
||||
call_all(multiworld, "pre_output")
|
||||
for key, data in multiworld.worlds[1].fill_slot_data().items():
|
||||
self.assertIsInstance(key, str, "keys in slot data must be a string")
|
||||
convert_to_base_types(data) # only put base data types into slot data
|
||||
@@ -95,7 +93,6 @@ class TestImplemented(unittest.TestCase):
|
||||
with self.subTest(game=game_name, seed=multiworld.seed):
|
||||
distribute_items_restrictive(multiworld)
|
||||
call_all(multiworld, "post_fill")
|
||||
call_all(multiworld, "finalize_multiworld")
|
||||
|
||||
# Note: `multiworld.get_spheres()` iterates a set of locations, so the order that locations are checked
|
||||
# is nondeterministic and may vary between runs with the same seed.
|
||||
|
||||
@@ -123,7 +123,6 @@ class TestBase(unittest.TestCase):
|
||||
call_all(multiworld, "pre_fill")
|
||||
distribute_items_restrictive(multiworld)
|
||||
call_all(multiworld, "post_fill")
|
||||
call_all(multiworld, "finalize_multiworld")
|
||||
self.assertTrue(multiworld.can_beat_game(CollectionState(multiworld)), f"seed = {multiworld.seed}")
|
||||
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import unittest
|
||||
|
||||
from BaseClasses import PlandoOptions
|
||||
from Options import Choice, TextChoice, ItemLinks, OptionSet, PlandoConnections, PlandoItems, PlandoTexts
|
||||
from Options import Choice, ItemLinks, OptionSet, PlandoConnections, PlandoItems, PlandoTexts
|
||||
from Utils import restricted_dumps
|
||||
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
@@ -16,29 +16,6 @@ class TestOptions(unittest.TestCase):
|
||||
with self.subTest(game=gamename, option=option_key):
|
||||
self.assertTrue(option.__doc__)
|
||||
|
||||
def test_option_defaults(self):
|
||||
"""Test that defaults for submitted options are valid."""
|
||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||
if not world_type.hidden:
|
||||
for option_key, option in world_type.options_dataclass.type_hints.items():
|
||||
with self.subTest(game=gamename, option=option_key):
|
||||
if issubclass(option, TextChoice):
|
||||
self.assertTrue(option.default in option.name_lookup,
|
||||
f"Default value {option.default} for TextChoice option {option.__name__} in"
|
||||
f" {gamename} does not resolve to a listed value!"
|
||||
)
|
||||
# Standard "can default generate" test
|
||||
err_raised = None
|
||||
try:
|
||||
option.from_any(option.default)
|
||||
except Exception as ex:
|
||||
err_raised = ex
|
||||
self.assertIsNone(err_raised,
|
||||
f"Default value {option.default} for option {option.__name__} in {gamename}"
|
||||
f" is not valid! Exception: {err_raised}"
|
||||
)
|
||||
|
||||
|
||||
def test_options_are_not_set_by_world(self):
|
||||
"""Test that options attribute is not already set"""
|
||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||
|
||||
@@ -6,7 +6,6 @@ import zipfile
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Iterable, Optional, cast
|
||||
|
||||
from Utils import utcnow
|
||||
from WebHostLib import to_python
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -134,7 +133,7 @@ def stop_room(app_client: "FlaskClient",
|
||||
room_id: str,
|
||||
timeout: Optional[float] = None,
|
||||
simulate_idle: bool = True) -> None:
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from time import sleep
|
||||
|
||||
from pony.orm import db_session
|
||||
@@ -152,11 +151,10 @@ def stop_room(app_client: "FlaskClient",
|
||||
|
||||
with db_session:
|
||||
room: Room = Room.get(id=room_uuid)
|
||||
now = utcnow()
|
||||
if simulate_idle:
|
||||
new_last_activity = now - timedelta(seconds=room.timeout + 5)
|
||||
new_last_activity = datetime.utcnow() - timedelta(seconds=room.timeout + 5)
|
||||
else:
|
||||
new_last_activity = now - timedelta(days=3)
|
||||
new_last_activity = datetime.utcnow() - timedelta(days=3)
|
||||
room.last_activity = new_last_activity
|
||||
address = f"localhost:{room.last_port}" if room.last_port > 0 else None
|
||||
if address:
|
||||
@@ -190,7 +188,6 @@ def stop_room(app_client: "FlaskClient",
|
||||
if address:
|
||||
room.timeout = original_timeout
|
||||
room.last_activity = new_last_activity
|
||||
room.commands.clear() # make sure there is no leftover /exit
|
||||
print("timeout restored")
|
||||
|
||||
|
||||
|
||||
@@ -61,7 +61,6 @@ class TestAllGamesMultiworld(MultiworldTestBase):
|
||||
with self.subTest("filling multiworld", seed=self.multiworld.seed):
|
||||
distribute_items_restrictive(self.multiworld)
|
||||
call_all(self.multiworld, "post_fill")
|
||||
call_all(self.multiworld, "finalize_multiworld")
|
||||
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
|
||||
|
||||
|
||||
@@ -79,5 +78,4 @@ class TestTwoPlayerMulti(MultiworldTestBase):
|
||||
with self.subTest("filling multiworld", games=world_type.game, seed=self.multiworld.seed):
|
||||
distribute_items_restrictive(self.multiworld)
|
||||
call_all(self.multiworld, "post_fill")
|
||||
call_all(self.multiworld, "finalize_multiworld")
|
||||
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
|
||||
|
||||
@@ -363,7 +363,7 @@ class World(metaclass=AutoWorldRegister):
|
||||
|
||||
def __getattr__(self, item: str) -> Any:
|
||||
if item == "settings":
|
||||
return getattr(self.__class__, item)
|
||||
return self.__class__.settings
|
||||
raise AttributeError
|
||||
|
||||
# overridable methods that get called by Main.py, sorted by execution order
|
||||
@@ -430,23 +430,6 @@ class World(metaclass=AutoWorldRegister):
|
||||
This happens before progression balancing, so the items may not be in their final locations yet.
|
||||
"""
|
||||
|
||||
def finalize_multiworld(self) -> None:
|
||||
"""
|
||||
Optional Method that is called after fill and progression balancing.
|
||||
This is the last stage of generation where worlds may change logically relevant data,
|
||||
such as item placements and connections. To not break assumptions,
|
||||
only ever increase accessibility, never decrease it.
|
||||
"""
|
||||
pass
|
||||
|
||||
def pre_output(self):
|
||||
"""
|
||||
Optional method that is called before output generation.
|
||||
Items and connections are not meant to be moved anymore,
|
||||
anything that would affect logical spheres is forbidden at this point.
|
||||
"""
|
||||
pass
|
||||
|
||||
def generate_output(self, output_directory: str) -> None:
|
||||
"""
|
||||
This method gets called from a threadpool, do not use multiworld.random here.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,15 @@
|
||||
import collections
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from .Regions import create_lw_region, create_dw_region, create_cave_region, create_dungeon_region
|
||||
from .SubClasses import LTTPRegionType
|
||||
|
||||
|
||||
def create_inverted_regions(multiworld: MultiWorld, player: int):
|
||||
def create_inverted_regions(world, player):
|
||||
|
||||
multiworld.regions += [
|
||||
create_dw_region(multiworld, player, 'Menu', None,
|
||||
world.regions += [
|
||||
create_dw_region(world, player, 'Menu', None,
|
||||
['Links House S&Q', 'Dark Sanctuary S&Q', 'Old Man S&Q', 'Castle Ledge S&Q']),
|
||||
create_lw_region(multiworld, player, 'Light World',
|
||||
create_lw_region(world, player, 'Light World',
|
||||
['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure', 'Purple Chest',
|
||||
'Bombos Tablet'],
|
||||
["Blinds Hideout", "Hyrule Castle Secret Entrance Drop", 'Kings Grave Outer Rocks', 'Dam',
|
||||
@@ -36,184 +35,184 @@ def create_inverted_regions(multiworld: MultiWorld, player: int):
|
||||
'Hyrule Castle Entrance (South)', 'Secret Passage Outer Bushes',
|
||||
'Bush Covered Lawn Outer Bushes',
|
||||
'Potion Shop Outer Bushes', 'Graveyard Cave Outer Bushes', 'Bomb Hut Outer Bushes']),
|
||||
create_lw_region(multiworld, player, 'Bush Covered Lawn', None,
|
||||
create_lw_region(world, player, 'Bush Covered Lawn', None,
|
||||
['Bush Covered House', 'Bush Covered Lawn Inner Bushes', 'Bush Covered Lawn Mirror Spot']),
|
||||
create_lw_region(multiworld, player, 'Bomb Hut Area', None,
|
||||
create_lw_region(world, player, 'Bomb Hut Area', None,
|
||||
['Light World Bomb Hut', 'Bomb Hut Inner Bushes', 'Bomb Hut Mirror Spot']),
|
||||
create_lw_region(multiworld, player, 'Hyrule Castle Secret Entrance Area', None,
|
||||
create_lw_region(world, player, 'Hyrule Castle Secret Entrance Area', None,
|
||||
['Hyrule Castle Secret Entrance Stairs', 'Secret Passage Inner Bushes']),
|
||||
create_lw_region(multiworld, player, 'Death Mountain Entrance', None,
|
||||
create_lw_region(world, player, 'Death Mountain Entrance', None,
|
||||
['Old Man Cave (West)', 'Death Mountain Entrance Drop', 'Bumper Cave Entrance Mirror Spot']),
|
||||
create_lw_region(multiworld, player, 'Lake Hylia Central Island', None,
|
||||
create_lw_region(world, player, 'Lake Hylia Central Island', None,
|
||||
['Capacity Upgrade', 'Lake Hylia Central Island Mirror Spot']),
|
||||
create_cave_region(multiworld, player, 'Blinds Hideout', 'a bounty of five items', ["Blind\'s Hideout - Top",
|
||||
create_cave_region(world, player, 'Blinds Hideout', 'a bounty of five items', ["Blind\'s Hideout - Top",
|
||||
"Blind\'s Hideout - Left",
|
||||
"Blind\'s Hideout - Right",
|
||||
"Blind\'s Hideout - Far Left",
|
||||
"Blind\'s Hideout - Far Right"]),
|
||||
create_lw_region(multiworld, player, 'Northeast Light World', None,
|
||||
create_lw_region(world, player, 'Northeast Light World', None,
|
||||
['Zoras River', 'Waterfall of Wishing Cave', 'Potion Shop Outer Rock', 'Catfish Mirror Spot',
|
||||
'Northeast Light World Warp']),
|
||||
create_lw_region(multiworld, player, 'Waterfall of Wishing Cave', None,
|
||||
create_lw_region(world, player, 'Waterfall of Wishing Cave', None,
|
||||
['Waterfall of Wishing', 'Northeast Light World Return']),
|
||||
create_lw_region(multiworld, player, 'Potion Shop Area', None,
|
||||
create_lw_region(world, player, 'Potion Shop Area', None,
|
||||
['Potion Shop', 'Potion Shop Inner Bushes', 'Potion Shop Inner Rock',
|
||||
'Potion Shop Mirror Spot', 'Potion Shop River Drop']),
|
||||
create_lw_region(multiworld, player, 'Graveyard Cave Area', None,
|
||||
create_lw_region(world, player, 'Graveyard Cave Area', None,
|
||||
['Graveyard Cave', 'Graveyard Cave Inner Bushes', 'Graveyard Cave Mirror Spot']),
|
||||
create_lw_region(multiworld, player, 'River', None, ['Light World Pier', 'Potion Shop Pier']),
|
||||
create_cave_region(multiworld, player, 'Hyrule Castle Secret Entrance', 'a drop\'s exit',
|
||||
create_lw_region(world, player, 'River', None, ['Light World Pier', 'Potion Shop Pier']),
|
||||
create_cave_region(world, player, 'Hyrule Castle Secret Entrance', 'a drop\'s exit',
|
||||
['Link\'s Uncle', 'Secret Passage'], ['Hyrule Castle Secret Entrance Exit']),
|
||||
create_lw_region(multiworld, player, 'Zoras River', ['King Zora', 'Zora\'s Ledge']),
|
||||
create_cave_region(multiworld, player, 'Waterfall of Wishing', 'a cave with two chests',
|
||||
create_lw_region(world, player, 'Zoras River', ['King Zora', 'Zora\'s Ledge']),
|
||||
create_cave_region(world, player, 'Waterfall of Wishing', 'a cave with two chests',
|
||||
['Waterfall Fairy - Left', 'Waterfall Fairy - Right']),
|
||||
create_lw_region(multiworld, player, 'Kings Grave Area', None, ['Kings Grave', 'Kings Grave Inner Rocks']),
|
||||
create_cave_region(multiworld, player, 'Kings Grave', 'a cave with a chest', ['King\'s Tomb']),
|
||||
create_cave_region(multiworld, player, 'North Fairy Cave', 'a drop\'s exit', None, ['North Fairy Cave Exit']),
|
||||
create_cave_region(multiworld, player, 'Dam', 'the dam', ['Floodgate', 'Floodgate Chest']),
|
||||
create_cave_region(multiworld, player, 'Inverted Links House', 'your house', ['Link\'s House'],
|
||||
create_lw_region(world, player, 'Kings Grave Area', None, ['Kings Grave', 'Kings Grave Inner Rocks']),
|
||||
create_cave_region(world, player, 'Kings Grave', 'a cave with a chest', ['King\'s Tomb']),
|
||||
create_cave_region(world, player, 'North Fairy Cave', 'a drop\'s exit', None, ['North Fairy Cave Exit']),
|
||||
create_cave_region(world, player, 'Dam', 'the dam', ['Floodgate', 'Floodgate Chest']),
|
||||
create_cave_region(world, player, 'Inverted Links House', 'your house', ['Link\'s House'],
|
||||
['Inverted Links House Exit']),
|
||||
create_cave_region(multiworld, player, 'Chris Houlihan Room', 'I AM ERROR', None, ['Chris Houlihan Room Exit']),
|
||||
create_cave_region(multiworld, player, 'Tavern', 'the tavern', ['Kakariko Tavern']),
|
||||
create_cave_region(multiworld, player, 'Elder House', 'a connector', None,
|
||||
create_cave_region(world, player, 'Chris Houlihan Room', 'I AM ERROR', None, ['Chris Houlihan Room Exit']),
|
||||
create_cave_region(world, player, 'Tavern', 'the tavern', ['Kakariko Tavern']),
|
||||
create_cave_region(world, player, 'Elder House', 'a connector', None,
|
||||
['Elder House Exit (East)', 'Elder House Exit (West)']),
|
||||
create_cave_region(multiworld, player, 'Snitch Lady (East)', 'a boring house'),
|
||||
create_cave_region(multiworld, player, 'Snitch Lady (West)', 'a boring house'),
|
||||
create_cave_region(multiworld, player, 'Bush Covered House', 'the grass man'),
|
||||
create_cave_region(multiworld, player, 'Tavern (Front)', 'the tavern'),
|
||||
create_cave_region(multiworld, player, 'Light World Bomb Hut', 'a restock room'),
|
||||
create_cave_region(multiworld, player, 'Kakariko Shop', 'a common shop'),
|
||||
create_cave_region(multiworld, player, 'Fortune Teller (Light)', 'a fortune teller'),
|
||||
create_cave_region(multiworld, player, 'Lake Hylia Fortune Teller', 'a fortune teller'),
|
||||
create_cave_region(multiworld, player, 'Lumberjack House', 'a boring house'),
|
||||
create_cave_region(multiworld, player, 'Bonk Fairy (Light)', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Bonk Fairy (Dark)', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Lake Hylia Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Swamp Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Desert Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Dark Lake Hylia Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Dark Lake Hylia Ledge Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Dark Desert Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Dark Death Mountain Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Chicken House', 'a house with a chest', ['Chicken House']),
|
||||
create_cave_region(multiworld, player, 'Aginahs Cave', 'a cave with a chest', ['Aginah\'s Cave']),
|
||||
create_cave_region(multiworld, player, 'Sahasrahlas Hut', 'Sahasrahla',
|
||||
create_cave_region(world, player, 'Snitch Lady (East)', 'a boring house'),
|
||||
create_cave_region(world, player, 'Snitch Lady (West)', 'a boring house'),
|
||||
create_cave_region(world, player, 'Bush Covered House', 'the grass man'),
|
||||
create_cave_region(world, player, 'Tavern (Front)', 'the tavern'),
|
||||
create_cave_region(world, player, 'Light World Bomb Hut', 'a restock room'),
|
||||
create_cave_region(world, player, 'Kakariko Shop', 'a common shop'),
|
||||
create_cave_region(world, player, 'Fortune Teller (Light)', 'a fortune teller'),
|
||||
create_cave_region(world, player, 'Lake Hylia Fortune Teller', 'a fortune teller'),
|
||||
create_cave_region(world, player, 'Lumberjack House', 'a boring house'),
|
||||
create_cave_region(world, player, 'Bonk Fairy (Light)', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Bonk Fairy (Dark)', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Lake Hylia Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Swamp Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Desert Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Dark Lake Hylia Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Dark Lake Hylia Ledge Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Dark Desert Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Dark Death Mountain Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Chicken House', 'a house with a chest', ['Chicken House']),
|
||||
create_cave_region(world, player, 'Aginahs Cave', 'a cave with a chest', ['Aginah\'s Cave']),
|
||||
create_cave_region(world, player, 'Sahasrahlas Hut', 'Sahasrahla',
|
||||
['Sahasrahla\'s Hut - Left', 'Sahasrahla\'s Hut - Middle', 'Sahasrahla\'s Hut - Right',
|
||||
'Sahasrahla']),
|
||||
create_cave_region(multiworld, player, 'Kakariko Well (top)', 'a drop\'s exit',
|
||||
create_cave_region(world, player, 'Kakariko Well (top)', 'a drop\'s exit',
|
||||
['Kakariko Well - Top', 'Kakariko Well - Left', 'Kakariko Well - Middle',
|
||||
'Kakariko Well - Right', 'Kakariko Well - Bottom'], ['Kakariko Well (top to bottom)']),
|
||||
create_cave_region(multiworld, player, 'Kakariko Well (bottom)', 'a drop\'s exit', None, ['Kakariko Well Exit']),
|
||||
create_cave_region(multiworld, player, 'Blacksmiths Hut', 'the smith', ['Blacksmith', 'Missing Smith']),
|
||||
create_lw_region(multiworld, player, 'Bat Cave Drop Ledge', None, ['Bat Cave Drop']),
|
||||
create_cave_region(multiworld, player, 'Bat Cave (right)', 'a drop\'s exit', ['Magic Bat'], ['Bat Cave Door']),
|
||||
create_cave_region(multiworld, player, 'Bat Cave (left)', 'a drop\'s exit', None, ['Bat Cave Exit']),
|
||||
create_cave_region(multiworld, player, 'Sick Kids House', 'the sick kid', ['Sick Kid']),
|
||||
create_lw_region(multiworld, player, 'Hobo Bridge', ['Hobo']),
|
||||
create_cave_region(multiworld, player, 'Lost Woods Hideout (top)', 'a drop\'s exit', ['Lost Woods Hideout'],
|
||||
create_cave_region(world, player, 'Kakariko Well (bottom)', 'a drop\'s exit', None, ['Kakariko Well Exit']),
|
||||
create_cave_region(world, player, 'Blacksmiths Hut', 'the smith', ['Blacksmith', 'Missing Smith']),
|
||||
create_lw_region(world, player, 'Bat Cave Drop Ledge', None, ['Bat Cave Drop']),
|
||||
create_cave_region(world, player, 'Bat Cave (right)', 'a drop\'s exit', ['Magic Bat'], ['Bat Cave Door']),
|
||||
create_cave_region(world, player, 'Bat Cave (left)', 'a drop\'s exit', None, ['Bat Cave Exit']),
|
||||
create_cave_region(world, player, 'Sick Kids House', 'the sick kid', ['Sick Kid']),
|
||||
create_lw_region(world, player, 'Hobo Bridge', ['Hobo']),
|
||||
create_cave_region(world, player, 'Lost Woods Hideout (top)', 'a drop\'s exit', ['Lost Woods Hideout'],
|
||||
['Lost Woods Hideout (top to bottom)']),
|
||||
create_cave_region(multiworld, player, 'Lost Woods Hideout (bottom)', 'a drop\'s exit', None,
|
||||
create_cave_region(world, player, 'Lost Woods Hideout (bottom)', 'a drop\'s exit', None,
|
||||
['Lost Woods Hideout Exit']),
|
||||
create_cave_region(multiworld, player, 'Lumberjack Tree (top)', 'a drop\'s exit', ['Lumberjack Tree'],
|
||||
create_cave_region(world, player, 'Lumberjack Tree (top)', 'a drop\'s exit', ['Lumberjack Tree'],
|
||||
['Lumberjack Tree (top to bottom)']),
|
||||
create_cave_region(multiworld, player, 'Lumberjack Tree (bottom)', 'a drop\'s exit', None, ['Lumberjack Tree Exit']),
|
||||
create_cave_region(multiworld, player, 'Cave 45', 'a cave with an item', ['Cave 45']),
|
||||
create_cave_region(multiworld, player, 'Graveyard Cave', 'a cave with an item', ['Graveyard Cave']),
|
||||
create_cave_region(multiworld, player, 'Checkerboard Cave', 'a cave with an item', ['Checkerboard Cave']),
|
||||
create_cave_region(multiworld, player, 'Long Fairy Cave', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Mini Moldorm Cave', 'a bounty of five items',
|
||||
create_cave_region(world, player, 'Lumberjack Tree (bottom)', 'a drop\'s exit', None, ['Lumberjack Tree Exit']),
|
||||
create_cave_region(world, player, 'Cave 45', 'a cave with an item', ['Cave 45']),
|
||||
create_cave_region(world, player, 'Graveyard Cave', 'a cave with an item', ['Graveyard Cave']),
|
||||
create_cave_region(world, player, 'Checkerboard Cave', 'a cave with an item', ['Checkerboard Cave']),
|
||||
create_cave_region(world, player, 'Long Fairy Cave', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Mini Moldorm Cave', 'a bounty of five items',
|
||||
['Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right',
|
||||
'Mini Moldorm Cave - Far Right', 'Mini Moldorm Cave - Generous Guy']),
|
||||
create_cave_region(multiworld, player, 'Ice Rod Cave', 'a cave with a chest', ['Ice Rod Cave']),
|
||||
create_cave_region(multiworld, player, 'Good Bee Cave', 'a cold bee'),
|
||||
create_cave_region(multiworld, player, '20 Rupee Cave', 'a cave with some cash'),
|
||||
create_cave_region(multiworld, player, 'Cave Shop (Lake Hylia)', 'a common shop'),
|
||||
create_cave_region(multiworld, player, 'Cave Shop (Dark Death Mountain)', 'a common shop'),
|
||||
create_cave_region(multiworld, player, 'Bonk Rock Cave', 'a cave with a chest', ['Bonk Rock Cave']),
|
||||
create_cave_region(multiworld, player, 'Library', 'the library', ['Library']),
|
||||
create_cave_region(multiworld, player, 'Kakariko Gamble Game', 'a game of chance'),
|
||||
create_cave_region(multiworld, player, 'Potion Shop', 'the potion shop', ['Potion Shop']),
|
||||
create_lw_region(multiworld, player, 'Lake Hylia Island', ['Lake Hylia Island']),
|
||||
create_cave_region(multiworld, player, 'Capacity Upgrade', 'the queen of fairies', ['Capacity Upgrade Shop']),
|
||||
create_cave_region(multiworld, player, 'Two Brothers House', 'a connector', None,
|
||||
create_cave_region(world, player, 'Ice Rod Cave', 'a cave with a chest', ['Ice Rod Cave']),
|
||||
create_cave_region(world, player, 'Good Bee Cave', 'a cold bee'),
|
||||
create_cave_region(world, player, '20 Rupee Cave', 'a cave with some cash'),
|
||||
create_cave_region(world, player, 'Cave Shop (Lake Hylia)', 'a common shop'),
|
||||
create_cave_region(world, player, 'Cave Shop (Dark Death Mountain)', 'a common shop'),
|
||||
create_cave_region(world, player, 'Bonk Rock Cave', 'a cave with a chest', ['Bonk Rock Cave']),
|
||||
create_cave_region(world, player, 'Library', 'the library', ['Library']),
|
||||
create_cave_region(world, player, 'Kakariko Gamble Game', 'a game of chance'),
|
||||
create_cave_region(world, player, 'Potion Shop', 'the potion shop', ['Potion Shop']),
|
||||
create_lw_region(world, player, 'Lake Hylia Island', ['Lake Hylia Island']),
|
||||
create_cave_region(world, player, 'Capacity Upgrade', 'the queen of fairies', ['Capacity Upgrade Shop']),
|
||||
create_cave_region(world, player, 'Two Brothers House', 'a connector', None,
|
||||
['Two Brothers House Exit (East)', 'Two Brothers House Exit (West)']),
|
||||
create_lw_region(multiworld, player, 'Maze Race Ledge', ['Maze Race'],
|
||||
create_lw_region(world, player, 'Maze Race Ledge', ['Maze Race'],
|
||||
['Two Brothers House (West)', 'Maze Race Mirror Spot']),
|
||||
create_cave_region(multiworld, player, '50 Rupee Cave', 'a cave with some cash'),
|
||||
create_lw_region(multiworld, player, 'Desert Ledge', ['Desert Ledge'],
|
||||
create_cave_region(world, player, '50 Rupee Cave', 'a cave with some cash'),
|
||||
create_lw_region(world, player, 'Desert Ledge', ['Desert Ledge'],
|
||||
['Desert Palace Entrance (North) Rocks', 'Desert Palace Entrance (West)',
|
||||
'Desert Ledge Drop']),
|
||||
create_lw_region(multiworld, player, 'Desert Palace Stairs', None,
|
||||
create_lw_region(world, player, 'Desert Palace Stairs', None,
|
||||
['Desert Palace Entrance (South)', 'Desert Palace Stairs Mirror Spot']),
|
||||
create_lw_region(multiworld, player, 'Desert Palace Lone Stairs', None,
|
||||
create_lw_region(world, player, 'Desert Palace Lone Stairs', None,
|
||||
['Desert Palace Stairs Drop', 'Desert Palace Entrance (East)']),
|
||||
create_lw_region(multiworld, player, 'Desert Palace Entrance (North) Spot', None,
|
||||
create_lw_region(world, player, 'Desert Palace Entrance (North) Spot', None,
|
||||
['Desert Palace Entrance (North)', 'Desert Ledge Return Rocks',
|
||||
'Desert Palace North Mirror Spot']),
|
||||
create_dungeon_region(multiworld, player, 'Desert Palace Main (Outer)', 'Desert Palace', ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'],
|
||||
create_dungeon_region(world, player, 'Desert Palace Main (Outer)', 'Desert Palace', ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'],
|
||||
['Desert Palace Pots (Outer)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)', 'Desert Palace East Wing']),
|
||||
create_dungeon_region(multiworld, player, 'Desert Palace Main (Inner)', 'Desert Palace', None, ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']),
|
||||
create_dungeon_region(multiworld, player, 'Desert Palace East', 'Desert Palace', ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']),
|
||||
create_dungeon_region(multiworld, player, 'Desert Palace North', 'Desert Palace',
|
||||
create_dungeon_region(world, player, 'Desert Palace Main (Inner)', 'Desert Palace', None, ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']),
|
||||
create_dungeon_region(world, player, 'Desert Palace East', 'Desert Palace', ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']),
|
||||
create_dungeon_region(world, player, 'Desert Palace North', 'Desert Palace',
|
||||
['Desert Palace - Desert Tiles 1 Pot Key', 'Desert Palace - Beamos Hall Pot Key',
|
||||
'Desert Palace - Desert Tiles 2 Pot Key',
|
||||
'Desert Palace - Boss', 'Desert Palace - Prize'], ['Desert Palace Exit (North)']),
|
||||
create_dungeon_region(multiworld, player, 'Eastern Palace', 'Eastern Palace',
|
||||
create_dungeon_region(world, player, 'Eastern Palace', 'Eastern Palace',
|
||||
['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest',
|
||||
'Eastern Palace - Cannonball Chest',
|
||||
'Eastern Palace - Dark Square Pot Key', 'Eastern Palace - Dark Eyegore Key Drop',
|
||||
'Eastern Palace - Big Key Chest',
|
||||
'Eastern Palace - Map Chest', 'Eastern Palace - Boss', 'Eastern Palace - Prize'],
|
||||
['Eastern Palace Exit']),
|
||||
create_lw_region(multiworld, player, 'Master Sword Meadow', ['Master Sword Pedestal']),
|
||||
create_cave_region(multiworld, player, 'Lost Woods Gamble', 'a game of chance'),
|
||||
create_lw_region(multiworld, player, 'Hyrule Castle Ledge', None, ['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Inverted Ganons Tower', 'Hyrule Castle Ledge Courtyard Drop', 'Inverted Pyramid Hole']),
|
||||
create_dungeon_region(multiworld, player, 'Hyrule Castle', 'Hyrule Castle',
|
||||
create_lw_region(world, player, 'Master Sword Meadow', ['Master Sword Pedestal']),
|
||||
create_cave_region(world, player, 'Lost Woods Gamble', 'a game of chance'),
|
||||
create_lw_region(world, player, 'Hyrule Castle Ledge', None, ['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Inverted Ganons Tower', 'Hyrule Castle Ledge Courtyard Drop', 'Inverted Pyramid Hole']),
|
||||
create_dungeon_region(world, player, 'Hyrule Castle', 'Hyrule Castle',
|
||||
['Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest',
|
||||
'Hyrule Castle - Zelda\'s Chest',
|
||||
'Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop',
|
||||
'Hyrule Castle - Big Key Drop'],
|
||||
['Hyrule Castle Exit (East)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (South)',
|
||||
'Throne Room']),
|
||||
create_dungeon_region(multiworld, player, 'Sewer Drop', 'a drop\'s exit', None, ['Sewer Drop']), # This exists only to be referenced for access checks
|
||||
create_dungeon_region(multiworld, player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross', 'Sewers - Key Rat Key Drop'], ['Sewers Door']),
|
||||
create_dungeon_region(multiworld, player, 'Sewers', 'a drop\'s exit', None, ['Sanctuary Push Door', 'Sewers Back Door', 'Sewers Secret Room']),
|
||||
create_dungeon_region(multiworld, player, 'Sewers Secret Room', 'a drop\'s exit', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle',
|
||||
create_dungeon_region(world, player, 'Sewer Drop', 'a drop\'s exit', None, ['Sewer Drop']), # This exists only to be referenced for access checks
|
||||
create_dungeon_region(world, player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross', 'Sewers - Key Rat Key Drop'], ['Sewers Door']),
|
||||
create_dungeon_region(world, player, 'Sewers', 'a drop\'s exit', None, ['Sanctuary Push Door', 'Sewers Back Door', 'Sewers Secret Room']),
|
||||
create_dungeon_region(world, player, 'Sewers Secret Room', 'a drop\'s exit', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle',
|
||||
'Sewers - Secret Room - Right']),
|
||||
create_dungeon_region(multiworld, player, 'Sanctuary', 'a drop\'s exit', ['Sanctuary'], ['Sanctuary Exit']),
|
||||
create_dungeon_region(multiworld, player, 'Inverted Agahnims Tower', 'Castle Tower', ['Castle Tower - Room 03', 'Castle Tower - Dark Maze', 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'], ['Agahnim 1', 'Inverted Agahnims Tower Exit']),
|
||||
create_dungeon_region(multiworld, player, 'Agahnim 1', 'Castle Tower', ['Agahnim 1'], None),
|
||||
create_cave_region(multiworld, player, 'Old Man Cave', 'a connector', ['Old Man'],
|
||||
create_dungeon_region(world, player, 'Sanctuary', 'a drop\'s exit', ['Sanctuary'], ['Sanctuary Exit']),
|
||||
create_dungeon_region(world, player, 'Inverted Agahnims Tower', 'Castle Tower', ['Castle Tower - Room 03', 'Castle Tower - Dark Maze', 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'], ['Agahnim 1', 'Inverted Agahnims Tower Exit']),
|
||||
create_dungeon_region(world, player, 'Agahnim 1', 'Castle Tower', ['Agahnim 1'], None),
|
||||
create_cave_region(world, player, 'Old Man Cave', 'a connector', ['Old Man'],
|
||||
['Old Man Cave Exit (East)', 'Old Man Cave Exit (West)']),
|
||||
create_cave_region(multiworld, player, 'Old Man House', 'a connector', None,
|
||||
create_cave_region(world, player, 'Old Man House', 'a connector', None,
|
||||
['Old Man House Exit (Bottom)', 'Old Man House Front to Back']),
|
||||
create_cave_region(multiworld, player, 'Old Man House Back', 'a connector', None,
|
||||
create_cave_region(world, player, 'Old Man House Back', 'a connector', None,
|
||||
['Old Man House Exit (Top)', 'Old Man House Back to Front']),
|
||||
create_lw_region(multiworld, player, 'Death Mountain', None,
|
||||
create_lw_region(world, player, 'Death Mountain', None,
|
||||
['Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)',
|
||||
'Death Mountain Return Cave (East)', 'Spectacle Rock Cave',
|
||||
'Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Bottom)', 'Broken Bridge (West)',
|
||||
'Death Mountain Mirror Spot']),
|
||||
create_cave_region(multiworld, player, 'Death Mountain Return Cave', 'a connector', None,
|
||||
create_cave_region(world, player, 'Death Mountain Return Cave', 'a connector', None,
|
||||
['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)']),
|
||||
create_lw_region(multiworld, player, 'Death Mountain Return Ledge', None,
|
||||
create_lw_region(world, player, 'Death Mountain Return Ledge', None,
|
||||
['Death Mountain Return Ledge Drop', 'Death Mountain Return Cave (West)',
|
||||
'Bumper Cave Ledge Mirror Spot']),
|
||||
create_cave_region(multiworld, player, 'Spectacle Rock Cave (Top)', 'a connector', ['Spectacle Rock Cave'],
|
||||
create_cave_region(world, player, 'Spectacle Rock Cave (Top)', 'a connector', ['Spectacle Rock Cave'],
|
||||
['Spectacle Rock Cave Drop', 'Spectacle Rock Cave Exit (Top)']),
|
||||
create_cave_region(multiworld, player, 'Spectacle Rock Cave (Bottom)', 'a connector', None,
|
||||
create_cave_region(world, player, 'Spectacle Rock Cave (Bottom)', 'a connector', None,
|
||||
['Spectacle Rock Cave Exit']),
|
||||
create_cave_region(multiworld, player, 'Spectacle Rock Cave (Peak)', 'a connector', None,
|
||||
create_cave_region(world, player, 'Spectacle Rock Cave (Peak)', 'a connector', None,
|
||||
['Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave Exit (Peak)']),
|
||||
create_lw_region(multiworld, player, 'East Death Mountain (Bottom)', None,
|
||||
create_lw_region(world, player, 'East Death Mountain (Bottom)', None,
|
||||
['Broken Bridge (East)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)',
|
||||
'East Death Mountain Mirror Spot (Bottom)', 'Hookshot Fairy',
|
||||
'Fairy Ascension Rocks', 'Spiral Cave (Bottom)']),
|
||||
create_cave_region(multiworld, player, 'Hookshot Fairy', 'fairies deep in a cave'),
|
||||
create_cave_region(multiworld, player, 'Paradox Cave Front', 'a connector', None,
|
||||
create_cave_region(world, player, 'Hookshot Fairy', 'fairies deep in a cave'),
|
||||
create_cave_region(world, player, 'Paradox Cave Front', 'a connector', None,
|
||||
['Paradox Cave Push Block Reverse', 'Paradox Cave Exit (Bottom)',
|
||||
'Light World Death Mountain Shop']),
|
||||
create_cave_region(multiworld, player, 'Paradox Cave Chest Area', 'a connector', ['Paradox Cave Lower - Far Left',
|
||||
create_cave_region(world, player, 'Paradox Cave Chest Area', 'a connector', ['Paradox Cave Lower - Far Left',
|
||||
'Paradox Cave Lower - Left',
|
||||
'Paradox Cave Lower - Right',
|
||||
'Paradox Cave Lower - Far Right',
|
||||
@@ -221,273 +220,273 @@ def create_inverted_regions(multiworld: MultiWorld, player: int):
|
||||
'Paradox Cave Upper - Left',
|
||||
'Paradox Cave Upper - Right'],
|
||||
['Paradox Cave Push Block', 'Paradox Cave Bomb Jump']),
|
||||
create_cave_region(multiworld, player, 'Paradox Cave', 'a connector', None,
|
||||
create_cave_region(world, player, 'Paradox Cave', 'a connector', None,
|
||||
['Paradox Cave Exit (Middle)', 'Paradox Cave Exit (Top)', 'Paradox Cave Drop']),
|
||||
create_cave_region(multiworld, player, 'Light World Death Mountain Shop', 'a common shop'),
|
||||
create_lw_region(multiworld, player, 'East Death Mountain (Top)', ['Floating Island'],
|
||||
create_cave_region(world, player, 'Light World Death Mountain Shop', 'a common shop'),
|
||||
create_lw_region(world, player, 'East Death Mountain (Top)', ['Floating Island'],
|
||||
['Paradox Cave (Top)', 'Death Mountain (Top)', 'Spiral Cave Ledge Access',
|
||||
'East Death Mountain Drop', 'East Death Mountain Mirror Spot (Top)',
|
||||
'Fairy Ascension Ledge Access', 'Mimic Cave Ledge Access',
|
||||
'Floating Island Mirror Spot']),
|
||||
create_lw_region(multiworld, player, 'Spiral Cave Ledge', None,
|
||||
create_lw_region(world, player, 'Spiral Cave Ledge', None,
|
||||
['Spiral Cave', 'Spiral Cave Ledge Drop', 'Dark Death Mountain Ledge Mirror Spot (West)']),
|
||||
create_lw_region(multiworld, player, 'Mimic Cave Ledge', None,
|
||||
create_lw_region(world, player, 'Mimic Cave Ledge', None,
|
||||
['Mimic Cave', 'Mimic Cave Ledge Drop', 'Dark Death Mountain Ledge Mirror Spot (East)']),
|
||||
create_cave_region(multiworld, player, 'Spiral Cave (Top)', 'a connector', ['Spiral Cave'],
|
||||
create_cave_region(world, player, 'Spiral Cave (Top)', 'a connector', ['Spiral Cave'],
|
||||
['Spiral Cave (top to bottom)', 'Spiral Cave Exit (Top)']),
|
||||
create_cave_region(multiworld, player, 'Spiral Cave (Bottom)', 'a connector', None, ['Spiral Cave Exit']),
|
||||
create_lw_region(multiworld, player, 'Fairy Ascension Plateau', None,
|
||||
create_cave_region(world, player, 'Spiral Cave (Bottom)', 'a connector', None, ['Spiral Cave Exit']),
|
||||
create_lw_region(world, player, 'Fairy Ascension Plateau', None,
|
||||
['Fairy Ascension Drop', 'Fairy Ascension Cave (Bottom)']),
|
||||
create_cave_region(multiworld, player, 'Fairy Ascension Cave (Bottom)', 'a connector', None,
|
||||
create_cave_region(world, player, 'Fairy Ascension Cave (Bottom)', 'a connector', None,
|
||||
['Fairy Ascension Cave Climb', 'Fairy Ascension Cave Exit (Bottom)']),
|
||||
create_cave_region(multiworld, player, 'Fairy Ascension Cave (Drop)', 'a connector', None,
|
||||
create_cave_region(world, player, 'Fairy Ascension Cave (Drop)', 'a connector', None,
|
||||
['Fairy Ascension Cave Pots']),
|
||||
create_cave_region(multiworld, player, 'Fairy Ascension Cave (Top)', 'a connector', None,
|
||||
create_cave_region(world, player, 'Fairy Ascension Cave (Top)', 'a connector', None,
|
||||
['Fairy Ascension Cave Exit (Top)', 'Fairy Ascension Cave Drop']),
|
||||
create_lw_region(multiworld, player, 'Fairy Ascension Ledge', None,
|
||||
create_lw_region(world, player, 'Fairy Ascension Ledge', None,
|
||||
['Fairy Ascension Ledge Drop', 'Fairy Ascension Cave (Top)', 'Laser Bridge Mirror Spot']),
|
||||
create_lw_region(multiworld, player, 'Death Mountain (Top)', ['Ether Tablet', 'Spectacle Rock'],
|
||||
create_lw_region(world, player, 'Death Mountain (Top)', ['Ether Tablet', 'Spectacle Rock'],
|
||||
['East Death Mountain (Top)', 'Tower of Hera', 'Death Mountain Drop',
|
||||
'Death Mountain (Top) Mirror Spot']),
|
||||
create_dw_region(multiworld, player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'],
|
||||
create_dw_region(world, player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'],
|
||||
['Bumper Cave Ledge Drop', 'Bumper Cave (Top)']),
|
||||
create_dungeon_region(multiworld, player, 'Tower of Hera (Bottom)', 'Tower of Hera', ['Tower of Hera - Basement Cage', 'Tower of Hera - Map Chest'], ['Tower of Hera Small Key Door', 'Tower of Hera Big Key Door', 'Tower of Hera Exit']),
|
||||
create_dungeon_region(multiworld, player, 'Tower of Hera (Basement)', 'Tower of Hera', ['Tower of Hera - Big Key Chest']),
|
||||
create_dungeon_region(multiworld, player, 'Tower of Hera (Top)', 'Tower of Hera', ['Tower of Hera - Compass Chest', 'Tower of Hera - Big Chest', 'Tower of Hera - Boss', 'Tower of Hera - Prize']),
|
||||
create_dungeon_region(world, player, 'Tower of Hera (Bottom)', 'Tower of Hera', ['Tower of Hera - Basement Cage', 'Tower of Hera - Map Chest'], ['Tower of Hera Small Key Door', 'Tower of Hera Big Key Door', 'Tower of Hera Exit']),
|
||||
create_dungeon_region(world, player, 'Tower of Hera (Basement)', 'Tower of Hera', ['Tower of Hera - Big Key Chest']),
|
||||
create_dungeon_region(world, player, 'Tower of Hera (Top)', 'Tower of Hera', ['Tower of Hera - Compass Chest', 'Tower of Hera - Big Chest', 'Tower of Hera - Boss', 'Tower of Hera - Prize']),
|
||||
|
||||
create_dw_region(multiworld, player, 'East Dark World', ['Pyramid'],
|
||||
create_dw_region(world, player, 'East Dark World', ['Pyramid'],
|
||||
['Pyramid Fairy', 'South Dark World Bridge', 'Palace of Darkness',
|
||||
'Dark Lake Hylia Drop (East)',
|
||||
'Dark Lake Hylia Fairy', 'Palace of Darkness Hint', 'East Dark World Hint',
|
||||
'Northeast Dark World Broken Bridge Pass', 'East Dark World Teleporter', 'EDW Flute']),
|
||||
create_dw_region(multiworld, player, 'Catfish', ['Catfish'], ['Catfish Exit Rock']),
|
||||
create_dw_region(multiworld, player, 'Northeast Dark World', None,
|
||||
create_dw_region(world, player, 'Catfish', ['Catfish'], ['Catfish Exit Rock']),
|
||||
create_dw_region(world, player, 'Northeast Dark World', None,
|
||||
['West Dark World Gap', 'Dark World Potion Shop', 'East Dark World Broken Bridge Pass',
|
||||
'NEDW Flute', 'Dark Lake Hylia Teleporter', 'Catfish Entrance Rock']),
|
||||
create_cave_region(multiworld, player, 'Palace of Darkness Hint', 'a storyteller'),
|
||||
create_cave_region(multiworld, player, 'East Dark World Hint', 'a storyteller'),
|
||||
create_dw_region(multiworld, player, 'South Dark World', ['Stumpy', 'Digging Game'],
|
||||
create_cave_region(world, player, 'Palace of Darkness Hint', 'a storyteller'),
|
||||
create_cave_region(world, player, 'East Dark World Hint', 'a storyteller'),
|
||||
create_dw_region(world, player, 'South Dark World', ['Stumpy', 'Digging Game'],
|
||||
['Dark Lake Hylia Drop (South)', 'Hype Cave', 'Swamp Palace', 'Village of Outcasts Heavy Rock',
|
||||
'East Dark World Bridge', 'Inverted Links House', 'Archery Game', 'Bonk Fairy (Dark)',
|
||||
'Dark Lake Hylia Shop', 'South Dark World Teleporter', 'Post Aga Teleporter', 'SDW Flute']),
|
||||
create_cave_region(multiworld, player, 'Inverted Big Bomb Shop', 'the bomb shop'),
|
||||
create_cave_region(multiworld, player, 'Archery Game', 'a game of skill'),
|
||||
create_dw_region(multiworld, player, 'Dark Lake Hylia', None,
|
||||
create_cave_region(world, player, 'Inverted Big Bomb Shop', 'the bomb shop'),
|
||||
create_cave_region(world, player, 'Archery Game', 'a game of skill'),
|
||||
create_dw_region(world, player, 'Dark Lake Hylia', None,
|
||||
['East Dark World Pier', 'Dark Lake Hylia Ledge Pier', 'Ice Palace Missing Wall']),
|
||||
create_dw_region(multiworld, player, 'Dark Lake Hylia Central Island', None,
|
||||
create_dw_region(world, player, 'Dark Lake Hylia Central Island', None,
|
||||
['Dark Lake Hylia Shallows', 'Ice Palace', 'Dark Lake Hylia Central Island Teleporter']),
|
||||
create_dw_region(multiworld, player, 'Dark Lake Hylia Ledge', None,
|
||||
create_dw_region(world, player, 'Dark Lake Hylia Ledge', None,
|
||||
['Dark Lake Hylia Ledge Drop', 'Dark Lake Hylia Ledge Fairy', 'Dark Lake Hylia Ledge Hint',
|
||||
'Dark Lake Hylia Ledge Spike Cave', 'DLHL Flute']),
|
||||
create_cave_region(multiworld, player, 'Dark Lake Hylia Ledge Hint', 'a storyteller'),
|
||||
create_cave_region(multiworld, player, 'Dark Lake Hylia Ledge Spike Cave', 'a spiky hint'),
|
||||
create_cave_region(multiworld, player, 'Hype Cave', 'a bounty of five items',
|
||||
create_cave_region(world, player, 'Dark Lake Hylia Ledge Hint', 'a storyteller'),
|
||||
create_cave_region(world, player, 'Dark Lake Hylia Ledge Spike Cave', 'a spiky hint'),
|
||||
create_cave_region(world, player, 'Hype Cave', 'a bounty of five items',
|
||||
['Hype Cave - Top', 'Hype Cave - Middle Right', 'Hype Cave - Middle Left',
|
||||
'Hype Cave - Bottom', 'Hype Cave - Generous Guy']),
|
||||
create_dw_region(multiworld, player, 'West Dark World', ['Frog', 'Flute Activation Spot'],
|
||||
create_dw_region(world, player, 'West Dark World', ['Frog', 'Flute Activation Spot'],
|
||||
['Village of Outcasts Drop', 'East Dark World River Pier', 'Brewery', 'C-Shaped House',
|
||||
'Chest Game', 'Thieves Town', 'Bumper Cave Entrance Rock',
|
||||
'Skull Woods Forest', 'Village of Outcasts Pegs', 'Village of Outcasts Eastern Rocks',
|
||||
'Red Shield Shop', 'Inverted Dark Sanctuary', 'Fortune Teller (Dark)',
|
||||
'Dark World Lumberjack Shop',
|
||||
'West Dark World Teleporter', 'WDW Flute']),
|
||||
create_dw_region(multiworld, player, 'Dark Grassy Lawn', None,
|
||||
create_dw_region(world, player, 'Dark Grassy Lawn', None,
|
||||
['Grassy Lawn Pegs', 'Village of Outcasts Shop', 'Dark Grassy Lawn Flute']),
|
||||
create_dw_region(multiworld, player, 'Hammer Peg Area', ['Dark Blacksmith Ruins'],
|
||||
create_dw_region(world, player, 'Hammer Peg Area', ['Dark Blacksmith Ruins'],
|
||||
['Dark World Hammer Peg Cave', 'Peg Area Rocks', 'Hammer Peg Area Flute']),
|
||||
create_dw_region(multiworld, player, 'Bumper Cave Entrance', None,
|
||||
create_dw_region(world, player, 'Bumper Cave Entrance', None,
|
||||
['Bumper Cave (Bottom)', 'Bumper Cave Entrance Drop']),
|
||||
create_cave_region(multiworld, player, 'Fortune Teller (Dark)', 'a fortune teller'),
|
||||
create_cave_region(multiworld, player, 'Village of Outcasts Shop', 'a common shop'),
|
||||
create_cave_region(multiworld, player, 'Dark Lake Hylia Shop', 'a common shop'),
|
||||
create_cave_region(multiworld, player, 'Dark World Lumberjack Shop', 'a common shop'),
|
||||
create_cave_region(multiworld, player, 'Dark World Potion Shop', 'a common shop'),
|
||||
create_cave_region(multiworld, player, 'Dark World Hammer Peg Cave', 'a cave with an item', ['Peg Cave']),
|
||||
create_cave_region(multiworld, player, 'Pyramid Fairy', 'a cave with two chests',
|
||||
create_cave_region(world, player, 'Fortune Teller (Dark)', 'a fortune teller'),
|
||||
create_cave_region(world, player, 'Village of Outcasts Shop', 'a common shop'),
|
||||
create_cave_region(world, player, 'Dark Lake Hylia Shop', 'a common shop'),
|
||||
create_cave_region(world, player, 'Dark World Lumberjack Shop', 'a common shop'),
|
||||
create_cave_region(world, player, 'Dark World Potion Shop', 'a common shop'),
|
||||
create_cave_region(world, player, 'Dark World Hammer Peg Cave', 'a cave with an item', ['Peg Cave']),
|
||||
create_cave_region(world, player, 'Pyramid Fairy', 'a cave with two chests',
|
||||
['Pyramid Fairy - Left', 'Pyramid Fairy - Right']),
|
||||
create_cave_region(multiworld, player, 'Brewery', 'a house with a chest', ['Brewery']),
|
||||
create_cave_region(multiworld, player, 'C-Shaped House', 'a house with a chest', ['C-Shaped House']),
|
||||
create_cave_region(multiworld, player, 'Chest Game', 'a game of 16 chests', ['Chest Game']),
|
||||
create_cave_region(multiworld, player, 'Red Shield Shop', 'the rare shop'),
|
||||
create_cave_region(multiworld, player, 'Inverted Dark Sanctuary', 'a storyteller', None,
|
||||
create_cave_region(world, player, 'Brewery', 'a house with a chest', ['Brewery']),
|
||||
create_cave_region(world, player, 'C-Shaped House', 'a house with a chest', ['C-Shaped House']),
|
||||
create_cave_region(world, player, 'Chest Game', 'a game of 16 chests', ['Chest Game']),
|
||||
create_cave_region(world, player, 'Red Shield Shop', 'the rare shop'),
|
||||
create_cave_region(world, player, 'Inverted Dark Sanctuary', 'a storyteller', None,
|
||||
['Inverted Dark Sanctuary Exit']),
|
||||
create_cave_region(multiworld, player, 'Bumper Cave', 'a connector', None,
|
||||
create_cave_region(world, player, 'Bumper Cave', 'a connector', None,
|
||||
['Bumper Cave Exit (Bottom)', 'Bumper Cave Exit (Top)']),
|
||||
create_dw_region(multiworld, player, 'Skull Woods Forest', None,
|
||||
create_dw_region(world, player, 'Skull Woods Forest', None,
|
||||
['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)',
|
||||
'Skull Woods First Section Hole (North)',
|
||||
'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)']),
|
||||
create_dw_region(multiworld, player, 'Skull Woods Forest (West)', None,
|
||||
create_dw_region(world, player, 'Skull Woods Forest (West)', None,
|
||||
['Skull Woods Second Section Hole', 'Skull Woods Second Section Door (West)',
|
||||
'Skull Woods Final Section']),
|
||||
create_dw_region(multiworld, player, 'Dark Desert', None,
|
||||
create_dw_region(world, player, 'Dark Desert', None,
|
||||
['Misery Mire', 'Mire Shed', 'Dark Desert Hint', 'Dark Desert Fairy', 'DD Flute']),
|
||||
create_dw_region(multiworld, player, 'Dark Desert Ledge', None, ['Dark Desert Drop', 'Dark Desert Teleporter']),
|
||||
create_cave_region(multiworld, player, 'Mire Shed', 'a cave with two chests',
|
||||
create_dw_region(world, player, 'Dark Desert Ledge', None, ['Dark Desert Drop', 'Dark Desert Teleporter']),
|
||||
create_cave_region(world, player, 'Mire Shed', 'a cave with two chests',
|
||||
['Mire Shed - Left', 'Mire Shed - Right']),
|
||||
create_cave_region(multiworld, player, 'Dark Desert Hint', 'a storyteller'),
|
||||
create_dw_region(multiworld, player, 'Dark Death Mountain', None,
|
||||
create_cave_region(world, player, 'Dark Desert Hint', 'a storyteller'),
|
||||
create_dw_region(world, player, 'Dark Death Mountain', None,
|
||||
['Dark Death Mountain Drop (East)', 'Inverted Agahnims Tower', 'Superbunny Cave (Top)',
|
||||
'Hookshot Cave', 'Turtle Rock',
|
||||
'Spike Cave', 'Dark Death Mountain Fairy', 'Dark Death Mountain Teleporter (West)',
|
||||
'Turtle Rock Tail Drop', 'DDM Flute']),
|
||||
create_dw_region(multiworld, player, 'Dark Death Mountain Ledge', None,
|
||||
create_dw_region(world, player, 'Dark Death Mountain Ledge', None,
|
||||
['Dark Death Mountain Ledge (East)', 'Dark Death Mountain Ledge (West)']),
|
||||
create_dw_region(multiworld, player, 'Turtle Rock (Top)', None,
|
||||
create_dw_region(world, player, 'Turtle Rock (Top)', None,
|
||||
['Dark Death Mountain Teleporter (East)', 'Turtle Rock Drop']),
|
||||
create_dw_region(multiworld, player, 'Dark Death Mountain Isolated Ledge', None,
|
||||
create_dw_region(world, player, 'Dark Death Mountain Isolated Ledge', None,
|
||||
['Turtle Rock Isolated Ledge Entrance']),
|
||||
create_dw_region(multiworld, player, 'Dark Death Mountain (East Bottom)', None,
|
||||
create_dw_region(world, player, 'Dark Death Mountain (East Bottom)', None,
|
||||
['Superbunny Cave (Bottom)', 'Cave Shop (Dark Death Mountain)',
|
||||
'Dark Death Mountain Teleporter (East Bottom)', 'EDDM Flute']),
|
||||
create_cave_region(multiworld, player, 'Superbunny Cave (Top)', 'a connector',
|
||||
create_cave_region(world, player, 'Superbunny Cave (Top)', 'a connector',
|
||||
['Superbunny Cave - Top', 'Superbunny Cave - Bottom'], ['Superbunny Cave Exit (Top)']),
|
||||
create_cave_region(multiworld, player, 'Superbunny Cave (Bottom)', 'a connector', None,
|
||||
create_cave_region(world, player, 'Superbunny Cave (Bottom)', 'a connector', None,
|
||||
['Superbunny Cave Climb', 'Superbunny Cave Exit (Bottom)']),
|
||||
create_cave_region(multiworld, player, 'Spike Cave', 'Spike Cave', ['Spike Cave']),
|
||||
create_cave_region(multiworld, player, 'Hookshot Cave', 'a connector',
|
||||
create_cave_region(world, player, 'Spike Cave', 'Spike Cave', ['Spike Cave']),
|
||||
create_cave_region(world, player, 'Hookshot Cave', 'a connector',
|
||||
['Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right',
|
||||
'Hookshot Cave - Bottom Left'],
|
||||
['Hookshot Cave Exit (South)', 'Hookshot Cave Bomb Wall (South)']),
|
||||
create_cave_region(multiworld, player, 'Hookshot Cave (Upper)', 'a connector', None, ['Hookshot Cave Exit (North)',
|
||||
create_cave_region(world, player, 'Hookshot Cave (Upper)', 'a connector', None, ['Hookshot Cave Exit (North)',
|
||||
'Hookshot Cave Bomb Wall (North)']),
|
||||
create_dw_region(multiworld, player, 'Death Mountain Floating Island (Dark World)', None,
|
||||
create_dw_region(world, player, 'Death Mountain Floating Island (Dark World)', None,
|
||||
['Floating Island Drop', 'Hookshot Cave Back Entrance']),
|
||||
create_cave_region(multiworld, player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']),
|
||||
create_cave_region(world, player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']),
|
||||
|
||||
create_dungeon_region(multiworld, player, 'Swamp Palace (Entrance)', 'Swamp Palace', None, ['Swamp Palace Moat', 'Swamp Palace Exit']),
|
||||
create_dungeon_region(multiworld, player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'], ['Swamp Palace Small Key Door']),
|
||||
create_dungeon_region(multiworld, player, 'Swamp Palace (Starting Area)', 'Swamp Palace', ['Swamp Palace - Map Chest', 'Swamp Palace - Pot Row Pot Key',
|
||||
create_dungeon_region(world, player, 'Swamp Palace (Entrance)', 'Swamp Palace', None, ['Swamp Palace Moat', 'Swamp Palace Exit']),
|
||||
create_dungeon_region(world, player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'], ['Swamp Palace Small Key Door']),
|
||||
create_dungeon_region(world, player, 'Swamp Palace (Starting Area)', 'Swamp Palace', ['Swamp Palace - Map Chest', 'Swamp Palace - Pot Row Pot Key',
|
||||
'Swamp Palace - Trench 1 Pot Key'], ['Swamp Palace (Center)']),
|
||||
create_dungeon_region(multiworld, player, 'Swamp Palace (Center)', 'Swamp Palace', ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest', 'Swamp Palace - Hookshot Pot Key',
|
||||
create_dungeon_region(world, player, 'Swamp Palace (Center)', 'Swamp Palace', ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest', 'Swamp Palace - Hookshot Pot Key',
|
||||
'Swamp Palace - Trench 2 Pot Key'], ['Swamp Palace (North)', 'Swamp Palace (West)']),
|
||||
create_dungeon_region(multiworld, player, 'Swamp Palace (West)', 'Swamp Palace', ['Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest']),
|
||||
create_dungeon_region(multiworld, player, 'Swamp Palace (North)', 'Swamp Palace', ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right',
|
||||
create_dungeon_region(world, player, 'Swamp Palace (West)', 'Swamp Palace', ['Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest']),
|
||||
create_dungeon_region(world, player, 'Swamp Palace (North)', 'Swamp Palace', ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right',
|
||||
'Swamp Palace - Waterway Pot Key', 'Swamp Palace - Waterfall Room',
|
||||
'Swamp Palace - Boss', 'Swamp Palace - Prize']),
|
||||
create_dungeon_region(multiworld, player, 'Thieves Town (Entrance)', 'Thieves\' Town', ['Thieves\' Town - Big Key Chest',
|
||||
create_dungeon_region(world, player, 'Thieves Town (Entrance)', 'Thieves\' Town', ['Thieves\' Town - Big Key Chest',
|
||||
'Thieves\' Town - Map Chest',
|
||||
'Thieves\' Town - Compass Chest',
|
||||
'Thieves\' Town - Ambush Chest'], ['Thieves Town Big Key Door', 'Thieves Town Exit']),
|
||||
create_dungeon_region(multiworld, player, 'Thieves Town (Deep)', 'Thieves\' Town', ['Thieves\' Town - Attic',
|
||||
create_dungeon_region(world, player, 'Thieves Town (Deep)', 'Thieves\' Town', ['Thieves\' Town - Attic',
|
||||
'Thieves\' Town - Big Chest',
|
||||
'Thieves\' Town - Hallway Pot Key',
|
||||
'Thieves\' Town - Spike Switch Pot Key',
|
||||
'Thieves\' Town - Blind\'s Cell'],
|
||||
['Blind Fight']),
|
||||
create_dungeon_region(multiworld, player, 'Blind Fight', 'Thieves\' Town', ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']),
|
||||
create_dungeon_region(multiworld, player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'], ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump', 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']),
|
||||
create_dungeon_region(multiworld, player, 'Skull Woods First Section (Right)', 'Skull Woods', ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']),
|
||||
create_dungeon_region(multiworld, player, 'Skull Woods First Section (Left)', 'Skull Woods', ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'], ['Skull Woods First Section (Left) Door to Exit', 'Skull Woods First Section (Left) Door to Right']),
|
||||
create_dungeon_region(multiworld, player, 'Skull Woods First Section (Top)', 'Skull Woods', ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']),
|
||||
create_dungeon_region(multiworld, player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None, ['Skull Woods Second Section (Drop)']),
|
||||
create_dungeon_region(multiworld, player, 'Skull Woods Second Section', 'Skull Woods', ['Skull Woods - Big Key Chest', 'Skull Woods - West Lobby Pot Key'], ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']),
|
||||
create_dungeon_region(multiworld, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', ['Skull Woods - Bridge Room'], ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']),
|
||||
create_dungeon_region(multiworld, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', ['Skull Woods - Spike Corner Key Drop', 'Skull Woods - Boss', 'Skull Woods - Prize']),
|
||||
create_dungeon_region(multiworld, player, 'Ice Palace (Entrance)', 'Ice Palace', ['Ice Palace - Jelly Key Drop', 'Ice Palace - Compass Chest'], ['Ice Palace (Second Section)', 'Ice Palace Exit']),
|
||||
create_dungeon_region(multiworld, player, 'Ice Palace (Second Section)', 'Ice Palace', ['Ice Palace - Conveyor Key Drop'], ['Ice Palace (Main)']),
|
||||
create_dungeon_region(multiworld, player, 'Ice Palace (Main)', 'Ice Palace', ['Ice Palace - Freezor Chest',
|
||||
create_dungeon_region(world, player, 'Blind Fight', 'Thieves\' Town', ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']),
|
||||
create_dungeon_region(world, player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'], ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump', 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']),
|
||||
create_dungeon_region(world, player, 'Skull Woods First Section (Right)', 'Skull Woods', ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']),
|
||||
create_dungeon_region(world, player, 'Skull Woods First Section (Left)', 'Skull Woods', ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'], ['Skull Woods First Section (Left) Door to Exit', 'Skull Woods First Section (Left) Door to Right']),
|
||||
create_dungeon_region(world, player, 'Skull Woods First Section (Top)', 'Skull Woods', ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']),
|
||||
create_dungeon_region(world, player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None, ['Skull Woods Second Section (Drop)']),
|
||||
create_dungeon_region(world, player, 'Skull Woods Second Section', 'Skull Woods', ['Skull Woods - Big Key Chest', 'Skull Woods - West Lobby Pot Key'], ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']),
|
||||
create_dungeon_region(world, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', ['Skull Woods - Bridge Room'], ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']),
|
||||
create_dungeon_region(world, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', ['Skull Woods - Spike Corner Key Drop', 'Skull Woods - Boss', 'Skull Woods - Prize']),
|
||||
create_dungeon_region(world, player, 'Ice Palace (Entrance)', 'Ice Palace', ['Ice Palace - Jelly Key Drop', 'Ice Palace - Compass Chest'], ['Ice Palace (Second Section)', 'Ice Palace Exit']),
|
||||
create_dungeon_region(world, player, 'Ice Palace (Second Section)', 'Ice Palace', ['Ice Palace - Conveyor Key Drop'], ['Ice Palace (Main)']),
|
||||
create_dungeon_region(world, player, 'Ice Palace (Main)', 'Ice Palace', ['Ice Palace - Freezor Chest',
|
||||
'Ice Palace - Many Pots Pot Key',
|
||||
'Ice Palace - Big Chest', 'Ice Palace - Iced T Room'], ['Ice Palace (East)', 'Ice Palace (Kholdstare)']),
|
||||
create_dungeon_region(multiworld, player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'], ['Ice Palace (East Top)']),
|
||||
create_dungeon_region(multiworld, player, 'Ice Palace (East Top)', 'Ice Palace', ['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest', 'Ice Palace - Hammer Block Key Drop']),
|
||||
create_dungeon_region(multiworld, player, 'Ice Palace (Kholdstare)', 'Ice Palace', ['Ice Palace - Boss', 'Ice Palace - Prize']),
|
||||
create_dungeon_region(multiworld, player, 'Misery Mire (Entrance)', 'Misery Mire', None, ['Misery Mire Entrance Gap', 'Misery Mire Exit']),
|
||||
create_dungeon_region(multiworld, player, 'Misery Mire (Main)', 'Misery Mire', ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby',
|
||||
create_dungeon_region(world, player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'], ['Ice Palace (East Top)']),
|
||||
create_dungeon_region(world, player, 'Ice Palace (East Top)', 'Ice Palace', ['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest', 'Ice Palace - Hammer Block Key Drop']),
|
||||
create_dungeon_region(world, player, 'Ice Palace (Kholdstare)', 'Ice Palace', ['Ice Palace - Boss', 'Ice Palace - Prize']),
|
||||
create_dungeon_region(world, player, 'Misery Mire (Entrance)', 'Misery Mire', None, ['Misery Mire Entrance Gap', 'Misery Mire Exit']),
|
||||
create_dungeon_region(world, player, 'Misery Mire (Main)', 'Misery Mire', ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby',
|
||||
'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest',
|
||||
'Misery Mire - Spikes Pot Key', 'Misery Mire - Fishbone Pot Key',
|
||||
'Misery Mire - Conveyor Crystal Key Drop'], ['Misery Mire (West)', 'Misery Mire Big Key Door']),
|
||||
create_dungeon_region(multiworld, player, 'Misery Mire (West)', 'Misery Mire', ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']),
|
||||
create_dungeon_region(multiworld, player, 'Misery Mire (Final Area)', 'Misery Mire', None, ['Misery Mire (Vitreous)']),
|
||||
create_dungeon_region(multiworld, player, 'Misery Mire (Vitreous)', 'Misery Mire', ['Misery Mire - Boss', 'Misery Mire - Prize']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Entrance)', 'Turtle Rock', None, ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (First Section)', 'Turtle Rock', ['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left',
|
||||
create_dungeon_region(world, player, 'Misery Mire (West)', 'Misery Mire', ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']),
|
||||
create_dungeon_region(world, player, 'Misery Mire (Final Area)', 'Misery Mire', None, ['Misery Mire (Vitreous)']),
|
||||
create_dungeon_region(world, player, 'Misery Mire (Vitreous)', 'Misery Mire', ['Misery Mire - Boss', 'Misery Mire - Prize']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Entrance)', 'Turtle Rock', None, ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (First Section)', 'Turtle Rock', ['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left',
|
||||
'Turtle Rock - Roller Room - Right'],
|
||||
['Turtle Rock Entrance to Pokey Room', 'Turtle Rock Entrance Gap Reverse']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Pokey Room)', 'Turtle Rock', ['Turtle Rock - Pokey 1 Key Drop'], ['Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Pokey Room) (South)']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock', ['Turtle Rock - Chain Chomps'],
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Pokey Room)', 'Turtle Rock', ['Turtle Rock - Pokey 1 Key Drop'], ['Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Pokey Room) (South)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock', ['Turtle Rock - Chain Chomps'],
|
||||
['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Second Section)', 'Turtle Rock',
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock',
|
||||
['Turtle Rock - Big Key Chest', 'Turtle Rock - Pokey 2 Key Drop'],
|
||||
['Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door',
|
||||
'Turtle Rock Second Section Bomb Wall']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Second Section Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Second Section from Bomb Wall']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Eye Bridge Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Isolated Ledge Exit', 'Turtle Rock Eye Bridge from Bomb Wall']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right',
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Second Section Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Second Section from Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Isolated Ledge Exit', 'Turtle Rock Eye Bridge from Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['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 Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Eye Bridge Bomb Wall']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']),
|
||||
create_dungeon_region(multiworld, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']),
|
||||
create_dungeon_region(multiworld, player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'],
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'],
|
||||
['Palace of Darkness Big Key Chest Staircase', 'Palace of Darkness (North)', 'Palace of Darkness Big Key Door']),
|
||||
create_dungeon_region(multiworld, player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness', ['Palace of Darkness - Big Key Chest']),
|
||||
create_dungeon_region(multiworld, player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'], ['Palace of Darkness Hammer Peg Drop']),
|
||||
create_dungeon_region(multiworld, player, 'Palace of Darkness (North)', 'Palace of Darkness', ['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Basement - Right'],
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness', ['Palace of Darkness - Big Key Chest']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'], ['Palace of Darkness Hammer Peg Drop']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (North)', 'Palace of Darkness', ['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Basement - Right'],
|
||||
['Palace of Darkness Spike Statue Room Door', 'Palace of Darkness Maze Door']),
|
||||
create_dungeon_region(multiworld, player, 'Palace of Darkness (Maze)', 'Palace of Darkness', ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Big Chest']),
|
||||
create_dungeon_region(multiworld, player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness', ['Palace of Darkness - Harmless Hellway']),
|
||||
create_dungeon_region(multiworld, player, 'Palace of Darkness (Final Section)', 'Palace of Darkness', ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']),
|
||||
create_dungeon_region(multiworld, player, 'Inverted Ganons Tower (Entrance)', 'Ganon\'s Tower',
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Maze)', 'Palace of Darkness', ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Big Chest']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness', ['Palace of Darkness - Harmless Hellway']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Final Section)', 'Palace of Darkness', ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']),
|
||||
create_dungeon_region(world, player, 'Inverted Ganons Tower (Entrance)', 'Ganon\'s Tower',
|
||||
['Ganons Tower - Bob\'s Torch', 'Ganons Tower - Hope Room - Left',
|
||||
'Ganons Tower - Hope Room - Right', 'Ganons Tower - Conveyor Cross Pot Key'],
|
||||
['Ganons Tower (Tile Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower Big Key Door',
|
||||
'Inverted Ganons Tower Exit']),
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Tile Room)', 'Ganon\'s Tower', ['Ganons Tower - Tile Room'],
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Tile Room)', 'Ganon\'s Tower', ['Ganons Tower - Tile Room'],
|
||||
['Ganons Tower (Tile Room) Key Door']),
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Compass Room)', 'Ganon\'s Tower',
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Compass Room)', 'Ganon\'s Tower',
|
||||
['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right',
|
||||
'Ganons Tower - Compass Room - Bottom Left',
|
||||
'Ganons Tower - Compass Room - Bottom Right',
|
||||
'Ganons Tower - Conveyor Star Pits Pot Key'],
|
||||
['Ganons Tower (Bottom) (East)']),
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Hookshot Room)', 'Ganon\'s Tower',
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Hookshot Room)', 'Ganon\'s Tower',
|
||||
['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 - Double Switch Pot Key'],
|
||||
['Ganons Tower (Map Room)', 'Ganons Tower (Double Switch Room)']),
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Map Room)', 'Ganon\'s Tower', ['Ganons Tower - Map Chest']),
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Firesnake Room)', 'Ganon\'s Tower',
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Map Room)', 'Ganon\'s Tower', ['Ganons Tower - Map Chest']),
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Firesnake Room)', 'Ganon\'s Tower',
|
||||
['Ganons Tower - Firesnake Room'], ['Ganons Tower (Firesnake Room)']),
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Teleport Room)', 'Ganon\'s Tower',
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Teleport Room)', 'Ganon\'s Tower',
|
||||
['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 (Bottom) (West)']),
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Bottom)', 'Ganon\'s Tower',
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Bottom)', 'Ganon\'s Tower',
|
||||
['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']),
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None, ['Ganons Tower Torch Rooms']),
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Before Moldorm)', 'Ganon\'s Tower',
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None, ['Ganons Tower Torch Rooms']),
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Before Moldorm)', 'Ganon\'s Tower',
|
||||
['Ganons Tower - Mini Helmasaur Room - Left',
|
||||
'Ganons Tower - Mini Helmasaur Room - Right',
|
||||
'Ganons Tower - Pre-Moldorm Chest', 'Ganons Tower - Mini Helmasaur Key Drop'],
|
||||
['Ganons Tower Moldorm Door']),
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None, ['Ganons Tower Moldorm Gap']),
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None, ['Ganons Tower Moldorm Gap']),
|
||||
|
||||
create_dungeon_region(multiworld, player, 'Agahnim 2', 'Ganon\'s Tower', ['Ganons Tower - Validation Chest', 'Agahnim 2'], None),
|
||||
create_cave_region(multiworld, player, 'Pyramid', 'a drop\'s exit', ['Ganon'], ['Ganon Drop']),
|
||||
create_cave_region(multiworld, player, 'Bottom of Pyramid', 'a drop\'s exit', None, ['Pyramid Exit']),
|
||||
create_dw_region(multiworld, player, 'Pyramid Ledge', None, ['Pyramid Drop']), # houlihan room exits here in inverted
|
||||
create_dungeon_region(world, player, 'Agahnim 2', 'Ganon\'s Tower', ['Ganons Tower - Validation Chest', 'Agahnim 2'], None),
|
||||
create_cave_region(world, player, 'Pyramid', 'a drop\'s exit', ['Ganon'], ['Ganon Drop']),
|
||||
create_cave_region(world, player, 'Bottom of Pyramid', 'a drop\'s exit', None, ['Pyramid Exit']),
|
||||
create_dw_region(world, player, 'Pyramid Ledge', None, ['Pyramid Drop']), # houlihan room exits here in inverted
|
||||
|
||||
# to simplify flute connections
|
||||
create_cave_region(multiworld, player, 'The Sky', 'A Dark Sky', None,
|
||||
create_cave_region(world, player, 'The Sky', 'A Dark Sky', None,
|
||||
['DDM Landing', 'NEDW Landing', 'WDW Landing', 'SDW Landing', 'EDW Landing', 'DD Landing',
|
||||
'DLHL Landing']),
|
||||
|
||||
create_lw_region(multiworld, player, 'Desert Northern Cliffs'),
|
||||
create_lw_region(multiworld, player, 'Death Mountain Bunny Descent Area')
|
||||
create_lw_region(world, player, 'Desert Northern Cliffs'),
|
||||
create_lw_region(world, player, 'Death Mountain Bunny Descent Area')
|
||||
]
|
||||
|
||||
|
||||
def mark_dark_world_regions(multiworld: MultiWorld, player: int):
|
||||
def mark_dark_world_regions(world, player):
|
||||
# cross world caves may have some sections marked as both in_light_world, and in_dark_work.
|
||||
# That is ok. the bunny logic will check for this case and incorporate special rules.
|
||||
queue = collections.deque(region for region in multiworld.get_regions(player) if region.type == LTTPRegionType.DarkWorld)
|
||||
queue = collections.deque(region for region in world.get_regions(player) if region.type == LTTPRegionType.DarkWorld)
|
||||
seen = set(queue)
|
||||
while queue:
|
||||
current = queue.popleft()
|
||||
@@ -500,7 +499,7 @@ def mark_dark_world_regions(multiworld: MultiWorld, player: int):
|
||||
seen.add(exit.connected_region)
|
||||
queue.append(exit.connected_region)
|
||||
|
||||
queue = collections.deque(region for region in multiworld.get_regions(player) if region.type == LTTPRegionType.LightWorld)
|
||||
queue = collections.deque(region for region in world.get_regions(player) if region.type == LTTPRegionType.LightWorld)
|
||||
seen = set(queue)
|
||||
while queue:
|
||||
current = queue.popleft()
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from collections import namedtuple
|
||||
import logging
|
||||
|
||||
from BaseClasses import ItemClassification, MultiWorld
|
||||
from BaseClasses import ItemClassification
|
||||
from Options import OptionError
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .SubClasses import ALttPLocation, LTTPRegion, LTTPRegionType
|
||||
from .Shops import TakeAny, total_shop_slots, set_up_shops, shop_table_by_location, ShopType
|
||||
@@ -15,9 +14,6 @@ from .Options import small_key_shuffle, compass_shuffle, big_key_shuffle, map_sh
|
||||
from .StateHelpers import has_triforce_pieces, has_melee_weapon
|
||||
from .Regions import key_drop_data
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import ALTTPWorld
|
||||
|
||||
# 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.
|
||||
|
||||
@@ -226,7 +222,7 @@ items_reduction_table = (
|
||||
)
|
||||
|
||||
|
||||
def generate_itempool(world: "ALTTPWorld"):
|
||||
def generate_itempool(world):
|
||||
player: int = world.player
|
||||
multiworld = world.multiworld
|
||||
|
||||
@@ -535,7 +531,7 @@ take_any_locations_inverted.sort()
|
||||
take_any_locations.sort()
|
||||
|
||||
|
||||
def set_up_take_anys(multiworld: MultiWorld, world: "ALTTPWorld", player: int):
|
||||
def set_up_take_anys(multiworld, world, player):
|
||||
# these are references, do not modify these lists in-place
|
||||
if world.options.mode == 'inverted':
|
||||
take_any_locs = take_any_locations_inverted
|
||||
@@ -589,15 +585,15 @@ def set_up_take_anys(multiworld: MultiWorld, world: "ALTTPWorld", player: int):
|
||||
location.place_locked_item(item_factory("Boss Heart Container", world))
|
||||
|
||||
|
||||
def get_pool_core(multiworld: MultiWorld, player: int):
|
||||
shuffle = multiworld.worlds[player].options.entrance_shuffle.current_key
|
||||
difficulty = multiworld.worlds[player].options.item_pool.current_key
|
||||
timer = multiworld.worlds[player].options.timer.current_key
|
||||
goal = multiworld.worlds[player].options.goal.current_key
|
||||
mode = multiworld.worlds[player].options.mode.current_key
|
||||
swordless = multiworld.worlds[player].options.swordless
|
||||
retro_bow = multiworld.worlds[player].options.retro_bow
|
||||
logic = multiworld.worlds[player].options.glitches_required
|
||||
def get_pool_core(world, player: int):
|
||||
shuffle = world.worlds[player].options.entrance_shuffle.current_key
|
||||
difficulty = world.worlds[player].options.item_pool.current_key
|
||||
timer = world.worlds[player].options.timer.current_key
|
||||
goal = world.worlds[player].options.goal.current_key
|
||||
mode = world.worlds[player].options.mode.current_key
|
||||
swordless = world.worlds[player].options.swordless
|
||||
retro_bow = world.worlds[player].options.retro_bow
|
||||
logic = world.worlds[player].options.glitches_required
|
||||
|
||||
pool = []
|
||||
placed_items = {}
|
||||
@@ -614,13 +610,13 @@ def get_pool_core(multiworld: MultiWorld, player: int):
|
||||
placed_items[loc] = item
|
||||
|
||||
# provide boots to major glitch dependent seeds
|
||||
if logic.current_key in {'overworld_glitches', 'hybrid_major_glitches', 'no_logic'} and multiworld.worlds[player].options.glitch_boots:
|
||||
if logic.current_key in {'overworld_glitches', 'hybrid_major_glitches', 'no_logic'} and world.worlds[player].options.glitch_boots:
|
||||
precollected_items.append('Pegasus Boots')
|
||||
pool.remove('Pegasus Boots')
|
||||
pool.append('Rupees (20)')
|
||||
want_progressives = multiworld.worlds[player].options.progressive.want_progressives
|
||||
want_progressives = world.worlds[player].options.progressive.want_progressives
|
||||
|
||||
if want_progressives(multiworld.random):
|
||||
if want_progressives(world.random):
|
||||
pool.extend(diff.progressiveglove)
|
||||
else:
|
||||
pool.extend(diff.basicglove)
|
||||
@@ -644,27 +640,27 @@ def get_pool_core(multiworld: MultiWorld, player: int):
|
||||
thisbottle = None
|
||||
for _ in range(diff.bottle_count):
|
||||
if not diff.same_bottle or not thisbottle:
|
||||
thisbottle = multiworld.random.choice(diff.bottles)
|
||||
thisbottle = world.random.choice(diff.bottles)
|
||||
pool.append(thisbottle)
|
||||
|
||||
if want_progressives(multiworld.random):
|
||||
if want_progressives(world.random):
|
||||
pool.extend(diff.progressiveshield)
|
||||
else:
|
||||
pool.extend(diff.basicshield)
|
||||
|
||||
if want_progressives(multiworld.random):
|
||||
if want_progressives(world.random):
|
||||
pool.extend(diff.progressivearmor)
|
||||
else:
|
||||
pool.extend(diff.basicarmor)
|
||||
|
||||
if want_progressives(multiworld.random):
|
||||
if want_progressives(world.random):
|
||||
pool.extend(diff.progressivemagic)
|
||||
else:
|
||||
pool.extend(diff.basicmagic)
|
||||
|
||||
if want_progressives(multiworld.random):
|
||||
if want_progressives(world.random):
|
||||
pool.extend(diff.progressivebow)
|
||||
multiworld.worlds[player].has_progressive_bows = True
|
||||
world.worlds[player].has_progressive_bows = True
|
||||
elif (swordless or logic == 'no_glitches'):
|
||||
swordless_bows = ['Bow', 'Silver Bow']
|
||||
if difficulty == "easy":
|
||||
@@ -676,7 +672,7 @@ def get_pool_core(multiworld: MultiWorld, player: int):
|
||||
if swordless:
|
||||
pool.extend(diff.swordless)
|
||||
else:
|
||||
progressive_swords = want_progressives(multiworld.random)
|
||||
progressive_swords = want_progressives(world.random)
|
||||
pool.extend(diff.progressivesword if progressive_swords else diff.basicsword)
|
||||
|
||||
extraitems = total_items_to_place - len(pool) - len(placed_items)
|
||||
@@ -692,29 +688,29 @@ def get_pool_core(multiworld: MultiWorld, player: int):
|
||||
additional_pieces_to_place = 0
|
||||
if 'triforce_hunt' in goal:
|
||||
|
||||
if multiworld.worlds[player].options.triforce_pieces_mode.value == TriforcePiecesMode.option_extra:
|
||||
treasure_hunt_total = (multiworld.worlds[player].options.triforce_pieces_required.value
|
||||
+ multiworld.worlds[player].options.triforce_pieces_extra.value)
|
||||
elif multiworld.worlds[player].options.triforce_pieces_mode.value == TriforcePiecesMode.option_percentage:
|
||||
percentage = float(multiworld.worlds[player].options.triforce_pieces_percentage.value) / 100
|
||||
treasure_hunt_total = int(round(multiworld.worlds[player].options.triforce_pieces_required.value * percentage, 0))
|
||||
if world.worlds[player].options.triforce_pieces_mode.value == TriforcePiecesMode.option_extra:
|
||||
treasure_hunt_total = (world.worlds[player].options.triforce_pieces_required.value
|
||||
+ world.worlds[player].options.triforce_pieces_extra.value)
|
||||
elif world.worlds[player].options.triforce_pieces_mode.value == TriforcePiecesMode.option_percentage:
|
||||
percentage = float(world.worlds[player].options.triforce_pieces_percentage.value) / 100
|
||||
treasure_hunt_total = int(round(world.worlds[player].options.triforce_pieces_required.value * percentage, 0))
|
||||
else: # available
|
||||
treasure_hunt_total = multiworld.worlds[player].options.triforce_pieces_available.value
|
||||
treasure_hunt_total = world.worlds[player].options.triforce_pieces_available.value
|
||||
|
||||
triforce_pieces = min(90, max(treasure_hunt_total, multiworld.worlds[player].options.triforce_pieces_required.value))
|
||||
triforce_pieces = min(90, max(treasure_hunt_total, world.worlds[player].options.triforce_pieces_required.value))
|
||||
|
||||
pieces_in_core = min(extraitems, triforce_pieces)
|
||||
additional_pieces_to_place = triforce_pieces - pieces_in_core
|
||||
pool.extend(["Triforce Piece"] * pieces_in_core)
|
||||
extraitems -= pieces_in_core
|
||||
treasure_hunt_required = multiworld.worlds[player].options.triforce_pieces_required.value
|
||||
treasure_hunt_required = world.worlds[player].options.triforce_pieces_required.value
|
||||
|
||||
for extra in diff.extras:
|
||||
if extraitems >= len(extra):
|
||||
pool.extend(extra)
|
||||
extraitems -= len(extra)
|
||||
elif extraitems > 0:
|
||||
pool.extend(multiworld.random.sample(extra, extraitems))
|
||||
pool.extend(world.random.sample(extra, extraitems))
|
||||
break
|
||||
else:
|
||||
break
|
||||
@@ -733,25 +729,25 @@ def get_pool_core(multiworld: MultiWorld, player: int):
|
||||
else:
|
||||
break
|
||||
|
||||
if multiworld.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal:
|
||||
if world.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal:
|
||||
pool.extend(diff.universal_keys)
|
||||
if mode == 'standard':
|
||||
if multiworld.worlds[player].options.key_drop_shuffle:
|
||||
if world.worlds[player].options.key_drop_shuffle:
|
||||
key_locations = ['Secret Passage', 'Hyrule Castle - Map Guard Key Drop']
|
||||
key_location = multiworld.random.choice(key_locations)
|
||||
key_location = world.random.choice(key_locations)
|
||||
key_locations.remove(key_location)
|
||||
place_item(key_location, "Small Key (Universal)")
|
||||
key_locations += ['Hyrule Castle - Boomerang Guard Key Drop', 'Hyrule Castle - Boomerang Chest',
|
||||
'Hyrule Castle - Map Chest']
|
||||
key_location = multiworld.random.choice(key_locations)
|
||||
key_location = world.random.choice(key_locations)
|
||||
key_locations.remove(key_location)
|
||||
place_item(key_location, "Small Key (Universal)")
|
||||
key_locations += ['Hyrule Castle - Big Key Drop', 'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross']
|
||||
key_location = multiworld.random.choice(key_locations)
|
||||
key_location = world.random.choice(key_locations)
|
||||
key_locations.remove(key_location)
|
||||
place_item(key_location, "Small Key (Universal)")
|
||||
key_locations += ['Sewers - Key Rat Key Drop']
|
||||
key_location = multiworld.random.choice(key_locations)
|
||||
key_location = world.random.choice(key_locations)
|
||||
place_item(key_location, "Small Key (Universal)")
|
||||
pool = pool[:-3]
|
||||
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import typing
|
||||
|
||||
from BaseClasses import MultiWorld, ItemClassification as IC
|
||||
from BaseClasses import ItemClassification as IC
|
||||
from worlds.AutoWorld import World
|
||||
|
||||
|
||||
def GetBeemizerItem(multiworld: MultiWorld, player: int, item):
|
||||
def GetBeemizerItem(world, player: int, item):
|
||||
item_name = item if isinstance(item, str) else item.name
|
||||
|
||||
if item_name not in trap_replaceable or player in multiworld.groups:
|
||||
if item_name not in trap_replaceable or player in world.groups:
|
||||
return item
|
||||
|
||||
# first roll - replaceable item should be replaced, within beemizer_total_chance
|
||||
if not multiworld.worlds[player].options.beemizer_total_chance or multiworld.random.random() > (multiworld.worlds[player].options.beemizer_total_chance / 100):
|
||||
if not world.worlds[player].options.beemizer_total_chance or world.random.random() > (world.worlds[player].options.beemizer_total_chance / 100):
|
||||
return item
|
||||
|
||||
# second roll - bee replacement should be trap, within beemizer_trap_chance
|
||||
if not multiworld.worlds[player].options.beemizer_trap_chance or multiworld.random.random() > (multiworld.worlds[player].options.beemizer_trap_chance / 100):
|
||||
return "Bee" if isinstance(item, str) else multiworld.create_item("Bee", player)
|
||||
if not world.worlds[player].options.beemizer_trap_chance or world.random.random() > (world.worlds[player].options.beemizer_trap_chance / 100):
|
||||
return "Bee" if isinstance(item, str) else world.create_item("Bee", player)
|
||||
else:
|
||||
return "Bee Trap" if isinstance(item, str) else multiworld.create_item("Bee Trap", player)
|
||||
return "Bee Trap" if isinstance(item, str) else world.create_item("Bee Trap", player)
|
||||
|
||||
|
||||
def item_factory(items: typing.Union[str, typing.Iterable[str]], world: World):
|
||||
|
||||
@@ -154,13 +154,13 @@ class OpenPyramid(Choice):
|
||||
alias_true = option_open
|
||||
alias_false = option_closed
|
||||
|
||||
def to_bool(self, multiworld: MultiWorld, player: int) -> bool:
|
||||
def to_bool(self, world: MultiWorld, player: int) -> bool:
|
||||
if self.value == self.option_goal:
|
||||
return multiworld.worlds[player].options.goal.current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'}
|
||||
return world.worlds[player].options.goal.current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'}
|
||||
elif self.value == self.option_auto:
|
||||
return multiworld.worlds[player].options.goal.current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'} \
|
||||
and (multiworld.worlds[player].options.entrance_shuffle.current_key in {'vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed'} or not
|
||||
multiworld.shuffle_ganon)
|
||||
return world.worlds[player].options.goal.current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'} \
|
||||
and (world.worlds[player].options.entrance_shuffle.current_key in {'vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed'} or not
|
||||
world.shuffle_ganon)
|
||||
elif self.value == self.option_open:
|
||||
return True
|
||||
else:
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
Helper functions to deliver entrance/exit/region sets to OWG rules.
|
||||
"""
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from .StateHelpers import can_lift_heavy_rocks, can_boots_clip_lw, can_boots_clip_dw, can_get_glitched_speed_dw
|
||||
|
||||
|
||||
@@ -201,7 +200,7 @@ def get_mirror_offset_spots_dw():
|
||||
yield ('Dark Death Mountain Offset Mirror', 'Dark Death Mountain (West Bottom)', 'East Dark World')
|
||||
|
||||
|
||||
def get_mirror_offset_spots_lw(player: int):
|
||||
def get_mirror_offset_spots_lw(player):
|
||||
"""
|
||||
Mirror shenanigans placing a mirror portal with a broken camera
|
||||
"""
|
||||
@@ -219,54 +218,54 @@ def get_invalid_bunny_revival_dungeons():
|
||||
yield 'Sanctuary'
|
||||
|
||||
|
||||
def overworld_glitch_connections(multiworld: MultiWorld, player: int):
|
||||
def overworld_glitch_connections(world, player):
|
||||
# Boots-accessible locations.
|
||||
create_owg_connections(player, multiworld, get_boots_clip_exits_lw(multiworld.worlds[player].options.mode == 'inverted'))
|
||||
create_owg_connections(player, multiworld, get_boots_clip_exits_dw(multiworld.worlds[player].options.mode == 'inverted', player))
|
||||
create_owg_connections(player, world, get_boots_clip_exits_lw(world.worlds[player].options.mode == 'inverted'))
|
||||
create_owg_connections(player, world, get_boots_clip_exits_dw(world.worlds[player].options.mode == 'inverted', player))
|
||||
|
||||
# Glitched speed drops.
|
||||
create_owg_connections(player, multiworld, get_glitched_speed_drops_dw(multiworld.worlds[player].options.mode == 'inverted'))
|
||||
create_owg_connections(player, world, get_glitched_speed_drops_dw(world.worlds[player].options.mode == 'inverted'))
|
||||
|
||||
# Mirror clip spots.
|
||||
if multiworld.worlds[player].options.mode != 'inverted':
|
||||
create_owg_connections(player, multiworld, get_mirror_clip_spots_dw())
|
||||
create_owg_connections(player, multiworld, get_mirror_offset_spots_dw())
|
||||
if world.worlds[player].options.mode != 'inverted':
|
||||
create_owg_connections(player, world, get_mirror_clip_spots_dw())
|
||||
create_owg_connections(player, world, get_mirror_offset_spots_dw())
|
||||
else:
|
||||
create_owg_connections(player, multiworld, get_mirror_offset_spots_lw(player))
|
||||
create_owg_connections(player, world, get_mirror_offset_spots_lw(player))
|
||||
|
||||
|
||||
def overworld_glitches_rules(multiworld: MultiWorld, player: int):
|
||||
def overworld_glitches_rules(world, player):
|
||||
|
||||
# Boots-accessible locations.
|
||||
set_owg_connection_rules(player, multiworld, get_boots_clip_exits_lw(multiworld.worlds[player].options.mode == 'inverted'), lambda state: can_boots_clip_lw(state, player))
|
||||
set_owg_connection_rules(player, multiworld, get_boots_clip_exits_dw(multiworld.worlds[player].options.mode == 'inverted', player), lambda state: can_boots_clip_dw(state, player))
|
||||
set_owg_connection_rules(player, world, get_boots_clip_exits_lw(world.worlds[player].options.mode == 'inverted'), lambda state: can_boots_clip_lw(state, player))
|
||||
set_owg_connection_rules(player, world, get_boots_clip_exits_dw(world.worlds[player].options.mode == 'inverted', player), lambda state: can_boots_clip_dw(state, player))
|
||||
|
||||
# Glitched speed drops.
|
||||
set_owg_connection_rules(player, multiworld, get_glitched_speed_drops_dw(multiworld.worlds[player].options.mode == 'inverted'), lambda state: can_get_glitched_speed_dw(state, player))
|
||||
set_owg_connection_rules(player, world, get_glitched_speed_drops_dw(world.worlds[player].options.mode == 'inverted'), lambda state: can_get_glitched_speed_dw(state, player))
|
||||
# Dark Death Mountain Ledge Clip Spot also accessible with mirror.
|
||||
if multiworld.worlds[player].options.mode != 'inverted':
|
||||
add_alternate_rule(multiworld.get_entrance('Dark Death Mountain Ledge Clip Spot', player), lambda state: state.has('Magic Mirror', player))
|
||||
if world.worlds[player].options.mode != 'inverted':
|
||||
add_alternate_rule(world.get_entrance('Dark Death Mountain Ledge Clip Spot', player), lambda state: state.has('Magic Mirror', player))
|
||||
|
||||
# Mirror clip spots.
|
||||
if multiworld.worlds[player].options.mode != 'inverted':
|
||||
set_owg_connection_rules(player, multiworld, get_mirror_clip_spots_dw(), lambda state: state.has('Magic Mirror', player))
|
||||
set_owg_connection_rules(player, multiworld, get_mirror_offset_spots_dw(), lambda state: state.has('Magic Mirror', player) and can_boots_clip_lw(state, player))
|
||||
if world.worlds[player].options.mode != '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 can_boots_clip_lw(state, player))
|
||||
else:
|
||||
set_owg_connection_rules(player, multiworld, get_mirror_offset_spots_lw(player), lambda state: state.has('Magic Mirror', player) and can_boots_clip_dw(state, 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 multiworld.worlds[player].options.mode != 'inverted':
|
||||
multiworld.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(multiworld.get_entrance('Waterfall of Wishing', player), lambda state: state.has('Moon Pearl', player) or state.has('Pegasus Boots', player))
|
||||
if world.worlds[player].options.mode != 'inverted':
|
||||
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(multiworld.get_entrance('Waterfall of Wishing Cave', player), lambda state: state.has('Moon Pearl', player))
|
||||
add_alternate_rule(world.get_entrance('Waterfall of Wishing Cave', player), lambda state: state.has('Moon Pearl', player))
|
||||
|
||||
multiworld.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(multiworld.get_entrance('Catfish Exit Rock', player), lambda state: can_boots_clip_dw(state, player))
|
||||
add_alternate_rule(multiworld.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: can_boots_clip_dw(state, 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(multiworld.get_location('Zora\'s Ledge', player), lambda state: state.has('Pegasus Boots', player))
|
||||
add_alternate_rule(world.get_location('Zora\'s Ledge', player), lambda state: state.has('Pegasus Boots', player))
|
||||
|
||||
|
||||
def add_alternate_rule(entrance, rule):
|
||||
@@ -274,22 +273,22 @@ def add_alternate_rule(entrance, rule):
|
||||
entrance.access_rule = lambda state: old_rule(state) or rule(state)
|
||||
|
||||
|
||||
def create_no_logic_connections(player: int, multiworld: MultiWorld, connections):
|
||||
def create_no_logic_connections(player, world, connections):
|
||||
for entrance, parent_region, target_region, *rule_override in connections:
|
||||
parent = multiworld.get_region(parent_region, player)
|
||||
target = multiworld.get_region(target_region, player)
|
||||
parent = world.get_region(parent_region, player)
|
||||
target = world.get_region(target_region, player)
|
||||
parent.connect(target, entrance)
|
||||
|
||||
|
||||
def create_owg_connections(player: int, multiworld: MultiWorld, connections):
|
||||
def create_owg_connections(player, world, connections):
|
||||
for entrance, parent_region, target_region, *rule_override in connections:
|
||||
parent = multiworld.get_region(parent_region, player)
|
||||
target = multiworld.get_region(target_region, player)
|
||||
parent = world.get_region(parent_region, player)
|
||||
target = world.get_region(target_region, player)
|
||||
parent.connect(target, entrance)
|
||||
|
||||
|
||||
def set_owg_connection_rules(player: int, multiworld: MultiWorld, connections, default_rule):
|
||||
def set_owg_connection_rules(player, world, connections, default_rule):
|
||||
for entrance, _, _, *rule_override in connections:
|
||||
connection = multiworld.get_entrance(entrance, player)
|
||||
connection = world.get_entrance(entrance, player)
|
||||
rule = rule_override[0] if len(rule_override) > 0 else default_rule
|
||||
connection.access_rule = rule
|
||||
|
||||
@@ -9,11 +9,11 @@ def is_main_entrance(entrance: LTTPEntrance) -> bool:
|
||||
return entrance.parent_region.type in {LTTPRegionType.DarkWorld, LTTPRegionType.LightWorld} if entrance.parent_region.type else True
|
||||
|
||||
|
||||
def create_regions(multiworld: MultiWorld, player: int):
|
||||
def create_regions(world, player):
|
||||
|
||||
multiworld.regions += [
|
||||
create_lw_region(multiworld, player, 'Menu', None, ['Links House S&Q', 'Sanctuary S&Q', 'Old Man S&Q']),
|
||||
create_lw_region(multiworld, player, 'Light World', ['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure',
|
||||
world.regions += [
|
||||
create_lw_region(world, player, 'Menu', None, ['Links House S&Q', 'Sanctuary S&Q', 'Old Man S&Q']),
|
||||
create_lw_region(world, player, 'Light World', ['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure',
|
||||
'Purple Chest', 'Flute Activation Spot'],
|
||||
["Blinds Hideout", "Hyrule Castle Secret Entrance Drop", 'Zoras River', 'Kings Grave Outer Rocks', 'Dam',
|
||||
'Links House', 'Tavern North', 'Chicken House', 'Aginahs Cave', 'Sahasrahlas Hut', 'Kakariko Well Drop', 'Kakariko Well Cave',
|
||||
@@ -24,122 +24,122 @@ def create_regions(multiworld: MultiWorld, player: int):
|
||||
'Elder House (East)', 'Elder House (West)', 'North Fairy Cave', 'North Fairy Cave Drop', 'Lost Woods Gamble', 'Snitch Lady (East)', 'Snitch Lady (West)', 'Tavern (Front)',
|
||||
'Bush Covered House', 'Light World Bomb Hut', 'Kakariko Shop', 'Long Fairy Cave', 'Good Bee Cave', '20 Rupee Cave', 'Cave Shop (Lake Hylia)', 'Waterfall of Wishing', 'Hyrule Castle Main Gate',
|
||||
'Bonk Fairy (Light)', '50 Rupee Cave', 'Fortune Teller (Light)', 'Lake Hylia Fairy', 'Light Hype Fairy', 'Desert Fairy', 'Lumberjack House', 'Lake Hylia Fortune Teller', 'Kakariko Gamble Game', 'Top of Pyramid']),
|
||||
create_lw_region(multiworld, player, 'Death Mountain Entrance', None, ['Old Man Cave (West)', 'Death Mountain Entrance Drop']),
|
||||
create_lw_region(multiworld, player, 'Lake Hylia Central Island', None, ['Capacity Upgrade', 'Lake Hylia Central Island Teleporter']),
|
||||
create_cave_region(multiworld, player, 'Blinds Hideout', 'a bounty of five items', ["Blind\'s Hideout - Top",
|
||||
create_lw_region(world, player, 'Death Mountain Entrance', None, ['Old Man Cave (West)', 'Death Mountain Entrance Drop']),
|
||||
create_lw_region(world, player, 'Lake Hylia Central Island', None, ['Capacity Upgrade', 'Lake Hylia Central Island Teleporter']),
|
||||
create_cave_region(world, player, 'Blinds Hideout', 'a bounty of five items', ["Blind\'s Hideout - Top",
|
||||
"Blind\'s Hideout - Left",
|
||||
"Blind\'s Hideout - Right",
|
||||
"Blind\'s Hideout - Far Left",
|
||||
"Blind\'s Hideout - Far Right"]),
|
||||
create_cave_region(multiworld, player, 'Hyrule Castle Secret Entrance', 'a drop\'s exit', ['Link\'s Uncle', 'Secret Passage'], ['Hyrule Castle Secret Entrance Exit']),
|
||||
create_lw_region(multiworld, player, 'Zoras River', ['King Zora', 'Zora\'s Ledge']),
|
||||
create_cave_region(multiworld, player, 'Waterfall of Wishing', 'a cave with two chests', ['Waterfall Fairy - Left', 'Waterfall Fairy - Right']),
|
||||
create_lw_region(multiworld, player, 'Kings Grave Area', None, ['Kings Grave', 'Kings Grave Inner Rocks']),
|
||||
create_cave_region(multiworld, player, 'Kings Grave', 'a cave with a chest', ['King\'s Tomb']),
|
||||
create_cave_region(multiworld, player, 'North Fairy Cave', 'a drop\'s exit', None, ['North Fairy Cave Exit']),
|
||||
create_cave_region(multiworld, player, 'Dam', 'the dam', ['Floodgate', 'Floodgate Chest']),
|
||||
create_cave_region(multiworld, player, 'Links House', 'your house', ['Link\'s House'], ['Links House Exit']),
|
||||
create_cave_region(multiworld, player, 'Chris Houlihan Room', 'I AM ERROR', None, ['Chris Houlihan Room Exit']),
|
||||
create_cave_region(multiworld, player, 'Tavern', 'the tavern', ['Kakariko Tavern']),
|
||||
create_cave_region(multiworld, player, 'Elder House', 'a connector', None, ['Elder House Exit (East)', 'Elder House Exit (West)']),
|
||||
create_cave_region(multiworld, player, 'Snitch Lady (East)', 'a boring house'),
|
||||
create_cave_region(multiworld, player, 'Snitch Lady (West)', 'a boring house'),
|
||||
create_cave_region(multiworld, player, 'Bush Covered House', 'the grass man'),
|
||||
create_cave_region(multiworld, player, 'Tavern (Front)', 'the tavern'),
|
||||
create_cave_region(multiworld, player, 'Light World Bomb Hut', 'a restock room'),
|
||||
create_cave_region(multiworld, player, 'Kakariko Shop', 'a common shop'),
|
||||
create_cave_region(multiworld, player, 'Fortune Teller (Light)', 'a fortune teller'),
|
||||
create_cave_region(multiworld, player, 'Lake Hylia Fortune Teller', 'a fortune teller'),
|
||||
create_cave_region(multiworld, player, 'Lumberjack House', 'a boring house'),
|
||||
create_cave_region(multiworld, player, 'Bonk Fairy (Light)', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Bonk Fairy (Dark)', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Lake Hylia Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Swamp Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Desert Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Dark Lake Hylia Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Dark Lake Hylia Ledge Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Dark Desert Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Dark Death Mountain Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Chicken House', 'a house with a chest', ['Chicken House']),
|
||||
create_cave_region(multiworld, player, 'Aginahs Cave', 'a cave with a chest', ['Aginah\'s Cave']),
|
||||
create_cave_region(multiworld, player, 'Sahasrahlas Hut', 'Sahasrahla', ['Sahasrahla\'s Hut - Left', 'Sahasrahla\'s Hut - Middle', 'Sahasrahla\'s Hut - Right', 'Sahasrahla']),
|
||||
create_cave_region(multiworld, player, 'Kakariko Well (top)', 'a drop\'s exit', ['Kakariko Well - Top', 'Kakariko Well - Left', 'Kakariko Well - Middle',
|
||||
create_cave_region(world, player, 'Hyrule Castle Secret Entrance', 'a drop\'s exit', ['Link\'s Uncle', 'Secret Passage'], ['Hyrule Castle Secret Entrance Exit']),
|
||||
create_lw_region(world, player, 'Zoras River', ['King Zora', 'Zora\'s Ledge']),
|
||||
create_cave_region(world, player, 'Waterfall of Wishing', 'a cave with two chests', ['Waterfall Fairy - Left', 'Waterfall Fairy - Right']),
|
||||
create_lw_region(world, player, 'Kings Grave Area', None, ['Kings Grave', 'Kings Grave Inner Rocks']),
|
||||
create_cave_region(world, player, 'Kings Grave', 'a cave with a chest', ['King\'s Tomb']),
|
||||
create_cave_region(world, player, 'North Fairy Cave', 'a drop\'s exit', None, ['North Fairy Cave Exit']),
|
||||
create_cave_region(world, player, 'Dam', 'the dam', ['Floodgate', 'Floodgate Chest']),
|
||||
create_cave_region(world, player, 'Links House', 'your house', ['Link\'s House'], ['Links House Exit']),
|
||||
create_cave_region(world, player, 'Chris Houlihan Room', 'I AM ERROR', None, ['Chris Houlihan Room Exit']),
|
||||
create_cave_region(world, player, 'Tavern', 'the tavern', ['Kakariko Tavern']),
|
||||
create_cave_region(world, player, 'Elder House', 'a connector', None, ['Elder House Exit (East)', 'Elder House Exit (West)']),
|
||||
create_cave_region(world, player, 'Snitch Lady (East)', 'a boring house'),
|
||||
create_cave_region(world, player, 'Snitch Lady (West)', 'a boring house'),
|
||||
create_cave_region(world, player, 'Bush Covered House', 'the grass man'),
|
||||
create_cave_region(world, player, 'Tavern (Front)', 'the tavern'),
|
||||
create_cave_region(world, player, 'Light World Bomb Hut', 'a restock room'),
|
||||
create_cave_region(world, player, 'Kakariko Shop', 'a common shop'),
|
||||
create_cave_region(world, player, 'Fortune Teller (Light)', 'a fortune teller'),
|
||||
create_cave_region(world, player, 'Lake Hylia Fortune Teller', 'a fortune teller'),
|
||||
create_cave_region(world, player, 'Lumberjack House', 'a boring house'),
|
||||
create_cave_region(world, player, 'Bonk Fairy (Light)', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Bonk Fairy (Dark)', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Lake Hylia Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Swamp Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Desert Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Dark Lake Hylia Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Dark Lake Hylia Ledge Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Dark Desert Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Dark Death Mountain Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Chicken House', 'a house with a chest', ['Chicken House']),
|
||||
create_cave_region(world, player, 'Aginahs Cave', 'a cave with a chest', ['Aginah\'s Cave']),
|
||||
create_cave_region(world, player, 'Sahasrahlas Hut', 'Sahasrahla', ['Sahasrahla\'s Hut - Left', 'Sahasrahla\'s Hut - Middle', 'Sahasrahla\'s Hut - Right', 'Sahasrahla']),
|
||||
create_cave_region(world, player, 'Kakariko Well (top)', 'a drop\'s exit', ['Kakariko Well - Top', 'Kakariko Well - Left', 'Kakariko Well - Middle',
|
||||
'Kakariko Well - Right', 'Kakariko Well - Bottom'], ['Kakariko Well (top to bottom)']),
|
||||
create_cave_region(multiworld, player, 'Kakariko Well (bottom)', 'a drop\'s exit', None, ['Kakariko Well Exit']),
|
||||
create_cave_region(multiworld, player, 'Blacksmiths Hut', 'the smith', ['Blacksmith', 'Missing Smith']),
|
||||
create_lw_region(multiworld, player, 'Bat Cave Drop Ledge', None, ['Bat Cave Drop']),
|
||||
create_cave_region(multiworld, player, 'Bat Cave (right)', 'a drop\'s exit', ['Magic Bat'], ['Bat Cave Door']),
|
||||
create_cave_region(multiworld, player, 'Bat Cave (left)', 'a drop\'s exit', None, ['Bat Cave Exit']),
|
||||
create_cave_region(multiworld, player, 'Sick Kids House', 'the sick kid', ['Sick Kid']),
|
||||
create_lw_region(multiworld, player, 'Hobo Bridge', ['Hobo']),
|
||||
create_cave_region(multiworld, player, 'Lost Woods Hideout (top)', 'a drop\'s exit', ['Lost Woods Hideout'], ['Lost Woods Hideout (top to bottom)']),
|
||||
create_cave_region(multiworld, player, 'Lost Woods Hideout (bottom)', 'a drop\'s exit', None, ['Lost Woods Hideout Exit']),
|
||||
create_cave_region(multiworld, player, 'Lumberjack Tree (top)', 'a drop\'s exit', ['Lumberjack Tree'], ['Lumberjack Tree (top to bottom)']),
|
||||
create_cave_region(multiworld, player, 'Lumberjack Tree (bottom)', 'a drop\'s exit', None, ['Lumberjack Tree Exit']),
|
||||
create_lw_region(multiworld, player, 'Cave 45 Ledge', None, ['Cave 45']),
|
||||
create_cave_region(multiworld, player, 'Cave 45', 'a cave with an item', ['Cave 45']),
|
||||
create_lw_region(multiworld, player, 'Graveyard Ledge', None, ['Graveyard Cave']),
|
||||
create_cave_region(multiworld, player, 'Graveyard Cave', 'a cave with an item', ['Graveyard Cave']),
|
||||
create_cave_region(multiworld, player, 'Checkerboard Cave', 'a cave with an item', ['Checkerboard Cave']),
|
||||
create_cave_region(multiworld, player, 'Long Fairy Cave', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Mini Moldorm Cave', 'a bounty of five items', ['Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right',
|
||||
create_cave_region(world, player, 'Kakariko Well (bottom)', 'a drop\'s exit', None, ['Kakariko Well Exit']),
|
||||
create_cave_region(world, player, 'Blacksmiths Hut', 'the smith', ['Blacksmith', 'Missing Smith']),
|
||||
create_lw_region(world, player, 'Bat Cave Drop Ledge', None, ['Bat Cave Drop']),
|
||||
create_cave_region(world, player, 'Bat Cave (right)', 'a drop\'s exit', ['Magic Bat'], ['Bat Cave Door']),
|
||||
create_cave_region(world, player, 'Bat Cave (left)', 'a drop\'s exit', None, ['Bat Cave Exit']),
|
||||
create_cave_region(world, player, 'Sick Kids House', 'the sick kid', ['Sick Kid']),
|
||||
create_lw_region(world, player, 'Hobo Bridge', ['Hobo']),
|
||||
create_cave_region(world, player, 'Lost Woods Hideout (top)', 'a drop\'s exit', ['Lost Woods Hideout'], ['Lost Woods Hideout (top to bottom)']),
|
||||
create_cave_region(world, player, 'Lost Woods Hideout (bottom)', 'a drop\'s exit', None, ['Lost Woods Hideout Exit']),
|
||||
create_cave_region(world, player, 'Lumberjack Tree (top)', 'a drop\'s exit', ['Lumberjack Tree'], ['Lumberjack Tree (top to bottom)']),
|
||||
create_cave_region(world, player, 'Lumberjack Tree (bottom)', 'a drop\'s exit', None, ['Lumberjack Tree Exit']),
|
||||
create_lw_region(world, player, 'Cave 45 Ledge', None, ['Cave 45']),
|
||||
create_cave_region(world, player, 'Cave 45', 'a cave with an item', ['Cave 45']),
|
||||
create_lw_region(world, player, 'Graveyard Ledge', None, ['Graveyard Cave']),
|
||||
create_cave_region(world, player, 'Graveyard Cave', 'a cave with an item', ['Graveyard Cave']),
|
||||
create_cave_region(world, player, 'Checkerboard Cave', 'a cave with an item', ['Checkerboard Cave']),
|
||||
create_cave_region(world, player, 'Long Fairy Cave', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Mini Moldorm Cave', 'a bounty of five items', ['Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right',
|
||||
'Mini Moldorm Cave - Far Right', 'Mini Moldorm Cave - Generous Guy']),
|
||||
create_cave_region(multiworld, player, 'Ice Rod Cave', 'a cave with a chest', ['Ice Rod Cave']),
|
||||
create_cave_region(multiworld, player, 'Good Bee Cave', 'a cold bee'),
|
||||
create_cave_region(multiworld, player, '20 Rupee Cave', 'a cave with some cash'),
|
||||
create_cave_region(multiworld, player, 'Cave Shop (Lake Hylia)', 'a common shop'),
|
||||
create_cave_region(multiworld, player, 'Cave Shop (Dark Death Mountain)', 'a common shop'),
|
||||
create_cave_region(multiworld, player, 'Bonk Rock Cave', 'a cave with a chest', ['Bonk Rock Cave']),
|
||||
create_cave_region(multiworld, player, 'Library', 'the library', ['Library']),
|
||||
create_cave_region(multiworld, player, 'Kakariko Gamble Game', 'a game of chance'),
|
||||
create_cave_region(multiworld, player, 'Potion Shop', 'the potion shop', ['Potion Shop']),
|
||||
create_lw_region(multiworld, player, 'Lake Hylia Island', ['Lake Hylia Island']),
|
||||
create_cave_region(multiworld, player, 'Capacity Upgrade', 'the queen of fairies', ['Capacity Upgrade Shop']),
|
||||
create_cave_region(multiworld, player, 'Two Brothers House', 'a connector', None, ['Two Brothers House Exit (East)', 'Two Brothers House Exit (West)']),
|
||||
create_lw_region(multiworld, player, 'Maze Race Ledge', ['Maze Race'], ['Two Brothers House (West)']),
|
||||
create_cave_region(multiworld, player, '50 Rupee Cave', 'a cave with some cash'),
|
||||
create_lw_region(multiworld, player, 'Desert Ledge', ['Desert Ledge'], ['Desert Palace Entrance (North) Rocks', 'Desert Palace Entrance (West)']),
|
||||
create_lw_region(multiworld, player, 'Desert Ledge (Northeast)', None, ['Checkerboard Cave']),
|
||||
create_lw_region(multiworld, player, 'Desert Palace Stairs', None, ['Desert Palace Entrance (South)']),
|
||||
create_lw_region(multiworld, player, 'Desert Palace Lone Stairs', None, ['Desert Palace Stairs Drop', 'Desert Palace Entrance (East)']),
|
||||
create_lw_region(multiworld, player, 'Desert Palace Entrance (North) Spot', None, ['Desert Palace Entrance (North)', 'Desert Ledge Return Rocks']),
|
||||
create_dungeon_region(multiworld, player, 'Desert Palace Main (Outer)', 'Desert Palace', ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'],
|
||||
create_cave_region(world, player, 'Ice Rod Cave', 'a cave with a chest', ['Ice Rod Cave']),
|
||||
create_cave_region(world, player, 'Good Bee Cave', 'a cold bee'),
|
||||
create_cave_region(world, player, '20 Rupee Cave', 'a cave with some cash'),
|
||||
create_cave_region(world, player, 'Cave Shop (Lake Hylia)', 'a common shop'),
|
||||
create_cave_region(world, player, 'Cave Shop (Dark Death Mountain)', 'a common shop'),
|
||||
create_cave_region(world, player, 'Bonk Rock Cave', 'a cave with a chest', ['Bonk Rock Cave']),
|
||||
create_cave_region(world, player, 'Library', 'the library', ['Library']),
|
||||
create_cave_region(world, player, 'Kakariko Gamble Game', 'a game of chance'),
|
||||
create_cave_region(world, player, 'Potion Shop', 'the potion shop', ['Potion Shop']),
|
||||
create_lw_region(world, player, 'Lake Hylia Island', ['Lake Hylia Island']),
|
||||
create_cave_region(world, player, 'Capacity Upgrade', 'the queen of fairies', ['Capacity Upgrade Shop']),
|
||||
create_cave_region(world, player, 'Two Brothers House', 'a connector', None, ['Two Brothers House Exit (East)', 'Two Brothers House Exit (West)']),
|
||||
create_lw_region(world, player, 'Maze Race Ledge', ['Maze Race'], ['Two Brothers House (West)']),
|
||||
create_cave_region(world, player, '50 Rupee Cave', 'a cave with some cash'),
|
||||
create_lw_region(world, player, 'Desert Ledge', ['Desert Ledge'], ['Desert Palace Entrance (North) Rocks', 'Desert Palace Entrance (West)']),
|
||||
create_lw_region(world, player, 'Desert Ledge (Northeast)', None, ['Checkerboard Cave']),
|
||||
create_lw_region(world, player, 'Desert Palace Stairs', None, ['Desert Palace Entrance (South)']),
|
||||
create_lw_region(world, player, 'Desert Palace Lone Stairs', None, ['Desert Palace Stairs Drop', 'Desert Palace Entrance (East)']),
|
||||
create_lw_region(world, player, 'Desert Palace Entrance (North) Spot', None, ['Desert Palace Entrance (North)', 'Desert Ledge Return Rocks']),
|
||||
create_dungeon_region(world, player, 'Desert Palace Main (Outer)', 'Desert Palace', ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'],
|
||||
['Desert Palace Pots (Outer)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)', 'Desert Palace East Wing']),
|
||||
create_dungeon_region(multiworld, player, 'Desert Palace Main (Inner)', 'Desert Palace', None, ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']),
|
||||
create_dungeon_region(multiworld, player, 'Desert Palace East', 'Desert Palace', ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']),
|
||||
create_dungeon_region(multiworld, player, 'Desert Palace North', 'Desert Palace', ['Desert Palace - Desert Tiles 1 Pot Key', 'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key',
|
||||
create_dungeon_region(world, player, 'Desert Palace Main (Inner)', 'Desert Palace', None, ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']),
|
||||
create_dungeon_region(world, player, 'Desert Palace East', 'Desert Palace', ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']),
|
||||
create_dungeon_region(world, player, 'Desert Palace North', 'Desert Palace', ['Desert Palace - Desert Tiles 1 Pot Key', 'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key',
|
||||
'Desert Palace - Boss', 'Desert Palace - Prize'], ['Desert Palace Exit (North)']),
|
||||
create_dungeon_region(multiworld, player, 'Eastern Palace', 'Eastern Palace', ['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest', 'Eastern Palace - Cannonball Chest',
|
||||
create_dungeon_region(world, player, 'Eastern Palace', 'Eastern Palace', ['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest', 'Eastern Palace - Cannonball Chest',
|
||||
'Eastern Palace - Dark Square Pot Key', 'Eastern Palace - Dark Eyegore Key Drop', 'Eastern Palace - Big Key Chest',
|
||||
'Eastern Palace - Map Chest', 'Eastern Palace - Boss', 'Eastern Palace - Prize'], ['Eastern Palace Exit']),
|
||||
create_lw_region(multiworld, player, 'Master Sword Meadow', ['Master Sword Pedestal']),
|
||||
create_cave_region(multiworld, player, 'Lost Woods Gamble', 'a game of chance'),
|
||||
create_lw_region(multiworld, player, 'Hyrule Castle Courtyard', None, ['Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Entrance (South)']),
|
||||
create_lw_region(multiworld, player, 'Hyrule Castle Ledge', None, ['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Agahnims Tower', 'Hyrule Castle Ledge Courtyard Drop']),
|
||||
create_dungeon_region(multiworld, player, 'Hyrule Castle', 'Hyrule Castle', ['Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', 'Hyrule Castle - Zelda\'s Chest',
|
||||
create_lw_region(world, player, 'Master Sword Meadow', ['Master Sword Pedestal']),
|
||||
create_cave_region(world, player, 'Lost Woods Gamble', 'a game of chance'),
|
||||
create_lw_region(world, player, 'Hyrule Castle Courtyard', None, ['Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Entrance (South)']),
|
||||
create_lw_region(world, player, 'Hyrule Castle Ledge', None, ['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Agahnims Tower', 'Hyrule Castle Ledge Courtyard Drop']),
|
||||
create_dungeon_region(world, player, 'Hyrule Castle', 'Hyrule Castle', ['Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', 'Hyrule Castle - Zelda\'s Chest',
|
||||
'Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop', 'Hyrule Castle - Big Key Drop'],
|
||||
['Hyrule Castle Exit (East)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (South)', 'Throne Room']),
|
||||
create_dungeon_region(multiworld, player, 'Sewer Drop', 'a drop\'s exit', None, ['Sewer Drop']), # This exists only to be referenced for access checks
|
||||
create_dungeon_region(multiworld, player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross', 'Sewers - Key Rat Key Drop'], ['Sewers Door']),
|
||||
create_dungeon_region(multiworld, player, 'Sewers', 'a drop\'s exit', None, ['Sanctuary Push Door', 'Sewers Back Door', 'Sewers Secret Room']),
|
||||
create_dungeon_region(multiworld, player, 'Sewers Secret Room', 'a drop\'s exit', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle',
|
||||
create_dungeon_region(world, player, 'Sewer Drop', 'a drop\'s exit', None, ['Sewer Drop']), # This exists only to be referenced for access checks
|
||||
create_dungeon_region(world, player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross', 'Sewers - Key Rat Key Drop'], ['Sewers Door']),
|
||||
create_dungeon_region(world, player, 'Sewers', 'a drop\'s exit', None, ['Sanctuary Push Door', 'Sewers Back Door', 'Sewers Secret Room']),
|
||||
create_dungeon_region(world, player, 'Sewers Secret Room', 'a drop\'s exit', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle',
|
||||
'Sewers - Secret Room - Right']),
|
||||
create_dungeon_region(multiworld, player, 'Sanctuary', 'a drop\'s exit', ['Sanctuary'], ['Sanctuary Exit']),
|
||||
create_dungeon_region(multiworld, player, 'Agahnims Tower', 'Castle Tower', ['Castle Tower - Room 03', 'Castle Tower - Dark Maze', 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'], ['Agahnim 1', 'Agahnims Tower Exit']),
|
||||
create_dungeon_region(multiworld, player, 'Agahnim 1', 'Castle Tower', ['Agahnim 1'], None),
|
||||
create_cave_region(multiworld, player, 'Old Man Cave', 'a connector', ['Old Man'], ['Old Man Cave Exit (East)', 'Old Man Cave Exit (West)']),
|
||||
create_cave_region(multiworld, player, 'Old Man House', 'a connector', None, ['Old Man House Exit (Bottom)', 'Old Man House Front to Back']),
|
||||
create_cave_region(multiworld, player, 'Old Man House Back', 'a connector', None, ['Old Man House Exit (Top)', 'Old Man House Back to Front']),
|
||||
create_lw_region(multiworld, player, 'Death Mountain', None, ['Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)', 'Death Mountain Return Cave (East)', 'Spectacle Rock Cave', 'Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Bottom)', 'Broken Bridge (West)', 'Death Mountain Teleporter']),
|
||||
create_cave_region(multiworld, player, 'Death Mountain Return Cave', 'a connector', None, ['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)']),
|
||||
create_lw_region(multiworld, player, 'Death Mountain Return Ledge', None, ['Death Mountain Return Ledge Drop', 'Death Mountain Return Cave (West)']),
|
||||
create_cave_region(multiworld, player, 'Spectacle Rock Cave (Top)', 'a connector', ['Spectacle Rock Cave'], ['Spectacle Rock Cave Drop', 'Spectacle Rock Cave Exit (Top)']),
|
||||
create_cave_region(multiworld, player, 'Spectacle Rock Cave (Bottom)', 'a connector', None, ['Spectacle Rock Cave Exit']),
|
||||
create_cave_region(multiworld, player, 'Spectacle Rock Cave (Peak)', 'a connector', None, ['Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave Exit (Peak)']),
|
||||
create_lw_region(multiworld, player, 'East Death Mountain (Bottom)', None, ['Broken Bridge (East)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'East Death Mountain Teleporter', 'Hookshot Fairy', 'Fairy Ascension Rocks', 'Spiral Cave (Bottom)']),
|
||||
create_cave_region(multiworld, player, 'Hookshot Fairy', 'fairies deep in a cave'),
|
||||
create_cave_region(multiworld, player, 'Paradox Cave Front', 'a connector', None, ['Paradox Cave Push Block Reverse', 'Paradox Cave Exit (Bottom)', 'Light World Death Mountain Shop']),
|
||||
create_cave_region(multiworld, player, 'Paradox Cave Chest Area', 'a connector', ['Paradox Cave Lower - Far Left',
|
||||
create_dungeon_region(world, player, 'Sanctuary', 'a drop\'s exit', ['Sanctuary'], ['Sanctuary Exit']),
|
||||
create_dungeon_region(world, player, 'Agahnims Tower', 'Castle Tower', ['Castle Tower - Room 03', 'Castle Tower - Dark Maze', 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'], ['Agahnim 1', 'Agahnims Tower Exit']),
|
||||
create_dungeon_region(world, player, 'Agahnim 1', 'Castle Tower', ['Agahnim 1'], None),
|
||||
create_cave_region(world, player, 'Old Man Cave', 'a connector', ['Old Man'], ['Old Man Cave Exit (East)', 'Old Man Cave Exit (West)']),
|
||||
create_cave_region(world, player, 'Old Man House', 'a connector', None, ['Old Man House Exit (Bottom)', 'Old Man House Front to Back']),
|
||||
create_cave_region(world, player, 'Old Man House Back', 'a connector', None, ['Old Man House Exit (Top)', 'Old Man House Back to Front']),
|
||||
create_lw_region(world, player, 'Death Mountain', None, ['Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)', 'Death Mountain Return Cave (East)', 'Spectacle Rock Cave', 'Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Bottom)', 'Broken Bridge (West)', 'Death Mountain Teleporter']),
|
||||
create_cave_region(world, player, 'Death Mountain Return Cave', 'a connector', None, ['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)']),
|
||||
create_lw_region(world, player, 'Death Mountain Return Ledge', None, ['Death Mountain Return Ledge Drop', 'Death Mountain Return Cave (West)']),
|
||||
create_cave_region(world, player, 'Spectacle Rock Cave (Top)', 'a connector', ['Spectacle Rock Cave'], ['Spectacle Rock Cave Drop', 'Spectacle Rock Cave Exit (Top)']),
|
||||
create_cave_region(world, player, 'Spectacle Rock Cave (Bottom)', 'a connector', None, ['Spectacle Rock Cave Exit']),
|
||||
create_cave_region(world, player, 'Spectacle Rock Cave (Peak)', 'a connector', None, ['Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave Exit (Peak)']),
|
||||
create_lw_region(world, player, 'East Death Mountain (Bottom)', None, ['Broken Bridge (East)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'East Death Mountain Teleporter', 'Hookshot Fairy', 'Fairy Ascension Rocks', 'Spiral Cave (Bottom)']),
|
||||
create_cave_region(world, player, 'Hookshot Fairy', 'fairies deep in a cave'),
|
||||
create_cave_region(world, player, 'Paradox Cave Front', 'a connector', None, ['Paradox Cave Push Block Reverse', 'Paradox Cave Exit (Bottom)', 'Light World Death Mountain Shop']),
|
||||
create_cave_region(world, player, 'Paradox Cave Chest Area', 'a connector', ['Paradox Cave Lower - Far Left',
|
||||
'Paradox Cave Lower - Left',
|
||||
'Paradox Cave Lower - Right',
|
||||
'Paradox Cave Lower - Far Right',
|
||||
@@ -147,267 +147,267 @@ def create_regions(multiworld: MultiWorld, player: int):
|
||||
'Paradox Cave Upper - Left',
|
||||
'Paradox Cave Upper - Right'],
|
||||
['Paradox Cave Push Block', 'Paradox Cave Bomb Jump']),
|
||||
create_cave_region(multiworld, player, 'Paradox Cave', 'a connector', None,
|
||||
create_cave_region(world, player, 'Paradox Cave', 'a connector', None,
|
||||
['Paradox Cave Exit (Middle)', 'Paradox Cave Exit (Top)', 'Paradox Cave Drop']),
|
||||
create_cave_region(multiworld, player, 'Light World Death Mountain Shop', 'a common shop'),
|
||||
create_lw_region(multiworld, player, 'East Death Mountain (Top)', None,
|
||||
create_cave_region(world, player, 'Light World Death Mountain Shop', 'a common shop'),
|
||||
create_lw_region(world, player, 'East Death Mountain (Top)', None,
|
||||
['Paradox Cave (Top)', 'Death Mountain (Top)', 'Spiral Cave Ledge Access',
|
||||
'East Death Mountain Drop', 'Turtle Rock Teleporter', 'Fairy Ascension Ledge']),
|
||||
create_lw_region(multiworld, player, 'Spiral Cave Ledge', None, ['Spiral Cave', 'Spiral Cave Ledge Drop']),
|
||||
create_cave_region(multiworld, player, 'Spiral Cave (Top)', 'a connector', ['Spiral Cave'],
|
||||
create_lw_region(world, player, 'Spiral Cave Ledge', None, ['Spiral Cave', 'Spiral Cave Ledge Drop']),
|
||||
create_cave_region(world, player, 'Spiral Cave (Top)', 'a connector', ['Spiral Cave'],
|
||||
['Spiral Cave (top to bottom)', 'Spiral Cave Exit (Top)']),
|
||||
create_cave_region(multiworld, player, 'Spiral Cave (Bottom)', 'a connector', None, ['Spiral Cave Exit']),
|
||||
create_lw_region(multiworld, player, 'Fairy Ascension Plateau', None,
|
||||
create_cave_region(world, player, 'Spiral Cave (Bottom)', 'a connector', None, ['Spiral Cave Exit']),
|
||||
create_lw_region(world, player, 'Fairy Ascension Plateau', None,
|
||||
['Fairy Ascension Drop', 'Fairy Ascension Cave (Bottom)']),
|
||||
create_cave_region(multiworld, player, 'Fairy Ascension Cave (Bottom)', 'a connector', None,
|
||||
create_cave_region(world, player, 'Fairy Ascension Cave (Bottom)', 'a connector', None,
|
||||
['Fairy Ascension Cave Climb', 'Fairy Ascension Cave Exit (Bottom)']),
|
||||
create_cave_region(multiworld, player, 'Fairy Ascension Cave (Drop)', 'a connector', None,
|
||||
create_cave_region(world, player, 'Fairy Ascension Cave (Drop)', 'a connector', None,
|
||||
['Fairy Ascension Cave Pots']),
|
||||
create_cave_region(multiworld, player, 'Fairy Ascension Cave (Top)', 'a connector', None,
|
||||
create_cave_region(world, player, 'Fairy Ascension Cave (Top)', 'a connector', None,
|
||||
['Fairy Ascension Cave Exit (Top)', 'Fairy Ascension Cave Drop']),
|
||||
create_lw_region(multiworld, player, 'Fairy Ascension Ledge', None,
|
||||
create_lw_region(world, player, 'Fairy Ascension Ledge', None,
|
||||
['Fairy Ascension Ledge Drop', 'Fairy Ascension Cave (Top)']),
|
||||
create_lw_region(multiworld, player, 'Death Mountain (Top)', ['Ether Tablet'],
|
||||
create_lw_region(world, player, 'Death Mountain (Top)', ['Ether Tablet'],
|
||||
['East Death Mountain (Top)', 'Tower of Hera', 'Death Mountain Drop']),
|
||||
create_lw_region(multiworld, player, 'Spectacle Rock', ['Spectacle Rock'], ['Spectacle Rock Drop']),
|
||||
create_dungeon_region(multiworld, player, 'Tower of Hera (Bottom)', 'Tower of Hera',
|
||||
create_lw_region(world, player, 'Spectacle Rock', ['Spectacle Rock'], ['Spectacle Rock Drop']),
|
||||
create_dungeon_region(world, player, 'Tower of Hera (Bottom)', 'Tower of Hera',
|
||||
['Tower of Hera - Basement Cage', 'Tower of Hera - Map Chest'],
|
||||
['Tower of Hera Small Key Door', 'Tower of Hera Big Key Door', 'Tower of Hera Exit']),
|
||||
create_dungeon_region(multiworld, player, 'Tower of Hera (Basement)', 'Tower of Hera',
|
||||
create_dungeon_region(world, player, 'Tower of Hera (Basement)', 'Tower of Hera',
|
||||
['Tower of Hera - Big Key Chest']),
|
||||
create_dungeon_region(multiworld, player, 'Tower of Hera (Top)', 'Tower of Hera',
|
||||
create_dungeon_region(world, player, 'Tower of Hera (Top)', 'Tower of Hera',
|
||||
['Tower of Hera - Compass Chest', 'Tower of Hera - Big Chest', 'Tower of Hera - Boss',
|
||||
'Tower of Hera - Prize']),
|
||||
|
||||
create_dw_region(multiworld, player, 'East Dark World', ['Pyramid'],
|
||||
create_dw_region(world, player, 'East Dark World', ['Pyramid'],
|
||||
['Pyramid Fairy', 'South Dark World Bridge', 'Palace of Darkness',
|
||||
'Dark Lake Hylia Drop (East)',
|
||||
'Hyrule Castle Ledge Mirror Spot', 'Dark Lake Hylia Fairy', 'Palace of Darkness Hint',
|
||||
'East Dark World Hint', 'Pyramid Hole', 'Northeast Dark World Broken Bridge Pass', ]),
|
||||
create_dw_region(multiworld, player, 'Catfish', ['Catfish'], ['Catfish Exit Rock']),
|
||||
create_dw_region(multiworld, player, 'Northeast Dark World', None,
|
||||
create_dw_region(world, player, 'Catfish', ['Catfish'], ['Catfish Exit Rock']),
|
||||
create_dw_region(world, player, 'Northeast Dark World', None,
|
||||
['West Dark World Gap', 'Dark World Potion Shop', 'East Dark World Broken Bridge Pass',
|
||||
'Catfish Entrance Rock', 'Dark Lake Hylia Teleporter']),
|
||||
create_cave_region(multiworld, player, 'Palace of Darkness Hint', 'a storyteller'),
|
||||
create_cave_region(multiworld, player, 'East Dark World Hint', 'a storyteller'),
|
||||
create_dw_region(multiworld, player, 'South Dark World', ['Stumpy', 'Digging Game'],
|
||||
create_cave_region(world, player, 'Palace of Darkness Hint', 'a storyteller'),
|
||||
create_cave_region(world, player, 'East Dark World Hint', 'a storyteller'),
|
||||
create_dw_region(world, player, 'South Dark World', ['Stumpy', 'Digging Game'],
|
||||
['Dark Lake Hylia Drop (South)', 'Hype Cave', 'Swamp Palace', 'Village of Outcasts Heavy Rock',
|
||||
'Maze Race Mirror Spot',
|
||||
'Cave 45 Mirror Spot', 'East Dark World Bridge', 'Big Bomb Shop', 'Archery Game',
|
||||
'Bonk Fairy (Dark)', 'Dark Lake Hylia Shop',
|
||||
'Bombos Tablet Mirror Spot']),
|
||||
create_lw_region(multiworld, player, 'Bombos Tablet Ledge', ['Bombos Tablet']),
|
||||
create_cave_region(multiworld, player, 'Big Bomb Shop', 'the bomb shop'),
|
||||
create_cave_region(multiworld, player, 'Archery Game', 'a game of skill'),
|
||||
create_dw_region(multiworld, player, 'Dark Lake Hylia', None,
|
||||
create_lw_region(world, player, 'Bombos Tablet Ledge', ['Bombos Tablet']),
|
||||
create_cave_region(world, player, 'Big Bomb Shop', 'the bomb shop'),
|
||||
create_cave_region(world, player, 'Archery Game', 'a game of skill'),
|
||||
create_dw_region(world, player, 'Dark Lake Hylia', None,
|
||||
['Lake Hylia Island Mirror Spot', 'East Dark World Pier', 'Dark Lake Hylia Ledge']),
|
||||
create_dw_region(multiworld, player, 'Dark Lake Hylia Central Island', None,
|
||||
create_dw_region(world, player, 'Dark Lake Hylia Central Island', None,
|
||||
['Ice Palace', 'Lake Hylia Central Island Mirror Spot']),
|
||||
create_dw_region(multiworld, player, 'Dark Lake Hylia Ledge', None,
|
||||
create_dw_region(world, player, 'Dark Lake Hylia Ledge', None,
|
||||
['Dark Lake Hylia Ledge Drop', 'Dark Lake Hylia Ledge Fairy', 'Dark Lake Hylia Ledge Hint',
|
||||
'Dark Lake Hylia Ledge Spike Cave']),
|
||||
create_cave_region(multiworld, player, 'Dark Lake Hylia Ledge Hint', 'a storyteller'),
|
||||
create_cave_region(multiworld, player, 'Dark Lake Hylia Ledge Spike Cave', 'a spiky hint'),
|
||||
create_cave_region(multiworld, player, 'Hype Cave', 'a bounty of five items',
|
||||
create_cave_region(world, player, 'Dark Lake Hylia Ledge Hint', 'a storyteller'),
|
||||
create_cave_region(world, player, 'Dark Lake Hylia Ledge Spike Cave', 'a spiky hint'),
|
||||
create_cave_region(world, player, 'Hype Cave', 'a bounty of five items',
|
||||
['Hype Cave - Top', 'Hype Cave - Middle Right', 'Hype Cave - Middle Left',
|
||||
'Hype Cave - Bottom', 'Hype Cave - Generous Guy']),
|
||||
create_dw_region(multiworld, player, 'West Dark World', ['Frog'],
|
||||
create_dw_region(world, player, 'West Dark World', ['Frog'],
|
||||
['Village of Outcasts Drop', 'East Dark World River Pier', 'Brewery', 'C-Shaped House',
|
||||
'Chest Game', 'Thieves Town', 'Graveyard Ledge Mirror Spot', 'Kings Grave Mirror Spot',
|
||||
'Bumper Cave Entrance Rock',
|
||||
'Skull Woods Forest', 'Village of Outcasts Pegs', 'Village of Outcasts Eastern Rocks',
|
||||
'Red Shield Shop', 'Dark Sanctuary Hint', 'Fortune Teller (Dark)',
|
||||
'Dark World Lumberjack Shop']),
|
||||
create_dw_region(multiworld, player, 'Dark Grassy Lawn', None, ['Grassy Lawn Pegs', 'Village of Outcasts Shop']),
|
||||
create_dw_region(multiworld, player, 'Hammer Peg Area', ['Dark Blacksmith Ruins'],
|
||||
create_dw_region(world, player, 'Dark Grassy Lawn', None, ['Grassy Lawn Pegs', 'Village of Outcasts Shop']),
|
||||
create_dw_region(world, player, 'Hammer Peg Area', ['Dark Blacksmith Ruins'],
|
||||
['Bat Cave Drop Ledge Mirror Spot', 'Dark World Hammer Peg Cave', 'Peg Area Rocks']),
|
||||
create_dw_region(multiworld, player, 'Bumper Cave Entrance', None,
|
||||
create_dw_region(world, player, 'Bumper Cave Entrance', None,
|
||||
['Bumper Cave (Bottom)', 'Bumper Cave Entrance Mirror Spot', 'Bumper Cave Entrance Drop']),
|
||||
create_cave_region(multiworld, player, 'Fortune Teller (Dark)', 'a fortune teller'),
|
||||
create_cave_region(multiworld, player, 'Village of Outcasts Shop', 'a common shop'),
|
||||
create_cave_region(multiworld, player, 'Dark Lake Hylia Shop', 'a common shop'),
|
||||
create_cave_region(multiworld, player, 'Dark World Lumberjack Shop', 'a common shop'),
|
||||
create_cave_region(multiworld, player, 'Dark World Potion Shop', 'a common shop'),
|
||||
create_cave_region(multiworld, player, 'Dark World Hammer Peg Cave', 'a cave with an item', ['Peg Cave']),
|
||||
create_cave_region(multiworld, player, 'Pyramid Fairy', 'a cave with two chests',
|
||||
create_cave_region(world, player, 'Fortune Teller (Dark)', 'a fortune teller'),
|
||||
create_cave_region(world, player, 'Village of Outcasts Shop', 'a common shop'),
|
||||
create_cave_region(world, player, 'Dark Lake Hylia Shop', 'a common shop'),
|
||||
create_cave_region(world, player, 'Dark World Lumberjack Shop', 'a common shop'),
|
||||
create_cave_region(world, player, 'Dark World Potion Shop', 'a common shop'),
|
||||
create_cave_region(world, player, 'Dark World Hammer Peg Cave', 'a cave with an item', ['Peg Cave']),
|
||||
create_cave_region(world, player, 'Pyramid Fairy', 'a cave with two chests',
|
||||
['Pyramid Fairy - Left', 'Pyramid Fairy - Right']),
|
||||
create_cave_region(multiworld, player, 'Brewery', 'a house with a chest', ['Brewery']),
|
||||
create_cave_region(multiworld, player, 'C-Shaped House', 'a house with a chest', ['C-Shaped House']),
|
||||
create_cave_region(multiworld, player, 'Chest Game', 'a game of 16 chests', ['Chest Game']),
|
||||
create_cave_region(multiworld, player, 'Red Shield Shop', 'the rare shop'),
|
||||
create_cave_region(multiworld, player, 'Dark Sanctuary Hint', 'a storyteller'),
|
||||
create_cave_region(multiworld, player, 'Bumper Cave', 'a connector', None,
|
||||
create_cave_region(world, player, 'Brewery', 'a house with a chest', ['Brewery']),
|
||||
create_cave_region(world, player, 'C-Shaped House', 'a house with a chest', ['C-Shaped House']),
|
||||
create_cave_region(world, player, 'Chest Game', 'a game of 16 chests', ['Chest Game']),
|
||||
create_cave_region(world, player, 'Red Shield Shop', 'the rare shop'),
|
||||
create_cave_region(world, player, 'Dark Sanctuary Hint', 'a storyteller'),
|
||||
create_cave_region(world, player, 'Bumper Cave', 'a connector', None,
|
||||
['Bumper Cave Exit (Bottom)', 'Bumper Cave Exit (Top)']),
|
||||
create_dw_region(multiworld, player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'],
|
||||
create_dw_region(world, player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'],
|
||||
['Bumper Cave Ledge Drop', 'Bumper Cave (Top)', 'Bumper Cave Ledge Mirror Spot']),
|
||||
create_dw_region(multiworld, player, 'Skull Woods Forest', None,
|
||||
create_dw_region(world, player, 'Skull Woods Forest', None,
|
||||
['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)',
|
||||
'Skull Woods First Section Hole (North)',
|
||||
'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)']),
|
||||
create_dw_region(multiworld, player, 'Skull Woods Forest (West)', None,
|
||||
create_dw_region(world, player, 'Skull Woods Forest (West)', None,
|
||||
['Skull Woods Second Section Hole', 'Skull Woods Second Section Door (West)',
|
||||
'Skull Woods Final Section']),
|
||||
create_dw_region(multiworld, player, 'Dark Desert', None,
|
||||
create_dw_region(world, player, 'Dark Desert', None,
|
||||
['Misery Mire', 'Mire Shed', 'Desert Ledge (Northeast) Mirror Spot',
|
||||
'Desert Ledge Mirror Spot', 'Desert Palace Stairs Mirror Spot',
|
||||
'Desert Palace Entrance (North) Mirror Spot', 'Dark Desert Hint', 'Dark Desert Fairy']),
|
||||
create_cave_region(multiworld, player, 'Mire Shed', 'a cave with two chests',
|
||||
create_cave_region(world, player, 'Mire Shed', 'a cave with two chests',
|
||||
['Mire Shed - Left', 'Mire Shed - Right']),
|
||||
create_cave_region(multiworld, player, 'Dark Desert Hint', 'a storyteller'),
|
||||
create_dw_region(multiworld, player, 'Dark Death Mountain (West Bottom)', None,
|
||||
create_cave_region(world, player, 'Dark Desert Hint', 'a storyteller'),
|
||||
create_dw_region(world, player, 'Dark Death Mountain (West Bottom)', None,
|
||||
['Spike Cave', 'Spectacle Rock Mirror Spot', 'Dark Death Mountain Fairy']),
|
||||
create_dw_region(multiworld, player, 'Dark Death Mountain (Top)', None,
|
||||
create_dw_region(world, player, 'Dark Death Mountain (Top)', None,
|
||||
['Dark Death Mountain Drop (East)', 'Dark Death Mountain Drop (West)', 'Ganons Tower',
|
||||
'Superbunny Cave (Top)',
|
||||
'Hookshot Cave', 'East Death Mountain (Top) Mirror Spot', 'Turtle Rock']),
|
||||
create_dw_region(multiworld, player, 'Dark Death Mountain Ledge', None,
|
||||
create_dw_region(world, player, 'Dark Death Mountain Ledge', None,
|
||||
['Dark Death Mountain Ledge (East)', 'Dark Death Mountain Ledge (West)',
|
||||
'Mimic Cave Mirror Spot', 'Spiral Cave Mirror Spot']),
|
||||
create_dw_region(multiworld, player, 'Dark Death Mountain Isolated Ledge', None,
|
||||
create_dw_region(world, player, 'Dark Death Mountain Isolated Ledge', None,
|
||||
['Isolated Ledge Mirror Spot', 'Turtle Rock Isolated Ledge Entrance']),
|
||||
create_dw_region(multiworld, player, 'Dark Death Mountain (East Bottom)', None,
|
||||
create_dw_region(world, player, 'Dark Death Mountain (East Bottom)', None,
|
||||
['Superbunny Cave (Bottom)', 'Cave Shop (Dark Death Mountain)',
|
||||
'Fairy Ascension Mirror Spot']),
|
||||
create_cave_region(multiworld, player, 'Superbunny Cave (Top)', 'a connector',
|
||||
create_cave_region(world, player, 'Superbunny Cave (Top)', 'a connector',
|
||||
['Superbunny Cave - Top', 'Superbunny Cave - Bottom'], ['Superbunny Cave Exit (Top)']),
|
||||
create_cave_region(multiworld, player, 'Superbunny Cave (Bottom)', 'a connector', None,
|
||||
create_cave_region(world, player, 'Superbunny Cave (Bottom)', 'a connector', None,
|
||||
['Superbunny Cave Climb', 'Superbunny Cave Exit (Bottom)']),
|
||||
create_cave_region(multiworld, player, 'Spike Cave', 'Spike Cave', ['Spike Cave']),
|
||||
create_cave_region(multiworld, player, 'Hookshot Cave', 'a connector',
|
||||
create_cave_region(world, player, 'Spike Cave', 'Spike Cave', ['Spike Cave']),
|
||||
create_cave_region(world, player, 'Hookshot Cave', 'a connector',
|
||||
['Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right',
|
||||
'Hookshot Cave - Bottom Left'],
|
||||
['Hookshot Cave Exit (South)', 'Hookshot Cave Bomb Wall (South)']),
|
||||
create_cave_region(multiworld, player, 'Hookshot Cave (Upper)', 'a connector', None, ['Hookshot Cave Exit (North)',
|
||||
create_cave_region(world, player, 'Hookshot Cave (Upper)', 'a connector', None, ['Hookshot Cave Exit (North)',
|
||||
'Hookshot Cave Bomb Wall (North)']),
|
||||
create_dw_region(multiworld, player, 'Death Mountain Floating Island (Dark World)', None,
|
||||
create_dw_region(world, player, 'Death Mountain Floating Island (Dark World)', None,
|
||||
['Floating Island Drop', 'Hookshot Cave Back Entrance', 'Floating Island Mirror Spot']),
|
||||
create_lw_region(multiworld, player, 'Death Mountain Floating Island (Light World)', ['Floating Island']),
|
||||
create_dw_region(multiworld, player, 'Turtle Rock (Top)', None, ['Turtle Rock Drop']),
|
||||
create_lw_region(multiworld, player, 'Mimic Cave Ledge', None, ['Mimic Cave']),
|
||||
create_cave_region(multiworld, player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']),
|
||||
create_lw_region(world, player, 'Death Mountain Floating Island (Light World)', ['Floating Island']),
|
||||
create_dw_region(world, player, 'Turtle Rock (Top)', None, ['Turtle Rock Drop']),
|
||||
create_lw_region(world, player, 'Mimic Cave Ledge', None, ['Mimic Cave']),
|
||||
create_cave_region(world, player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']),
|
||||
|
||||
create_dungeon_region(multiworld, player, 'Swamp Palace (Entrance)', 'Swamp Palace', None, ['Swamp Palace Moat', 'Swamp Palace Exit']),
|
||||
create_dungeon_region(multiworld, player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'], ['Swamp Palace Small Key Door']),
|
||||
create_dungeon_region(multiworld, player, 'Swamp Palace (Starting Area)', 'Swamp Palace', ['Swamp Palace - Map Chest', 'Swamp Palace - Pot Row Pot Key',
|
||||
create_dungeon_region(world, player, 'Swamp Palace (Entrance)', 'Swamp Palace', None, ['Swamp Palace Moat', 'Swamp Palace Exit']),
|
||||
create_dungeon_region(world, player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'], ['Swamp Palace Small Key Door']),
|
||||
create_dungeon_region(world, player, 'Swamp Palace (Starting Area)', 'Swamp Palace', ['Swamp Palace - Map Chest', 'Swamp Palace - Pot Row Pot Key',
|
||||
'Swamp Palace - Trench 1 Pot Key'], ['Swamp Palace (Center)']),
|
||||
create_dungeon_region(multiworld, player, 'Swamp Palace (Center)', 'Swamp Palace', ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest', 'Swamp Palace - Hookshot Pot Key',
|
||||
create_dungeon_region(world, player, 'Swamp Palace (Center)', 'Swamp Palace', ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest', 'Swamp Palace - Hookshot Pot Key',
|
||||
'Swamp Palace - Trench 2 Pot Key'], ['Swamp Palace (North)', 'Swamp Palace (West)']),
|
||||
create_dungeon_region(multiworld, player, 'Swamp Palace (West)', 'Swamp Palace', ['Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest']),
|
||||
create_dungeon_region(multiworld, player, 'Swamp Palace (North)', 'Swamp Palace', ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right',
|
||||
create_dungeon_region(world, player, 'Swamp Palace (West)', 'Swamp Palace', ['Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest']),
|
||||
create_dungeon_region(world, player, 'Swamp Palace (North)', 'Swamp Palace', ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right',
|
||||
'Swamp Palace - Waterway Pot Key', 'Swamp Palace - Waterfall Room',
|
||||
'Swamp Palace - Boss', 'Swamp Palace - Prize']),
|
||||
create_dungeon_region(multiworld, player, 'Thieves Town (Entrance)', 'Thieves\' Town', ['Thieves\' Town - Big Key Chest',
|
||||
create_dungeon_region(world, player, 'Thieves Town (Entrance)', 'Thieves\' Town', ['Thieves\' Town - Big Key Chest',
|
||||
'Thieves\' Town - Map Chest',
|
||||
'Thieves\' Town - Compass Chest',
|
||||
'Thieves\' Town - Ambush Chest'], ['Thieves Town Big Key Door', 'Thieves Town Exit']),
|
||||
create_dungeon_region(multiworld, player, 'Thieves Town (Deep)', 'Thieves\' Town', ['Thieves\' Town - Attic',
|
||||
create_dungeon_region(world, player, 'Thieves Town (Deep)', 'Thieves\' Town', ['Thieves\' Town - Attic',
|
||||
'Thieves\' Town - Big Chest',
|
||||
'Thieves\' Town - Hallway Pot Key',
|
||||
'Thieves\' Town - Spike Switch Pot Key',
|
||||
'Thieves\' Town - Blind\'s Cell'], ['Blind Fight']),
|
||||
create_dungeon_region(multiworld, player, 'Blind Fight', 'Thieves\' Town', ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']),
|
||||
create_dungeon_region(multiworld, player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'], ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump', 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']),
|
||||
create_dungeon_region(multiworld, player, 'Skull Woods First Section (Right)', 'Skull Woods', ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']),
|
||||
create_dungeon_region(multiworld, player, 'Skull Woods First Section (Left)', 'Skull Woods', ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'], ['Skull Woods First Section (Left) Door to Exit', 'Skull Woods First Section (Left) Door to Right']),
|
||||
create_dungeon_region(multiworld, player, 'Skull Woods First Section (Top)', 'Skull Woods', ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']),
|
||||
create_dungeon_region(multiworld, player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None, ['Skull Woods Second Section (Drop)']),
|
||||
create_dungeon_region(multiworld, player, 'Skull Woods Second Section', 'Skull Woods', ['Skull Woods - Big Key Chest', 'Skull Woods - West Lobby Pot Key'], ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']),
|
||||
create_dungeon_region(multiworld, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', ['Skull Woods - Bridge Room'], ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']),
|
||||
create_dungeon_region(multiworld, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', ['Skull Woods - Spike Corner Key Drop', 'Skull Woods - Boss', 'Skull Woods - Prize']),
|
||||
create_dungeon_region(multiworld, player, 'Ice Palace (Entrance)', 'Ice Palace', ['Ice Palace - Jelly Key Drop', 'Ice Palace - Compass Chest'], ['Ice Palace (Second Section)', 'Ice Palace Exit']),
|
||||
create_dungeon_region(multiworld, player, 'Ice Palace (Second Section)', 'Ice Palace', ['Ice Palace - Conveyor Key Drop'], ['Ice Palace (Main)']),
|
||||
create_dungeon_region(multiworld, player, 'Ice Palace (Main)', 'Ice Palace', ['Ice Palace - Freezor Chest',
|
||||
create_dungeon_region(world, player, 'Blind Fight', 'Thieves\' Town', ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']),
|
||||
create_dungeon_region(world, player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'], ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump', 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']),
|
||||
create_dungeon_region(world, player, 'Skull Woods First Section (Right)', 'Skull Woods', ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']),
|
||||
create_dungeon_region(world, player, 'Skull Woods First Section (Left)', 'Skull Woods', ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'], ['Skull Woods First Section (Left) Door to Exit', 'Skull Woods First Section (Left) Door to Right']),
|
||||
create_dungeon_region(world, player, 'Skull Woods First Section (Top)', 'Skull Woods', ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']),
|
||||
create_dungeon_region(world, player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None, ['Skull Woods Second Section (Drop)']),
|
||||
create_dungeon_region(world, player, 'Skull Woods Second Section', 'Skull Woods', ['Skull Woods - Big Key Chest', 'Skull Woods - West Lobby Pot Key'], ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']),
|
||||
create_dungeon_region(world, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', ['Skull Woods - Bridge Room'], ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']),
|
||||
create_dungeon_region(world, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', ['Skull Woods - Spike Corner Key Drop', 'Skull Woods - Boss', 'Skull Woods - Prize']),
|
||||
create_dungeon_region(world, player, 'Ice Palace (Entrance)', 'Ice Palace', ['Ice Palace - Jelly Key Drop', 'Ice Palace - Compass Chest'], ['Ice Palace (Second Section)', 'Ice Palace Exit']),
|
||||
create_dungeon_region(world, player, 'Ice Palace (Second Section)', 'Ice Palace', ['Ice Palace - Conveyor Key Drop'], ['Ice Palace (Main)']),
|
||||
create_dungeon_region(world, player, 'Ice Palace (Main)', 'Ice Palace', ['Ice Palace - Freezor Chest',
|
||||
'Ice Palace - Many Pots Pot Key',
|
||||
'Ice Palace - Big Chest', 'Ice Palace - Iced T Room'], ['Ice Palace (East)', 'Ice Palace (Kholdstare)']),
|
||||
create_dungeon_region(multiworld, player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'], ['Ice Palace (East Top)']),
|
||||
create_dungeon_region(multiworld, player, 'Ice Palace (East Top)', 'Ice Palace', ['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest', 'Ice Palace - Hammer Block Key Drop']),
|
||||
create_dungeon_region(multiworld, player, 'Ice Palace (Kholdstare)', 'Ice Palace', ['Ice Palace - Boss', 'Ice Palace - Prize']),
|
||||
create_dungeon_region(multiworld, player, 'Misery Mire (Entrance)', 'Misery Mire', None, ['Misery Mire Entrance Gap', 'Misery Mire Exit']),
|
||||
create_dungeon_region(multiworld, player, 'Misery Mire (Main)', 'Misery Mire', ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby',
|
||||
create_dungeon_region(world, player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'], ['Ice Palace (East Top)']),
|
||||
create_dungeon_region(world, player, 'Ice Palace (East Top)', 'Ice Palace', ['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest', 'Ice Palace - Hammer Block Key Drop']),
|
||||
create_dungeon_region(world, player, 'Ice Palace (Kholdstare)', 'Ice Palace', ['Ice Palace - Boss', 'Ice Palace - Prize']),
|
||||
create_dungeon_region(world, player, 'Misery Mire (Entrance)', 'Misery Mire', None, ['Misery Mire Entrance Gap', 'Misery Mire Exit']),
|
||||
create_dungeon_region(world, player, 'Misery Mire (Main)', 'Misery Mire', ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby',
|
||||
'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest',
|
||||
'Misery Mire - Spikes Pot Key', 'Misery Mire - Fishbone Pot Key',
|
||||
'Misery Mire - Conveyor Crystal Key Drop'], ['Misery Mire (West)', 'Misery Mire Big Key Door']),
|
||||
create_dungeon_region(multiworld, player, 'Misery Mire (West)', 'Misery Mire', ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']),
|
||||
create_dungeon_region(multiworld, player, 'Misery Mire (Final Area)', 'Misery Mire', None, ['Misery Mire (Vitreous)']),
|
||||
create_dungeon_region(multiworld, player, 'Misery Mire (Vitreous)', 'Misery Mire', ['Misery Mire - Boss', 'Misery Mire - Prize']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Entrance)', 'Turtle Rock', None, ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (First Section)', 'Turtle Rock', ['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left',
|
||||
create_dungeon_region(world, player, 'Misery Mire (West)', 'Misery Mire', ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']),
|
||||
create_dungeon_region(world, player, 'Misery Mire (Final Area)', 'Misery Mire', None, ['Misery Mire (Vitreous)']),
|
||||
create_dungeon_region(world, player, 'Misery Mire (Vitreous)', 'Misery Mire', ['Misery Mire - Boss', 'Misery Mire - Prize']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Entrance)', 'Turtle Rock', None, ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (First Section)', 'Turtle Rock', ['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left',
|
||||
'Turtle Rock - Roller Room - Right'],
|
||||
['Turtle Rock Entrance to Pokey Room', 'Turtle Rock Entrance Gap Reverse']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Pokey Room)', 'Turtle Rock', ['Turtle Rock - Pokey 1 Key Drop'], ['Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Pokey Room) (South)']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock', ['Turtle Rock - Chain Chomps'], ['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Second Section)', 'Turtle Rock', ['Turtle Rock - Big Key Chest', 'Turtle Rock - Pokey 2 Key Drop'], ['Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door', 'Turtle Rock Second Section Bomb Wall']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Second Section Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Second Section from Bomb Wall']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Eye Bridge Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Isolated Ledge Exit', 'Turtle Rock Eye Bridge from Bomb Wall']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right',
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Pokey Room)', 'Turtle Rock', ['Turtle Rock - Pokey 1 Key Drop'], ['Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Pokey Room) (South)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock', ['Turtle Rock - Chain Chomps'], ['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock', ['Turtle Rock - Big Key Chest', 'Turtle Rock - Pokey 2 Key Drop'], ['Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door', 'Turtle Rock Second Section Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Second Section Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Second Section from Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Isolated Ledge Exit', 'Turtle Rock Eye Bridge from Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['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 Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Eye Bridge Bomb Wall']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']),
|
||||
create_dungeon_region(multiworld, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']),
|
||||
create_dungeon_region(multiworld, player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'],
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'],
|
||||
['Palace of Darkness Big Key Chest Staircase', 'Palace of Darkness (North)', 'Palace of Darkness Big Key Door']),
|
||||
create_dungeon_region(multiworld, player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness', ['Palace of Darkness - Big Key Chest']),
|
||||
create_dungeon_region(multiworld, player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'], ['Palace of Darkness Hammer Peg Drop']),
|
||||
create_dungeon_region(multiworld, player, 'Palace of Darkness (North)', 'Palace of Darkness', ['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Basement - Right'],
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness', ['Palace of Darkness - Big Key Chest']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'], ['Palace of Darkness Hammer Peg Drop']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (North)', 'Palace of Darkness', ['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Basement - Right'],
|
||||
['Palace of Darkness Spike Statue Room Door', 'Palace of Darkness Maze Door']),
|
||||
create_dungeon_region(multiworld, player, 'Palace of Darkness (Maze)', 'Palace of Darkness', ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Big Chest']),
|
||||
create_dungeon_region(multiworld, player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness', ['Palace of Darkness - Harmless Hellway']),
|
||||
create_dungeon_region(multiworld, player, 'Palace of Darkness (Final Section)', 'Palace of Darkness', ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']),
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Entrance)', 'Ganon\'s Tower', ['Ganons Tower - Bob\'s Torch', 'Ganons Tower - Hope Room - Left',
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Maze)', 'Palace of Darkness', ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Big Chest']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness', ['Palace of Darkness - Harmless Hellway']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Final Section)', 'Palace of Darkness', ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']),
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Entrance)', 'Ganon\'s Tower', ['Ganons Tower - Bob\'s Torch', 'Ganons Tower - Hope Room - Left',
|
||||
'Ganons Tower - Hope Room - Right', 'Ganons Tower - Conveyor Cross Pot Key'],
|
||||
['Ganons Tower (Tile Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower Big Key Door', 'Ganons Tower Exit']),
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Tile Room)', 'Ganon\'s Tower', ['Ganons Tower - Tile Room'], ['Ganons Tower (Tile Room) Key Door']),
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Compass Room)', 'Ganon\'s Tower', ['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right',
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Tile Room)', 'Ganon\'s Tower', ['Ganons Tower - Tile Room'], ['Ganons Tower (Tile Room) Key Door']),
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Compass Room)', 'Ganon\'s Tower', ['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right',
|
||||
'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Compass Room - Bottom Right',
|
||||
'Ganons Tower - Conveyor Star Pits Pot Key'],
|
||||
['Ganons Tower (Bottom) (East)']),
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Hookshot Room)', 'Ganon\'s Tower', ['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right',
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Hookshot Room)', 'Ganon\'s Tower', ['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 - Double Switch Pot Key'],
|
||||
['Ganons Tower (Map Room)', 'Ganons Tower (Double Switch Room)']),
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Map Room)', 'Ganon\'s Tower', ['Ganons Tower - Map Chest']),
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Firesnake Room)', 'Ganon\'s Tower', ['Ganons Tower - Firesnake Room'], ['Ganons Tower (Firesnake Room)']),
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Teleport Room)', 'Ganon\'s Tower', ['Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right',
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Map Room)', 'Ganon\'s Tower', ['Ganons Tower - Map Chest']),
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Firesnake Room)', 'Ganon\'s Tower', ['Ganons Tower - Firesnake Room'], ['Ganons Tower (Firesnake Room)']),
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Teleport Room)', 'Ganon\'s Tower', ['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 (Bottom) (West)']),
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Bottom)', 'Ganon\'s Tower', ['Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest', 'Ganons Tower - Big Key Room - Left',
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Bottom)', 'Ganon\'s Tower', ['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']),
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None, ['Ganons Tower Torch Rooms']),
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Before Moldorm)', 'Ganon\'s Tower', ['Ganons Tower - Mini Helmasaur Room - Left', 'Ganons Tower - Mini Helmasaur Room - Right',
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None, ['Ganons Tower Torch Rooms']),
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Before Moldorm)', 'Ganon\'s Tower', ['Ganons Tower - Mini Helmasaur Room - Left', 'Ganons Tower - Mini Helmasaur Room - Right',
|
||||
'Ganons Tower - Pre-Moldorm Chest', 'Ganons Tower - Mini Helmasaur Key Drop'], ['Ganons Tower Moldorm Door']),
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None, ['Ganons Tower Moldorm Gap']),
|
||||
create_dungeon_region(multiworld, player, 'Agahnim 2', 'Ganon\'s Tower', ['Ganons Tower - Validation Chest', 'Agahnim 2'], None),
|
||||
create_cave_region(multiworld, player, 'Pyramid', 'a drop\'s exit', ['Ganon'], ['Ganon Drop']),
|
||||
create_cave_region(multiworld, player, 'Bottom of Pyramid', 'a drop\'s exit', None, ['Pyramid Exit']),
|
||||
create_dw_region(multiworld, player, 'Pyramid Ledge', None, ['Pyramid Entrance', 'Pyramid Drop']),
|
||||
create_lw_region(multiworld, player, 'Desert Northern Cliffs'),
|
||||
create_dw_region(multiworld, player, 'Dark Death Mountain Bunny Descent Area')
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None, ['Ganons Tower Moldorm Gap']),
|
||||
create_dungeon_region(world, player, 'Agahnim 2', 'Ganon\'s Tower', ['Ganons Tower - Validation Chest', 'Agahnim 2'], None),
|
||||
create_cave_region(world, player, 'Pyramid', 'a drop\'s exit', ['Ganon'], ['Ganon Drop']),
|
||||
create_cave_region(world, player, 'Bottom of Pyramid', 'a drop\'s exit', None, ['Pyramid Exit']),
|
||||
create_dw_region(world, player, 'Pyramid Ledge', None, ['Pyramid Entrance', 'Pyramid Drop']),
|
||||
create_lw_region(world, player, 'Desert Northern Cliffs'),
|
||||
create_dw_region(world, player, 'Dark Death Mountain Bunny Descent Area')
|
||||
]
|
||||
|
||||
|
||||
def create_lw_region(multiworld: MultiWorld, player: int, name: str, locations=None, exits=None):
|
||||
return _create_region(multiworld, player, name, LTTPRegionType.LightWorld, 'Light World', locations, exits)
|
||||
def create_lw_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
|
||||
return _create_region(world, player, name, LTTPRegionType.LightWorld, 'Light World', locations, exits)
|
||||
|
||||
|
||||
def create_dw_region(multiworld: MultiWorld, player: int, name: str, locations=None, exits=None):
|
||||
return _create_region(multiworld, player, name, LTTPRegionType.DarkWorld, 'Dark World', locations, exits)
|
||||
def create_dw_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
|
||||
return _create_region(world, player, name, LTTPRegionType.DarkWorld, 'Dark World', locations, exits)
|
||||
|
||||
|
||||
def create_cave_region(multiworld: MultiWorld, player: int, name: str, hint: str, locations=None, exits=None):
|
||||
return _create_region(multiworld, player, name, LTTPRegionType.Cave, hint, locations, exits)
|
||||
def create_cave_region(world: MultiWorld, player: int, name: str, hint: str, locations=None, exits=None):
|
||||
return _create_region(world, player, name, LTTPRegionType.Cave, hint, locations, exits)
|
||||
|
||||
|
||||
def create_dungeon_region(multiworld: MultiWorld, player: int, name: str, hint: str, locations=None, exits=None):
|
||||
return _create_region(multiworld, player, name, LTTPRegionType.Dungeon, hint, locations, exits)
|
||||
def create_dungeon_region(world: MultiWorld, player: int, name: str, hint: str, locations=None, exits=None):
|
||||
return _create_region(world, player, name, LTTPRegionType.Dungeon, hint, locations, exits)
|
||||
|
||||
|
||||
def _create_region(multiworld: MultiWorld, player: int, name: str, type: LTTPRegionType, hint: str, locations=None,
|
||||
def _create_region(world: MultiWorld, player: int, name: str, type: LTTPRegionType, hint: str, locations=None,
|
||||
exits=None):
|
||||
from .SubClasses import ALttPLocation
|
||||
ret = LTTPRegion(name, type, hint, player, multiworld)
|
||||
ret = LTTPRegion(name, type, hint, player, world)
|
||||
if exits:
|
||||
for exit in exits:
|
||||
ret.create_exit(exit)
|
||||
@@ -422,10 +422,10 @@ def _create_region(multiworld: MultiWorld, player: int, name: str, type: LTTPReg
|
||||
return ret
|
||||
|
||||
|
||||
def mark_light_world_regions(multiworld: MultiWorld, player: int):
|
||||
def mark_light_world_regions(world, player: int):
|
||||
# cross world caves may have some sections marked as both in_light_world, and in_dark_work.
|
||||
# That is ok. the bunny logic will check for this case and incorporate special rules.
|
||||
queue = collections.deque(region for region in multiworld.get_regions(player) if region.type == LTTPRegionType.LightWorld)
|
||||
queue = collections.deque(region for region in world.get_regions(player) if region.type == LTTPRegionType.LightWorld)
|
||||
seen = set(queue)
|
||||
while queue:
|
||||
current = queue.popleft()
|
||||
@@ -438,7 +438,7 @@ def mark_light_world_regions(multiworld: MultiWorld, player: int):
|
||||
seen.add(exit.connected_region)
|
||||
queue.append(exit.connected_region)
|
||||
|
||||
queue = collections.deque(region for region in multiworld.get_regions(player) if region.type == LTTPRegionType.DarkWorld)
|
||||
queue = collections.deque(region for region in world.get_regions(player) if region.type == LTTPRegionType.DarkWorld)
|
||||
seen = set(queue)
|
||||
while queue:
|
||||
current = queue.popleft()
|
||||
|
||||
@@ -19,7 +19,7 @@ import subprocess
|
||||
import threading
|
||||
import concurrent.futures
|
||||
import bsdiff4
|
||||
from typing import Collection, Optional, List, SupportsIndex, TYPE_CHECKING
|
||||
from typing import Collection, Optional, List, SupportsIndex
|
||||
|
||||
from BaseClasses import CollectionState, Region, Location, MultiWorld
|
||||
from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, parse_yaml, read_snes_rom
|
||||
@@ -39,9 +39,6 @@ from .Items import item_table, item_name_groups, progression_items
|
||||
from .EntranceShuffle import door_addresses
|
||||
from .Options import small_key_shuffle
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import ALTTPWorld
|
||||
|
||||
try:
|
||||
from maseya import z3pr
|
||||
from maseya.z3pr.palette_randomizer import build_offset_collections
|
||||
@@ -795,13 +792,13 @@ def get_nonnative_item_sprite(code: int) -> int:
|
||||
# https://discord.com/channels/731205301247803413/827141303330406408/852102450822905886
|
||||
|
||||
|
||||
def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
local_random = multiworld.worlds[player].random
|
||||
local_world = multiworld.worlds[player]
|
||||
def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
local_random = world.worlds[player].random
|
||||
local_world = world.worlds[player]
|
||||
|
||||
# patch items
|
||||
|
||||
for location in multiworld.get_locations(player):
|
||||
for location in world.get_locations(player):
|
||||
if location.address is None or location.shop_slot is not None:
|
||||
continue
|
||||
|
||||
@@ -855,7 +852,7 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
|
||||
rom.write_byte(0x155C9, local_random.choice([0x11, 0x16])) # Randomize GT music too with map shuffle
|
||||
|
||||
# patch entrance/exits/holes
|
||||
for region in multiworld.get_regions(player):
|
||||
for region in world.get_regions(player):
|
||||
for exit in region.exits:
|
||||
if exit.target is not None:
|
||||
if isinstance(exit.addresses, tuple):
|
||||
@@ -888,7 +885,7 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
|
||||
rom.write_int16(0x15DB5 + 2 * offset, 0x0640)
|
||||
elif room_id == 0x00d6 and local_world.fix_trock_exit:
|
||||
rom.write_int16(0x15DB5 + 2 * offset, 0x0134)
|
||||
elif room_id == 0x000c and multiworld.shuffle_ganon: # fix ganons tower exit point
|
||||
elif room_id == 0x000c and world.shuffle_ganon: # fix ganons tower exit point
|
||||
rom.write_int16(0x15DB5 + 2 * offset, 0x00A4)
|
||||
else:
|
||||
rom.write_int16(0x15DB5 + 2 * offset, link_y)
|
||||
@@ -908,9 +905,9 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
|
||||
# patch door table
|
||||
rom.write_byte(0xDBB73 + exit.addresses, exit.target)
|
||||
if local_world.options.mode == 'inverted':
|
||||
patch_shuffled_dark_sanc(multiworld, rom, player)
|
||||
patch_shuffled_dark_sanc(world, rom, player)
|
||||
|
||||
write_custom_shops(rom, multiworld, player)
|
||||
write_custom_shops(rom, world, player)
|
||||
|
||||
def credits_digit(num):
|
||||
# top: $54 is 1, 55 2, etc , so 57=4, 5C=9
|
||||
@@ -984,11 +981,11 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
|
||||
if local_world.options.mode in ['open', 'inverted']:
|
||||
rom.write_byte(0x180032, 0x01) # open mode
|
||||
if local_world.options.mode == 'inverted':
|
||||
set_inverted_mode(multiworld, player, rom)
|
||||
set_inverted_mode(world, player, rom)
|
||||
elif local_world.options.mode == 'standard':
|
||||
rom.write_byte(0x180032, 0x00) # standard mode
|
||||
|
||||
uncle_location = multiworld.get_location('Link\'s Uncle', player)
|
||||
uncle_location = world.get_location('Link\'s Uncle', player)
|
||||
if uncle_location.item is None or uncle_location.item.name not in ['Master Sword', 'Tempered Sword',
|
||||
'Fighter Sword', 'Golden Sword',
|
||||
'Progressive Sword']:
|
||||
@@ -1283,7 +1280,7 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
|
||||
|
||||
# set up goals for treasure hunt
|
||||
rom.write_int16(0x180163, max(0, local_world.treasure_hunt_required -
|
||||
sum(1 for item in multiworld.precollected_items[player] if item.name == "Triforce Piece")))
|
||||
sum(1 for item in world.precollected_items[player] if item.name == "Triforce Piece")))
|
||||
rom.write_bytes(0x180165, [0x0E, 0x28]) # Triforce Piece Sprite
|
||||
rom.write_byte(0x180194, 1) # Must turn in triforced pieces (instant win not enabled)
|
||||
|
||||
@@ -1312,7 +1309,7 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
|
||||
rom.write_bytes(0x50563, [0x3F, 0x14]) # disable below ganon chest
|
||||
rom.write_byte(0x50599, 0x00) # disable below ganon chest
|
||||
rom.write_bytes(0xE9A5, [0x7E, 0x00, 0x24]) # disable below ganon chest
|
||||
rom.write_byte(0x18008B, 0x01 if local_world.options.open_pyramid.to_bool(multiworld, player) else 0x00) # pre-open Pyramid Hole
|
||||
rom.write_byte(0x18008B, 0x01 if local_world.options.open_pyramid.to_bool(world, player) else 0x00) # pre-open Pyramid Hole
|
||||
rom.write_byte(0x18008C, 0x01 if local_world.options.crystals_needed_for_gt == 0 else 0x00) # GT pre-opened if crystal requirement is 0
|
||||
rom.write_byte(0xF5D73, 0xF0) # bees are catchable
|
||||
rom.write_byte(0xF5F10, 0xF0) # bees are catchable
|
||||
@@ -1330,7 +1327,7 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
|
||||
starting_max_bombs = 0 if local_world.options.bombless_start else 10
|
||||
starting_max_arrows = 30
|
||||
|
||||
startingstate = CollectionState(multiworld)
|
||||
startingstate = CollectionState(world)
|
||||
|
||||
if startingstate.has('Silver Bow', player):
|
||||
equip[0x340] = 1
|
||||
@@ -1378,7 +1375,7 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
|
||||
equip[0x37B] = 1
|
||||
equip[0x36E] = 0x80
|
||||
|
||||
for item in multiworld.precollected_items[player]:
|
||||
for item in world.precollected_items[player]:
|
||||
|
||||
if item.name in {'Bow', 'Silver Bow', 'Silver Arrows', 'Progressive Bow', 'Progressive Bow (Alt)',
|
||||
'Titans Mitts', 'Power Glove', 'Progressive Glove',
|
||||
@@ -1593,7 +1590,7 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
|
||||
}
|
||||
|
||||
def get_reveal_bytes(itemName):
|
||||
locations = multiworld.find_item_locations(itemName, player)
|
||||
locations = world.find_item_locations(itemName, player)
|
||||
if len(locations) < 1:
|
||||
return 0x0000
|
||||
location = locations[0]
|
||||
@@ -1670,7 +1667,7 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
|
||||
rom.write_byte(0x18004C, 0x01)
|
||||
|
||||
# set correct flag for hera basement item
|
||||
hera_basement = multiworld.get_location('Tower of Hera - Basement Cage', player)
|
||||
hera_basement = world.get_location('Tower of Hera - Basement Cage', player)
|
||||
if hera_basement.item is not None and hera_basement.item.name == 'Small Key (Tower of Hera)' and hera_basement.item.player == player:
|
||||
rom.write_byte(0x4E3BB, 0xE4)
|
||||
else:
|
||||
@@ -1687,26 +1684,27 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
|
||||
rom.write_byte(0xFEE41, 0x2A) # bombable exit
|
||||
|
||||
if local_world.options.tile_shuffle:
|
||||
tile_set = TileSet.get_random_tile_set(multiworld.worlds[player].random)
|
||||
tile_set = TileSet.get_random_tile_set(world.worlds[player].random)
|
||||
rom.write_byte(0x4BA21, tile_set.get_speed())
|
||||
rom.write_byte(0x4BA1D, tile_set.get_len())
|
||||
rom.write_bytes(0x4BA2A, tile_set.get_bytes())
|
||||
|
||||
write_strings(rom, multiworld, player)
|
||||
write_strings(rom, world, player)
|
||||
|
||||
# remote items flag, does not currently work
|
||||
rom.write_byte(0x18637C, 0)
|
||||
|
||||
# set rom name
|
||||
# 21 bytes
|
||||
rom.name = bytearray(f'AP{local_world.world_version.as_simple_string().replace(".", "")[0:3]}_{player}_{multiworld.seed:11}\0', 'utf8')[:21]
|
||||
from Utils import __version__
|
||||
rom.name = bytearray(f'AP{__version__.replace(".", "")[0:3]}_{player}_{world.seed:11}\0', 'utf8')[:21]
|
||||
rom.name.extend([0] * (21 - len(rom.name)))
|
||||
rom.write_bytes(0x7FC0, rom.name)
|
||||
|
||||
# set player names
|
||||
encoded_players = multiworld.players + len(multiworld.groups)
|
||||
encoded_players = world.players + len(world.groups)
|
||||
for p in range(1, min(encoded_players, ROM_PLAYER_LIMIT) + 1):
|
||||
rom.write_bytes(0x195FFC + ((p - 1) * 32), hud_format_text(multiworld.player_name[p]))
|
||||
rom.write_bytes(0x195FFC + ((p - 1) * 32), hud_format_text(world.player_name[p]))
|
||||
if encoded_players > ROM_PLAYER_LIMIT:
|
||||
rom.write_bytes(0x195FFC + ((ROM_PLAYER_LIMIT - 1) * 32), hud_format_text("Archipelago"))
|
||||
|
||||
@@ -1725,9 +1723,9 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
|
||||
return rom
|
||||
|
||||
|
||||
def patch_race_rom(rom: LocalRom, multiworld: MultiWorld, player: int):
|
||||
def patch_race_rom(rom, world, player):
|
||||
rom.write_bytes(0x180213, [0x01, 0x00]) # Tournament Seed
|
||||
rom.encrypt(multiworld, player)
|
||||
rom.encrypt(world, player)
|
||||
|
||||
|
||||
def get_price_data(price: int, price_type: int) -> List[int]:
|
||||
@@ -1740,8 +1738,8 @@ def get_price_data(price: int, price_type: int) -> List[int]:
|
||||
return int16_as_bytes(price)
|
||||
|
||||
|
||||
def write_custom_shops(rom: LocalRom, multiworld: MultiWorld, player: int):
|
||||
shops = sorted([shop for shop in multiworld.worlds[player].shops if shop.custom], key=lambda shop: shop.sram_offset)
|
||||
def write_custom_shops(rom, world, player):
|
||||
shops = sorted([shop for shop in world.worlds[player].shops if shop.custom], key=lambda shop: shop.sram_offset)
|
||||
|
||||
shop_data = bytearray()
|
||||
items_data = bytearray()
|
||||
@@ -1760,9 +1758,9 @@ def write_custom_shops(rom: LocalRom, multiworld: MultiWorld, player: int):
|
||||
slot = 0 if shop.type == ShopType.TakeAny else index
|
||||
if item is None:
|
||||
break
|
||||
if multiworld.worlds[player].options.shop_item_slots or shop.type == ShopType.TakeAny:
|
||||
count_shop = (shop.region.name != 'Potion Shop' or multiworld.worlds[player].options.include_witch_hut) and \
|
||||
(shop.region.name != 'Capacity Upgrade' or multiworld.worlds[player].options.shuffle_capacity_upgrades)
|
||||
if world.worlds[player].options.shop_item_slots or shop.type == ShopType.TakeAny:
|
||||
count_shop = (shop.region.name != 'Potion Shop' or world.worlds[player].options.include_witch_hut) and \
|
||||
(shop.region.name != 'Capacity Upgrade' or world.worlds[player].options.shuffle_capacity_upgrades)
|
||||
rom.write_byte(0x186560 + shop.sram_offset + slot, 1 if count_shop else 0)
|
||||
if item['item'] == 'Single Arrow' and item['player'] == 0:
|
||||
arrow_mask |= 1 << index
|
||||
@@ -1775,11 +1773,11 @@ def write_custom_shops(rom: LocalRom, multiworld: MultiWorld, player: int):
|
||||
price_data = get_price_data(item['price'], item["price_type"])
|
||||
replacement_price_data = get_price_data(item['replacement_price'], item['replacement_price_type'])
|
||||
slot = 0 if shop.type == ShopType.TakeAny else index
|
||||
if item['player'] and multiworld.game[item['player']] != "A Link to the Past": # item not native to ALTTP
|
||||
item_code = get_nonnative_item_sprite(multiworld.worlds[item['player']].item_name_to_id[item['item']])
|
||||
if item['player'] and world.game[item['player']] != "A Link to the Past": # item not native to ALTTP
|
||||
item_code = get_nonnative_item_sprite(world.worlds[item['player']].item_name_to_id[item['item']])
|
||||
else:
|
||||
item_code = item_table[item["item"]].item_code
|
||||
if item['item'] == 'Single Arrow' and item['player'] == 0 and multiworld.worlds[player].options.retro_bow:
|
||||
if item['item'] == 'Single Arrow' and item['player'] == 0 and world.worlds[player].options.retro_bow:
|
||||
rom.write_byte(0x186500 + shop.sram_offset + slot, arrow_mask)
|
||||
|
||||
item_data = [shop_id, item_code] + price_data + \
|
||||
@@ -1792,12 +1790,12 @@ def write_custom_shops(rom: LocalRom, multiworld: MultiWorld, player: int):
|
||||
items_data.extend([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])
|
||||
rom.write_bytes(0x184900, items_data)
|
||||
|
||||
if multiworld.worlds[player].options.retro_bow:
|
||||
if world.worlds[player].options.retro_bow:
|
||||
retro_shop_slots.append(0xFF)
|
||||
rom.write_bytes(0x186540, retro_shop_slots)
|
||||
|
||||
|
||||
def hud_format_text(text: str):
|
||||
def hud_format_text(text):
|
||||
output = bytes()
|
||||
for char in text.lower():
|
||||
if 'a' <= char <= 'z':
|
||||
@@ -1814,7 +1812,7 @@ def hud_format_text(text: str):
|
||||
output += b'\x7f\x00'
|
||||
return output[:32]
|
||||
|
||||
def apply_oof_sfx(rom: LocalRom, oof: str):
|
||||
def apply_oof_sfx(rom, oof: str):
|
||||
with open(oof, 'rb') as stream:
|
||||
oof_bytes = bytearray(stream.read())
|
||||
|
||||
@@ -1864,10 +1862,9 @@ def apply_oof_sfx(rom: LocalRom, oof: str):
|
||||
rom.write_bytes(0x13000D, [0x00, 0x00, 0x00, 0x08])
|
||||
|
||||
|
||||
def apply_rom_settings(rom: LocalRom, beep: str, color: str, quickswap: bool, menuspeed: str, music: bool, sprite: str,
|
||||
oof: str, palettes_options: dict[str, str], world: "ALTTPWorld | None" = None, player: int = 1,
|
||||
allow_random_on_event: bool = False, reduceflashing: bool = False, triforcehud: str = None,
|
||||
deathlink: bool = False, allowcollect: bool = False):
|
||||
def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, sprite: str, oof: str, palettes_options,
|
||||
world=None, player=1, allow_random_on_event=False, reduceflashing=False,
|
||||
triforcehud: str = None, deathlink: bool = False, allowcollect: bool = False):
|
||||
local_random = random if not world else world.worlds[player].random
|
||||
disable_music: bool = not music
|
||||
# enable instant item menu
|
||||
@@ -1951,7 +1948,7 @@ def apply_rom_settings(rom: LocalRom, beep: str, color: str, quickswap: bool, me
|
||||
rom.write_byte(0x180167, triforce_flag)
|
||||
|
||||
if z3pr:
|
||||
def buildAndRandomize(option_name: str, mode: str):
|
||||
def buildAndRandomize(option_name, mode):
|
||||
options = {
|
||||
option_name: True
|
||||
}
|
||||
@@ -2015,7 +2012,7 @@ def apply_rom_settings(rom: LocalRom, beep: str, color: str, quickswap: bool, me
|
||||
rom.write_crc()
|
||||
|
||||
|
||||
def restore_maseya_colors(rom: LocalRom, offsets_array: list[list[int]]):
|
||||
def restore_maseya_colors(rom, offsets_array):
|
||||
if not rom.orig_buffer:
|
||||
return
|
||||
for offsetC in offsets_array:
|
||||
@@ -2023,7 +2020,7 @@ def restore_maseya_colors(rom: LocalRom, offsets_array: list[list[int]]):
|
||||
rom.write_bytes(address, rom.orig_buffer[address:address + 2])
|
||||
|
||||
|
||||
def set_color(rom: LocalRom, address: int, color: tuple[int, int, int], shade: int):
|
||||
def set_color(rom, address, color, shade):
|
||||
r = round(min(color[0], 0xFF) * pow(0.8, shade) * 0x1F / 0xFF)
|
||||
g = round(min(color[1], 0xFF) * pow(0.8, shade) * 0x1F / 0xFF)
|
||||
b = round(min(color[2], 0xFF) * pow(0.8, shade) * 0x1F / 0xFF)
|
||||
@@ -2031,7 +2028,7 @@ def set_color(rom: LocalRom, address: int, color: tuple[int, int, int], shade: i
|
||||
rom.write_bytes(address, ((b << 10) | (g << 5) | (r << 0)).to_bytes(2, byteorder='little', signed=False))
|
||||
|
||||
|
||||
def default_ow_palettes(rom: LocalRom):
|
||||
def default_ow_palettes(rom):
|
||||
if not rom.orig_buffer:
|
||||
return
|
||||
rom.write_bytes(0xDE604, rom.orig_buffer[0xDE604:0xDEBB4])
|
||||
@@ -2040,7 +2037,7 @@ def default_ow_palettes(rom: LocalRom):
|
||||
rom.write_bytes(address, rom.orig_buffer[address:address + 2])
|
||||
|
||||
|
||||
def randomize_ow_palettes(rom: LocalRom, local_random: random.Random):
|
||||
def randomize_ow_palettes(rom, local_random):
|
||||
grass, grass2, grass3, dirt, dirt2, water, clouds, dwdirt, \
|
||||
dwgrass, dwwater, dwdmdirt, dwdmgrass, dwdmclouds1, dwdmclouds2 = [[local_random.randint(60, 215) for _ in range(3)]
|
||||
for _ in range(14)]
|
||||
@@ -2116,7 +2113,7 @@ def randomize_ow_palettes(rom: LocalRom, local_random: random.Random):
|
||||
set_color(rom, address, color, shade)
|
||||
|
||||
|
||||
def blackout_ow_palettes(rom: LocalRom):
|
||||
def blackout_ow_palettes(rom):
|
||||
rom.write_bytes(0xDE604, [0] * 0xC4)
|
||||
for i in range(0xDE6C8, 0xDE86C, 70):
|
||||
rom.write_bytes(i, [0] * 64)
|
||||
@@ -2127,13 +2124,13 @@ def blackout_ow_palettes(rom: LocalRom):
|
||||
rom.write_bytes(address, [0, 0])
|
||||
|
||||
|
||||
def default_uw_palettes(rom: LocalRom):
|
||||
def default_uw_palettes(rom):
|
||||
if not rom.orig_buffer:
|
||||
return
|
||||
rom.write_bytes(0xDD734, rom.orig_buffer[0xDD734:0xDE544])
|
||||
|
||||
|
||||
def randomize_uw_palettes(rom: LocalRom, local_random: random.Random):
|
||||
def randomize_uw_palettes(rom, local_random):
|
||||
for dungeon in range(20):
|
||||
wall, pot, chest, floor1, floor2, floor3 = [[local_random.randint(60, 240) for _ in range(3)] for _ in range(6)]
|
||||
|
||||
@@ -2180,7 +2177,7 @@ def randomize_uw_palettes(rom: LocalRom, local_random: random.Random):
|
||||
set_color(rom, 0x0DD796 + (0xB4 * dungeon), floor3, 4)
|
||||
|
||||
|
||||
def blackout_uw_palettes(rom: LocalRom):
|
||||
def blackout_uw_palettes(rom):
|
||||
for i in range(0xDD734, 0xDE544, 180):
|
||||
rom.write_bytes(i, [0] * 38)
|
||||
rom.write_bytes(i + 44, [0] * 76)
|
||||
@@ -2191,25 +2188,25 @@ def get_hash_string(hash):
|
||||
return ", ".join([hash_alphabet[code & 0x1F] for code in hash])
|
||||
|
||||
|
||||
def write_string_to_rom(rom: LocalRom, target: str, string: str):
|
||||
def write_string_to_rom(rom, target, string):
|
||||
address, maxbytes = text_addresses[target]
|
||||
rom.write_bytes(address, MultiByteTextMapper.convert(string, maxbytes))
|
||||
|
||||
|
||||
def write_strings(rom: LocalRom, multiworld: MultiWorld, player: int):
|
||||
def write_strings(rom, world, player):
|
||||
from . import ALTTPWorld
|
||||
local_random = multiworld.worlds[player].random
|
||||
w: ALTTPWorld = multiworld.worlds[player]
|
||||
local_random = world.worlds[player].random
|
||||
w: ALTTPWorld = world.worlds[player]
|
||||
|
||||
tt = TextTable()
|
||||
tt.removeUnwantedText()
|
||||
|
||||
# Let's keep this guy's text accurate to the shuffle setting.
|
||||
if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']:
|
||||
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']:
|
||||
tt['kakariko_flophouse_man_no_flippers'] = 'I really hate mowing my yard.\n{PAGEBREAK}\nI should move.'
|
||||
tt['kakariko_flophouse_man'] = 'I really hate mowing my yard.\n{PAGEBREAK}\nI should move.'
|
||||
|
||||
if multiworld.worlds[player].options.mode == 'inverted':
|
||||
if world.worlds[player].options.mode == 'inverted':
|
||||
tt['sign_village_of_outcasts'] = 'attention\nferal ducks sighted\nhiding in statues\n\nflute players beware\n'
|
||||
|
||||
def hint_text(dest, ped_hint=False):
|
||||
@@ -2221,45 +2218,45 @@ def write_strings(rom: LocalRom, multiworld: MultiWorld, player: int):
|
||||
hint = dest.hint_text
|
||||
if dest.player != player:
|
||||
if ped_hint:
|
||||
hint += f" for {multiworld.player_name[dest.player]}!"
|
||||
hint += f" for {world.player_name[dest.player]}!"
|
||||
elif isinstance(dest, (Region, Location)):
|
||||
hint += f" in {multiworld.player_name[dest.player]}'s world"
|
||||
hint += f" in {world.player_name[dest.player]}'s world"
|
||||
else:
|
||||
hint += f" for {multiworld.player_name[dest.player]}"
|
||||
hint += f" for {world.player_name[dest.player]}"
|
||||
return hint
|
||||
|
||||
if multiworld.worlds[player].options.scams.gives_king_zora_hint:
|
||||
if world.worlds[player].options.scams.gives_king_zora_hint:
|
||||
# Zora hint
|
||||
zora_location = multiworld.get_location("King Zora", player)
|
||||
zora_location = world.get_location("King Zora", player)
|
||||
tt['zora_tells_cost'] = f"You got 500 rupees to buy {hint_text(zora_location.item)}" \
|
||||
f"\n ≥ Duh\n Oh carp\n{{CHOICE}}"
|
||||
if multiworld.worlds[player].options.scams.gives_bottle_merchant_hint:
|
||||
if world.worlds[player].options.scams.gives_bottle_merchant_hint:
|
||||
# Bottle Vendor hint
|
||||
vendor_location = multiworld.get_location("Bottle Merchant", player)
|
||||
vendor_location = world.get_location("Bottle Merchant", player)
|
||||
tt['bottle_vendor_choice'] = f"I gots {hint_text(vendor_location.item)}\nYous gots 100 rupees?" \
|
||||
f"\n ≥ I want\n no way!\n{{CHOICE}}"
|
||||
|
||||
# First we write hints about entrances, some from the inconvenient list others from all reasonable entrances.
|
||||
if multiworld.worlds[player].options.hints:
|
||||
if multiworld.worlds[player].options.hints.value >= 2:
|
||||
if multiworld.worlds[player].options.hints == "full":
|
||||
if world.worlds[player].options.hints:
|
||||
if world.worlds[player].options.hints.value >= 2:
|
||||
if world.worlds[player].options.hints == "full":
|
||||
tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles have hints!'
|
||||
else:
|
||||
tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles can have hints!'
|
||||
hint_locations = HintLocations.copy()
|
||||
local_random.shuffle(hint_locations)
|
||||
all_entrances = list(multiworld.get_entrances(player))
|
||||
all_entrances = list(world.get_entrances(player))
|
||||
local_random.shuffle(all_entrances)
|
||||
|
||||
# First we take care of the one inconvenient dungeon in the appropriately simple shuffles.
|
||||
entrances_to_hint = {}
|
||||
entrances_to_hint.update(InconvenientDungeonEntrances)
|
||||
if multiworld.shuffle_ganon:
|
||||
if multiworld.worlds[player].options.mode == 'inverted':
|
||||
if world.shuffle_ganon:
|
||||
if world.worlds[player].options.mode == 'inverted':
|
||||
entrances_to_hint.update({'Inverted Ganons Tower': 'The sealed castle door'})
|
||||
else:
|
||||
entrances_to_hint.update({'Ganons Tower': 'Ganon\'s Tower'})
|
||||
if multiworld.worlds[player].options.entrance_shuffle in ['simple', 'restricted']:
|
||||
if world.worlds[player].options.entrance_shuffle in ['simple', 'restricted']:
|
||||
for entrance in all_entrances:
|
||||
if entrance.name in entrances_to_hint:
|
||||
this_hint = entrances_to_hint[entrance.name] + ' leads to ' + hint_text(
|
||||
@@ -2269,9 +2266,9 @@ def write_strings(rom: LocalRom, multiworld: MultiWorld, player: int):
|
||||
break
|
||||
# Now we write inconvenient locations for most shuffles and finish taking care of the less chaotic ones.
|
||||
entrances_to_hint.update(InconvenientOtherEntrances)
|
||||
if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
||||
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
||||
hint_count = 0
|
||||
elif multiworld.worlds[player].options.entrance_shuffle in ['simple', 'restricted']:
|
||||
elif world.worlds[player].options.entrance_shuffle in ['simple', 'restricted']:
|
||||
hint_count = 2
|
||||
else:
|
||||
hint_count = 4
|
||||
@@ -2288,31 +2285,31 @@ def write_strings(rom: LocalRom, multiworld: MultiWorld, player: int):
|
||||
|
||||
# Next we handle hints for randomly selected other entrances,
|
||||
# curating the selection intelligently based on shuffle.
|
||||
if multiworld.worlds[player].options.entrance_shuffle not in ['simple', 'restricted']:
|
||||
if world.worlds[player].options.entrance_shuffle not in ['simple', 'restricted']:
|
||||
entrances_to_hint.update(ConnectorEntrances)
|
||||
entrances_to_hint.update(DungeonEntrances)
|
||||
if multiworld.worlds[player].options.mode == 'inverted':
|
||||
if world.worlds[player].options.mode == 'inverted':
|
||||
entrances_to_hint.update({'Inverted Agahnims Tower': 'The dark mountain tower'})
|
||||
else:
|
||||
entrances_to_hint.update({'Agahnims Tower': 'The sealed castle door'})
|
||||
elif multiworld.worlds[player].options.entrance_shuffle == 'restricted':
|
||||
elif world.worlds[player].options.entrance_shuffle == 'restricted':
|
||||
entrances_to_hint.update(ConnectorEntrances)
|
||||
entrances_to_hint.update(OtherEntrances)
|
||||
if multiworld.worlds[player].options.mode == 'inverted':
|
||||
if world.worlds[player].options.mode == 'inverted':
|
||||
entrances_to_hint.update({'Inverted Dark Sanctuary': 'The dark sanctuary cave'})
|
||||
entrances_to_hint.update({'Inverted Big Bomb Shop': 'The old hero\'s dark home'})
|
||||
entrances_to_hint.update({'Inverted Links House': 'The old hero\'s light home'})
|
||||
else:
|
||||
entrances_to_hint.update({'Dark Sanctuary Hint': 'The dark sanctuary cave'})
|
||||
entrances_to_hint.update({'Big Bomb Shop': 'The old bomb shop'})
|
||||
if multiworld.worlds[player].options.entrance_shuffle != 'insanity':
|
||||
if world.worlds[player].options.entrance_shuffle != 'insanity':
|
||||
entrances_to_hint.update(InsanityEntrances)
|
||||
if multiworld.shuffle_ganon:
|
||||
if multiworld.worlds[player].options.mode == 'inverted':
|
||||
if world.shuffle_ganon:
|
||||
if world.worlds[player].options.mode == 'inverted':
|
||||
entrances_to_hint.update({'Inverted Pyramid Entrance': 'The extra castle passage'})
|
||||
else:
|
||||
entrances_to_hint.update({'Pyramid Ledge': 'The pyramid ledge'})
|
||||
hint_count = 4 if multiworld.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full',
|
||||
hint_count = 4 if world.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full',
|
||||
'dungeons_crossed'] else 0
|
||||
for entrance in all_entrances:
|
||||
if entrance.name in entrances_to_hint:
|
||||
@@ -2327,77 +2324,77 @@ def write_strings(rom: LocalRom, multiworld: MultiWorld, player: int):
|
||||
|
||||
# Next we write a few hints for specific inconvenient locations. We don't make many because in entrance this is highly unpredictable.
|
||||
locations_to_hint = InconvenientLocations.copy()
|
||||
if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
||||
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
||||
locations_to_hint.extend(InconvenientVanillaLocations)
|
||||
local_random.shuffle(locations_to_hint)
|
||||
hint_count = 3 if multiworld.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full',
|
||||
hint_count = 3 if world.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full',
|
||||
'dungeons_crossed'] else 5
|
||||
for location in locations_to_hint[:hint_count]:
|
||||
if location == 'Swamp Left':
|
||||
if local_random.randint(0, 1):
|
||||
first_item = hint_text(multiworld.get_location('Swamp Palace - West Chest', player).item)
|
||||
second_item = hint_text(multiworld.get_location('Swamp Palace - Big Key Chest', player).item)
|
||||
first_item = hint_text(world.get_location('Swamp Palace - West Chest', player).item)
|
||||
second_item = hint_text(world.get_location('Swamp Palace - Big Key Chest', player).item)
|
||||
else:
|
||||
second_item = hint_text(multiworld.get_location('Swamp Palace - West Chest', player).item)
|
||||
first_item = hint_text(multiworld.get_location('Swamp Palace - Big Key Chest', player).item)
|
||||
second_item = hint_text(world.get_location('Swamp Palace - West Chest', player).item)
|
||||
first_item = hint_text(world.get_location('Swamp Palace - Big Key Chest', player).item)
|
||||
this_hint = ('The westmost chests in Swamp Palace contain ' + first_item + ' and ' + second_item + '.')
|
||||
tt[hint_locations.pop(0)] = this_hint
|
||||
elif location == 'Mire Left':
|
||||
if local_random.randint(0, 1):
|
||||
first_item = hint_text(multiworld.get_location('Misery Mire - Compass Chest', player).item)
|
||||
second_item = hint_text(multiworld.get_location('Misery Mire - Big Key Chest', player).item)
|
||||
first_item = hint_text(world.get_location('Misery Mire - Compass Chest', player).item)
|
||||
second_item = hint_text(world.get_location('Misery Mire - Big Key Chest', player).item)
|
||||
else:
|
||||
second_item = hint_text(multiworld.get_location('Misery Mire - Compass Chest', player).item)
|
||||
first_item = hint_text(multiworld.get_location('Misery Mire - Big Key Chest', player).item)
|
||||
second_item = hint_text(world.get_location('Misery Mire - Compass Chest', player).item)
|
||||
first_item = hint_text(world.get_location('Misery Mire - Big Key Chest', player).item)
|
||||
this_hint = ('The westmost chests in Misery Mire contain ' + first_item + ' and ' + second_item + '.')
|
||||
tt[hint_locations.pop(0)] = this_hint
|
||||
elif location == 'Tower of Hera - Big Key Chest':
|
||||
this_hint = 'Waiting in the Tower of Hera basement leads to ' + hint_text(
|
||||
multiworld.get_location(location, player).item) + '.'
|
||||
world.get_location(location, player).item) + '.'
|
||||
tt[hint_locations.pop(0)] = this_hint
|
||||
elif location == 'Ganons Tower - Big Chest':
|
||||
this_hint = 'The big chest in Ganon\'s Tower contains ' + hint_text(
|
||||
multiworld.get_location(location, player).item) + '.'
|
||||
world.get_location(location, player).item) + '.'
|
||||
tt[hint_locations.pop(0)] = this_hint
|
||||
elif location == 'Thieves\' Town - Big Chest':
|
||||
this_hint = 'The big chest in Thieves\' Town contains ' + hint_text(
|
||||
multiworld.get_location(location, player).item) + '.'
|
||||
world.get_location(location, player).item) + '.'
|
||||
tt[hint_locations.pop(0)] = this_hint
|
||||
elif location == 'Ice Palace - Big Chest':
|
||||
this_hint = 'The big chest in Ice Palace contains ' + hint_text(
|
||||
multiworld.get_location(location, player).item) + '.'
|
||||
world.get_location(location, player).item) + '.'
|
||||
tt[hint_locations.pop(0)] = this_hint
|
||||
elif location == 'Eastern Palace - Big Key Chest':
|
||||
this_hint = 'The antifairy guarded chest in Eastern Palace contains ' + hint_text(
|
||||
multiworld.get_location(location, player).item) + '.'
|
||||
world.get_location(location, player).item) + '.'
|
||||
tt[hint_locations.pop(0)] = this_hint
|
||||
elif location == 'Sahasrahla':
|
||||
this_hint = 'Sahasrahla seeks a green pendant for ' + hint_text(
|
||||
multiworld.get_location(location, player).item) + '.'
|
||||
world.get_location(location, player).item) + '.'
|
||||
tt[hint_locations.pop(0)] = this_hint
|
||||
elif location == 'Graveyard Cave':
|
||||
this_hint = 'The cave north of the graveyard contains ' + hint_text(
|
||||
multiworld.get_location(location, player).item) + '.'
|
||||
world.get_location(location, player).item) + '.'
|
||||
tt[hint_locations.pop(0)] = this_hint
|
||||
else:
|
||||
this_hint = location + ' contains ' + hint_text(multiworld.get_location(location, player).item) + '.'
|
||||
this_hint = location + ' contains ' + hint_text(world.get_location(location, player).item) + '.'
|
||||
tt[hint_locations.pop(0)] = this_hint
|
||||
|
||||
# Lastly we write hints to show where certain interesting items are.
|
||||
items_to_hint = RelevantItems.copy()
|
||||
if multiworld.worlds[player].options.small_key_shuffle.hints_useful:
|
||||
if world.worlds[player].options.small_key_shuffle.hints_useful:
|
||||
items_to_hint |= item_name_groups["Small Keys"]
|
||||
if multiworld.worlds[player].options.big_key_shuffle.hints_useful:
|
||||
if world.worlds[player].options.big_key_shuffle.hints_useful:
|
||||
items_to_hint |= item_name_groups["Big Keys"]
|
||||
|
||||
if multiworld.worlds[player].options.hints == "full":
|
||||
if world.worlds[player].options.hints == "full":
|
||||
hint_count = len(hint_locations) # fill all remaining hint locations with Item hints.
|
||||
else:
|
||||
hint_count = 5 if multiworld.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full',
|
||||
hint_count = 5 if world.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full',
|
||||
'dungeons_crossed'] else 8
|
||||
hint_count = min(hint_count, len(items_to_hint), len(hint_locations))
|
||||
if hint_count:
|
||||
locations = multiworld.find_items_in_locations(items_to_hint, player, True)
|
||||
locations = world.find_items_in_locations(items_to_hint, player, True)
|
||||
local_random.shuffle(locations)
|
||||
# make locked locations less likely to appear as hint,
|
||||
# chances are the lock means the player already knows.
|
||||
@@ -2417,15 +2414,15 @@ def write_strings(rom: LocalRom, multiworld: MultiWorld, player: int):
|
||||
|
||||
# We still need the older hints of course. Those are done here.
|
||||
|
||||
silverarrows = multiworld.find_item_locations('Silver Bow', player, True)
|
||||
silverarrows = world.find_item_locations('Silver Bow', player, True)
|
||||
local_random.shuffle(silverarrows)
|
||||
silverarrow_hint = (
|
||||
' %s?' % hint_text(silverarrows[0]).replace('Ganon\'s', 'my')) if silverarrows else '?\nI think not!'
|
||||
tt['ganon_phase_3_no_silvers'] = 'Did you find the silver arrows%s' % silverarrow_hint
|
||||
tt['ganon_phase_3_no_silvers_alt'] = 'Did you find the silver arrows%s' % silverarrow_hint
|
||||
if multiworld.worlds[player].has_progressive_bows and (w.difficulty_requirements.progressive_bow_limit >= 2 or (
|
||||
multiworld.worlds[player].options.swordless or multiworld.worlds[player].options.glitches_required == 'no_glitches')):
|
||||
prog_bow_locs = multiworld.find_item_locations('Progressive Bow', player, True)
|
||||
if world.worlds[player].has_progressive_bows and (w.difficulty_requirements.progressive_bow_limit >= 2 or (
|
||||
world.worlds[player].options.swordless or world.worlds[player].options.glitches_required == 'no_glitches')):
|
||||
prog_bow_locs = world.find_item_locations('Progressive Bow', player, True)
|
||||
local_random.shuffle(prog_bow_locs)
|
||||
found_bow = False
|
||||
found_bow_alt = False
|
||||
@@ -2440,34 +2437,34 @@ def write_strings(rom: LocalRom, multiworld: MultiWorld, player: int):
|
||||
silverarrow_hint = (' %s?' % hint_text(bow_loc).replace('Ganon\'s', 'my'))
|
||||
tt[target] = 'Did you find the silver arrows%s' % silverarrow_hint
|
||||
|
||||
crystal5 = multiworld.find_item('Crystal 5', player)
|
||||
crystal6 = multiworld.find_item('Crystal 6', player)
|
||||
crystal5 = world.find_item('Crystal 5', player)
|
||||
crystal6 = world.find_item('Crystal 6', player)
|
||||
tt['bomb_shop'] = 'Big Bomb?\nMy supply is blocked until you clear %s and %s.' % (
|
||||
crystal5.hint_text, crystal6.hint_text)
|
||||
|
||||
greenpendant = multiworld.find_item('Green Pendant', player)
|
||||
greenpendant = world.find_item('Green Pendant', player)
|
||||
tt['sahasrahla_bring_courage'] = 'I lost my family heirloom in %s' % greenpendant.hint_text
|
||||
|
||||
if multiworld.worlds[player].options.crystals_needed_for_gt == 1:
|
||||
if world.worlds[player].options.crystals_needed_for_gt == 1:
|
||||
tt['sign_ganons_tower'] = 'You need a crystal to enter.'
|
||||
else:
|
||||
tt['sign_ganons_tower'] = f'You need {multiworld.worlds[player].options.crystals_needed_for_gt} crystals to enter.'
|
||||
tt['sign_ganons_tower'] = f'You need {world.worlds[player].options.crystals_needed_for_gt} crystals to enter.'
|
||||
|
||||
if multiworld.worlds[player].options.goal == 'bosses':
|
||||
if world.worlds[player].options.goal == 'bosses':
|
||||
tt['sign_ganon'] = 'You need to kill all bosses, Ganon last.'
|
||||
elif multiworld.worlds[player].options.goal == 'ganon_pedestal':
|
||||
elif world.worlds[player].options.goal == 'ganon_pedestal':
|
||||
tt['sign_ganon'] = 'You need to pull the pedestal to defeat Ganon.'
|
||||
elif multiworld.worlds[player].options.goal == "ganon":
|
||||
if multiworld.worlds[player].options.crystals_needed_for_ganon == 1:
|
||||
elif world.worlds[player].options.goal == "ganon":
|
||||
if world.worlds[player].options.crystals_needed_for_ganon == 1:
|
||||
tt['sign_ganon'] = 'You need a crystal to beat Ganon and have beaten Agahnim atop Ganons Tower.'
|
||||
else:
|
||||
tt['sign_ganon'] = f'You need {multiworld.worlds[player].options.crystals_needed_for_ganon} crystals to beat Ganon and ' \
|
||||
tt['sign_ganon'] = f'You need {world.worlds[player].options.crystals_needed_for_ganon} crystals to beat Ganon and ' \
|
||||
f'have beaten Agahnim atop Ganons Tower'
|
||||
else:
|
||||
if multiworld.worlds[player].options.crystals_needed_for_ganon == 1:
|
||||
if world.worlds[player].options.crystals_needed_for_ganon == 1:
|
||||
tt['sign_ganon'] = 'You need a crystal to beat Ganon.'
|
||||
else:
|
||||
tt['sign_ganon'] = f'You need {multiworld.worlds[player].options.crystals_needed_for_ganon} crystals to beat Ganon.'
|
||||
tt['sign_ganon'] = f'You need {world.worlds[player].options.crystals_needed_for_ganon} crystals to beat Ganon.'
|
||||
|
||||
tt['uncle_leaving_text'] = Uncle_texts[local_random.randint(0, len(Uncle_texts) - 1)]
|
||||
tt['end_triforce'] = "{NOBORDER}\n" + Triforce_texts[local_random.randint(0, len(Triforce_texts) - 1)]
|
||||
@@ -2478,12 +2475,12 @@ def write_strings(rom: LocalRom, multiworld: MultiWorld, player: int):
|
||||
tt['blind_by_the_light'] = Blind_texts[local_random.randint(0, len(Blind_texts) - 1)]
|
||||
|
||||
triforce_pieces_required = max(0, w.treasure_hunt_required -
|
||||
sum(1 for item in multiworld.precollected_items[player] if item.name == "Triforce Piece"))
|
||||
sum(1 for item in world.precollected_items[player] if item.name == "Triforce Piece"))
|
||||
|
||||
if multiworld.worlds[player].options.goal in ['triforce_hunt', 'local_triforce_hunt']:
|
||||
if world.worlds[player].options.goal in ['triforce_hunt', 'local_triforce_hunt']:
|
||||
tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Get the Triforce Pieces.'
|
||||
tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.'
|
||||
if multiworld.worlds[player].options.goal == 'triforce_hunt' and multiworld.players > 1:
|
||||
if world.worlds[player].options.goal == 'triforce_hunt' and world.players > 1:
|
||||
tt['sign_ganon'] = 'Go find the Triforce pieces with your friends... Ganon is invincible!'
|
||||
else:
|
||||
tt['sign_ganon'] = 'Go find the Triforce pieces... Ganon is invincible!'
|
||||
@@ -2497,7 +2494,7 @@ def write_strings(rom: LocalRom, multiworld: MultiWorld, player: int):
|
||||
"invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \
|
||||
"hidden in a hollow tree. If you bring\n%d Triforce piece out of %d, I can reassemble it." % \
|
||||
(triforce_pieces_required, w.treasure_hunt_total)
|
||||
elif multiworld.worlds[player].options.goal in ['pedestal']:
|
||||
elif world.worlds[player].options.goal in ['pedestal']:
|
||||
tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Your goal is at the pedestal.'
|
||||
tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.'
|
||||
tt['sign_ganon'] = 'You need to get to the pedestal... Ganon is invincible!'
|
||||
@@ -2506,44 +2503,44 @@ def write_strings(rom: LocalRom, multiworld: MultiWorld, player: int):
|
||||
tt['ganon_fall_in_alt'] = 'You cannot defeat me until you finish your goal!'
|
||||
tt['ganon_phase_3_alt'] = 'Got wax in\nyour ears?\nI can not die!'
|
||||
if triforce_pieces_required > 1:
|
||||
if multiworld.worlds[player].options.goal == 'ganon_triforce_hunt' and multiworld.players > 1:
|
||||
if world.worlds[player].options.goal == 'ganon_triforce_hunt' and world.players > 1:
|
||||
tt['sign_ganon'] = 'You need to find %d Triforce pieces out of %d with your friends to defeat Ganon.' % \
|
||||
(triforce_pieces_required, w.treasure_hunt_total)
|
||||
elif multiworld.worlds[player].options.goal in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
|
||||
elif world.worlds[player].options.goal in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
|
||||
tt['sign_ganon'] = 'You need to find %d Triforce pieces out of %d to defeat Ganon.' % \
|
||||
(triforce_pieces_required, w.treasure_hunt_total)
|
||||
else:
|
||||
if multiworld.worlds[player].options.goal == 'ganon_triforce_hunt' and multiworld.players > 1:
|
||||
if world.worlds[player].options.goal == 'ganon_triforce_hunt' and world.players > 1:
|
||||
tt['sign_ganon'] = 'You need to find %d Triforce piece out of %d with your friends to defeat Ganon.' % \
|
||||
(triforce_pieces_required, w.treasure_hunt_total)
|
||||
elif multiworld.worlds[player].options.goal in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
|
||||
elif world.worlds[player].options.goal in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
|
||||
tt['sign_ganon'] = 'You need to find %d Triforce piece out of %d to defeat Ganon.' % \
|
||||
(triforce_pieces_required, w.treasure_hunt_total)
|
||||
|
||||
tt['kakariko_tavern_fisherman'] = TavernMan_texts[local_random.randint(0, len(TavernMan_texts) - 1)]
|
||||
|
||||
pedestalitem = multiworld.get_location('Master Sword Pedestal', player).item
|
||||
pedestalitem = world.get_location('Master Sword Pedestal', player).item
|
||||
pedestal_text = 'Some Hot Air' if pedestalitem is None else hint_text(pedestalitem,
|
||||
True) if pedestalitem.pedestal_hint_text is not None else 'Unknown Item'
|
||||
tt['mastersword_pedestal_translated'] = pedestal_text
|
||||
pedestal_credit_text = 'and the Hot Air' if pedestalitem is None else \
|
||||
w.pedestal_credit_texts.get(pedestalitem.code, 'and the Unknown Item')
|
||||
|
||||
etheritem = multiworld.get_location('Ether Tablet', player).item
|
||||
etheritem = world.get_location('Ether Tablet', player).item
|
||||
ether_text = 'Some Hot Air' if etheritem is None else hint_text(etheritem,
|
||||
True) if etheritem.pedestal_hint_text is not None else 'Unknown Item'
|
||||
tt['tablet_ether_book'] = ether_text
|
||||
bombositem = multiworld.get_location('Bombos Tablet', player).item
|
||||
bombositem = world.get_location('Bombos Tablet', player).item
|
||||
bombos_text = 'Some Hot Air' if bombositem is None else hint_text(bombositem,
|
||||
True) if bombositem.pedestal_hint_text is not None else 'Unknown Item'
|
||||
tt['tablet_bombos_book'] = bombos_text
|
||||
|
||||
# inverted spawn menu changes
|
||||
if multiworld.worlds[player].options.mode == 'inverted':
|
||||
if world.worlds[player].options.mode == 'inverted':
|
||||
tt['menu_start_2'] = "{MENU}\n{SPEED0}\n≥@'s house\n Dark Chapel\n{CHOICE3}"
|
||||
tt['menu_start_3'] = "{MENU}\n{SPEED0}\n≥@'s house\n Dark Chapel\n Mountain Cave\n{CHOICE2}"
|
||||
|
||||
for at, text, _ in multiworld.worlds[player].options.plando_texts:
|
||||
for at, text, _ in world.worlds[player].options.plando_texts:
|
||||
|
||||
if at not in tt:
|
||||
raise Exception(f"No text target \"{at}\" found.")
|
||||
@@ -2554,22 +2551,22 @@ def write_strings(rom: LocalRom, multiworld: MultiWorld, player: int):
|
||||
|
||||
credits = Credits()
|
||||
|
||||
sickkiditem = multiworld.get_location('Sick Kid', player).item
|
||||
sickkiditem = world.get_location('Sick Kid', player).item
|
||||
sickkiditem_text = local_random.choice(SickKid_texts) \
|
||||
if sickkiditem is None or sickkiditem.code not in w.sickkid_credit_texts \
|
||||
else w.sickkid_credit_texts[sickkiditem.code]
|
||||
|
||||
zoraitem = multiworld.get_location('King Zora', player).item
|
||||
zoraitem = world.get_location('King Zora', player).item
|
||||
zoraitem_text = local_random.choice(Zora_texts) \
|
||||
if zoraitem is None or zoraitem.code not in w.zora_credit_texts \
|
||||
else w.zora_credit_texts[zoraitem.code]
|
||||
|
||||
magicshopitem = multiworld.get_location('Potion Shop', player).item
|
||||
magicshopitem = world.get_location('Potion Shop', player).item
|
||||
magicshopitem_text = local_random.choice(MagicShop_texts) \
|
||||
if magicshopitem is None or magicshopitem.code not in w.magicshop_credit_texts \
|
||||
else w.magicshop_credit_texts[magicshopitem.code]
|
||||
|
||||
fluteboyitem = multiworld.get_location('Flute Spot', player).item
|
||||
fluteboyitem = world.get_location('Flute Spot', player).item
|
||||
fluteboyitem_text = local_random.choice(FluteBoy_texts) \
|
||||
if fluteboyitem is None or fluteboyitem.code not in w.fluteboy_credit_texts \
|
||||
else w.fluteboy_credit_texts[fluteboyitem.code]
|
||||
@@ -2598,7 +2595,7 @@ def write_strings(rom: LocalRom, multiworld: MultiWorld, player: int):
|
||||
rom.write_bytes(0x76CC0, [byte for p in pointers for byte in [p & 0xFF, p >> 8 & 0xFF]])
|
||||
|
||||
|
||||
def set_inverted_mode(multiworld: MultiWorld, player: int, rom: LocalRom):
|
||||
def set_inverted_mode(world, player, rom):
|
||||
rom.write_byte(snes_to_pc(0x0283E0), 0xF0) # residual portals
|
||||
rom.write_byte(snes_to_pc(0x02B34D), 0xF0)
|
||||
rom.write_byte(snes_to_pc(0x06DB78), 0x8B)
|
||||
@@ -2616,12 +2613,12 @@ def set_inverted_mode(multiworld: MultiWorld, player: int, rom: LocalRom):
|
||||
rom.write_byte(snes_to_pc(0x08D40C), 0xD0) # morph proof
|
||||
# the following bytes should only be written in vanilla
|
||||
# or they'll overwrite the randomizer's shuffles
|
||||
if multiworld.worlds[player].options.entrance_shuffle == 'vanilla':
|
||||
if world.worlds[player].options.entrance_shuffle == 'vanilla':
|
||||
rom.write_byte(0xDBB73 + 0x23, 0x37) # switch AT and GT
|
||||
rom.write_byte(0xDBB73 + 0x36, 0x24)
|
||||
rom.write_int16(0x15AEE + 2 * 0x38, 0x00E0)
|
||||
rom.write_int16(0x15AEE + 2 * 0x25, 0x000C)
|
||||
if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
||||
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
||||
rom.write_byte(0x15B8C, 0x6C)
|
||||
rom.write_byte(0xDBB73 + 0x00, 0x53) # switch bomb shop and links house
|
||||
rom.write_byte(0xDBB73 + 0x52, 0x01)
|
||||
@@ -2679,7 +2676,7 @@ def set_inverted_mode(multiworld: MultiWorld, player: int, rom: LocalRom):
|
||||
rom.write_int16(snes_to_pc(0x02D9A6), 0x005A)
|
||||
rom.write_byte(snes_to_pc(0x02D9B3), 0x12)
|
||||
# keep the old man spawn point at old man house unless shuffle is vanilla
|
||||
if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']:
|
||||
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']:
|
||||
rom.write_bytes(snes_to_pc(0x308350), [0x00, 0x00, 0x01])
|
||||
rom.write_int16(snes_to_pc(0x02D8DE), 0x00F1)
|
||||
rom.write_bytes(snes_to_pc(0x02D910), [0x1F, 0x1E, 0x1F, 0x1F, 0x03, 0x02, 0x03, 0x03])
|
||||
@@ -2742,7 +2739,7 @@ def set_inverted_mode(multiworld: MultiWorld, player: int, rom: LocalRom):
|
||||
rom.write_int16s(snes_to_pc(0x1bb836), [0x001B, 0x001B, 0x001B])
|
||||
rom.write_int16(snes_to_pc(0x308300), 0x0140) # new pyramid hole entrance
|
||||
rom.write_int16(snes_to_pc(0x308320), 0x001B)
|
||||
if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
||||
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
||||
rom.write_byte(snes_to_pc(0x308340), 0x7B)
|
||||
rom.write_int16(snes_to_pc(0x1af504), 0x148B)
|
||||
rom.write_int16(snes_to_pc(0x1af50c), 0x149B)
|
||||
@@ -2779,10 +2776,10 @@ def set_inverted_mode(multiworld: MultiWorld, player: int, rom: LocalRom):
|
||||
rom.write_bytes(snes_to_pc(0x1BC85A), [0x50, 0x0F, 0x82])
|
||||
rom.write_int16(0xDB96F + 2 * 0x35, 0x001B) # move pyramid exit door
|
||||
rom.write_int16(0xDBA71 + 2 * 0x35, 0x06A4)
|
||||
if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
||||
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
||||
rom.write_byte(0xDBB73 + 0x35, 0x36)
|
||||
rom.write_byte(snes_to_pc(0x09D436), 0xF3) # remove castle gate warp
|
||||
if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
||||
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
||||
rom.write_int16(0x15AEE + 2 * 0x37, 0x0010) # pyramid exit to new hc area
|
||||
rom.write_byte(0x15B8C + 0x37, 0x1B)
|
||||
rom.write_int16(0x15BDB + 2 * 0x37, 0x0418)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ from Utils import int16_as_bytes
|
||||
|
||||
from worlds.generic.Rules import add_rule
|
||||
|
||||
from BaseClasses import CollectionState, Item, MultiWorld
|
||||
from BaseClasses import CollectionState
|
||||
from .SubClasses import ALttPLocation
|
||||
|
||||
from .Items import item_name_groups
|
||||
@@ -159,7 +159,7 @@ shop_class_mapping = {ShopType.UpgradeShop: UpgradeShop,
|
||||
ShopType.TakeAny: TakeAny}
|
||||
|
||||
|
||||
def push_shop_inventories(multiworld: MultiWorld):
|
||||
def push_shop_inventories(multiworld):
|
||||
all_shops = []
|
||||
for world in multiworld.get_game_worlds(ALttPLocation.game):
|
||||
all_shops.extend(world.shops)
|
||||
@@ -183,7 +183,7 @@ def push_shop_inventories(multiworld: MultiWorld):
|
||||
world.pushed_shop_inventories.set()
|
||||
|
||||
|
||||
def create_shops(multiworld: MultiWorld, player: int):
|
||||
def create_shops(multiworld, player: int):
|
||||
from .Options import RandomizeShopInventories
|
||||
player_shop_table = shop_table.copy()
|
||||
if multiworld.worlds[player].options.include_witch_hut:
|
||||
@@ -306,7 +306,7 @@ shop_generation_types = {
|
||||
}
|
||||
|
||||
|
||||
def set_up_shops(multiworld: MultiWorld, player: int):
|
||||
def set_up_shops(multiworld, player: int):
|
||||
from .Options import small_key_shuffle
|
||||
# TODO: move hard+ mode changes for shields here, utilizing the new shops
|
||||
|
||||
@@ -408,7 +408,7 @@ price_rate_display = {
|
||||
}
|
||||
|
||||
|
||||
def get_price_modifier(item: Item) -> float:
|
||||
def get_price_modifier(item) -> float:
|
||||
if item.game == "A Link to the Past":
|
||||
if any(x in item.name for x in
|
||||
['Compass', 'Map', 'Single Bomb', 'Single Arrow', 'Piece of Heart']):
|
||||
@@ -428,7 +428,7 @@ def get_price_modifier(item: Item) -> float:
|
||||
return 0.25
|
||||
|
||||
|
||||
def get_price(multiworld: MultiWorld, item: Item, player: int, price_type=None):
|
||||
def get_price(multiworld, item, player: int, price_type=None):
|
||||
"""Converts a raw Rupee price into a special price type"""
|
||||
from .Options import small_key_shuffle
|
||||
if price_type:
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from BaseClasses import MultiWorld, CollectionState
|
||||
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
|
||||
from .SubClasses import LTTPEntrance
|
||||
@@ -6,27 +5,27 @@ from .SubClasses import LTTPEntrance
|
||||
|
||||
# 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.
|
||||
def underworld_glitch_connections(multiworld: MultiWorld, player: int):
|
||||
specrock = multiworld.get_region('Spectacle Rock Cave (Bottom)', player)
|
||||
mire = multiworld.get_region('Misery Mire (West)', player)
|
||||
def underworld_glitch_connections(world, player):
|
||||
specrock = world.get_region('Spectacle Rock Cave (Bottom)', player)
|
||||
mire = world.get_region('Misery Mire (West)', player)
|
||||
|
||||
kikiskip = specrock.create_exit('Kiki Skip')
|
||||
mire_to_hera = mire.create_exit('Mire to Hera Clip')
|
||||
mire_to_swamp = mire.create_exit('Hera to Swamp Clip')
|
||||
|
||||
if multiworld.worlds[player].fix_fake_world:
|
||||
kikiskip.connect(multiworld.get_entrance('Palace of Darkness Exit', player).connected_region)
|
||||
mire_to_hera.connect(multiworld.get_entrance('Tower of Hera Exit', player).connected_region)
|
||||
mire_to_swamp.connect(multiworld.get_entrance('Swamp Palace Exit', player).connected_region)
|
||||
if world.worlds[player].fix_fake_world:
|
||||
kikiskip.connect(world.get_entrance('Palace of Darkness Exit', player).connected_region)
|
||||
mire_to_hera.connect(world.get_entrance('Tower of Hera Exit', player).connected_region)
|
||||
mire_to_swamp.connect(world.get_entrance('Swamp Palace Exit', player).connected_region)
|
||||
else:
|
||||
kikiskip.connect(multiworld.get_region('Palace of Darkness (Entrance)', player))
|
||||
mire_to_hera.connect(multiworld.get_region('Tower of Hera (Bottom)', player))
|
||||
mire_to_swamp.connect(multiworld.get_region('Swamp Palace (Entrance)', player))
|
||||
kikiskip.connect(world.get_region('Palace of Darkness (Entrance)', player))
|
||||
mire_to_hera.connect(world.get_region('Tower of Hera (Bottom)', player))
|
||||
mire_to_swamp.connect(world.get_region('Swamp Palace (Entrance)', player))
|
||||
|
||||
|
||||
# For some entrances, we need to fake having pearl, because we're in fake DW/LW.
|
||||
# This creates a copy of the input state that has Moon Pearl.
|
||||
def fake_pearl_state(state: CollectionState, player: int):
|
||||
def fake_pearl_state(state, player):
|
||||
if state.has('Moon Pearl', player):
|
||||
return state
|
||||
fake_state = state.copy()
|
||||
@@ -36,11 +35,11 @@ def fake_pearl_state(state: CollectionState, player: int):
|
||||
|
||||
# Sets the rules on where we can actually go using this clip.
|
||||
# Behavior differs based on what type of ER shuffle we're playing.
|
||||
def dungeon_reentry_rules(multiworld: MultiWorld, player: int, clip: LTTPEntrance, dungeon_region: str, dungeon_exit: str):
|
||||
fix_dungeon_exits = multiworld.worlds[player].fix_palaceofdarkness_exit
|
||||
fix_fake_worlds = multiworld.worlds[player].fix_fake_world
|
||||
def dungeon_reentry_rules(world, player, clip: LTTPEntrance, dungeon_region: str, dungeon_exit: str):
|
||||
fix_dungeon_exits = world.worlds[player].fix_palaceofdarkness_exit
|
||||
fix_fake_worlds = world.worlds[player].fix_fake_world
|
||||
|
||||
dungeon_entrance = [r for r in multiworld.get_region(dungeon_region, player).entrances if r.name != clip.name][0]
|
||||
dungeon_entrance = [r for r in world.get_region(dungeon_region, player).entrances if r.name != clip.name][0]
|
||||
if not fix_dungeon_exits: # vanilla, simple, restricted, dungeons_simple; should never have fake worlds fix
|
||||
# Dungeons are only shuffled among themselves. We need to check SW, MM, and AT because they can't be reentered trivially.
|
||||
if dungeon_entrance.name == 'Skull Woods Final Section':
|
||||
@@ -50,64 +49,64 @@ def dungeon_reentry_rules(multiworld: MultiWorld, player: int, clip: LTTPEntranc
|
||||
elif dungeon_entrance.name == 'Agahnims Tower':
|
||||
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(multiworld.get_entrance(dungeon_exit, player), lambda state: dungeon_entrance.can_reach(state))
|
||||
add_rule(world.get_entrance(dungeon_exit, player), lambda state: dungeon_entrance.can_reach(state))
|
||||
elif not fix_fake_worlds: # full, dungeons_full; fixed dungeon exits, but no fake worlds fix
|
||||
# Entry requires the entrance's requirements plus a fake pearl, but you don't gain logical access to the surrounding region.
|
||||
add_rule(clip, lambda state: dungeon_entrance.access_rule(fake_pearl_state(state, player)))
|
||||
# exiting restriction
|
||||
add_rule(multiworld.get_entrance(dungeon_exit, player), lambda state: dungeon_entrance.can_reach(state))
|
||||
add_rule(world.get_entrance(dungeon_exit, player), lambda state: dungeon_entrance.can_reach(state))
|
||||
# Otherwise, the shuffle type is crossed, dungeons_crossed, or insanity; all of these do not need additional rules on where we can go,
|
||||
# since the clip links directly to the exterior region.
|
||||
|
||||
|
||||
def underworld_glitches_rules(multiworld: MultiWorld, player: int):
|
||||
def underworld_glitches_rules(world, player):
|
||||
# Ice Palace Entrance Clip
|
||||
# This is the easiest one since it's a simple internal clip.
|
||||
# Need to also add melting to freezor chest since it's otherwise assumed.
|
||||
# Also can pick up the first jelly key from behind.
|
||||
add_rule(multiworld.get_entrance('Ice Palace (Main)', player), lambda state: can_bomb_clip(state, multiworld.get_region('Ice Palace (Entrance)', player), player), combine='or')
|
||||
add_rule(multiworld.get_location('Ice Palace - Freezor Chest', player), lambda state: can_melt_things(state, player))
|
||||
add_rule(multiworld.get_location('Ice Palace - Jelly Key Drop', player), lambda state: can_bomb_clip(state, multiworld.get_region('Ice Palace (Entrance)', player), player), combine='or')
|
||||
add_rule(world.get_entrance('Ice Palace (Main)', 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))
|
||||
add_rule(world.get_location('Ice Palace - Jelly Key Drop', player), lambda state: can_bomb_clip(state, world.get_region('Ice Palace (Entrance)', player), player), combine='or')
|
||||
|
||||
|
||||
# Kiki Skip
|
||||
kikiskip = multiworld.get_entrance('Kiki Skip', player)
|
||||
kikiskip = world.get_entrance('Kiki Skip', player)
|
||||
set_rule(kikiskip, lambda state: can_bomb_clip(state, kikiskip.parent_region, player))
|
||||
dungeon_reentry_rules(multiworld, player, kikiskip, 'Palace of Darkness (Entrance)', 'Palace of Darkness Exit')
|
||||
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 = multiworld.get_region('Misery Mire (West)', player)
|
||||
mire = world.get_region('Misery Mire (West)', 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, multiworld.get_region('Tower of Hera (Top)', player), player)
|
||||
add_rule(multiworld.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(multiworld.get_entrance('Swamp Palace Small Key Door', player), lambda state: mire_clip(state), combine='or')
|
||||
add_rule(multiworld.get_entrance('Swamp Palace (Center)', player), lambda state: mire_clip(state) or hera_clip(state), combine='or')
|
||||
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')
|
||||
|
||||
# Build the rule for SP moat.
|
||||
# We need to be able to s+q to old man, then go to either Mire or Hera at either Hera or GT.
|
||||
# First we require a certain type of entrance shuffle, then build the rule from its pieces.
|
||||
if not multiworld.worlds[player].swamp_patch_required:
|
||||
if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
||||
if not world.worlds[player].swamp_patch_required:
|
||||
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
||||
rule_map = {
|
||||
'Misery Mire (Entrance)': (lambda state: True),
|
||||
'Tower of Hera (Bottom)': (lambda state: state.can_reach('Tower of Hera Big Key Door', 'Entrance', player))
|
||||
}
|
||||
inverted = multiworld.worlds[player].options.mode == 'inverted'
|
||||
inverted = world.worlds[player].options.mode == 'inverted'
|
||||
hera_rule = lambda state: (state.has('Moon Pearl', player) or not inverted) and \
|
||||
rule_map.get(multiworld.get_entrance('Tower of Hera', player).connected_region.name, lambda state: False)(state)
|
||||
rule_map.get(world.get_entrance('Tower of Hera', player).connected_region.name, lambda state: False)(state)
|
||||
gt_rule = lambda state: (state.has('Moon Pearl', player) or inverted) and \
|
||||
rule_map.get(multiworld.get_entrance(('Ganons Tower' if not inverted else 'Inverted Ganons Tower'), player).connected_region.name, lambda state: False)(state)
|
||||
rule_map.get(world.get_entrance(('Ganons Tower' if not inverted else 'Inverted Ganons Tower'), player).connected_region.name, lambda state: False)(state)
|
||||
mirrorless_moat_rule = lambda state: state.can_reach('Old Man S&Q', 'Entrance', player) and mire_clip(state) and (hera_rule(state) or gt_rule(state))
|
||||
add_rule(multiworld.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player) or mirrorless_moat_rule(state))
|
||||
add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player) or mirrorless_moat_rule(state))
|
||||
else:
|
||||
add_rule(multiworld.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player))
|
||||
add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player))
|
||||
|
||||
# Using the entrances for various ER types. Hera -> Swamp never matters because you can only logically traverse with the mire keys
|
||||
mire_to_hera = multiworld.get_entrance('Mire to Hera Clip', player)
|
||||
mire_to_swamp = multiworld.get_entrance('Hera to Swamp Clip', player)
|
||||
mire_to_hera = world.get_entrance('Mire to Hera Clip', player)
|
||||
mire_to_swamp = world.get_entrance('Hera to Swamp Clip', player)
|
||||
set_rule(mire_to_hera, mire_clip)
|
||||
set_rule(mire_to_swamp, lambda state: mire_clip(state) and state.has('Flippers', player))
|
||||
dungeon_reentry_rules(multiworld, player, mire_to_hera, 'Tower of Hera (Bottom)', 'Tower of Hera Exit')
|
||||
dungeon_reentry_rules(multiworld, player, mire_to_swamp, 'Swamp Palace (Entrance)', 'Swamp Palace Exit')
|
||||
dungeon_reentry_rules(world, player, mire_to_hera, 'Tower of Hera (Bottom)', 'Tower of Hera Exit')
|
||||
dungeon_reentry_rules(world, player, mire_to_swamp, 'Swamp Palace (Entrance)', 'Swamp Palace Exit')
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"game": "A Link to the Past",
|
||||
"minimum_ap_version": "0.6.6",
|
||||
"world_version": "5.1.0",
|
||||
"authors": ["Berserker"]
|
||||
}
|
||||
@@ -2025,13 +2025,13 @@ location_tables: Dict[str, List[DS3LocationData]] = {
|
||||
DS3LocationData("LC: Rusted Coin - chapel", "Rusted Coin x2"),
|
||||
DS3LocationData("LC: Braille Divine Tome of Lothric - wyvern room",
|
||||
"Braille Divine Tome of Lothric", hidden=True), # Hidden fall
|
||||
DS3LocationData("LC: Red Tearstone Ring - chapel, balcony before drop", "Red Tearstone Ring"),
|
||||
DS3LocationData("LC: Red Tearstone Ring - chapel, drop onto roof", "Red Tearstone Ring"),
|
||||
DS3LocationData("LC: Twinkling Titanite - moat, left side", "Twinkling Titanite x2"),
|
||||
DS3LocationData("LC: Large Soul of a Nameless Soldier - plaza left, by pillar",
|
||||
"Large Soul of a Nameless Soldier"),
|
||||
DS3LocationData("LC: Titanite Scale - altar", "Titanite Scale x3"),
|
||||
DS3LocationData("LC: Titanite Scale - chapel, chest", "Titanite Scale"),
|
||||
DS3LocationData("LC: Hood of Prayer - ascent, chest at beginning", "Hood of Prayer"),
|
||||
DS3LocationData("LC: Hood of Prayer", "Hood of Prayer"),
|
||||
DS3LocationData("LC: Robe of Prayer - ascent, chest at beginning", "Robe of Prayer"),
|
||||
DS3LocationData("LC: Skirt of Prayer - ascent, chest at beginning", "Skirt of Prayer"),
|
||||
DS3LocationData("LC: Spirit Tree Crest Shield - basement, chest",
|
||||
|
||||
@@ -6,7 +6,6 @@ from logging import warning
|
||||
from typing import cast, Any, Callable, Dict, Set, List, Optional, TextIO, Union
|
||||
|
||||
from BaseClasses import CollectionState, MultiWorld, Region, Location, LocationProgressType, Entrance, Tutorial, ItemClassification
|
||||
from Fill import remaining_fill
|
||||
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
from worlds.generic.Rules import CollectionRule, ItemRule, add_rule, add_item_rule
|
||||
@@ -1474,7 +1473,6 @@ class DarkSouls3World(World):
|
||||
f"contain smoothed items, but only {len(converted_item_order)} items to smooth."
|
||||
)
|
||||
|
||||
sorted_spheres = []
|
||||
for sphere in locations_by_sphere:
|
||||
locations = [loc for loc in sphere if loc.item.name in names]
|
||||
|
||||
@@ -1482,12 +1480,12 @@ class DarkSouls3World(World):
|
||||
offworld = ds3_world._shuffle([loc for loc in locations if loc.game != "Dark Souls III"])
|
||||
onworld = sorted((loc for loc in locations if loc.game == "Dark Souls III"),
|
||||
key=lambda loc: loc.data.region_value)
|
||||
# Give offworld regions the last (best) items within a given sphere
|
||||
sorted_spheres.extend(onworld)
|
||||
sorted_spheres.extend(offworld)
|
||||
|
||||
converted_item_order.reverse()
|
||||
remaining_fill(multiworld, sorted_spheres, converted_item_order, name="DS3 Smoothing", check_location_can_fill=True)
|
||||
# Give offworld regions the last (best) items within a given sphere
|
||||
for location in onworld + offworld:
|
||||
new_item = ds3_world._pop_item(location, converted_item_order)
|
||||
location.item = new_item
|
||||
new_item.location = location
|
||||
|
||||
if ds3_world.options.smooth_upgrade_items:
|
||||
base_names = {
|
||||
@@ -1520,6 +1518,19 @@ class DarkSouls3World(World):
|
||||
self.random.shuffle(copy)
|
||||
return copy
|
||||
|
||||
def _pop_item(
|
||||
self,
|
||||
location: Location,
|
||||
items: List[DarkSouls3Item]
|
||||
) -> DarkSouls3Item:
|
||||
"""Returns the next item in items that can be assigned to location."""
|
||||
for i, item in enumerate(items):
|
||||
if location.can_fill(self.multiworld.state, item, False):
|
||||
return items.pop(i)
|
||||
|
||||
# If we can't find a suitable item, give up and assign an unsuitable one.
|
||||
return items.pop(0)
|
||||
|
||||
def _get_our_locations(self) -> List[DarkSouls3Location]:
|
||||
return cast(List[DarkSouls3Location], self.multiworld.get_locations(self.player))
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"game": "Final Fantasy",
|
||||
"world_version": "1.0.0",
|
||||
"authors": ["Rosalie"]
|
||||
}
|
||||
@@ -26,10 +26,7 @@ class GenericWeb(WebWorld):
|
||||
'English', 'setup_en.md', 'setup/en', ['alwaysintreble'])
|
||||
triggers = Tutorial('Archipelago Triggers Guide', 'A guide to setting up and using triggers in your game settings.',
|
||||
'English', 'triggers_en.md', 'triggers/en', ['alwaysintreble'])
|
||||
other = Tutorial('Other Games and Tools',
|
||||
'A guide to additional games and tools that can be used with Archipelago.',
|
||||
'English', 'other_en.md', 'other/en', ['Berserker'])
|
||||
tutorials = [setup, mac, commands, advanced_settings, triggers, plando, other]
|
||||
tutorials = [setup, mac, commands, advanced_settings, triggers, plando]
|
||||
|
||||
|
||||
class GenericWorld(World):
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# Other Games and Tools
|
||||
|
||||
This guide provides information on additional community resources, tools, and games that function with Archipelago.
|
||||
|
||||
## Community Resources
|
||||
|
||||
The Archipelago community is active across several platforms where you can find support, new games, and tools.
|
||||
|
||||
### Discord Servers
|
||||
Archipelago has two primary Discord servers for community interaction, game support, and hosting public games:
|
||||
- **[Archipelago Official Discord](https://discord.gg/8Z65BR2)**: The main hub for the community, including general discussion, support, and public multiworld hosting.
|
||||
- **[Archipelago After Dark Discord](https://discord.gg/fqvNCCRsu4)**: An adults-only server for 18+ and unrated content.
|
||||
|
||||
Both servers feature an **#apworld-index** channel. These channels are repositories for "APWorlds" — additional game implementations that can be easily added to your Archipelago installation to support more games.
|
||||
|
||||
### Documentation
|
||||
- **[Archipelago Wiki](https://archipelago.miraheze.org/)**: A community-maintained wiki.
|
||||
|
||||
## Community Tools
|
||||
|
||||
These community-developed tools are frequently used alongside Archipelago to improve the player experience.
|
||||
|
||||
### PopTracker
|
||||
**[PopTracker](https://github.com/black-sliver/PopTracker)** is a universal multi-platform tracking application designed for randomizers. It supports many Archipelago games through tracker packs, providing both manual and automatic autotracking capabilities by connecting directly to an Archipelago server.
|
||||
@@ -216,28 +216,6 @@ dungeon major item chests. Because the from_pool value is `false`, a copy of the
|
||||
while the originals remain in the item pool to be shuffled. The second block will place the Kokiri Sword in the Deku
|
||||
Tree Slingshot Chest, again not from the pool.
|
||||
|
||||
```yaml
|
||||
plando_items:
|
||||
# Example block - Hollow Knight
|
||||
- items:
|
||||
Claw : true
|
||||
world:
|
||||
- BobsWitness
|
||||
- BobsRogueLegacy
|
||||
```
|
||||
This block will attempt to place all items in the Claw item group into any locations within the game slots named
|
||||
"BobsWitness" and "BobsRogueLegacy."
|
||||
|
||||
**NOTE:** As item groups may contain items that are not currently present in the item pool, use of `true` with
|
||||
item groups, as shown here, is strongly recommended to avoid creation of unintended items.
|
||||
|
||||
For example, the Claw item group for Hollow Knight includes Mantis_Claw, Left_Mantis_Claw, and Right_Mantis_Claw.
|
||||
Depending on a different yaml setting, the Generator will create either one Mantis_Claw item, or one each of the
|
||||
Left_Mantis_Claw and Right_Mantis_Claw items. By default, the Generator will create any missing item(s) in addition
|
||||
to using the intended item(s), resulting in placement of all three items from the item group: Mantis_Claw,
|
||||
Left_Mantis_Claw and Right_Mantis_Claw. Use of the true value, as shown in the example, restricts the Generator to
|
||||
using only the items from the item group that are already present in the item pool.
|
||||
|
||||
## Boss Plando
|
||||
|
||||
This is currently only supported by A Link to the Past and Kirby's Dream Land 3. Boss plando allows a player to place a
|
||||
|
||||
@@ -1281,7 +1281,7 @@ exclusion_table = {
|
||||
LocationName.HadesCupTrophyParadoxCups,
|
||||
LocationName.MusicalOrichalcumPlus,
|
||||
],
|
||||
"HitlistCasual": [
|
||||
"HitlistCasual": {
|
||||
LocationName.FuturePete,
|
||||
LocationName.BetwixtandBetweenBondofFlame,
|
||||
LocationName.GrimReaper2,
|
||||
@@ -1299,7 +1299,7 @@ exclusion_table = {
|
||||
LocationName.MCP,
|
||||
LocationName.Lvl50,
|
||||
LocationName.Lvl99
|
||||
],
|
||||
},
|
||||
"Cups": {
|
||||
LocationName.ProtectBeltPainandPanicCup,
|
||||
LocationName.SerenityGemPainandPanicCup,
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"game": "Lingo",
|
||||
"authors": ["hatkirby"],
|
||||
"minimum_ap_version": "0.6.3",
|
||||
"world_version": "5.0.0"
|
||||
}
|
||||
@@ -4470,10 +4470,6 @@
|
||||
panel: SEVEN (1)
|
||||
- room: Outside The Initiated
|
||||
panel: SEVEN (2)
|
||||
First Eight:
|
||||
event: True
|
||||
panels:
|
||||
- EIGHT
|
||||
Nines:
|
||||
id:
|
||||
- Count Up Room Area Doors/Door_nine_hider
|
||||
@@ -4616,7 +4612,7 @@
|
||||
enter_only: True
|
||||
orientation: east
|
||||
required_door:
|
||||
door: First Eight
|
||||
door: Eights
|
||||
progression:
|
||||
Progressive Number Hunt:
|
||||
panel_doors:
|
||||
|
||||
Binary file not shown.
@@ -1,8 +1,7 @@
|
||||
import logging
|
||||
from typing import Any, ClassVar, TextIO
|
||||
|
||||
from BaseClasses import CollectionState, Entrance, EntranceType, Item, ItemClassification, MultiWorld, Tutorial, \
|
||||
PlandoOptions
|
||||
from BaseClasses import CollectionState, Entrance, EntranceType, Item, ItemClassification, MultiWorld, Tutorial
|
||||
from Options import Accessibility
|
||||
from Utils import output_path
|
||||
from settings import FilePath, Group
|
||||
@@ -19,7 +18,6 @@ from .rules import MessengerHardRules, MessengerOOBRules, MessengerRules
|
||||
from .shop import FIGURINES, PROG_SHOP_ITEMS, SHOP_ITEMS, USEFUL_SHOP_ITEMS, shuffle_shop_prices
|
||||
from .subclasses import MessengerItem, MessengerRegion, MessengerShopLocation
|
||||
from .transitions import disconnect_entrances, shuffle_transitions
|
||||
from .universal_tracker import reverse_portal_exits_into_portal_plando, reverse_transitions_into_plando_connections
|
||||
|
||||
components.append(
|
||||
Component(
|
||||
@@ -153,10 +151,6 @@ class MessengerWorld(World):
|
||||
reachable_locs: bool = False
|
||||
filler: dict[str, int]
|
||||
|
||||
@staticmethod
|
||||
def interpret_slot_data(slot_data: dict[str, Any]) -> dict[str, Any]:
|
||||
return slot_data
|
||||
|
||||
def generate_early(self) -> None:
|
||||
if self.options.goal == Goal.option_power_seal_hunt:
|
||||
self.total_seals = self.options.total_seals.value
|
||||
@@ -194,11 +188,6 @@ class MessengerWorld(World):
|
||||
self.spoiler_portal_mapping = {}
|
||||
self.transitions = []
|
||||
|
||||
if hasattr(self.multiworld, "re_gen_passthrough"):
|
||||
slot_data = self.multiworld.re_gen_passthrough.get(self.game)
|
||||
if slot_data:
|
||||
self.starting_portals = slot_data["starting_portals"]
|
||||
|
||||
def create_regions(self) -> None:
|
||||
# MessengerRegion adds itself to the multiworld
|
||||
# create simple regions
|
||||
@@ -290,16 +279,6 @@ class MessengerWorld(World):
|
||||
def connect_entrances(self) -> None:
|
||||
if self.options.shuffle_transitions:
|
||||
disconnect_entrances(self)
|
||||
keep_entrance_logic = False
|
||||
|
||||
if hasattr(self.multiworld, "re_gen_passthrough"):
|
||||
slot_data = self.multiworld.re_gen_passthrough.get(self.game)
|
||||
if slot_data:
|
||||
self.multiworld.plando_options |= PlandoOptions.connections
|
||||
self.options.portal_plando.value = reverse_portal_exits_into_portal_plando(slot_data["portal_exits"])
|
||||
self.options.plando_connections.value = reverse_transitions_into_plando_connections(slot_data["transitions"])
|
||||
keep_entrance_logic = True
|
||||
|
||||
add_closed_portal_reqs(self)
|
||||
# i need portal shuffle to happen after rules exist so i can validate it
|
||||
attempts = 20
|
||||
@@ -316,7 +295,7 @@ class MessengerWorld(World):
|
||||
raise RuntimeError("Unable to generate valid portal output.")
|
||||
|
||||
if self.options.shuffle_transitions:
|
||||
shuffle_transitions(self, keep_entrance_logic)
|
||||
shuffle_transitions(self)
|
||||
|
||||
def write_spoiler_header(self, spoiler_handle: TextIO) -> None:
|
||||
if self.options.available_portals < 6:
|
||||
@@ -484,7 +463,7 @@ class MessengerWorld(World):
|
||||
"loc_data": {loc.address: {loc.item.name: [loc.item.code, loc.item.flags]}
|
||||
for loc in multiworld.get_filled_locations() if loc.address},
|
||||
}
|
||||
|
||||
|
||||
output = orjson.dumps(data, option=orjson.OPT_NON_STR_KEYS)
|
||||
with open(out_path, "wb") as f:
|
||||
f.write(output)
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"game": "The Messenger",
|
||||
"authors": ["alwaysintreble"]
|
||||
}
|
||||
@@ -28,8 +28,6 @@ def create_yes_no_popup(title: str, text: str, callback: Callable[[str], None])
|
||||
|
||||
def launch_game(*args) -> None:
|
||||
"""Check the game installation, then launch it"""
|
||||
prompt: ButtonsPrompt | None = None
|
||||
|
||||
def courier_installed() -> bool:
|
||||
"""Check if Courier is installed"""
|
||||
assembly_path = os.path.join(game_folder, "TheMessenger_Data", "Managed", "Assembly-CSharp.dll")
|
||||
@@ -192,7 +190,7 @@ def launch_game(*args) -> None:
|
||||
|
||||
def launch(answer: str | None = None) -> None:
|
||||
"""Launch the game."""
|
||||
nonlocal args, prompt
|
||||
nonlocal args
|
||||
|
||||
if prompt:
|
||||
prompt.dismiss()
|
||||
@@ -258,5 +256,3 @@ def launch_game(*args) -> None:
|
||||
prompt = create_yes_no_popup("Launch Game",
|
||||
"Mod installed and up to date. Would you like to launch the game now?",
|
||||
launch)
|
||||
else:
|
||||
launch()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from BaseClasses import CollectionState, CollectionRule
|
||||
from worlds.generic.Rules import add_rule, allow_self_locking_items
|
||||
from BaseClasses import CollectionState
|
||||
from worlds.generic.Rules import CollectionRule, add_rule, allow_self_locking_items
|
||||
from .constants import NOTES, PHOBEKINS
|
||||
from .options import MessengerAccessibility
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from functools import cached_property
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from BaseClasses import CollectionState, Item, ItemClassification, Location, Region
|
||||
from BaseClasses import CollectionState, Entrance, EntranceType, Item, ItemClassification, Location, Region
|
||||
from entrance_rando import ERPlacementState
|
||||
from .regions import LOCATIONS, MEGA_SHARDS
|
||||
from .shop import FIGURINES, SHOP_ITEMS
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from BaseClasses import Region, CollectionRule
|
||||
from BaseClasses import Entrance, Region
|
||||
from entrance_rando import EntranceType, randomize_entrances
|
||||
from .connections import RANDOMIZED_CONNECTIONS, TRANSITIONS
|
||||
from .options import ShuffleTransitions, TransitionPlando
|
||||
@@ -26,6 +26,7 @@ def disconnect_entrances(world: "MessengerWorld") -> None:
|
||||
entrance.randomization_type = er_type
|
||||
mock_entrance.randomization_type = er_type
|
||||
|
||||
|
||||
for parent, child in RANDOMIZED_CONNECTIONS.items():
|
||||
if child == "Corrupted Future":
|
||||
entrance = world.get_entrance("Artificer's Portal")
|
||||
@@ -35,9 +36,8 @@ def disconnect_entrances(world: "MessengerWorld") -> None:
|
||||
entrance = world.get_entrance(f"{parent} -> {child}")
|
||||
disconnect_entrance()
|
||||
|
||||
|
||||
def connect_plando(world: "MessengerWorld", plando_connections: TransitionPlando, keep_logic: bool = False) -> None:
|
||||
def remove_dangling_exit(region: Region) -> CollectionRule:
|
||||
def connect_plando(world: "MessengerWorld", plando_connections: TransitionPlando) -> None:
|
||||
def remove_dangling_exit(region: Region) -> None:
|
||||
# find the disconnected exit and remove references to it
|
||||
for _exit in region.exits:
|
||||
if not _exit.connected_region:
|
||||
@@ -45,7 +45,6 @@ def connect_plando(world: "MessengerWorld", plando_connections: TransitionPlando
|
||||
else:
|
||||
raise ValueError(f"Unable to find randomized transition for {plando_connection}")
|
||||
region.exits.remove(_exit)
|
||||
return _exit.access_rule
|
||||
|
||||
def remove_dangling_entrance(region: Region) -> None:
|
||||
# find the disconnected entrance and remove references to it
|
||||
@@ -66,35 +65,30 @@ def connect_plando(world: "MessengerWorld", plando_connections: TransitionPlando
|
||||
else:
|
||||
dangling_exit = world.get_entrance("Artificer's Challenge")
|
||||
reg1.exits.remove(dangling_exit)
|
||||
access_rule = dangling_exit.access_rule
|
||||
else:
|
||||
reg1 = world.get_region(plando_connection.entrance)
|
||||
access_rule = remove_dangling_exit(reg1)
|
||||
|
||||
remove_dangling_exit(reg1)
|
||||
|
||||
reg2 = world.get_region(plando_connection.exit)
|
||||
remove_dangling_entrance(reg2)
|
||||
# connect the regions
|
||||
new_exit1 = reg1.connect(reg2)
|
||||
if keep_logic:
|
||||
new_exit1.access_rule = access_rule
|
||||
reg1.connect(reg2)
|
||||
|
||||
# pretend the user set the plando direction as "both" regardless of what they actually put on coupled
|
||||
if ((world.options.shuffle_transitions == ShuffleTransitions.option_coupled
|
||||
or plando_connection.direction == "both")
|
||||
and plando_connection.exit in RANDOMIZED_CONNECTIONS):
|
||||
access_rule = remove_dangling_exit(reg2)
|
||||
remove_dangling_exit(reg2)
|
||||
remove_dangling_entrance(reg1)
|
||||
new_exit2 = reg2.connect(reg1)
|
||||
if keep_logic:
|
||||
new_exit2.access_rule = access_rule
|
||||
reg2.connect(reg1)
|
||||
|
||||
|
||||
def shuffle_transitions(world: "MessengerWorld", keep_logic: bool = False) -> None:
|
||||
def shuffle_transitions(world: "MessengerWorld") -> None:
|
||||
coupled = world.options.shuffle_transitions == ShuffleTransitions.option_coupled
|
||||
|
||||
plando = world.options.plando_connections
|
||||
if plando:
|
||||
connect_plando(world, plando, keep_logic)
|
||||
connect_plando(world, plando)
|
||||
|
||||
result = randomize_entrances(world, coupled, {0: [0]})
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
from Options import PlandoConnection
|
||||
from .connections import RANDOMIZED_CONNECTIONS
|
||||
from .portals import REGION_ORDER, SHOP_POINTS, CHECKPOINTS
|
||||
from .transitions import TRANSITIONS
|
||||
|
||||
REVERSED_RANDOMIZED_CONNECTIONS = {v: k for k, v in RANDOMIZED_CONNECTIONS.items()}
|
||||
|
||||
|
||||
def find_spot(portal_key: int) -> str:
|
||||
"""finds the spot associated with the portal key"""
|
||||
parent = REGION_ORDER[portal_key // 100]
|
||||
if portal_key % 100 == 0:
|
||||
return f"{parent} Portal"
|
||||
if portal_key % 100 // 10 == 1:
|
||||
return SHOP_POINTS[parent][portal_key % 10]
|
||||
return CHECKPOINTS[parent][portal_key % 10]
|
||||
|
||||
|
||||
def reverse_portal_exits_into_portal_plando(portal_exits: list[int]) -> list[PlandoConnection]:
|
||||
return [
|
||||
PlandoConnection("Autumn Hills", find_spot(portal_exits[0]), "both"),
|
||||
PlandoConnection("Riviere Turquoise", find_spot(portal_exits[1]), "both"),
|
||||
PlandoConnection("Howling Grotto", find_spot(portal_exits[2]), "both"),
|
||||
PlandoConnection("Sunken Shrine", find_spot(portal_exits[3]), "both"),
|
||||
PlandoConnection("Searing Crags", find_spot(portal_exits[4]), "both"),
|
||||
PlandoConnection("Glacial Peak", find_spot(portal_exits[5]), "both"),
|
||||
]
|
||||
|
||||
|
||||
def reverse_transitions_into_plando_connections(transitions: list[list[int]]) -> list[PlandoConnection]:
|
||||
plando_connections = []
|
||||
|
||||
for connection in [
|
||||
PlandoConnection(REVERSED_RANDOMIZED_CONNECTIONS[TRANSITIONS[transition[0]]], TRANSITIONS[transition[1]], "both")
|
||||
for transition in transitions
|
||||
]:
|
||||
if connection.exit in {con.entrance for con in plando_connections}:
|
||||
continue
|
||||
plando_connections.append(connection)
|
||||
|
||||
return plando_connections
|
||||
@@ -1,11 +1,11 @@
|
||||
import asyncio
|
||||
from typing import TYPE_CHECKING, Optional, Set, List, Dict
|
||||
import struct
|
||||
|
||||
from typing import TYPE_CHECKING, Optional, Set, List, Dict
|
||||
from NetUtils import ClientStatus
|
||||
from .Locations import roomCount, nonBlock, beanstones, roomException, shop, badge, pants, eReward
|
||||
from .Items import items_by_id
|
||||
|
||||
import asyncio
|
||||
|
||||
import worlds._bizhawk as bizhawk
|
||||
from worlds._bizhawk.client import BizHawkClient
|
||||
@@ -41,6 +41,8 @@ class MLSSClient(BizHawkClient):
|
||||
self.local_events = []
|
||||
|
||||
async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
|
||||
from CommonClient import logger
|
||||
|
||||
try:
|
||||
# Check ROM name/patch version
|
||||
rom_name_bytes = await bizhawk.read(ctx.bizhawk_ctx, [(0xA0, 14, "ROM")])
|
||||
@@ -70,15 +72,20 @@ class MLSSClient(BizHawkClient):
|
||||
async def set_auth(self, ctx: "BizHawkClientContext") -> None:
|
||||
ctx.auth = self.player_name
|
||||
|
||||
def on_package(self, ctx, cmd, args) -> None:
|
||||
if cmd == "RoomInfo":
|
||||
ctx.seed_name = args["seed_name"]
|
||||
|
||||
async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
|
||||
from CommonClient import logger
|
||||
|
||||
try:
|
||||
if ctx.server_seed_name is None:
|
||||
if ctx.seed_name is None:
|
||||
return
|
||||
if not self.seed_verify:
|
||||
seed = await bizhawk.read(ctx.bizhawk_ctx, [(0xDF00A0, len(ctx.server_seed_name), "ROM")])
|
||||
seed = await bizhawk.read(ctx.bizhawk_ctx, [(0xDF00A0, len(ctx.seed_name), "ROM")])
|
||||
seed = seed[0].decode("UTF-8")
|
||||
if seed not in ctx.server_seed_name:
|
||||
if seed not in ctx.seed_name:
|
||||
logger.info(
|
||||
"ERROR: The ROM you loaded is for a different game of AP. "
|
||||
"Please make sure the host has sent you the correct patch file, "
|
||||
|
||||
@@ -140,8 +140,8 @@ def cmd_pool(self: "BizHawkClientCommandProcessor") -> None:
|
||||
|
||||
|
||||
def cmd_request(self: "BizHawkClientCommandProcessor", amount: str, target: str) -> None:
|
||||
"""Request a refill from EnergyLink."""
|
||||
from worlds._bizhawk.context import BizHawkClientContext
|
||||
"""Request a refill from EnergyLink."""
|
||||
if self.ctx.game != "Mega Man 2":
|
||||
logger.warning("This command can only be used when playing Mega Man 2.")
|
||||
return
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
/src/*
|
||||
@@ -1,275 +0,0 @@
|
||||
import hashlib
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from typing import Any, Sequence, ClassVar
|
||||
|
||||
from BaseClasses import Tutorial, ItemClassification, MultiWorld, Item, Location
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
from .names import (gamma, gemini_man_stage, needle_man_stage, hard_man_stage, magnet_man_stage, top_man_stage,
|
||||
snake_man_stage, spark_man_stage, shadow_man_stage, rush_marine, rush_jet, rush_coil)
|
||||
from .items import (item_table, item_names, MM3Item, filler_item_weights, robot_master_weapon_table,
|
||||
stage_access_table, rush_item_table, lookup_item_to_id)
|
||||
from .locations import (MM3Location, mm3_regions, MM3Region, lookup_location_to_id,
|
||||
location_groups)
|
||||
from .rom import patch_rom, MM3ProcedurePatch, MM3LCHASH, MM3VCHASH, PROTEUSHASH, MM3NESHASH
|
||||
from .options import MM3Options, Consumables
|
||||
from .client import MegaMan3Client
|
||||
from .rules import set_rules, weapon_damage, robot_masters, weapons_to_name, minimum_weakness_requirement
|
||||
import os
|
||||
import threading
|
||||
import base64
|
||||
import settings
|
||||
logger = logging.getLogger("Mega Man 3")
|
||||
|
||||
|
||||
class MM3Settings(settings.Group):
|
||||
class RomFile(settings.UserFilePath):
|
||||
"""File name of the MM3 EN rom"""
|
||||
description = "Mega Man 3 ROM File"
|
||||
copy_to: str | None = "Mega Man 3 (USA).nes"
|
||||
md5s = [MM3NESHASH, MM3LCHASH, PROTEUSHASH, MM3VCHASH]
|
||||
|
||||
def browse(self: settings.T,
|
||||
filetypes: Sequence[tuple[str, Sequence[str]]] | None = None,
|
||||
**kwargs: Any) -> settings.T | None:
|
||||
if not filetypes:
|
||||
file_types = [("NES", [".nes"]), ("Program", [".exe"])] # LC1 is only a windows executable, no linux
|
||||
return super().browse(file_types, **kwargs)
|
||||
else:
|
||||
return super().browse(filetypes, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def validate(cls, path: str) -> None:
|
||||
"""Try to open and validate file against hashes"""
|
||||
with open(path, "rb", buffering=0) as f:
|
||||
try:
|
||||
f.seek(0)
|
||||
if f.read(4) == b"NES\x1A":
|
||||
f.seek(16)
|
||||
else:
|
||||
f.seek(0)
|
||||
cls._validate_stream_hashes(f)
|
||||
base_rom_bytes = f.read()
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(base_rom_bytes)
|
||||
if basemd5.hexdigest() == PROTEUSHASH:
|
||||
# we need special behavior here
|
||||
cls.copy_to = None
|
||||
except ValueError:
|
||||
raise ValueError(f"File hash does not match for {path}")
|
||||
|
||||
rom_file: RomFile = RomFile(RomFile.copy_to)
|
||||
|
||||
|
||||
class MM3WebWorld(WebWorld):
|
||||
theme = "partyTime"
|
||||
tutorials = [
|
||||
|
||||
Tutorial(
|
||||
"Multiworld Setup Guide",
|
||||
"A guide to setting up the Mega Man 3 randomizer connected to an Archipelago Multiworld.",
|
||||
"English",
|
||||
"setup_en.md",
|
||||
"setup/en",
|
||||
["Silvris"]
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
class MM3World(World):
|
||||
"""
|
||||
Following his second defeat by Mega Man, Dr. Wily has finally come to his senses. He and Dr. Light begin work on
|
||||
Gamma, a giant peacekeeping robot. However, Gamma's power source, the Energy Elements, are being guarded by the
|
||||
Robot Masters sent to retrieve them. It's up to Mega Man to retrieve the Energy Elements and defeat the mastermind
|
||||
behind the Robot Masters' betrayal.
|
||||
"""
|
||||
|
||||
game = "Mega Man 3"
|
||||
settings: ClassVar[MM3Settings]
|
||||
options_dataclass = MM3Options
|
||||
options: MM3Options
|
||||
item_name_to_id = lookup_item_to_id
|
||||
location_name_to_id = lookup_location_to_id
|
||||
item_name_groups = item_names
|
||||
location_name_groups = location_groups
|
||||
web = MM3WebWorld()
|
||||
rom_name: bytearray
|
||||
|
||||
def __init__(self, world: MultiWorld, player: int):
|
||||
self.rom_name = bytearray()
|
||||
self.rom_name_available_event = threading.Event()
|
||||
super().__init__(world, player)
|
||||
self.weapon_damage = deepcopy(weapon_damage)
|
||||
self.wily_4_weapons: dict[int, list[int]] = {}
|
||||
|
||||
def create_regions(self) -> None:
|
||||
menu = MM3Region("Menu", self.player, self.multiworld)
|
||||
self.multiworld.regions.append(menu)
|
||||
location: MM3Location
|
||||
for name, region in mm3_regions.items():
|
||||
stage = MM3Region(name, self.player, self.multiworld)
|
||||
if not region.parent:
|
||||
menu.connect(stage, f"To {name}",
|
||||
lambda state, req=tuple(region.required_items): state.has_all(req, self.player))
|
||||
else:
|
||||
old_stage = self.get_region(region.parent)
|
||||
old_stage.connect(stage, f"To {name}",
|
||||
lambda state, req=tuple(region.required_items): state.has_all(req, self.player))
|
||||
stage.add_locations({loc: data.location_id for loc, data in region.locations.items()
|
||||
if (not data.energy or self.options.consumables.value in (Consumables.option_weapon_health, Consumables.option_all))
|
||||
and (not data.oneup_tank or self.options.consumables.value in (Consumables.option_1up_etank, Consumables.option_all))})
|
||||
for location in stage.get_locations():
|
||||
if location.address is None and location.name != gamma:
|
||||
location.place_locked_item(MM3Item(location.name, ItemClassification.progression,
|
||||
None, self.player))
|
||||
self.multiworld.regions.append(stage)
|
||||
goal_location = self.get_location(gamma)
|
||||
goal_location.place_locked_item(MM3Item("Victory", ItemClassification.progression, None, self.player))
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
|
||||
|
||||
def create_item(self, name: str, force_non_progression: bool = False) -> MM3Item:
|
||||
item = item_table[name]
|
||||
classification = ItemClassification.filler
|
||||
if item.progression and not force_non_progression:
|
||||
classification = ItemClassification.progression_skip_balancing \
|
||||
if item.skip_balancing else ItemClassification.progression
|
||||
if item.useful:
|
||||
classification |= ItemClassification.useful
|
||||
return MM3Item(name, classification, item.code, self.player)
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return self.random.choices(list(filler_item_weights.keys()),
|
||||
weights=list(filler_item_weights.values()))[0]
|
||||
|
||||
def create_items(self) -> None:
|
||||
itempool = []
|
||||
# grab first robot master
|
||||
robot_master = self.item_id_to_name[0x0101 + self.options.starting_robot_master.value]
|
||||
self.multiworld.push_precollected(self.create_item(robot_master))
|
||||
itempool.extend([self.create_item(name) for name in stage_access_table.keys()
|
||||
if name != robot_master])
|
||||
itempool.extend([self.create_item(name) for name in robot_master_weapon_table.keys()])
|
||||
itempool.extend([self.create_item(name) for name in rush_item_table.keys()])
|
||||
total_checks = 31
|
||||
if self.options.consumables in (Consumables.option_1up_etank,
|
||||
Consumables.option_all):
|
||||
total_checks += 33
|
||||
if self.options.consumables in (Consumables.option_weapon_health,
|
||||
Consumables.option_all):
|
||||
total_checks += 106
|
||||
remaining = total_checks - len(itempool)
|
||||
itempool.extend([self.create_item(name)
|
||||
for name in self.random.choices(list(filler_item_weights.keys()),
|
||||
weights=list(filler_item_weights.values()),
|
||||
k=remaining)])
|
||||
self.multiworld.itempool += itempool
|
||||
|
||||
set_rules = set_rules
|
||||
|
||||
def generate_early(self) -> None:
|
||||
if (self.options.starting_robot_master.current_key == "gemini_man"
|
||||
and not any(item in self.options.start_inventory for item in rush_item_table.keys())) or \
|
||||
(self.options.starting_robot_master.current_key == "hard_man"
|
||||
and not any(item in self.options.start_inventory for item in [rush_coil, rush_jet])):
|
||||
robot_master_pool = [0, 1, 4, 5, 6, 7, ]
|
||||
if rush_marine in self.options.start_inventory:
|
||||
robot_master_pool.append(2)
|
||||
self.options.starting_robot_master.value = self.random.choice(robot_master_pool)
|
||||
logger.warning(
|
||||
f"Incompatible starting Robot Master, changing to "
|
||||
f"{self.options.starting_robot_master.current_key.replace('_', ' ').title()}")
|
||||
|
||||
def fill_hook(self,
|
||||
prog_item_pool: list["Item"],
|
||||
useful_item_pool: list["Item"],
|
||||
filler_item_pool: list["Item"],
|
||||
fill_locations: list["Location"]) -> None:
|
||||
# on a solo gen, fill can try to force Wily into sphere 2, but for most generations this is impossible
|
||||
# MM3 is worse than MM2 here, some of the RBMs can also require Rush
|
||||
if self.multiworld.players > 1:
|
||||
return # Don't need to change anything on a multi gen, fill should be able to solve it with a 4 sphere 1
|
||||
rbm_to_item = {
|
||||
0: needle_man_stage,
|
||||
1: magnet_man_stage,
|
||||
2: gemini_man_stage,
|
||||
3: hard_man_stage,
|
||||
4: top_man_stage,
|
||||
5: snake_man_stage,
|
||||
6: spark_man_stage,
|
||||
7: shadow_man_stage
|
||||
}
|
||||
affected_rbm = [2, 3] # Gemini and Hard will always have this happen
|
||||
possible_rbm = [0, 7] # Needle and Shadow are always valid targets, due to Rush Marine/Jet receive
|
||||
if self.options.consumables:
|
||||
possible_rbm.extend([4, 5]) # every stage has at least one of each consumable
|
||||
if self.options.consumables in (Consumables.option_weapon_health, Consumables.option_all):
|
||||
possible_rbm.extend([1, 6])
|
||||
else:
|
||||
affected_rbm.extend([1, 6])
|
||||
else:
|
||||
affected_rbm.extend([1, 4, 5, 6]) # only two checks on non consumables
|
||||
if self.options.starting_robot_master.value in affected_rbm:
|
||||
rbm_names = list(map(lambda s: rbm_to_item[s], possible_rbm))
|
||||
valid_second = [item for item in prog_item_pool
|
||||
if item.name in rbm_names
|
||||
and item.player == self.player]
|
||||
placed_item = self.random.choice(valid_second)
|
||||
rbm_defeated = (f"{robot_masters[self.options.starting_robot_master.value].replace(' Defeated', '')}"
|
||||
f" - Defeated")
|
||||
rbm_location = self.get_location(rbm_defeated)
|
||||
rbm_location.place_locked_item(placed_item)
|
||||
prog_item_pool.remove(placed_item)
|
||||
fill_locations.remove(rbm_location)
|
||||
target_rbm = (placed_item.code & 0xF) - 1
|
||||
if self.options.strict_weakness or (self.options.random_weakness
|
||||
and not (self.weapon_damage[0][target_rbm] > 0)):
|
||||
# we need to find a weakness for this boss
|
||||
weaknesses = [weapon for weapon in range(1, 9)
|
||||
if self.weapon_damage[weapon][target_rbm] >= minimum_weakness_requirement[weapon]]
|
||||
weapons = list(map(lambda s: weapons_to_name[s], weaknesses))
|
||||
valid_weapons = [item for item in prog_item_pool
|
||||
if item.name in weapons
|
||||
and item.player == self.player]
|
||||
placed_weapon = self.random.choice(valid_weapons)
|
||||
weapon_name = next(name for name, idx in lookup_location_to_id.items()
|
||||
if idx == 0x0101 + self.options.starting_robot_master.value)
|
||||
weapon_location = self.get_location(weapon_name)
|
||||
weapon_location.place_locked_item(placed_weapon)
|
||||
prog_item_pool.remove(placed_weapon)
|
||||
fill_locations.remove(weapon_location)
|
||||
|
||||
def generate_output(self, output_directory: str) -> None:
|
||||
try:
|
||||
patch = MM3ProcedurePatch(player=self.player, player_name=self.player_name)
|
||||
patch_rom(self, patch)
|
||||
|
||||
self.rom_name = patch.name
|
||||
|
||||
patch.write(os.path.join(output_directory,
|
||||
f"{self.multiworld.get_out_file_name_base(self.player)}{patch.patch_file_ending}"))
|
||||
except Exception:
|
||||
raise
|
||||
finally:
|
||||
self.rom_name_available_event.set() # make sure threading continues and errors are collected
|
||||
|
||||
def fill_slot_data(self) -> dict[str, Any]:
|
||||
return {
|
||||
"death_link": self.options.death_link.value,
|
||||
"weapon_damage": self.weapon_damage,
|
||||
"wily_4_weapons": self.wily_4_weapons
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def interpret_slot_data(slot_data: dict[str, Any]) -> dict[str, Any]:
|
||||
local_weapon = {int(key): value for key, value in slot_data["weapon_damage"].items()}
|
||||
local_wily = {int(key): value for key, value in slot_data["wily_4_weapons"].items()}
|
||||
return {"weapon_damage": local_weapon, "wily_4_weapons": local_wily}
|
||||
|
||||
def modify_multidata(self, multidata: dict[str, Any]) -> None:
|
||||
# wait for self.rom_name to be available.
|
||||
self.rom_name_available_event.wait()
|
||||
rom_name = getattr(self, "rom_name", None)
|
||||
# we skip in case of error, so that the original error in the output thread is the one that gets raised
|
||||
if rom_name:
|
||||
new_name = base64.b64encode(bytes(self.rom_name)).decode()
|
||||
multidata["connect_names"][new_name] = multidata["connect_names"][self.player_name]
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"game": "Mega Man 3",
|
||||
"authors": ["Silvris"],
|
||||
"world_version": "0.1.7",
|
||||
"minimum_ap_version": "0.6.4"
|
||||
}
|
||||
@@ -1,783 +0,0 @@
|
||||
import logging
|
||||
import time
|
||||
from enum import IntEnum
|
||||
from base64 import b64encode
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from NetUtils import ClientStatus, color, NetworkItem
|
||||
from worlds._bizhawk.client import BizHawkClient
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from worlds._bizhawk.context import BizHawkClientContext, BizHawkClientCommandProcessor
|
||||
|
||||
nes_logger = logging.getLogger("NES")
|
||||
logger = logging.getLogger("Client")
|
||||
|
||||
MM3_CURRENT_STAGE = 0x22
|
||||
MM3_MEGAMAN_STATE = 0x30
|
||||
MM3_PROG_STATE = 0x60
|
||||
MM3_ROBOT_MASTERS_DEFEATED = 0x61
|
||||
MM3_DOC_STATUS = 0x62
|
||||
MM3_HEALTH = 0xA2
|
||||
MM3_WEAPON_ENERGY = 0xA3
|
||||
MM3_WEAPONS = {
|
||||
1: 1,
|
||||
2: 3,
|
||||
3: 0,
|
||||
4: 2,
|
||||
5: 4,
|
||||
6: 5,
|
||||
7: 7,
|
||||
8: 9,
|
||||
0x11: 6,
|
||||
0x12: 8,
|
||||
0x13: 10,
|
||||
}
|
||||
|
||||
MM3_DOC_REMAP = {
|
||||
0: 0,
|
||||
1: 1,
|
||||
2: 2,
|
||||
3: 3,
|
||||
4: 6,
|
||||
5: 7,
|
||||
6: 4,
|
||||
7: 5
|
||||
}
|
||||
MM3_LIVES = 0xAE
|
||||
MM3_E_TANKS = 0xAF
|
||||
MM3_ENERGY_BAR = 0xB2
|
||||
MM3_CONSUMABLES = 0x150
|
||||
MM3_ROBOT_MASTERS_UNLOCKED = 0x680
|
||||
MM3_DOC_ROBOT_UNLOCKED = 0x681
|
||||
MM3_ENERGYLINK = 0x682
|
||||
MM3_LAST_WILY = 0x683
|
||||
MM3_RBM_STROBE = 0x684
|
||||
MM3_SFX_QUEUE = 0x685
|
||||
MM3_DOC_ROBOT_DEFEATED = 0x686
|
||||
MM3_COMPLETED_STAGES = 0x687
|
||||
MM3_RECEIVED_ITEMS = 0x688
|
||||
MM3_RUSH_RECEIVED = 0x689
|
||||
|
||||
MM3_CONSUMABLE_TABLE: dict[int, dict[int, tuple[int, int]]] = {
|
||||
# Stage:
|
||||
# Item: (byte offset, bit mask)
|
||||
0: {
|
||||
0x0200: (0, 5),
|
||||
0x0201: (3, 2),
|
||||
},
|
||||
1: {
|
||||
0x0202: (2, 6),
|
||||
0x0203: (2, 5),
|
||||
0x0204: (2, 4),
|
||||
0x0205: (2, 3),
|
||||
0x0206: (3, 6),
|
||||
0x0207: (3, 5),
|
||||
0x0208: (3, 7),
|
||||
0x0209: (4, 0)
|
||||
},
|
||||
2: {
|
||||
0x020A: (2, 7),
|
||||
0x020B: (3, 0),
|
||||
0x020C: (3, 1),
|
||||
0x020D: (3, 2),
|
||||
0x020E: (4, 2),
|
||||
0x020F: (4, 3),
|
||||
0x0210: (4, 7),
|
||||
0x0211: (5, 1),
|
||||
0x0212: (6, 1),
|
||||
0x0213: (7, 0)
|
||||
},
|
||||
3: {
|
||||
0x0214: (0, 6),
|
||||
0x0215: (1, 5),
|
||||
0x0216: (2, 3),
|
||||
0x0217: (2, 7),
|
||||
0x0218: (2, 6),
|
||||
0x0219: (2, 5),
|
||||
0x021A: (4, 5),
|
||||
},
|
||||
4: {
|
||||
0x021B: (1, 3),
|
||||
0x021C: (1, 5),
|
||||
0x021D: (1, 7),
|
||||
0x021E: (2, 0),
|
||||
0x021F: (1, 6),
|
||||
0x0220: (2, 4),
|
||||
0x0221: (2, 5),
|
||||
0x0222: (4, 5)
|
||||
},
|
||||
5: {
|
||||
0x0223: (3, 0),
|
||||
0x0224: (3, 2),
|
||||
0x0225: (4, 5),
|
||||
0x0226: (4, 6),
|
||||
0x0227: (6, 4),
|
||||
},
|
||||
6: {
|
||||
0x0228: (2, 0),
|
||||
0x0229: (2, 1),
|
||||
0x022A: (3, 1),
|
||||
0x022B: (3, 2),
|
||||
0x022C: (3, 3),
|
||||
0x022D: (3, 4),
|
||||
},
|
||||
7: {
|
||||
0x022E: (3, 5),
|
||||
0x022F: (3, 4),
|
||||
0x0230: (3, 3),
|
||||
0x0231: (3, 2),
|
||||
},
|
||||
8: {
|
||||
0x0232: (1, 4),
|
||||
0x0233: (2, 1),
|
||||
0x0234: (2, 2),
|
||||
0x0235: (2, 5),
|
||||
0x0236: (3, 5),
|
||||
0x0237: (4, 2),
|
||||
0x0238: (4, 4),
|
||||
0x0239: (5, 3),
|
||||
0x023A: (6, 0),
|
||||
0x023B: (6, 1),
|
||||
0x023C: (7, 5),
|
||||
|
||||
},
|
||||
9: {
|
||||
0x023D: (3, 2),
|
||||
0x023E: (3, 6),
|
||||
0x023F: (4, 5),
|
||||
0x0240: (5, 4),
|
||||
},
|
||||
10: {
|
||||
0x0241: (0, 2),
|
||||
0x0242: (2, 4)
|
||||
},
|
||||
11: {
|
||||
0x0243: (4, 1),
|
||||
0x0244: (6, 0),
|
||||
0x0245: (6, 1),
|
||||
0x0246: (6, 2),
|
||||
0x0247: (6, 3),
|
||||
},
|
||||
12: {
|
||||
0x0248: (0, 0),
|
||||
0x0249: (0, 3),
|
||||
0x024A: (0, 5),
|
||||
0x024B: (1, 6),
|
||||
0x024C: (2, 7),
|
||||
0x024D: (2, 3),
|
||||
0x024E: (2, 1),
|
||||
0x024F: (2, 2),
|
||||
0x0250: (3, 5),
|
||||
0x0251: (3, 4),
|
||||
0x0252: (3, 6),
|
||||
0x0253: (3, 7)
|
||||
},
|
||||
13: {
|
||||
0x0254: (0, 3),
|
||||
0x0255: (0, 6),
|
||||
0x0256: (1, 0),
|
||||
0x0257: (3, 0),
|
||||
0x0258: (3, 2),
|
||||
0x0259: (3, 3),
|
||||
0x025A: (3, 4),
|
||||
0x025B: (3, 5),
|
||||
0x025C: (3, 6),
|
||||
0x025D: (4, 0),
|
||||
0x025E: (3, 7),
|
||||
0x025F: (4, 1),
|
||||
0x0260: (4, 2),
|
||||
},
|
||||
14: {
|
||||
0x0261: (0, 3),
|
||||
0x0262: (0, 2),
|
||||
0x0263: (0, 6),
|
||||
0x0264: (1, 2),
|
||||
0x0265: (1, 7),
|
||||
0x0266: (2, 0),
|
||||
0x0267: (2, 1),
|
||||
0x0268: (2, 2),
|
||||
0x0269: (2, 3),
|
||||
0x026A: (5, 2),
|
||||
0x026B: (5, 3),
|
||||
},
|
||||
15: {
|
||||
0x026C: (0, 0),
|
||||
0x026D: (0, 1),
|
||||
0x026E: (0, 2),
|
||||
0x026F: (0, 3),
|
||||
0x0270: (0, 4),
|
||||
0x0271: (0, 6),
|
||||
0x0272: (1, 0),
|
||||
0x0273: (1, 2),
|
||||
0x0274: (1, 3),
|
||||
0x0275: (1, 1),
|
||||
0x0276: (0, 7),
|
||||
0x0277: (3, 2),
|
||||
0x0278: (2, 2),
|
||||
0x0279: (2, 3),
|
||||
0x027A: (2, 4),
|
||||
0x027B: (2, 5),
|
||||
0x027C: (3, 1),
|
||||
0x027D: (3, 0),
|
||||
0x027E: (2, 7),
|
||||
0x027F: (2, 6),
|
||||
},
|
||||
16: {
|
||||
0x0280: (0, 0),
|
||||
0x0281: (0, 3),
|
||||
0x0282: (0, 1),
|
||||
0x0283: (0, 2),
|
||||
},
|
||||
17: {
|
||||
0x0284: (0, 2),
|
||||
0x0285: (0, 6),
|
||||
0x0286: (0, 1),
|
||||
0x0287: (0, 5),
|
||||
0x0288: (0, 3),
|
||||
0x0289: (0, 0),
|
||||
0x028A: (0, 4)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def to_oneup_format(val: int) -> int:
|
||||
return ((val // 10) * 0x10) + val % 10
|
||||
|
||||
|
||||
def from_oneup_format(val: int) -> int:
|
||||
return ((val // 0x10) * 10) + val % 0x10
|
||||
|
||||
|
||||
class MM3EnergyLinkType(IntEnum):
|
||||
Life = 0
|
||||
NeedleCannon = 1
|
||||
MagnetMissile = 2
|
||||
GeminiLaser = 3
|
||||
HardKnuckle = 4
|
||||
TopSpin = 5
|
||||
SearchSnake = 6
|
||||
SparkShot = 7
|
||||
ShadowBlade = 8
|
||||
OneUP = 12
|
||||
RushCoil = 0x11
|
||||
RushMarine = 0x12
|
||||
RushJet = 0x13
|
||||
|
||||
|
||||
request_to_name: dict[str, str] = {
|
||||
"HP": "health",
|
||||
"NE": "Needle Cannon energy",
|
||||
"MA": "Magnet Missile energy",
|
||||
"GE": "Gemini Laser energy",
|
||||
"HA": "Hard Knuckle energy",
|
||||
"TO": "Top Spin energy",
|
||||
"SN": "Search Snake energy",
|
||||
"SP": "Spark Shot energy",
|
||||
"SH": "Shadow Blade energy",
|
||||
"RC": "Rush Coil energy",
|
||||
"RM": "Rush Marine energy",
|
||||
"RJ": "Rush Jet energy",
|
||||
"1U": "lives"
|
||||
}
|
||||
|
||||
HP_EXCHANGE_RATE = 500000000
|
||||
WEAPON_EXCHANGE_RATE = 250000000
|
||||
ONEUP_EXCHANGE_RATE = 14000000000
|
||||
|
||||
|
||||
def cmd_pool(self: "BizHawkClientCommandProcessor") -> None:
|
||||
"""Check the current pool of EnergyLink, and requestable refills from it."""
|
||||
if self.ctx.game != "Mega Man 3":
|
||||
logger.warning("This command can only be used when playing Mega Man 3.")
|
||||
return
|
||||
if not self.ctx.server or not self.ctx.slot:
|
||||
logger.warning("You must be connected to a server to use this command.")
|
||||
return
|
||||
energylink = self.ctx.stored_data.get(f"EnergyLink{self.ctx.team}", 0)
|
||||
health_points = energylink // HP_EXCHANGE_RATE
|
||||
weapon_points = energylink // WEAPON_EXCHANGE_RATE
|
||||
lives = energylink // ONEUP_EXCHANGE_RATE
|
||||
logger.info(f"Healing available: {health_points}\n"
|
||||
f"Weapon refill available: {weapon_points}\n"
|
||||
f"Lives available: {lives}")
|
||||
|
||||
|
||||
def cmd_request(self: "BizHawkClientCommandProcessor", amount: str, target: str) -> None:
|
||||
"""Request a refill from EnergyLink."""
|
||||
from worlds._bizhawk.context import BizHawkClientContext
|
||||
if self.ctx.game != "Mega Man 3":
|
||||
logger.warning("This command can only be used when playing Mega Man 3.")
|
||||
return
|
||||
if not self.ctx.server or not self.ctx.slot:
|
||||
logger.warning("You must be connected to a server to use this command.")
|
||||
return
|
||||
valid_targets: dict[str, MM3EnergyLinkType] = {
|
||||
"HP": MM3EnergyLinkType.Life,
|
||||
"NE": MM3EnergyLinkType.NeedleCannon,
|
||||
"MA": MM3EnergyLinkType.MagnetMissile,
|
||||
"GE": MM3EnergyLinkType.GeminiLaser,
|
||||
"HA": MM3EnergyLinkType.HardKnuckle,
|
||||
"TO": MM3EnergyLinkType.TopSpin,
|
||||
"SN": MM3EnergyLinkType.SearchSnake,
|
||||
"SP": MM3EnergyLinkType.SparkShot,
|
||||
"SH": MM3EnergyLinkType.ShadowBlade,
|
||||
"RC": MM3EnergyLinkType.RushCoil,
|
||||
"RM": MM3EnergyLinkType.RushMarine,
|
||||
"RJ": MM3EnergyLinkType.RushJet,
|
||||
"1U": MM3EnergyLinkType.OneUP
|
||||
}
|
||||
if target.upper() not in valid_targets:
|
||||
logger.warning(f"Unrecognized target {target.upper()}. Available targets: {', '.join(valid_targets.keys())}")
|
||||
return
|
||||
ctx = self.ctx
|
||||
assert isinstance(ctx, BizHawkClientContext)
|
||||
client = ctx.client_handler
|
||||
assert isinstance(client, MegaMan3Client)
|
||||
client.refill_queue.append((valid_targets[target.upper()], int(amount)))
|
||||
logger.info(f"Restoring {amount} {request_to_name[target.upper()]}.")
|
||||
|
||||
|
||||
def cmd_autoheal(self: "BizHawkClientCommandProcessor") -> None:
|
||||
"""Enable auto heal from EnergyLink."""
|
||||
if self.ctx.game != "Mega Man 3":
|
||||
logger.warning("This command can only be used when playing Mega Man 3.")
|
||||
return
|
||||
if not self.ctx.server or not self.ctx.slot:
|
||||
logger.warning("You must be connected to a server to use this command.")
|
||||
return
|
||||
else:
|
||||
assert isinstance(self.ctx.client_handler, MegaMan3Client)
|
||||
if self.ctx.client_handler.auto_heal:
|
||||
self.ctx.client_handler.auto_heal = False
|
||||
logger.info(f"Auto healing disabled.")
|
||||
else:
|
||||
self.ctx.client_handler.auto_heal = True
|
||||
logger.info(f"Auto healing enabled.")
|
||||
|
||||
|
||||
def get_sfx_writes(sfx: int) -> tuple[int, bytes, str]:
|
||||
return MM3_SFX_QUEUE, sfx.to_bytes(1, 'little'), "RAM"
|
||||
|
||||
|
||||
class MegaMan3Client(BizHawkClient):
|
||||
game = "Mega Man 3"
|
||||
system = "NES"
|
||||
patch_suffix = ".apmm3"
|
||||
item_queue: list[NetworkItem] = []
|
||||
pending_death_link: bool = False
|
||||
# default to true, as we don't want to send a deathlink until Mega Man's HP is initialized once
|
||||
sending_death_link: bool = True
|
||||
death_link: bool = False
|
||||
energy_link: bool = False
|
||||
rom: bytes | None = None
|
||||
weapon_energy: int = 0
|
||||
health_energy: int = 0
|
||||
auto_heal: bool = False
|
||||
refill_queue: list[tuple[MM3EnergyLinkType, int]] = []
|
||||
last_wily: int | None = None # default to wily 1
|
||||
doc_status: int | None = None # default to no doc progress
|
||||
|
||||
async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
|
||||
from worlds._bizhawk import RequestFailedError, read, get_memory_size
|
||||
from . import MM3World
|
||||
|
||||
try:
|
||||
|
||||
if (await get_memory_size(ctx.bizhawk_ctx, "PRG ROM")) < 0x3FFB0:
|
||||
# not the entire size, but enough to check validation
|
||||
if "pool" in ctx.command_processor.commands:
|
||||
ctx.command_processor.commands.pop("pool")
|
||||
if "request" in ctx.command_processor.commands:
|
||||
ctx.command_processor.commands.pop("request")
|
||||
if "autoheal" in ctx.command_processor.commands:
|
||||
ctx.command_processor.commands.pop("autoheal")
|
||||
return False
|
||||
|
||||
game_name, version = (await read(ctx.bizhawk_ctx, [(0x3F320, 21, "PRG ROM"),
|
||||
(0x3F33C, 3, "PRG ROM")]))
|
||||
if game_name[:3] != b"MM3" or version != bytes(MM3World.world_version):
|
||||
if game_name[:3] == b"MM3":
|
||||
# I think this is an easier check than the other?
|
||||
older_version = f"{version[0]}.{version[1]}.{version[2]}"
|
||||
logger.warning(f"This Mega Man 3 patch was generated for an different version of the apworld. "
|
||||
f"Please use that version to connect instead.\n"
|
||||
f"Patch version: ({older_version})\n"
|
||||
f"Client version: ({'.'.join([str(i) for i in MM3World.world_version])})")
|
||||
if "pool" in ctx.command_processor.commands:
|
||||
ctx.command_processor.commands.pop("pool")
|
||||
if "request" in ctx.command_processor.commands:
|
||||
ctx.command_processor.commands.pop("request")
|
||||
if "autoheal" in ctx.command_processor.commands:
|
||||
ctx.command_processor.commands.pop("autoheal")
|
||||
return False
|
||||
except UnicodeDecodeError:
|
||||
return False
|
||||
except RequestFailedError:
|
||||
return False # Should verify on the next pass
|
||||
|
||||
ctx.game = self.game
|
||||
self.rom = game_name
|
||||
ctx.items_handling = 0b111
|
||||
ctx.want_slot_data = False
|
||||
deathlink = (await read(ctx.bizhawk_ctx, [(0x3F336, 1, "PRG ROM")]))[0][0]
|
||||
if deathlink & 0x01:
|
||||
self.death_link = True
|
||||
await ctx.update_death_link(self.death_link)
|
||||
if deathlink & 0x02:
|
||||
self.energy_link = True
|
||||
|
||||
if self.energy_link:
|
||||
if "pool" not in ctx.command_processor.commands:
|
||||
ctx.command_processor.commands["pool"] = cmd_pool
|
||||
if "request" not in ctx.command_processor.commands:
|
||||
ctx.command_processor.commands["request"] = cmd_request
|
||||
if "autoheal" not in ctx.command_processor.commands:
|
||||
ctx.command_processor.commands["autoheal"] = cmd_autoheal
|
||||
|
||||
return True
|
||||
|
||||
async def set_auth(self, ctx: "BizHawkClientContext") -> None:
|
||||
if self.rom:
|
||||
ctx.auth = b64encode(self.rom).decode()
|
||||
|
||||
def on_package(self, ctx: "BizHawkClientContext", cmd: str, args: dict[str, Any]) -> None:
|
||||
if cmd == "Bounced":
|
||||
if "tags" in args:
|
||||
assert ctx.slot is not None
|
||||
if "DeathLink" in args["tags"] and args["data"]["source"] != ctx.slot_info[ctx.slot].name:
|
||||
self.on_deathlink(ctx)
|
||||
elif cmd == "Retrieved":
|
||||
if f"MM3_LAST_WILY_{ctx.team}_{ctx.slot}" in args["keys"]:
|
||||
self.last_wily = args["keys"][f"MM3_LAST_WILY_{ctx.team}_{ctx.slot}"]
|
||||
if f"MM3_DOC_STATUS_{ctx.team}_{ctx.slot}" in args["keys"]:
|
||||
self.doc_status = args["keys"][f"MM3_DOC_STATUS_{ctx.team}_{ctx.slot}"]
|
||||
elif cmd == "Connected":
|
||||
if self.energy_link:
|
||||
ctx.set_notify(f"EnergyLink{ctx.team}")
|
||||
if ctx.ui:
|
||||
ctx.ui.enable_energy_link()
|
||||
|
||||
async def send_deathlink(self, ctx: "BizHawkClientContext") -> None:
|
||||
self.sending_death_link = True
|
||||
ctx.last_death_link = time.time()
|
||||
await ctx.send_death("Mega Man was defeated.")
|
||||
|
||||
def on_deathlink(self, ctx: "BizHawkClientContext") -> None:
|
||||
ctx.last_death_link = time.time()
|
||||
self.pending_death_link = True
|
||||
|
||||
async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
|
||||
from worlds._bizhawk import read, write
|
||||
|
||||
if ctx.server is None:
|
||||
return
|
||||
|
||||
if ctx.slot is None:
|
||||
return
|
||||
|
||||
# get our relevant bytes
|
||||
(prog_state, robot_masters_unlocked, robot_masters_defeated, doc_status, doc_robo_unlocked, doc_robo_defeated,
|
||||
rush_acquired, received_items, completed_stages, consumable_checks,
|
||||
e_tanks, lives, weapon_energy, health, state, bar_state, current_stage,
|
||||
energy_link_packet, last_wily) = await read(ctx.bizhawk_ctx, [
|
||||
(MM3_PROG_STATE, 1, "RAM"),
|
||||
(MM3_ROBOT_MASTERS_UNLOCKED, 1, "RAM"),
|
||||
(MM3_ROBOT_MASTERS_DEFEATED, 1, "RAM"),
|
||||
(MM3_DOC_STATUS, 1, "RAM"),
|
||||
(MM3_DOC_ROBOT_UNLOCKED, 1, "RAM"),
|
||||
(MM3_DOC_ROBOT_DEFEATED, 1, "RAM"),
|
||||
(MM3_RUSH_RECEIVED, 1, "RAM"),
|
||||
(MM3_RECEIVED_ITEMS, 1, "RAM"),
|
||||
(MM3_COMPLETED_STAGES, 0x1, "RAM"),
|
||||
(MM3_CONSUMABLES, 16, "RAM"), # Could be more but 16 definitely catches all current
|
||||
(MM3_E_TANKS, 1, "RAM"),
|
||||
(MM3_LIVES, 1, "RAM"),
|
||||
(MM3_WEAPON_ENERGY, 11, "RAM"),
|
||||
(MM3_HEALTH, 1, "RAM"),
|
||||
(MM3_MEGAMAN_STATE, 1, "RAM"),
|
||||
(MM3_ENERGY_BAR, 2, "RAM"),
|
||||
(MM3_CURRENT_STAGE, 1, "RAM"),
|
||||
(MM3_ENERGYLINK, 1, "RAM"),
|
||||
(MM3_LAST_WILY, 1, "RAM"),
|
||||
])
|
||||
|
||||
if bar_state[0] not in (0x00, 0x80):
|
||||
return # Game is not initialized
|
||||
# Bit of a trick here, bar state can only be 0x00 or 0x80 (display health bar, or don't)
|
||||
# This means it can double as init guard and in-stage tracker
|
||||
|
||||
if not ctx.finished_game and completed_stages[0] & 0x20:
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "StatusUpdate",
|
||||
"status": ClientStatus.CLIENT_GOAL
|
||||
}])
|
||||
writes = []
|
||||
|
||||
# deathlink
|
||||
# only handle deathlink in bar state 0x80 (in stage)
|
||||
if bar_state[0] == 0x80:
|
||||
if self.pending_death_link:
|
||||
writes.append((MM3_MEGAMAN_STATE, bytes([0x0E]), "RAM"))
|
||||
self.pending_death_link = False
|
||||
self.sending_death_link = True
|
||||
if "DeathLink" in ctx.tags and ctx.last_death_link + 1 < time.time():
|
||||
if state[0] == 0x0E and not self.sending_death_link:
|
||||
await self.send_deathlink(ctx)
|
||||
elif state[0] != 0x0E:
|
||||
self.sending_death_link = False
|
||||
|
||||
if self.last_wily != last_wily[0]:
|
||||
if self.last_wily is None:
|
||||
# revalidate last wily from data storage
|
||||
await ctx.send_msgs([{"cmd": "Set", "key": f"MM3_LAST_WILY_{ctx.team}_{ctx.slot}", "operations": [
|
||||
{"operation": "default", "value": 0xC}
|
||||
]}])
|
||||
await ctx.send_msgs([{"cmd": "Get", "keys": [f"MM3_LAST_WILY_{ctx.team}_{ctx.slot}"]}])
|
||||
elif last_wily[0] == 0:
|
||||
writes.append((MM3_LAST_WILY, self.last_wily.to_bytes(1, "little"), "RAM"))
|
||||
else:
|
||||
# correct our setting
|
||||
self.last_wily = last_wily[0]
|
||||
await ctx.send_msgs([{"cmd": "Set", "key": f"MM3_LAST_WILY_{ctx.team}_{ctx.slot}", "operations": [
|
||||
{"operation": "replace", "value": self.last_wily}
|
||||
]}])
|
||||
|
||||
if self.doc_status != doc_status[0]:
|
||||
if self.doc_status is None:
|
||||
# revalidate doc status from data storage
|
||||
await ctx.send_msgs([{"cmd": "Set", "key": f"MM3_DOC_STATUS_{ctx.team}_{ctx.slot}", "operations": [
|
||||
{"operation": "default", "value": 0}
|
||||
]}])
|
||||
await ctx.send_msgs([{"cmd": "Get", "keys": [f"MM3_DOC_STATUS_{ctx.team}_{ctx.slot}"]}])
|
||||
elif doc_status[0] == 0:
|
||||
writes.append((MM3_DOC_STATUS, self.doc_status.to_bytes(1, "little"), "RAM"))
|
||||
else:
|
||||
# correct our setting
|
||||
# shouldn't be possible to desync, but we'll account for it anyways
|
||||
self.doc_status |= doc_status[0]
|
||||
await ctx.send_msgs([{"cmd": "Set", "key": f"MM3_DOC_STATUS_{ctx.team}_{ctx.slot}", "operations": [
|
||||
{"operation": "replace", "value": self.doc_status}
|
||||
]}])
|
||||
|
||||
weapon_energy = bytearray(weapon_energy)
|
||||
# handle receiving items
|
||||
recv_amount = received_items[0]
|
||||
if recv_amount < len(ctx.items_received):
|
||||
item = ctx.items_received[recv_amount]
|
||||
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
|
||||
color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'),
|
||||
color(ctx.player_names[item.player], 'yellow'),
|
||||
ctx.location_names.lookup_in_slot(item.location, item.player), recv_amount, len(ctx.items_received)))
|
||||
|
||||
if item.item & 0x120 == 0:
|
||||
# Robot Master Weapon, or Rush
|
||||
new_weapons = item.item & 0xFF
|
||||
weapon_energy[MM3_WEAPONS[new_weapons]] |= 0x9C
|
||||
writes.append((MM3_WEAPON_ENERGY, weapon_energy, "RAM"))
|
||||
writes.append(get_sfx_writes(0x32))
|
||||
elif item.item & 0x20 == 0:
|
||||
# Robot Master Stage Access
|
||||
# Catch the Doc Robo here
|
||||
if item.item & 0x10:
|
||||
ptr = MM3_DOC_ROBOT_UNLOCKED
|
||||
unlocked = doc_robo_unlocked
|
||||
else:
|
||||
ptr = MM3_ROBOT_MASTERS_UNLOCKED
|
||||
unlocked = robot_masters_unlocked
|
||||
new_stages = unlocked[0] | (1 << ((item.item & 0xF) - 1))
|
||||
print(new_stages)
|
||||
writes.append((ptr, new_stages.to_bytes(1, 'little'), "RAM"))
|
||||
writes.append(get_sfx_writes(0x34))
|
||||
writes.append((MM3_RBM_STROBE, b"\x01", "RAM"))
|
||||
else:
|
||||
# append to the queue, so we handle it later
|
||||
self.item_queue.append(item)
|
||||
recv_amount += 1
|
||||
writes.append((MM3_RECEIVED_ITEMS, recv_amount.to_bytes(1, 'little'), "RAM"))
|
||||
|
||||
if energy_link_packet[0]:
|
||||
pickup = energy_link_packet[0]
|
||||
if pickup in (0x64, 0x65):
|
||||
# Health pickups
|
||||
if pickup == 0x65:
|
||||
value = 2
|
||||
else:
|
||||
value = 10
|
||||
exchange_rate = HP_EXCHANGE_RATE
|
||||
elif pickup in (0x66, 0x67):
|
||||
# Weapon Energy
|
||||
if pickup == 0x67:
|
||||
value = 2
|
||||
else:
|
||||
value = 10
|
||||
exchange_rate = WEAPON_EXCHANGE_RATE
|
||||
elif pickup == 0x69:
|
||||
# 1-Up
|
||||
value = 1
|
||||
exchange_rate = ONEUP_EXCHANGE_RATE
|
||||
else:
|
||||
# if we managed to pickup something else, we should just fall through
|
||||
value = 0
|
||||
exchange_rate = 0
|
||||
contribution = (value * exchange_rate) >> 1
|
||||
if contribution:
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "Set", "key": f"EnergyLink{ctx.team}", "slot": ctx.slot, "operations":
|
||||
[{"operation": "add", "value": contribution},
|
||||
{"operation": "max", "value": 0}]}])
|
||||
logger.info(f"Deposited {contribution / HP_EXCHANGE_RATE} health into the pool.")
|
||||
writes.append((MM3_ENERGYLINK, 0x00.to_bytes(1, "little"), "RAM"))
|
||||
|
||||
if self.weapon_energy:
|
||||
# Weapon Energy
|
||||
# We parse the whole thing to spread it as thin as possible
|
||||
current_energy = self.weapon_energy
|
||||
for i, weapon in zip(range(len(weapon_energy)), weapon_energy):
|
||||
if weapon & 0x80 and (weapon & 0x7F) < 0x1C:
|
||||
missing = 0x1C - (weapon & 0x7F)
|
||||
if missing > self.weapon_energy:
|
||||
missing = self.weapon_energy
|
||||
self.weapon_energy -= missing
|
||||
weapon_energy[i] = weapon + missing
|
||||
if not self.weapon_energy:
|
||||
writes.append((MM3_WEAPON_ENERGY, weapon_energy, "RAM"))
|
||||
break
|
||||
else:
|
||||
if current_energy != self.weapon_energy:
|
||||
writes.append((MM3_WEAPON_ENERGY, weapon_energy, "RAM"))
|
||||
|
||||
if self.health_energy or self.auto_heal:
|
||||
# Health Energy
|
||||
# We save this if the player has not taken any damage
|
||||
current_health = health[0]
|
||||
if 0 < (current_health & 0x7F) < 0x1C:
|
||||
health_diff = 0x1C - (current_health & 0x7F)
|
||||
if self.health_energy:
|
||||
if health_diff > self.health_energy:
|
||||
health_diff = self.health_energy
|
||||
self.health_energy -= health_diff
|
||||
else:
|
||||
pool = ctx.stored_data.get(f"EnergyLink{ctx.team}", 0)
|
||||
if health_diff * HP_EXCHANGE_RATE > pool:
|
||||
health_diff = int(pool // HP_EXCHANGE_RATE)
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "Set", "key": f"EnergyLink{ctx.team}", "slot": ctx.slot, "operations":
|
||||
[{"operation": "add", "value": -health_diff * HP_EXCHANGE_RATE},
|
||||
{"operation": "max", "value": 0}]}])
|
||||
current_health += health_diff
|
||||
writes.append((MM3_HEALTH, current_health.to_bytes(1, 'little'), "RAM"))
|
||||
|
||||
if self.refill_queue:
|
||||
refill_type, refill_amount = self.refill_queue.pop()
|
||||
if refill_type == MM3EnergyLinkType.Life:
|
||||
exchange_rate = HP_EXCHANGE_RATE
|
||||
elif refill_type == MM3EnergyLinkType.OneUP:
|
||||
exchange_rate = ONEUP_EXCHANGE_RATE
|
||||
else:
|
||||
exchange_rate = WEAPON_EXCHANGE_RATE
|
||||
pool = ctx.stored_data.get(f"EnergyLink{ctx.team}", 0)
|
||||
request = exchange_rate * refill_amount
|
||||
if request > pool:
|
||||
logger.warning(
|
||||
f"Not enough energy to fulfill the request. Maximum request: {pool // exchange_rate}")
|
||||
else:
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "Set", "key": f"EnergyLink{ctx.team}", "slot": ctx.slot, "operations":
|
||||
[{"operation": "add", "value": -request},
|
||||
{"operation": "max", "value": 0}]}])
|
||||
if refill_type == MM3EnergyLinkType.Life:
|
||||
refill_ptr = MM3_HEALTH
|
||||
elif refill_type == MM3EnergyLinkType.OneUP:
|
||||
refill_ptr = MM3_LIVES
|
||||
else:
|
||||
refill_ptr = MM3_WEAPON_ENERGY + MM3_WEAPONS[refill_type]
|
||||
current_value = (await read(ctx.bizhawk_ctx, [(refill_ptr, 1, "RAM")]))[0][0]
|
||||
if refill_type == MM3EnergyLinkType.OneUP:
|
||||
current_value = from_oneup_format(current_value)
|
||||
new_value = min(0x9C if refill_type != MM3EnergyLinkType.OneUP else 99, current_value + refill_amount)
|
||||
if refill_type == MM3EnergyLinkType.OneUP:
|
||||
new_value = to_oneup_format(new_value)
|
||||
writes.append((refill_ptr, new_value.to_bytes(1, "little"), "RAM"))
|
||||
|
||||
if len(self.item_queue):
|
||||
item = self.item_queue.pop(0)
|
||||
idx = item.item & 0xF
|
||||
if idx == 0:
|
||||
# 1-Up
|
||||
current_lives = from_oneup_format(lives[0])
|
||||
if current_lives > 99:
|
||||
self.item_queue.append(item)
|
||||
else:
|
||||
current_lives += 1
|
||||
current_lives = to_oneup_format(current_lives)
|
||||
writes.append((MM3_LIVES, current_lives.to_bytes(1, 'little'), "RAM"))
|
||||
writes.append(get_sfx_writes(0x14))
|
||||
elif idx == 1:
|
||||
self.weapon_energy += 0xE
|
||||
writes.append(get_sfx_writes(0x1C))
|
||||
elif idx == 2:
|
||||
self.health_energy += 0xE
|
||||
writes.append(get_sfx_writes(0x1C))
|
||||
elif idx == 3:
|
||||
current_tanks = from_oneup_format(e_tanks[0])
|
||||
if current_tanks > 99:
|
||||
self.item_queue.append(item)
|
||||
else:
|
||||
current_tanks += 1
|
||||
current_tanks = to_oneup_format(current_tanks)
|
||||
writes.append((MM3_E_TANKS, current_tanks.to_bytes(1, 'little'), "RAM"))
|
||||
writes.append(get_sfx_writes(0x14))
|
||||
|
||||
await write(ctx.bizhawk_ctx, writes)
|
||||
|
||||
new_checks = []
|
||||
# check for locations
|
||||
for i in range(8):
|
||||
flag = 1 << i
|
||||
if robot_masters_defeated[0] & flag:
|
||||
rbm_id = 0x0001 + i
|
||||
if rbm_id not in ctx.checked_locations:
|
||||
new_checks.append(rbm_id)
|
||||
wep_id = 0x0101 + i
|
||||
if wep_id not in ctx.checked_locations:
|
||||
new_checks.append(wep_id)
|
||||
if doc_robo_defeated[0] & flag:
|
||||
doc_id = 0x0010 + MM3_DOC_REMAP[i]
|
||||
if doc_id not in ctx.checked_locations:
|
||||
new_checks.append(doc_id)
|
||||
|
||||
for i in range(2):
|
||||
flag = 1 << i
|
||||
if rush_acquired[0] & flag:
|
||||
itm_id = 0x0111 + i
|
||||
if itm_id not in ctx.checked_locations:
|
||||
new_checks.append(itm_id)
|
||||
|
||||
for i in (0, 1, 2, 4):
|
||||
# Wily 4 does not have a boss check
|
||||
boss_id = 0x0009 + i
|
||||
if completed_stages[0] & (1 << i) != 0:
|
||||
if boss_id not in ctx.checked_locations:
|
||||
new_checks.append(boss_id)
|
||||
|
||||
if completed_stages[0] & 0x80 and 0x000F not in ctx.checked_locations:
|
||||
new_checks.append(0x000F)
|
||||
|
||||
if bar_state[0] == 0x80: # currently in stage
|
||||
if (prog_state[0] > 0x00 and current_stage[0] >= 8) or prog_state[0] == 0x00:
|
||||
# need to block the specific state of Break Man prog=0x12 stage=0x5
|
||||
# it doesn't clean the consumable table and he doesn't have any anyways
|
||||
for consumable in MM3_CONSUMABLE_TABLE[current_stage[0]]:
|
||||
consumable_info = MM3_CONSUMABLE_TABLE[current_stage[0]][consumable]
|
||||
if consumable not in ctx.checked_locations:
|
||||
is_checked = consumable_checks[consumable_info[0]] & (1 << consumable_info[1])
|
||||
if is_checked:
|
||||
new_checks.append(consumable)
|
||||
|
||||
for new_check_id in new_checks:
|
||||
ctx.locations_checked.add(new_check_id)
|
||||
location = ctx.location_names.lookup_in_game(new_check_id)
|
||||
nes_logger.info(
|
||||
f'New Check: {location} ({len(ctx.locations_checked)}/'
|
||||
f'{len(ctx.missing_locations) + len(ctx.checked_locations)})')
|
||||
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}])
|
||||
@@ -1,331 +0,0 @@
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
from . import names
|
||||
from zlib import crc32
|
||||
import struct
|
||||
import logging
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import MM3World
|
||||
from .rom import MM3ProcedurePatch
|
||||
|
||||
HTML_TO_NES: dict[str, int] = {
|
||||
'SNOW': 0x20,
|
||||
'LINEN': 0x36,
|
||||
'SEASHELL': 0x36,
|
||||
'AZURE': 0x3C,
|
||||
'LAVENDER': 0x33,
|
||||
'WHITE': 0x30,
|
||||
'BLACK': 0x0F,
|
||||
'GREY': 0x00,
|
||||
'GRAY': 0x00,
|
||||
'ROYALBLUE': 0x12,
|
||||
'BLUE': 0x11,
|
||||
'SKYBLUE': 0x21,
|
||||
'LIGHTBLUE': 0x31,
|
||||
'TURQUOISE': 0x2B,
|
||||
'CYAN': 0x2C,
|
||||
'AQUAMARINE': 0x3B,
|
||||
'DARKGREEN': 0x0A,
|
||||
'GREEN': 0x1A,
|
||||
'YELLOW': 0x28,
|
||||
'GOLD': 0x28,
|
||||
'WHEAT': 0x37,
|
||||
'TAN': 0x37,
|
||||
'CHOCOLATE': 0x07,
|
||||
'BROWN': 0x07,
|
||||
'SALMON': 0x26,
|
||||
'ORANGE': 0x27,
|
||||
'CORAL': 0x36,
|
||||
'TOMATO': 0x16,
|
||||
'RED': 0x16,
|
||||
'PINK': 0x25,
|
||||
'MAROON': 0x06,
|
||||
'MAGENTA': 0x24,
|
||||
'FUSCHIA': 0x24,
|
||||
'VIOLET': 0x24,
|
||||
'PLUM': 0x33,
|
||||
'PURPLE': 0x14,
|
||||
'THISTLE': 0x34,
|
||||
'DARKBLUE': 0x01,
|
||||
'SILVER': 0x10,
|
||||
'NAVY': 0x02,
|
||||
'TEAL': 0x1C,
|
||||
'OLIVE': 0x18,
|
||||
'LIME': 0x2A,
|
||||
'AQUA': 0x2C,
|
||||
# can add more as needed
|
||||
}
|
||||
|
||||
MM3_COLORS: dict[str, tuple[int, int]] = {
|
||||
names.gemini_laser: (0x30, 0x21),
|
||||
names.needle_cannon: (0x30, 0x17),
|
||||
names.hard_knuckle: (0x10, 0x01),
|
||||
names.magnet_missile: (0x10, 0x16),
|
||||
names.top_spin: (0x36, 0x00),
|
||||
names.search_snake: (0x30, 0x19),
|
||||
names.rush_coil: (0x30, 0x15),
|
||||
names.spark_shock: (0x30, 0x26),
|
||||
names.rush_marine: (0x30, 0x15),
|
||||
names.shadow_blade: (0x34, 0x14),
|
||||
names.rush_jet: (0x30, 0x15),
|
||||
names.needle_man_stage: (0x3C, 0x11),
|
||||
names.magnet_man_stage: (0x30, 0x15),
|
||||
names.gemini_man_stage: (0x30, 0x21),
|
||||
names.hard_man_stage: (0x10, 0xC),
|
||||
names.top_man_stage: (0x30, 0x26),
|
||||
names.snake_man_stage: (0x30, 0x29),
|
||||
names.spark_man_stage: (0x30, 0x26),
|
||||
names.shadow_man_stage: (0x30, 0x11),
|
||||
names.doc_needle_stage: (0x27, 0x15),
|
||||
names.doc_gemini_stage: (0x27, 0x15),
|
||||
names.doc_spark_stage: (0x27, 0x15),
|
||||
names.doc_shadow_stage: (0x27, 0x15),
|
||||
}
|
||||
|
||||
MM3_KNOWN_COLORS: dict[str, tuple[int, int]] = {
|
||||
**MM3_COLORS,
|
||||
# Metroid series
|
||||
"Varia Suit": (0x27, 0x16),
|
||||
"Gravity Suit": (0x14, 0x16),
|
||||
"Phazon Suit": (0x06, 0x1D),
|
||||
# Street Fighter, technically
|
||||
"Hadouken": (0x3C, 0x11),
|
||||
"Shoryuken": (0x38, 0x16),
|
||||
# X Series
|
||||
"Z-Saber": (0x20, 0x16),
|
||||
"Helmet Upgrade": (0x20, 0x01),
|
||||
"Body Upgrade": (0x20, 0x01),
|
||||
"Arms Upgrade": (0x20, 0x01),
|
||||
"Plasma Shot Upgrade": (0x20, 0x01),
|
||||
"Stock Charge Upgrade": (0x20, 0x01),
|
||||
"Legs Upgrade": (0x20, 0x01),
|
||||
# X1
|
||||
"Homing Torpedo": (0x3D, 0x37),
|
||||
"Chameleon Sting": (0x3B, 0x1A),
|
||||
"Rolling Shield": (0x3A, 0x25),
|
||||
"Fire Wave": (0x37, 0x26),
|
||||
"Storm Tornado": (0x34, 0x14),
|
||||
"Electric Spark": (0x3D, 0x28),
|
||||
"Boomerang Cutter": (0x3B, 0x2D),
|
||||
"Shotgun Ice": (0x28, 0x2C),
|
||||
# X2
|
||||
"Crystal Hunter": (0x33, 0x21),
|
||||
"Bubble Splash": (0x35, 0x28),
|
||||
"Spin Wheel": (0x34, 0x1B),
|
||||
"Silk Shot": (0x3B, 0x27),
|
||||
"Sonic Slicer": (0x27, 0x01),
|
||||
"Strike Chain": (0x30, 0x23),
|
||||
"Magnet Mine": (0x28, 0x2D),
|
||||
"Speed Burner": (0x31, 0x16),
|
||||
# X3
|
||||
"Acid Burst": (0x28, 0x2A),
|
||||
"Tornado Fang": (0x28, 0x2C),
|
||||
"Triad Thunder": (0x2B, 0x23),
|
||||
"Spinning Blade": (0x20, 0x16),
|
||||
"Ray Splasher": (0x28, 0x17),
|
||||
"Gravity Well": (0x38, 0x14),
|
||||
"Parasitic Bomb": (0x31, 0x28),
|
||||
"Frost Shield": (0x23, 0x2C),
|
||||
# X4
|
||||
"Lightning Web": (0x3D, 0x28),
|
||||
"Aiming Laser": (0x2C, 0x14),
|
||||
"Double Cyclone": (0x28, 0x1A),
|
||||
"Rising Fire": (0x20, 0x16),
|
||||
"Ground Hunter": (0x2C, 0x15),
|
||||
"Soul Body": (0x37, 0x27),
|
||||
"Twin Slasher": (0x28, 0x00),
|
||||
"Frost Tower": (0x3D, 0x2C),
|
||||
}
|
||||
|
||||
if "worlds.mm2" in sys.modules:
|
||||
# is this the proper way to do this? who knows!
|
||||
try:
|
||||
mm2 = sys.modules["worlds.mm2"]
|
||||
MM3_KNOWN_COLORS.update(mm2.color.MM2_COLORS)
|
||||
for item in MM3_COLORS:
|
||||
mm2.color.add_color_to_mm2(item, MM3_COLORS[item])
|
||||
except AttributeError:
|
||||
# pass through if an old MM2 is found
|
||||
pass
|
||||
|
||||
palette_pointers: dict[str, list[int]] = {
|
||||
"Mega Buster": [0x7C8A8, 0x4650],
|
||||
"Gemini Laser": [0x4654],
|
||||
"Needle Cannon": [0x4658],
|
||||
"Hard Knuckle": [0x465C],
|
||||
"Magnet Missile": [0x4660],
|
||||
"Top Spin": [0x4664],
|
||||
"Search Snake": [0x4668],
|
||||
"Rush Coil": [0x466C],
|
||||
"Spark Shock": [0x4670],
|
||||
"Rush Marine": [0x4674],
|
||||
"Shadow Blade": [0x4678],
|
||||
"Rush Jet": [0x467C],
|
||||
"Needle Man": [0x216C],
|
||||
"Magnet Man": [0x215C],
|
||||
"Gemini Man": [0x217C],
|
||||
"Hard Man": [0x2164],
|
||||
"Top Man": [0x2194],
|
||||
"Snake Man": [0x2174],
|
||||
"Spark Man": [0x2184],
|
||||
"Shadow Man": [0x218C],
|
||||
"Doc Robot": [0x20B8]
|
||||
}
|
||||
|
||||
|
||||
def add_color_to_mm3(name: str, color: tuple[int, int]) -> None:
|
||||
"""
|
||||
Add a color combo for Mega Man 3 to recognize as the color to display for a given item.
|
||||
For information on available colors: https://www.nesdev.org/wiki/PPU_palettes#2C02
|
||||
"""
|
||||
MM3_KNOWN_COLORS[name] = validate_colors(*color)
|
||||
|
||||
|
||||
def extrapolate_color(color: int) -> tuple[int, int]:
|
||||
if color > 0x1F:
|
||||
color_1 = color
|
||||
color_2 = color_1 - 0x10
|
||||
else:
|
||||
color_2 = color
|
||||
color_1 = color_2 + 0x10
|
||||
return color_1, color_2
|
||||
|
||||
|
||||
def validate_colors(color_1: int, color_2: int, allow_match: bool = False) -> tuple[int, int]:
|
||||
# Black should be reserved for outlines, a gray should suffice
|
||||
if color_1 in [0x0D, 0x0E, 0x0F, 0x1E, 0x2E, 0x3E, 0x1F, 0x2F, 0x3F]:
|
||||
color_1 = 0x10
|
||||
if color_2 in [0x0D, 0x0E, 0x0F, 0x1E, 0x2E, 0x3E, 0x1F, 0x2F, 0x3F]:
|
||||
color_2 = 0x10
|
||||
|
||||
# one final check, make sure we don't have two matching
|
||||
if not allow_match and color_1 == color_2:
|
||||
color_1 = 0x30 # color 1 to white works with about any paired color
|
||||
|
||||
return color_1, color_2
|
||||
|
||||
|
||||
def expand_colors(color_1: int, color_2: int) -> tuple[tuple[int, int, int], tuple[int, int, int]]:
|
||||
if color_2 >= 0x30:
|
||||
color_a = color_b = color_2
|
||||
else:
|
||||
color_a = color_2 + 0x10
|
||||
color_b = color_2
|
||||
|
||||
if color_1 < 0x10:
|
||||
color_c = color_1 + 0x10
|
||||
color_d = color_1
|
||||
color_e = color_1 + 0x20
|
||||
elif color_1 >= 0x30:
|
||||
color_c = color_1 - 0x10
|
||||
color_d = color_1 - 0x20
|
||||
color_e = color_1
|
||||
else:
|
||||
color_c = color_1
|
||||
color_d = color_1 - 0x10
|
||||
color_e = color_1 + 0x10
|
||||
|
||||
return (0x30, color_a, color_b), (color_d, color_e, color_c)
|
||||
|
||||
|
||||
def get_colors_for_item(name: str) -> tuple[tuple[int, int, int], tuple[int, int, int]]:
|
||||
if name in MM3_KNOWN_COLORS:
|
||||
return expand_colors(*MM3_KNOWN_COLORS[name])
|
||||
|
||||
check_colors = {color: color in name.upper().replace(" ", '') for color in HTML_TO_NES}
|
||||
colors = [color for color in check_colors if check_colors[color]]
|
||||
if colors:
|
||||
# we have at least one color pattern matched
|
||||
if len(colors) > 1:
|
||||
# we have at least 2
|
||||
color_1 = HTML_TO_NES[colors[0]]
|
||||
color_2 = HTML_TO_NES[colors[1]]
|
||||
else:
|
||||
color_1, color_2 = extrapolate_color(HTML_TO_NES[colors[0]])
|
||||
else:
|
||||
# generate hash
|
||||
crc_hash = crc32(name.encode('utf-8'))
|
||||
hash_color = struct.pack("I", crc_hash)
|
||||
color_1 = hash_color[0] % 0x3F
|
||||
color_2 = hash_color[1] % 0x3F
|
||||
|
||||
if color_1 < color_2:
|
||||
temp = color_1
|
||||
color_1 = color_2
|
||||
color_2 = temp
|
||||
|
||||
color_1, color_2 = validate_colors(color_1, color_2)
|
||||
|
||||
return expand_colors(color_1, color_2)
|
||||
|
||||
|
||||
def parse_color(colors: list[str]) -> tuple[int, int]:
|
||||
color_a = colors[0]
|
||||
if color_a.startswith("$"):
|
||||
color_1 = int(color_a[1:], 16)
|
||||
else:
|
||||
# assume it's in our list of colors
|
||||
color_1 = HTML_TO_NES[color_a.upper()]
|
||||
|
||||
if len(colors) == 1:
|
||||
color_1, color_2 = extrapolate_color(color_1)
|
||||
else:
|
||||
color_b = colors[1]
|
||||
if color_b.startswith("$"):
|
||||
color_2 = int(color_b[1:], 16)
|
||||
else:
|
||||
color_2 = HTML_TO_NES[color_b.upper()]
|
||||
return color_1, color_2
|
||||
|
||||
|
||||
def write_palette_shuffle(world: "MM3World", rom: "MM3ProcedurePatch") -> None:
|
||||
palette_shuffle: int | str = world.options.palette_shuffle.value
|
||||
palettes_to_write: dict[str, tuple[int, int]] = {}
|
||||
if isinstance(palette_shuffle, str):
|
||||
color_sets = palette_shuffle.split(";")
|
||||
if len(color_sets) == 1:
|
||||
palette_shuffle = world.options.palette_shuffle.option_none
|
||||
# singularity is more correct, but this is faster
|
||||
else:
|
||||
palette_shuffle = world.options.palette_shuffle.options[color_sets.pop()]
|
||||
for color_set in color_sets:
|
||||
if "-" in color_set:
|
||||
character, color = color_set.split("-")
|
||||
if character.title() not in palette_pointers:
|
||||
logging.warning(f"Player {world.player_name} "
|
||||
f"attempted to set color for unrecognized option {character}")
|
||||
colors = color.split("|")
|
||||
real_colors = validate_colors(*parse_color(colors), allow_match=True)
|
||||
palettes_to_write[character.title()] = real_colors
|
||||
else:
|
||||
# If color is provided with no character, assume singularity
|
||||
colors = color_set.split("|")
|
||||
real_colors = validate_colors(*parse_color(colors), allow_match=True)
|
||||
for character in palette_pointers:
|
||||
palettes_to_write[character] = real_colors
|
||||
# Now we handle the real values
|
||||
if palette_shuffle != 0:
|
||||
if palette_shuffle > 1:
|
||||
if palette_shuffle == 3:
|
||||
# singularity
|
||||
real_colors = validate_colors(world.random.randint(0, 0x3F), world.random.randint(0, 0x3F))
|
||||
for character in palette_pointers:
|
||||
if character not in palettes_to_write:
|
||||
palettes_to_write[character] = real_colors
|
||||
else:
|
||||
for character in palette_pointers:
|
||||
if character not in palettes_to_write:
|
||||
real_colors = validate_colors(world.random.randint(0, 0x3F), world.random.randint(0, 0x3F))
|
||||
palettes_to_write[character] = real_colors
|
||||
else:
|
||||
shuffled_colors = list(MM3_COLORS.values())[:-3] # only include one Doc Robot
|
||||
shuffled_colors.append((0x2C, 0x11)) # Mega Buster
|
||||
world.random.shuffle(shuffled_colors)
|
||||
for character in palette_pointers:
|
||||
if character not in palettes_to_write:
|
||||
palettes_to_write[character] = shuffled_colors.pop()
|
||||
|
||||
for character in palettes_to_write:
|
||||
for pointer in palette_pointers[character]:
|
||||
rom.write_bytes(pointer + 2, bytes(palettes_to_write[character]))
|
||||
Binary file not shown.
@@ -1,131 +0,0 @@
|
||||
# Mega Man 3
|
||||
|
||||
## Where is the options page?
|
||||
|
||||
The [player options page for this game](../player-options) contains all the options you need to configure and export a
|
||||
config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
|
||||
Weapons received from Robot Masters, access to each individual stage (including Doc Robot stages), and Items from Dr. Light are randomized
|
||||
into the multiworld. Access to the Wily Stages is locked behind clearing the 4 Doc Robot stages and defeating Break Man. The game is complete upon
|
||||
viewing the ending sequence after defeating Gamma.
|
||||
|
||||
## What Mega Man 3 items can appear in other players' worlds?
|
||||
- Robot Master weapons
|
||||
- Robot Master Access Codes (stage access)
|
||||
- Doc Robot Access Codes (stage access)
|
||||
- Rush Coil/Jet/Marine
|
||||
- 1-Ups
|
||||
- E-Tanks
|
||||
- Health Energy (L)
|
||||
- Weapon Energy (L)
|
||||
|
||||
## What is considered a location check in Mega Man 3?
|
||||
- The defeat of a Robot Master, Doc Robot, or Wily Boss
|
||||
- Receiving a weapon or Rush item from Dr. Light
|
||||
- Optionally, 1-Ups and E-Tanks present within stages
|
||||
- Optionally, Weapon and Health Energy pickups present within stages
|
||||
|
||||
## When the player receives an item, what happens?
|
||||
A sound effect will play based on the type of item received, and the effects of the item will be immediately applied,
|
||||
such as unlocking the use of a weapon mid-stage. If the effects of the item cannot be fully applied (such as receiving
|
||||
Health Energy while at full health), the remaining are withheld until they can be applied.
|
||||
|
||||
## How do I access the Doc Robot stages?
|
||||
By pressing Select on the Robot Master screen, the screen will transition between Robot Masters and
|
||||
Doc Robots.
|
||||
|
||||
## Useful Information
|
||||
* **NesHawk is the recommended core for this game!** Players using QuickNes (or QuickerNes) will experience graphical
|
||||
glitches while in Gemini Man's stage and fighting Gamma.
|
||||
* Pressing A+B+Start+Select while in a stage will take you to the Game Over screen, allowing you to leave the stage.
|
||||
Your E-Tanks will be preserved.
|
||||
* Your current progress through the Wily stages is saved to the multiworld, allowing you to return to the last stage you
|
||||
reached should you need to leave and enter a Robot Master stage. If you need to return to an earlier Wily stage, holding
|
||||
Select while entering Break Man's stage will take you to Wily 1.
|
||||
* When Random Weaknesses are enabled, Break Man's weakness will be changed from Mega Buster to one random weapon.
|
||||
|
||||
|
||||
## What is EnergyLink?
|
||||
EnergyLink is an energy storage supported by certain games that is shared across all worlds in a multiworld. In Mega Man
|
||||
3, when enabled, drops from enemies are not applied directly to Mega Man and are instead deposited into the EnergyLink.
|
||||
Half of the energy that would be gained is lost upon transfer to the EnergyLink.
|
||||
|
||||
Energy from the EnergyLink storage can be converted into health, weapon energy, and lives at different conversion rates.
|
||||
You can find out how much of each type you can pull using `/pool` in the client. Additionally, you can have it
|
||||
automatically pull from the EnergyLink storage to keep Mega Man healed using the `/autoheal` command in the client.
|
||||
Finally, you can use the `/request` command to request a certain type of energy from the storage.
|
||||
|
||||
## Plando Palettes
|
||||
The palette shuffle option supports specifying a specific palette for a given weapon/Robot Master. The format for doing
|
||||
so is `Character-Color1|Color2;Option`. Character is the individual that this should apply to, and can only be one of
|
||||
the following:
|
||||
- Mega Buster
|
||||
- Gemini Laser
|
||||
- Needle Cannon
|
||||
- Hard Knuckle
|
||||
- Magnet Missile
|
||||
- Top Spin
|
||||
- Search Snake
|
||||
- Spark Shot
|
||||
- Shadow Blade
|
||||
- Rush Coil
|
||||
- Rush Jet
|
||||
- Rush Marine
|
||||
- Needle Man
|
||||
- Magnet Man
|
||||
- Gemini Man
|
||||
- Hard Man
|
||||
- Top Man
|
||||
- Snake Man
|
||||
- Spark Man
|
||||
- Shadow Man
|
||||
- Doc Robot
|
||||
|
||||
Colors attempt to map a list of HTML-defined colors to what the NES can render. A full list of applicable colors can be
|
||||
found [here](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/mm2/Color.py#L11). Alternatively, colors can
|
||||
be supplied directly using `$xx` format. A full list of NES colors can be found [here](https://www.nesdev.org/wiki/PPU_palettes#2C02).
|
||||
|
||||
You can also pass only one color (such as `Mega Buster-Red`) and it will interpret a second color based off of the color
|
||||
given. Additionally, passing only colors (such as `Red|Blue`) and not any specific boss/weapon will apply that color to
|
||||
all weapons/bosses that did not have a prior color specified.
|
||||
|
||||
The option is the method to be used to set the palettes of the remaining bosses/weapons, and will not overwrite any
|
||||
plando placements.
|
||||
|
||||
## Plando Weaknesses
|
||||
Plando Weaknesses allows you to override the amount of damage a boss should take from a given weapon, ignoring prior
|
||||
weaknesses generated by strict/random weakness options. Formatting for this is as follows:
|
||||
```yaml
|
||||
plando_weakness:
|
||||
Needle Man:
|
||||
Top Spin: 0
|
||||
Hard Knuckle: 4
|
||||
```
|
||||
This would cause Air Man to take 4 damage from Hard Knuckle, and 0 from Top Spin.
|
||||
|
||||
Note: it is possible that plando weakness is not be respected should the plando create a situation in which the game
|
||||
becomes impossible to complete. In this situation, the damage would be boosted to the minimum required to defeat the
|
||||
Robot Master.
|
||||
|
||||
|
||||
## Unique Local Commands
|
||||
- `/pool` Only present with EnergyLink, prints the max amount of each type of request that could be fulfilled.
|
||||
- `/autoheal` Only present with EnergyLink, will automatically drain energy from the EnergyLink in order to
|
||||
restore Mega Man's health.
|
||||
- `/request <amount> <type>` Only present with EnergyLink, sends a request of a certain type of energy to be pulled from
|
||||
the EnergyLink. Types are as follows:
|
||||
- `HP` Health
|
||||
- `NE` Needle Cannon
|
||||
- `MA` Magnet Missile
|
||||
- `GE` Gemini Laser
|
||||
- `HA` Hard Knuckle
|
||||
- `TO` Top Spin
|
||||
- `SN` Search Snake
|
||||
- `SP` Spark Shot
|
||||
- `SH` Shadow Blade
|
||||
- `RC` Rush Coil
|
||||
- `RM` Rush Marine
|
||||
- `RJ` Rush Jet
|
||||
- `1U` Lives
|
||||
@@ -1,53 +0,0 @@
|
||||
# Mega Man 3 Setup Guide
|
||||
|
||||
## Required Software
|
||||
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
- An English Mega Man 3 ROM. Alternatively, the [Mega Man Legacy Collection](https://store.steampowered.com/app/363440/Mega_Man_Legacy_Collection/) on Steam.
|
||||
- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) 2.7 or later. Bizhawk 2.10
|
||||
|
||||
### Configuring Bizhawk
|
||||
|
||||
Once you have installed BizHawk, open `EmuHawk.exe` and change the following settings:
|
||||
|
||||
- If you're using BizHawk 2.7 or 2.8, go to `Config > Customize`. On the Advanced tab, switch the Lua Core from
|
||||
`NLua+KopiLua` to `Lua+LuaInterface`, then restart EmuHawk. (If you're using BizHawk 2.9, you can skip this step.)
|
||||
- Under `Config > Customize`, check the "Run in background" option to prevent disconnecting from the client while you're
|
||||
tabbed out of EmuHawk.
|
||||
- Open a `.nes` file in EmuHawk and go to `Config > Controllers…` to configure your inputs. If you can't click
|
||||
`Controllers…`, load any `.nes` ROM first.
|
||||
- Consider clearing keybinds in `Config > Hotkeys…` if you don't intend to use them. Select the keybind and press Esc to
|
||||
clear it.
|
||||
|
||||
## Generating and Patching a Game
|
||||
|
||||
1. Create your options file (YAML). You can make one on the
|
||||
[Mega Man 3 options page](../../../games/Mega%20Man%203/player-options).
|
||||
2. Follow the general Archipelago instructions for [generating a game](../../Archipelago/setup/en#generating-a-game).
|
||||
This will generate an output file for you. Your patch file will have the `.apmm3` file extension.
|
||||
3. Open `ArchipelagoLauncher.exe`
|
||||
4. Select "Open Patch" on the left side and select your patch file.
|
||||
5. If this is your first time patching, you will be prompted to locate your vanilla ROM. If you are using the Legacy
|
||||
Collection, provide `Proteus.exe` in place of your rom.
|
||||
6. A patched `.nes` file will be created in the same place as the patch file.
|
||||
7. On your first time opening a patch with BizHawk Client, you will also be asked to locate `EmuHawk.exe` in your
|
||||
BizHawk install.
|
||||
|
||||
## Connecting to a Server
|
||||
|
||||
By default, opening a patch file will do steps 1-5 below for you automatically. Even so, keep them in your memory just
|
||||
in case you have to close and reopen a window mid-game for some reason.
|
||||
|
||||
1. Mega Man 3 uses Archipelago's BizHawk Client. If the client isn't still open from when you patched your game,
|
||||
you can re-open it from the launcher.
|
||||
2. Ensure EmuHawk is running the patched ROM.
|
||||
3. In EmuHawk, go to `Tools > Lua Console`. This window must stay open while playing.
|
||||
4. In the Lua Console window, go to `Script > Open Script…`.
|
||||
5. Navigate to your Archipelago install folder and open `data/lua/connector_bizhawk_generic.lua`.
|
||||
6. The emulator and client will eventually connect to each other. The BizHawk Client window should indicate that it
|
||||
connected and recognized Mega Man 3.
|
||||
7. To connect the client to the server, enter your room's address and port (e.g. `archipelago.gg:38281`) into the
|
||||
top text field of the client and click Connect.
|
||||
|
||||
You should now be able to receive and send items. You'll need to do these steps every time you want to reconnect. It is
|
||||
perfectly safe to make progress offline; everything will re-sync when you reconnect.
|
||||
@@ -1,80 +0,0 @@
|
||||
from BaseClasses import Item
|
||||
from typing import NamedTuple
|
||||
from .names import (needle_cannon, magnet_missile, gemini_laser, hard_knuckle, top_spin, search_snake, spark_shock,
|
||||
shadow_blade, rush_coil, rush_marine, rush_jet, needle_man_stage, magnet_man_stage,
|
||||
gemini_man_stage, hard_man_stage, top_man_stage, snake_man_stage, spark_man_stage, shadow_man_stage,
|
||||
doc_needle_stage, doc_gemini_stage, doc_spark_stage, doc_shadow_stage, e_tank, weapon_energy,
|
||||
health_energy, one_up)
|
||||
|
||||
|
||||
class ItemData(NamedTuple):
|
||||
code: int
|
||||
progression: bool
|
||||
useful: bool = False # primarily use this for incredibly useful items of their class, like Metal Blade
|
||||
skip_balancing: bool = False
|
||||
|
||||
|
||||
class MM3Item(Item):
|
||||
game = "Mega Man 3"
|
||||
|
||||
|
||||
robot_master_weapon_table = {
|
||||
needle_cannon: ItemData(0x0001, True),
|
||||
magnet_missile: ItemData(0x0002, True, True),
|
||||
gemini_laser: ItemData(0x0003, True),
|
||||
hard_knuckle: ItemData(0x0004, True),
|
||||
top_spin: ItemData(0x0005, True, True),
|
||||
search_snake: ItemData(0x0006, True),
|
||||
spark_shock: ItemData(0x0007, True),
|
||||
shadow_blade: ItemData(0x0008, True, True),
|
||||
}
|
||||
|
||||
stage_access_table = {
|
||||
needle_man_stage: ItemData(0x0101, True),
|
||||
magnet_man_stage: ItemData(0x0102, True),
|
||||
gemini_man_stage: ItemData(0x0103, True),
|
||||
hard_man_stage: ItemData(0x0104, True),
|
||||
top_man_stage: ItemData(0x0105, True),
|
||||
snake_man_stage: ItemData(0x0106, True),
|
||||
spark_man_stage: ItemData(0x0107, True),
|
||||
shadow_man_stage: ItemData(0x0108, True),
|
||||
doc_needle_stage: ItemData(0x0111, True, True),
|
||||
doc_gemini_stage: ItemData(0x0113, True, True),
|
||||
doc_spark_stage: ItemData(0x0117, True, True),
|
||||
doc_shadow_stage: ItemData(0x0118, True, True),
|
||||
}
|
||||
|
||||
rush_item_table = {
|
||||
rush_coil: ItemData(0x0011, True, True),
|
||||
rush_marine: ItemData(0x0012, True),
|
||||
rush_jet: ItemData(0x0013, True, True),
|
||||
}
|
||||
|
||||
filler_item_table = {
|
||||
one_up: ItemData(0x0020, False),
|
||||
weapon_energy: ItemData(0x0021, False),
|
||||
health_energy: ItemData(0x0022, False),
|
||||
e_tank: ItemData(0x0023, False, True),
|
||||
}
|
||||
|
||||
filler_item_weights = {
|
||||
one_up: 1,
|
||||
weapon_energy: 4,
|
||||
health_energy: 1,
|
||||
e_tank: 2,
|
||||
}
|
||||
|
||||
item_table = {
|
||||
**robot_master_weapon_table,
|
||||
**stage_access_table,
|
||||
**rush_item_table,
|
||||
**filler_item_table,
|
||||
}
|
||||
|
||||
item_names = {
|
||||
"Weapons": {name for name in robot_master_weapon_table.keys()},
|
||||
"Stages": {name for name in stage_access_table.keys()},
|
||||
"Rush": {name for name in rush_item_table.keys()}
|
||||
}
|
||||
|
||||
lookup_item_to_id: dict[str, int] = {item_name: data.code for item_name, data in item_table.items() if data.code}
|
||||
@@ -1,312 +0,0 @@
|
||||
from BaseClasses import Location, Region
|
||||
from typing import NamedTuple
|
||||
from . import names
|
||||
|
||||
|
||||
class MM3Location(Location):
|
||||
game = "Mega Man 3"
|
||||
|
||||
|
||||
class MM3Region(Region):
|
||||
game = "Mega Man 3"
|
||||
|
||||
|
||||
class LocationData(NamedTuple):
|
||||
location_id: int | None
|
||||
energy: bool = False
|
||||
oneup_tank: bool = False
|
||||
|
||||
|
||||
class RegionData(NamedTuple):
|
||||
locations: dict[str, LocationData]
|
||||
required_items: list[str]
|
||||
parent: str = ""
|
||||
|
||||
mm3_regions: dict[str, RegionData] = {
|
||||
"Needle Man Stage": RegionData({
|
||||
names.needle_man: LocationData(0x0001),
|
||||
names.get_needle_cannon: LocationData(0x0101),
|
||||
names.get_rush_jet: LocationData(0x0111),
|
||||
names.needle_man_c1: LocationData(0x0200, energy=True),
|
||||
names.needle_man_c2: LocationData(0x0201, oneup_tank=True),
|
||||
}, [names.needle_man_stage]),
|
||||
|
||||
"Magnet Man Stage": RegionData({
|
||||
names.magnet_man: LocationData(0x0002),
|
||||
names.get_magnet_missile: LocationData(0x0102),
|
||||
names.magnet_man_c1: LocationData(0x0202, energy=True),
|
||||
names.magnet_man_c2: LocationData(0x0203, energy=True),
|
||||
names.magnet_man_c3: LocationData(0x0204, energy=True),
|
||||
names.magnet_man_c4: LocationData(0x0205, energy=True),
|
||||
names.magnet_man_c5: LocationData(0x0206, energy=True),
|
||||
names.magnet_man_c6: LocationData(0x0207, energy=True),
|
||||
names.magnet_man_c7: LocationData(0x0208, energy=True),
|
||||
names.magnet_man_c8: LocationData(0x0209, energy=True),
|
||||
}, [names.magnet_man_stage]),
|
||||
|
||||
"Gemini Man Stage": RegionData({
|
||||
names.gemini_man: LocationData(0x0003),
|
||||
names.get_gemini_laser: LocationData(0x0103),
|
||||
names.gemini_man_c1: LocationData(0x020A, oneup_tank=True),
|
||||
names.gemini_man_c2: LocationData(0x020B, energy=True),
|
||||
names.gemini_man_c3: LocationData(0x020C, oneup_tank=True),
|
||||
names.gemini_man_c4: LocationData(0x020D, energy=True),
|
||||
names.gemini_man_c5: LocationData(0x020E, energy=True),
|
||||
names.gemini_man_c6: LocationData(0x020F, oneup_tank=True),
|
||||
names.gemini_man_c7: LocationData(0x0210, oneup_tank=True),
|
||||
names.gemini_man_c8: LocationData(0x0211, energy=True),
|
||||
names.gemini_man_c9: LocationData(0x0212, energy=True),
|
||||
names.gemini_man_c10: LocationData(0x0213, oneup_tank=True),
|
||||
}, [names.gemini_man_stage]),
|
||||
|
||||
"Hard Man Stage": RegionData({
|
||||
names.hard_man: LocationData(0x0004),
|
||||
names.get_hard_knuckle: LocationData(0x0104),
|
||||
names.hard_man_c1: LocationData(0x0214, energy=True),
|
||||
names.hard_man_c2: LocationData(0x0215, energy=True),
|
||||
names.hard_man_c3: LocationData(0x0216, oneup_tank=True),
|
||||
names.hard_man_c4: LocationData(0x0217, energy=True),
|
||||
names.hard_man_c5: LocationData(0x0218, energy=True),
|
||||
names.hard_man_c6: LocationData(0x0219, energy=True),
|
||||
names.hard_man_c7: LocationData(0x021A, energy=True),
|
||||
}, [names.hard_man_stage]),
|
||||
|
||||
"Top Man Stage": RegionData({
|
||||
names.top_man: LocationData(0x0005),
|
||||
names.get_top_spin: LocationData(0x0105),
|
||||
names.top_man_c1: LocationData(0x021B, energy=True),
|
||||
names.top_man_c2: LocationData(0x021C, energy=True),
|
||||
names.top_man_c3: LocationData(0x021D, energy=True),
|
||||
names.top_man_c4: LocationData(0x021E, energy=True),
|
||||
names.top_man_c5: LocationData(0x021F, energy=True),
|
||||
names.top_man_c6: LocationData(0x0220, oneup_tank=True),
|
||||
names.top_man_c7: LocationData(0x0221, energy=True),
|
||||
names.top_man_c8: LocationData(0x0222, energy=True),
|
||||
}, [names.top_man_stage]),
|
||||
|
||||
"Snake Man Stage": RegionData({
|
||||
names.snake_man: LocationData(0x0006),
|
||||
names.get_search_snake: LocationData(0x0106),
|
||||
names.snake_man_c1: LocationData(0x0223, energy=True),
|
||||
names.snake_man_c2: LocationData(0x0224, energy=True),
|
||||
names.snake_man_c3: LocationData(0x0225, oneup_tank=True),
|
||||
names.snake_man_c4: LocationData(0x0226, oneup_tank=True),
|
||||
names.snake_man_c5: LocationData(0x0227, energy=True),
|
||||
}, [names.snake_man_stage]),
|
||||
|
||||
"Spark Man Stage": RegionData({
|
||||
names.spark_man: LocationData(0x0007),
|
||||
names.get_spark_shock: LocationData(0x0107),
|
||||
names.spark_man_c1: LocationData(0x0228, energy=True),
|
||||
names.spark_man_c2: LocationData(0x0229, energy=True),
|
||||
names.spark_man_c3: LocationData(0x022A, energy=True),
|
||||
names.spark_man_c4: LocationData(0x022B, energy=True),
|
||||
names.spark_man_c5: LocationData(0x022C, energy=True),
|
||||
names.spark_man_c6: LocationData(0x022D, energy=True),
|
||||
}, [names.spark_man_stage]),
|
||||
|
||||
"Shadow Man Stage": RegionData({
|
||||
names.shadow_man: LocationData(0x0008),
|
||||
names.get_shadow_blade: LocationData(0x0108),
|
||||
names.get_rush_marine: LocationData(0x0112),
|
||||
names.shadow_man_c1: LocationData(0x022E, energy=True),
|
||||
names.shadow_man_c2: LocationData(0x022F, energy=True),
|
||||
names.shadow_man_c3: LocationData(0x0230, energy=True),
|
||||
names.shadow_man_c4: LocationData(0x0231, energy=True),
|
||||
}, [names.shadow_man_stage]),
|
||||
|
||||
"Doc Robot (Needle) - Air": RegionData({
|
||||
names.doc_air: LocationData(0x0010),
|
||||
names.doc_needle_c1: LocationData(0x0232, energy=True),
|
||||
names.doc_needle_c2: LocationData(0x0233, oneup_tank=True),
|
||||
names.doc_needle_c3: LocationData(0x0234, oneup_tank=True),
|
||||
}, [names.doc_needle_stage]),
|
||||
|
||||
"Doc Robot (Needle) - Crash": RegionData({
|
||||
names.doc_crash: LocationData(0x0011),
|
||||
names.doc_needle: LocationData(None),
|
||||
names.doc_needle_c4: LocationData(0x0235, energy=True),
|
||||
names.doc_needle_c5: LocationData(0x0236, energy=True),
|
||||
names.doc_needle_c6: LocationData(0x0237, energy=True),
|
||||
names.doc_needle_c7: LocationData(0x0238, energy=True),
|
||||
names.doc_needle_c8: LocationData(0x0239, energy=True),
|
||||
names.doc_needle_c9: LocationData(0x023A, energy=True),
|
||||
names.doc_needle_c10: LocationData(0x023B, energy=True),
|
||||
names.doc_needle_c11: LocationData(0x023C, energy=True),
|
||||
}, [], parent="Doc Robot (Needle) - Air"),
|
||||
|
||||
"Doc Robot (Gemini) - Flash": RegionData({
|
||||
names.doc_flash: LocationData(0x0012),
|
||||
names.doc_gemini_c1: LocationData(0x023D, oneup_tank=True),
|
||||
names.doc_gemini_c2: LocationData(0x023E, oneup_tank=True),
|
||||
}, [names.doc_gemini_stage]),
|
||||
|
||||
"Doc Robot (Gemini) - Bubble": RegionData({
|
||||
names.doc_bubble: LocationData(0x0013),
|
||||
names.doc_gemini: LocationData(None),
|
||||
names.doc_gemini_c3: LocationData(0x023F, energy=True),
|
||||
names.doc_gemini_c4: LocationData(0x0240, energy=True),
|
||||
}, [], parent="Doc Robot (Gemini) - Flash"),
|
||||
|
||||
"Doc Robot (Shadow) - Wood": RegionData({
|
||||
names.doc_wood: LocationData(0x0014),
|
||||
}, [names.doc_shadow_stage]),
|
||||
|
||||
"Doc Robot (Shadow) - Heat": RegionData({
|
||||
names.doc_heat: LocationData(0x0015),
|
||||
names.doc_shadow: LocationData(None),
|
||||
names.doc_shadow_c1: LocationData(0x0243, energy=True),
|
||||
names.doc_shadow_c2: LocationData(0x0244, energy=True),
|
||||
names.doc_shadow_c3: LocationData(0x0245, energy=True),
|
||||
names.doc_shadow_c4: LocationData(0x0246, energy=True),
|
||||
names.doc_shadow_c5: LocationData(0x0247, energy=True),
|
||||
}, [], parent="Doc Robot (Shadow) - Wood"),
|
||||
|
||||
"Doc Robot (Spark) - Metal": RegionData({
|
||||
names.doc_metal: LocationData(0x0016),
|
||||
names.doc_spark_c1: LocationData(0x0241, energy=True),
|
||||
}, [names.doc_spark_stage]),
|
||||
|
||||
"Doc Robot (Spark) - Quick": RegionData({
|
||||
names.doc_quick: LocationData(0x0017),
|
||||
names.doc_spark: LocationData(None),
|
||||
names.doc_spark_c2: LocationData(0x0242, energy=True),
|
||||
}, [], parent="Doc Robot (Spark) - Metal"),
|
||||
|
||||
"Break Man": RegionData({
|
||||
names.break_man: LocationData(0x000F),
|
||||
names.break_stage: LocationData(None),
|
||||
}, [names.doc_needle, names.doc_gemini, names.doc_spark, names.doc_shadow]),
|
||||
|
||||
"Wily Stage 1": RegionData({
|
||||
names.wily_1_boss: LocationData(0x0009),
|
||||
names.wily_stage_1: LocationData(None),
|
||||
names.wily_1_c1: LocationData(0x0248, oneup_tank=True),
|
||||
names.wily_1_c2: LocationData(0x0249, oneup_tank=True),
|
||||
names.wily_1_c3: LocationData(0x024A, energy=True),
|
||||
names.wily_1_c4: LocationData(0x024B, oneup_tank=True),
|
||||
names.wily_1_c5: LocationData(0x024C, energy=True),
|
||||
names.wily_1_c6: LocationData(0x024D, energy=True),
|
||||
names.wily_1_c7: LocationData(0x024E, energy=True),
|
||||
names.wily_1_c8: LocationData(0x024F, oneup_tank=True),
|
||||
names.wily_1_c9: LocationData(0x0250, energy=True),
|
||||
names.wily_1_c10: LocationData(0x0251, energy=True),
|
||||
names.wily_1_c11: LocationData(0x0252, energy=True),
|
||||
names.wily_1_c12: LocationData(0x0253, energy=True),
|
||||
}, [names.break_stage], parent="Break Man"),
|
||||
|
||||
"Wily Stage 2": RegionData({
|
||||
names.wily_2_boss: LocationData(0x000A),
|
||||
names.wily_stage_2: LocationData(None),
|
||||
names.wily_2_c1: LocationData(0x0254, energy=True),
|
||||
names.wily_2_c2: LocationData(0x0255, energy=True),
|
||||
names.wily_2_c3: LocationData(0x0256, oneup_tank=True),
|
||||
names.wily_2_c4: LocationData(0x0257, energy=True),
|
||||
names.wily_2_c5: LocationData(0x0258, energy=True),
|
||||
names.wily_2_c6: LocationData(0x0259, energy=True),
|
||||
names.wily_2_c7: LocationData(0x025A, energy=True),
|
||||
names.wily_2_c8: LocationData(0x025B, energy=True),
|
||||
names.wily_2_c9: LocationData(0x025C, oneup_tank=True),
|
||||
names.wily_2_c10: LocationData(0x025D, energy=True),
|
||||
names.wily_2_c11: LocationData(0x025E, oneup_tank=True),
|
||||
names.wily_2_c12: LocationData(0x025F, energy=True),
|
||||
names.wily_2_c13: LocationData(0x0260, energy=True),
|
||||
}, [names.wily_stage_1], parent="Wily Stage 1"),
|
||||
|
||||
"Wily Stage 3": RegionData({
|
||||
names.wily_3_boss: LocationData(0x000B),
|
||||
names.wily_stage_3: LocationData(None),
|
||||
names.wily_3_c1: LocationData(0x0261, energy=True),
|
||||
names.wily_3_c2: LocationData(0x0262, energy=True),
|
||||
names.wily_3_c3: LocationData(0x0263, oneup_tank=True),
|
||||
names.wily_3_c4: LocationData(0x0264, oneup_tank=True),
|
||||
names.wily_3_c5: LocationData(0x0265, energy=True),
|
||||
names.wily_3_c6: LocationData(0x0266, energy=True),
|
||||
names.wily_3_c7: LocationData(0x0267, energy=True),
|
||||
names.wily_3_c8: LocationData(0x0268, energy=True),
|
||||
names.wily_3_c9: LocationData(0x0269, energy=True),
|
||||
names.wily_3_c10: LocationData(0x026A, oneup_tank=True),
|
||||
names.wily_3_c11: LocationData(0x026B, oneup_tank=True)
|
||||
}, [names.wily_stage_2], parent="Wily Stage 2"),
|
||||
|
||||
"Wily Stage 4": RegionData({
|
||||
names.wily_stage_4: LocationData(None),
|
||||
names.wily_4_c1: LocationData(0x026C, energy=True),
|
||||
names.wily_4_c2: LocationData(0x026D, energy=True),
|
||||
names.wily_4_c3: LocationData(0x026E, energy=True),
|
||||
names.wily_4_c4: LocationData(0x026F, energy=True),
|
||||
names.wily_4_c5: LocationData(0x0270, energy=True),
|
||||
names.wily_4_c6: LocationData(0x0271, energy=True),
|
||||
names.wily_4_c7: LocationData(0x0272, energy=True),
|
||||
names.wily_4_c8: LocationData(0x0273, energy=True),
|
||||
names.wily_4_c9: LocationData(0x0274, energy=True),
|
||||
names.wily_4_c10: LocationData(0x0275, oneup_tank=True),
|
||||
names.wily_4_c11: LocationData(0x0276, energy=True),
|
||||
names.wily_4_c12: LocationData(0x0277, oneup_tank=True),
|
||||
names.wily_4_c13: LocationData(0x0278, energy=True),
|
||||
names.wily_4_c14: LocationData(0x0279, energy=True),
|
||||
names.wily_4_c15: LocationData(0x027A, energy=True),
|
||||
names.wily_4_c16: LocationData(0x027B, energy=True),
|
||||
names.wily_4_c17: LocationData(0x027C, energy=True),
|
||||
names.wily_4_c18: LocationData(0x027D, energy=True),
|
||||
names.wily_4_c19: LocationData(0x027E, energy=True),
|
||||
names.wily_4_c20: LocationData(0x027F, energy=True),
|
||||
}, [names.wily_stage_3], parent="Wily Stage 3"),
|
||||
|
||||
"Wily Stage 5": RegionData({
|
||||
names.wily_5_boss: LocationData(0x000D),
|
||||
names.wily_stage_5: LocationData(None),
|
||||
names.wily_5_c1: LocationData(0x0280, energy=True),
|
||||
names.wily_5_c2: LocationData(0x0281, energy=True),
|
||||
names.wily_5_c3: LocationData(0x0282, oneup_tank=True),
|
||||
names.wily_5_c4: LocationData(0x0283, oneup_tank=True),
|
||||
}, [names.wily_stage_4], parent="Wily Stage 4"),
|
||||
|
||||
"Wily Stage 6": RegionData({
|
||||
names.gamma: LocationData(None),
|
||||
names.wily_6_c1: LocationData(0x0284, oneup_tank=True),
|
||||
names.wily_6_c2: LocationData(0x0285, oneup_tank=True),
|
||||
names.wily_6_c3: LocationData(0x0286, energy=True),
|
||||
names.wily_6_c4: LocationData(0x0287, energy=True),
|
||||
names.wily_6_c5: LocationData(0x0288, oneup_tank=True),
|
||||
names.wily_6_c6: LocationData(0x0289, oneup_tank=True),
|
||||
names.wily_6_c7: LocationData(0x028A, energy=True),
|
||||
}, [names.wily_stage_5], parent="Wily Stage 5"),
|
||||
}
|
||||
|
||||
|
||||
def get_boss_locations(region: str) -> list[str]:
|
||||
return [location for location, data in mm3_regions[region].locations.items()
|
||||
if not data.energy and not data.oneup_tank]
|
||||
|
||||
|
||||
def get_energy_locations(region: str) -> list[str]:
|
||||
return [location for location, data in mm3_regions[region].locations.items() if data.energy]
|
||||
|
||||
|
||||
def get_oneup_locations(region: str) -> list[str]:
|
||||
return [location for location, data in mm3_regions[region].locations.items() if data.oneup_tank]
|
||||
|
||||
|
||||
location_table: dict[str, int | None] = {
|
||||
location: data.location_id for region in mm3_regions.values() for location, data in region.locations.items()
|
||||
}
|
||||
|
||||
|
||||
location_groups = {
|
||||
"Get Equipped": {
|
||||
names.get_needle_cannon,
|
||||
names.get_magnet_missile,
|
||||
names.get_gemini_laser,
|
||||
names.get_hard_knuckle,
|
||||
names.get_top_spin,
|
||||
names.get_search_snake,
|
||||
names.get_spark_shock,
|
||||
names.get_shadow_blade,
|
||||
names.get_rush_marine,
|
||||
names.get_rush_jet,
|
||||
},
|
||||
**{name: {location for location, data in region.locations.items() if data.location_id} for name, region in mm3_regions.items()}
|
||||
}
|
||||
|
||||
lookup_location_to_id: dict[str, int] = {location: idx for location, idx in location_table.items() if idx is not None}
|
||||
@@ -1,221 +0,0 @@
|
||||
# Robot Master Weapons
|
||||
gemini_laser = "Gemini Laser"
|
||||
needle_cannon = "Needle Cannon"
|
||||
hard_knuckle = "Hard Knuckle"
|
||||
magnet_missile = "Magnet Missile"
|
||||
top_spin = "Top Spin"
|
||||
search_snake = "Search Snake"
|
||||
spark_shock = "Spark Shock"
|
||||
shadow_blade = "Shadow Blade"
|
||||
|
||||
# Rush
|
||||
rush_coil = "Rush Coil"
|
||||
rush_jet = "Rush Jet"
|
||||
rush_marine = "Rush Marine"
|
||||
|
||||
# Access Codes
|
||||
needle_man_stage = "Needle Man Access Codes"
|
||||
magnet_man_stage = "Magnet Man Access Codes"
|
||||
gemini_man_stage = "Gemini Man Access Codes"
|
||||
hard_man_stage = "Hard Man Access Codes"
|
||||
top_man_stage = "Top Man Access Codes"
|
||||
snake_man_stage = "Snake Man Access Codes"
|
||||
spark_man_stage = "Spark Man Access Codes"
|
||||
shadow_man_stage = "Shadow Man Access Codes"
|
||||
doc_needle_stage = "Doc Robot (Needle) Access Codes"
|
||||
doc_gemini_stage = "Doc Robot (Gemini) Access Codes"
|
||||
doc_spark_stage = "Doc Robot (Spark) Access Codes"
|
||||
doc_shadow_stage = "Doc Robot (Shadow) Access Codes"
|
||||
|
||||
# Misc. Items
|
||||
one_up = "1-Up"
|
||||
weapon_energy = "Weapon Energy (L)"
|
||||
health_energy = "Health Energy (L)"
|
||||
e_tank = "E-Tank"
|
||||
|
||||
needle_man = "Needle Man - Defeated"
|
||||
magnet_man = "Magnet Man - Defeated"
|
||||
gemini_man = "Gemini Man - Defeated"
|
||||
hard_man = "Hard Man - Defeated"
|
||||
top_man = "Top Man - Defeated"
|
||||
snake_man = "Snake Man - Defeated"
|
||||
spark_man = "Spark Man - Defeated"
|
||||
shadow_man = "Shadow Man - Defeated"
|
||||
doc_air = "Doc Robot (Air) - Defeated"
|
||||
doc_crash = "Doc Robot (Crash) - Defeated"
|
||||
doc_flash = "Doc Robot (Flash) - Defeated"
|
||||
doc_bubble = "Doc Robot (Bubble) - Defeated"
|
||||
doc_wood = "Doc Robot (Wood) - Defeated"
|
||||
doc_heat = "Doc Robot (Heat) - Defeated"
|
||||
doc_metal = "Doc Robot (Metal) - Defeated"
|
||||
doc_quick = "Doc Robot (Quick) - Defeated"
|
||||
break_man = "Break Man - Defeated"
|
||||
wily_1_boss = "Kamegoro Maker - Defeated"
|
||||
wily_2_boss = "Yellow Devil MK-II - Defeated"
|
||||
wily_3_boss = "Holograph Mega Man - Defeated"
|
||||
wily_5_boss = "Wily Machine 3 - Defeated"
|
||||
gamma = "Gamma - Defeated"
|
||||
|
||||
get_gemini_laser = "Gemini Laser - Received"
|
||||
get_needle_cannon = "Needle Cannon - Received"
|
||||
get_hard_knuckle = "Hard Knuckle - Received"
|
||||
get_magnet_missile = "Magnet Missile - Received"
|
||||
get_top_spin = "Top Spin - Received"
|
||||
get_search_snake = "Search Snake - Received"
|
||||
get_spark_shock = "Spark Shock - Received"
|
||||
get_shadow_blade = "Shadow Blade - Received"
|
||||
get_rush_jet = "Rush Jet - Received"
|
||||
get_rush_marine = "Rush Marine - Received"
|
||||
|
||||
# Wily Stage Event Items
|
||||
doc_needle = "Doc Robot (Needle) - Completed"
|
||||
doc_gemini = "Doc Robot (Gemini) - Completed"
|
||||
doc_spark = "Doc Robot (Spark) - Completed"
|
||||
doc_shadow = "Doc Robot (Shadow) - Completed"
|
||||
break_stage = "Break Man"
|
||||
wily_stage_1 = "Wily Stage 1 - Completed"
|
||||
wily_stage_2 = "Wily Stage 2 - Completed"
|
||||
wily_stage_3 = "Wily Stage 3 - Completed"
|
||||
wily_stage_4 = "Wily Stage 4 - Completed"
|
||||
wily_stage_5 = "Wily Stage 5 - Completed"
|
||||
|
||||
# Consumable Locations
|
||||
needle_man_c1 = "Needle Man Stage - Weapon Energy 1"
|
||||
needle_man_c2 = "Needle Man Stage - E-Tank"
|
||||
magnet_man_c1 = "Magnet Man Stage - Health Energy 1"
|
||||
magnet_man_c2 = "Magnet Man Stage - Health Energy 2"
|
||||
magnet_man_c3 = "Magnet Man Stage - Health Energy 3"
|
||||
magnet_man_c4 = "Magnet Man Stage - Health Energy 4"
|
||||
magnet_man_c5 = "Magnet Man Stage - Weapon Energy 1"
|
||||
magnet_man_c6 = "Magnet Man Stage - Weapon Energy 2"
|
||||
magnet_man_c7 = "Magnet Man Stage - Weapon Energy 3"
|
||||
magnet_man_c8 = "Magnet Man Stage - Health Energy 5"
|
||||
gemini_man_c1 = "Gemini Man Stage - 1-Up 1"
|
||||
gemini_man_c2 = "Gemini Man Stage - Health Energy 1"
|
||||
gemini_man_c3 = "Gemini Man Stage - Mystery Tank"
|
||||
gemini_man_c4 = "Gemini Man Stage - Weapon Energy 1"
|
||||
gemini_man_c5 = "Gemini Man Stage - Health Energy 2"
|
||||
gemini_man_c6 = "Gemini Man Stage - 1-Up 2"
|
||||
gemini_man_c7 = "Gemini Man Stage - E-Tank 1"
|
||||
gemini_man_c8 = "Gemini Man Stage - Weapon Energy 2"
|
||||
gemini_man_c9 = "Gemini Man Stage - Weapon Energy 3"
|
||||
gemini_man_c10 = "Gemini Man Stage - E-Tank 2"
|
||||
hard_man_c1 = "Hard Man Stage - Health Energy 1"
|
||||
hard_man_c2 = "Hard Man Stage - Health Energy 2"
|
||||
hard_man_c3 = "Hard Man Stage - E-Tank"
|
||||
hard_man_c4 = "Hard Man Stage - Health Energy 3"
|
||||
hard_man_c5 = "Hard Man Stage - Health Energy 4"
|
||||
hard_man_c6 = "Hard Man Stage - Health Energy 5"
|
||||
hard_man_c7 = "Hard Man Stage - Health Energy 6"
|
||||
top_man_c1 = "Top Man Stage - Health Energy 1"
|
||||
top_man_c2 = "Top Man Stage - Health Energy 2"
|
||||
top_man_c3 = "Top Man Stage - Health Energy 3"
|
||||
top_man_c4 = "Top Man Stage - Health Energy 4"
|
||||
top_man_c5 = "Top Man Stage - Weapon Energy 1"
|
||||
top_man_c6 = "Top Man Stage - 1-Up"
|
||||
top_man_c7 = "Top Man Stage - Health Energy 5"
|
||||
top_man_c8 = "Top Man Stage - Health Energy 6"
|
||||
snake_man_c1 = "Snake Man Stage - Health Energy 1"
|
||||
snake_man_c2 = "Snake Man Stage - Health Energy 2"
|
||||
snake_man_c3 = "Snake Man Stage - Mystery Tank 1"
|
||||
snake_man_c4 = "Snake Man Stage - Mystery Tank 2"
|
||||
snake_man_c5 = "Snake Man Stage - Health Energy 3"
|
||||
spark_man_c1 = "Spark Man Stage - Health Energy 1"
|
||||
spark_man_c2 = "Spark Man Stage - Weapon Energy 1"
|
||||
spark_man_c3 = "Spark Man Stage - Weapon Energy 2"
|
||||
spark_man_c4 = "Spark Man Stage - Weapon Energy 3"
|
||||
spark_man_c5 = "Spark Man Stage - Weapon Energy 4"
|
||||
spark_man_c6 = "Spark Man Stage - Weapon Energy 5"
|
||||
shadow_man_c1 = "Shadow Man Stage - Weapon Energy 1"
|
||||
shadow_man_c2 = "Shadow Man Stage - Weapon Energy 2"
|
||||
shadow_man_c3 = "Shadow Man Stage - Weapon Energy 3"
|
||||
shadow_man_c4 = "Shadow Man Stage - Weapon Energy 4"
|
||||
doc_needle_c1 = "Doc Robot (Needle) - Health Energy 1"
|
||||
doc_needle_c2 = "Doc Robot (Needle) - 1-Up 1"
|
||||
doc_needle_c3 = "Doc Robot (Needle) - E-Tank 1"
|
||||
doc_needle_c4 = "Doc Robot (Needle) - Weapon Energy 1"
|
||||
doc_needle_c5 = "Doc Robot (Needle) - Weapon Energy 2"
|
||||
doc_needle_c6 = "Doc Robot (Needle) - Weapon Energy 3"
|
||||
doc_needle_c7 = "Doc Robot (Needle) - Weapon Energy 4"
|
||||
doc_needle_c8 = "Doc Robot (Needle) - Weapon Energy 5"
|
||||
doc_needle_c9 = "Doc Robot (Needle) - Weapon Energy 6"
|
||||
doc_needle_c10 = "Doc Robot (Needle) - Weapon Energy 7"
|
||||
doc_needle_c11 = "Doc Robot (Needle) - Health Energy 2"
|
||||
doc_gemini_c1 = "Doc Robot (Gemini) - Mystery Tank 1"
|
||||
doc_gemini_c2 = "Doc Robot (Gemini) - Mystery Tank 2"
|
||||
doc_gemini_c3 = "Doc Robot (Gemini) - Weapon Energy 1"
|
||||
doc_gemini_c4 = "Doc Robot (Gemini) - Weapon Energy 2"
|
||||
doc_spark_c1 = "Doc Robot (Spark) - Health Energy 1"
|
||||
doc_spark_c2 = "Doc Robot (Spark) - Health Energy 2"
|
||||
doc_shadow_c1 = "Doc Robot (Shadow) - Health Energy 1"
|
||||
doc_shadow_c2 = "Doc Robot (Shadow) - Weapon Energy 1"
|
||||
doc_shadow_c3 = "Doc Robot (Shadow) - Weapon Energy 2"
|
||||
doc_shadow_c4 = "Doc Robot (Shadow) - Weapon Energy 3"
|
||||
doc_shadow_c5 = "Doc Robot (Shadow) - Weapon Energy 4"
|
||||
wily_1_c1 = "Wily Stage 1 - 1-Up 1"
|
||||
wily_1_c2 = "Wily Stage 1 - E-Tank 1"
|
||||
wily_1_c3 = "Wily Stage 1 - Weapon Energy 1"
|
||||
wily_1_c4 = "Wily Stage 1 - 1-Up 2" # Hard Knuckle
|
||||
wily_1_c5 = "Wily Stage 1 - Health Energy 1" # Hard Knuckle
|
||||
wily_1_c6 = "Wily Stage 1 - Weapon Energy 2" # Hard Knuckle & Rush Vertical
|
||||
wily_1_c7 = "Wily Stage 1 - Health Energy 2" # Hard Knuckle & Rush Vertical
|
||||
wily_1_c8 = "Wily Stage 1 - E-Tank 2" # Hard Knuckle & Rush Vertical
|
||||
wily_1_c9 = "Wily Stage 1 - Health Energy 3"
|
||||
wily_1_c10 = "Wily Stage 1 - Health Energy 4"
|
||||
wily_1_c11 = "Wily Stage 1 - Weapon Energy 3" # Rush Vertical
|
||||
wily_1_c12 = "Wily Stage 1 - Weapon Energy 4" # Rush Vertical
|
||||
wily_2_c1 = "Wily Stage 2 - Weapon Energy 1"
|
||||
wily_2_c2 = "Wily Stage 2 - Weapon Energy 2"
|
||||
wily_2_c3 = "Wily Stage 2 - 1-Up 1"
|
||||
wily_2_c4 = "Wily Stage 2 - Weapon Energy 3"
|
||||
wily_2_c5 = "Wily Stage 2 - Health Energy 1"
|
||||
wily_2_c6 = "Wily Stage 2 - Health Energy 2"
|
||||
wily_2_c7 = "Wily Stage 2 - Health Energy 3"
|
||||
wily_2_c8 = "Wily Stage 2 - Weapon Energy 4"
|
||||
wily_2_c9 = "Wily Stage 2 - E-Tank 1"
|
||||
wily_2_c10 = "Wily Stage 2 - Weapon Energy 5"
|
||||
wily_2_c11 = "Wily Stage 2 - E-Tank 2"
|
||||
wily_2_c12 = "Wily Stage 2 - Weapon Energy 6"
|
||||
wily_2_c13 = "Wily Stage 2 - Weapon Energy 7"
|
||||
wily_3_c1 = "Wily Stage 3 - Weapon Energy 1" # Hard Knuckle
|
||||
wily_3_c2 = "Wily Stage 3 - Weapon Energy 2" # Hard Knuckle
|
||||
wily_3_c3 = "Wily Stage 3 - E-Tank 1"
|
||||
wily_3_c4 = "Wily Stage 3 - 1-Up 1"
|
||||
wily_3_c5 = "Wily Stage 3 - Health Energy 1"
|
||||
wily_3_c6 = "Wily Stage 3 - Health Energy 2"
|
||||
wily_3_c7 = "Wily Stage 3 - Health Energy 3"
|
||||
wily_3_c8 = "Wily Stage 3 - Health Energy 4"
|
||||
wily_3_c9 = "Wily Stage 3 - Weapon Energy 3"
|
||||
wily_3_c10 = "Wily Stage 3 - Mystery Tank 1" # Hard Knuckle
|
||||
wily_3_c11 = "Wily Stage 3 - Mystery Tank 2" # Hard Knuckle
|
||||
wily_4_c1 = "Wily Stage 4 - Weapon Energy 1"
|
||||
wily_4_c2 = "Wily Stage 4 - Weapon Energy 2"
|
||||
wily_4_c3 = "Wily Stage 4 - Weapon Energy 3"
|
||||
wily_4_c4 = "Wily Stage 4 - Weapon Energy 4"
|
||||
wily_4_c5 = "Wily Stage 4 - Weapon Energy 5"
|
||||
wily_4_c6 = "Wily Stage 4 - Health Energy 1"
|
||||
wily_4_c7 = "Wily Stage 4 - Health Energy 2"
|
||||
wily_4_c8 = "Wily Stage 4 - Health Energy 3"
|
||||
wily_4_c9 = "Wily Stage 4 - Health Energy 4"
|
||||
wily_4_c10 = "Wily Stage 4 - Mystery Tank"
|
||||
wily_4_c11 = "Wily Stage 4 - Weapon Energy 6"
|
||||
wily_4_c12 = "Wily Stage 4 - 1-Up"
|
||||
wily_4_c13 = "Wily Stage 4 - Weapon Energy 7"
|
||||
wily_4_c14 = "Wily Stage 4 - Weapon Energy 8"
|
||||
wily_4_c15 = "Wily Stage 4 - Weapon Energy 9"
|
||||
wily_4_c16 = "Wily Stage 4 - Weapon Energy 10"
|
||||
wily_4_c17 = "Wily Stage 4 - Weapon Energy 11"
|
||||
wily_4_c18 = "Wily Stage 4 - Weapon Energy 12"
|
||||
wily_4_c19 = "Wily Stage 4 - Weapon Energy 13"
|
||||
wily_4_c20 = "Wily Stage 4 - Weapon Energy 14"
|
||||
wily_5_c1 = "Wily Stage 5 - Weapon Energy 1"
|
||||
wily_5_c2 = "Wily Stage 5 - Weapon Energy 2"
|
||||
wily_5_c3 = "Wily Stage 5 - Mystery Tank 1"
|
||||
wily_5_c4 = "Wily Stage 5 - Mystery Tank 2"
|
||||
wily_6_c1 = "Wily Stage 6 - Mystery Tank 1"
|
||||
wily_6_c2 = "Wily Stage 6 - Mystery Tank 2"
|
||||
wily_6_c3 = "Wily Stage 6 - Weapon Energy 1"
|
||||
wily_6_c4 = "Wily Stage 6 - Weapon Energy 2"
|
||||
wily_6_c5 = "Wily Stage 6 - 1-Up"
|
||||
wily_6_c6 = "Wily Stage 6 - E-Tank"
|
||||
wily_6_c7 = "Wily Stage 6 - Health Energy"
|
||||
@@ -1,164 +0,0 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from Options import Choice, Toggle, DeathLink, TextChoice, Range, OptionDict, PerGameCommonOptions
|
||||
from schema import Schema, And, Use, Optional
|
||||
from .rules import bosses, weapons_to_id
|
||||
|
||||
|
||||
class EnergyLink(Toggle):
|
||||
"""
|
||||
Enables EnergyLink support.
|
||||
When enabled, pickups dropped from enemies are sent to the EnergyLink pool, and healing/weapon energy/1-Ups can
|
||||
be requested from the EnergyLink pool.
|
||||
Some of the energy sent to the pool will be lost on transfer.
|
||||
"""
|
||||
display_name = "EnergyLink"
|
||||
|
||||
|
||||
class StartingRobotMaster(Choice):
|
||||
"""
|
||||
The initial stage unlocked at the start.
|
||||
"""
|
||||
display_name = "Starting Robot Master"
|
||||
option_needle_man = 0
|
||||
option_magnet_man = 1
|
||||
option_gemini_man = 2
|
||||
option_hard_man = 3
|
||||
option_top_man = 4
|
||||
option_snake_man = 5
|
||||
option_spark_man = 6
|
||||
option_shadow_man = 7
|
||||
default = "random"
|
||||
|
||||
|
||||
class Consumables(Choice):
|
||||
"""
|
||||
When enabled, e-tanks/1-ups/health/weapon energy will be added to the pool of items and included as checks.
|
||||
"""
|
||||
display_name = "Consumables"
|
||||
option_none = 0
|
||||
option_1up_etank = 1
|
||||
option_weapon_health = 2
|
||||
option_all = 3
|
||||
default = 1
|
||||
alias_true = 3
|
||||
alias_false = 0
|
||||
|
||||
@classmethod
|
||||
def get_option_name(cls, value: int) -> str:
|
||||
if value == 1:
|
||||
return "1-Ups/E-Tanks"
|
||||
elif value == 2:
|
||||
return "Weapon/Health Energy"
|
||||
return super().get_option_name(value)
|
||||
|
||||
|
||||
class PaletteShuffle(TextChoice):
|
||||
"""
|
||||
Change the color of Mega Man and the Robot Masters.
|
||||
None: The palettes are unchanged.
|
||||
Shuffled: Palette colors are shuffled amongst the robot masters.
|
||||
Randomized: Random (usually good) palettes are generated for each robot master.
|
||||
Singularity: one palette is generated and used for all robot masters.
|
||||
Supports custom palettes using HTML named colors in the
|
||||
following format: Mega Buster-Lavender|Violet;randomized
|
||||
The first value is the character whose palette you'd like to define, then separated by - is a set of 2 colors for
|
||||
that character. separate every color with a pipe, and separate every character as well as the remaining shuffle with
|
||||
a semicolon.
|
||||
"""
|
||||
display_name = "Palette Shuffle"
|
||||
option_none = 0
|
||||
option_shuffled = 1
|
||||
option_randomized = 2
|
||||
option_singularity = 3
|
||||
|
||||
|
||||
class EnemyWeaknesses(Toggle):
|
||||
"""
|
||||
Randomizes the damage dealt to enemies by weapons. Certain enemies will always take damage from the buster.
|
||||
"""
|
||||
display_name = "Random Enemy Weaknesses"
|
||||
|
||||
|
||||
class StrictWeaknesses(Toggle):
|
||||
"""
|
||||
Only your starting Robot Master will take damage from the Mega Buster, the rest must be defeated with weapons.
|
||||
Weapons that only do 1-3 damage to bosses no longer deal damage (aside from Wily/Gamma).
|
||||
"""
|
||||
display_name = "Strict Boss Weaknesses"
|
||||
|
||||
|
||||
class RandomWeaknesses(Choice):
|
||||
"""
|
||||
None: Bosses will have their regular weaknesses.
|
||||
Shuffled: Weapon damage will be shuffled amongst the weapons, so Shadow Blade may do Top Spin damage.
|
||||
Randomized: Weapon damage will be fully randomized.
|
||||
"""
|
||||
display_name = "Random Boss Weaknesses"
|
||||
option_none = 0
|
||||
option_shuffled = 1
|
||||
option_randomized = 2
|
||||
alias_false = 0
|
||||
alias_true = 2
|
||||
|
||||
|
||||
class Wily4Requirement(Range):
|
||||
"""
|
||||
Change the amount of Robot Masters that are required to be defeated for
|
||||
the door to the Wily Machine to open.
|
||||
"""
|
||||
display_name = "Wily 4 Requirement"
|
||||
default = 8
|
||||
range_start = 1
|
||||
range_end = 8
|
||||
|
||||
|
||||
class WeaknessPlando(OptionDict):
|
||||
"""
|
||||
Specify specific damage numbers for boss damage. Can be used even without strict/random weaknesses.
|
||||
plando_weakness:
|
||||
Robot Master:
|
||||
Weapon: Damage
|
||||
"""
|
||||
display_name = "Plando Weaknesses"
|
||||
schema = Schema({
|
||||
Optional(And(str, Use(str.title), lambda s: s in bosses)): {
|
||||
And(str, Use(str.title), lambda s: s in weapons_to_id): And(int, lambda i: i in range(0, 14))
|
||||
}
|
||||
})
|
||||
default = {}
|
||||
|
||||
|
||||
class ReduceFlashing(Toggle):
|
||||
"""
|
||||
Reduce flashing seen in gameplay, such as in stages and when defeating certain bosses.
|
||||
"""
|
||||
display_name = "Reduce Flashing"
|
||||
|
||||
|
||||
class MusicShuffle(Choice):
|
||||
"""
|
||||
Shuffle the music that plays in every stage
|
||||
"""
|
||||
display_name = "Music Shuffle"
|
||||
option_none = 0
|
||||
option_shuffled = 1
|
||||
option_randomized = 2
|
||||
option_no_music = 3
|
||||
default = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class MM3Options(PerGameCommonOptions):
|
||||
death_link: DeathLink
|
||||
energy_link: EnergyLink
|
||||
starting_robot_master: StartingRobotMaster
|
||||
consumables: Consumables
|
||||
enemy_weakness: EnemyWeaknesses
|
||||
strict_weakness: StrictWeaknesses
|
||||
random_weakness: RandomWeaknesses
|
||||
wily_4_requirement: Wily4Requirement
|
||||
plando_weakness: WeaknessPlando
|
||||
palette_shuffle: PaletteShuffle
|
||||
reduce_flashing: ReduceFlashing
|
||||
music_shuffle: MusicShuffle
|
||||
@@ -1,374 +0,0 @@
|
||||
import pkgutil
|
||||
from typing import TYPE_CHECKING, Iterable
|
||||
import hashlib
|
||||
import Utils
|
||||
import os
|
||||
|
||||
from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes
|
||||
from . import names
|
||||
from .rules import bosses
|
||||
|
||||
from .text import MM3TextEntry
|
||||
from .color import get_colors_for_item, write_palette_shuffle
|
||||
from .options import Consumables
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import MM3World
|
||||
|
||||
MM3LCHASH = "5266687de215e790b2008284402f3917"
|
||||
PROTEUSHASH = "b69fff40212b80c94f19e786d1efbf61"
|
||||
MM3NESHASH = "4a53b6f58067d62c9a43404fe835dd5c"
|
||||
MM3VCHASH = "c50008f1ac86fae8d083232cdd3001a5"
|
||||
|
||||
enemy_weakness_ptrs: dict[int, int] = {
|
||||
0: 0x14100,
|
||||
1: 0x14200,
|
||||
2: 0x14300,
|
||||
3: 0x14400,
|
||||
4: 0x14500,
|
||||
5: 0x14600,
|
||||
6: 0x14700,
|
||||
7: 0x14800,
|
||||
8: 0x14900,
|
||||
}
|
||||
|
||||
enemy_addresses: dict[str, int] = {
|
||||
"Dada": 0x12,
|
||||
"Potton": 0x13,
|
||||
"New Shotman": 0x15,
|
||||
"Hammer Joe": 0x16,
|
||||
"Peterchy": 0x17,
|
||||
"Bubukan": 0x18,
|
||||
"Vault Pole": 0x19, # Capcom..., why did you name an enemy Pole?
|
||||
"Bomb Flier": 0x1A,
|
||||
"Yambow": 0x1D,
|
||||
"Metall 2": 0x1E,
|
||||
"Cannon": 0x22,
|
||||
"Jamacy": 0x25,
|
||||
"Jamacy 2": 0x26, # dunno what this is, but I won't question
|
||||
"Jamacy 3": 0x27,
|
||||
"Jamacy 4": 0x28, # tf is this Capcom
|
||||
"Mag Fly": 0x2A,
|
||||
"Egg": 0x2D,
|
||||
"Gyoraibo 2": 0x2E,
|
||||
"Junk Golem": 0x2F,
|
||||
"Pickelman Bull": 0x30,
|
||||
"Nitron": 0x35,
|
||||
"Pole": 0x37,
|
||||
"Gyoraibo": 0x38,
|
||||
"Hari Harry": 0x3A,
|
||||
"Penpen Maker": 0x3B,
|
||||
"Returning Monking": 0x3C,
|
||||
"Have 'Su' Bee": 0x3E,
|
||||
"Hive": 0x3F,
|
||||
"Bolton-Nutton": 0x40,
|
||||
"Walking Bomb": 0x44,
|
||||
"Elec'n": 0x45,
|
||||
"Mechakkero": 0x47,
|
||||
"Chibee": 0x4B,
|
||||
"Swimming Penpen": 0x4D,
|
||||
"Top": 0x52,
|
||||
"Penpen": 0x56,
|
||||
"Komasaburo": 0x57,
|
||||
"Parasyu": 0x59,
|
||||
"Hologran (Static)": 0x5A,
|
||||
"Hologran (Moving)": 0x5B,
|
||||
"Bomber Pepe": 0x5C,
|
||||
"Metall DX": 0x5D,
|
||||
"Petit Snakey": 0x5E,
|
||||
"Proto Man": 0x62,
|
||||
"Break Man": 0x63,
|
||||
"Metall": 0x7D,
|
||||
"Giant Springer": 0x83,
|
||||
"Springer Missile": 0x85,
|
||||
"Giant Snakey": 0x99,
|
||||
"Tama": 0x9A,
|
||||
"Doc Robot (Flash)": 0xB0,
|
||||
"Doc Robot (Wood)": 0xB1,
|
||||
"Doc Robot (Crash)": 0xB2,
|
||||
"Doc Robot (Metal)": 0xB3,
|
||||
"Doc Robot (Bubble)": 0xC0,
|
||||
"Doc Robot (Heat)": 0xC1,
|
||||
"Doc Robot (Quick)": 0xC2,
|
||||
"Doc Robot (Air)": 0xC3,
|
||||
"Snake": 0xCA,
|
||||
"Needle Man": 0xD0,
|
||||
"Magnet Man": 0xD1,
|
||||
"Top Man": 0xD2,
|
||||
"Shadow Man": 0xD3,
|
||||
"Top Man's Top": 0xD5,
|
||||
"Shadow Man (Sliding)": 0xD8, # Capcom I swear
|
||||
"Hard Man": 0xE0,
|
||||
"Spark Man": 0xE2,
|
||||
"Snake Man": 0xE4,
|
||||
"Gemini Man": 0xE6,
|
||||
"Gemini Man (Clone)": 0xE7, # Capcom why
|
||||
"Yellow Devil MK-II": 0xF1,
|
||||
"Wily Machine 3": 0xF3,
|
||||
"Gamma": 0xF8,
|
||||
"Kamegoro": 0x101,
|
||||
"Kamegoro Shell": 0x102,
|
||||
"Holograph Mega Man": 0x105,
|
||||
"Giant Metall": 0x10C, # This is technically FC but we're +16 from the rom header
|
||||
}
|
||||
|
||||
# addresses printed when assembling basepatch
|
||||
wily_4_ptr: int = 0x7F570
|
||||
consumables_ptr: int = 0x7FDEA
|
||||
energylink_ptr: int = 0x7FDF9
|
||||
|
||||
|
||||
class MM3ProcedurePatch(APProcedurePatch, APTokenMixin):
|
||||
hash = [MM3LCHASH, MM3NESHASH, MM3VCHASH]
|
||||
game = "Mega Man 3"
|
||||
patch_file_ending = ".apmm3"
|
||||
result_file_ending = ".nes"
|
||||
name: bytearray
|
||||
procedure = [
|
||||
("apply_bsdiff4", ["mm3_basepatch.bsdiff4"]),
|
||||
("apply_tokens", ["token_patch.bin"]),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_source_data(cls) -> bytes:
|
||||
return get_base_rom_bytes()
|
||||
|
||||
def write_byte(self, offset: int, value: int) -> None:
|
||||
self.write_token(APTokenTypes.WRITE, offset, value.to_bytes(1, "little"))
|
||||
|
||||
def write_bytes(self, offset: int, value: Iterable[int]) -> None:
|
||||
self.write_token(APTokenTypes.WRITE, offset, bytes(value))
|
||||
|
||||
|
||||
def patch_rom(world: "MM3World", patch: MM3ProcedurePatch) -> None:
|
||||
patch.write_file("mm3_basepatch.bsdiff4", pkgutil.get_data(__name__, os.path.join("data", "mm3_basepatch.bsdiff4")))
|
||||
# text writing
|
||||
|
||||
base_address = 0x3C000
|
||||
color_address = 0x31BC7
|
||||
for i, offset, location in zip([0, 8, 1, 2,
|
||||
3, 4, 5, 6,
|
||||
7, 9],
|
||||
[0x10, 0x50, 0x91, 0xD2,
|
||||
0x113, 0x154, 0x195, 0x1D6,
|
||||
0x217, 0x257],
|
||||
[
|
||||
names.get_needle_cannon,
|
||||
names.get_rush_jet,
|
||||
names.get_magnet_missile,
|
||||
names.get_gemini_laser,
|
||||
names.get_hard_knuckle,
|
||||
names.get_top_spin,
|
||||
names.get_search_snake,
|
||||
names.get_spark_shock,
|
||||
names.get_shadow_blade,
|
||||
names.get_rush_marine,
|
||||
]):
|
||||
item = world.get_location(location).item
|
||||
if item:
|
||||
if len(item.name) <= 13:
|
||||
# we want to just place it in the center
|
||||
first_str = ""
|
||||
second_str = item.name
|
||||
third_str = ""
|
||||
elif len(item.name) <= 26:
|
||||
# spread across second and third
|
||||
first_str = ""
|
||||
second_str = item.name[:13]
|
||||
third_str = item.name[13:]
|
||||
else:
|
||||
# all three
|
||||
first_str = item.name[:13]
|
||||
second_str = item.name[13:26]
|
||||
third_str = item.name[26:]
|
||||
if len(third_str) > 13:
|
||||
third_str = third_str[:13]
|
||||
player_str = world.multiworld.get_player_name(item.player)
|
||||
if len(player_str) > 13:
|
||||
player_str = player_str[:13]
|
||||
y_coords = 0xA5
|
||||
row = 0x21
|
||||
if location in [names.get_rush_marine, names.get_rush_jet]:
|
||||
y_coords = 0x45
|
||||
row = 0x22
|
||||
patch.write_bytes(base_address + offset, MM3TextEntry(first_str, y_coords, row).resolve())
|
||||
patch.write_bytes(base_address + 16 + offset, MM3TextEntry(second_str, y_coords + 0x20, row).resolve())
|
||||
patch.write_bytes(base_address + 32 + offset, MM3TextEntry(third_str, y_coords + 0x40, row).resolve())
|
||||
if y_coords + 0x60 > 0xFF:
|
||||
row += 1
|
||||
y_coords = 0x01
|
||||
patch.write_bytes(base_address + 48 + offset, MM3TextEntry(player_str, y_coords, row).resolve())
|
||||
colors_high, colors_low = get_colors_for_item(item.name)
|
||||
patch.write_bytes(color_address + (i * 8) + 1, colors_high)
|
||||
patch.write_bytes(color_address + (i * 8) + 5, colors_low)
|
||||
else:
|
||||
patch.write_bytes(base_address + 48 + offset, MM3TextEntry(player_str, y_coords + 0x60, row).resolve())
|
||||
|
||||
write_palette_shuffle(world, patch)
|
||||
|
||||
enemy_weaknesses: dict[str, dict[int, int]] = {}
|
||||
|
||||
if world.options.strict_weakness or world.options.random_weakness or world.options.plando_weakness:
|
||||
# we need to write boss weaknesses
|
||||
for boss in bosses:
|
||||
if boss == "Kamegoro Maker":
|
||||
enemy_weaknesses["Kamegoro"] = {i: world.weapon_damage[i][bosses[boss]] for i in world.weapon_damage}
|
||||
enemy_weaknesses["Kamegoro Shell"] = {i: world.weapon_damage[i][bosses[boss]]
|
||||
for i in world.weapon_damage}
|
||||
elif boss == "Gemini Man":
|
||||
enemy_weaknesses[boss] = {i: world.weapon_damage[i][bosses[boss]] for i in world.weapon_damage}
|
||||
enemy_weaknesses["Gemini Man (Clone)"] = {i: world.weapon_damage[i][bosses[boss]]
|
||||
for i in world.weapon_damage}
|
||||
elif boss == "Shadow Man":
|
||||
enemy_weaknesses[boss] = {i: world.weapon_damage[i][bosses[boss]] for i in world.weapon_damage}
|
||||
enemy_weaknesses["Shadow Man (Sliding)"] = {i: world.weapon_damage[i][bosses[boss]]
|
||||
for i in world.weapon_damage}
|
||||
else:
|
||||
enemy_weaknesses[boss] = {i: world.weapon_damage[i][bosses[boss]] for i in world.weapon_damage}
|
||||
|
||||
if world.options.enemy_weakness:
|
||||
for enemy in enemy_addresses:
|
||||
if enemy in [*bosses.keys(), "Kamegoro", "Kamegoro Shell", "Gemini Man (Clone)", "Shadow Man (Sliding)"]:
|
||||
continue
|
||||
enemy_weaknesses[enemy] = {weapon: world.random.randint(-4, 4) for weapon in enemy_weakness_ptrs}
|
||||
if enemy in ["Tama", "Giant Snakey", "Proto Man", "Giant Metall"] and enemy_weaknesses[enemy][0] <= 0:
|
||||
enemy_weaknesses[enemy][0] = 1
|
||||
elif enemy == "Jamacy 2":
|
||||
# bruh
|
||||
if not enemy_weaknesses[enemy][8] > 0:
|
||||
enemy_weaknesses[enemy][8] = 1
|
||||
if not enemy_weaknesses[enemy][3] > 0:
|
||||
enemy_weaknesses[enemy][3] = 1
|
||||
|
||||
for enemy, damage in enemy_weaknesses.items():
|
||||
for weapon in enemy_weakness_ptrs:
|
||||
if damage[weapon] < 0:
|
||||
damage[weapon] = 256 + damage[weapon]
|
||||
patch.write_byte(enemy_weakness_ptrs[weapon] + enemy_addresses[enemy], damage[weapon])
|
||||
|
||||
if world.options.consumables != Consumables.option_all:
|
||||
value_a = 0x64
|
||||
value_b = 0x6A
|
||||
if world.options.consumables in (Consumables.option_none, Consumables.option_1up_etank):
|
||||
value_a = 0x68
|
||||
if world.options.consumables in (Consumables.option_none, Consumables.option_weapon_health):
|
||||
value_b = 0x67
|
||||
patch.write_byte(consumables_ptr - 3, value_a)
|
||||
patch.write_byte(consumables_ptr + 1, value_b)
|
||||
|
||||
patch.write_byte(wily_4_ptr + 1, world.options.wily_4_requirement.value)
|
||||
|
||||
patch.write_byte(energylink_ptr + 1, world.options.energy_link.value)
|
||||
|
||||
if world.options.reduce_flashing:
|
||||
# Spark Man
|
||||
patch.write_byte(0x12649, 8)
|
||||
patch.write_byte(0x1264E, 8)
|
||||
patch.write_byte(0x12653, 8)
|
||||
# Shadow Man
|
||||
patch.write_byte(0x12658, 0x10)
|
||||
# Gemini Man
|
||||
patch.write_byte(0x12637, 0x20)
|
||||
patch.write_byte(0x1263D, 0x20)
|
||||
patch.write_byte(0x12643, 0x20)
|
||||
# Gamma
|
||||
patch.write_byte(0x7DA4A, 0xF)
|
||||
|
||||
if world.options.music_shuffle:
|
||||
if world.options.music_shuffle.current_key == "no_music":
|
||||
pool = [0xF0] * 18
|
||||
elif world.options.music_shuffle.current_key == "randomized":
|
||||
pool = world.random.choices(range(1, 0xC), k=18)
|
||||
else:
|
||||
pool = [1, 2, 3, 4, 5, 6, 7, 8, 1, 3, 7, 8, 9, 9, 10, 10, 11, 11]
|
||||
world.random.shuffle(pool)
|
||||
patch.write_bytes(0x7CD1C, pool)
|
||||
|
||||
from Utils import __version__
|
||||
patch.name = bytearray(f'MM3{__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0',
|
||||
'utf8')[:21]
|
||||
patch.name.extend([0] * (21 - len(patch.name)))
|
||||
patch.write_bytes(0x3F330, patch.name) # We changed this section, but this pointer is still valid!
|
||||
deathlink_byte = world.options.death_link.value | (world.options.energy_link.value << 1)
|
||||
patch.write_byte(0x3F346, deathlink_byte)
|
||||
|
||||
patch.write_bytes(0x3F34C, world.world_version)
|
||||
|
||||
version_map = {
|
||||
"0": 0x00,
|
||||
"1": 0x01,
|
||||
"2": 0x02,
|
||||
"3": 0x03,
|
||||
"4": 0x04,
|
||||
"5": 0x05,
|
||||
"6": 0x06,
|
||||
"7": 0x07,
|
||||
"8": 0x08,
|
||||
"9": 0x09,
|
||||
".": 0x26
|
||||
}
|
||||
patch.write_token(APTokenTypes.RLE, 0x653B, (11, 0x25))
|
||||
patch.write_token(APTokenTypes.RLE, 0x6549, (25, 0x25))
|
||||
|
||||
# BY SILVRIS
|
||||
patch.write_bytes(0x653B, [0x0B, 0x22, 0x25, 0x1C, 0x12, 0x15, 0x1F, 0x1B, 0x12, 0x1C])
|
||||
# ARCHIPELAGO x.x.x
|
||||
patch.write_bytes(0x654D,
|
||||
[0x0A, 0x1B, 0x0C, 0x11, 0x12, 0x19, 0x0E, 0x15, 0x0A, 0x10, 0x18])
|
||||
patch.write_bytes(0x6559, list(map(lambda c: version_map[c], __version__)))
|
||||
|
||||
patch.write_file("token_patch.bin", patch.get_token_binary())
|
||||
|
||||
|
||||
header = b"\x4E\x45\x53\x1A\x10\x10\x40\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
|
||||
|
||||
def read_headerless_nes_rom(rom: bytes) -> bytes:
|
||||
if rom[:4] == b"NES\x1A":
|
||||
return rom[16:]
|
||||
else:
|
||||
return rom
|
||||
|
||||
|
||||
def get_base_rom_bytes(file_name: str = "") -> bytes:
|
||||
base_rom_bytes: bytes | None = 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 = read_headerless_nes_rom(bytes(open(file_name, "rb").read()))
|
||||
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(base_rom_bytes)
|
||||
if basemd5.hexdigest() == PROTEUSHASH:
|
||||
base_rom_bytes = extract_mm3(base_rom_bytes)
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(base_rom_bytes)
|
||||
if basemd5.hexdigest() not in {MM3LCHASH, MM3NESHASH, MM3VCHASH}:
|
||||
print(basemd5.hexdigest())
|
||||
raise Exception("Supplied Base Rom does not match known MD5 for US, LC, or US VC release. "
|
||||
"Get the correct game and version, then dump it")
|
||||
headered_rom = bytearray(base_rom_bytes)
|
||||
headered_rom[0:0] = header
|
||||
setattr(get_base_rom_bytes, "base_rom_bytes", bytes(headered_rom))
|
||||
return bytes(headered_rom)
|
||||
return base_rom_bytes
|
||||
|
||||
|
||||
def get_base_rom_path(file_name: str = "") -> str:
|
||||
from . import MM3World
|
||||
if not file_name:
|
||||
file_name = MM3World.settings.rom_file
|
||||
if not os.path.exists(file_name):
|
||||
file_name = Utils.user_path(file_name)
|
||||
return file_name
|
||||
|
||||
|
||||
prg_offset = 0xCF1B0
|
||||
prg_size = 0x40000
|
||||
chr_offset = 0x10F1B0
|
||||
chr_size = 0x20000
|
||||
|
||||
|
||||
def extract_mm3(proteus: bytes) -> bytes:
|
||||
mm3 = bytearray(proteus[prg_offset:prg_offset + prg_size])
|
||||
mm3.extend(proteus[chr_offset:chr_offset + chr_size])
|
||||
return bytes(mm3)
|
||||
@@ -1,388 +0,0 @@
|
||||
from math import ceil
|
||||
from typing import TYPE_CHECKING
|
||||
from . import names
|
||||
from .locations import get_boss_locations, get_oneup_locations, get_energy_locations
|
||||
from worlds.generic.Rules import add_rule
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import MM3World
|
||||
from BaseClasses import CollectionState
|
||||
|
||||
bosses: dict[str, int] = {
|
||||
"Needle Man": 0,
|
||||
"Magnet Man": 1,
|
||||
"Gemini Man": 2,
|
||||
"Hard Man": 3,
|
||||
"Top Man": 4,
|
||||
"Snake Man": 5,
|
||||
"Spark Man": 6,
|
||||
"Shadow Man": 7,
|
||||
"Doc Robot (Metal)": 8,
|
||||
"Doc Robot (Quick)": 9,
|
||||
"Doc Robot (Air)": 10,
|
||||
"Doc Robot (Crash)": 11,
|
||||
"Doc Robot (Flash)": 12,
|
||||
"Doc Robot (Bubble)": 13,
|
||||
"Doc Robot (Wood)": 14,
|
||||
"Doc Robot (Heat)": 15,
|
||||
"Break Man": 16,
|
||||
"Kamegoro Maker": 17,
|
||||
"Yellow Devil MK-II": 18,
|
||||
"Holograph Mega Man": 19,
|
||||
"Wily Machine 3": 20,
|
||||
"Gamma": 21
|
||||
}
|
||||
|
||||
weapons_to_id: dict[str, int] = {
|
||||
"Mega Buster": 0,
|
||||
"Needle Cannon": 1,
|
||||
"Magnet Missile": 2,
|
||||
"Gemini Laser": 3,
|
||||
"Hard Knuckle": 4,
|
||||
"Top Spin": 5,
|
||||
"Search Snake": 6,
|
||||
"Spark Shot": 7,
|
||||
"Shadow Blade": 8,
|
||||
}
|
||||
|
||||
weapon_damage: dict[int, list[int]] = {
|
||||
0: [1, 2, 1, 1, 2, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 3, 1, 1, 1, 0, ], # Mega Buster
|
||||
1: [4, 1, 1, 0, 2, 4, 2, 1, 0, 1, 1, 2, 4, 2, 4, 2, 0, 3, 1, 1, 1, 0, ], # Needle Cannon
|
||||
2: [1, 4, 2, 4, 1, 0, 0, 1, 4, 2, 4, 1, 1, 0, 0, 1, 0, 3, 1, 0, 1, 0, ], # Magnet Missile
|
||||
3: [7, 2, 4, 1, 0, 1, 1, 1, 1, 4, 2, 0, 4, 1, 1, 1, 0, 3, 1, 1, 1, 0, ], # Gemini Laser
|
||||
4: [0, 2, 2, 4, 7, 2, 2, 2, 4, 1, 2, 7, 0, 2, 2, 2, 0, 1, 5, 4, 7, 4, ], # Hard Knuckle
|
||||
5: [1, 1, 2, 0, 4, 2, 1, 7, 0, 1, 1, 4, 1, 1, 2, 7, 0, 1, 0, 7, 0, 2, ], # Top Spin
|
||||
6: [1, 1, 5, 0, 1, 4, 0, 1, 0, 4, 1, 1, 1, 0, 4, 1, 0, 1, 0, 7, 4, 2, ], # Search Snake
|
||||
7: [0, 7, 1, 0, 1, 1, 4, 1, 2, 1, 4, 1, 0, 4, 1, 1, 0, 0, 0, 0, 7, 0, ], # Spark Shot
|
||||
8: [2, 7, 2, 0, 1, 2, 4, 4, 2, 2, 0, 1, 2, 4, 2, 4, 0, 1, 3, 2, 2, 2, ], # Shadow Blade
|
||||
}
|
||||
|
||||
weapons_to_name: dict[int, str] = {
|
||||
1: names.needle_cannon,
|
||||
2: names.magnet_missile,
|
||||
3: names.gemini_laser,
|
||||
4: names.hard_knuckle,
|
||||
5: names.top_spin,
|
||||
6: names.search_snake,
|
||||
7: names.spark_shock,
|
||||
8: names.shadow_blade
|
||||
}
|
||||
|
||||
minimum_weakness_requirement: dict[int, int] = {
|
||||
0: 1, # Mega Buster is free
|
||||
1: 1, # 112 shots of Needle Cannon
|
||||
2: 2, # 14 shots of Magnet Missile
|
||||
3: 2, # 14 shots of Gemini Laser
|
||||
4: 2, # 14 uses of Hard Knuckle
|
||||
5: 4, # an unknown amount of Top Spin (4 means you should be able to be fine)
|
||||
6: 1, # 56 uses of Search Snake
|
||||
7: 2, # 14 functional uses of Spark Shot (fires in twos)
|
||||
8: 1, # 56 uses of Shadow Blade
|
||||
}
|
||||
|
||||
robot_masters: dict[int, str] = {
|
||||
0: "Needle Man Defeated",
|
||||
1: "Magnet Man Defeated",
|
||||
2: "Gemini Man Defeated",
|
||||
3: "Hard Man Defeated",
|
||||
4: "Top Man Defeated",
|
||||
5: "Snake Man Defeated",
|
||||
6: "Spark Man Defeated",
|
||||
7: "Shadow Man Defeated"
|
||||
}
|
||||
|
||||
weapon_costs = {
|
||||
0: 0,
|
||||
1: 0.25,
|
||||
2: 2,
|
||||
3: 2,
|
||||
4: 2,
|
||||
5: 7, # Not really, but we can really only rely on Top for one RBM
|
||||
6: 0.5,
|
||||
7: 2,
|
||||
8: 0.5,
|
||||
}
|
||||
|
||||
|
||||
def can_defeat_enough_rbms(state: "CollectionState", player: int,
|
||||
required: int, boss_requirements: dict[int, list[int]]) -> bool:
|
||||
can_defeat = 0
|
||||
for boss, reqs in boss_requirements.items():
|
||||
if boss in robot_masters:
|
||||
if state.has_all(map(lambda x: weapons_to_name[x], reqs), player):
|
||||
can_defeat += 1
|
||||
if can_defeat >= required:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def has_rush_vertical(state: "CollectionState", player: int) -> bool:
|
||||
return state.has_any([names.rush_coil, names.rush_jet], player)
|
||||
|
||||
|
||||
def can_traverse_long_water(state: "CollectionState", player: int) -> bool:
|
||||
return state.has_any([names.rush_marine, names.rush_jet], player)
|
||||
|
||||
|
||||
def has_any_rush(state: "CollectionState", player: int) -> bool:
|
||||
return state.has_any([names.rush_coil, names.rush_jet, names.rush_marine], player)
|
||||
|
||||
|
||||
def has_rush_jet(state: "CollectionState", player: int) -> bool:
|
||||
return state.has(names.rush_jet, player)
|
||||
|
||||
|
||||
def set_rules(world: "MM3World") -> None:
|
||||
# most rules are set on region, so we only worry about rules required within stage access
|
||||
# or rules variable on settings
|
||||
if hasattr(world.multiworld, "re_gen_passthrough"):
|
||||
slot_data = getattr(world.multiworld, "re_gen_passthrough")["Mega Man 3"]
|
||||
world.weapon_damage = slot_data["weapon_damage"]
|
||||
else:
|
||||
if world.options.random_weakness == world.options.random_weakness.option_shuffled:
|
||||
weapon_tables = [table.copy() for weapon, table in weapon_damage.items() if weapon != 0]
|
||||
world.random.shuffle(weapon_tables)
|
||||
for i in range(1, 9):
|
||||
world.weapon_damage[i] = weapon_tables.pop()
|
||||
elif world.options.random_weakness == world.options.random_weakness.option_randomized:
|
||||
world.weapon_damage = {i: [] for i in range(9)}
|
||||
for boss in range(22):
|
||||
for weapon in world.weapon_damage:
|
||||
world.weapon_damage[weapon].append(min(14, max(0, int(world.random.normalvariate(3, 3)))))
|
||||
if not any([world.weapon_damage[weapon][boss] >= 4
|
||||
for weapon in range(1, 9)]):
|
||||
# failsafe, there should be at least one defined non-Buster weakness
|
||||
weapon = world.random.randint(1, 7)
|
||||
world.weapon_damage[weapon][boss] = world.random.randint(4, 14) # Force weakness
|
||||
# handle Break Man
|
||||
boss = 16
|
||||
for weapon in world.weapon_damage:
|
||||
world.weapon_damage[weapon][boss] = 0
|
||||
weapon = world.random.choice(list(world.weapon_damage.keys()))
|
||||
world.weapon_damage[weapon][boss] = minimum_weakness_requirement[weapon]
|
||||
|
||||
if world.options.strict_weakness:
|
||||
for weapon in weapon_damage:
|
||||
for i in range(22):
|
||||
if i == 16:
|
||||
continue # Break is only weak to buster on non-random, and minimal damage on random
|
||||
elif weapon == 0:
|
||||
world.weapon_damage[weapon][i] = 0
|
||||
elif i in (20, 21) and not world.options.random_weakness:
|
||||
continue
|
||||
# Gamma and Wily Machine need all weaknesses present, so allow
|
||||
elif not world.options.random_weakness == world.options.random_weakness.option_randomized \
|
||||
and i == 17:
|
||||
if 3 > world.weapon_damage[weapon][i] > 0:
|
||||
# Kamegoros take 3 max from weapons on non-random
|
||||
world.weapon_damage[weapon][i] = 0
|
||||
elif 4 > world.weapon_damage[weapon][i] > 0:
|
||||
world.weapon_damage[weapon][i] = 0
|
||||
|
||||
for p_boss in world.options.plando_weakness:
|
||||
for p_weapon in world.options.plando_weakness[p_boss]:
|
||||
if not any(w for w in world.weapon_damage
|
||||
if w != weapons_to_id[p_weapon]
|
||||
and world.weapon_damage[w][bosses[p_boss]] > minimum_weakness_requirement[w]):
|
||||
# we need to replace this weakness
|
||||
weakness = world.random.choice([key for key in world.weapon_damage
|
||||
if key != weapons_to_id[p_weapon]])
|
||||
world.weapon_damage[weakness][bosses[p_boss]] = minimum_weakness_requirement[weakness]
|
||||
world.weapon_damage[weapons_to_id[p_weapon]][bosses[p_boss]] \
|
||||
= world.options.plando_weakness[p_boss][p_weapon]
|
||||
|
||||
# handle special cases
|
||||
for boss in range(22):
|
||||
for weapon in range(1, 9):
|
||||
if (0 < world.weapon_damage[weapon][boss] < minimum_weakness_requirement[weapon] and
|
||||
not any(world.weapon_damage[i][boss] >= minimum_weakness_requirement[weapon]
|
||||
for i in range(1, 8) if i != weapon)):
|
||||
world.weapon_damage[weapon][boss] = minimum_weakness_requirement[weapon]
|
||||
|
||||
if world.weapon_damage[0][world.options.starting_robot_master.value] < 1:
|
||||
world.weapon_damage[0][world.options.starting_robot_master.value] = 1
|
||||
|
||||
# weakness validation, it is better to confirm a completable seed than respect plando
|
||||
boss_health = {boss: 0x1C for boss in range(8)}
|
||||
|
||||
weapon_energy = {key: float(0x1C) for key in weapon_costs}
|
||||
weapon_boss = {boss: {weapon: world.weapon_damage[weapon][boss] for weapon in world.weapon_damage}
|
||||
for boss in range(8)}
|
||||
flexibility = {
|
||||
boss: (
|
||||
sum(damage_value > 0 for damage_value in
|
||||
weapon_damages.values()) # Amount of weapons that hit this boss
|
||||
* sum(weapon_damages.values()) # Overall damage that those weapons do
|
||||
)
|
||||
for boss, weapon_damages in weapon_boss.items()
|
||||
}
|
||||
boss_flexibility = sorted(flexibility, key=flexibility.get) # Fast way to sort dict by value
|
||||
used_weapons: dict[int, set[int]] = {i: set() for i in range(8)}
|
||||
for boss in boss_flexibility:
|
||||
boss_damage = weapon_boss[boss]
|
||||
weapon_weight = {weapon: (weapon_energy[weapon] / damage) if damage else 0 for weapon, damage in
|
||||
boss_damage.items() if weapon_energy[weapon] > 0}
|
||||
while boss_health[boss] > 0:
|
||||
if boss_damage[0] > 0:
|
||||
boss_health[boss] = 0 # if we can buster, we should buster
|
||||
continue
|
||||
highest, wp = max(zip(weapon_weight.values(), weapon_weight.keys()))
|
||||
uses = weapon_energy[wp] // weapon_costs[wp]
|
||||
if int(uses * boss_damage[wp]) >= boss_health[boss]:
|
||||
used = ceil(boss_health[boss] / boss_damage[wp])
|
||||
weapon_energy[wp] -= weapon_costs[wp] * used
|
||||
boss_health[boss] = 0
|
||||
used_weapons[boss].add(wp)
|
||||
elif highest <= 0:
|
||||
# we are out of weapons that can actually damage the boss
|
||||
# so find the weapon that has the most uses, and apply that as an additional weakness
|
||||
# it should be impossible to be out of energy
|
||||
max_uses, wp = max((weapon_energy[weapon] // weapon_costs[weapon], weapon)
|
||||
for weapon in weapon_weight
|
||||
if weapon != 0)
|
||||
world.weapon_damage[wp][boss] = minimum_weakness_requirement[wp]
|
||||
used = min(int(weapon_energy[wp] // weapon_costs[wp]),
|
||||
ceil(boss_health[boss] / minimum_weakness_requirement[wp]))
|
||||
weapon_energy[wp] -= weapon_costs[wp] * used
|
||||
boss_health[boss] -= int(used * minimum_weakness_requirement[wp])
|
||||
weapon_weight.pop(wp)
|
||||
used_weapons[boss].add(wp)
|
||||
else:
|
||||
# drain the weapon and continue
|
||||
boss_health[boss] -= int(uses * boss_damage[wp])
|
||||
weapon_energy[wp] -= weapon_costs[wp] * uses
|
||||
weapon_weight.pop(wp)
|
||||
used_weapons[boss].add(wp)
|
||||
|
||||
world.wily_4_weapons = {boss: sorted(weapons) for boss, weapons in used_weapons.items()}
|
||||
|
||||
for i, boss_locations in zip(range(22), [
|
||||
get_boss_locations("Needle Man Stage"),
|
||||
get_boss_locations("Magnet Man Stage"),
|
||||
get_boss_locations("Gemini Man Stage"),
|
||||
get_boss_locations("Hard Man Stage"),
|
||||
get_boss_locations("Top Man Stage"),
|
||||
get_boss_locations("Snake Man Stage"),
|
||||
get_boss_locations("Spark Man Stage"),
|
||||
get_boss_locations("Shadow Man Stage"),
|
||||
get_boss_locations("Doc Robot (Spark) - Metal"),
|
||||
get_boss_locations("Doc Robot (Spark) - Quick"),
|
||||
get_boss_locations("Doc Robot (Needle) - Air"),
|
||||
get_boss_locations("Doc Robot (Needle) - Crash"),
|
||||
get_boss_locations("Doc Robot (Gemini) - Flash"),
|
||||
get_boss_locations("Doc Robot (Gemini) - Bubble"),
|
||||
get_boss_locations("Doc Robot (Shadow) - Wood"),
|
||||
get_boss_locations("Doc Robot (Shadow) - Heat"),
|
||||
get_boss_locations("Break Man"),
|
||||
get_boss_locations("Wily Stage 1"),
|
||||
get_boss_locations("Wily Stage 2"),
|
||||
get_boss_locations("Wily Stage 3"),
|
||||
get_boss_locations("Wily Stage 5"),
|
||||
get_boss_locations("Wily Stage 6")
|
||||
]):
|
||||
if world.weapon_damage[0][i] > 0:
|
||||
continue # this can always be in logic
|
||||
weapons = []
|
||||
for weapon in range(1, 9):
|
||||
if world.weapon_damage[weapon][i] > 0:
|
||||
if world.weapon_damage[weapon][i] < minimum_weakness_requirement[weapon]:
|
||||
continue
|
||||
weapons.append(weapons_to_name[weapon])
|
||||
if not weapons:
|
||||
raise Exception(f"Attempted to have boss {i} with no weakness! Seed: {world.multiworld.seed}")
|
||||
for location in boss_locations:
|
||||
if i in (20, 21):
|
||||
# multi-phase fights, get all potential weaknesses
|
||||
# we should probably do this smarter, but this works for now
|
||||
add_rule(world.get_location(location),
|
||||
lambda state, weps=tuple(weapons): state.has_all(weps, world.player))
|
||||
else:
|
||||
add_rule(world.get_location(location),
|
||||
lambda state, weps=tuple(weapons): state.has_any(weps, world.player))
|
||||
|
||||
# Need to defeat x amount of robot masters for Wily 4
|
||||
add_rule(world.get_location(names.wily_stage_4),
|
||||
lambda state: can_defeat_enough_rbms(state, world.player, world.options.wily_4_requirement.value,
|
||||
world.wily_4_weapons))
|
||||
|
||||
# Handle Doc Robo stage connections
|
||||
for entrance, location in (("To Doc Robot (Needle) - Crash", names.doc_air),
|
||||
("To Doc Robot (Gemini) - Bubble", names.doc_flash),
|
||||
("To Doc Robot (Shadow) - Heat", names.doc_wood),
|
||||
("To Doc Robot (Spark) - Quick", names.doc_metal)):
|
||||
entrance_object = world.get_entrance(entrance)
|
||||
add_rule(entrance_object, lambda state, loc=location: state.can_reach(loc, "Location", world.player))
|
||||
|
||||
# finally, real logic
|
||||
for location in get_boss_locations("Hard Man Stage"):
|
||||
add_rule(world.get_location(location), lambda state: has_rush_vertical(state, world.player))
|
||||
|
||||
for location in get_boss_locations("Gemini Man Stage"):
|
||||
add_rule(world.get_location(location), lambda state: has_any_rush(state, world.player))
|
||||
|
||||
add_rule(world.get_entrance("To Doc Robot (Spark) - Metal"),
|
||||
lambda state: has_rush_vertical(state, world.player) and
|
||||
state.has_any([names.shadow_blade, names.gemini_laser], world.player))
|
||||
add_rule(world.get_entrance("To Doc Robot (Needle) - Air"),
|
||||
lambda state: has_rush_vertical(state, world.player))
|
||||
add_rule(world.get_entrance("To Doc Robot (Needle) - Crash"),
|
||||
lambda state: has_rush_jet(state, world.player))
|
||||
add_rule(world.get_entrance("To Doc Robot (Gemini) - Bubble"),
|
||||
lambda state: has_rush_vertical(state, world.player) and can_traverse_long_water(state, world.player))
|
||||
|
||||
for location in get_boss_locations("Wily Stage 1"):
|
||||
add_rule(world.get_location(location), lambda state: has_rush_vertical(state, world.player))
|
||||
|
||||
for location in get_boss_locations("Wily Stage 2"):
|
||||
add_rule(world.get_location(location), lambda state: has_rush_jet(state, world.player))
|
||||
|
||||
# Wily 3 technically needs vertical
|
||||
# However, Wily 3 requires beating Wily 2, and Wily 2 explicitly needs Jet
|
||||
# So we can skip the additional rule on Wily 3
|
||||
|
||||
if world.options.consumables in (world.options.consumables.option_1up_etank,
|
||||
world.options.consumables.option_all):
|
||||
add_rule(world.get_location(names.needle_man_c2), lambda state: has_rush_jet(state, world.player))
|
||||
add_rule(world.get_location(names.gemini_man_c1), lambda state: has_rush_jet(state, world.player))
|
||||
add_rule(world.get_location(names.gemini_man_c3),
|
||||
lambda state: has_rush_vertical(state, world.player)
|
||||
or state.has_any([names.gemini_laser, names.shadow_blade], world.player))
|
||||
for location in (names.gemini_man_c6, names.gemini_man_c7, names.gemini_man_c10):
|
||||
add_rule(world.get_location(location), lambda state: has_any_rush(state, world.player))
|
||||
for location in get_oneup_locations("Hard Man Stage"):
|
||||
add_rule(world.get_location(location), lambda state: has_rush_vertical(state, world.player))
|
||||
add_rule(world.get_location(names.top_man_c6), lambda state: has_rush_vertical(state, world.player))
|
||||
add_rule(world.get_location(names.doc_needle_c2), lambda state: has_rush_jet(state, world.player))
|
||||
add_rule(world.get_location(names.doc_needle_c3), lambda state: has_rush_jet(state, world.player))
|
||||
add_rule(world.get_location(names.doc_gemini_c1), lambda state: has_rush_vertical(state, world.player))
|
||||
add_rule(world.get_location(names.doc_gemini_c2), lambda state: has_rush_vertical(state, world.player))
|
||||
add_rule(world.get_location(names.wily_1_c8), lambda state: has_rush_vertical(state, world.player))
|
||||
for location in [names.wily_1_c4, names.wily_1_c8]:
|
||||
add_rule(world.get_location(location), lambda state: state.has(names.hard_knuckle, world.player))
|
||||
for location in get_oneup_locations("Wily Stage 2"):
|
||||
if location == names.wily_2_c3:
|
||||
continue
|
||||
add_rule(world.get_location(location), lambda state: has_rush_jet(state, world.player))
|
||||
if world.options.consumables in (world.options.consumables.option_weapon_health,
|
||||
world.options.consumables.option_all):
|
||||
add_rule(world.get_location(names.gemini_man_c2), lambda state: has_rush_vertical(state, world.player))
|
||||
add_rule(world.get_location(names.gemini_man_c4), lambda state: has_rush_vertical(state, world.player))
|
||||
add_rule(world.get_location(names.gemini_man_c5), lambda state: has_rush_vertical(state, world.player))
|
||||
for location in (names.gemini_man_c8, names.gemini_man_c9):
|
||||
add_rule(world.get_location(location), lambda state: has_any_rush(state, world.player))
|
||||
for location in get_energy_locations("Hard Man Stage"):
|
||||
if location == names.hard_man_c1:
|
||||
continue
|
||||
add_rule(world.get_location(location), lambda state: has_rush_vertical(state, world.player))
|
||||
for location in (names.spark_man_c1, names.spark_man_c2):
|
||||
add_rule(world.get_location(location), lambda state: has_rush_vertical(state, world.player))
|
||||
for location in [names.top_man_c2, names.top_man_c3, names.top_man_c4, names.top_man_c7]:
|
||||
add_rule(world.get_location(location), lambda state: has_rush_vertical(state, world.player))
|
||||
for location in [names.wily_1_c5, names.wily_1_c6, names.wily_1_c7]:
|
||||
add_rule(world.get_location(location), lambda state: state.has(names.hard_knuckle, world.player))
|
||||
for location in [names.wily_1_c6, names.wily_1_c7, names.wily_1_c11, names.wily_1_c12]:
|
||||
add_rule(world.get_location(location), lambda state: has_rush_vertical(state, world.player))
|
||||
for location in get_energy_locations("Wily Stage 2"):
|
||||
if location in (names.wily_2_c1, names.wily_2_c2, names.wily_2_c4):
|
||||
continue
|
||||
add_rule(world.get_location(location), lambda state: has_rush_jet(state, world.player))
|
||||
@@ -1,781 +0,0 @@
|
||||
norom
|
||||
!headersize = 16
|
||||
|
||||
!controller_flip = $14 ; only on first frame of input, used by crash man, etc
|
||||
!controller_mirror = $16
|
||||
!current_stage = $22
|
||||
!current_state = $60
|
||||
!completed_rbm_stages = $61
|
||||
!completed_doc_stages = $62
|
||||
!current_wily = $75
|
||||
!received_rbm_stages = $680
|
||||
!received_doc_stages = $681
|
||||
; !deathlink = $30, set to $0E
|
||||
!energylink_packet = $682
|
||||
!last_wily = $683
|
||||
!rbm_strobe = $684
|
||||
!sound_effect_strobe = $685
|
||||
!doc_robo_kills = $686
|
||||
!wily_stage_completion = $687
|
||||
;!received_items = $688
|
||||
!acquired_rush = $689
|
||||
|
||||
!current_weapon = $A0
|
||||
!current_health = $A2
|
||||
!received_weapons = $A3
|
||||
|
||||
'0' = $00
|
||||
'1' = $01
|
||||
'2' = $02
|
||||
'3' = $03
|
||||
'4' = $04
|
||||
'5' = $05
|
||||
'6' = $06
|
||||
'7' = $07
|
||||
'8' = $08
|
||||
'9' = $09
|
||||
'A' = $0A
|
||||
'B' = $0B
|
||||
'C' = $0C
|
||||
'D' = $0D
|
||||
'E' = $0E
|
||||
'F' = $0F
|
||||
'G' = $10
|
||||
'H' = $11
|
||||
'I' = $12
|
||||
'J' = $13
|
||||
'K' = $14
|
||||
'L' = $15
|
||||
'M' = $16
|
||||
'N' = $17
|
||||
'O' = $18
|
||||
'P' = $19
|
||||
'Q' = $1A
|
||||
'R' = $1B
|
||||
'S' = $1C
|
||||
'T' = $1D
|
||||
'U' = $1E
|
||||
'V' = $1F
|
||||
'W' = $20
|
||||
'X' = $21
|
||||
'Y' = $22
|
||||
'Z' = $23
|
||||
' ' = $25
|
||||
'.' = $26
|
||||
',' = $27
|
||||
'!' = $29
|
||||
'r' = $2A
|
||||
':' = $2B
|
||||
|
||||
; !consumable_checks = $0F80 ; have to find in-stage solutions for this, there's literally not enough ram
|
||||
|
||||
!CONTROLLER_SELECT = #$20
|
||||
!CONTROLLER_SELECT_START = #$30
|
||||
!CONTROLLER_ALL_BUTTON = #$F0
|
||||
|
||||
!PpuControl_2000 = $2000
|
||||
!PpuMask_2001 = $2001
|
||||
!PpuAddr_2006 = $2006
|
||||
!PpuData_2007 = $2007
|
||||
|
||||
;!LOAD_BANK = $C000
|
||||
|
||||
macro org(address,bank)
|
||||
if <bank> == $3E
|
||||
org <address>-$C000+($2000*<bank>)+!headersize ; org sets the position in the output file to write to (in norom, at least)
|
||||
base <address> ; base sets the position that all labels are relative to - this is necessary so labels will still start from $8000, instead of $0000 or somewhere
|
||||
else
|
||||
if <bank> == $3F
|
||||
org <address>-$E000+($2000*<bank>)+!headersize ; org sets the position in the output file to write to (in norom, at least)
|
||||
base <address> ; base sets the position that all labels are relative to - this is necessary so labels will still start from $8000, instead of $0000 or somewhere
|
||||
else
|
||||
if <address> >= $A000
|
||||
org <address>-$A000+($2000*<bank>)+!headersize
|
||||
base <address>
|
||||
else
|
||||
org <address>-$8000+($2000*<bank>)+!headersize
|
||||
base <address>
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
endmacro
|
||||
|
||||
; capcom.....
|
||||
; i can't keep defending you like this
|
||||
|
||||
;P
|
||||
%org($BEBA, $13)
|
||||
RemoveP:
|
||||
db $25
|
||||
;A
|
||||
%org($BD7D, $13)
|
||||
RemoveA:
|
||||
db $25
|
||||
;S
|
||||
%org($BE7D, $13)
|
||||
RemoveS1:
|
||||
db $25
|
||||
;S
|
||||
%org($BDD5, $13)
|
||||
RemoveS2:
|
||||
db $25
|
||||
|
||||
;W
|
||||
%org($BDC7, $13)
|
||||
RemoveW:
|
||||
db $25
|
||||
;O
|
||||
%org($BEC7, $13)
|
||||
RemoveO:
|
||||
db $25
|
||||
;R
|
||||
%org($BDCF, $13)
|
||||
RemoveR:
|
||||
db $25
|
||||
;D
|
||||
%org($BECF, $13)
|
||||
RemoveD:
|
||||
db $25
|
||||
|
||||
%org($A17C, $02)
|
||||
AdjustWeaponRefill:
|
||||
; compare vs unreceived instead. Since the stage ends anyways, this just means you aren't granted the weapon if you don't have it already
|
||||
CMP #$1C
|
||||
BCS WeaponRefillJump
|
||||
|
||||
%org($A18B, $02)
|
||||
WeaponRefillJump:
|
||||
; just as a branch target
|
||||
|
||||
%org($A3BF, $02)
|
||||
FixPseudoSnake:
|
||||
JMP CheckFirstWep
|
||||
NOP
|
||||
|
||||
%org($A3CB, $02)
|
||||
FixPseudoRush:
|
||||
JMP CheckRushWeapon
|
||||
NOP
|
||||
|
||||
%org($BF80, $02)
|
||||
CheckRushWeapon:
|
||||
AND #$01
|
||||
BNE .Rush
|
||||
JMP $A3CF
|
||||
.Rush:
|
||||
LDA $A1
|
||||
CLC
|
||||
ADC $B4
|
||||
TAY
|
||||
LDA $00A2, Y
|
||||
BNE .Skip
|
||||
DEC $A1
|
||||
.Skip:
|
||||
JMP $A477
|
||||
|
||||
; don't even try to go past this point
|
||||
|
||||
%org($802F, $0B)
|
||||
HookBreakMan:
|
||||
JSR SetBreakMan
|
||||
NOP
|
||||
|
||||
%org($90BC, $18)
|
||||
BlockPassword:
|
||||
AND #$08 ; originally 0C, just block down inputs
|
||||
|
||||
%org($9258, $18)
|
||||
HookStageSelect:
|
||||
JSR ChangeStageMode
|
||||
NOP
|
||||
|
||||
%org($92F2, $18)
|
||||
AccessStageTarget:
|
||||
|
||||
%org($9316, $18)
|
||||
AccessStage:
|
||||
JSR RewireDocRobotAccess
|
||||
NOP #2
|
||||
BEQ AccessStageTarget
|
||||
|
||||
%org($9468, $18)
|
||||
HookWeaponGet:
|
||||
JSR WeaponReceived
|
||||
NOP #4
|
||||
|
||||
%org($9917, $18)
|
||||
GameOverStageSelect:
|
||||
; fix it returning to Wily 1
|
||||
CMP #$16
|
||||
|
||||
%org($9966, $18)
|
||||
SwapSelectTiles:
|
||||
; swaps when stage select face tiles should be shown
|
||||
JMP InvertSelectTiles
|
||||
NOP
|
||||
|
||||
%org($9A54, $18)
|
||||
SwapSelectSprites:
|
||||
JMP InvertSelectSprites
|
||||
NOP
|
||||
|
||||
%org($9AFF, $18)
|
||||
BreakManSelect:
|
||||
JSR ApplyLastWily
|
||||
NOP
|
||||
|
||||
%org($BE22, $1D)
|
||||
ConsumableHook:
|
||||
JMP CheckConsumable
|
||||
|
||||
%org($BE32, $1D)
|
||||
EnergyLinkHook:
|
||||
JSR EnergyLink
|
||||
|
||||
%org($A000, $1E)
|
||||
db $21, $A5, $0C, "PLACEHOLDER 1"
|
||||
db $21, $C5, $0C, "PLACEHOLDER 2"
|
||||
db $21, $E5, $0C, "PLACEHOLDER 3"
|
||||
db $22, $05, $0C, "PLACEHOLDER P"
|
||||
db $22, $45, $0C, "PLACEHOLDER 1"
|
||||
db $22, $65, $0C, "PLACEHOLDER 2"
|
||||
db $22, $85, $0C, "PLACEHOLDER 3"
|
||||
db $22, $A5, $0C, "PLACEHOLDER P", $FF
|
||||
db $21, $A5, $0C, "PLACEHOLDER 1"
|
||||
db $21, $C5, $0C, "PLACEHOLDER 2"
|
||||
db $21, $E5, $0C, "PLACEHOLDER 3"
|
||||
db $22, $05, $0C, "PLACEHOLDER P", $FF
|
||||
db $21, $A5, $0C, "PLACEHOLDER 1"
|
||||
db $21, $C5, $0C, "PLACEHOLDER 2"
|
||||
db $21, $E5, $0C, "PLACEHOLDER 3"
|
||||
db $22, $05, $0C, "PLACEHOLDER P", $FF
|
||||
db $21, $A5, $0C, "PLACEHOLDER 1"
|
||||
db $21, $C5, $0C, "PLACEHOLDER 2"
|
||||
db $21, $E5, $0C, "PLACEHOLDER 3"
|
||||
db $22, $05, $0C, "PLACEHOLDER P", $FF
|
||||
db $21, $A5, $0C, "PLACEHOLDER 1"
|
||||
db $21, $C5, $0C, "PLACEHOLDER 2"
|
||||
db $21, $E5, $0C, "PLACEHOLDER 3"
|
||||
db $22, $05, $0C, "PLACEHOLDER P", $FF
|
||||
db $21, $A5, $0C, "PLACEHOLDER 1"
|
||||
db $21, $C5, $0C, "PLACEHOLDER 2"
|
||||
db $21, $E5, $0C, "PLACEHOLDER 3"
|
||||
db $22, $05, $0C, "PLACEHOLDER P", $FF
|
||||
db $21, $A5, $0C, "PLACEHOLDER 1"
|
||||
db $21, $C5, $0C, "PLACEHOLDER 2"
|
||||
db $21, $E5, $0C, "PLACEHOLDER 3"
|
||||
db $22, $05, $0C, "PLACEHOLDER P", $FF
|
||||
db $21, $A5, $0C, "PLACEHOLDER 1"
|
||||
db $21, $C5, $0C, "PLACEHOLDER 2"
|
||||
db $21, $E5, $0C, "PLACEHOLDER 3"
|
||||
db $22, $05, $0C, "PLACEHOLDER P"
|
||||
db $22, $45, $0C, "PLACEHOLDER 1"
|
||||
db $22, $65, $0C, "PLACEHOLDER 2"
|
||||
db $22, $85, $0C, "PLACEHOLDER 3"
|
||||
db $22, $A5, $0C, "PLACEHOLDER P", $FF
|
||||
|
||||
ShowItemString:
|
||||
STY $04
|
||||
LDA ItemLower,X
|
||||
STA $02
|
||||
LDA ItemUpper,X
|
||||
STA $03
|
||||
LDY #$00
|
||||
.LoadString:
|
||||
LDA ($02),Y
|
||||
ORA $10
|
||||
STA $0780,Y
|
||||
BMI .Return
|
||||
INY
|
||||
LDA ($02),Y
|
||||
STA $0780,Y
|
||||
INY
|
||||
LDA ($02),Y
|
||||
STA $0780,Y
|
||||
STA $00
|
||||
INY
|
||||
.LoadCharacters:
|
||||
LDA ($02),Y
|
||||
STA $0780,Y
|
||||
INY
|
||||
DEC $00
|
||||
BPL .LoadCharacters
|
||||
BMI .LoadString
|
||||
.Return:
|
||||
STA $19
|
||||
LDY $04
|
||||
RTS
|
||||
|
||||
ItemUpper:
|
||||
db $A0, $A0, $A0, $A1, $A1, $A1, $A1, $A2, $A2
|
||||
|
||||
ItemLower:
|
||||
db $00, $81, $C2, $03, $44, $85, $C6, $07, $47
|
||||
|
||||
%org($C8F7, $3E)
|
||||
RemoveRushCoil:
|
||||
NOP #4
|
||||
|
||||
%org($CA73, $3E)
|
||||
HookController:
|
||||
JMP ControllerHook
|
||||
NOP
|
||||
|
||||
%org($DA18, $3E)
|
||||
NullWeaponGet:
|
||||
NOP #5 ; TODO: see if I can reroute this write instead for nicer timings
|
||||
|
||||
%org($DB99, $3E)
|
||||
HookMidDoc:
|
||||
JSR SetMidDoc
|
||||
NOP
|
||||
|
||||
%org($DBB0, $3E)
|
||||
HoodEndDoc:
|
||||
JSR SetEndDoc
|
||||
NOP
|
||||
|
||||
%org($DC57, $3E)
|
||||
RerouteStageComplete:
|
||||
LDA $60
|
||||
JSR SetStageComplete
|
||||
NOP #2
|
||||
|
||||
%org($DC6F, $3E)
|
||||
RerouteRushMarine:
|
||||
JMP SetRushMarine
|
||||
NOP
|
||||
|
||||
%org($DC6A, $3E)
|
||||
RerouteRushJet:
|
||||
JMP SetRushJet
|
||||
NOP
|
||||
|
||||
%org($DC78, $3E)
|
||||
RerouteWilyComplete:
|
||||
JMP SetEndWily
|
||||
NOP
|
||||
EndWilyReturn:
|
||||
|
||||
%org($DF81, $3E)
|
||||
NullBreak:
|
||||
NOP #5 ; nop break man giving every weapon
|
||||
|
||||
%org($E15F, $3F)
|
||||
Wily4:
|
||||
JMP Wily4Comparison
|
||||
NOP
|
||||
|
||||
|
||||
%org($F340, $3F)
|
||||
RewireDocRobotAccess:
|
||||
LDA !current_state
|
||||
BNE .DocRobo
|
||||
LDA !received_rbm_stages
|
||||
SEC
|
||||
BCS .Return
|
||||
.DocRobo:
|
||||
LDA !received_doc_stages
|
||||
.Return:
|
||||
AND $9DED,Y
|
||||
RTS
|
||||
|
||||
ChangeStageMode:
|
||||
; also handles hot reload of stage select
|
||||
; kinda broken, sprites don't disappear and palettes go wonky with Break Man access
|
||||
; but like, it functions!
|
||||
LDA !sound_effect_strobe
|
||||
BEQ .Continue
|
||||
JSR $F89A
|
||||
LDA #$00
|
||||
STA !sound_effect_strobe
|
||||
.Continue:
|
||||
LDA $14
|
||||
AND #$20
|
||||
BEQ .Next
|
||||
LDA !current_state
|
||||
BNE .Set
|
||||
LDA !completed_doc_stages
|
||||
CMP #$C5
|
||||
BEQ .BreakMan
|
||||
LDA #$09
|
||||
SEC
|
||||
BCS .Set
|
||||
.EarlyReturn:
|
||||
LDA $14
|
||||
AND #$90
|
||||
RTS
|
||||
.BreakMan:
|
||||
LDA #$12
|
||||
.Set:
|
||||
EOR !current_state
|
||||
STA !current_state
|
||||
LDA #$01
|
||||
STA !rbm_strobe
|
||||
.Next:
|
||||
LDA !rbm_strobe
|
||||
BEQ .EarlyReturn
|
||||
LDA #$00
|
||||
STA !rbm_strobe
|
||||
; Clear the sprite buffer
|
||||
LDX #$98
|
||||
.Loop:
|
||||
LDA #$00
|
||||
STA $01FF, X
|
||||
DEX
|
||||
STA $01FF, X
|
||||
DEX
|
||||
STA $01FF, X
|
||||
DEX
|
||||
LDA #$F8
|
||||
STA $01FF, X
|
||||
DEX
|
||||
CPX #$00
|
||||
BNE .Loop
|
||||
; Break Man Sprites
|
||||
LDX #$24
|
||||
.Loop2:
|
||||
LDA #$00
|
||||
STA $02DB, X
|
||||
DEX
|
||||
STA $02DB, X
|
||||
DEX
|
||||
STA $02DB, X
|
||||
DEX
|
||||
LDA #$F8
|
||||
STA $02DB, X
|
||||
DEX
|
||||
CPX #$00
|
||||
BNE .Loop2
|
||||
; Swap out the tilemap and write sprites
|
||||
LDY #$10
|
||||
LDA $11
|
||||
BMI .B1
|
||||
LDA $FD
|
||||
EOR #$01
|
||||
ASL A
|
||||
ASL A
|
||||
STA $10
|
||||
LDA #$01
|
||||
JSR $E8B4
|
||||
LDA #$00
|
||||
STA $70
|
||||
STA $EE
|
||||
.B3:
|
||||
LDA $10
|
||||
PHA
|
||||
JSR $EF8C
|
||||
PLA
|
||||
STA $10
|
||||
JSR $FF21
|
||||
LDA $70
|
||||
BNE .B3
|
||||
JSR $995C
|
||||
LDX #$03
|
||||
JSR $939E
|
||||
JSR $FF21
|
||||
LDX #$04
|
||||
JSR $939E
|
||||
LDA $FD
|
||||
EOR #$01
|
||||
STA $FD
|
||||
LDY #$00
|
||||
LDA #$7E
|
||||
STA $E9
|
||||
JSR $FF3C
|
||||
.B1:
|
||||
LDX #$00
|
||||
; palettes
|
||||
.B2:
|
||||
LDA $9C33,Y
|
||||
STA $0600,X
|
||||
LDA $9C23,Y
|
||||
STA $0610,X
|
||||
INY
|
||||
INX
|
||||
CPX #$10
|
||||
BNE .B2
|
||||
LDA #$FF
|
||||
STA $18
|
||||
LDA #$01
|
||||
STA $12
|
||||
LDA #$03
|
||||
STA $13
|
||||
LDA $11
|
||||
JSR $99FA
|
||||
LDA $14
|
||||
AND #$90
|
||||
RTS
|
||||
|
||||
InvertSelectTiles:
|
||||
LDY !current_state
|
||||
BNE .DocRobo
|
||||
AND !received_rbm_stages
|
||||
SEC
|
||||
BCS .Compare
|
||||
.DocRobo:
|
||||
AND !received_doc_stages
|
||||
.Compare:
|
||||
BNE .False
|
||||
JMP $996A
|
||||
.False:
|
||||
JMP $99BA
|
||||
|
||||
InvertSelectSprites:
|
||||
LDY !current_state
|
||||
BNE .DocRobo
|
||||
AND !received_rbm_stages
|
||||
SEC
|
||||
BCS .Compare
|
||||
.DocRobo:
|
||||
AND !received_doc_stages
|
||||
.Compare:
|
||||
BNE .False
|
||||
JMP $9A58
|
||||
.False:
|
||||
JMP $9A6D
|
||||
|
||||
SetStageComplete:
|
||||
CMP #$00
|
||||
BNE .DocRobo
|
||||
LDA !completed_rbm_stages
|
||||
ORA $DEC2, Y
|
||||
STA !completed_rbm_stages
|
||||
SEC
|
||||
BCS .Return
|
||||
.DocRobo:
|
||||
LDA !completed_doc_stages
|
||||
ORA $DEC2, Y
|
||||
STA !completed_doc_stages
|
||||
.Return:
|
||||
RTS
|
||||
|
||||
ControllerHook:
|
||||
; Jump in here too for sfx
|
||||
LDA !sound_effect_strobe
|
||||
BEQ .Next
|
||||
JSR $F89A
|
||||
LDA #$00
|
||||
STA !sound_effect_strobe
|
||||
.Next:
|
||||
LDA !controller_mirror
|
||||
CMP !CONTROLLER_ALL_BUTTON
|
||||
BNE .Continue
|
||||
JMP $CBB1
|
||||
.Continue:
|
||||
LDA !controller_flip
|
||||
AND #$10 ; start
|
||||
JMP $CA77
|
||||
|
||||
SetRushMarine:
|
||||
LDA #$01
|
||||
SEC
|
||||
BCS SetRushAcquire
|
||||
|
||||
SetRushJet:
|
||||
LDA #$02
|
||||
SEC
|
||||
BCS SetRushAcquire
|
||||
|
||||
SetRushAcquire:
|
||||
ORA !acquired_rush
|
||||
STA !acquired_rush
|
||||
RTS
|
||||
|
||||
ApplyLastWily:
|
||||
LDA !controller_mirror
|
||||
AND !CONTROLLER_SELECT
|
||||
BEQ .LastWily
|
||||
.Default:
|
||||
LDA #$00
|
||||
SEC
|
||||
BCS .Set
|
||||
.LastWily:
|
||||
LDA !last_wily
|
||||
BEQ .Default
|
||||
SEC
|
||||
SBC #$0C
|
||||
.Set:
|
||||
STA $75 ; wily index
|
||||
LDA #$03
|
||||
STA !current_stage
|
||||
RTS
|
||||
|
||||
SetMidDoc:
|
||||
LDA !current_stage
|
||||
SEC
|
||||
SBC #$08
|
||||
ASL
|
||||
TAY
|
||||
LDA #$01
|
||||
.Loop:
|
||||
CPY #$00
|
||||
BEQ .Return
|
||||
DEY
|
||||
ASL
|
||||
SEC
|
||||
BCS .Loop
|
||||
.Return:
|
||||
ORA !doc_robo_kills
|
||||
STA !doc_robo_kills
|
||||
LDA #$00
|
||||
STA $30
|
||||
RTS
|
||||
|
||||
SetEndDoc:
|
||||
LDA !current_stage
|
||||
SEC
|
||||
SBC #$08
|
||||
ASL
|
||||
TAY
|
||||
INY
|
||||
LDA #$01
|
||||
.Loop:
|
||||
CPY #$00
|
||||
BEQ .Set
|
||||
DEY
|
||||
ASL
|
||||
SEC
|
||||
BCS .Loop
|
||||
.Set:
|
||||
ORA !doc_robo_kills
|
||||
STA !doc_robo_kills
|
||||
.Return:
|
||||
LDA #$0D
|
||||
STA $30
|
||||
RTS
|
||||
|
||||
SetEndWily:
|
||||
LDA !current_wily
|
||||
PHA
|
||||
CLC
|
||||
ADC #$0C
|
||||
STA !last_wily
|
||||
PLA
|
||||
TAX
|
||||
LDA #$01
|
||||
.WLoop:
|
||||
CPX #$00
|
||||
BEQ .WContinue
|
||||
DEX
|
||||
ASL A
|
||||
SEC
|
||||
BCS .WLoop
|
||||
.WContinue:
|
||||
ORA !wily_stage_completion
|
||||
STA !wily_stage_completion
|
||||
INC !current_wily
|
||||
LDA #$9C
|
||||
JMP EndWilyReturn
|
||||
|
||||
|
||||
SetBreakMan:
|
||||
LDA #$80
|
||||
ORA !wily_stage_completion
|
||||
STA !wily_stage_completion
|
||||
LDA #$16
|
||||
STA $22
|
||||
RTS
|
||||
|
||||
CheckFirstWep:
|
||||
LDA $B4
|
||||
BEQ .SetNone
|
||||
TAY
|
||||
.Loop:
|
||||
LDA $00A2,Y
|
||||
BMI .SetNew
|
||||
INY
|
||||
CPY #$0C
|
||||
BEQ .SetSame
|
||||
BCC .Loop
|
||||
.SetSame:
|
||||
LDA #$80
|
||||
STA $A1
|
||||
JMP $A3A1
|
||||
.SetNew:
|
||||
TYA
|
||||
SEC
|
||||
SBC $B4
|
||||
BCS .Set
|
||||
.SetNone:
|
||||
LDA #$00
|
||||
.Set:
|
||||
STA $A1
|
||||
JMP $A3DE
|
||||
|
||||
Wily4Comparison:
|
||||
TYA
|
||||
PHA
|
||||
TXA
|
||||
PHA
|
||||
LDY #$00
|
||||
LDX #$08
|
||||
LDA #$01
|
||||
.Loop:
|
||||
PHA
|
||||
AND $6E
|
||||
BEQ .Skip
|
||||
INY
|
||||
.Skip:
|
||||
PLA
|
||||
ASL
|
||||
DEX
|
||||
BNE .Loop
|
||||
print "Wily 4 Requirement:", hex(realbase())
|
||||
CPY #$08
|
||||
BCC .Return
|
||||
LDA #$FF
|
||||
STA $6E
|
||||
.Return:
|
||||
PLA
|
||||
TAX
|
||||
PLA
|
||||
TAY
|
||||
LDA #$0C
|
||||
STA $EC
|
||||
RTS
|
||||
|
||||
; out of space here :(
|
||||
|
||||
%org($FDBA, $3F)
|
||||
WeaponReceived:
|
||||
TAX
|
||||
LDA $F5
|
||||
PHA
|
||||
LDA #$1E
|
||||
STA $F5
|
||||
JSR $FF6B
|
||||
TXA
|
||||
JSR ShowItemString
|
||||
PLA
|
||||
STA $F5
|
||||
JSR $FF6B
|
||||
RTS
|
||||
|
||||
CheckConsumable:
|
||||
STA $0150, Y
|
||||
LDA $0320, X
|
||||
CMP #$64
|
||||
BMI .Return
|
||||
print "Consumables (replace 67): ", hex(realbase())
|
||||
CMP #$6A
|
||||
BPL .Return
|
||||
LDA #$00
|
||||
STA $0300, X
|
||||
JMP $BE49
|
||||
.Return:
|
||||
JMP $BE25
|
||||
|
||||
EnergyLink:
|
||||
print "Energylink: ", hex(realbase())
|
||||
LDA #$01
|
||||
BEQ .Return
|
||||
TYA
|
||||
STA !energylink_packet
|
||||
LDA #$49
|
||||
STA $00
|
||||
.Return:
|
||||
LDA $BDEC, Y
|
||||
RTS
|
||||
|
||||
; out of room here :(
|
||||
@@ -1,8 +0,0 @@
|
||||
import os
|
||||
|
||||
os.chdir(os.path.dirname(os.path.realpath(__file__)))
|
||||
|
||||
mm3 = bytearray(open("Mega Man 3 (USA).nes", 'rb').read())
|
||||
mm3[0x3C010:0x3C010] = [0] * 0x40000
|
||||
mm3[0x4] = 0x20 # have to do it here, because we don't this in the basepatch itself
|
||||
open("mm3_basepatch.nes", 'wb').write(mm3)
|
||||
@@ -1,5 +0,0 @@
|
||||
from test.bases import WorldTestBase
|
||||
|
||||
|
||||
class MM3TestBase(WorldTestBase):
|
||||
game = "Mega Man 3"
|
||||
@@ -1,105 +0,0 @@
|
||||
from math import ceil
|
||||
|
||||
from .bases import MM3TestBase
|
||||
from ..rules import minimum_weakness_requirement, bosses
|
||||
|
||||
|
||||
# Need to figure out how this test should work
|
||||
def validate_wily_4(base: MM3TestBase) -> None:
|
||||
world = base.multiworld.worlds[base.player]
|
||||
weapon_damage = world.weapon_damage
|
||||
weapon_costs = {
|
||||
0: 0,
|
||||
1: 0.25,
|
||||
2: 2,
|
||||
3: 1,
|
||||
4: 2,
|
||||
5: 7, # Not really, but we can really only rely on Top for one RBM
|
||||
6: 0.5,
|
||||
7: 2,
|
||||
8: 0.5,
|
||||
}
|
||||
boss_health = {boss: 0x1C for boss in range(8)}
|
||||
weapon_energy = {key: float(0x1C) for key in weapon_costs}
|
||||
weapon_boss = {boss: {weapon: world.weapon_damage[weapon][boss] for weapon in world.weapon_damage}
|
||||
for boss in range(8)}
|
||||
flexibility = {
|
||||
boss: (
|
||||
sum(damage_value > 0 for damage_value in
|
||||
weapon_damages.values()) # Amount of weapons that hit this boss
|
||||
* sum(weapon_damages.values()) # Overall damage that those weapons do
|
||||
)
|
||||
for boss, weapon_damages in weapon_boss.items()
|
||||
}
|
||||
boss_flexibility = sorted(flexibility, key=flexibility.get) # Fast way to sort dict by value
|
||||
used_weapons: dict[int, set[int]] = {i: set() for i in range(8)}
|
||||
for boss in boss_flexibility:
|
||||
boss_damage = weapon_boss[boss]
|
||||
weapon_weight = {weapon: (weapon_energy[weapon] / damage) if damage else 0 for weapon, damage in
|
||||
boss_damage.items() if weapon_energy[weapon] > 0}
|
||||
while boss_health[boss] > 0:
|
||||
if boss_damage[0] > 0:
|
||||
boss_health[boss] = 0 # if we can buster, we should buster
|
||||
continue
|
||||
highest, wp = max(zip(weapon_weight.values(), weapon_weight.keys()))
|
||||
uses = weapon_energy[wp] // weapon_costs[wp]
|
||||
used_weapons[boss].add(wp)
|
||||
if int(uses * boss_damage[wp]) > boss_health[boss]:
|
||||
used = ceil(boss_health[boss] / boss_damage[wp])
|
||||
weapon_energy[wp] -= weapon_costs[wp] * used
|
||||
boss_health[boss] = 0
|
||||
elif highest <= 0:
|
||||
# we are out of weapons that can actually damage the boss
|
||||
base.fail(f"Ran out of weapon energy to damage "
|
||||
f"{next(name for name in bosses if bosses[name] == boss)}\n"
|
||||
f"Seed: {base.multiworld.seed}\n"
|
||||
f"Damage Table: {weapon_damage}")
|
||||
else:
|
||||
# drain the weapon and continue
|
||||
boss_health[boss] -= int(uses * boss_damage[wp])
|
||||
weapon_energy[wp] -= weapon_costs[wp] * uses
|
||||
weapon_weight.pop(wp)
|
||||
|
||||
|
||||
class WeaknessTests(MM3TestBase):
|
||||
def test_that_every_boss_has_a_weakness(self) -> None:
|
||||
world = self.multiworld.worlds[self.player]
|
||||
weapon_damage = world.weapon_damage
|
||||
for boss in range(22):
|
||||
if not any(weapon_damage[weapon][boss] >= minimum_weakness_requirement[weapon] for weapon in range(9)):
|
||||
self.fail(f"Boss {boss} generated without weakness! Seed: {self.multiworld.seed}")
|
||||
|
||||
def test_wily_4(self) -> None:
|
||||
validate_wily_4(self)
|
||||
|
||||
|
||||
class StrictWeaknessTests(WeaknessTests):
|
||||
options = {
|
||||
"strict_weakness": True,
|
||||
}
|
||||
|
||||
|
||||
class RandomWeaknessTests(WeaknessTests):
|
||||
options = {
|
||||
"random_weakness": "randomized"
|
||||
}
|
||||
|
||||
|
||||
class ShuffledWeaknessTests(WeaknessTests):
|
||||
options = {
|
||||
"random_weakness": "shuffled"
|
||||
}
|
||||
|
||||
|
||||
class RandomStrictWeaknessTests(WeaknessTests):
|
||||
options = {
|
||||
"strict_weakness": True,
|
||||
"random_weakness": "randomized",
|
||||
}
|
||||
|
||||
|
||||
class ShuffledStrictWeaknessTests(WeaknessTests):
|
||||
options = {
|
||||
"strict_weakness": True,
|
||||
"random_weakness": "shuffled"
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
from collections import defaultdict
|
||||
from typing import DefaultDict
|
||||
|
||||
MM3_WEAPON_ENCODING: DefaultDict[str, int] = defaultdict(lambda: 0x25, {
|
||||
'0': 0x00,
|
||||
'1': 0x01,
|
||||
'2': 0x02,
|
||||
'3': 0x03,
|
||||
'4': 0x04,
|
||||
'5': 0x05,
|
||||
'6': 0x06,
|
||||
'7': 0x07,
|
||||
'8': 0x08,
|
||||
'9': 0x09,
|
||||
'A': 0x0A,
|
||||
'B': 0x0B,
|
||||
'C': 0x0C,
|
||||
'D': 0x0D,
|
||||
'E': 0x0E,
|
||||
'F': 0x0F,
|
||||
'G': 0x10,
|
||||
'H': 0x11,
|
||||
'I': 0x12,
|
||||
'J': 0x13,
|
||||
'K': 0x14,
|
||||
'L': 0x15,
|
||||
'M': 0x16,
|
||||
'N': 0x17,
|
||||
'O': 0x18,
|
||||
'P': 0x19,
|
||||
'Q': 0x1A,
|
||||
'R': 0x1B,
|
||||
'S': 0x1C,
|
||||
'T': 0x1D,
|
||||
'U': 0x1E,
|
||||
'V': 0x1F,
|
||||
'W': 0x20,
|
||||
'X': 0x21,
|
||||
'Y': 0x22,
|
||||
'Z': 0x23,
|
||||
' ': 0x25,
|
||||
'.': 0x26,
|
||||
',': 0x27,
|
||||
'\'': 0x28,
|
||||
'!': 0x29,
|
||||
':': 0x2B
|
||||
})
|
||||
|
||||
|
||||
class MM3TextEntry:
|
||||
def __init__(self, text: str = "", y_coords: int = 0xA5, row: int = 0x21):
|
||||
self.target_area: int = row # don't change
|
||||
self.coords: int = y_coords # 0xYX, Y can only be increments of 0x20
|
||||
self.text: str = text
|
||||
|
||||
def resolve(self) -> bytes:
|
||||
data = bytearray()
|
||||
data.append(self.target_area)
|
||||
data.append(self.coords)
|
||||
data.append(12)
|
||||
data.extend([MM3_WEAPON_ENCODING[x] for x in self.text.upper()])
|
||||
data.extend([0x25] * (13 - len(self.text)))
|
||||
return bytes(data)
|
||||
@@ -28,7 +28,6 @@ class MuseDashCollections:
|
||||
"Miku in Museland", # Paid DLC not included in Muse Plus
|
||||
"Rin Len's Mirrorland", # Paid DLC not included in Muse Plus
|
||||
"MSR Anthology_Vol.02", # Goes away January 26, 2026.
|
||||
"MD-level Tactical Training Blu-ray", # Goes away December 27, 2025.
|
||||
]
|
||||
|
||||
REMOVED_SONGS = [
|
||||
@@ -39,7 +38,6 @@ class MuseDashCollections:
|
||||
"Tsukuyomi Ni Naru Replaced",
|
||||
"Heart Message feat. Aoi Tokimori Secret",
|
||||
"Meow Rock feat. Chun Ge, Yuan Shen",
|
||||
"Stra Stella Secret",
|
||||
]
|
||||
|
||||
song_items = SONG_DATA
|
||||
|
||||
@@ -625,7 +625,7 @@ SONG_DATA: Dict[str, SongData] = {
|
||||
"Synthesis.": SongData(2900749, "83-1", "Cosmic Radio 2024", True, 6, 8, 10),
|
||||
"COSMiC FANFARE!!!!": SongData(2900750, "83-2", "Cosmic Radio 2024", False, 7, 9, 11),
|
||||
"Sharp Bubbles": SongData(2900751, "83-3", "Cosmic Radio 2024", True, 7, 9, 11),
|
||||
"Replay": SongData(2900752, "83-4", "Cosmic Radio 2024", False, 5, 7, 9),
|
||||
"Replay": SongData(2900752, "83-4", "Cosmic Radio 2024", True, 5, 7, 9),
|
||||
"Cosmic Dusty Girl": SongData(2900753, "83-5", "Cosmic Radio 2024", True, 5, 7, 9),
|
||||
"Meow Rock feat. Chun Ge, Yuan Shen": SongData(2900754, "84-0", "Muse Dash・Legend", True, None, None, None),
|
||||
"Even if you make an old radio song with AI": SongData(2900755, "84-1", "Muse Dash・Legend", False, 3, 6, 8),
|
||||
@@ -677,30 +677,4 @@ SONG_DATA: Dict[str, SongData] = {
|
||||
"City Lights": SongData(2900801, "90-3", "MEDIUM5 Echoes", True, 4, 6, 9),
|
||||
"Polaris Wandering Night": SongData(2900802, "90-4", "MEDIUM5 Echoes", True, 5, 8, 10),
|
||||
"Chasing the Moonlight": SongData(2900803, "90-5", "MEDIUM5 Echoes", True, 4, 6, 8),
|
||||
"WILDCARD": SongData(2900804, "91-0", "48 Hours After Discharge", True, 3, 6, 9),
|
||||
"It was all just a dream!": SongData(2900805, "91-1", "48 Hours After Discharge", True, 5, 7, 9),
|
||||
"Science": SongData(2900806, "91-2", "48 Hours After Discharge", False, 4, 7, 9),
|
||||
"Hit Maker": SongData(2900807, "91-3", "48 Hours After Discharge", False, 4, 6, 9),
|
||||
"THX 4 playing": SongData(2900808, "91-4", "48 Hours After Discharge", True, 3, 5, 8),
|
||||
"Theory of Existence": SongData(2900809, "91-5", "48 Hours After Discharge", True, 4, 6, 9),
|
||||
"Kirakira Noel Story!!": SongData(2900810, "43-68", "MD Plus Project", False, 6, 8, 10),
|
||||
"Fantasista LAST END": SongData(2900811, "92-0", "HARDCORE MOTTO TANO*C", True, 7, 9, 11),
|
||||
"Colorful Universe": SongData(2900812, "92-1", "HARDCORE MOTTO TANO*C", True, 3, 6, 9),
|
||||
"Future Flux": SongData(2900813, "92-2", "HARDCORE MOTTO TANO*C", True, 5, 8, 10),
|
||||
"SOMEONE STOP ME!!!": SongData(2900814, "92-3", "HARDCORE MOTTO TANO*C", True, 6, 8, 10),
|
||||
"Azathoth": SongData(2900815, "92-4", "HARDCORE MOTTO TANO*C", True, 6, 8, 10),
|
||||
"Change the Game feat. Iori Matsunaga": SongData(2900816, "92-5", "HARDCORE MOTTO TANO*C", False, 6, 8, 10),
|
||||
"Stra Stella Secret": SongData(2900817, "0-59", "Default Music", False, 6, 8, 10),
|
||||
"Stra Stella": SongData(2900818, "0-60", "Default Music", False, 1, 4, None),
|
||||
"Ultra-Digital Super Detox": SongData(2900819, "43-69", "MD Plus Project", False, 3, 6, 9),
|
||||
"Otsukimi Koete Otsukiai": SongData(2900820, "43-70", "MD Plus Project", True, 6, 8, 10),
|
||||
"Obenkyou Time": SongData(2900821, "43-71", "MD Plus Project", False, 6, 8, 11),
|
||||
"Retry Now": SongData(2900822, "43-72", "MD Plus Project", False, 3, 6, 9),
|
||||
"Master Bancho's Sushi Class ": SongData(2900823, "93-0", "Welcome to the Blue Hole!", False, None, None, None),
|
||||
"CHAOTiC BATTLE": SongData(2900824, "94-0", "Cosmic Radio 2025", False, 7, 9, 11),
|
||||
"FATAL GAME": SongData(2900825, "94-1", "Cosmic Radio 2025", False, 3, 6, 9),
|
||||
"Aria": SongData(2900826, "94-2", "Cosmic Radio 2025", False, 4, 6, 9),
|
||||
"+1 UNKNOWN -NUMBER": SongData(2900827, "94-3", "Cosmic Radio 2025", True, 4, 7, 10),
|
||||
"To the Beyond, from the Nameless Seaside": SongData(2900828, "94-4", "Cosmic Radio 2025", False, 5, 8, 10),
|
||||
"REK421": SongData(2900829, "94-5", "Cosmic Radio 2025", True, 7, 9, 11),
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"game": "Muse Dash",
|
||||
"authors": ["DeamonHunter"],
|
||||
"world_version": "1.5.29",
|
||||
"world_version": "1.5.26",
|
||||
"minimum_ap_version": "0.6.3"
|
||||
}
|
||||
@@ -272,7 +272,7 @@ def patch_rom(world, rom):
|
||||
world_str = ""
|
||||
rom.write_bytes(rom.sym('WORLD_STRING_TXT'), makebytes(world_str, 12))
|
||||
|
||||
time_str = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d %H:%M") + " UTC"
|
||||
time_str = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M") + " UTC"
|
||||
rom.write_bytes(rom.sym('TIME_STRING_TXT'), makebytes(time_str, 25))
|
||||
|
||||
rom.write_byte(rom.sym('CFG_SHOW_SETTING_INFO'), 0x01)
|
||||
|
||||
@@ -7,7 +7,7 @@ Da wir BizHawk benutzen, gilt diese Anleitung nur für Windows und Linux.
|
||||
## Benötigte Software
|
||||
|
||||
- BizHawk: [BizHawk Veröffentlichungen von TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
|
||||
- Version 2.10 und neuer werden unterstützt. Version 2.10 ist empfohlen.
|
||||
- Version 2.3.1 und später werden unterstützt. Version 2.10 ist empfohlen.
|
||||
- Detailierte Installtionsanweisungen für BizHawk können über den obrigen Link gefunden werden.
|
||||
- Windows-Benutzer müssen die Prerequisiten installiert haben. Diese können ebenfalls über
|
||||
den obrigen Link gefunden werden.
|
||||
@@ -19,6 +19,11 @@ Da wir BizHawk benutzen, gilt diese Anleitung nur für Windows und Linux.
|
||||
|
||||
Sobald Bizhawk einmal installiert wurde, öffne **EmuHawk** und ändere die folgenen Einsteluungen:
|
||||
|
||||
- (≤ 2.8) Gehe zu `Config > Customize`. Wechlse zu dem `Advanced`-Reiter, wechsle dann den `Lua Core` von "NLua+KopiLua" zu
|
||||
`"Lua+LuaInterface"`. Starte danach EmuHawk neu. Dies ist zwingend notwendig, damit die Lua-Scripts, mit denen man sich mit dem Client verbindet, ordnungsgemäß funktionieren.
|
||||
**ANMERKUNG: Selbst wenn "Lua+LuaInterface" bereits ausgewählt ist, wechsle zwischen den beiden Optionen umher und**
|
||||
**wähle es erneut aus. Neue Installationen oder Versionen von EmuHawk neigen dazu "Lua+LuaInterface" als die**
|
||||
**Standard-Option anzuzeigen, aber laden dennoch "NLua+KopiLua", bis dieser Schritt getan ist.**
|
||||
- Unter `Config > Customize > Advanced`, gehe sicher dass der Haken bei `AutoSaveRAM` ausgeählt ist, und klicke dann
|
||||
den 5s-Knopf. Dies verringert die Wahrscheinlichkeit den Speicherfrotschritt zu verlieren, sollte der Emulator mal
|
||||
abstürzen.
|
||||
|
||||
@@ -7,7 +7,7 @@ As we are using BizHawk, this guide is only applicable to Windows and Linux syst
|
||||
## Required Software
|
||||
|
||||
- BizHawk: [BizHawk Releases from TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
|
||||
- Version 2.10 and later are supported. Version 2.10 is recommended for stability.
|
||||
- Version 2.3.1 and later are supported. Version 2.10 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).
|
||||
@@ -17,6 +17,11 @@ As we are using BizHawk, this guide is only applicable to Windows and Linux syst
|
||||
|
||||
Once BizHawk has been installed, open EmuHawk and change the following settings:
|
||||
|
||||
- (≤ 2.8) Go to Config > Customize. Switch to the Advanced tab, then switch the Lua Core from "NLua+KopiLua" to
|
||||
"Lua+LuaInterface". Then restart EmuHawk. 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 EmuHawk 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 > Advanced, make sure the box for AutoSaveRAM is checked, and click the 5s button.
|
||||
This reduces the possibility of losing save data in emulator crashes.
|
||||
- Under Config > Customize, check the "Run in background" and "Accept background input" boxes. This will allow you to
|
||||
|
||||
@@ -7,7 +7,7 @@ Comme nous utilisons BizHawk, ce guide s'applique uniquement aux systèmes Windo
|
||||
## Logiciel requis
|
||||
|
||||
- BizHawk : [Sorties BizHawk de TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
|
||||
- Les versions 2.10 et ultérieures sont prises en charge. La version 2.10 est recommandée pour des raisons de stabilité.
|
||||
- Les versions 2.3.1 et ultérieures sont prises en charge. La version 2.10 est recommandée pour des raisons de 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 des prérequis, 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)
|
||||
@@ -18,6 +18,10 @@ Comme nous utilisons BizHawk, ce guide s'applique uniquement aux systèmes Windo
|
||||
|
||||
Une fois BizHawk installé, ouvrez EmuHawk et modifiez les paramètres suivants :
|
||||
|
||||
- (≤ 2,8) Allez dans Config > Personnaliser. Passez à l'onglet Avancé, puis faites passer le Lua Core de "NLua+KopiLua" à
|
||||
"Lua+LuaInterface". Puis redémarrez EmuHawk. 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-la. Nouvelles installations**
|
||||
**des versions plus récentes d'EmuHawk ont tendance à afficher "Lua+LuaInterface" comme option sélectionnée par défaut mais ce pendant refait l'épate juste au dessus par précautions**
|
||||
- Sous Config > Personnaliser > Avancé, assurez-vous que la case 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 crash 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 continuez à jouer en arrière-plan, même si une autre fenêtre est sélectionnée.
|
||||
|
||||
@@ -123,7 +123,6 @@ class PokemonEmeraldWorld(World):
|
||||
blacklisted_wilds: Set[int]
|
||||
blacklisted_starters: Set[int]
|
||||
blacklisted_opponent_pokemon: Set[int]
|
||||
allowed_dexsanity_species: set[int]
|
||||
hm_requirements: Dict[str, Union[int, List[str]]]
|
||||
auth: bytes
|
||||
|
||||
@@ -143,7 +142,6 @@ class PokemonEmeraldWorld(World):
|
||||
self.blacklisted_wilds = set()
|
||||
self.blacklisted_starters = set()
|
||||
self.blacklisted_opponent_pokemon = set()
|
||||
self.allowed_dexsanity_species = set()
|
||||
self.modified_maps = copy.deepcopy(emerald_data.maps)
|
||||
self.modified_species = copy.deepcopy(emerald_data.species)
|
||||
self.modified_tmhm_moves = []
|
||||
@@ -267,7 +265,6 @@ class PokemonEmeraldWorld(World):
|
||||
from .regions import create_regions
|
||||
all_regions = create_regions(self)
|
||||
|
||||
randomize_wild_encounters(self)
|
||||
# Categories with progression items always included
|
||||
categories = {
|
||||
LocationCategory.BADGE,
|
||||
@@ -497,6 +494,7 @@ class PokemonEmeraldWorld(World):
|
||||
set_rules(self)
|
||||
|
||||
def connect_entrances(self):
|
||||
randomize_wild_encounters(self)
|
||||
self.shuffle_badges_hms()
|
||||
# For entrance randomization, disconnect entrances here, randomize map, then
|
||||
# undo badge/HM placement and re-shuffle them in the new map.
|
||||
|
||||
@@ -110,7 +110,7 @@ def create_locations_by_category(world: "PokemonEmeraldWorld", regions: Dict[str
|
||||
national_dex_id = int(location_name[-3:]) # Location names are formatted POKEDEX_REWARD_###
|
||||
|
||||
# Don't create this pokedex location if player can't find it in the wild
|
||||
if NATIONAL_ID_TO_SPECIES_ID[national_dex_id] in world.blacklisted_wilds or NATIONAL_ID_TO_SPECIES_ID[national_dex_id] not in world.allowed_dexsanity_species:
|
||||
if NATIONAL_ID_TO_SPECIES_ID[national_dex_id] in world.blacklisted_wilds:
|
||||
continue
|
||||
|
||||
location_id += POKEDEX_OFFSET + national_dex_id
|
||||
|
||||
@@ -63,7 +63,7 @@ def randomize_opponent_parties(world: "PokemonEmeraldWorld") -> None:
|
||||
if len(merged_blacklist) < NUM_REAL_SPECIES:
|
||||
break
|
||||
else:
|
||||
merged_blacklist: Set[int] = set()
|
||||
raise RuntimeError("This should never happen")
|
||||
|
||||
candidates = [
|
||||
species
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user