mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-06-12 03:16:17 -07:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e85f190dfd | |||
| 775f56036c | |||
| 39342ad5d5 | |||
| ce09144261 |
@@ -46,6 +46,7 @@ dist
|
||||
/prof/
|
||||
README.html
|
||||
.vs/
|
||||
EnemizerCLI/
|
||||
/Players/
|
||||
/SNI/
|
||||
/sni-*/
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
- name: Set env
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV # tag x.y.z will become "Archipelago x.y.z"
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
||||
uses: softprops/action-gh-release@975c1b265e11dd76618af1c374e7981f9a6ff44a
|
||||
with:
|
||||
draft: true # don't publish right away, especially since windows build is added by hand
|
||||
prerelease: false
|
||||
@@ -97,15 +97,13 @@ jobs:
|
||||
build/exe.*/ArchipelagoServer.exe
|
||||
setups/*
|
||||
- name: Add to Release
|
||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
||||
uses: softprops/action-gh-release@975c1b265e11dd76618af1c374e7981f9a6ff44a
|
||||
with:
|
||||
draft: true # see above
|
||||
prerelease: false
|
||||
name: Archipelago ${{ env.RELEASE_VERSION }}
|
||||
files: |
|
||||
setups/*
|
||||
fail_on_unmatched_files: true
|
||||
overwrite_files: false # Windows release is usually built by hand
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -167,14 +165,12 @@ jobs:
|
||||
build/exe.*/ArchipelagoServer
|
||||
dist/*
|
||||
- name: Add to Release
|
||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
||||
uses: softprops/action-gh-release@975c1b265e11dd76618af1c374e7981f9a6ff44a
|
||||
with:
|
||||
draft: true # see above
|
||||
prerelease: false
|
||||
name: Archipelago ${{ env.RELEASE_VERSION }}
|
||||
files: |
|
||||
dist/*
|
||||
fail_on_unmatched_files: true
|
||||
overwrite_files: false # should never happen; avoids accidentally changing a release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
+2
-9
@@ -374,7 +374,8 @@ class MultiWorld():
|
||||
items_to_add.append(AutoWorld.call_single(self, "create_item", item_player,
|
||||
group["replacement_items"][player]))
|
||||
else:
|
||||
items_to_add.append(AutoWorld.call_single(self, "create_filler", item_player))
|
||||
items_to_add.append(AutoWorld.call_single(self, "create_filler", item_player,
|
||||
AutoWorld.FillerReason.item_link))
|
||||
self.random.shuffle(items_to_add)
|
||||
self.itempool.extend(items_to_add[:itemcount - len(self.itempool)])
|
||||
|
||||
@@ -1038,8 +1039,6 @@ class CollectionState():
|
||||
|
||||
def has_from_list(self, items: Iterable[str], player: int, count: int) -> bool:
|
||||
"""Returns True if the state contains at least `count` items matching any of the item names from a list."""
|
||||
if count <= 0:
|
||||
return True
|
||||
found: int = 0
|
||||
player_prog_items = self.prog_items[player]
|
||||
for item_name in items:
|
||||
@@ -1051,8 +1050,6 @@ class CollectionState():
|
||||
def has_from_list_unique(self, items: Iterable[str], player: int, count: int) -> bool:
|
||||
"""Returns True if the state contains at least `count` items matching any of the item names from a list.
|
||||
Ignores duplicates of the same item."""
|
||||
if count <= 0:
|
||||
return True
|
||||
found: int = 0
|
||||
player_prog_items = self.prog_items[player]
|
||||
for item_name in items:
|
||||
@@ -1081,8 +1078,6 @@ class CollectionState():
|
||||
# item name group related
|
||||
def has_group(self, item_name_group: str, player: int, count: int = 1) -> bool:
|
||||
"""Returns True if the state contains at least `count` items present in a specified item group."""
|
||||
if count <= 0:
|
||||
return True
|
||||
found: int = 0
|
||||
player_prog_items = self.prog_items[player]
|
||||
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]:
|
||||
@@ -1095,8 +1090,6 @@ class CollectionState():
|
||||
"""Returns True if the state contains at least `count` items present in a specified item group.
|
||||
Ignores duplicates of the same item.
|
||||
"""
|
||||
if count <= 0:
|
||||
return True
|
||||
found: int = 0
|
||||
player_prog_items = self.prog_items[player]
|
||||
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]:
|
||||
|
||||
+1
-5
@@ -1069,7 +1069,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
||||
if "players" in args:
|
||||
ctx.consume_players_package(args["players"])
|
||||
if "hint_points" in args:
|
||||
ctx.hint_points = args["hint_points"]
|
||||
ctx.hint_points = args['hint_points']
|
||||
if "checked_locations" in args:
|
||||
checked = set(args["checked_locations"])
|
||||
ctx.checked_locations |= checked
|
||||
@@ -1077,10 +1077,6 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
||||
if "permissions" in args:
|
||||
ctx.update_permissions(args["permissions"])
|
||||
|
||||
# Update hint info for local display
|
||||
if "hint_cost" in args:
|
||||
ctx.hint_cost = int(args["hint_cost"])
|
||||
|
||||
elif cmd == 'Print':
|
||||
ctx.on_print(args)
|
||||
|
||||
|
||||
+27
@@ -1,5 +1,23 @@
|
||||
# hadolint global ignore=SC1090,SC1091
|
||||
|
||||
# Source
|
||||
FROM scratch AS release
|
||||
WORKDIR /release
|
||||
ADD https://github.com/Ijwu/Enemizer/releases/latest/download/ubuntu.16.04-x64.zip Enemizer.zip
|
||||
|
||||
# Enemizer
|
||||
FROM alpine:3.21 AS enemizer
|
||||
ARG TARGETARCH
|
||||
WORKDIR /release
|
||||
COPY --from=release /release/Enemizer.zip .
|
||||
|
||||
# No release for arm architecture. Skip.
|
||||
RUN if [ "$TARGETARCH" = "amd64" ]; then \
|
||||
apk add unzip=6.0-r15 --no-cache && \
|
||||
unzip -u Enemizer.zip -d EnemizerCLI && \
|
||||
chmod -R 777 EnemizerCLI; \
|
||||
else touch EnemizerCLI; fi
|
||||
|
||||
# Cython builder stage
|
||||
FROM python:3.12 AS cython-builder
|
||||
|
||||
@@ -63,6 +81,15 @@ RUN apt-get purge -y \
|
||||
g++ && \
|
||||
apt-get autoremove -y
|
||||
|
||||
# Copy necessary components
|
||||
COPY --from=enemizer /release/EnemizerCLI /tmp/EnemizerCLI
|
||||
|
||||
# No release for arm architecture. Skip.
|
||||
RUN if [ "$TARGETARCH" = "amd64" ]; then \
|
||||
cp -r /tmp/EnemizerCLI EnemizerCLI; \
|
||||
fi; \
|
||||
rm -rf /tmp/EnemizerCLI
|
||||
|
||||
# Define health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||
CMD curl -f http://localhost:${PORT:-80} || exit 1
|
||||
|
||||
@@ -7,7 +7,7 @@ from collections import Counter, deque
|
||||
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, PlandoItemBlock
|
||||
from Options import Accessibility
|
||||
|
||||
from worlds.AutoWorld import call_all
|
||||
from worlds.AutoWorld import call_all, FillerReason
|
||||
from worlds.generic.Rules import add_item_rule
|
||||
|
||||
|
||||
@@ -340,7 +340,7 @@ def remaining_fill(multiworld: MultiWorld,
|
||||
for item in unplaced_items:
|
||||
logging.debug(f"Moved {item} to start_inventory to prevent fill failure.")
|
||||
multiworld.push_precollected(item)
|
||||
last_batch.append(multiworld.worlds[item.player].create_filler())
|
||||
last_batch.append(multiworld.worlds[item.player].create_filler(FillerReason.panic_fill))
|
||||
remaining_fill(multiworld, locations, unplaced_items, name + " Start Inventory Retry")
|
||||
else:
|
||||
raise FillError(f"No more spots to place {len(unplaced_items)} items. Remaining locations are invalid.\n"
|
||||
@@ -599,7 +599,7 @@ def distribute_items_restrictive(multiworld: MultiWorld,
|
||||
for item in progitempool:
|
||||
logging.debug(f"Moved {item} to start_inventory to prevent fill failure.")
|
||||
multiworld.push_precollected(item)
|
||||
filleritempool.append(multiworld.worlds[item.player].create_filler())
|
||||
filleritempool.append(multiworld.worlds[item.player].create_filler(FillerReason.panic_fill))
|
||||
logging.warning(f"{len(progitempool)} items moved to start inventory,"
|
||||
f" due to failure in Progression fill step.")
|
||||
progitempool[:] = []
|
||||
@@ -623,7 +623,7 @@ def distribute_items_restrictive(multiworld: MultiWorld,
|
||||
inaccessible_location_rules(multiworld, multiworld.state, defaultlocations)
|
||||
|
||||
remaining_fill(multiworld, excludedlocations, filleritempool, "Remaining Excluded",
|
||||
move_unplaceable_to_start_inventory=panic_method=="start_inventory")
|
||||
move_unplaceable_to_start_inventory=panic_method == "start_inventory")
|
||||
|
||||
if excludedlocations:
|
||||
raise FillError(
|
||||
@@ -635,7 +635,7 @@ def distribute_items_restrictive(multiworld: MultiWorld,
|
||||
restitempool = filleritempool + usefulitempool
|
||||
|
||||
remaining_fill(multiworld, defaultlocations, restitempool,
|
||||
move_unplaceable_to_start_inventory=panic_method=="start_inventory")
|
||||
move_unplaceable_to_start_inventory=panic_method == "start_inventory")
|
||||
|
||||
unplaced = restitempool
|
||||
unfilled = defaultlocations
|
||||
|
||||
+2
-13
@@ -40,8 +40,6 @@ def mystery_argparse(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
parser.add_argument('--spoiler', type=int, default=defaults.spoiler)
|
||||
parser.add_argument('--outputpath', default=settings.general_options.output_path,
|
||||
help="Path to output folder. Absolute or relative to cwd.") # absolute or relative to cwd
|
||||
parser.add_argument('--allow_quantity', action="store_true", default=defaults.allow_quantity,
|
||||
help='Allows the use of the quantity option in yamls. Default is the set value in the host.yaml.')
|
||||
parser.add_argument('--race', action='store_true', default=defaults.race)
|
||||
parser.add_argument('--meta_file_path', default=defaults.meta_file_path)
|
||||
parser.add_argument('--log_level', default=defaults.loglevel, help='Sets log level')
|
||||
@@ -125,7 +123,6 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
|
||||
player_id: int = 1
|
||||
player_files: dict[int, str] = {}
|
||||
player_errors: list[str] = []
|
||||
allow_quantity = args.allow_quantity
|
||||
for file in os.scandir(args.player_files_path):
|
||||
fname = file.name
|
||||
if file.is_file() and not fname.startswith(".") and not fname.lower().endswith(".ini") and \
|
||||
@@ -137,14 +134,7 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
|
||||
if yaml is None:
|
||||
logging.warning(f"Ignoring empty yaml document #{doc_idx + 1} in {fname}")
|
||||
else:
|
||||
quantity = yaml.get("quantity", 1)
|
||||
if quantity <= 0:
|
||||
raise ValueError("A quantity of 0 or less is invalid. Please change it to at least 1.")
|
||||
if not allow_quantity and quantity > 1:
|
||||
raise ValueError("Quantity greater than 1 is deactivated by host settings.")
|
||||
|
||||
for _ in range(quantity):
|
||||
weights_for_file.append(yaml)
|
||||
weights_for_file.append(yaml)
|
||||
weights_cache[fname] = tuple(weights_for_file)
|
||||
|
||||
except Exception as e:
|
||||
@@ -585,8 +575,7 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
|
||||
raise Exception(f"Invalid game: {ret.game}")
|
||||
if ret.game not in AutoWorldRegister.world_types:
|
||||
from worlds import failed_world_loads
|
||||
picks = Utils.get_fuzzy_results(ret.game, list(AutoWorldRegister.world_types) + list(failed_world_loads.keys()),
|
||||
limit=1)[0]
|
||||
picks = Utils.get_fuzzy_results(ret.game, list(AutoWorldRegister.world_types) + failed_world_loads, limit=1)[0]
|
||||
if picks[0] in failed_world_loads:
|
||||
raise Exception(f"No functional world found to handle game {ret.game}. "
|
||||
f"Did you mean '{picks[0]}' ({picks[1]}% sure)? "
|
||||
|
||||
-35
@@ -36,7 +36,6 @@ if __name__ == "__main__":
|
||||
init_logging('Launcher')
|
||||
|
||||
from worlds.LauncherComponents import Component, components, icon_paths, SuffixIdentifier, Type
|
||||
from worlds import failed_world_loads
|
||||
|
||||
|
||||
def open_host_yaml():
|
||||
@@ -276,7 +275,6 @@ def run_gui(launch_components: list[Component], args: Any) -> None:
|
||||
search_box: MDTextField = ObjectProperty(None)
|
||||
cards: list[LauncherCard]
|
||||
current_filter: Sequence[str | Type] | None
|
||||
failed_worlds: bool = bool(failed_world_loads)
|
||||
|
||||
def __init__(self, ctx=None, components=None, args=None):
|
||||
self.title = self.base_title + " " + Utils.__version__
|
||||
@@ -424,39 +422,6 @@ def run_gui(launch_components: list[Component], args: Any) -> None:
|
||||
MDSnackbar(MDSnackbarText(text=open_text), y=dp(24), pos_hint={"center_x": 0.5},
|
||||
size_hint_x=0.5).open()
|
||||
|
||||
@staticmethod
|
||||
def copy_to_clipboard(text):
|
||||
from kivy.core.clipboard import Clipboard
|
||||
Clipboard.copy(text)
|
||||
MDSnackbar(MDSnackbarText(text="Copied to clipboard."), y=dp(24), pos_hint={"center_x": 0.5},
|
||||
size_hint_x=0.5).open()
|
||||
|
||||
def display_failed(self):
|
||||
"""Display a dialog showing the exceptions produced by any world that failed to load during
|
||||
initialization."""
|
||||
if not self.failed_worlds:
|
||||
return
|
||||
from kivymd.uix.dialog import MDDialog, MDDialogIcon, MDDialogHeadlineText, MDDialogContentContainer
|
||||
from kivymd.uix.divider import MDDivider
|
||||
from kivymd.uix.list import MDListItem, MDListItemHeadlineText, MDListItemSupportingText
|
||||
entries = []
|
||||
for world, reason in failed_world_loads.items():
|
||||
entries.append(MDListItem(
|
||||
MDListItemHeadlineText(text=world),
|
||||
MDListItemSupportingText(text=reason),
|
||||
on_release=lambda x, r=reason: self.copy_to_clipboard(r)
|
||||
))
|
||||
dialog = MDDialog(
|
||||
MDDialogIcon(icon="alert"),
|
||||
MDDialogHeadlineText(text="Failed World Loads"),
|
||||
MDDialogContentContainer(
|
||||
MDDivider(),
|
||||
*entries,
|
||||
orientation="vertical",
|
||||
)
|
||||
)
|
||||
dialog.open()
|
||||
|
||||
def _on_drop_file(self, window: Window, filename: bytes, x: int, y: int) -> None:
|
||||
""" When a patch file is dropped into the window, run the associated component. """
|
||||
file, component = identify(filename.decode())
|
||||
|
||||
+2
-2
@@ -241,8 +241,8 @@ async def gba_sync_task(ctx: MMBN3Context):
|
||||
await ctx.server_auth(False)
|
||||
else:
|
||||
if not ctx.version_warning:
|
||||
logger.warning(f"Your Lua script is version {reported_version}, expected {script_version}. "
|
||||
"Please update to the latest version. "
|
||||
logger.warning(f"Your Lua script is version {reported_version}, expected {script_version}."
|
||||
"Please update to the latest version."
|
||||
"Your connection to the Archipelago server will not be accepted.")
|
||||
ctx.version_warning = True
|
||||
except asyncio.TimeoutError:
|
||||
|
||||
@@ -174,7 +174,8 @@ def main(args, seed=None, baked_server_options: dict[str, object] | None = None)
|
||||
logger.warning(f"{player_name} tried to remove items from their pool that don't exist: {unfound_items}")
|
||||
|
||||
needed_items = target_per_player[player] - sum(unfound_items.values())
|
||||
new_itempool += [multiworld.worlds[player].create_filler() for _ in range(needed_items)]
|
||||
new_itempool += [multiworld.worlds[player].create_filler(AutoWorld.FillerReason.start_inventory_from_pool)
|
||||
for _ in range(needed_items)]
|
||||
|
||||
assert len(multiworld.itempool) == len(new_itempool), "Item Pool amounts should not change."
|
||||
multiworld.itempool[:] = new_itempool
|
||||
|
||||
+2
-2
@@ -2633,8 +2633,8 @@ def parse_args() -> argparse.Namespace:
|
||||
goal: !remaining can be used after goal completion
|
||||
''')
|
||||
parser.add_argument('--auto_shutdown', default=defaults["auto_shutdown"], type=int,
|
||||
help="automatically shut down the server after this many seconds without new location checks. "
|
||||
"0 to keep running.")
|
||||
help="automatically shut down the server after this many minutes without new location checks. "
|
||||
"0 to keep running. Not yet implemented.")
|
||||
parser.add_argument('--use_embedded_options', action="store_true",
|
||||
help='retrieve release, remaining and hint options from the multidata file,'
|
||||
' instead of host.yaml')
|
||||
|
||||
@@ -527,11 +527,7 @@ else:
|
||||
except ImportError:
|
||||
pyximport = None
|
||||
try:
|
||||
import logging
|
||||
logger = logging.getLogger()
|
||||
old_level = logger.level
|
||||
from _speedups import LocationStore
|
||||
logger.setLevel(old_level)
|
||||
except ImportError:
|
||||
warnings.warn("_speedups not available. Falling back to pure python LocationStore. "
|
||||
"Install a matching C++ compiler for your platform to compile _speedups.")
|
||||
|
||||
+22
-25
@@ -1469,7 +1469,7 @@ class NonLocalItems(ItemSet):
|
||||
|
||||
|
||||
class StartInventory(ItemDict):
|
||||
"""Start with the specified amount of these items. Example: {Bomb: 1, Arrow: 3} """
|
||||
"""Start with the specified amount of these items. Example: "Bomb: 1" """
|
||||
verify_item_name = True
|
||||
display_name = "Start Inventory"
|
||||
rich_text_doc = True
|
||||
@@ -1477,7 +1477,7 @@ class StartInventory(ItemDict):
|
||||
|
||||
|
||||
class StartInventoryPool(StartInventory):
|
||||
"""Start with the specified amount of these items and don't place them in the world. Example: {Bomb: 1, Arrow: 3}
|
||||
"""Start with the specified amount of these items and don't place them in the world. Example: "Bomb: 1"
|
||||
|
||||
The game decides what the replacement items will be.
|
||||
"""
|
||||
@@ -1856,30 +1856,27 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
|
||||
|
||||
for game_name, world in AutoWorldRegister.world_types.items():
|
||||
if not world.hidden or generate_hidden:
|
||||
try:
|
||||
presets = world.web.options_presets.copy()
|
||||
presets.update({"": {}})
|
||||
presets = world.web.options_presets.copy()
|
||||
presets.update({"": {}})
|
||||
|
||||
option_groups = get_option_groups(world)
|
||||
for name, preset in presets.items():
|
||||
res = template.render(
|
||||
option_groups=option_groups,
|
||||
__version__=__version__,
|
||||
game=game_name,
|
||||
world_version=world.world_version.as_simple_string(),
|
||||
yaml_dump=yaml_dump_scalar,
|
||||
dictify_range=dictify_range,
|
||||
cleandoc=cleandoc,
|
||||
preset_name=name,
|
||||
preset=preset,
|
||||
)
|
||||
preset_name = f" - {name}" if name else ""
|
||||
with open(os.path.join(preset_folder if name else target_folder,
|
||||
get_file_safe_name(game_name + preset_name) + ".yaml"),
|
||||
"w", encoding="utf-8-sig") as f:
|
||||
f.write(res)
|
||||
except Exception as ex:
|
||||
raise Exception(f"Template generation failed for world {game_name}") from ex
|
||||
option_groups = get_option_groups(world)
|
||||
for name, preset in presets.items():
|
||||
res = template.render(
|
||||
option_groups=option_groups,
|
||||
__version__=__version__,
|
||||
game=game_name,
|
||||
world_version=world.world_version.as_simple_string(),
|
||||
yaml_dump=yaml_dump_scalar,
|
||||
dictify_range=dictify_range,
|
||||
cleandoc=cleandoc,
|
||||
preset_name=name,
|
||||
preset=preset,
|
||||
)
|
||||
preset_name = f" - {name}" if name else ""
|
||||
with open(os.path.join(preset_folder if name else target_folder,
|
||||
get_file_safe_name(game_name + preset_name) + ".yaml"),
|
||||
"w", encoding="utf-8-sig") as f:
|
||||
f.write(res)
|
||||
|
||||
|
||||
def dump_player_options(multiworld: MultiWorld) -> None:
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ class UndertaleCommandProcessor(ClientCommandProcessor):
|
||||
if isinstance(self.ctx, UndertaleContext):
|
||||
os.makedirs(name=Utils.user_path("Undertale"), exist_ok=True)
|
||||
tempInstall = steaminstall
|
||||
if tempInstall and not os.path.isfile(os.path.join(tempInstall, "data.win")):
|
||||
if not os.path.isfile(os.path.join(tempInstall, "data.win")):
|
||||
tempInstall = None
|
||||
if tempInstall is None:
|
||||
tempInstall = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"
|
||||
|
||||
@@ -52,7 +52,7 @@ class Version(typing.NamedTuple):
|
||||
return ".".join(str(item) for item in self)
|
||||
|
||||
|
||||
__version__ = "0.6.8"
|
||||
__version__ = "0.6.7"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
is_linux = sys.platform.startswith("linux")
|
||||
|
||||
@@ -48,8 +48,6 @@ app.config["JOB_THRESHOLD"] = 1
|
||||
app.config["JOB_TIME"] = 600
|
||||
# maximum time in seconds since last activity for a room to be hosted
|
||||
app.config["MAX_ROOM_TIMEOUT"] = 259200
|
||||
# minimum time in days since last activity for a room to be deleted. 0 to disable.
|
||||
app.config["ROOM_AUTO_DELETE"] = 0
|
||||
# memory limit for generator processes in bytes
|
||||
app.config["GENERATOR_MEMORY_LIMIT"] = 4294967296
|
||||
|
||||
|
||||
@@ -100,18 +100,13 @@ def init_generator(config: dict[str, Any]) -> None:
|
||||
db.generate_mapping()
|
||||
|
||||
|
||||
def cleanup(config: dict[str, Any]):
|
||||
"""delete unowned or old user-content"""
|
||||
auto_delete: int = config.get("ROOM_AUTO_DELETE", 0)
|
||||
def cleanup():
|
||||
"""delete unowned user-content"""
|
||||
with db_session:
|
||||
# >>> bool(uuid.UUID(int=0))
|
||||
# True
|
||||
rooms = Room.select(lambda room: room.owner == UUID(int=0)).delete(bulk=True)
|
||||
seeds = Seed.select(lambda seed: seed.owner == UUID(int=0) and not seed.rooms).delete(bulk=True)
|
||||
if auto_delete > 0:
|
||||
cutoff = utcnow() - timedelta(days=auto_delete)
|
||||
rooms += Room.select(lambda room: room.last_activity < cutoff).delete(bulk=True)
|
||||
seeds += Seed.select(lambda seed: not seed.rooms and seed.creation_time < cutoff).delete(bulk=True)
|
||||
slots = Slot.select(lambda slot: not slot.seed).delete(bulk=True)
|
||||
# Command gets deleted by ponyorm Cascade Delete, as Room is Required
|
||||
if rooms or seeds or slots:
|
||||
@@ -123,7 +118,7 @@ def autohost(config: dict):
|
||||
stop_event = _stop_event
|
||||
try:
|
||||
with Locker("autohost"):
|
||||
cleanup(config)
|
||||
cleanup()
|
||||
hosters = []
|
||||
for x in range(config["HOSTERS"]):
|
||||
hoster = MultiworldInstance(config, x)
|
||||
|
||||
@@ -10,5 +10,5 @@ Flask-Cors==6.0.2
|
||||
bokeh==3.8.2
|
||||
markupsafe==3.0.3
|
||||
setproctitle==1.3.7
|
||||
mistune==3.2.1
|
||||
mistune==3.2.0
|
||||
docutils==0.22.4
|
||||
|
||||
@@ -123,26 +123,12 @@ window.addEventListener('load', () => {
|
||||
});
|
||||
|
||||
const addRangeRow = (optionName) => {
|
||||
const inputQuery = `input[data-option="${optionName}"]`;
|
||||
const inputQuery = `input[type=number][data-option="${optionName}"].range-option-value`;
|
||||
const inputTarget = document.querySelector(inputQuery);
|
||||
const newValue = inputTarget.value;
|
||||
switch (inputTarget.type) {
|
||||
case 'number':
|
||||
if (!/^-?\d+$/.test(newValue)) {
|
||||
alert('Range values must be a positive or negative integer!');
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case 'text':
|
||||
if (newValue === "") {
|
||||
alert('Range values for text must be a non-empty string!');
|
||||
return;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.error(`Found unsupported input type: ${inputTarget.type}`);
|
||||
return;
|
||||
break;
|
||||
if (!/^-?\d+$/.test(newValue)) {
|
||||
alert('Range values must be a positive or negative integer!');
|
||||
return;
|
||||
}
|
||||
inputTarget.value = '';
|
||||
const tBody = document.querySelector(`table[data-option="${optionName}"].range-rows tbody`);
|
||||
|
||||
@@ -42,7 +42,3 @@
|
||||
#games .page-controls button{
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
#games .author-label{
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
@@ -49,13 +49,6 @@
|
||||
<details data-game="{{ game_name }}">
|
||||
<summary class="h2">{{ game_name }}</summary>
|
||||
{{ world.__doc__ | default("No description provided.", true) }}<br />
|
||||
{% if "authors" in world.manifest %}
|
||||
{% if world.manifest["authors"]|length == 1 %}
|
||||
<p class="author-label">Author: {{ world.manifest["authors"][0] }}</p>
|
||||
{% else %}
|
||||
<p class="author-label">Authors: {{ world.manifest["authors"] | join(", ") }}</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<a href="{{ url_for("game_info", game=game_name, lang="en") }}">Game Page</a>
|
||||
{% if world.web.tutorials %}
|
||||
<span class="link-spacer">|</span>
|
||||
|
||||
@@ -71,10 +71,10 @@
|
||||
<div class="hint-text">
|
||||
This option allows custom values only. Please enter your desired values below.
|
||||
<div class="custom-value-wrapper">
|
||||
<input type="text" class="custom-value" data-option="{{ option_name }}" placeholder="Custom Value" />
|
||||
<button type="button" class="add-range-option-button" data-option="{{ option_name }}">Add</button>
|
||||
<input class="custom-value" data-option="{{ option_name }}" placeholder="Custom Value" />
|
||||
<button type="button" data-option="{{ option_name }}">Add</button>
|
||||
</div>
|
||||
<table class="range-rows" data-option="{{ option_name }}">
|
||||
<table>
|
||||
<tbody>
|
||||
{% if option.default %}
|
||||
{{ RangeRow(option_name, option, option.default, option.default) }}
|
||||
@@ -88,11 +88,11 @@
|
||||
<div class="hint-text">
|
||||
Custom values are also allowed for this option. To create one, enter it into the input box below.
|
||||
<div class="custom-value-wrapper">
|
||||
<input type="text" class="custom-value" data-option="{{ option_name }}" placeholder="Custom Value" />
|
||||
<button type="button" class="add-range-option-button" data-option="{{ option_name }}">Add</button>
|
||||
<input class="custom-value" data-option="{{ option_name }}" placeholder="Custom Value" />
|
||||
<button type="button" data-option="{{ option_name }}">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="range-rows" data-option="{{ option_name }}">
|
||||
<table>
|
||||
<tbody>
|
||||
{% for id, name in option.name_lookup.items() %}
|
||||
{% if name != 'random' %}
|
||||
|
||||
@@ -140,15 +140,6 @@ MDFloatLayout:
|
||||
|
||||
MDNavigationDrawerDivider:
|
||||
|
||||
MDBoxLayout:
|
||||
orientation: "horizontal"
|
||||
MDIconButton:
|
||||
icon: "alert" if app.failed_worlds else ""
|
||||
theme_text_color: "Custom"
|
||||
text_color: "D23C42"
|
||||
disabled: not app.failed_worlds
|
||||
on_release: app.display_failed()
|
||||
|
||||
|
||||
MDGridLayout:
|
||||
id: main_layout
|
||||
|
||||
@@ -92,9 +92,8 @@ for setup).
|
||||
|
||||
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
|
||||
regarding the API can be found in the [world api doc](/docs/world%20api.md), and the [APQuest](/worlds/apquest/) world
|
||||
is a complete world implementation that functions as an introduction to world development. Before publishing, make sure
|
||||
to also check out [world maintainer.md](/docs/world%20maintainer.md).
|
||||
regarding the API can be found in the [world api doc](/docs/world%20api.md). Before publishing, make sure to also
|
||||
check out [world maintainer.md](/docs/world%20maintainer.md).
|
||||
|
||||
### Hard Requirements
|
||||
|
||||
|
||||
@@ -35,8 +35,8 @@ There are also the following optional fields:
|
||||
* `world_version` - an arbitrary version for that world in order to only load the newest valid world.
|
||||
An APWorld without a world_version is always treated as older than one with a version
|
||||
(**Must** use exactly the format `"major.minor.build"`, e.g. `1.0.0`)
|
||||
* `authors` - a list of authors of the world. Displayed in user-facing places like the Supported Games page
|
||||
on WebHost. Should always be a list of strings.
|
||||
* `authors` - a list of authors, to eventually be displayed in various user-facing places such as WebHost and
|
||||
package managers. Should always be a list of strings.
|
||||
|
||||
If the APWorld is packaged as an `.apworld` zip file, it also needs to have `version` and `compatible_version`,
|
||||
which refer to the version of the APContainer packaging scheme defined in [Files.py](../worlds/Files.py).
|
||||
|
||||
@@ -77,6 +77,15 @@ Changes made to `docker-compose.yaml` can be applied by running `docker compose
|
||||
It is possible to carry out these deployment steps on Windows under [Windows Subsystem for Linux](https://learn.microsoft.com/en-us/windows/wsl/install).
|
||||
|
||||
|
||||
## Optional: A Link to the Past Enemizer
|
||||
|
||||
Only required to generate seeds that include A Link to the Past with certain options enabled. You will receive an
|
||||
error if it is required.
|
||||
Enemizer can be enabled on `x86_64` platform architecture, and is included in the image build process. Enemizer requires a version 1.0 Japanese "Zelda no Densetsu" `.sfc` rom file to be placed in the application directory:
|
||||
`docker run archipelago -v "/path/to/zelda.sfc:/app/Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"`.
|
||||
Enemizer is not currently available for `aarch64`.
|
||||
|
||||
|
||||
## Optional: Git
|
||||
|
||||
Building the image requires a local copy of the ArchipelagoMW source code.
|
||||
|
||||
+61
-162
@@ -1,7 +1,6 @@
|
||||
# Rule Builder
|
||||
|
||||
This document describes the API provided for the rule builder. Using this API provides you with with a simple interface
|
||||
to define rules and the following advantages:
|
||||
This document describes the API provided for the rule builder. Using this API provides you with with a simple interface to define rules and the following advantages:
|
||||
|
||||
- Rule classes that avoid all the common pitfalls
|
||||
- Logic optimization
|
||||
@@ -13,21 +12,13 @@ to define rules and the following advantages:
|
||||
|
||||
The rule builder consists of 3 main parts:
|
||||
|
||||
1. The rules, which are classes that inherit from `rule_builder.rules.Rule`. These are what you write for your logic.
|
||||
They can be combined and take into account your world's options. There are a number of default rules listed below,
|
||||
and you can create as many custom rules for your world as needed. When assigning the rules to a location or entrance
|
||||
they must be resolved.
|
||||
2. Resolved rules, which are classes that inherit from `rule_builder.rules.Rule.Resolved`. These are the optimized rules
|
||||
specific to one player that are set as a location or entrance's access rule. You generally shouldn't be directly
|
||||
creating these but they'll be created when assigning rules to locations or entrances. These are what power the
|
||||
human-readable logic explanations.
|
||||
3. The optional rule builder world subclass `CachedRuleBuilderWorld`, which is a class your world can inherit from
|
||||
instead of `World`. It adds a caching system to the rules that will lazy evaluate and cache the result.
|
||||
1. The rules, which are classes that inherit from `rule_builder.rules.Rule`. These are what you write for your logic. They can be combined and take into account your world's options. There are a number of default rules listed below, and you can create as many custom rules for your world as needed. When assigning the rules to a location or entrance they must be resolved.
|
||||
1. Resolved rules, which are classes that inherit from `rule_builder.rules.Rule.Resolved`. These are the optimized rules specific to one player that are set as a location or entrance's access rule. You generally shouldn't be directly creating these but they'll be created when assigning rules to locations or entrances. These are what power the human-readable logic explanations.
|
||||
1. The optional rule builder world subclass `CachedRuleBuilderWorld`, which is a class your world can inherit from instead of `World`. It adds a caching system to the rules that will lazy evaluate and cache the result.
|
||||
|
||||
## Usage
|
||||
|
||||
For the most part the only difference in usage is instead of writing lambdas for your logic, you write static Rule
|
||||
objects. You then must use `world.set_rule` to assign the rule to a location or entrance.
|
||||
For the most part the only difference in usage is instead of writing lambdas for your logic, you write static Rule objects. You then must use `world.set_rule` to assign the rule to a location or entrance.
|
||||
|
||||
```python
|
||||
# In your world's create_regions method
|
||||
@@ -41,7 +32,6 @@ The rule builder comes with a number of rules by default:
|
||||
- `False_`: Always returns false
|
||||
- `And`: Checks that all child rules are true (also provided by `&` operator)
|
||||
- `Or`: Checks that at least one child rule is true (also provided by `|` operator)
|
||||
- `AtLeast`: Checks that at least some count of rules is true
|
||||
- `Has`: Checks that the player has the given item with the given count (default 1)
|
||||
- `HasAll`: Checks that the player has all given items
|
||||
- `HasAny`: Checks that the player has at least one of the given items
|
||||
@@ -50,22 +40,18 @@ The rule builder comes with a number of rules by default:
|
||||
- `HasFromList`: Checks that the player has some number of given items
|
||||
- `HasFromListUnique`: Checks that the player has some number of given items, ignoring duplicates of the same item
|
||||
- `HasGroup`: Checks that the player has some number of items from a given item group
|
||||
- `HasGroupUnique`: Checks that the player has some number of items from a given item group, ignoring duplicates of the
|
||||
same item
|
||||
- `HasGroupUnique`: Checks that the player has some number of items from a given item group, ignoring duplicates of the same item
|
||||
- `CanReachLocation`: Checks that the player can logically reach the given location
|
||||
- `CanReachRegion`: Checks that the player can logically reach the given region
|
||||
- `CanReachEntrance`: Checks that the player can logically reach the given entrance
|
||||
|
||||
You can combine these rules together to describe the logic required for something. For example, to check if a player
|
||||
either has `Movement ability` or they have both `Key 1` and `Key 2`, you can do:
|
||||
You can combine these rules together to describe the logic required for something. For example, to check if a player either has `Movement ability` or they have both `Key 1` and `Key 2`, you can do:
|
||||
|
||||
```python
|
||||
rule = Has("Movement ability") | HasAll("Key 1", "Key 2")
|
||||
```
|
||||
|
||||
> ⚠️ Composing rules with the `and` and `or` keywords will not work. You must use the bitwise `&` and `|` operators. In
|
||||
> order to catch mistakes, the rule builder will not let you do boolean operations. As a consequence, in order to check
|
||||
> if a rule is defined you must use `if rule is not None`.
|
||||
> ⚠️ Composing rules with the `and` and `or` keywords will not work. You must use the bitwise `&` and `|` operators. In order to catch mistakes, the rule builder will not let you do boolean operations. As a consequence, in order to check if a rule is defined you must use `if rule is not None`.
|
||||
|
||||
### Assigning rules
|
||||
|
||||
@@ -75,16 +61,13 @@ When assigning the rule you must use the `set_rule` helper to correctly resolve
|
||||
self.set_rule(location_or_entrance, rule)
|
||||
```
|
||||
|
||||
There is also a `create_entrance` helper that will resolve the rule, check if it's `False`, and if not create the
|
||||
entrance and set the rule. This allows you to skip creating entrances that will never be valid. You can also specify
|
||||
`force_creation=True` if you would like to create the entrance even if the rule is `False`.
|
||||
There is also a `create_entrance` helper that will resolve the rule, check if it's `False`, and if not create the entrance and set the rule. This allows you to skip creating entrances that will never be valid. You can also specify `force_creation=True` if you would like to create the entrance even if the rule is `False`.
|
||||
|
||||
```python
|
||||
self.create_entrance(from_region, to_region, rule)
|
||||
```
|
||||
|
||||
> ⚠️ If you use a `CanReachLocation` rule on an entrance, you will either have to create the locations first, or specify
|
||||
> the location's parent region name with the `parent_region_name` argument of `CanReachLocation`.
|
||||
> ⚠️ If you use a `CanReachLocation` rule on an entrance, you will either have to create the locations first, or specify the location's parent region name with the `parent_region_name` argument of `CanReachLocation`.
|
||||
|
||||
You can also set a rule for your world's completion condition:
|
||||
|
||||
@@ -94,42 +77,21 @@ self.set_completion_rule(rule)
|
||||
|
||||
### Restricting options
|
||||
|
||||
Every rule allows you to specify which options it's applicable for. You can provide the argument `options` which is an
|
||||
iterable of `OptionFilter` instances. When resolved, if no filters are provided or all of them pass then the rule will
|
||||
resolve as normal. Otherwise, the rule will be replaced with `True` or `False` depending on what `filtered_resolution`
|
||||
is set to, which defaults to `False`.
|
||||
Every rule allows you to specify which options it's applicable for. You can provide the argument `options` which is an iterable of `OptionFilter` instances. Rules that pass the options check will be resolved as normal, and those that fail will be resolved as `False`.
|
||||
|
||||
```python
|
||||
rule1 = Has(
|
||||
"Fast Travel Spell",
|
||||
options=[OptionFilter(RandoFastTravel, RandoFastTravel.option_true)],
|
||||
)
|
||||
rule2 = Has(
|
||||
"Starting Party Member",
|
||||
options=[OptionFilter(RandoParty, 1)], # option attributes are suggested but any value works
|
||||
filtered_resolution=True,
|
||||
)
|
||||
```
|
||||
If you want a comparison that isn't equals, you can specify with the `operator` argument. The following operators are allowed:
|
||||
|
||||
If you want a comparison that isn't equals, you can specify with the `operator` argument. The following operators are
|
||||
allowed:
|
||||
- `eq`: `==`
|
||||
- `ne`: `!=`
|
||||
- `gt`: `>`
|
||||
- `lt`: `<`
|
||||
- `ge`: `>=`
|
||||
- `le`: `<=`
|
||||
- `contains`: `in`
|
||||
|
||||
- `eq`: `option_value == filter_value`
|
||||
- `ne`: `option_value != filter_value`
|
||||
- `gt`: `option_value > filter_value`
|
||||
- `lt`: `option_value < filter_value`
|
||||
- `ge`: `option_value >= filter_value`
|
||||
- `le`: `option_value <= filter_value`
|
||||
- `in`: `option_value in filter_value`
|
||||
- `contains`: `filter_value in option_value` (note reversed operands)
|
||||
By default rules that are excluded by their options will default to `False`. If you want to default to `True` instead, you can specify `filtered_resolution=True` on your rule.
|
||||
|
||||
```python
|
||||
rule1 = Has("Movement Ability", options=[OptionFilter(SkipsLevel, SkipsLevel.option_hard, operator="lt")])
|
||||
rule2 = Has("Item", options=[OptionFilter(ChoiceOption, [1, 5], operator="in")])
|
||||
```
|
||||
|
||||
To check if the player has received the switch item if switches are randomized, or if they can reach the switch when not
|
||||
randomized:
|
||||
To check if the player can reach a switch, or if they've received the switch item if switches are randomized:
|
||||
|
||||
```python
|
||||
rule = (
|
||||
@@ -153,12 +115,12 @@ If you would like to provide option filters when reusing or composing rules, you
|
||||
common_rule = Has("A") | HasAny("B", "C")
|
||||
...
|
||||
rule = (
|
||||
Filtered(common_rule, options=[OptionFilter(Opt, 0)])
|
||||
| Filtered(Has("X") | CanReachRegion("Y"), options=[OptionFilter(Opt, 1)])
|
||||
Filtered(common_rule, options=[OptionFilter(Opt, 0)]),
|
||||
| Filtered(Has("X") | CanReachRegion("Y"), options=[OptionFilter(Opt, 1)]),
|
||||
)
|
||||
```
|
||||
|
||||
For convenience, you can also use the `&` and `|` operators to apply options to rules:
|
||||
You can also use the & and | operators to apply options to rules:
|
||||
|
||||
```python
|
||||
common_rule = Has("A")
|
||||
@@ -167,22 +129,14 @@ common_rule_only_on_easy = common_rule & easy_filter
|
||||
common_rule_skipped_on_easy = common_rule | easy_filter
|
||||
```
|
||||
|
||||
Combining the above, you can easily bypass a requirement based on option choices:
|
||||
|
||||
```python
|
||||
rule = Has("Some Upgrade") | OptionFilter(CombatDifficulty, CombatDifficulty.option_medium, operator="ge")
|
||||
```
|
||||
|
||||
### Field resolvers
|
||||
|
||||
When creating rules you may sometimes need to set a field to a value that depends on the world instance. You can use a
|
||||
`FieldResolver` to define how to populate that field when the rule is being resolved.
|
||||
When creating rules you may sometimes need to set a field to a value that depends on the world instance. You can use a `FieldResolver` to define how to populate that field when the rule is being resolved.
|
||||
|
||||
There are two build-in field resolvers:
|
||||
|
||||
- `FromOption`: Resolves to the value of the given option
|
||||
- `FromWorldAttr`: Resolves to the value of the given world instance attribute, can specify a dotted path `a.b.c` to get
|
||||
a nested attribute or dict item
|
||||
- `FromWorldAttr`: Resolves to the value of the given world instance attribute, can specify a dotted path `a.b.c` to get a nested attribute or dict item
|
||||
|
||||
```python
|
||||
world.options.mcguffin_count = 5
|
||||
@@ -194,8 +148,7 @@ rule = (
|
||||
# Results in Has("A", count=5) | HasGroup("Important items", count=99)
|
||||
```
|
||||
|
||||
You can define your own resolvers by creating a class that inherits from `FieldResolver`, provides your game name, and
|
||||
implements a `resolve` function:
|
||||
You can define your own resolvers by creating a class that inherits from `FieldResolver`, provides your game name, and implements a `resolve` function:
|
||||
|
||||
```python
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
@@ -210,30 +163,24 @@ class FromCustomResolution(FieldResolver, game="MyGame"):
|
||||
rule = Has("Combat Level", count=FromCustomResolution("combat"))
|
||||
```
|
||||
|
||||
If you want to support rule serialization and your resolver contains non-serializable properties you may need to
|
||||
override `to_dict` or `from_dict`.
|
||||
If you want to support rule serialization and your resolver contains non-serializable properties you may need to override `to_dict` or `from_dict`.
|
||||
|
||||
## Enabling caching
|
||||
|
||||
The rule builder provides a `CachedRuleBuilderWorld` base class for your `World` class that enables caching on your
|
||||
rules.
|
||||
The rule builder provides a `CachedRuleBuilderWorld` base class for your `World` class that enables caching on your rules.
|
||||
|
||||
```python
|
||||
class MyWorld(CachedRuleBuilderWorld):
|
||||
game = "My Game"
|
||||
```
|
||||
|
||||
If your world's logic is very simple and you don't have many nested rules, the caching system may have more overhead
|
||||
cost than time it saves. You'll have to benchmark your own world to see if it should be enabled or not.
|
||||
If your world's logic is very simple and you don't have many nested rules, the caching system may have more overhead cost than time it saves. You'll have to benchmark your own world to see if it should be enabled or not.
|
||||
|
||||
### Item name mapping
|
||||
|
||||
If you have multiple real items that map to a single logic item, add a `item_mapping` class dict to your world that maps
|
||||
actual item names to real item names so the cache system knows what to invalidate.
|
||||
If you have multiple real items that map to a single logic item, add a `item_mapping` class dict to your world that maps actual item names to real item names so the cache system knows what to invalidate.
|
||||
|
||||
For example, if you have multiple `Currency x<num>` items on locations, but your rules only check a singular logical
|
||||
`Currency` item, eg `Has("Currency", 1000)`, you'll want to map each numerical currency item to the single logical
|
||||
`Currency`.
|
||||
For example, if you have multiple `Currency x<num>` items on locations, but your rules only check a singular logical `Currency` item, eg `Has("Currency", 1000)`, you'll want to map each numerical currency item to the single logical `Currency`.
|
||||
|
||||
```python
|
||||
class MyWorld(CachedRuleBuilderWorld):
|
||||
@@ -247,13 +194,9 @@ class MyWorld(CachedRuleBuilderWorld):
|
||||
|
||||
## Defining custom rules
|
||||
|
||||
You can create a custom rule by creating a class that inherits from `Rule` or any of the default rules. You must provide
|
||||
the game name as an argument to the class. It's recommended to use the `@dataclass` decorator to reduce boilerplate, and
|
||||
to also provide your world as a type argument to add correct type checking to the `_instantiate` method.
|
||||
You can create a custom rule by creating a class that inherits from `Rule` or any of the default rules. You must provide the game name as an argument to the class. It's recommended to use the `@dataclass` decorator to reduce boilerplate, and to also provide your world as a type argument to add correct type checking to the `_instantiate` method.
|
||||
|
||||
You must provide or inherit a `Resolved` child class that defines an `_evaluate` method. This class will automatically
|
||||
be converted into a frozen `dataclass`. If your world has caching enabled you may need to define one or more
|
||||
dependencies functions as outlined below.
|
||||
You must provide or inherit a `Resolved` child class that defines an `_evaluate` method. This class will automatically be converted into a frozen `dataclass`. If your world has caching enabled you may need to define one or more dependencies functions as outlined below.
|
||||
|
||||
To add a rule that checks if the user has enough mcguffins to goal, with a randomized requirement:
|
||||
|
||||
@@ -302,10 +245,7 @@ class ComplicatedFilter(Rule["MyWorld"], game="My Game"):
|
||||
|
||||
### Item dependencies
|
||||
|
||||
If your world inherits from `CachedRuleBuilderWorld` and there are items that when collected will affect the result of
|
||||
your rule evaluation, it must define an `item_dependencies` function that returns a mapping of the item name to the id
|
||||
of your rule. These dependencies will be combined to inform the caching system. It may be worthwhile to define this
|
||||
function even when caching is disabled as more things may use it in the future.
|
||||
If your world inherits from `CachedRuleBuilderWorld` and there are items that when collected will affect the result of your rule evaluation, it must define an `item_dependencies` function that returns a mapping of the item name to the id of your rule. These dependencies will be combined to inform the caching system. It may be worthwhile to define this function even when caching is disabled as more things may use it in the future.
|
||||
|
||||
```python
|
||||
@dataclasses.dataclass()
|
||||
@@ -322,10 +262,7 @@ All of the default `Has*` rules define this function already.
|
||||
|
||||
### Region dependencies
|
||||
|
||||
If your custom rule references other regions, it must define a `region_dependencies` function that returns a mapping of
|
||||
region names to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These
|
||||
dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the
|
||||
caching system if applicable.
|
||||
If your custom rule references other regions, it must define a `region_dependencies` function that returns a mapping of region names to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the caching system if applicable.
|
||||
|
||||
```python
|
||||
@dataclasses.dataclass()
|
||||
@@ -342,10 +279,7 @@ The default `CanReachLocation`, `CanReachRegion`, and `CanReachEntrance` rules d
|
||||
|
||||
### Location dependencies
|
||||
|
||||
If your custom rule references other locations, it must define a `location_dependencies` function that returns a mapping
|
||||
of the location name to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These
|
||||
dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the
|
||||
caching system if applicable.
|
||||
If your custom rule references other locations, it must define a `location_dependencies` function that returns a mapping of the location name to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the caching system if applicable.
|
||||
|
||||
```python
|
||||
@dataclasses.dataclass()
|
||||
@@ -362,10 +296,7 @@ The default `CanReachLocation` rule defines this function already.
|
||||
|
||||
### Entrance dependencies
|
||||
|
||||
If your custom rule references other entrances, it must define a `entrance_dependencies` function that returns a mapping
|
||||
of the entrance name to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These
|
||||
dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the
|
||||
caching system if applicable.
|
||||
If your custom rule references other entrances, it must define a `entrance_dependencies` function that returns a mapping of the entrance name to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the caching system if applicable.
|
||||
|
||||
```python
|
||||
@dataclasses.dataclass()
|
||||
@@ -382,13 +313,9 @@ The default `CanReachEntrance` rule defines this function already.
|
||||
|
||||
### Rule explanations
|
||||
|
||||
Resolved rules have a default implementation for `explain_json` and `explain_str` functions. The former optionally
|
||||
accepts a `CollectionState` and returns a list of `JSONMessagePart` appropriate for `print_json` in a client. It will
|
||||
display a human-readable message that explains what the rule requires. The latter is similar but returns a string. It is
|
||||
useful when debugging. There is also a `__str__` method defined to check what a rule is without a state.
|
||||
Resolved rules have a default implementation for `explain_json` and `explain_str` functions. The former optionally accepts a `CollectionState` and returns a list of `JSONMessagePart` appropriate for `print_json` in a client. It will display a human-readable message that explains what the rule requires. The latter is similar but returns a string. It is useful when debugging. There is also a `__str__` method defined to check what a rule is without a state.
|
||||
|
||||
To implement a custom message with a custom rule, override the `explain_json` and/or `explain_str` method on your
|
||||
`Resolved` class:
|
||||
To implement a custom message with a custom rule, override the `explain_json` and/or `explain_str` method on your `Resolved` class:
|
||||
|
||||
```python
|
||||
class MyRule(Rule, game="My Game"):
|
||||
@@ -425,35 +352,22 @@ class MyRule(Rule, game="My Game"):
|
||||
|
||||
### Cache control
|
||||
|
||||
By default your custom rule will work through the cache system as any other rule if caching is enabled. There are two
|
||||
class attributes on the `Resolved` class you can override to change this behavior.
|
||||
By default your custom rule will work through the cache system as any other rule if caching is enabled. There are two class attributes on the `Resolved` class you can override to change this behavior.
|
||||
|
||||
- `force_recalculate`: Setting this to `True` will cause your custom rule to skip going through the caching system and
|
||||
always recalculate when being evaluated. When a rule with this flag enabled is composed with `And` or `Or` it will
|
||||
cause any parent rules to always force recalculate as well. Use this flag when it's difficult to determine when your
|
||||
rule should be marked as stale.
|
||||
- `skip_cache`: Setting this to `True` will also cause your custom rule to skip going through the caching system when
|
||||
being evaluated. However, it will **not** affect any other rules when composed with `And` or `Or`, so it must still
|
||||
define its `*_dependencies` functions as required. Use this flag when the evaluation of this rule is trivial and the
|
||||
overhead of the caching system will slow it down.
|
||||
- `force_recalculate`: Setting this to `True` will cause your custom rule to skip going through the caching system and always recalculate when being evaluated. When a rule with this flag enabled is composed with `And` or `Or` it will cause any parent rules to always force recalculate as well. Use this flag when it's difficult to determine when your rule should be marked as stale.
|
||||
- `skip_cache`: Setting this to `True` will also cause your custom rule to skip going through the caching system when being evaluated. However, it will **not** affect any other rules when composed with `And` or `Or`, so it must still define its `*_dependencies` functions as required. Use this flag when the evaluation of this rule is trivial and the overhead of the caching system will slow it down.
|
||||
|
||||
### Caveats
|
||||
|
||||
- Ensure you are passing `caching_enabled=True` in your `_instantiate` function when creating resolved rule instances if
|
||||
your world has opted into caching.
|
||||
- Ensure you are passing `caching_enabled=True` in your `_instantiate` function when creating resolved rule instances if your world has opted into caching.
|
||||
- Resolved rules are forced to be frozen dataclasses. They and all their attributes must be immutable and hashable.
|
||||
- If your rule creates child rules ensure they are being resolved through the world rather than creating `Resolved`
|
||||
instances directly.
|
||||
- If your rule creates child rules ensure they are being resolved through the world rather than creating `Resolved` instances directly.
|
||||
|
||||
## Serialization
|
||||
|
||||
The rule builder is intended to be written first in Python for optimization and type safety. To facilitate exporting the
|
||||
rules to a client or tracker, rules have a `to_dict` method that returns a JSON-compatible dict. Since the location and
|
||||
entrance logic structure varies greatly from world to world, the actual JSON dumping is left up to the world dev.
|
||||
The rule builder is intended to be written first in Python for optimization and type safety. To facilitate exporting the rules to a client or tracker, rules have a `to_dict` method that returns a JSON-compatible dict. Since the location and entrance logic structure varies greatly from world to world, the actual JSON dumping is left up to the world dev.
|
||||
|
||||
The dict contains a `rule` key with the name of the rule, an `options` key with the rule's list of option filters, and
|
||||
an `args` key that contains any other arguments the individual rule has. For example, this is what a simple `Has` rule
|
||||
would look like:
|
||||
The dict contains a `rule` key with the name of the rule, an `options` key with the rule's list of option filters, and an `args` key that contains any other arguments the individual rule has. For example, this is what a simple `Has` rule would look like:
|
||||
|
||||
```python
|
||||
{
|
||||
@@ -466,8 +380,7 @@ would look like:
|
||||
}
|
||||
```
|
||||
|
||||
For `And` and `Or` rules, instead of an `args` key, they have a `children` key containing a list of their child rules in
|
||||
the same serializable format:
|
||||
For `And` and `Or` rules, instead of an `args` key, they have a `children` key containing a list of their child rules in the same serializable format:
|
||||
|
||||
```python
|
||||
{
|
||||
@@ -551,8 +464,7 @@ class BasicLogicRule(Rule, game="My Game"):
|
||||
}
|
||||
```
|
||||
|
||||
If your logic has been done in custom JSON first, you can define a `from_dict` class method on your rules to parse it
|
||||
correctly:
|
||||
If your logic has been done in custom JSON first, you can define a `from_dict` class method on your rules to parse it correctly:
|
||||
|
||||
```python
|
||||
class BasicLogicRule(Rule, game="My Game"):
|
||||
@@ -573,14 +485,10 @@ These are properties and helpers that are available to you in your world.
|
||||
#### Methods
|
||||
|
||||
- `rule_from_dict(data)`: Create a rule instance from a deserialized dict representation
|
||||
- `register_rule_builder_dependencies()`: Register all rules that depend on location or entrance access with the
|
||||
inherited dependencies, gets called automatically after set_rules
|
||||
- `set_rule(spot: Location | Entrance, rule: Rule)`: Resolve a rule, register its dependencies, and set it on the given
|
||||
location or entrance
|
||||
- `register_rule_builder_dependencies()`: Register all rules that depend on location or entrance access with the inherited dependencies, gets called automatically after set_rules
|
||||
- `set_rule(spot: Location | Entrance, rule: Rule)`: Resolve a rule, register its dependencies, and set it on the given location or entrance
|
||||
- `set_completion_rule(rule: Rule)`: Sets the completion condition for this world
|
||||
- `create_entrance(from_region: Region, to_region: Region, rule: Rule | None, name: str | None = None, force_creation: bool = False)`:
|
||||
Attempt to create an entrance from `from_region` to `to_region`, skipping creation if `rule` is defined and evaluates
|
||||
to `False_()` unless force_creation is `True`
|
||||
- `create_entrance(from_region: Region, to_region: Region, rule: Rule | None, name: str | None = None, force_creation: bool = False)`: Attempt to create an entrance from `from_region` to `to_region`, skipping creation if `rule` is defined and evaluates to `False_()` unless force_creation is `True`
|
||||
|
||||
#### CachedRuleBuilderWorld Properties
|
||||
|
||||
@@ -593,27 +501,18 @@ The following property is only available when inheriting from `CachedRuleBuilder
|
||||
These are properties and helpers that you can use or override for custom rules.
|
||||
|
||||
- `_instantiate(world: World)`: Create a new resolved rule instance, override for custom rules as required
|
||||
- `to_dict()`: Create a JSON-compatible dict representation of this rule, override if you want to customize your rule's
|
||||
serialization
|
||||
- `from_dict(data, world_cls: type[World])`: Return a new rule instance from a deserialized representation, override if
|
||||
you've overridden `to_dict`
|
||||
- `to_dict()`: Create a JSON-compatible dict representation of this rule, override if you want to customize your rule's serialization
|
||||
- `from_dict(data, world_cls: type[World])`: Return a new rule instance from a deserialized representation, override if you've overridden `to_dict`
|
||||
- `__str__()`: Basic string representation of a rule, useful for debugging
|
||||
|
||||
#### Resolved rule API
|
||||
|
||||
- `player: int`: The slot this rule is resolved for
|
||||
- `_evaluate(state: CollectionState)`: Evaluate this rule against the given state, override this to define the logic for
|
||||
this rule
|
||||
- `item_dependencies()`: A mapping of item name to set of ids, override this if your custom rule depends on item
|
||||
collection
|
||||
- `region_dependencies()`: A mapping of region name to set of ids, override this if your custom rule depends on reaching
|
||||
regions
|
||||
- `location_dependencies()`: A mapping of location name to set of ids, override this if your custom rule depends on
|
||||
reaching locations
|
||||
- `entrance_dependencies()`: A mapping of entrance name to set of ids, override this if your custom rule depends on
|
||||
reaching entrances
|
||||
- `explain_json(state: CollectionState | None = None)`: Return a list of printJSON messages describing this rule's logic
|
||||
(and if state is defined its evaluation) in a human readable way, override to explain custom rules
|
||||
- `explain_str(state: CollectionState | None = None)`: Return a string describing this rule's logic (and if state is
|
||||
defined its evaluation) in a human readable way, override to explain custom rules, more useful for debugging
|
||||
- `_evaluate(state: CollectionState)`: Evaluate this rule against the given state, override this to define the logic for this rule
|
||||
- `item_dependencies()`: A mapping of item name to set of ids, override this if your custom rule depends on item collection
|
||||
- `region_dependencies()`: A mapping of region name to set of ids, override this if your custom rule depends on reaching regions
|
||||
- `location_dependencies()`: A mapping of location name to set of ids, override this if your custom rule depends on reaching locations
|
||||
- `entrance_dependencies()`: A mapping of entrance name to set of ids, override this if your custom rule depends on reaching entrances
|
||||
- `explain_json(state: CollectionState | None = None)`: Return a list of printJSON messages describing this rule's logic (and if state is defined its evaluation) in a human readable way, override to explain custom rules
|
||||
- `explain_str(state: CollectionState | None = None)`: Return a string describing this rule's logic (and if state is defined its evaluation) in a human readable way, override to explain custom rules, more useful for debugging
|
||||
- `__str__()`: A string describing this rule's logic without its evaluation, override to explain custom rules
|
||||
|
||||
@@ -78,6 +78,16 @@ first generate the binary distribution and then run `python setup.py bdist_appim
|
||||
put an `appimagetool` into the directory you run the command from, rename it to `appimagetool` and make it executable.
|
||||
|
||||
|
||||
## Optional: A Link to the Past Enemizer
|
||||
|
||||
Only required to generate seeds that include A Link to the Past with certain options enabled. You will receive an
|
||||
error if it is required.
|
||||
|
||||
You can get the latest Enemizer release at [Enemizer Github releases](https://github.com/Ijwu/Enemizer/releases).
|
||||
It should be dropped as "EnemizerCLI" into the root folder of the project. Alternatively, you can point the Enemizer
|
||||
setting in host.yaml at your Enemizer executable.
|
||||
|
||||
|
||||
## Optional: SNI
|
||||
|
||||
[SNI](https://github.com/alttpo/sni/blob/main/README.md) is required to use SNIClient. If not integrated into the project, it has to be started manually.
|
||||
|
||||
@@ -327,11 +327,6 @@ reject the placement of an item there.
|
||||
|
||||
### Events (or "generation-only items/locations")
|
||||
|
||||
> **Warning:** If you're trying to tell the Archipelago server that the player has achieved their goal, you want to send
|
||||
a [StatusUpdate packet](network%20protocol.md#statusupdate), or however [your client library](network%20protocol.md)
|
||||
wraps it. Despite the popularity of "victory events" during generation, events have nothing to do with how goals are
|
||||
triggered during gameplay.
|
||||
|
||||
An event item or location is one that only exists during multiworld generation; the server is never made aware of them.
|
||||
Event locations can never be checked by the player, and event items cannot be received during play.
|
||||
|
||||
|
||||
+3
-1
@@ -57,8 +57,9 @@ Name: "custom"; Description: "Custom installation"; Flags: iscustom
|
||||
NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-modify authusers-modify;
|
||||
|
||||
[Files]
|
||||
Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
Source: "{#source_path}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs;
|
||||
Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs;
|
||||
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
|
||||
|
||||
[Icons]
|
||||
@@ -82,6 +83,7 @@ Type: files; Name: "{app}\*.exe"
|
||||
Type: files; Name: "{app}\data\lua\connector_pkmn_rb.lua"
|
||||
Type: files; Name: "{app}\data\lua\connector_ff1.lua"
|
||||
Type: filesandordirs; Name: "{app}\SNI\lua*"
|
||||
Type: filesandordirs; Name: "{app}\EnemizerCLI*"
|
||||
#include "installdelete.iss"
|
||||
|
||||
[Registry]
|
||||
|
||||
@@ -363,16 +363,15 @@ class ServerLabel(HoverBehavior, MDTooltip, MDBoxLayout):
|
||||
text += "\nPermissions:"
|
||||
for permission_name, permission_data in ctx.permissions.items():
|
||||
text += f"\n {permission_name}: {permission_data}"
|
||||
if ctx.total_locations and ctx.hint_cost is not None:
|
||||
if ctx.hint_cost == 0:
|
||||
text += "\n!hint is free to use."
|
||||
else:
|
||||
min_cost = int(ctx.server_version >= (0, 3, 9))
|
||||
text += f"\nA new !hint <itemname> costs {ctx.hint_cost}% of checks made. " \
|
||||
f"For you this means every " \
|
||||
f"{max(min_cost, int(ctx.hint_cost * 0.01 * ctx.total_locations))} " \
|
||||
"location checks." \
|
||||
f"\nYou currently have {ctx.hint_points} points."
|
||||
if ctx.hint_cost is not None and ctx.total_locations:
|
||||
min_cost = int(ctx.server_version >= (0, 3, 9))
|
||||
text += f"\nA new !hint <itemname> costs {ctx.hint_cost}% of checks made. " \
|
||||
f"For you this means every " \
|
||||
f"{max(min_cost, int(ctx.hint_cost * 0.01 * ctx.total_locations))} " \
|
||||
"location checks." \
|
||||
f"\nYou currently have {ctx.hint_points} points."
|
||||
elif ctx.hint_cost == 0:
|
||||
text += "\n!hint is free to use."
|
||||
if ctx.stored_data and "_read_race_mode" in ctx.stored_data:
|
||||
text += "\nRace mode is enabled." \
|
||||
if ctx.stored_data["_read_race_mode"] else "\nRace mode is disabled."
|
||||
|
||||
+14
-156
@@ -36,7 +36,7 @@ def _create_hash_fn(resolved_rule_cls: "CustomRuleRegister") -> Callable[..., in
|
||||
class CustomRuleRegister(type):
|
||||
"""A metaclass to contain world custom rules and automatically convert resolved rules to frozen dataclasses"""
|
||||
|
||||
resolved_rules: ClassVar[dict["Rule.Resolved", "Rule.Resolved"]] = {}
|
||||
resolved_rules: ClassVar[dict[int, "Rule.Resolved"]] = {}
|
||||
"""A cached of resolved rules to turn each unique one into a singleton"""
|
||||
|
||||
custom_rules: ClassVar[dict[str, dict[str, type["Rule[Any]"]]]] = {}
|
||||
@@ -64,9 +64,10 @@ class CustomRuleRegister(type):
|
||||
@override
|
||||
def __call__(cls, *args: Any, **kwds: Any) -> Any:
|
||||
rule = super().__call__(*args, **kwds)
|
||||
if rule in cls.resolved_rules:
|
||||
return cls.resolved_rules[rule]
|
||||
cls.resolved_rules[rule] = rule
|
||||
rule_hash = hash(rule)
|
||||
if rule_hash in cls.resolved_rules:
|
||||
return cls.resolved_rules[rule_hash]
|
||||
cls.resolved_rules[rule_hash] = rule
|
||||
return rule
|
||||
|
||||
@classmethod
|
||||
@@ -425,142 +426,13 @@ class NestedRule(Rule[TWorld], game="Archipelago"):
|
||||
return combined_deps
|
||||
|
||||
|
||||
class AtLeast(NestedRule[TWorld], game="Archipelago"):
|
||||
"""A rule that returns true when at least N child rules evaluate as true"""
|
||||
|
||||
count: int | FieldResolver
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
count: int | FieldResolver,
|
||||
*children: Rule[TWorld],
|
||||
options: Iterable[OptionFilter] = (),
|
||||
filtered_resolution: bool = False,
|
||||
) -> None:
|
||||
super().__init__(*children, options=options, filtered_resolution=filtered_resolution)
|
||||
self.count = count
|
||||
|
||||
@override
|
||||
def _instantiate(self, world: TWorld) -> Rule.Resolved:
|
||||
count = resolve_field(self.count, world, int)
|
||||
if count == 0:
|
||||
return True_().resolve(world)
|
||||
|
||||
children_to_process = [c.resolve(world) for c in self.children]
|
||||
return AtLeast.from_resolved(count, world, children_to_process)
|
||||
|
||||
@classmethod
|
||||
def from_resolved(cls, count: int, world: TWorld, children_to_process: list[Rule.Resolved]) -> Rule.Resolved:
|
||||
clauses: list[Rule.Resolved] = []
|
||||
|
||||
while children_to_process:
|
||||
child = children_to_process.pop(0)
|
||||
if child.always_true:
|
||||
if count == 1:
|
||||
return child
|
||||
count -= 1
|
||||
continue
|
||||
if child.always_false:
|
||||
# falses can be ignored
|
||||
continue
|
||||
|
||||
clauses.append(child)
|
||||
|
||||
if len(clauses) < count:
|
||||
return False_().resolve(world)
|
||||
if count == 1:
|
||||
# Switch to Or which has more optimized handling
|
||||
return Or.from_resolved(world, clauses)
|
||||
if count == len(clauses):
|
||||
# Switch to And which has more optimized handling
|
||||
return And.from_resolved(world, clauses)
|
||||
return AtLeast.Resolved(
|
||||
tuple(clauses),
|
||||
count=count,
|
||||
player=world.player,
|
||||
caching_enabled=getattr(world, "rule_caching_enabled", False),
|
||||
)
|
||||
|
||||
@override
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
output = super().to_dict()
|
||||
count = self.count
|
||||
output["count"] = count.to_dict() if isinstance(count, FieldResolver) else count
|
||||
return output
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def from_dict(cls, data: Mapping[str, Any], world_cls: "type[World]") -> Self:
|
||||
args = cls._parse_field_resolvers(data, world_cls.game)
|
||||
options = OptionFilter.multiple_from_dict(data.get("options", ()))
|
||||
children = [world_cls.rule_from_dict(c) for c in data.get("children", ())]
|
||||
return cls(
|
||||
args.pop("count"),
|
||||
*children,
|
||||
options=options,
|
||||
filtered_resolution=data.get("filtered_resolution", False),
|
||||
)
|
||||
|
||||
class Resolved(NestedRule.Resolved):
|
||||
count: int
|
||||
|
||||
@override
|
||||
def _evaluate(self, state: CollectionState) -> bool:
|
||||
count = self.count
|
||||
for rule in self.children:
|
||||
if rule(state):
|
||||
if count == 1:
|
||||
return True
|
||||
count -= 1
|
||||
return False
|
||||
|
||||
@override
|
||||
def explain_json(self, state: CollectionState | None = None) -> list[JSONMessagePart]:
|
||||
messages: list[JSONMessagePart] = []
|
||||
if state is None:
|
||||
messages = [
|
||||
{"type": "text", "text": "At least "},
|
||||
{"type": "color", "color": "cyan", "text": str(self.count)},
|
||||
{"type": "text", "text": " of ("},
|
||||
]
|
||||
else:
|
||||
satisfied_count = sum(1 if child(state) else 0 for child in self.children)
|
||||
messages = [
|
||||
{"type": "text", "text": "At least "},
|
||||
{"type": "color", "color": "cyan", "text": f"{satisfied_count}/{self.count}"},
|
||||
{"type": "text", "text": " of ("},
|
||||
]
|
||||
for i, child in enumerate(self.children):
|
||||
if i > 0:
|
||||
messages.append({"type": "text", "text": ", "})
|
||||
messages.extend(child.explain_json(state))
|
||||
messages.append({"type": "text", "text": ")"})
|
||||
return messages
|
||||
|
||||
@override
|
||||
def explain_str(self, state: CollectionState | None = None) -> str:
|
||||
clauses = ", ".join([c.explain_str(state) for c in self.children])
|
||||
if state is None:
|
||||
return f"At least {self.count} of ({clauses})"
|
||||
satisfied_count = sum(1 if child(state) else 0 for child in self.children)
|
||||
return f"At least {satisfied_count}/{self.count} of ({clauses})"
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
clauses = ", ".join([str(c) for c in self.children])
|
||||
return f"At least {self.count} of ({clauses})"
|
||||
|
||||
|
||||
@dataclasses.dataclass(init=False)
|
||||
class And(NestedRule[TWorld], game="Archipelago"):
|
||||
"""A rule that only returns true when all child rules evaluate as true"""
|
||||
|
||||
@override
|
||||
def _instantiate(self, world: TWorld) -> Rule.Resolved:
|
||||
return And.from_resolved(world, [c.resolve(world) for c in self.children])
|
||||
|
||||
@classmethod
|
||||
def from_resolved(cls, world: TWorld, children_to_process: list[Rule.Resolved]) -> Rule.Resolved:
|
||||
children_to_process = [c.resolve(world) for c in self.children]
|
||||
clauses: list[Rule.Resolved] = []
|
||||
items: dict[str, int] = {}
|
||||
true_rule: Rule.Resolved | None = None
|
||||
@@ -593,7 +465,7 @@ class And(NestedRule[TWorld], game="Archipelago"):
|
||||
clauses.append(child)
|
||||
|
||||
if not clauses and not items:
|
||||
return true_rule or True_().resolve(world)
|
||||
return true_rule or False_().resolve(world)
|
||||
|
||||
if len(items) == 1:
|
||||
item, count = next(iter(items.items()))
|
||||
@@ -647,10 +519,7 @@ class Or(NestedRule[TWorld], game="Archipelago"):
|
||||
|
||||
@override
|
||||
def _instantiate(self, world: TWorld) -> Rule.Resolved:
|
||||
return Or.from_resolved(world, [c.resolve(world) for c in self.children])
|
||||
|
||||
@classmethod
|
||||
def from_resolved(cls, world: TWorld, children_to_process: list[Rule.Resolved]) -> Rule.Resolved:
|
||||
children_to_process = [c.resolve(world) for c in self.children]
|
||||
clauses: list[Rule.Resolved] = []
|
||||
items: dict[str, int] = {}
|
||||
|
||||
@@ -1403,16 +1272,14 @@ class HasFromList(Rule[TWorld], game="Archipelago"):
|
||||
|
||||
@override
|
||||
def _instantiate(self, world: TWorld) -> Rule.Resolved:
|
||||
count = resolve_field(self.count, world, int)
|
||||
if count <= 0:
|
||||
return True_().resolve(world)
|
||||
if len(self.item_names) == 0:
|
||||
# match state.has_from_list
|
||||
return False_().resolve(world)
|
||||
if len(self.item_names) == 1:
|
||||
return Has(self.item_names[0], self.count).resolve(world)
|
||||
return self.Resolved(
|
||||
self.item_names,
|
||||
count=count,
|
||||
count=resolve_field(self.count, world, int),
|
||||
player=world.player,
|
||||
caching_enabled=getattr(world, "rule_caching_enabled", False),
|
||||
)
|
||||
@@ -1540,9 +1407,8 @@ class HasFromListUnique(Rule[TWorld], game="Archipelago"):
|
||||
@override
|
||||
def _instantiate(self, world: TWorld) -> Rule.Resolved:
|
||||
count = resolve_field(self.count, world, int)
|
||||
if count <= 0:
|
||||
return True_().resolve(world)
|
||||
if len(self.item_names) < count:
|
||||
if len(self.item_names) == 0 or len(self.item_names) < count:
|
||||
# match state.has_from_list_unique
|
||||
return False_().resolve(world)
|
||||
if len(self.item_names) == 1:
|
||||
return Has(self.item_names[0]).resolve(world)
|
||||
@@ -1660,14 +1526,11 @@ class HasGroup(Rule[TWorld], game="Archipelago"):
|
||||
|
||||
@override
|
||||
def _instantiate(self, world: TWorld) -> Rule.Resolved:
|
||||
count = resolve_field(self.count, world, int)
|
||||
if count <= 0:
|
||||
return True_().resolve(world)
|
||||
item_names = tuple(sorted(world.item_name_groups[self.item_name_group]))
|
||||
return self.Resolved(
|
||||
self.item_name_group,
|
||||
item_names,
|
||||
count=count,
|
||||
count=resolve_field(self.count, world, int),
|
||||
player=world.player,
|
||||
caching_enabled=getattr(world, "rule_caching_enabled", False),
|
||||
)
|
||||
@@ -1737,16 +1600,11 @@ class HasGroupUnique(Rule[TWorld], game="Archipelago"):
|
||||
|
||||
@override
|
||||
def _instantiate(self, world: TWorld) -> Rule.Resolved:
|
||||
count = resolve_field(self.count, world, int)
|
||||
if count <= 0:
|
||||
return True_().resolve(world)
|
||||
item_names = tuple(sorted(world.item_name_groups[self.item_name_group]))
|
||||
if len(item_names) < count:
|
||||
return False_().resolve(world)
|
||||
return self.Resolved(
|
||||
self.item_name_group,
|
||||
item_names,
|
||||
count=count,
|
||||
count=resolve_field(self.count, world, int),
|
||||
player=world.player,
|
||||
caching_enabled=getattr(world, "rule_caching_enabled", False),
|
||||
)
|
||||
|
||||
+5
-9
@@ -98,8 +98,6 @@ class Group:
|
||||
self._changed = True
|
||||
attr = new
|
||||
# resolve the path immediately when accessing it
|
||||
if attr.exists():
|
||||
attr.__class__.validate(attr.resolve())
|
||||
return attr.__class__(attr.resolve())
|
||||
return attr
|
||||
|
||||
@@ -635,6 +633,10 @@ class ServerOptions(Group):
|
||||
class GeneratorOptions(Group):
|
||||
"""Options for Generation"""
|
||||
|
||||
class EnemizerPath(LocalFilePath):
|
||||
"""Location of your Enemizer CLI, available here: https://github.com/Ijwu/Enemizer/releases"""
|
||||
is_exe = True
|
||||
|
||||
class PlayerFilesPath(OptionalUserFolderPath):
|
||||
"""Folder from which the player yaml files are pulled from"""
|
||||
# created on demand, so marked as optional
|
||||
@@ -642,12 +644,6 @@ class GeneratorOptions(Group):
|
||||
class Players(int):
|
||||
"""amount of players, 0 to infer from player files"""
|
||||
|
||||
class AllowQuantity(Bool):
|
||||
"""
|
||||
allow players to set an individual quantity for their yaml settings
|
||||
with 'false' any amounts from the players will be ignored and set to 1
|
||||
"""
|
||||
|
||||
class WeightsFilePath(str):
|
||||
"""
|
||||
general weights file, within the stated player_files_path location
|
||||
@@ -691,9 +687,9 @@ class GeneratorOptions(Group):
|
||||
start_inventory -> Move remaining items to start_inventory, generate additional filler items to fill locations.
|
||||
"""
|
||||
|
||||
enemizer_path: EnemizerPath = EnemizerPath("EnemizerCLI/EnemizerCLI.Core") # + ".exe" is implied on Windows
|
||||
player_files_path: PlayerFilesPath = PlayerFilesPath("Players")
|
||||
players: Players = Players(0)
|
||||
allow_quantity: AllowQuantity | bool = False
|
||||
weights_file_path: WeightsFilePath = WeightsFilePath("weights.yaml")
|
||||
meta_file_path: MetaFilePath = MetaFilePath("meta.yaml")
|
||||
spoiler: Spoiler = Spoiler(3)
|
||||
|
||||
@@ -201,7 +201,7 @@ if is_windows:
|
||||
icon=resolve_icon(c.icon),
|
||||
))
|
||||
|
||||
extra_data = ["LICENSE", "data", "SNI"]
|
||||
extra_data = ["LICENSE", "data", "EnemizerCLI", "SNI"]
|
||||
extra_libs = ["libssl.so", "libcrypto.so"] if is_linux else []
|
||||
|
||||
|
||||
@@ -456,8 +456,9 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
|
||||
for world_directory in folders_to_remove)
|
||||
else:
|
||||
# make sure extra programs are executable
|
||||
enemizer_exe = self.buildfolder / 'EnemizerCLI/EnemizerCLI.Core'
|
||||
sni_exe = self.buildfolder / 'SNI/sni'
|
||||
extra_exes = (sni_exe,)
|
||||
extra_exes = (enemizer_exe, sni_exe)
|
||||
for extra_exe in extra_exes:
|
||||
if extra_exe.is_file():
|
||||
extra_exe.chmod(0o755)
|
||||
|
||||
@@ -54,7 +54,7 @@ class TestImplemented(unittest.TestCase):
|
||||
|
||||
def test_no_failed_world_loads(self):
|
||||
if failed_world_loads:
|
||||
self.fail(f"The following worlds failed to load: {failed_world_loads.keys()}")
|
||||
self.fail(f"The following worlds failed to load: {failed_world_loads}")
|
||||
|
||||
def test_prefill_items(self):
|
||||
"""Test that every world can reach every location from allstate before pre_fill."""
|
||||
|
||||
@@ -12,7 +12,6 @@ from rule_builder.field_resolvers import FieldResolver, FromOption, FromWorldAtt
|
||||
from rule_builder.options import Operator, OptionFilter
|
||||
from rule_builder.rules import (
|
||||
And,
|
||||
AtLeast,
|
||||
CanReachEntrance,
|
||||
CanReachLocation,
|
||||
CanReachRegion,
|
||||
@@ -159,14 +158,6 @@ class CachedRuleBuilderTestCase(RuleBuilderTestCase):
|
||||
|
||||
@classvar_matrix(
|
||||
rules=(
|
||||
(
|
||||
And(),
|
||||
True_.Resolved(player=1)
|
||||
),
|
||||
(
|
||||
Or(),
|
||||
False_.Resolved(player=1)
|
||||
),
|
||||
(
|
||||
And(Has("A", 1), Has("A", 2)),
|
||||
Has.Resolved("A", 2, player=1),
|
||||
@@ -259,40 +250,6 @@ class CachedRuleBuilderTestCase(RuleBuilderTestCase):
|
||||
Or(HasAnyCount({"A": 1, "B": 2}), HasAnyCount({"A": 2, "B": 2})),
|
||||
HasAnyCount.Resolved((("A", 1), ("B", 2)), player=1),
|
||||
),
|
||||
(
|
||||
AtLeast(0, Has("A")),
|
||||
True_.Resolved(player=1),
|
||||
),
|
||||
(
|
||||
AtLeast(3, True_(), Has("A"), Has("B"), Has("C")),
|
||||
AtLeast.Resolved(
|
||||
(Has.Resolved("A", player=1), Has.Resolved("B", player=1), Has.Resolved("C", player=1)), 2, player=1
|
||||
),
|
||||
),
|
||||
(
|
||||
AtLeast(2, False_(), Has("A"), Has("B"), Has("C")),
|
||||
AtLeast.Resolved(
|
||||
(Has.Resolved("A", player=1), Has.Resolved("B", player=1), Has.Resolved("C", player=1)), 2, player=1
|
||||
),
|
||||
),
|
||||
(
|
||||
AtLeast(2, True_(), True_(), Has("A")),
|
||||
True_.Resolved(player=1),
|
||||
),
|
||||
(
|
||||
AtLeast(3, Has("A"), Has("B")),
|
||||
False_.Resolved(player=1),
|
||||
),
|
||||
(
|
||||
# This test will fail when Or(Rule, Rule) will be optimized to Rule
|
||||
AtLeast(1, Rule(), Rule()),
|
||||
Or.Resolved((Rule.Resolved(player=1), Rule.Resolved(player=1)), player=1),
|
||||
),
|
||||
(
|
||||
# This test will fail when And(Rule, Rule) will be optimized to Rule
|
||||
AtLeast(2, Rule(), Rule()),
|
||||
And.Resolved((Rule.Resolved(player=1), Rule.Resolved(player=1)), player=1),
|
||||
),
|
||||
)
|
||||
)
|
||||
class TestSimplify(RuleBuilderTestCase):
|
||||
@@ -459,15 +416,6 @@ class TestHashes(RuleBuilderTestCase):
|
||||
rule2 = HasAll("2", "2", "2", "1")
|
||||
self.assertEqual(hash(rule1.resolve(world)), hash(rule2.resolve(world)))
|
||||
|
||||
def test_hash_collision(self) -> None:
|
||||
multiworld = setup_solo_multiworld(self.world_cls, steps=("generate_early",), seed=0)
|
||||
world = multiworld.worlds[1]
|
||||
rule1 = Has("A", count=1).resolve(world)
|
||||
rule2 = Has("A", count=1 << 61).resolve(world)
|
||||
self.assertEqual(hash(rule1), hash(rule2))
|
||||
self.assertNotEqual(rule1, rule2)
|
||||
self.assertNotEqual(id(rule1), id(rule2))
|
||||
|
||||
|
||||
class TestCaching(CachedRuleBuilderTestCase):
|
||||
multiworld: MultiWorld # pyright: ignore[reportUninitializedInstanceVariable]
|
||||
@@ -674,24 +622,6 @@ class TestRules(RuleBuilderTestCase):
|
||||
self.state.remove(item)
|
||||
self.assertFalse(resolved_rule(self.state))
|
||||
|
||||
def test_at_least(self) -> None:
|
||||
# Has has to be relied on as True_ and False_ would be optimized out
|
||||
rule = AtLeast(2, Has("Item 1"), Has("Item 1"), Has("Item 2"), Has("Item 3"))
|
||||
resolved_rule = rule.resolve(self.world)
|
||||
self.world.register_rule_dependencies(resolved_rule)
|
||||
item1 = self.world.create_item("Item 1")
|
||||
item2 = self.world.create_item("Item 2")
|
||||
item3 = self.world.create_item("Item 3")
|
||||
self.assertFalse(resolved_rule(self.state))
|
||||
self.state.collect(item1)
|
||||
self.assertTrue(resolved_rule(self.state))
|
||||
self.state.collect(item2)
|
||||
self.assertTrue(resolved_rule(self.state))
|
||||
self.state.remove(item1)
|
||||
self.assertFalse(resolved_rule(self.state))
|
||||
self.state.collect(item3)
|
||||
self.assertTrue(resolved_rule(self.state))
|
||||
|
||||
def test_has_all(self) -> None:
|
||||
rule = HasAll("Item 1", "Item 2")
|
||||
resolved_rule = rule.resolve(self.world)
|
||||
@@ -867,13 +797,8 @@ class TestSerialization(RuleBuilderTestCase):
|
||||
OptionFilter(ChoiceOption, ChoiceOption.option_second, "ge"),
|
||||
],
|
||||
),
|
||||
AtLeast(
|
||||
FromWorldAttr("instance_data.at_least_requirement"),
|
||||
Has("i15", count=2),
|
||||
HasGroup("g2", count=3),
|
||||
),
|
||||
CanReachEntrance("e1"),
|
||||
HasGroupUnique("g3", count=5),
|
||||
HasGroupUnique("g2", count=5),
|
||||
)
|
||||
|
||||
rule_dict: ClassVar[dict[str, Any]] = {
|
||||
@@ -997,29 +922,6 @@ class TestSerialization(RuleBuilderTestCase):
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"rule": "AtLeast",
|
||||
"options": [],
|
||||
"filtered_resolution": False,
|
||||
"count": {"resolver": "FromWorldAttr", "name": "instance_data.at_least_requirement"},
|
||||
"children": [
|
||||
{
|
||||
"rule": "Has",
|
||||
"options": [],
|
||||
"filtered_resolution": False,
|
||||
"args": {
|
||||
"item_name": "i15",
|
||||
"count": 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
"rule": "HasGroup",
|
||||
"options": [],
|
||||
"filtered_resolution": False,
|
||||
"args": {"item_name_group": "g2", "count": 3},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"rule": "CanReachEntrance",
|
||||
"options": [],
|
||||
@@ -1030,7 +932,7 @@ class TestSerialization(RuleBuilderTestCase):
|
||||
"rule": "HasGroupUnique",
|
||||
"options": [],
|
||||
"filtered_resolution": False,
|
||||
"args": {"item_name_group": "g3", "count": 5},
|
||||
"args": {"item_name_group": "g2", "count": 5},
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1062,15 +964,9 @@ class TestExplain(RuleBuilderTestCase):
|
||||
),
|
||||
player=1,
|
||||
),
|
||||
AtLeast.Resolved(
|
||||
children=(
|
||||
HasAllCounts.Resolved((("Item 6", 1), ("Item 7", 5)), player=1),
|
||||
HasAnyCount.Resolved((("Item 8", 2), ("Item 9", 3)), player=1),
|
||||
HasFromList.Resolved(("Item 10", "Item 11", "Item 12"), count=2, player=1),
|
||||
),
|
||||
count=2,
|
||||
player=1,
|
||||
),
|
||||
HasAllCounts.Resolved((("Item 6", 1), ("Item 7", 5)), player=1),
|
||||
HasAnyCount.Resolved((("Item 8", 2), ("Item 9", 3)), player=1),
|
||||
HasFromList.Resolved(("Item 10", "Item 11", "Item 12"), count=2, player=1),
|
||||
HasFromListUnique.Resolved(("Item 13", "Item 14"), player=1),
|
||||
HasGroup.Resolved("Group 1", ("Item 15", "Item 16", "Item 17"), player=1),
|
||||
HasGroupUnique.Resolved("Group 2", ("Item 18", "Item 19"), count=2, player=1),
|
||||
@@ -1135,9 +1031,6 @@ class TestExplain(RuleBuilderTestCase):
|
||||
{"type": "text", "text": ")"},
|
||||
{"type": "text", "text": ")"},
|
||||
{"type": "text", "text": " & "},
|
||||
{"type": "text", "text": "At least "},
|
||||
{"type": "color", "color": "cyan", "text": "0/2"},
|
||||
{"type": "text", "text": " of ("},
|
||||
{"type": "text", "text": "Missing "},
|
||||
{"type": "color", "color": "cyan", "text": "some"},
|
||||
{"type": "text", "text": " of ("},
|
||||
@@ -1148,7 +1041,7 @@ class TestExplain(RuleBuilderTestCase):
|
||||
{"type": "color", "color": "salmon", "text": "Item 7"},
|
||||
{"type": "text", "text": " x5"},
|
||||
{"type": "text", "text": ")"},
|
||||
{"type": "text", "text": ", "},
|
||||
{"type": "text", "text": " & "},
|
||||
{"type": "text", "text": "Missing "},
|
||||
{"type": "color", "color": "cyan", "text": "all"},
|
||||
{"type": "text", "text": " of ("},
|
||||
@@ -1159,7 +1052,7 @@ class TestExplain(RuleBuilderTestCase):
|
||||
{"type": "color", "color": "salmon", "text": "Item 9"},
|
||||
{"type": "text", "text": " x3"},
|
||||
{"type": "text", "text": ")"},
|
||||
{"type": "text", "text": ", "},
|
||||
{"type": "text", "text": " & "},
|
||||
{"type": "text", "text": "Has "},
|
||||
{"type": "color", "color": "salmon", "text": "0/2"},
|
||||
{"type": "text", "text": " items from ("},
|
||||
@@ -1170,7 +1063,6 @@ class TestExplain(RuleBuilderTestCase):
|
||||
{"type": "text", "text": ", "},
|
||||
{"type": "color", "color": "salmon", "text": "Item 12"},
|
||||
{"type": "text", "text": ")"},
|
||||
{"type": "text", "text": ")"},
|
||||
{"type": "text", "text": " & "},
|
||||
{"type": "text", "text": "Has "},
|
||||
{"type": "color", "color": "salmon", "text": "0/1"},
|
||||
@@ -1237,9 +1129,6 @@ class TestExplain(RuleBuilderTestCase):
|
||||
{"type": "text", "text": ")"},
|
||||
{"type": "text", "text": ")"},
|
||||
{"type": "text", "text": " & "},
|
||||
{"type": "text", "text": "At least "},
|
||||
{"type": "color", "color": "cyan", "text": "3/2"},
|
||||
{"type": "text", "text": " of ("},
|
||||
{"type": "text", "text": "Has "},
|
||||
{"type": "color", "color": "cyan", "text": "all"},
|
||||
{"type": "text", "text": " of ("},
|
||||
@@ -1250,7 +1139,7 @@ class TestExplain(RuleBuilderTestCase):
|
||||
{"type": "color", "color": "green", "text": "Item 7"},
|
||||
{"type": "text", "text": " x5"},
|
||||
{"type": "text", "text": ")"},
|
||||
{"type": "text", "text": ", "},
|
||||
{"type": "text", "text": " & "},
|
||||
{"type": "text", "text": "Has "},
|
||||
{"type": "color", "color": "cyan", "text": "some"},
|
||||
{"type": "text", "text": " of ("},
|
||||
@@ -1261,7 +1150,7 @@ class TestExplain(RuleBuilderTestCase):
|
||||
{"type": "color", "color": "green", "text": "Item 9"},
|
||||
{"type": "text", "text": " x3"},
|
||||
{"type": "text", "text": ")"},
|
||||
{"type": "text", "text": ", "},
|
||||
{"type": "text", "text": " & "},
|
||||
{"type": "text", "text": "Has "},
|
||||
{"type": "color", "color": "green", "text": "30/2"},
|
||||
{"type": "text", "text": " items from ("},
|
||||
@@ -1272,7 +1161,6 @@ class TestExplain(RuleBuilderTestCase):
|
||||
{"type": "text", "text": ", "},
|
||||
{"type": "color", "color": "green", "text": "Item 12"},
|
||||
{"type": "text", "text": ")"},
|
||||
{"type": "text", "text": ")"},
|
||||
{"type": "text", "text": " & "},
|
||||
{"type": "text", "text": "Has "},
|
||||
{"type": "color", "color": "green", "text": "2/1"},
|
||||
@@ -1307,7 +1195,7 @@ class TestExplain(RuleBuilderTestCase):
|
||||
{"type": "color", "color": "salmon", "text": "False"},
|
||||
{"type": "text", "text": ")"},
|
||||
]
|
||||
self.assertEqual(self.resolved_rule.explain_json(self.state), expected)
|
||||
assert self.resolved_rule.explain_json(self.state) == expected
|
||||
|
||||
def test_explain_json_without_state(self) -> None:
|
||||
expected: list[JSONMessagePart] = [
|
||||
@@ -1335,9 +1223,6 @@ class TestExplain(RuleBuilderTestCase):
|
||||
{"type": "text", "text": ")"},
|
||||
{"type": "text", "text": ")"},
|
||||
{"type": "text", "text": " & "},
|
||||
{"type": "text", "text": "At least "},
|
||||
{"type": "color", "color": "cyan", "text": "2"},
|
||||
{"type": "text", "text": " of ("},
|
||||
{"type": "text", "text": "Has "},
|
||||
{"type": "color", "color": "cyan", "text": "all"},
|
||||
{"type": "text", "text": " of ("},
|
||||
@@ -1347,7 +1232,7 @@ class TestExplain(RuleBuilderTestCase):
|
||||
{"type": "item_name", "flags": 1, "text": "Item 7", "player": 1},
|
||||
{"type": "text", "text": " x5"},
|
||||
{"type": "text", "text": ")"},
|
||||
{"type": "text", "text": ", "},
|
||||
{"type": "text", "text": " & "},
|
||||
{"type": "text", "text": "Has "},
|
||||
{"type": "color", "color": "cyan", "text": "any"},
|
||||
{"type": "text", "text": " of ("},
|
||||
@@ -1357,7 +1242,7 @@ class TestExplain(RuleBuilderTestCase):
|
||||
{"type": "item_name", "flags": 1, "text": "Item 9", "player": 1},
|
||||
{"type": "text", "text": " x3"},
|
||||
{"type": "text", "text": ")"},
|
||||
{"type": "text", "text": ", "},
|
||||
{"type": "text", "text": " & "},
|
||||
{"type": "text", "text": "Has "},
|
||||
{"type": "color", "color": "cyan", "text": "2"},
|
||||
{"type": "text", "text": "x items from ("},
|
||||
@@ -1367,7 +1252,6 @@ class TestExplain(RuleBuilderTestCase):
|
||||
{"type": "text", "text": ", "},
|
||||
{"type": "item_name", "flags": 1, "text": "Item 12", "player": 1},
|
||||
{"type": "text", "text": ")"},
|
||||
{"type": "text", "text": ")"},
|
||||
{"type": "text", "text": " & "},
|
||||
{"type": "text", "text": "Has "},
|
||||
{"type": "color", "color": "cyan", "text": "1"},
|
||||
@@ -1401,16 +1285,16 @@ class TestExplain(RuleBuilderTestCase):
|
||||
{"type": "color", "color": "salmon", "text": "False"},
|
||||
{"type": "text", "text": ")"},
|
||||
]
|
||||
self.assertEqual(self.resolved_rule.explain_json(), expected)
|
||||
assert self.resolved_rule.explain_json() == expected
|
||||
|
||||
def test_explain_str_with_state_no_items(self) -> None:
|
||||
expected = (
|
||||
"((Missing 4x Item 1",
|
||||
"| Missing some of (Missing: Item 2, Item 3)",
|
||||
"| Missing all of (Missing: Item 4, Item 5))",
|
||||
"& At least 0/2 of (Missing some of (Missing: Item 6 x1, Item 7 x5),",
|
||||
"Missing all of (Missing: Item 8 x2, Item 9 x3),",
|
||||
"Has 0/2 items from (Missing: Item 10, Item 11, Item 12))",
|
||||
"& Missing some of (Missing: Item 6 x1, Item 7 x5)",
|
||||
"& Missing all of (Missing: Item 8 x2, Item 9 x3)",
|
||||
"& Has 0/2 items from (Missing: Item 10, Item 11, Item 12)",
|
||||
"& Has 0/1 unique items from (Missing: Item 13, Item 14)",
|
||||
"& Has 0/1 items from Group 1",
|
||||
"& Has 0/2 unique items from Group 2",
|
||||
@@ -1420,7 +1304,7 @@ class TestExplain(RuleBuilderTestCase):
|
||||
"& True",
|
||||
"& False)",
|
||||
)
|
||||
self.assertEqual(self.resolved_rule.explain_str(self.state), " ".join(expected))
|
||||
assert self.resolved_rule.explain_str(self.state) == " ".join(expected)
|
||||
|
||||
def test_explain_str_with_state_all_items(self) -> None:
|
||||
self._collect_all()
|
||||
@@ -1429,9 +1313,9 @@ class TestExplain(RuleBuilderTestCase):
|
||||
"((Has 4x Item 1",
|
||||
"| Has all of (Found: Item 2, Item 3)",
|
||||
"| Has some of (Found: Item 4, Item 5))",
|
||||
"& At least 3/2 of (Has all of (Found: Item 6 x1, Item 7 x5),",
|
||||
"Has some of (Found: Item 8 x2, Item 9 x3),",
|
||||
"Has 30/2 items from (Found: Item 10, Item 11, Item 12))",
|
||||
"& Has all of (Found: Item 6 x1, Item 7 x5)",
|
||||
"& Has some of (Found: Item 8 x2, Item 9 x3)",
|
||||
"& Has 30/2 items from (Found: Item 10, Item 11, Item 12)",
|
||||
"& Has 2/1 unique items from (Found: Item 13, Item 14)",
|
||||
"& Has 30/1 items from Group 1",
|
||||
"& Has 2/2 unique items from Group 2",
|
||||
@@ -1441,16 +1325,16 @@ class TestExplain(RuleBuilderTestCase):
|
||||
"& True",
|
||||
"& False)",
|
||||
)
|
||||
self.assertEqual(self.resolved_rule.explain_str(self.state), " ".join(expected))
|
||||
assert self.resolved_rule.explain_str(self.state) == " ".join(expected)
|
||||
|
||||
def test_explain_str_without_state(self) -> None:
|
||||
expected = (
|
||||
"((Has 4x Item 1",
|
||||
"| Has all of (Item 2, Item 3)",
|
||||
"| Has any of (Item 4, Item 5))",
|
||||
"& At least 2 of (Has all of (Item 6 x1, Item 7 x5),",
|
||||
"Has any of (Item 8 x2, Item 9 x3),",
|
||||
"Has 2x items from (Item 10, Item 11, Item 12))",
|
||||
"& Has all of (Item 6 x1, Item 7 x5)",
|
||||
"& Has any of (Item 8 x2, Item 9 x3)",
|
||||
"& Has 2x items from (Item 10, Item 11, Item 12)",
|
||||
"& Has a unique item from (Item 13, Item 14)",
|
||||
"& Has an item from Group 1",
|
||||
"& Has 2x unique items from Group 2",
|
||||
@@ -1460,16 +1344,16 @@ class TestExplain(RuleBuilderTestCase):
|
||||
"& True",
|
||||
"& False)",
|
||||
)
|
||||
self.assertEqual(self.resolved_rule.explain_str(), " ".join(expected))
|
||||
assert self.resolved_rule.explain_str() == " ".join(expected)
|
||||
|
||||
def test_str(self) -> None:
|
||||
expected = (
|
||||
"((Has 4x Item 1",
|
||||
"| Has all of (Item 2, Item 3)",
|
||||
"| Has any of (Item 4, Item 5))",
|
||||
"& At least 2 of (Has all of (Item 6 x1, Item 7 x5),",
|
||||
"Has any of (Item 8 x2, Item 9 x3),",
|
||||
"Has 2x items from (Item 10, Item 11, Item 12))",
|
||||
"& Has all of (Item 6 x1, Item 7 x5)",
|
||||
"& Has any of (Item 8 x2, Item 9 x3)",
|
||||
"& Has 2x items from (Item 10, Item 11, Item 12)",
|
||||
"& Has a unique item from (Item 13, Item 14)",
|
||||
"& Has an item from Group 1",
|
||||
"& Has 2x unique items from Group 2",
|
||||
@@ -1479,7 +1363,7 @@ class TestExplain(RuleBuilderTestCase):
|
||||
"& True",
|
||||
"& False)",
|
||||
)
|
||||
self.assertEqual(str(self.resolved_rule), " ".join(expected))
|
||||
assert str(self.resolved_rule) == " ".join(expected)
|
||||
|
||||
|
||||
@classvar_matrix(
|
||||
|
||||
@@ -33,9 +33,4 @@ class TestBase(unittest.TestCase):
|
||||
cls.app = raw_app
|
||||
|
||||
def setUp(self) -> None:
|
||||
from WebHostLib.models import db
|
||||
from pony.orm import db_session
|
||||
with db_session:
|
||||
for entity in db.entities.values():
|
||||
entity.select().delete(bulk=True)
|
||||
self.client = self.app.test_client()
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
from datetime import timedelta
|
||||
from uuid import UUID, uuid4
|
||||
from pony.orm import db_session, commit
|
||||
|
||||
from Utils import utcnow
|
||||
from WebHostLib.autolauncher import cleanup
|
||||
from WebHostLib.models import Room, Seed, Slot
|
||||
from . import TestBase
|
||||
|
||||
|
||||
class TestCleanup(TestBase):
|
||||
def test_cleanup_unowned(self) -> None:
|
||||
with db_session:
|
||||
s1 = Seed(id=uuid4(), multidata=b"", owner=UUID(int=0))
|
||||
Room(id=uuid4(), owner=UUID(int=0), seed=s1)
|
||||
|
||||
s2 = Seed(id=uuid4(), multidata=b"", owner=uuid4()) # Owned
|
||||
Room(id=uuid4(), owner=UUID(int=0), seed=s2) # Unowned room of owned seed
|
||||
|
||||
Seed(id=uuid4(), multidata=b"", owner=UUID(int=0)) # Unowned seed with no rooms
|
||||
|
||||
commit()
|
||||
|
||||
cleanup({"ROOM_AUTO_DELETE": 0})
|
||||
|
||||
with db_session:
|
||||
self.assertEqual(Room.select().count(), 0) # Both rooms were unowned
|
||||
self.assertEqual(Seed.select().count(), 1) # s2 is owned
|
||||
self.assertIsNotNone(Seed.get(id=s2.id))
|
||||
|
||||
def test_cleanup_auto_delete(self) -> None:
|
||||
now = utcnow()
|
||||
old_time = now - timedelta(days=10)
|
||||
recent_time = now - timedelta(days=2)
|
||||
|
||||
with db_session:
|
||||
# Case 1: Old room, owned
|
||||
s1 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=old_time)
|
||||
r1 = Room(id=uuid4(), owner=uuid4(), seed=s1, last_activity=old_time)
|
||||
|
||||
# Case 2: Recent room, owned
|
||||
s2 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=old_time)
|
||||
r2 = Room(id=uuid4(), owner=uuid4(), seed=s2, last_activity=recent_time)
|
||||
|
||||
# Case 3: Old seed, no rooms, owned
|
||||
s3 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=old_time)
|
||||
|
||||
# Case 4: Recent seed, no rooms, owned
|
||||
s4 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=recent_time)
|
||||
|
||||
# Case 5: Old seed with recent room (should not be deleted)
|
||||
s5 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=old_time)
|
||||
r5 = Room(id=uuid4(), owner=uuid4(), seed=s5, last_activity=recent_time)
|
||||
|
||||
commit()
|
||||
|
||||
# Delete items older than 5 days
|
||||
cleanup({"ROOM_AUTO_DELETE": 5})
|
||||
|
||||
with db_session:
|
||||
self.assertIsNone(Room.get(id=r1.id), "Old room should be deleted")
|
||||
self.assertIsNotNone(Room.get(id=r2.id), "Recent room should NOT be deleted")
|
||||
self.assertIsNone(Seed.get(id=s3.id), "Old seed without rooms should be deleted")
|
||||
self.assertIsNotNone(Seed.get(id=s4.id), "Recent seed without rooms should NOT be deleted")
|
||||
self.assertIsNotNone(Seed.get(id=s5.id), "Old seed with recent room should NOT be deleted")
|
||||
self.assertIsNotNone(Room.get(id=r5.id), "Recent room for old seed should NOT be deleted")
|
||||
|
||||
# Seeds are deleted if they have NO rooms AND are old.
|
||||
# After r1 is deleted, s1 has no rooms. Since it's old, it should be deleted.
|
||||
self.assertIsNone(Seed.get(id=s1.id), "Old seed whose only room was deleted should be deleted")
|
||||
|
||||
def test_cleanup_disabled(self) -> None:
|
||||
now = utcnow()
|
||||
old_time = now - timedelta(days=10)
|
||||
|
||||
with db_session:
|
||||
s1 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=old_time)
|
||||
r1 = Room(id=uuid4(), owner=uuid4(), seed=s1, last_activity=old_time)
|
||||
commit()
|
||||
|
||||
cleanup({"ROOM_AUTO_DELETE": 0})
|
||||
|
||||
with db_session:
|
||||
self.assertIsNotNone(Room.get(id=r1.id), "Room should NOT be deleted when auto-delete is 0")
|
||||
self.assertIsNotNone(Seed.get(id=s1.id), "Seed should NOT be deleted when auto-delete is 0")
|
||||
|
||||
def test_cleanup_slots(self) -> None:
|
||||
now = utcnow()
|
||||
old_time = now - timedelta(days=10)
|
||||
|
||||
with db_session:
|
||||
s1 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=old_time)
|
||||
slot1 = Slot(player_id=1, player_name="P1", seed=s1, game="TestGame")
|
||||
|
||||
s2 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=now)
|
||||
slot2 = Slot(player_id=2, player_name="P2", seed=s2, game="TestGame")
|
||||
|
||||
commit()
|
||||
|
||||
# Delete items older than 5 days
|
||||
cleanup({"ROOM_AUTO_DELETE": 5})
|
||||
|
||||
with db_session:
|
||||
self.assertIsNone(Seed.get(id=s1.id), "Old seed should be deleted")
|
||||
self.assertIsNone(Slot.get(id=slot1.id), "Slot of deleted seed should be deleted")
|
||||
self.assertIsNotNone(Seed.get(id=s2.id), "Recent seed should NOT be deleted")
|
||||
self.assertIsNotNone(Slot.get(id=slot2.id), "Slot of recent seed should NOT be deleted")
|
||||
+12
-6
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
import hashlib
|
||||
import logging
|
||||
import pathlib
|
||||
@@ -20,6 +21,15 @@ if TYPE_CHECKING:
|
||||
from NetUtils import GamesPackage, MultiData
|
||||
from settings import Group
|
||||
|
||||
|
||||
class FillerReason(enum.StrEnum):
|
||||
undefined = enum.auto()
|
||||
item_link = enum.auto()
|
||||
panic_fill = enum.auto()
|
||||
start_inventory_from_pool = enum.auto()
|
||||
world = enum.auto()
|
||||
|
||||
|
||||
perf_logger = logging.getLogger("performance")
|
||||
|
||||
|
||||
@@ -353,8 +363,6 @@ class World(metaclass=AutoWorldRegister):
|
||||
"""path it was loaded from"""
|
||||
world_version: ClassVar[Version] = Version(0, 0, 0)
|
||||
"""Optional world version loaded from archipelago.json"""
|
||||
manifest: ClassVar[dict[str, Any]] = {}
|
||||
"""Mapping of the world's archipelago.json manifest. Use game and world_version attrs instead for those values."""
|
||||
|
||||
def __init__(self, multiworld: "MultiWorld", player: int):
|
||||
assert multiworld is not None
|
||||
@@ -514,9 +522,7 @@ class World(metaclass=AutoWorldRegister):
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
"""
|
||||
If core AP removes an item from your item pool, this method is called to choose a replacement item
|
||||
so item count and location count remain equal.
|
||||
For example: plando, item_links and start_inventory_from_pool are features that may cause this.
|
||||
Called when the item pool needs to be filled with additional items to match location count.
|
||||
|
||||
Any returned item name must be for a "repeatable" item, i.e. one that it's okay to generate arbitrarily many of.
|
||||
For most worlds this will be one or more of your filler items, but the classification of these items
|
||||
@@ -581,7 +587,7 @@ class World(metaclass=AutoWorldRegister):
|
||||
pass
|
||||
|
||||
# following methods should not need to be overridden.
|
||||
def create_filler(self) -> "Item":
|
||||
def create_filler(self, reason: FillerReason = FillerReason.undefined) -> "Item":
|
||||
return self.create_item(self.get_filler_item_name())
|
||||
|
||||
# convenience methods
|
||||
|
||||
+5
-16
@@ -11,7 +11,7 @@ import json
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
from typing import List, Sequence
|
||||
from zipfile import ZipFile, BadZipFile
|
||||
from zipfile import BadZipFile
|
||||
|
||||
from NetUtils import DataPackage
|
||||
from Utils import local_path, user_path, Version, version_tuple, tuplize_version, messagebox
|
||||
@@ -33,7 +33,7 @@ __all__ = [
|
||||
]
|
||||
|
||||
|
||||
failed_world_loads: dict[str, str] = {}
|
||||
failed_world_loads: List[str] = []
|
||||
|
||||
|
||||
@dataclasses.dataclass(order=True)
|
||||
@@ -68,9 +68,8 @@ class WorldSource:
|
||||
print(f"Could not load world {self}:", file=file_like)
|
||||
traceback.print_exc(file=file_like)
|
||||
file_like.seek(0)
|
||||
reason = file_like.read()
|
||||
logging.exception(reason)
|
||||
failed_world_loads[os.path.basename(self.path).rsplit(".", 1)[0]] = reason
|
||||
logging.exception(file_like.read())
|
||||
failed_world_loads.append(os.path.basename(self.path).rsplit(".", 1)[0])
|
||||
return False
|
||||
|
||||
|
||||
@@ -119,7 +118,6 @@ for world_source in world_sources:
|
||||
game = manifest.get("game")
|
||||
if game in AutoWorldRegister.world_types:
|
||||
AutoWorldRegister.world_types[game].world_version = tuplize_version(manifest.get("world_version", "0.0.0"))
|
||||
AutoWorldRegister.world_types[game].manifest = manifest
|
||||
|
||||
if apworlds:
|
||||
# encapsulation for namespace / gc purposes
|
||||
@@ -130,7 +128,7 @@ if apworlds:
|
||||
|
||||
def fail_world(game_name: str, reason: str, add_as_failed_to_load: bool = True) -> None:
|
||||
if add_as_failed_to_load:
|
||||
failed_world_loads[game_name] = reason
|
||||
failed_world_loads.append(game_name)
|
||||
logging.warning(reason)
|
||||
|
||||
for apworld_source in apworlds:
|
||||
@@ -201,15 +199,6 @@ if apworlds:
|
||||
# world could fail to load at this point
|
||||
if apworld.world_version:
|
||||
AutoWorldRegister.world_types[apworld.game].world_version = apworld.world_version
|
||||
|
||||
assert apworld.path
|
||||
with ZipFile(apworld.path, "r") as zf:
|
||||
manifest = apworld.read_contents(zf)
|
||||
# version/compatible_version shouldn't be needed by world, makes it consistent with folder world
|
||||
manifest.pop("version", None)
|
||||
manifest.pop("compatible_version", None)
|
||||
AutoWorldRegister.world_types[apworld.game].manifest = manifest
|
||||
|
||||
load_apworlds()
|
||||
del load_apworlds
|
||||
|
||||
|
||||
@@ -50,17 +50,16 @@ make sure ***Enable Developer Console*** is checked in Game Settings and press t
|
||||
|
||||
## FAQ/Common Issues
|
||||
|
||||
### The game is crashing on startup repeatedly!
|
||||
This is a common issue on older versions of the game, caused by the game failing to interface with the Steam Workshop.
|
||||
To fix it you can try the following (from least to most effort required)
|
||||
- Subscribe to any random workshop mod, then unsubscribe from it
|
||||
- Restart Steam
|
||||
- Restart your computer
|
||||
- Delete the game's config directory from the files `steamapps/common/HatinTime/HatinTimeGame/Config` then verify the game files
|
||||
- Reinstall the game
|
||||
### The game is not connecting when starting a new save!
|
||||
For unknown reasons, the mod will randomly disable itself in the mod menu. To fix this, go to the Mods menu
|
||||
(rocket icon) in-game, and re-enable the mod.
|
||||
|
||||
### Why do relics disappear from the stands in the Spaceship after they're completed?
|
||||
This is intentional behaviour. Because of how randomizer logic works, there is no way to predict the order that
|
||||
a player will place their relics. Since there are a limited amount of relic stands in the Spaceship, relics are removed
|
||||
after being completed to allow for the placement of more relics without being potentially locked out.
|
||||
The level that the relic set unlocked will stay unlocked.
|
||||
|
||||
### When I start a new save file, the intro cinematic doesn't get skipped, Hat Kid's body is missing and the mod doesn't work!
|
||||
There is a bug on older versions of A Hat in Time that causes save file creation to fail to work properly
|
||||
if you have too many save files. Delete them and it should fix the problem.
|
||||
|
||||
@@ -1,478 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from functools import lru_cache
|
||||
import hashlib
|
||||
import random
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from Utils import pc_to_snes, snes_to_pc
|
||||
from .enemizer_data.base_patch_data import ENEMIZER_BASE_PATCHES
|
||||
from .enemizer_data.symbols import ENEMIZER_SYMBOLS
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import ALTTPWorld
|
||||
from .Rom import LocalRom
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BossPatchData:
|
||||
pointer: tuple[int, int]
|
||||
graphics: int
|
||||
sprite_array: tuple[int, ...]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DungeonBossPatchData:
|
||||
room_id: int
|
||||
sprite_pointer_address: int
|
||||
shell_x: int
|
||||
shell_y: int
|
||||
clear_layer2: bool = False
|
||||
extra_sprites: tuple[int, ...] = ()
|
||||
gt_sprite_write_address: Optional[int] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class RoomObjectTable:
|
||||
header_byte_0: int
|
||||
header_byte_1: int
|
||||
layer_1_objects: list[bytes] = field(default_factory=list)
|
||||
layer_1_doors: list[bytes] = field(default_factory=list)
|
||||
layer_2_objects: list[bytes] = field(default_factory=list)
|
||||
layer_2_doors: list[bytes] = field(default_factory=list)
|
||||
layer_3_objects: list[bytes] = field(default_factory=list)
|
||||
layer_3_doors: list[bytes] = field(default_factory=list)
|
||||
|
||||
@classmethod
|
||||
def from_rom(cls, rom: "LocalRom", start_address: int) -> "RoomObjectTable":
|
||||
table = cls(rom.read_byte(start_address), rom.read_byte(start_address + 1))
|
||||
layers = (
|
||||
(table.layer_1_objects, table.layer_1_doors),
|
||||
(table.layer_2_objects, table.layer_2_doors),
|
||||
(table.layer_3_objects, table.layer_3_doors),
|
||||
)
|
||||
index = start_address + 2
|
||||
|
||||
for objects, doors in layers:
|
||||
is_door = False
|
||||
while True:
|
||||
if rom.read_bytes(index, 2) == bytearray((0xF0, 0xFF)):
|
||||
is_door = True
|
||||
index += 2
|
||||
continue
|
||||
if rom.read_bytes(index, 2) == bytearray((0xFF, 0xFF)):
|
||||
index += 2
|
||||
break
|
||||
if is_door:
|
||||
doors.append(bytes(rom.read_bytes(index, 2)))
|
||||
index += 2
|
||||
else:
|
||||
objects.append(bytes(rom.read_bytes(index, 3)))
|
||||
index += 3
|
||||
|
||||
return table
|
||||
|
||||
def add_shell(self, x: int, y: int, clear_layer_2: bool, shell_id: int) -> None:
|
||||
self.header_byte_0 = 0xF0
|
||||
if clear_layer_2:
|
||||
self.layer_2_objects.clear()
|
||||
self.layer_2_objects.append(_build_subtype_3_object(x, y, shell_id))
|
||||
|
||||
def remove_shell(self, shell_id: int) -> None:
|
||||
self.layer_2_objects = [obj for obj in self.layer_2_objects if _object_id(obj) != shell_id]
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
output = bytearray((self.header_byte_0, self.header_byte_1))
|
||||
output.extend(self._serialize_layer(self.layer_1_objects, self.layer_1_doors, is_last_layer=False))
|
||||
output.extend(self._serialize_layer(self.layer_2_objects, self.layer_2_doors, is_last_layer=False))
|
||||
output.extend(self._serialize_layer(self.layer_3_objects, self.layer_3_doors, is_last_layer=True))
|
||||
return bytes(output)
|
||||
|
||||
@staticmethod
|
||||
def _serialize_layer(objects: list[bytes], doors: list[bytes], is_last_layer: bool) -> bytes:
|
||||
output = bytearray()
|
||||
for obj in objects:
|
||||
output.extend(obj)
|
||||
if is_last_layer or doors:
|
||||
output.extend((0xF0, 0xFF))
|
||||
for door in doors:
|
||||
output.extend(door)
|
||||
output.extend((0xFF, 0xFF))
|
||||
return bytes(output)
|
||||
|
||||
|
||||
BOSS_PATCH_DATA: dict[str, BossPatchData] = {
|
||||
"Armos": BossPatchData((0x87, 0xE8), 9, (0x05, 0x04, 0x53, 0x05, 0x07, 0x53, 0x05, 0x0A, 0x53,
|
||||
0x08, 0x0A, 0x53, 0x08, 0x07, 0x53, 0x08, 0x04, 0x53,
|
||||
0x08, 0xE7, 0x19)),
|
||||
"Arrghus": BossPatchData((0x97, 0xD9), 20, (0x07, 0x07, 0x8C, 0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D,
|
||||
0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D,
|
||||
0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D,
|
||||
0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D,
|
||||
0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D)),
|
||||
"Blind": BossPatchData((0x54, 0xE6), 32, (0x05, 0x09, 0xCE)),
|
||||
"Helmasaur": BossPatchData((0x49, 0xE0), 21, (0x06, 0x07, 0x92)),
|
||||
"Kholdstare": BossPatchData((0x01, 0xEA), 22, (0x05, 0x07, 0xA3, 0x05, 0x07, 0xA4, 0x05, 0x07, 0xA2)),
|
||||
"Lanmola": BossPatchData((0xCB, 0xDC), 11, (0x07, 0x06, 0x54, 0x07, 0x09, 0x54, 0x09, 0x07, 0x54)),
|
||||
"Moldorm": BossPatchData((0xC3, 0xD9), 12, (0x09, 0x09, 0x09)),
|
||||
"Mothula": BossPatchData((0x31, 0xDC), 26, (0x06, 0x08, 0x88)),
|
||||
"Trinexx": BossPatchData((0xBA, 0xE5), 23, (0x05, 0x07, 0xCB, 0x05, 0x07, 0xCC, 0x05, 0x07, 0xCD)),
|
||||
"Vitreous": BossPatchData((0x57, 0xE4), 22, (0x05, 0x07, 0xBD)),
|
||||
}
|
||||
|
||||
DUNGEON_BOSS_PATCH_DATA: dict[tuple[str, Optional[str]], DungeonBossPatchData] = {
|
||||
("Eastern Palace", None): DungeonBossPatchData(200, 0x04D7BE, 0x2B, 0x28),
|
||||
("Desert Palace", None): DungeonBossPatchData(51, 0x04D694, 0x0B, 0x28),
|
||||
("Tower of Hera", None): DungeonBossPatchData(7, 0x04D63C, 0x18, 0x16),
|
||||
("Palace of Darkness", None): DungeonBossPatchData(90, 0x04D6E2, 0x2B, 0x28),
|
||||
("Swamp Palace", None): DungeonBossPatchData(6, 0x04D63A, 0x0B, 0x28),
|
||||
("Skull Woods", None): DungeonBossPatchData(41, 0x04D680, 0x2B, 0x28),
|
||||
("Thieves Town", None): DungeonBossPatchData(172, 0x04D786, 0x2B, 0x28, clear_layer2=True),
|
||||
("Ice Palace", None): DungeonBossPatchData(222, 0x04D7EA, 0x2B, 0x08, clear_layer2=True),
|
||||
("Misery Mire", None): DungeonBossPatchData(144, 0x04D74E, 0x0B, 0x28, clear_layer2=True),
|
||||
("Turtle Rock", None): DungeonBossPatchData(164, 0x04D776, 0x0B, 0x28, clear_layer2=True),
|
||||
("Ganons Tower", "bottom"): DungeonBossPatchData(
|
||||
28, 0x04D666, 0x2B, 0x28, extra_sprites=(0x07, 0x07, 0xE3, 0x07, 0x08, 0xE3, 0x08, 0x07, 0xE3, 0x08, 0x08, 0xE3),
|
||||
gt_sprite_write_address=0x04D87E,
|
||||
),
|
||||
("Ganons Tower", "middle"): DungeonBossPatchData(
|
||||
108, 0x04D706, 0x0B, 0x28, extra_sprites=(0x18, 0x17, 0xD1, 0x1C, 0x03, 0xC5), gt_sprite_write_address=0x04D8B6,
|
||||
),
|
||||
("Ganons Tower", "top"): DungeonBossPatchData(77, 0x04D6C8, 0x18, 0x16),
|
||||
}
|
||||
|
||||
TRINEXX_SHELL_OBJECT_ID = 0xFF2
|
||||
KHOLDSTARE_SHELL_OBJECT_ID = 0xF95
|
||||
TRINEXX_VANILLA_ROOM_ID = 164
|
||||
KHOLDSTARE_VANILLA_ROOM_ID = 222
|
||||
ENEMY_HP_TABLE_ADDRESS = 0x6B173
|
||||
ENEMY_DAMAGE_TABLE_ADDRESS = 0x6B266
|
||||
HIDDEN_ENEMY_CHANCE_POOL_ADDRESS = 0xD7BBB
|
||||
DAMAGE_GROUP_TABLE_ADDRESS = 0x3742D
|
||||
RETRO_ARROW_REPLACEMENT_CHECK_ADDRESS = 0x301FC
|
||||
RETRO_RUPEE_REPLACEMENT_SPRITE_ID = 0xDA
|
||||
ARROW_REFILL_5_SPRITE_ID = 0xE1
|
||||
THIEF_SPRITE_ID = 0xC4
|
||||
THIEF_DEFAULT_HP = 4
|
||||
VANILLA_HIDDEN_ENEMY_CHANCE_POOL = (
|
||||
0x01, 0x01, 0x01, 0x01, 0x0F, 0x01, 0x01, 0x12,
|
||||
0x10, 0x01, 0x01, 0x01, 0x11, 0x01, 0x01, 0x03,
|
||||
)
|
||||
RANDOMIZED_HIDDEN_ENEMY_CHANCE_POOL = (
|
||||
0x01, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x12,
|
||||
0x0F, 0x01, 0x0F, 0x0F, 0x11, 0x0F, 0x0F, 0x03,
|
||||
)
|
||||
EXCLUDED_ENEMY_TABLE_SPRITE_IDS = frozenset({
|
||||
0x09, 0x53, 0x54, 0x70, 0x7A, 0x7B, 0x88, 0x89, 0x8C, 0x8D, 0x92,
|
||||
0xA2, 0xA3, 0xA4, 0xBD, 0xBE, 0xBF, 0xCB, 0xCC, 0xCD, 0xCE, 0xD6, 0xD7,
|
||||
})
|
||||
ENEMY_HEALTH_RANGE_BY_KEY = {
|
||||
"easy": (1, 4),
|
||||
"normal": (2, 15),
|
||||
"hard": (2, 25),
|
||||
"expert": (4, 50),
|
||||
}
|
||||
|
||||
_ENEMIZER_SYMBOLS: Optional[dict[str, int]] = None
|
||||
|
||||
BOSS_GFX_SHEET_INDEXES = {
|
||||
"Agahnim1": 0x8D,
|
||||
"Agahnim2": 0xB5,
|
||||
"Agahnim3": 0xC8,
|
||||
"Agahnim4": 0xB6,
|
||||
"ArmosKnight1": 0x90,
|
||||
"Ganon1": 0x94,
|
||||
"Ganon2": 0xA6,
|
||||
"Ganon3": 0xB4,
|
||||
"Ganon4": 0xB8,
|
||||
"Moldorm1": 0xA3,
|
||||
"Lanmola1": 0xA4,
|
||||
"Arrghus1": 0xAC,
|
||||
"Mothula1": 0xAB,
|
||||
"Helmasaure1": 0xAD,
|
||||
"Helmasaure2": 0xB1,
|
||||
"Blind1": 0xAE,
|
||||
"Kholdstare1": 0xAF,
|
||||
"Vitreous1": 0xB0,
|
||||
"Trinexx1": 0xB2,
|
||||
"Trinexx2": 0xB3,
|
||||
}
|
||||
|
||||
BOSS_GFX_TABLE = {
|
||||
"Agahnim1": (21, 190, 228),
|
||||
"Agahnim2": (22, 255, 135),
|
||||
"Agahnim3": (23, 220, 101),
|
||||
"Agahnim4": (23, 132, 92),
|
||||
"ArmosKnight1": (21, 206, 27),
|
||||
"Ganon1": (21, 227, 160),
|
||||
"Ganon2": (22, 186, 55),
|
||||
"Ganon3": (22, 250, 199),
|
||||
"Ganon4": (23, 142, 33),
|
||||
"Moldorm1": (22, 175, 152),
|
||||
"Lanmola1": (22, 180, 23),
|
||||
"Arrghus1": (22, 214, 147),
|
||||
"Mothula1": (22, 210, 84),
|
||||
"Helmasaure1": (22, 219, 114),
|
||||
"Helmasaure2": (22, 239, 177),
|
||||
"Blind1": (22, 224, 90),
|
||||
"Kholdstare1": (22, 230, 31),
|
||||
"Vitreous1": (22, 235, 9),
|
||||
"Trinexx1": (22, 243, 89),
|
||||
"Trinexx2": (22, 246, 35),
|
||||
}
|
||||
|
||||
TRINEXX_ICE_FLOOR_ROUTINE_ADDRESS = 0x04B37E
|
||||
TRINEXX_ICE_PROJECTILE_TILE_ADDRESS = 0xE7A5
|
||||
TILE_TRAP_FLOOR_TILE_ADDRESS = 0xF3BED
|
||||
|
||||
|
||||
def apply_enemizer_base_patch(rom: "LocalRom") -> None:
|
||||
for address, patch_data in _load_enemizer_base_patches():
|
||||
rom.write_bytes(address, patch_data)
|
||||
_apply_trinexx_room_fixes(rom)
|
||||
|
||||
def patch_bosses(world: "ALTTPWorld", rom: "LocalRom") -> None:
|
||||
dungeon_header_base = _get_enemizer_symbol("room_header_table")
|
||||
moved_room_object_base = _get_enemizer_symbol("modified_room_object_table")
|
||||
gt_dungeon_name = "Ganons Tower" if world.options.mode != "inverted" else "Inverted Ganons Tower"
|
||||
gt_dungeon = world.dungeons[gt_dungeon_name]
|
||||
|
||||
placements = (
|
||||
(world.dungeons["Eastern Palace"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Eastern Palace", None)]),
|
||||
(world.dungeons["Desert Palace"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Desert Palace", None)]),
|
||||
(world.dungeons["Tower of Hera"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Tower of Hera", None)]),
|
||||
(world.dungeons["Palace of Darkness"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Palace of Darkness", None)]),
|
||||
(world.dungeons["Swamp Palace"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Swamp Palace", None)]),
|
||||
(world.dungeons["Skull Woods"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Skull Woods", None)]),
|
||||
(world.dungeons["Thieves Town"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Thieves Town", None)]),
|
||||
(world.dungeons["Ice Palace"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Ice Palace", None)]),
|
||||
(world.dungeons["Misery Mire"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Misery Mire", None)]),
|
||||
(world.dungeons["Turtle Rock"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Turtle Rock", None)]),
|
||||
(gt_dungeon.bosses["bottom"].enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Ganons Tower", "bottom")]),
|
||||
(gt_dungeon.bosses["middle"].enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Ganons Tower", "middle")]),
|
||||
(gt_dungeon.bosses["top"].enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Ganons Tower", "top")]),
|
||||
)
|
||||
|
||||
modified_room_tables: dict[int, RoomObjectTable] = {}
|
||||
|
||||
for boss_name, dungeon_data in placements:
|
||||
boss_data = BOSS_PATCH_DATA[boss_name]
|
||||
rom.write_bytes(dungeon_data.sprite_pointer_address, boss_data.pointer)
|
||||
rom.write_byte(dungeon_header_base + (dungeon_data.room_id * 14) + 3, boss_data.graphics)
|
||||
|
||||
if boss_name == "Trinexx" and dungeon_data.room_id != TRINEXX_VANILLA_ROOM_ID:
|
||||
room_table = _get_room_object_table(rom, modified_room_tables, dungeon_data.room_id)
|
||||
room_table.add_shell(
|
||||
dungeon_data.shell_x,
|
||||
dungeon_data.shell_y - 2,
|
||||
dungeon_data.clear_layer2,
|
||||
TRINEXX_SHELL_OBJECT_ID,
|
||||
)
|
||||
rom.write_byte(dungeon_header_base + (dungeon_data.room_id * 14), 0x60)
|
||||
rom.write_byte(dungeon_header_base + (dungeon_data.room_id * 14) + 4, 0x04)
|
||||
|
||||
if boss_name == "Kholdstare" and dungeon_data.room_id != KHOLDSTARE_VANILLA_ROOM_ID:
|
||||
room_table = _get_room_object_table(rom, modified_room_tables, dungeon_data.room_id)
|
||||
room_table.add_shell(
|
||||
dungeon_data.shell_x,
|
||||
dungeon_data.shell_y,
|
||||
dungeon_data.clear_layer2,
|
||||
KHOLDSTARE_SHELL_OBJECT_ID,
|
||||
)
|
||||
rom.write_byte(dungeon_header_base + (dungeon_data.room_id * 14), 0xE0)
|
||||
rom.write_byte(dungeon_header_base + (dungeon_data.room_id * 14) + 4, 0x01)
|
||||
|
||||
if boss_name != "Trinexx" and dungeon_data.room_id == TRINEXX_VANILLA_ROOM_ID:
|
||||
_get_room_object_table(rom, modified_room_tables, dungeon_data.room_id).remove_shell(TRINEXX_SHELL_OBJECT_ID)
|
||||
|
||||
if boss_name != "Kholdstare" and dungeon_data.room_id == KHOLDSTARE_VANILLA_ROOM_ID:
|
||||
_get_room_object_table(rom, modified_room_tables, dungeon_data.room_id).remove_shell(KHOLDSTARE_SHELL_OBJECT_ID)
|
||||
|
||||
if dungeon_data.gt_sprite_write_address is not None:
|
||||
_write_gt_boss_sprite_block(rom, dungeon_data, boss_data)
|
||||
|
||||
write_address = moved_room_object_base
|
||||
for room_id in sorted(modified_room_tables):
|
||||
table_bytes = modified_room_tables[room_id].to_bytes()
|
||||
_write_room_object_pointer(rom, room_id, write_address)
|
||||
rom.write_bytes(write_address, table_bytes)
|
||||
write_address += len(table_bytes)
|
||||
|
||||
rom.write_byte(0x1B0101, 0x01)
|
||||
rom.write_byte(0x04DE81, 0x00)
|
||||
if world.dungeons["Thieves Town"].boss.enemizer_name == "Blind":
|
||||
rom.write_byte(0x04DE81, 0x06)
|
||||
rom.write_byte(0x1B0101, 0x00)
|
||||
|
||||
|
||||
def _get_room_object_table(rom: "LocalRom", cache: dict[int, RoomObjectTable], room_id: int) -> RoomObjectTable:
|
||||
room_table = cache.get(room_id)
|
||||
if room_table is not None:
|
||||
return room_table
|
||||
|
||||
pointer_address = 0xF8000 + (room_id * 3)
|
||||
snes_address_bytes = rom.read_bytes(pointer_address, 3)
|
||||
snes_address = (snes_address_bytes[2] << 16) | (snes_address_bytes[1] << 8) | snes_address_bytes[0]
|
||||
room_table = RoomObjectTable.from_rom(rom, snes_to_pc(snes_address))
|
||||
cache[room_id] = room_table
|
||||
return room_table
|
||||
|
||||
|
||||
def _write_gt_boss_sprite_block(rom: "LocalRom", dungeon_data: DungeonBossPatchData, boss_data: BossPatchData) -> None:
|
||||
assert dungeon_data.gt_sprite_write_address is not None
|
||||
rom.write_int16(dungeon_data.sprite_pointer_address, dungeon_data.gt_sprite_write_address)
|
||||
|
||||
sprite_block = bytearray((0x00,))
|
||||
sprite_block.extend(boss_data.sprite_array)
|
||||
if dungeon_data.room_id == 28 and boss_data.pointer == BOSS_PATCH_DATA["Arrghus"].pointer:
|
||||
sprite_block.extend(dungeon_data.extra_sprites[:6])
|
||||
else:
|
||||
sprite_block.extend(dungeon_data.extra_sprites)
|
||||
sprite_block.append(0xFF)
|
||||
rom.write_bytes(dungeon_data.gt_sprite_write_address, sprite_block)
|
||||
|
||||
|
||||
def _write_room_object_pointer(rom: "LocalRom", room_id: int, pc_address: int) -> None:
|
||||
snes_address = pc_to_snes(pc_address)
|
||||
pointer_address = 0xF8000 + (room_id * 3)
|
||||
rom.write_bytes(pointer_address, (
|
||||
snes_address & 0xFF,
|
||||
(snes_address >> 8) & 0xFF,
|
||||
(snes_address >> 16) & 0xFF,
|
||||
))
|
||||
|
||||
|
||||
def _build_subtype_3_object(x: int, y: int, object_id: int) -> bytes:
|
||||
return bytes((
|
||||
((x << 2) & 0xFC) | (object_id & 0x03),
|
||||
((y << 2) & 0xFC) | ((object_id >> 2) & 0x03),
|
||||
0xF0 | ((object_id >> 4) & 0x0F),
|
||||
))
|
||||
|
||||
|
||||
def _object_id(object_bytes: bytes) -> Optional[int]:
|
||||
if len(object_bytes) != 3:
|
||||
return None
|
||||
if object_bytes[0] >= 0xFC:
|
||||
return (object_bytes[2] & 0x3F) + 0x100
|
||||
if object_bytes[2] >= 0xF8:
|
||||
return 0xF00 | ((object_bytes[2] & 0x0F) << 4) | ((object_bytes[1] & 0x03) << 2) | (object_bytes[0] & 0x03)
|
||||
return object_bytes[2]
|
||||
|
||||
|
||||
def _set_enemizer_flag(rom: "LocalRom", symbol_name: str, enabled: bool) -> None:
|
||||
rom.write_byte(_get_enemizer_symbol(symbol_name), 0x01 if enabled else 0x00)
|
||||
|
||||
|
||||
def _apply_killable_thief(rom: "LocalRom") -> None:
|
||||
rom.write_byte(_get_enemizer_symbol("notItemSprite_Mimic") + 4, THIEF_SPRITE_ID)
|
||||
thief_hp_address = ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID
|
||||
if rom.read_byte(thief_hp_address) != 0xFF:
|
||||
rom.write_byte(thief_hp_address, THIEF_DEFAULT_HP)
|
||||
|
||||
|
||||
def _randomize_enemy_health(rom: "LocalRom", rng: random.Random, enemy_health_key: str) -> None:
|
||||
min_hp, max_hp = ENEMY_HEALTH_RANGE_BY_KEY[enemy_health_key]
|
||||
for sprite_id in range(0xF3):
|
||||
hp_address = ENEMY_HP_TABLE_ADDRESS + sprite_id
|
||||
if rom.read_byte(hp_address) == 0xFF or sprite_id in EXCLUDED_ENEMY_TABLE_SPRITE_IDS:
|
||||
continue
|
||||
rom.write_byte(hp_address, rng.randrange(min_hp, max_hp))
|
||||
|
||||
|
||||
def _randomize_enemy_damage(rom: "LocalRom", rng: random.Random, allow_zero_damage: bool) -> None:
|
||||
for sprite_id in range(0xF3):
|
||||
if sprite_id in EXCLUDED_ENEMY_TABLE_SPRITE_IDS:
|
||||
continue
|
||||
new_damage = rng.randrange(8)
|
||||
if not allow_zero_damage and new_damage == 2:
|
||||
continue
|
||||
rom.write_byte(ENEMY_DAMAGE_TABLE_ADDRESS + sprite_id, new_damage)
|
||||
|
||||
|
||||
def _shuffle_damage_groups(
|
||||
rom: "LocalRom",
|
||||
rng: random.Random,
|
||||
*,
|
||||
chaos_mode: bool,
|
||||
allow_zero_damage: bool,
|
||||
) -> None:
|
||||
min_damage = 0 if allow_zero_damage else 4
|
||||
max_damage = 64 if chaos_mode else 32
|
||||
|
||||
for group_id in range(10):
|
||||
green_mail_damage = rng.randrange(min_damage, max_damage)
|
||||
if chaos_mode:
|
||||
blue_mail_damage = rng.randrange(min_damage, max_damage)
|
||||
red_mail_damage = rng.randrange(min_damage, max_damage)
|
||||
else:
|
||||
blue_mail_damage = green_mail_damage * 3 // 4
|
||||
red_mail_damage = green_mail_damage * 3 // 8
|
||||
group_address = DAMAGE_GROUP_TABLE_ADDRESS + (group_id * 3)
|
||||
rom.write_bytes(group_address, (green_mail_damage, blue_mail_damage, red_mail_damage))
|
||||
|
||||
|
||||
def _update_hidden_enemy_item_table_for_retro_mode(rom: "LocalRom") -> None:
|
||||
if rom.read_byte(RETRO_ARROW_REPLACEMENT_CHECK_ADDRESS) != RETRO_RUPEE_REPLACEMENT_SPRITE_ID:
|
||||
return
|
||||
|
||||
item_table_address = _get_enemizer_symbol("sprite_bush_spawn_item_table")
|
||||
for index in range(22):
|
||||
if rom.read_byte(item_table_address + index) == ARROW_REFILL_5_SPRITE_ID:
|
||||
rom.write_byte(item_table_address + index, RETRO_RUPEE_REPLACEMENT_SPRITE_ID)
|
||||
|
||||
|
||||
def _apply_trinexx_room_fixes(rom: "LocalRom") -> None:
|
||||
# Match original Enemizer's unconditional Trinexx ice-floor removal so
|
||||
# blue-head projectiles do not create solid walls in non-vanilla rooms.
|
||||
rom.write_bytes(TRINEXX_ICE_FLOOR_ROUTINE_ADDRESS, (0xEA, 0xEA, 0xEA, 0xEA))
|
||||
|
||||
|
||||
def _apply_randomized_tile_trap_floor_tile(rom: "LocalRom") -> None:
|
||||
# Original Enemizer's RandomizeTileTrapFloorTile option changes the tile
|
||||
# left behind by flying floor tile traps. AP does not currently expose or
|
||||
# call this option, so keep the implementation isolated and unused.
|
||||
rom.write_bytes(TRINEXX_ICE_PROJECTILE_TILE_ADDRESS, (0x88, 0x01))
|
||||
rom.write_byte(TILE_TRAP_FLOOR_TILE_ADDRESS, 0x12)
|
||||
|
||||
|
||||
def _make_native_enemizer_rng(world: "ALTTPWorld") -> random.Random:
|
||||
seed_material = "|".join((
|
||||
str(world.multiworld.seed),
|
||||
world.multiworld.seed_name,
|
||||
str(world.player),
|
||||
_option_key(world.options.enemy_health),
|
||||
_option_key(world.options.enemy_damage),
|
||||
str(int(bool(world.options.enemy_shuffle))),
|
||||
str(int(bool(world.options.bush_shuffle))),
|
||||
str(int(bool(world.options.killable_thieves))),
|
||||
))
|
||||
seed = int.from_bytes(hashlib.sha256(seed_material.encode("utf-8")).digest()[:8], "big")
|
||||
return random.Random(seed)
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _load_enemizer_base_patches() -> tuple[tuple[int, bytes], ...]:
|
||||
return tuple(
|
||||
(entry.address, entry.patch_data)
|
||||
for entry in ENEMIZER_BASE_PATCHES
|
||||
)
|
||||
|
||||
|
||||
def _option_key(option: object) -> str:
|
||||
return str(getattr(option, "current_key", option))
|
||||
|
||||
|
||||
def _get_enemizer_symbol(symbol_name: str) -> int:
|
||||
global _ENEMIZER_SYMBOLS
|
||||
if _ENEMIZER_SYMBOLS is None:
|
||||
_ENEMIZER_SYMBOLS = _load_enemizer_symbols()
|
||||
return _ENEMIZER_SYMBOLS[symbol_name]
|
||||
|
||||
|
||||
def _load_enemizer_symbols() -> dict[str, int]:
|
||||
return {
|
||||
name: snes_to_pc(snes_address)
|
||||
for name, snes_address in ENEMIZER_SYMBOLS.items()
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,6 @@ from .SubClasses import ALttPLocation, LTTPRegion, LTTPRegionType
|
||||
from .Shops import TakeAny, total_shop_slots, set_up_shops, shop_table_by_location, ShopType
|
||||
from .Bosses import place_bosses
|
||||
from .Dungeons import get_dungeon_item_pool_player
|
||||
from .EnemyShuffle import generate_enemy_shuffle_state
|
||||
from .EntranceShuffle import connect_entrance
|
||||
from .Items import item_factory, GetBeemizerItem, trap_replaceable, item_name_groups
|
||||
from .Options import small_key_shuffle, compass_shuffle, big_key_shuffle, map_shuffle, TriforcePiecesMode, LTTPBosses
|
||||
@@ -512,8 +511,6 @@ def generate_itempool(world: "ALTTPWorld"):
|
||||
world.options.turtle_rock_medallion.current_key.title())
|
||||
|
||||
place_bosses(world)
|
||||
if world.options.enemy_shuffle:
|
||||
world.enemy_shuffle_state = generate_enemy_shuffle_state(world)
|
||||
|
||||
multiworld.itempool += items
|
||||
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from Utils import snes_to_pc
|
||||
from .enemizer_data.pot_shuffle_data import POT_ROOMS
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import ALTTPWorld
|
||||
from .Rom import LocalRom
|
||||
|
||||
|
||||
POT_ITEM_POINTER_TABLE = 0xDB67
|
||||
POT_KEY = 0x08
|
||||
POT_ARROW = 0x09
|
||||
POT_BLUE_RUPEE = 0x07
|
||||
POT_SWITCH = 0x88
|
||||
POT_HOLE = 0x80
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PotData:
|
||||
x: int
|
||||
y: int
|
||||
reserved: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PotRoomData:
|
||||
room_id: int
|
||||
pots: tuple[PotData, ...]
|
||||
items: tuple[int, ...]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FilledPot:
|
||||
x: int
|
||||
y: int
|
||||
item: int
|
||||
|
||||
|
||||
def generate_pot_shuffle(world: "ALTTPWorld") -> dict[int, tuple[FilledPot, ...]]:
|
||||
room_data = _load_pot_room_data()
|
||||
shuffled_pots: dict[int, tuple[FilledPot, ...]] = {}
|
||||
|
||||
for room in room_data:
|
||||
room_items = [item for item in room.items if item != POT_HOLE]
|
||||
if world.options.retro_bow:
|
||||
room_items = [POT_BLUE_RUPEE if item == POT_ARROW else item for item in room_items]
|
||||
|
||||
empty_pots: list[PotData] = []
|
||||
filled_pots: list[FilledPot] = []
|
||||
|
||||
for pot in room.pots:
|
||||
if pot.reserved == 3:
|
||||
filled_pots.append(FilledPot(pot.x, pot.y, POT_HOLE))
|
||||
else:
|
||||
empty_pots.append(pot)
|
||||
|
||||
while POT_KEY in room_items:
|
||||
candidate_indices = [index for index, pot in enumerate(empty_pots) if pot.reserved == 1]
|
||||
if not candidate_indices:
|
||||
break
|
||||
pot_index = world.random.choice(candidate_indices)
|
||||
pot = empty_pots.pop(pot_index)
|
||||
room_items.remove(POT_KEY)
|
||||
filled_pots.append(FilledPot(pot.x, pot.y, POT_KEY))
|
||||
|
||||
while POT_SWITCH in room_items:
|
||||
candidate_indices = [index for index, pot in enumerate(empty_pots) if pot.reserved == 2]
|
||||
if not candidate_indices:
|
||||
break
|
||||
pot_index = world.random.choice(candidate_indices)
|
||||
pot = empty_pots.pop(pot_index)
|
||||
room_items.remove(POT_SWITCH)
|
||||
filled_pots.append(FilledPot(pot.x, pot.y, POT_SWITCH))
|
||||
|
||||
while room_items and empty_pots:
|
||||
pot_index = world.random.randrange(len(empty_pots))
|
||||
item_index = world.random.randrange(len(room_items))
|
||||
pot = empty_pots.pop(pot_index)
|
||||
item = room_items.pop(item_index)
|
||||
filled_pots.append(FilledPot(pot.x, pot.y, item))
|
||||
|
||||
shuffled_pots[room.room_id] = tuple(filled_pots)
|
||||
|
||||
return shuffled_pots
|
||||
|
||||
|
||||
def apply_pot_shuffle(rom: "LocalRom", shuffled_pots: dict[int, tuple[FilledPot, ...]]) -> None:
|
||||
for room_id, pots in shuffled_pots.items():
|
||||
pointer_address = POT_ITEM_POINTER_TABLE + (room_id * 2)
|
||||
snes_address = rom.read_byte(pointer_address) | (rom.read_byte(pointer_address + 1) << 8) | (0x01 << 16)
|
||||
address = snes_to_pc(snes_address)
|
||||
for index, pot in enumerate(pots):
|
||||
rom.write_bytes(address + (index * 3), (pot.x, pot.y, pot.item))
|
||||
|
||||
|
||||
def get_unique_pot_item_position(
|
||||
shuffled_pots: dict[int, tuple[FilledPot, ...]],
|
||||
room_id: int,
|
||||
item: int,
|
||||
) -> tuple[int, int]:
|
||||
positions = [
|
||||
(pot.x, pot.y)
|
||||
for pot in shuffled_pots.get(room_id, ())
|
||||
if pot.item == item
|
||||
]
|
||||
if len(positions) != 1:
|
||||
raise ValueError(
|
||||
f"Expected exactly one pot item {hex(item)} in room {hex(room_id)}, found {len(positions)}"
|
||||
)
|
||||
return positions[0]
|
||||
|
||||
|
||||
def _load_pot_room_data() -> tuple[PotRoomData, ...]:
|
||||
return tuple(
|
||||
PotRoomData(
|
||||
room_id=room.room_id,
|
||||
pots=tuple(PotData(x=pot.x, y=pot.y, reserved=pot.reserved) for pot in room.pots),
|
||||
items=room.items,
|
||||
)
|
||||
for room in POT_ROOMS
|
||||
)
|
||||
+225
-132
@@ -15,6 +15,7 @@ import logging
|
||||
import os
|
||||
import random
|
||||
import struct
|
||||
import subprocess
|
||||
import threading
|
||||
import concurrent.futures
|
||||
import bsdiff4
|
||||
@@ -52,6 +53,9 @@ try:
|
||||
except:
|
||||
xxtea = None
|
||||
|
||||
enemizer_logger = logging.getLogger("Enemizer")
|
||||
|
||||
|
||||
class LocalRom:
|
||||
|
||||
def __init__(self, file, patch=True, vanillaRom=None, name=None, hash=None):
|
||||
@@ -175,6 +179,43 @@ class LocalRom:
|
||||
self.write_int32(startaddress + (i * 4), value)
|
||||
|
||||
|
||||
check_lock = threading.Lock()
|
||||
|
||||
|
||||
def check_enemizer(enemizercli):
|
||||
if getattr(check_enemizer, "done", None):
|
||||
return
|
||||
if not os.path.exists(enemizercli) and not os.path.exists(enemizercli + ".exe"):
|
||||
raise Exception(f"Enemizer not found at {enemizercli}, please install it. "
|
||||
f"Such as https://github.com/Ijwu/Enemizer/releases")
|
||||
|
||||
with check_lock:
|
||||
# some time may have passed since the lock was acquired, as such a quick re-check doesn't hurt
|
||||
if getattr(check_enemizer, "done", None):
|
||||
return
|
||||
wanted_version = (7, 1, 0)
|
||||
# version info is saved on the lib, for some reason
|
||||
library_info = os.path.join(os.path.dirname(enemizercli), "EnemizerCLI.Core.deps.json")
|
||||
with open(library_info) as f:
|
||||
info = json.load(f)
|
||||
|
||||
for lib in info["libraries"]:
|
||||
if lib.startswith("EnemizerLibrary/"):
|
||||
version = lib.split("/")[-1]
|
||||
version = tuple(int(element) for element in version.split("."))
|
||||
enemizer_logger.debug(f"Found Enemizer version {version}")
|
||||
if version < wanted_version:
|
||||
raise Exception(
|
||||
f"Enemizer found at {enemizercli} is outdated ({version}) < ({wanted_version}), "
|
||||
f"please update your Enemizer. "
|
||||
f"Such as from https://github.com/Ijwu/Enemizer/releases")
|
||||
break
|
||||
else:
|
||||
raise Exception(f"Could not find Enemizer library version information in {library_info}")
|
||||
|
||||
check_enemizer.done = True
|
||||
|
||||
|
||||
def apply_random_sprite_on_event(rom: LocalRom, sprite, local_random, allow_random_on_event, sprite_pool):
|
||||
userandomsprites = False
|
||||
if sprite and not isinstance(sprite, Sprite):
|
||||
@@ -241,6 +282,174 @@ def apply_random_sprite_on_event(rom: LocalRom, sprite, local_random, allow_rand
|
||||
rom.write_bytes(0x307000 + (i * 0x8000), sprite.palette)
|
||||
rom.write_bytes(0x307078 + (i * 0x8000), sprite.glove_palette)
|
||||
|
||||
|
||||
def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
|
||||
player = world.player
|
||||
check_enemizer(enemizercli)
|
||||
randopatch_path = os.path.abspath(os.path.join(output_directory, f'enemizer_randopatch_{player}.sfc'))
|
||||
options_path = os.path.abspath(os.path.join(output_directory, f'enemizer_options_{player}.json'))
|
||||
enemizer_output_path = os.path.abspath(os.path.join(output_directory, f'enemizer_output_{player}.sfc'))
|
||||
|
||||
# write options file for enemizer
|
||||
options = {
|
||||
'RandomizeEnemies': world.options.enemy_shuffle.value,
|
||||
'RandomizeEnemiesType': 3,
|
||||
'RandomizeBushEnemyChance': world.options.bush_shuffle.value,
|
||||
'RandomizeEnemyHealthRange': world.options.enemy_health != 'default',
|
||||
'RandomizeEnemyHealthType': {'default': 0, 'easy': 0, 'normal': 1, 'hard': 2, 'expert': 3}[
|
||||
world.options.enemy_health.current_key],
|
||||
'OHKO': False,
|
||||
'RandomizeEnemyDamage': world.options.enemy_damage != 'default',
|
||||
'AllowEnemyZeroDamage': True,
|
||||
'ShuffleEnemyDamageGroups': world.options.enemy_damage != 'default',
|
||||
'EnemyDamageChaosMode': world.options.enemy_damage == 'chaos',
|
||||
'EasyModeEscape': world.options.mode == "standard",
|
||||
'EnemiesAbsorbable': False,
|
||||
'AbsorbableSpawnRate': 10,
|
||||
'AbsorbableTypes': {
|
||||
'FullMagic': True, 'SmallMagic': True, 'Bomb_1': True, 'BlueRupee': True, 'Heart': True, 'BigKey': True,
|
||||
'Key': True,
|
||||
'Fairy': True, 'Arrow_10': True, 'Arrow_5': True, 'Bomb_8': True, 'Bomb_4': True, 'GreenRupee': True,
|
||||
'RedRupee': True
|
||||
},
|
||||
'BossMadness': False,
|
||||
'RandomizeBosses': True,
|
||||
'RandomizeBossesType': 0,
|
||||
'RandomizeBossHealth': False,
|
||||
'RandomizeBossHealthMinAmount': 0,
|
||||
'RandomizeBossHealthMaxAmount': 300,
|
||||
'RandomizeBossDamage': False,
|
||||
'RandomizeBossDamageMinAmount': 0,
|
||||
'RandomizeBossDamageMaxAmount': 200,
|
||||
'RandomizeBossBehavior': False,
|
||||
'RandomizeDungeonPalettes': False,
|
||||
'SetBlackoutMode': False,
|
||||
'RandomizeOverworldPalettes': False,
|
||||
'RandomizeSpritePalettes': False,
|
||||
'SetAdvancedSpritePalettes': False,
|
||||
'PukeMode': False,
|
||||
'NegativeMode': False,
|
||||
'GrayscaleMode': False,
|
||||
'GenerateSpoilers': False,
|
||||
'RandomizeLinkSpritePalette': False,
|
||||
'RandomizePots': world.options.pot_shuffle.value,
|
||||
'ShuffleMusic': False,
|
||||
'BootlegMagic': True,
|
||||
'CustomBosses': False,
|
||||
'AndyMode': False,
|
||||
'HeartBeepSpeed': 0,
|
||||
'AlternateGfx': False,
|
||||
'ShieldGraphics': "shield_gfx/normal.gfx",
|
||||
'SwordGraphics': "sword_gfx/normal.gfx",
|
||||
'BeeMizer': False,
|
||||
'BeesLevel': 0,
|
||||
'RandomizeTileTrapPattern': False,
|
||||
'RandomizeTileTrapFloorTile': False,
|
||||
'AllowKillableThief': world.options.killable_thieves.value,
|
||||
'RandomizeSpriteOnHit': False,
|
||||
'DebugMode': False,
|
||||
'DebugForceEnemy': False,
|
||||
'DebugForceEnemyId': 0,
|
||||
'DebugForceBoss': False,
|
||||
'DebugForceBossId': 0,
|
||||
'DebugOpenShutterDoors': False,
|
||||
'DebugForceEnemyDamageZero': False,
|
||||
'DebugShowRoomIdInRupeeCounter': False,
|
||||
'UseManualBosses': True,
|
||||
'ManualBosses': {
|
||||
'EasternPalace': world.dungeons["Eastern Palace"].boss.enemizer_name,
|
||||
'DesertPalace': world.dungeons["Desert Palace"].boss.enemizer_name,
|
||||
'TowerOfHera': world.dungeons["Tower of Hera"].boss.enemizer_name,
|
||||
'AgahnimsTower': 'Agahnim',
|
||||
'PalaceOfDarkness': world.dungeons["Palace of Darkness"].boss.enemizer_name,
|
||||
'SwampPalace': world.dungeons["Swamp Palace"].boss.enemizer_name,
|
||||
'SkullWoods': world.dungeons["Skull Woods"].boss.enemizer_name,
|
||||
'ThievesTown': world.dungeons["Thieves Town"].boss.enemizer_name,
|
||||
'IcePalace': world.dungeons["Ice Palace"].boss.enemizer_name,
|
||||
'MiseryMire': world.dungeons["Misery Mire"].boss.enemizer_name,
|
||||
'TurtleRock': world.dungeons["Turtle Rock"].boss.enemizer_name,
|
||||
'GanonsTower1':
|
||||
world.dungeons["Ganons Tower" if world.options.mode != 'inverted' else
|
||||
"Inverted Ganons Tower"].bosses['bottom'].enemizer_name,
|
||||
'GanonsTower2':
|
||||
world.dungeons["Ganons Tower" if world.options.mode != 'inverted' else
|
||||
"Inverted Ganons Tower"].bosses['middle'].enemizer_name,
|
||||
'GanonsTower3':
|
||||
world.dungeons["Ganons Tower" if world.options.mode != 'inverted' else
|
||||
"Inverted Ganons Tower"].bosses['top'].enemizer_name,
|
||||
'GanonsTower4': 'Agahnim2',
|
||||
'Ganon': 'Ganon',
|
||||
}
|
||||
}
|
||||
|
||||
rom.write_to_file(randopatch_path)
|
||||
|
||||
with open(options_path, 'w') as f:
|
||||
json.dump(options, f)
|
||||
|
||||
max_enemizer_tries = 5
|
||||
for i in range(max_enemizer_tries):
|
||||
enemizer_seed = str(world.random.randint(0, 999999999))
|
||||
enemizer_command = [os.path.abspath(enemizercli),
|
||||
'--rom', randopatch_path,
|
||||
'--seed', enemizer_seed,
|
||||
'--binary',
|
||||
'--enemizer', options_path,
|
||||
'--output', enemizer_output_path]
|
||||
|
||||
p_open = subprocess.Popen(enemizer_command,
|
||||
cwd=os.path.dirname(enemizercli),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
universal_newlines=True)
|
||||
|
||||
enemizer_logger.debug(
|
||||
f"Enemizer attempt {i + 1} of {max_enemizer_tries} for player {player} using enemizer seed {enemizer_seed}")
|
||||
for stdout_line in iter(p_open.stdout.readline, ""):
|
||||
if i == max_enemizer_tries - 1:
|
||||
enemizer_logger.warning(stdout_line.rstrip())
|
||||
else:
|
||||
enemizer_logger.debug(stdout_line.rstrip())
|
||||
p_open.stdout.close()
|
||||
|
||||
return_code = p_open.wait()
|
||||
if return_code:
|
||||
if i == max_enemizer_tries - 1:
|
||||
raise subprocess.CalledProcessError(return_code, enemizer_command)
|
||||
continue
|
||||
|
||||
for j in range(i + 1, max_enemizer_tries):
|
||||
world.random.randint(0, 999999999)
|
||||
# Sacrifice all remaining random numbers that would have been used for unused enemizer tries.
|
||||
# This allows for future enemizer bug fixes to NOT affect the rest of the seed's randomness
|
||||
break
|
||||
|
||||
rom.read_from_file(enemizer_output_path)
|
||||
os.remove(enemizer_output_path)
|
||||
|
||||
if world.dungeons["Thieves Town"].boss.enemizer_name == "Blind":
|
||||
rom.write_byte(0x04DE81, 6)
|
||||
rom.write_byte(0x1B0101, 0) # Do not close boss room door on entry.
|
||||
|
||||
# Moblins attached to "key drop" locations crash the game when dropping their item when Key Drop Shuffle is on.
|
||||
# Replace them with a Slime enemy if they are placed.
|
||||
if world.options.key_drop_shuffle:
|
||||
key_drop_enemies = {
|
||||
0x4DA20, 0x4DA5C, 0x4DB7F, 0x4DD73, 0x4DDC3, 0x4DE07, 0x4E201,
|
||||
0x4E20A, 0x4E326, 0x4E4F7, 0x4E687, 0x4E70C, 0x4E7C8, 0x4E7FA
|
||||
}
|
||||
for enemy in key_drop_enemies:
|
||||
if rom.read_byte(enemy) == 0x12:
|
||||
logging.debug(f"Moblin found and replaced at {enemy} in world {player}")
|
||||
rom.write_byte(enemy, 0x8F)
|
||||
|
||||
for used in (randopatch_path, options_path):
|
||||
try:
|
||||
os.remove(used)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
tile_list_lock = threading.Lock()
|
||||
_tile_collection_table = []
|
||||
|
||||
@@ -586,13 +795,9 @@ def get_nonnative_item_sprite(code: int) -> int:
|
||||
# https://discord.com/channels/731205301247803413/827141303330406408/852102450822905886
|
||||
|
||||
|
||||
def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int):
|
||||
def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
local_random = multiworld.worlds[player].random
|
||||
local_world = multiworld.worlds[player]
|
||||
enemized = bool(local_world.options.boss_shuffle or local_world.options.enemy_shuffle
|
||||
or local_world.options.enemy_health != 'default' or local_world.options.enemy_damage != 'default'
|
||||
or local_world.options.pot_shuffle or local_world.options.bush_shuffle
|
||||
or local_world.options.killable_thieves)
|
||||
|
||||
# patch items
|
||||
|
||||
@@ -1126,13 +1331,6 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int):
|
||||
starting_max_arrows = 30
|
||||
|
||||
startingstate = CollectionState(multiworld)
|
||||
has_blue_shield = False
|
||||
has_red_shield = False
|
||||
has_mirror_shield = False
|
||||
progressive_shields = 0
|
||||
has_blue_mail = False
|
||||
has_red_mail = False
|
||||
progressive_mail = 0
|
||||
|
||||
if startingstate.has('Silver Bow', player):
|
||||
equip[0x340] = 1
|
||||
@@ -1161,6 +1359,18 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int):
|
||||
elif startingstate.has('Fighter Sword', player):
|
||||
equip[0x359] = 1
|
||||
|
||||
if startingstate.has('Mirror Shield', player):
|
||||
equip[0x35A] = 3
|
||||
elif startingstate.has('Red Shield', player):
|
||||
equip[0x35A] = 2
|
||||
elif startingstate.has('Blue Shield', player):
|
||||
equip[0x35A] = 1
|
||||
|
||||
if startingstate.has('Red Mail', player):
|
||||
equip[0x35B] = 2
|
||||
elif startingstate.has('Blue Mail', player):
|
||||
equip[0x35B] = 1
|
||||
|
||||
if startingstate.has('Magic Upgrade (1/4)', player):
|
||||
equip[0x37B] = 2
|
||||
equip[0x36E] = 0x80
|
||||
@@ -1173,6 +1383,8 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int):
|
||||
if item.name in {'Bow', 'Silver Bow', 'Silver Arrows', 'Progressive Bow', 'Progressive Bow (Alt)',
|
||||
'Titans Mitts', 'Power Glove', 'Progressive Glove',
|
||||
'Golden Sword', 'Tempered Sword', 'Master Sword', 'Fighter Sword', 'Progressive Sword',
|
||||
'Mirror Shield', 'Red Shield', 'Blue Shield', 'Progressive Shield',
|
||||
'Red Mail', 'Blue Mail', 'Progressive Mail',
|
||||
'Magic Upgrade (1/4)', 'Magic Upgrade (1/2)', 'Triforce Piece'}:
|
||||
continue
|
||||
|
||||
@@ -1277,63 +1489,9 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int):
|
||||
if item.name != 'Piece of Heart' or equip[0x36B] == 0:
|
||||
equip[0x36C] = min(equip[0x36C] + 0x08, 0xA0)
|
||||
equip[0x36D] = min(equip[0x36D] + 0x08, 0xA0)
|
||||
elif item.name == 'Blue Shield':
|
||||
has_blue_shield = True
|
||||
continue
|
||||
elif item.name == 'Red Shield':
|
||||
has_red_shield = True
|
||||
continue
|
||||
elif item.name == 'Mirror Shield':
|
||||
has_mirror_shield = True
|
||||
continue
|
||||
elif item.name == 'Progressive Shield':
|
||||
progressive_shields += 1
|
||||
continue
|
||||
elif item.name == 'Blue Mail':
|
||||
has_blue_mail = True
|
||||
continue
|
||||
elif item.name == 'Red Mail':
|
||||
has_red_mail = True
|
||||
continue
|
||||
elif item.name == 'Progressive Mail':
|
||||
progressive_mail += 1
|
||||
continue
|
||||
else:
|
||||
raise RuntimeError(f'Unsupported item in starting equipment: {item.name}')
|
||||
|
||||
for _ in range(progressive_shields):
|
||||
if has_mirror_shield:
|
||||
continue
|
||||
if has_red_shield and local_world.difficulty_requirements.progressive_shield_limit >= 3:
|
||||
has_mirror_shield = True
|
||||
continue
|
||||
if has_blue_shield and local_world.difficulty_requirements.progressive_shield_limit >= 2:
|
||||
has_red_shield = True
|
||||
continue
|
||||
if local_world.difficulty_requirements.progressive_shield_limit >= 1:
|
||||
has_blue_shield = True
|
||||
|
||||
for _ in range(progressive_mail):
|
||||
if has_red_mail:
|
||||
continue
|
||||
if has_blue_mail and local_world.difficulty_requirements.progressive_armor_limit >= 2:
|
||||
has_red_mail = True
|
||||
continue
|
||||
if local_world.difficulty_requirements.progressive_armor_limit >= 1:
|
||||
has_blue_mail = True
|
||||
|
||||
if has_mirror_shield:
|
||||
equip[0x35A] = 3
|
||||
elif has_red_shield:
|
||||
equip[0x35A] = 2
|
||||
elif has_blue_shield:
|
||||
equip[0x35A] = 1
|
||||
|
||||
if has_red_mail:
|
||||
equip[0x35B] = 2
|
||||
elif has_blue_mail:
|
||||
equip[0x35B] = 1
|
||||
|
||||
equip[0x343] = min(equip[0x343], starting_max_bombs)
|
||||
rom.write_byte(0x180034, starting_max_bombs)
|
||||
equip[0x377] = min(equip[0x377], starting_max_arrows)
|
||||
@@ -1552,71 +1710,6 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int):
|
||||
if encoded_players > ROM_PLAYER_LIMIT:
|
||||
rom.write_bytes(0x195FFC + ((ROM_PLAYER_LIMIT - 1) * 32), hud_format_text("Archipelago"))
|
||||
|
||||
if enemized:
|
||||
from . import EnemizerPatches as enemizer_patches
|
||||
from .EnemyShuffle import apply_enemy_shuffle
|
||||
from .PotShuffle import apply_pot_shuffle
|
||||
|
||||
enemizer_patches.apply_enemizer_base_patch(rom)
|
||||
|
||||
enemy_shuffle_enabled = bool(local_world.options.enemy_shuffle)
|
||||
bush_shuffle_enabled = bool(local_world.options.bush_shuffle)
|
||||
enemy_health_key = enemizer_patches._option_key(local_world.options.enemy_health)
|
||||
enemy_damage_key = enemizer_patches._option_key(local_world.options.enemy_damage)
|
||||
|
||||
if enemy_shuffle_enabled or bush_shuffle_enabled:
|
||||
enemizer_patches._set_enemizer_flag(rom, "EnemizerFlags_randomize_bushes", True)
|
||||
hidden_enemy_chance_pool = (
|
||||
enemizer_patches.RANDOMIZED_HIDDEN_ENEMY_CHANCE_POOL
|
||||
if bush_shuffle_enabled
|
||||
else enemizer_patches.VANILLA_HIDDEN_ENEMY_CHANCE_POOL
|
||||
)
|
||||
rom.write_bytes(enemizer_patches.HIDDEN_ENEMY_CHANCE_POOL_ADDRESS, hidden_enemy_chance_pool)
|
||||
enemizer_patches._update_hidden_enemy_item_table_for_retro_mode(rom)
|
||||
|
||||
if enemy_shuffle_enabled:
|
||||
enemizer_patches._set_enemizer_flag(rom, "EnemizerFlags_randomize_sprites", True)
|
||||
enemizer_patches._set_enemizer_flag(rom, "EnemizerFlags_enable_mimic_override", True)
|
||||
enemizer_patches._set_enemizer_flag(rom, "EnemizerFlags_enable_terrorpin_ai_fix", True)
|
||||
rom.write_bytes(0x1F2D5, (0x54, 0x9C))
|
||||
rom.write_byte(0x1F2E5, 0xB0)
|
||||
rom.write_byte(0x1F2EB, 0xD0)
|
||||
|
||||
if local_world.options.killable_thieves:
|
||||
enemizer_patches._apply_killable_thief(rom)
|
||||
|
||||
if enemy_health_key != "default" or enemy_damage_key != "default":
|
||||
rng = enemizer_patches._make_native_enemizer_rng(local_world)
|
||||
else:
|
||||
rng = None
|
||||
|
||||
if enemy_health_key != "default":
|
||||
assert rng is not None
|
||||
enemizer_patches._randomize_enemy_health(rom, rng, enemy_health_key)
|
||||
|
||||
if enemy_damage_key != "default":
|
||||
assert rng is not None
|
||||
enemizer_patches._randomize_enemy_damage(rom, rng, allow_zero_damage=True)
|
||||
enemizer_patches._shuffle_damage_groups(
|
||||
rom,
|
||||
rng,
|
||||
chaos_mode=enemy_damage_key == "chaos",
|
||||
allow_zero_damage=True,
|
||||
)
|
||||
|
||||
enemy_shuffle_state = getattr(local_world, "enemy_shuffle_state", None)
|
||||
if local_world.options.enemy_shuffle and enemy_shuffle_state is not None:
|
||||
apply_enemy_shuffle(rom, enemy_shuffle_state)
|
||||
|
||||
if local_world.options.boss_shuffle:
|
||||
# Boss shuffle must run after enemy shuffle so boss room sprite pointers
|
||||
# and graphics block IDs are not restored to the enemy-shuffled room values.
|
||||
enemizer_patches.patch_bosses(local_world, rom)
|
||||
|
||||
pot_shuffle_state = getattr(local_world, "pot_shuffle_state", None)
|
||||
if local_world.options.pot_shuffle and pot_shuffle_state is not None:
|
||||
apply_pot_shuffle(rom, pot_shuffle_state)
|
||||
|
||||
# Write title screen Code
|
||||
hashint = int(rom.get_hash(), 16)
|
||||
code = [
|
||||
@@ -1767,7 +1860,7 @@ def apply_oof_sfx(rom: LocalRom, oof: str):
|
||||
rom.write_bytes(0x12803A, oof_bytes)
|
||||
rom.write_bytes(0x12803A + len(oof_bytes), [0xEB, 0xEB])
|
||||
|
||||
# Preserve SPC $3188 instead of writing the unused "WHAT" sound effect there.
|
||||
# Enemizer patch: prevent Enemizer from overwriting $3188 in SPC memory with an unused sound effect ("WHAT")
|
||||
rom.write_bytes(0x13000D, [0x00, 0x00, 0x00, 0x08])
|
||||
|
||||
|
||||
|
||||
@@ -14,10 +14,9 @@ from .InvertedRegions import create_inverted_regions, mark_dark_world_regions
|
||||
from .ItemPool import generate_itempool, difficulties
|
||||
from .Items import item_init_table, item_name_groups, item_table, GetBeemizerItem
|
||||
from .Options import ALTTPOptions, small_key_shuffle
|
||||
from .PotShuffle import generate_pot_shuffle
|
||||
from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions, lookup_vanilla_location_to_entrance, \
|
||||
is_main_entrance, key_drop_data
|
||||
from .Rom import LocalRom, patch_rom, patch_race_rom, apply_rom_settings, \
|
||||
from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enemizer, apply_rom_settings, \
|
||||
get_hash_string, get_base_rom_path, LttPDeltaPatch
|
||||
from .Rules import set_rules
|
||||
from .Shops import create_shops, Shop, push_shop_inventories, ShopType, price_rate_display, price_type_display_name
|
||||
@@ -254,6 +253,17 @@ class ALTTPWorld(World):
|
||||
|
||||
create_items = generate_itempool
|
||||
|
||||
_enemizer_path: typing.ClassVar[typing.Optional[str]] = None
|
||||
|
||||
@property
|
||||
def enemizer_path(self) -> str:
|
||||
# TODO: directly use settings
|
||||
cls = self.__class__
|
||||
if cls._enemizer_path is None:
|
||||
cls._enemizer_path = settings.get_settings().generator.enemizer_path
|
||||
assert isinstance(cls._enemizer_path, str)
|
||||
return cls._enemizer_path
|
||||
|
||||
# custom instance vars
|
||||
dungeon_local_item_names: typing.Set[str]
|
||||
dungeon_specific_item_names: typing.Set[str]
|
||||
@@ -295,8 +305,6 @@ class ALTTPWorld(World):
|
||||
self.required_medallions = ["Ether", "Quake"]
|
||||
self.escape_assist = []
|
||||
self.shops = []
|
||||
self.enemy_shuffle_state = None
|
||||
self.pot_shuffle_state = None
|
||||
self.logical_heart_containers = 10
|
||||
self.logical_heart_pieces = 24
|
||||
super(ALTTPWorld, self).__init__(*args, **kwargs)
|
||||
@@ -308,6 +316,10 @@ class ALTTPWorld(World):
|
||||
raise FileNotFoundError(rom_file)
|
||||
if multiworld.is_race:
|
||||
import xxtea # noqa
|
||||
for player in multiworld.get_game_players(cls.game):
|
||||
if multiworld.worlds[player].use_enemizer:
|
||||
check_enemizer(multiworld.worlds[player].enemizer_path)
|
||||
break
|
||||
|
||||
def generate_early(self):
|
||||
multiworld = self.multiworld
|
||||
@@ -327,9 +339,6 @@ class ALTTPWorld(World):
|
||||
self.waterfall_fairy_bottle_fill = self.random.choice(bottle_options)
|
||||
self.pyramid_fairy_bottle_fill = self.random.choice(bottle_options)
|
||||
|
||||
if self.options.pot_shuffle:
|
||||
self.pot_shuffle_state = generate_pot_shuffle(self)
|
||||
|
||||
if self.options.mode == 'standard':
|
||||
if self.options.small_key_shuffle:
|
||||
if (self.options.small_key_shuffle not in
|
||||
@@ -555,6 +564,13 @@ class ALTTPWorld(World):
|
||||
def stage_generate_output(cls, multiworld, output_directory):
|
||||
push_shop_inventories(multiworld)
|
||||
|
||||
@property
|
||||
def use_enemizer(self) -> bool:
|
||||
return bool(self.options.boss_shuffle or self.options.enemy_shuffle
|
||||
or self.options.enemy_health != 'default' or self.options.enemy_damage != 'default'
|
||||
or self.options.pot_shuffle or self.options.bush_shuffle
|
||||
or self.options.killable_thieves)
|
||||
|
||||
def generate_output(self, output_directory: str):
|
||||
multiworld = self.multiworld
|
||||
player = self.player
|
||||
@@ -562,9 +578,14 @@ class ALTTPWorld(World):
|
||||
self.pushed_shop_inventories.wait()
|
||||
|
||||
try:
|
||||
use_enemizer = self.use_enemizer
|
||||
|
||||
rom = LocalRom(get_base_rom_path())
|
||||
|
||||
patch_rom(multiworld, rom, player)
|
||||
patch_rom(multiworld, rom, player, use_enemizer)
|
||||
|
||||
if use_enemizer:
|
||||
patch_enemizer(self, rom, self.enemizer_path, output_directory)
|
||||
|
||||
if multiworld.is_race:
|
||||
patch_race_rom(rom, multiworld, player)
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
These modules are vendored/generated from the upstream Enemizer compiled release and source that were already present
|
||||
locally in `/home/alchav/PycharmProjects/Archipelago/EnemizerCLI` and `/home/alchav/PycharmProjects/Archipelago/Enemizer`.
|
||||
|
||||
Source details:
|
||||
|
||||
- Upstream project: `Ijwu/Enemizer`
|
||||
- Release family: `7.1`
|
||||
- Library version from `EnemizerCLI/EnemizerCLI.Core.deps.json`: `EnemizerLibrary/7.1.0`
|
||||
|
||||
Vendored data modules:
|
||||
|
||||
- `base_patch_data.py`
|
||||
- `symbols.py`
|
||||
- `enemy_room_metadata.py`
|
||||
- `enemy_sprite_requirements.py`
|
||||
- `overworld_enemy_metadata.py`
|
||||
- `dungeon_sprite_addresses.py`
|
||||
- `pot_shuffle_data.py`
|
||||
|
||||
Purpose:
|
||||
|
||||
- `base_patch_data.py` contains the generated base patch Enemizer applies before feature-specific randomization.
|
||||
- `symbols.py` contains the assembled symbol map consumed by Enemizer's runtime code for ROM addresses.
|
||||
- `enemy_room_metadata.py` and `overworld_enemy_metadata.py` contain room and area grouping/randomization constraints.
|
||||
- `enemy_sprite_requirements.py` contains the sprite metadata used by the native enemy shuffle implementation.
|
||||
- `dungeon_sprite_addresses.py` contains dungeon sprite slot metadata derived from Enemizer's source tables and keyed-enemy address list.
|
||||
- `pot_shuffle_data.py` contains the native pot shuffle room/item source data.
|
||||
@@ -1 +0,0 @@
|
||||
"""Native ALTTP Enemizer data modules."""
|
||||
File diff suppressed because one or more lines are too long
@@ -1,202 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
class DungeonSpriteAddressData(NamedTuple):
|
||||
room_id: int
|
||||
sprite_id_addresses: tuple[int, ...]
|
||||
|
||||
DUNGEON_SPRITE_ADDRESSES = (
|
||||
DungeonSpriteAddressData(room_id=2, sprite_id_addresses=(317750, 317753, 317756, 317759, 317762, 317792, 317795)),
|
||||
DungeonSpriteAddressData(room_id=4, sprite_id_addresses=(317803, 317806, 317809, 317812, 317827, 317839, 317842, 317845)),
|
||||
DungeonSpriteAddressData(room_id=9, sprite_id_addresses=(317904, 317907, 317910)),
|
||||
DungeonSpriteAddressData(room_id=10, sprite_id_addresses=(317915, 317918, 317921, 317924, 317930, 317933)),
|
||||
DungeonSpriteAddressData(room_id=11, sprite_id_addresses=(317941, 317944, 317947, 317950, 317953, 317956, 317959, 317962, 317965)),
|
||||
DungeonSpriteAddressData(room_id=14, sprite_id_addresses=(317978, 317981, 317984)),
|
||||
DungeonSpriteAddressData(room_id=17, sprite_id_addresses=(317992, 317995, 317998, 318001, 318004, 318007, 318010, 318013)),
|
||||
DungeonSpriteAddressData(room_id=19, sprite_id_addresses=(318029, 318032, 318035, 318038, 318044, 318056, 318053, 318041)),
|
||||
DungeonSpriteAddressData(room_id=21, sprite_id_addresses=(318105, 318108, 318111, 318114, 318117, 318120)),
|
||||
DungeonSpriteAddressData(room_id=22, sprite_id_addresses=(318125, 318128, 318131, 318134, 318137, 318140, 318143)),
|
||||
DungeonSpriteAddressData(room_id=23, sprite_id_addresses=(318157, 318160, 318163, 318166, 318169, 318172)),
|
||||
DungeonSpriteAddressData(room_id=25, sprite_id_addresses=(318177, 318180, 318183, 318186)),
|
||||
DungeonSpriteAddressData(room_id=26, sprite_id_addresses=(318191, 318194, 318197, 318200, 318203, 318206, 318209, 318212, 318218)),
|
||||
DungeonSpriteAddressData(room_id=27, sprite_id_addresses=(318232, 318235, 318238, 318241)),
|
||||
DungeonSpriteAddressData(room_id=30, sprite_id_addresses=(318284, 318287, 318290, 318293, 318296, 318299)),
|
||||
DungeonSpriteAddressData(room_id=31, sprite_id_addresses=(318304, 318307, 318310, 318313, 318316, 318319, 318322, 318325)),
|
||||
DungeonSpriteAddressData(room_id=33, sprite_id_addresses=(318335, 318341, 318344, 318347, 318350, 318353, 318356, 318359, 318362, 318365, 318368)),
|
||||
DungeonSpriteAddressData(room_id=34, sprite_id_addresses=(318373, 318376, 318379, 318382, 318385, 318388, 318391)),
|
||||
DungeonSpriteAddressData(room_id=36, sprite_id_addresses=(318413, 318416, 318419, 318422, 318425, 318428, 318431)),
|
||||
DungeonSpriteAddressData(room_id=38, sprite_id_addresses=(318471, 318438, 318441, 318444, 318447, 318450, 318453, 318459, 318462, 318465, 318468)),
|
||||
DungeonSpriteAddressData(room_id=39, sprite_id_addresses=(318476, 318479, 318482, 318485, 318488, 318491, 318494)),
|
||||
DungeonSpriteAddressData(room_id=40, sprite_id_addresses=(318511,)),
|
||||
DungeonSpriteAddressData(room_id=42, sprite_id_addresses=(318530, 318533, 318536, 318539, 318542, 318545)),
|
||||
DungeonSpriteAddressData(room_id=43, sprite_id_addresses=(318556, 318559, 318562, 318565, 318568, 318571)),
|
||||
DungeonSpriteAddressData(room_id=46, sprite_id_addresses=(318590, 318593, 318596, 318599, 318602, 318605)),
|
||||
DungeonSpriteAddressData(room_id=49, sprite_id_addresses=(318621, 318624, 318627, 318630, 318633, 318636, 318639, 318642, 318645, 318648)),
|
||||
DungeonSpriteAddressData(room_id=50, sprite_id_addresses=(318653, 318656, 318659, 318662, 318665)),
|
||||
DungeonSpriteAddressData(room_id=52, sprite_id_addresses=(318681, 318684, 318687, 318693, 318696, 318699, 318690)),
|
||||
DungeonSpriteAddressData(room_id=53, sprite_id_addresses=(318710, 318713, 318716, 318719, 318722, 318728, 318731, 318734, 318725)),
|
||||
DungeonSpriteAddressData(room_id=54, sprite_id_addresses=(318742, 318745, 318754, 318760, 318763)),
|
||||
DungeonSpriteAddressData(room_id=55, sprite_id_addresses=(318777, 318780, 318783, 318786, 318792, 318795, 318798, 318801, 318789)),
|
||||
DungeonSpriteAddressData(room_id=56, sprite_id_addresses=(318806, 318809, 318812, 318815, 318818, 318821, 318824)),
|
||||
DungeonSpriteAddressData(room_id=57, sprite_id_addresses=(318829, 318835, 318841, 318844, 318847, 318850)),
|
||||
DungeonSpriteAddressData(room_id=58, sprite_id_addresses=(318855, 318858, 318861, 318864, 318867, 318870)),
|
||||
DungeonSpriteAddressData(room_id=59, sprite_id_addresses=(318875, 318878, 318881, 318884, 318887, 318890, 318893)),
|
||||
DungeonSpriteAddressData(room_id=60, sprite_id_addresses=(318898, 318901, 318904)),
|
||||
DungeonSpriteAddressData(room_id=61, sprite_id_addresses=(318915, 318921, 318924, 318927, 318930, 318933, 318939, 318942, 318945, 318948, 318951)),
|
||||
DungeonSpriteAddressData(room_id=62, sprite_id_addresses=(318959, 318962, 318980, 318983, 318989, 318992)),
|
||||
DungeonSpriteAddressData(room_id=63, sprite_id_addresses=(319000, 319006, 319009)),
|
||||
DungeonSpriteAddressData(room_id=64, sprite_id_addresses=(319014, 319017, 319023, 319026, 319029)),
|
||||
DungeonSpriteAddressData(room_id=65, sprite_id_addresses=(319036, 319039, 319042, 319045)),
|
||||
DungeonSpriteAddressData(room_id=66, sprite_id_addresses=(319050, 319053, 319056, 319059, 319062, 319065)),
|
||||
DungeonSpriteAddressData(room_id=67, sprite_id_addresses=(319070, 319073)),
|
||||
DungeonSpriteAddressData(room_id=68, sprite_id_addresses=(319084, 319087, 319090, 319093, 319096, 319102)),
|
||||
DungeonSpriteAddressData(room_id=69, sprite_id_addresses=(319110, 319116, 319119, 319131, 319134, 319137, 319113, 319122, 319125, 319128)),
|
||||
DungeonSpriteAddressData(room_id=70, sprite_id_addresses=(319142, 319148, 319154)),
|
||||
DungeonSpriteAddressData(room_id=73, sprite_id_addresses=(319161, 319164, 319167, 319170, 319173, 319176, 319182, 319185, 319188, 319191, 319194, 319197)),
|
||||
DungeonSpriteAddressData(room_id=74, sprite_id_addresses=(319205, 319208)),
|
||||
DungeonSpriteAddressData(room_id=75, sprite_id_addresses=(319213, 319216, 319219, 319222, 319225, 319228, 319231, 319234)),
|
||||
DungeonSpriteAddressData(room_id=76, sprite_id_addresses=(319245, 319248, 319251, 319254, 319257, 319260)),
|
||||
DungeonSpriteAddressData(room_id=78, sprite_id_addresses=(319270, 319273, 319276, 319279)),
|
||||
DungeonSpriteAddressData(room_id=80, sprite_id_addresses=(319295, 319298, 319301)),
|
||||
DungeonSpriteAddressData(room_id=81, sprite_id_addresses=(319309, 319312)),
|
||||
DungeonSpriteAddressData(room_id=82, sprite_id_addresses=(319317, 319320, 319323)),
|
||||
DungeonSpriteAddressData(room_id=83, sprite_id_addresses=(319328, 319331, 319334, 319337, 319340, 319343, 319346, 319349, 319352, 319355, 319358, 319361, 319364)),
|
||||
DungeonSpriteAddressData(room_id=84, sprite_id_addresses=(319369, 319372, 319375, 319378, 319381, 319384, 319387, 319390)),
|
||||
DungeonSpriteAddressData(room_id=85, sprite_id_addresses=(319398, 319401)),
|
||||
DungeonSpriteAddressData(room_id=86, sprite_id_addresses=(319415, 319418, 319421, 319424, 319430, 319433, 319436, 319442, 319439)),
|
||||
DungeonSpriteAddressData(room_id=87, sprite_id_addresses=(319447, 319450, 319453, 319456, 319459, 319462, 319468, 319471, 319474, 319477, 319480, 319483, 319486, 319489)),
|
||||
DungeonSpriteAddressData(room_id=88, sprite_id_addresses=(319497, 319500, 319506, 319509, 319515, 319518, 319521)),
|
||||
DungeonSpriteAddressData(room_id=89, sprite_id_addresses=(319526, 319529, 319538, 319544, 319547, 319550, 319553, 319556, 319559, 319541)),
|
||||
DungeonSpriteAddressData(room_id=91, sprite_id_addresses=(319575, 319578, 319581, 319584)),
|
||||
DungeonSpriteAddressData(room_id=93, sprite_id_addresses=(319615, 319618, 319621, 319624, 319627, 319633, 319636, 319639, 319651, 319642, 319645, 319648, 319630)),
|
||||
DungeonSpriteAddressData(room_id=94, sprite_id_addresses=(319659, 319662, 319665, 319668)),
|
||||
DungeonSpriteAddressData(room_id=95, sprite_id_addresses=(319673, 319676, 319679)),
|
||||
DungeonSpriteAddressData(room_id=96, sprite_id_addresses=(319684,)),
|
||||
DungeonSpriteAddressData(room_id=97, sprite_id_addresses=(319689, 319692, 319695)),
|
||||
DungeonSpriteAddressData(room_id=98, sprite_id_addresses=(319700, 319703, 319706)),
|
||||
DungeonSpriteAddressData(room_id=99, sprite_id_addresses=(319714, 319711)),
|
||||
DungeonSpriteAddressData(room_id=100, sprite_id_addresses=(319719, 319725, 319728, 319731, 319734, 319737)),
|
||||
DungeonSpriteAddressData(room_id=101, sprite_id_addresses=(319760, 319763, 319766, 319769, 319772)),
|
||||
DungeonSpriteAddressData(room_id=102, sprite_id_addresses=(319777, 319783, 319786, 319795, 319798, 319801, 319804, 319810)),
|
||||
DungeonSpriteAddressData(room_id=103, sprite_id_addresses=(319818, 319821, 319824, 319827, 319830, 319833, 319836, 319839, 319842)),
|
||||
DungeonSpriteAddressData(room_id=104, sprite_id_addresses=(319859, 319865, 319868)),
|
||||
DungeonSpriteAddressData(room_id=106, sprite_id_addresses=(319873, 319876, 319879, 319882, 319885, 319888)),
|
||||
DungeonSpriteAddressData(room_id=107, sprite_id_addresses=(319899, 319902, 319905, 319911, 319914, 319917, 319920, 319923, 319926, 319929, 319932)),
|
||||
DungeonSpriteAddressData(room_id=109, sprite_id_addresses=(319954, 319957, 319960, 319963, 319966, 319969, 319972, 319975, 319978)),
|
||||
DungeonSpriteAddressData(room_id=110, sprite_id_addresses=(319983, 319986, 319989, 319992, 319995)),
|
||||
DungeonSpriteAddressData(room_id=113, sprite_id_addresses=(320000, 320003)),
|
||||
DungeonSpriteAddressData(room_id=114, sprite_id_addresses=(320011, 320017)),
|
||||
DungeonSpriteAddressData(room_id=115, sprite_id_addresses=(320022, 320025, 320028, 320031, 320034, 320037)),
|
||||
DungeonSpriteAddressData(room_id=116, sprite_id_addresses=(320045, 320048, 320051, 320054, 320057, 320060, 320063, 320066)),
|
||||
DungeonSpriteAddressData(room_id=117, sprite_id_addresses=(320071, 320074, 320077, 320080, 320083, 320086, 320095, 320098)),
|
||||
DungeonSpriteAddressData(room_id=118, sprite_id_addresses=(320106, 320109, 320112, 320115, 320121)),
|
||||
DungeonSpriteAddressData(room_id=119, sprite_id_addresses=(320126, 320138, 320141)),
|
||||
DungeonSpriteAddressData(room_id=123, sprite_id_addresses=(320146, 320149, 320152, 320155, 320158, 320161, 320167, 320170, 320173, 320176)),
|
||||
DungeonSpriteAddressData(room_id=124, sprite_id_addresses=(320181, 320184, 320187, 320190, 320193, 320196)),
|
||||
DungeonSpriteAddressData(room_id=125, sprite_id_addresses=(320216, 320219, 320225, 320228, 320234, 320222, 320231, 320204, 320207, 320210, 320213, 320222, 320231)),
|
||||
DungeonSpriteAddressData(room_id=126, sprite_id_addresses=(320242, 320245, 320254, 320257)),
|
||||
DungeonSpriteAddressData(room_id=128, sprite_id_addresses=(320291, 320294)),
|
||||
DungeonSpriteAddressData(room_id=129, sprite_id_addresses=(320302, 320305)),
|
||||
DungeonSpriteAddressData(room_id=130, sprite_id_addresses=(320310, 320313, 320316)),
|
||||
DungeonSpriteAddressData(room_id=131, sprite_id_addresses=(320321, 320324, 320327, 320330, 320333, 320336, 320339, 320342, 320345, 320348)),
|
||||
DungeonSpriteAddressData(room_id=132, sprite_id_addresses=(320353, 320356, 320359, 320362, 320365, 320368, 320371)),
|
||||
DungeonSpriteAddressData(room_id=133, sprite_id_addresses=(320376, 320379, 320382, 320385, 320388, 320391, 320394, 320397, 320400, 320403)),
|
||||
DungeonSpriteAddressData(room_id=135, sprite_id_addresses=(320410, 320413, 320416, 320419, 320434, 320437, 320440, 320446, 320422)),
|
||||
DungeonSpriteAddressData(room_id=139, sprite_id_addresses=(320468, 320471, 320474, 320477, 320480)),
|
||||
DungeonSpriteAddressData(room_id=140, sprite_id_addresses=(320503, 320506, 320509, 320512, 320518, 320521, 320527, 320524, 320515)),
|
||||
DungeonSpriteAddressData(room_id=141, sprite_id_addresses=(320538, 320541, 320544, 320547, 320550, 320556, 320559, 320562, 320565, 320568, 320571, 320535)),
|
||||
DungeonSpriteAddressData(room_id=142, sprite_id_addresses=(320579, 320582, 320585, 320588, 320591, 320594, 320597)),
|
||||
DungeonSpriteAddressData(room_id=145, sprite_id_addresses=(320610, 320616, 320619, 320622, 320625, 320613)),
|
||||
DungeonSpriteAddressData(room_id=146, sprite_id_addresses=(320636, 320639, 320642, 320645, 320648, 320654, 320657, 320660, 320663)),
|
||||
DungeonSpriteAddressData(room_id=147, sprite_id_addresses=(320668, 320671, 320674, 320677, 320680, 320683, 320686, 320689)),
|
||||
DungeonSpriteAddressData(room_id=149, sprite_id_addresses=(320694, 320697, 320700, 320703)),
|
||||
DungeonSpriteAddressData(room_id=151, sprite_id_addresses=(320728,)),
|
||||
DungeonSpriteAddressData(room_id=152, sprite_id_addresses=(320733, 320736, 320739, 320742, 320745)),
|
||||
DungeonSpriteAddressData(room_id=153, sprite_id_addresses=(320750, 320753, 320756, 320759, 320765, 320768, 320771, 320774, 320777, 320780)),
|
||||
DungeonSpriteAddressData(room_id=155, sprite_id_addresses=(320794, 320797, 320800, 320803, 320806, 320809, 320812, 320815, 320818, 320821)),
|
||||
DungeonSpriteAddressData(room_id=156, sprite_id_addresses=(320826, 320829, 320832, 320835, 320838, 320841)),
|
||||
DungeonSpriteAddressData(room_id=157, sprite_id_addresses=(320852, 320855, 320858, 320861, 320864, 320867, 320870, 320873)),
|
||||
DungeonSpriteAddressData(room_id=158, sprite_id_addresses=(320878, 320881, 320884, 320887)),
|
||||
DungeonSpriteAddressData(room_id=159, sprite_id_addresses=(320907, 320910)),
|
||||
DungeonSpriteAddressData(room_id=160, sprite_id_addresses=(320915, 320918, 320921)),
|
||||
DungeonSpriteAddressData(room_id=161, sprite_id_addresses=(320929, 320932, 320935, 320938, 320941, 320944, 320947, 320950)),
|
||||
DungeonSpriteAddressData(room_id=165, sprite_id_addresses=(320968, 320971, 320974, 320977, 320980, 320983, 320986, 320989, 320998, 321001)),
|
||||
DungeonSpriteAddressData(room_id=167, sprite_id_addresses=(321014, 321017)),
|
||||
DungeonSpriteAddressData(room_id=168, sprite_id_addresses=(321022, 321025, 321028, 321031, 321034)),
|
||||
DungeonSpriteAddressData(room_id=169, sprite_id_addresses=(321039, 321042, 321057, 321060, 321045, 321048, 321051, 321054)),
|
||||
DungeonSpriteAddressData(room_id=170, sprite_id_addresses=(321065, 321068, 321071, 321074, 321077, 321080)),
|
||||
DungeonSpriteAddressData(room_id=171, sprite_id_addresses=(321088, 321091, 321094, 321097, 321100, 321103, 321106)),
|
||||
DungeonSpriteAddressData(room_id=174, sprite_id_addresses=(321116, 321119)),
|
||||
DungeonSpriteAddressData(room_id=176, sprite_id_addresses=(321129, 321132, 321135, 321138, 321141, 321144, 321147, 321150, 321153, 321156, 321159, 321165, 321168)),
|
||||
DungeonSpriteAddressData(room_id=177, sprite_id_addresses=(321173, 321176, 321179, 321182, 321185, 321188, 321191, 321194, 321197, 321200)),
|
||||
DungeonSpriteAddressData(room_id=178, sprite_id_addresses=(321205, 321208, 321211, 321214, 321217, 321220, 321223, 321226, 321229, 321232, 321235, 321238, 321241, 321244)),
|
||||
DungeonSpriteAddressData(room_id=179, sprite_id_addresses=(321249, 321252, 321255, 321258, 321261)),
|
||||
DungeonSpriteAddressData(room_id=182, sprite_id_addresses=(321277, 321280, 321289, 321292, 321301, 321304)),
|
||||
DungeonSpriteAddressData(room_id=183, sprite_id_addresses=(321309, 321312)),
|
||||
DungeonSpriteAddressData(room_id=184, sprite_id_addresses=(321317, 321320, 321323, 321326, 321329, 321332)),
|
||||
DungeonSpriteAddressData(room_id=186, sprite_id_addresses=(321342, 321345, 321348, 321351, 321354, 321357, 321360)),
|
||||
DungeonSpriteAddressData(room_id=187, sprite_id_addresses=(321365, 321368, 321371, 321374, 321377, 321380, 321386, 321389, 321392, 321395, 321383)),
|
||||
DungeonSpriteAddressData(room_id=188, sprite_id_addresses=(321403, 321406, 321409, 321412, 321418, 321421, 321424, 321433, 321400, 321415, 321427, 321430)),
|
||||
DungeonSpriteAddressData(room_id=190, sprite_id_addresses=(321440, 321446, 321449, 321452, 321455, 321458)),
|
||||
DungeonSpriteAddressData(room_id=192, sprite_id_addresses=(321471, 321474, 321477, 321480, 321486, 321489, 321492, 321495)),
|
||||
DungeonSpriteAddressData(room_id=193, sprite_id_addresses=(321503, 321506, 321509, 321512, 321518, 321524, 321527, 321530, 321521, 321515, 321536)),
|
||||
DungeonSpriteAddressData(room_id=194, sprite_id_addresses=(321547, 321550, 321553, 321556, 321562, 321559, 321544, 321541)),
|
||||
DungeonSpriteAddressData(room_id=195, sprite_id_addresses=(321567, 321585, 321588)),
|
||||
DungeonSpriteAddressData(room_id=196, sprite_id_addresses=(321605, 321608, 321611, 321614, 321617, 321620)),
|
||||
DungeonSpriteAddressData(room_id=201, sprite_id_addresses=(321697, 321700, 321703)),
|
||||
DungeonSpriteAddressData(room_id=203, sprite_id_addresses=(321708, 321717, 321720, 321723, 321726, 321729, 321732, 321735, 321738, 321741, 321714, 321711)),
|
||||
DungeonSpriteAddressData(room_id=204, sprite_id_addresses=(321746, 321749, 321755, 321758, 321761, 321770, 321773, 321776, 321779, 321782, 321785, 321752, 321764, 321767)),
|
||||
DungeonSpriteAddressData(room_id=206, sprite_id_addresses=(321790, 321793, 321799, 321802, 321805, 321808, 321811)),
|
||||
DungeonSpriteAddressData(room_id=208, sprite_id_addresses=(321816, 321819, 321822, 321825, 321828, 321831, 321834, 321837, 321840, 321843, 321846)),
|
||||
DungeonSpriteAddressData(room_id=209, sprite_id_addresses=(321851, 321854, 321857, 321860, 321863, 321866, 321869, 321872)),
|
||||
DungeonSpriteAddressData(room_id=210, sprite_id_addresses=(321877, 321880, 321883, 321886, 321889, 321892, 321895, 321898, 321901, 321904)),
|
||||
DungeonSpriteAddressData(room_id=216, sprite_id_addresses=(321937, 321940, 321943, 321946, 321949, 321952, 321955, 321958, 321961, 321964, 321967)),
|
||||
DungeonSpriteAddressData(room_id=217, sprite_id_addresses=(321975, 321978, 321981, 321972)),
|
||||
DungeonSpriteAddressData(room_id=218, sprite_id_addresses=(321986, 321989)),
|
||||
DungeonSpriteAddressData(room_id=219, sprite_id_addresses=(321994, 321997, 322000, 322006, 322003, 322012, 322009)),
|
||||
DungeonSpriteAddressData(room_id=220, sprite_id_addresses=(322020, 322023, 322026, 322029, 322032, 322035, 322047, 322017, 322038, 322041, 322044)),
|
||||
DungeonSpriteAddressData(room_id=223, sprite_id_addresses=(322063, 322066)),
|
||||
DungeonSpriteAddressData(room_id=224, sprite_id_addresses=(322071, 322074, 322077, 322080)),
|
||||
DungeonSpriteAddressData(room_id=232, sprite_id_addresses=(322189, 322192, 322195, 322198)),
|
||||
DungeonSpriteAddressData(room_id=238, sprite_id_addresses=(322213, 322216, 322219, 322222, 322225)),
|
||||
DungeonSpriteAddressData(room_id=239, sprite_id_addresses=(322230, 322233, 322236)),
|
||||
DungeonSpriteAddressData(room_id=249, sprite_id_addresses=(322323, 322326, 322329, 322332)),
|
||||
DungeonSpriteAddressData(room_id=254, sprite_id_addresses=(322378, 322381, 322384, 322387, 322390)),
|
||||
DungeonSpriteAddressData(room_id=263, sprite_id_addresses=(322444, 322447)),
|
||||
DungeonSpriteAddressData(room_id=264, sprite_id_addresses=(322452, 322455, 322458, 322461)),
|
||||
DungeonSpriteAddressData(room_id=267, sprite_id_addresses=(322494,)),
|
||||
DungeonSpriteAddressData(room_id=269, sprite_id_addresses=(322525, 322528)),
|
||||
DungeonSpriteAddressData(room_id=291, sprite_id_addresses=(322671, 322674, 322677, 322680)),
|
||||
)
|
||||
|
||||
KEYED_SPRITE_ID_ADDRESSES = frozenset((317984,
|
||||
318044,
|
||||
318335,
|
||||
318835,
|
||||
318915,
|
||||
318983,
|
||||
320003,
|
||||
320011,
|
||||
320294,
|
||||
320759,
|
||||
321292,
|
||||
321480,
|
||||
321530,
|
||||
320000,
|
||||
321159,
|
||||
321937,
|
||||
321940,
|
||||
321943,
|
||||
321946,
|
||||
321949,
|
||||
321952,
|
||||
321955,
|
||||
321958,
|
||||
321961,
|
||||
321964,
|
||||
321967,
|
||||
321424,
|
||||
321421,
|
||||
321418))
|
||||
@@ -1,106 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import NamedTuple, Optional
|
||||
|
||||
|
||||
class RoomGroupRequirementData(NamedTuple):
|
||||
group_id: Optional[int]
|
||||
subgroup_0: Optional[int]
|
||||
subgroup_1: Optional[int]
|
||||
subgroup_2: Optional[int]
|
||||
subgroup_3: Optional[int]
|
||||
rooms: tuple[int, ...]
|
||||
|
||||
SHUTTER_ROOM_IDS = frozenset((184,
|
||||
11,
|
||||
27,
|
||||
75,
|
||||
4,
|
||||
36,
|
||||
182,
|
||||
40,
|
||||
14,
|
||||
46,
|
||||
62,
|
||||
110,
|
||||
49,
|
||||
135,
|
||||
68,
|
||||
69,
|
||||
83,
|
||||
117,
|
||||
133,
|
||||
61,
|
||||
93,
|
||||
107,
|
||||
109,
|
||||
123,
|
||||
125,
|
||||
141,
|
||||
150,
|
||||
165,
|
||||
113,
|
||||
168,
|
||||
216,
|
||||
176,
|
||||
192,
|
||||
224,
|
||||
178,
|
||||
210,
|
||||
239,
|
||||
268,
|
||||
291))
|
||||
WATER_ROOM_IDS = frozenset((22, 40, 52, 54, 56, 70, 102))
|
||||
DONT_RANDOMIZE_ROOM_IDS = frozenset((0, 1, 3, 13, 20, 32, 48, 127))
|
||||
NO_SPECIAL_ENEMIES_STANDARD_ROOM_IDS = frozenset((1, 2, 17, 33, 34, 50, 65, 66, 80, 81, 82, 85, 96, 97, 98, 112, 113, 114, 128, 129, 130))
|
||||
BOSS_ROOM_IDS = frozenset((200, 51, 108, 7, 77, 90, 6, 41, 172, 222, 144, 164, 32, 13, 0))
|
||||
|
||||
ROOM_GROUP_REQUIREMENTS = (
|
||||
RoomGroupRequirementData(group_id=1, subgroup_0=70, subgroup_1=73, subgroup_2=28, subgroup_3=82, rooms=(228, 240)),
|
||||
RoomGroupRequirementData(group_id=5, subgroup_0=75, subgroup_1=77, subgroup_2=74, subgroup_3=90, rooms=(243, 265, 270, 271, 272, 273, 282, 284, 290)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=75, subgroup_1=None, subgroup_2=None, subgroup_3=None, rooms=(255, 274, 287)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=77, subgroup_2=None, subgroup_3=21, rooms=(289,)),
|
||||
RoomGroupRequirementData(group_id=7, subgroup_0=75, subgroup_1=77, subgroup_2=57, subgroup_3=54, rooms=(8, 44, 276, 277)),
|
||||
RoomGroupRequirementData(group_id=13, subgroup_0=81, subgroup_1=None, subgroup_2=None, subgroup_3=None, rooms=(85, 258, 260)),
|
||||
RoomGroupRequirementData(group_id=14, subgroup_0=71, subgroup_1=73, subgroup_2=76, subgroup_3=80, rooms=(18, 261, 266)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=80, rooms=(264,)),
|
||||
RoomGroupRequirementData(group_id=15, subgroup_0=79, subgroup_1=77, subgroup_2=74, subgroup_3=80, rooms=(244, 245, 257, 259, 262, 280, 281)),
|
||||
RoomGroupRequirementData(group_id=18, subgroup_0=85, subgroup_1=61, subgroup_2=66, subgroup_3=67, rooms=(32, 48)),
|
||||
RoomGroupRequirementData(group_id=24, subgroup_0=85, subgroup_1=26, subgroup_2=66, subgroup_3=67, rooms=(13,)),
|
||||
RoomGroupRequirementData(group_id=34, subgroup_0=33, subgroup_1=65, subgroup_2=69, subgroup_3=51, rooms=(0,)),
|
||||
RoomGroupRequirementData(group_id=40, subgroup_0=14, subgroup_1=None, subgroup_2=74, subgroup_3=80, rooms=(225, 256, 293, 292, 294)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=14, subgroup_1=30, subgroup_2=None, subgroup_3=None, rooms=(291,)),
|
||||
RoomGroupRequirementData(group_id=23, subgroup_0=64, subgroup_1=None, subgroup_2=None, subgroup_3=63, rooms=()),
|
||||
RoomGroupRequirementData(group_id=9, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=29, rooms=(227,)),
|
||||
RoomGroupRequirementData(group_id=11, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=61, rooms=()),
|
||||
RoomGroupRequirementData(group_id=22, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=49, rooms=()),
|
||||
RoomGroupRequirementData(group_id=22, subgroup_0=None, subgroup_1=None, subgroup_2=60, subgroup_3=None, rooms=()),
|
||||
RoomGroupRequirementData(group_id=21, subgroup_0=None, subgroup_1=None, subgroup_2=58, subgroup_3=62, rooms=()),
|
||||
RoomGroupRequirementData(group_id=28, subgroup_0=None, subgroup_1=None, subgroup_2=38, subgroup_3=82, rooms=(14, 126, 142, 158, 190)),
|
||||
RoomGroupRequirementData(group_id=12, subgroup_0=None, subgroup_1=None, subgroup_2=48, subgroup_3=None, rooms=()),
|
||||
RoomGroupRequirementData(group_id=26, subgroup_0=None, subgroup_1=None, subgroup_2=56, subgroup_3=None, rooms=()),
|
||||
RoomGroupRequirementData(group_id=20, subgroup_0=None, subgroup_1=None, subgroup_2=57, subgroup_3=None, rooms=()),
|
||||
RoomGroupRequirementData(group_id=32, subgroup_0=None, subgroup_1=44, subgroup_2=59, subgroup_3=None, rooms=()),
|
||||
RoomGroupRequirementData(group_id=3, subgroup_0=93, subgroup_1=None, subgroup_2=None, subgroup_3=None, rooms=(81,)),
|
||||
RoomGroupRequirementData(group_id=42, subgroup_0=21, subgroup_1=None, subgroup_2=None, subgroup_3=None, rooms=(286,)),
|
||||
RoomGroupRequirementData(group_id=10, subgroup_0=47, subgroup_1=None, subgroup_2=46, subgroup_3=None, rooms=(92, 117, 185, 217)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=34, subgroup_3=None, rooms=(54, 70, 102, 118)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=32, subgroup_2=None, subgroup_3=None, rooms=(62, 159)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=31, subgroup_1=None, subgroup_2=None, subgroup_3=None, rooms=(127,)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=35, subgroup_3=None, rooms=(57, 73, 86, 87, 104, 141)),
|
||||
RoomGroupRequirementData(group_id=37, subgroup_0=31, subgroup_1=None, subgroup_2=39, subgroup_3=82, rooms=(36, 180, 181, 198, 199, 214)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=82, rooms=(23, 42, 68, 76, 86, 88, 89, 103, 104, 126, 139, 235, 251)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=83, rooms=(23, 42, 76, 89, 103, 104, 126, 139, 235, 251)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=82, rooms=(11, 19, 27, 30, 42, 43, 49, 61, 62, 91, 107, 119, 135, 139, 145, 146, 155, 157, 161, 171, 182, 191, 193, 196, 239)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=83, rooms=(11, 19, 27, 30, 42, 43, 49, 53, 62, 91, 107, 119, 135, 139, 145, 146, 155, 157, 161, 171, 182, 191, 193, 196, 239)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=82, rooms=(19, 35, 150, 165, 195, 197, 213)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=83, rooms=(19, 35, 150, 165, 197, 213)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=82, rooms=(26, 38, 43, 64, 74, 87, 107, 123)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=83, rooms=(38, 43, 64, 74, 87, 107, 123, 206)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=82, rooms=(2, 88, 100, 140, 267)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=82, rooms=(26, 61, 68, 86, 94, 124, 149, 195)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=83, rooms=(4, 63, 206)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=83, rooms=(53, 55, 118)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=34, subgroup_3=None, rooms=(40,)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=37, subgroup_3=None, rooms=(151,)),
|
||||
)
|
||||
@@ -1,295 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import NamedTuple, Optional
|
||||
|
||||
|
||||
class EnemySpriteRequirementData(NamedTuple):
|
||||
sprite_name: str
|
||||
sprite_id: int
|
||||
boss: bool
|
||||
overlord: bool
|
||||
do_not_randomize: bool
|
||||
killable: bool
|
||||
npc: bool
|
||||
never_use_dungeon: bool
|
||||
never_use_overworld: bool
|
||||
cannot_have_key: bool
|
||||
is_object: bool
|
||||
absorbable: bool
|
||||
is_water_sprite: bool
|
||||
is_enemy_sprite: bool
|
||||
group_ids: tuple[int, ...]
|
||||
subgroup_0: tuple[int, ...]
|
||||
subgroup_1: tuple[int, ...]
|
||||
subgroup_2: tuple[int, ...]
|
||||
subgroup_3: tuple[int, ...]
|
||||
parameters: Optional[int]
|
||||
special_glitched: bool
|
||||
excluded_rooms: tuple[int, ...]
|
||||
dont_randomize_rooms: tuple[int, ...]
|
||||
spawnable_rooms: tuple[int, ...]
|
||||
|
||||
ENEMY_SPRITE_REQUIREMENTS = (
|
||||
EnemySpriteRequirementData(sprite_name='RavenSprite', sprite_id=0, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17, 25), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='VultureSprite', sprite_id=1, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(18,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='EmptySprite', sprite_id=3, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='PullSwitch_GoodSprite', sprite_id=4, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='PullSwitch_TrapSprite', sprite_id=6, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='Octorok_OneWaySprite', sprite_id=8, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12, 24), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MoldormSprite', sprite_id=9, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(48,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='Octorok_FourWaySprite', sprite_id=10, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='ChickenSprite', sprite_id=11, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(21, 80), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BuzzblobSprite', sprite_id=13, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='SnapdragonSprite', sprite_id=14, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(22,), subgroup_1=(), subgroup_2=(23,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OctoballoonSprite', sprite_id=15, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OctoballoonHatchlingsSprite', sprite_id=16, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='HinoxSprite', sprite_id=17, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(22,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MoblinSprite', sprite_id=18, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(23,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MiniHelmasaurSprite', sprite_id=19, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(30,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='GargoylesDomainGateSprite', sprite_id=20, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='AntifairySprite', sprite_id=21, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(64, 210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='SahasrahlaAginahSprite', sprite_id=22, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(76,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BushHoarderSprite', sprite_id=23, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MiniMoldormSprite', sprite_id=24, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(30,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='PoeSprite', sprite_id=25, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(14, 21), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='DwarvesSprite', sprite_id=26, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(77,), subgroup_2=(), subgroup_3=(21,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='ArrowInWall_MaybeSprite', sprite_id=27, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='StatueSprite', sprite_id=28, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268, 63), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='WeathervaneSprite', sprite_id=29, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='CrystalSwitchSprite', sprite_id=30, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BugCatchingKidSprite', sprite_id=31, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(81,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='SluggulaSprite', sprite_id=32, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(37,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='PushSwitchSprite', sprite_id=33, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(83,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='RopaSprite', sprite_id=34, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(22,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='RedBariSprite', sprite_id=35, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(127,), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BlueBariSprite', sprite_id=36, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(127,), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='TalkingTreeSprite', sprite_id=37, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(21,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='HardhatBeetleSprite', sprite_id=38, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(30,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='DeadrockSprite', sprite_id=39, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(16,), parameters=None, special_glitched=False, excluded_rooms=(127, 268), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='StorytellersSprite', sprite_id=40, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BlindHideoutAttendantSprite', sprite_id=41, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(14, 79), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='SweepingLadySprite', sprite_id=42, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MultipurposeSpriteSprite', sprite_id=43, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='LumberjacksSprite', sprite_id=44, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(74,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='TelepathicStones_NoIdeaWhatThisActuallyIsLikelyUnusedSprite', sprite_id=45, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='FluteBoysNotesSprite', sprite_id=46, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='RaceHPNPCsSprite', sprite_id=47, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='Person_MaybeSprite', sprite_id=48, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='FortuneTellerSprite', sprite_id=49, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(75,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='AngryBrothersSprite', sprite_id=50, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(79,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='PullForRupeesSpriteSprite', sprite_id=51, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='ScaredGirl2Sprite', sprite_id=52, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='InnkeeperSprite', sprite_id=53, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='WitchSprite', sprite_id=54, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(76,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='WaterfallSprite', sprite_id=55, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='ArrowTargetSprite', sprite_id=56, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='AverageMiddleAgedManSprite', sprite_id=57, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='HalfMagicBatSprite', sprite_id=58, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(29,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='DashItemSprite', sprite_id=59, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='VillageKidSprite', sprite_id=60, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='Signs_ChickenLadyAlsoShowedUp_ScaredLadiesOutsideHousesSprite', sprite_id=61, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='RockHoarderSprite', sprite_id=62, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='TutorialSoldierSprite', sprite_id=63, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='LightningLockSprite', sprite_id=64, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(63,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BlueSwordSoldier_DetectPlayerSprite', sprite_id=65, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(13, 73), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='GreenSwordSoldierSprite', sprite_id=66, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='RedSpearSoldierSprite', sprite_id=67, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(13, 73), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='AssaultSwordSoldierSprite', sprite_id=68, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='GreenSpearSoldierSprite', sprite_id=69, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(13, 73), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BlueArcherSprite', sprite_id=70, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(72,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='GreenArcherSprite', sprite_id=71, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(72,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='RedJavelinSoldierSprite', sprite_id=72, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='RedJavelinSoldier2Sprite', sprite_id=73, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='RedBombSoldiersSprite', sprite_id=74, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='GreenSoldierRecruits_HMKnightSprite', sprite_id=75, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(73,), subgroup_2=(19,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='GeldmanSprite', sprite_id=76, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(18,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='RabbitSprite', sprite_id=77, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='PopoSprite', sprite_id=78, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(44,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='Popo2Sprite', sprite_id=79, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(44,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='CannonBallsSprite', sprite_id=80, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(46,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='ArmosSprite', sprite_id=81, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(16,), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='GiantZoraSprite', sprite_id=82, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(68,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='ArmosKnightsSprite', sprite_id=83, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(29,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='LanmolasSprite', sprite_id=84, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(49,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='FireballZoraSprite', sprite_id=85, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=True, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12, 24), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='WalkingZoraSprite', sprite_id=86, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=True, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12,), subgroup_3=(68,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='DesertPalaceBarriersSprite', sprite_id=87, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(18,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='CrabSprite', sprite_id=88, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BirdSprite', sprite_id=89, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(55,), subgroup_3=(54,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='SquirrelSprite', sprite_id=90, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(55,), subgroup_3=(54,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='Spark_LeftToRightSprite', sprite_id=91, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='Spark_RightToLeftSprite', sprite_id=92, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='Roller_VerticalMovingSprite', sprite_id=93, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='Roller_VerticalMoving2Sprite', sprite_id=94, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='RollerSprite', sprite_id=95, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='Roller_HorizontalMovingSprite', sprite_id=96, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BeamosSprite', sprite_id=97, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(44,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MasterSwordSprite', sprite_id=98, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(55,), subgroup_3=(54,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='Devalant_NonShooterSprite', sprite_id=99, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='Devalant_ShooterSprite', sprite_id=100, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='ShootingGalleryProprietorSprite', sprite_id=101, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(75,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MovingCannonBallShooters_RightSprite', sprite_id=102, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MovingCannonBallShooters_LeftSprite', sprite_id=103, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MovingCannonBallShooters_DownSprite', sprite_id=104, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MovingCannonBallShooters_UpSprite', sprite_id=105, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BallNChainTrooperSprite', sprite_id=106, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='CannonSoldierSprite', sprite_id=107, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MirrorPortalSprite', sprite_id=108, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='RatSprite', sprite_id=109, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(28, 36), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='RopeSprite', sprite_id=110, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(28, 36), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='KeeseSprite', sprite_id=111, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(28, 36), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='LeeverSprite', sprite_id=113, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='ActivatoForThePonds_WhereYouThrowInItemsSprite', sprite_id=114, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(54,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='UnclePriestSprite', sprite_id=115, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(71, 81), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='RunningManSprite', sprite_id=116, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BottleSalesmanSprite', sprite_id=117, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='PrincessZeldaSprite', sprite_id=118, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='VillageElderSprite', sprite_id=120, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(75,), subgroup_1=(77,), subgroup_2=(74,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='AgahnimSprite', sprite_id=122, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(85,), subgroup_1=(26, 61), subgroup_2=(66,), subgroup_3=(67,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='AgahnimEnergyBallSprite', sprite_id=123, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='FloatingStalfosHeadSprite', sprite_id=124, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BigSpikeTrapSprite', sprite_id=125, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='GuruguruBar_ClockwiseSprite', sprite_id=126, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(181, 150), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='GuruguruBar_CounterClockwiseSprite', sprite_id=127, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(181, 150), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='WinderSprite', sprite_id=128, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='WaterTektiteSprite', sprite_id=129, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=True, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(34,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(40,), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='AntifairyCircleSprite', sprite_id=130, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='GreenEyegoreSprite', sprite_id=131, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(46,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='RedEyegoreSprite', sprite_id=132, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(46,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='KodongosSprite', sprite_id=134, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(42,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MothulaSprite', sprite_id=136, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(56,), subgroup_3=(82,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MothulasBeamSprite', sprite_id=137, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(56,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='SpikeTrapSprite', sprite_id=138, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(40, 11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='GibdoSprite', sprite_id=139, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(35,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='ArrghusSprite', sprite_id=140, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(57,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='ArrghusSpawnSprite', sprite_id=141, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(57,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='TerrorpinSprite', sprite_id=142, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(42,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='SlimeSprite_JumpsOutOfTheFloor', sprite_id=143, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(32,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='WallmasterSprite', sprite_id=144, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(35,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(1, 2, 17, 33, 34, 50, 65, 66, 80, 81, 82, 96, 97, 98, 112, 113, 114, 128, 129, 130, 137, 153, 168, 169, 170, 184, 185, 186, 200, 201, 216, 217, 218, 51, 67, 83, 99, 115, 116, 117, 131, 132, 133, 7, 23, 39, 49, 119, 135, 167, 32, 48, 64, 176, 192, 208, 224, 9, 10, 11, 25, 26, 27, 42, 43, 58, 59, 74, 75, 90, 106, 6, 22, 38, 40, 52, 53, 54, 55, 56, 70, 84, 102, 118, 41, 57, 73, 86, 87, 88, 89, 103, 104, 68, 69, 100, 101, 171, 172, 187, 188, 203, 204, 219, 220, 14, 30, 31, 46, 62, 63, 78, 79, 94, 95, 110, 126, 127, 142, 158, 159, 174, 175, 190, 191, 206, 222, 144, 145, 146, 147, 151, 152, 160, 161, 162, 163, 177, 178, 179, 193, 194, 195, 209, 210, 4, 19, 20, 21, 35, 36, 164, 180, 181, 182, 183, 196, 197, 198, 199, 213, 214, 12, 13, 28, 29, 61, 76, 77, 91, 92, 93, 107, 108, 109, 123, 124, 125, 139, 140, 141, 149, 150, 155, 156, 157, 165, 166)),
|
||||
EnemySpriteRequirementData(sprite_name='StalfosKnightSprite', sprite_id=145, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(32,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='HelmasaurKingSprite', sprite_id=146, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(58,), subgroup_3=(62,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BumperSprite', sprite_id=147, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='SwimmersEvilSprite', sprite_id=148, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=True, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='EyeLaser_RightSprite', sprite_id=149, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='EyeLaser_LeftSprite', sprite_id=150, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='EyeLaser_DownSprite', sprite_id=151, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='EyeLaser_UpSprite', sprite_id=152, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='PengatorSprite', sprite_id=153, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(38,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='KyameronWaterSplashSprite', sprite_id=154, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=True, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(34,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(40,), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='WizzrobeSprite', sprite_id=155, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(37, 41), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='VerminHorizontalSprite', sprite_id=156, boss=False, overlord=False, do_not_randomize=True, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(32,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='VerminVerticalSprite', sprite_id=157, boss=False, overlord=False, do_not_randomize=True, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(32,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='Ostrich_HauntedGroveSprite', sprite_id=158, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(78,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='FluteSprite', sprite_id=159, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='Birds_HauntedGroveSprite', sprite_id=160, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(78,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='FreezorSprite', sprite_id=161, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(38,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='KholdstareSprite', sprite_id=162, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(60,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='KholdstaresShellSprite', sprite_id=163, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='FallingIceSprite', sprite_id=164, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(60,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BlueZazakSprite', sprite_id=165, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(40,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='RedZazakSprite', sprite_id=166, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(40,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='StalfosSprite', sprite_id=167, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BomberFlyingCreaturesFromDarkworldSprite', sprite_id=168, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(27,), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BomberFlyingCreaturesFromDarkworld2Sprite', sprite_id=169, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(27,), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='PikitSprite', sprite_id=170, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(27,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MaidenSprite', sprite_id=171, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='AppleSprite', sprite_id=172, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='LostOldManSprite', sprite_id=173, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(28,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='DownPipeSprite', sprite_id=174, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='UpPipeSprite', sprite_id=175, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='RightPipeSprite', sprite_id=176, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='LeftPipeSprite', sprite_id=177, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='GoodBee_AgainMaybeSprite', sprite_id=178, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='HylianInscriptionSprite', sprite_id=179, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='ThiefsChestSprite', sprite_id=180, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(21,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BombSalesmanSprite', sprite_id=181, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(77,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='KikiSprite', sprite_id=182, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(25,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MaidenInBlindDungeonSprite', sprite_id=183, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MimicSprite', sprite_id=184, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(44,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='FeudingFriendsOnDeathMountainSprite', sprite_id=185, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(20,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='WhirlpoolSprite', sprite_id=186, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(75,), subgroup_1=(), subgroup_2=(74,), subgroup_3=(90,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(255, 274, 287)),
|
||||
EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(75,), subgroup_1=(77,), subgroup_2=(74,), subgroup_3=(90,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(271, 272)),
|
||||
EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(77,), subgroup_2=(74,), subgroup_3=(90,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(272,)),
|
||||
EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(79,), subgroup_1=(), subgroup_2=(74,), subgroup_3=(90,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(280,)),
|
||||
EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(14,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(291, 292)),
|
||||
EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(14,), subgroup_1=(), subgroup_2=(74,), subgroup_3=(90,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(291, 292)),
|
||||
EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(14,), subgroup_1=(), subgroup_2=(74,), subgroup_3=(80,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(293,)),
|
||||
EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(21,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(286,)),
|
||||
EnemySpriteRequirementData(sprite_name='DrunkInTheInnSprite', sprite_id=188, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(79,), subgroup_1=(77,), subgroup_2=(74,), subgroup_3=(80,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='Vitreous_LargeEyeballSprite', sprite_id=189, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(61,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='Vitreous_SmallEyeballSprite', sprite_id=190, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(61,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='VitreousLightningSprite', sprite_id=191, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(61,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='CatFish_QuakeMedallionSprite', sprite_id=192, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(24,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='AgahnimTeleportingZeldaToDarkworldSprite', sprite_id=193, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(85,), subgroup_1=(61,), subgroup_2=(66,), subgroup_3=(67,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BouldersSprite', sprite_id=194, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(16,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='Gibo_FloatingBlobSprite', sprite_id=195, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(40,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='ThiefSprite', sprite_id=196, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(14, 21), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MedusaSprite', sprite_id=197, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='FourWayFireballSpittersSprite', sprite_id=198, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='HokkuBokkuSprite', sprite_id=199, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BigFairyWhoHealsYouSprite', sprite_id=200, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(57,), subgroup_3=(54,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='TektiteSprite', sprite_id=201, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(16,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='ChainChompSprite', sprite_id=202, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='TrinexxSprite', sprite_id=203, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(64,), subgroup_1=(), subgroup_2=(), subgroup_3=(63,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='AnotherPartOfTrinexxSprite', sprite_id=204, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(64,), subgroup_1=(), subgroup_2=(), subgroup_3=(63,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='YetAnotherPartOfTrinexxSprite', sprite_id=205, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(64,), subgroup_1=(), subgroup_2=(), subgroup_3=(63,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BlindTheThiefSprite', sprite_id=206, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(44,), subgroup_2=(59,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='SwamolaSprite', sprite_id=207, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=True, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(25,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='LynelSprite', sprite_id=208, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(20,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BunnyBeamSprite', sprite_id=209, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='FloppingFishSprite', sprite_id=210, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='StalSprite', sprite_id=211, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='LandmineSprite', sprite_id=212, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='DiggingGameProprietorSprite', sprite_id=213, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(42,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='GanonSprite', sprite_id=214, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(33,), subgroup_1=(65,), subgroup_2=(69,), subgroup_3=(51,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='CopyOfGanon_ExceptInvincibleSprite', sprite_id=215, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='HeartSprite', sprite_id=216, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='GreenRupeeSprite', sprite_id=217, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BlueRupeeSprite', sprite_id=218, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='RedRupeeSprite', sprite_id=219, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BombRefill1Sprite', sprite_id=220, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BombRefill4Sprite', sprite_id=221, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BombRefill8Sprite', sprite_id=222, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='SmallMagicRefillSprite', sprite_id=223, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='FullMagicRefillSprite', sprite_id=224, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='ArrowRefill5Sprite', sprite_id=225, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='ArrowRefill10Sprite', sprite_id=226, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='FairySprite', sprite_id=227, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='KeySprite', sprite_id=228, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BigKeySprite', sprite_id=229, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='ShieldEaterSprite', sprite_id=230, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(27,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MushroomSprite', sprite_id=231, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='FakeMasterSwordSprite', sprite_id=232, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MagicShopDude_HisItemsIncludingTheMagicPowderSprite', sprite_id=233, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(75,), subgroup_1=(), subgroup_2=(), subgroup_3=(90,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='HeartContainerSprite', sprite_id=234, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='HeartPieceSprite', sprite_id=235, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BushesSprite', sprite_id=236, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='CaneOfSomariaPlatformSprite', sprite_id=237, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MantleSprite', sprite_id=238, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(93,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='CaneOfSomariaPlatform_Unused1Sprite', sprite_id=239, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='CaneOfSomariaPlatform_Unused2Sprite', sprite_id=240, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='CaneOfSomariaPlatform_Unused3Sprite', sprite_id=241, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MedallionTabletSprite', sprite_id=242, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(18,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OW_OL_FallingRocks', sprite_id=244, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(16,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_CanonBalls_EP4Walls', sprite_id=258, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(46,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_CanonBalls_EPEntrance', sprite_id=259, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(46,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_StalfosHeadTrap', sprite_id=261, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_BombDrop_RopeTrap', sprite_id=262, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(28, 36), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_MovingFloor', sprite_id=263, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_SlimeDropper', sprite_id=264, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(32,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_Wallmaster', sprite_id=265, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(35,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_FloorDrop_Square', sprite_id=266, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_FloorDrop_Path', sprite_id=267, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_RightEvil_PirogusuSpawner', sprite_id=272, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(34,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_LeftEvil_PirogusuSpawner', sprite_id=273, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(34,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_DownEvil_PirogusuSpawner', sprite_id=274, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(34,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_UpEvil_PirogusuSpawner', sprite_id=275, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(34,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_FlyingFloorTileTrap', sprite_id=276, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_WizzrobeSpawner', sprite_id=277, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(37, 41), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_BlackSpawn_Zoro_BombHole', sprite_id=278, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(32,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_4Skull_Trap_Pot', sprite_id=279, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_Stalfos_Spawn_Trap_EP', sprite_id=280, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_ArmosKnight_Trigger', sprite_id=281, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_BombDrop_BombTrap', sprite_id=282, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
)
|
||||
@@ -1,131 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import NamedTuple, Optional
|
||||
|
||||
|
||||
class OverworldGroupRequirementData(NamedTuple):
|
||||
group_id: Optional[int]
|
||||
subgroup_0: Optional[int]
|
||||
subgroup_1: Optional[int]
|
||||
subgroup_2: Optional[int]
|
||||
subgroup_3: Optional[int]
|
||||
areas: tuple[int, ...]
|
||||
|
||||
AREA_IDS = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207)
|
||||
DO_NOT_RANDOMIZE_AREA_IDS = frozenset((1,
|
||||
4,
|
||||
6,
|
||||
8,
|
||||
9,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
25,
|
||||
28,
|
||||
31,
|
||||
32,
|
||||
33,
|
||||
35,
|
||||
36,
|
||||
38,
|
||||
39,
|
||||
49,
|
||||
54,
|
||||
56,
|
||||
57,
|
||||
61,
|
||||
62,
|
||||
65,
|
||||
68,
|
||||
70,
|
||||
72,
|
||||
73,
|
||||
75,
|
||||
76,
|
||||
77,
|
||||
78,
|
||||
89,
|
||||
92,
|
||||
95,
|
||||
96,
|
||||
97,
|
||||
99,
|
||||
100,
|
||||
102,
|
||||
103,
|
||||
113,
|
||||
118,
|
||||
120,
|
||||
121,
|
||||
125,
|
||||
126,
|
||||
42,
|
||||
106,
|
||||
130,
|
||||
131,
|
||||
132,
|
||||
133,
|
||||
134,
|
||||
135,
|
||||
136,
|
||||
137,
|
||||
138,
|
||||
139,
|
||||
140,
|
||||
141,
|
||||
142,
|
||||
143,
|
||||
186,
|
||||
250,
|
||||
145,
|
||||
148,
|
||||
150,
|
||||
152,
|
||||
153,
|
||||
155,
|
||||
156,
|
||||
158,
|
||||
169,
|
||||
172,
|
||||
175,
|
||||
176,
|
||||
177,
|
||||
179,
|
||||
180,
|
||||
182,
|
||||
183,
|
||||
193,
|
||||
198,
|
||||
200,
|
||||
274,
|
||||
275,
|
||||
276,
|
||||
277,
|
||||
278,
|
||||
279,
|
||||
281,
|
||||
288))
|
||||
|
||||
FORCED_GROUP_REQUIREMENTS = (
|
||||
OverworldGroupRequirementData(group_id=7, subgroup_0=None, subgroup_1=None, subgroup_2=74, subgroup_3=None, areas=(2,)),
|
||||
OverworldGroupRequirementData(group_id=16, subgroup_0=None, subgroup_1=None, subgroup_2=18, subgroup_3=16, areas=(3, 147)),
|
||||
OverworldGroupRequirementData(group_id=7, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=17, areas=(10, 154)),
|
||||
OverworldGroupRequirementData(group_id=4, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=None, areas=(15, 159)),
|
||||
OverworldGroupRequirementData(group_id=3, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=14, areas=(20, 164)),
|
||||
OverworldGroupRequirementData(group_id=1, subgroup_0=None, subgroup_1=None, subgroup_2=76, subgroup_3=63, areas=(27, 171)),
|
||||
OverworldGroupRequirementData(group_id=6, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=None, areas=(34, 40, 178, 184)),
|
||||
OverworldGroupRequirementData(group_id=8, subgroup_0=None, subgroup_1=None, subgroup_2=18, subgroup_3=None, areas=(48, 192)),
|
||||
OverworldGroupRequirementData(group_id=10, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=None, areas=(58, 202)),
|
||||
OverworldGroupRequirementData(group_id=22, subgroup_0=None, subgroup_1=None, subgroup_2=24, subgroup_3=None, areas=(79, 223)),
|
||||
OverworldGroupRequirementData(group_id=21, subgroup_0=21, subgroup_1=None, subgroup_2=None, subgroup_3=21, areas=(98, 242)),
|
||||
OverworldGroupRequirementData(group_id=27, subgroup_0=None, subgroup_1=42, subgroup_2=None, subgroup_3=None, areas=(104, 248)),
|
||||
OverworldGroupRequirementData(group_id=13, subgroup_0=None, subgroup_1=None, subgroup_2=76, subgroup_3=None, areas=(22, 166)),
|
||||
OverworldGroupRequirementData(group_id=29, subgroup_0=None, subgroup_1=77, subgroup_2=None, subgroup_3=21, areas=(105, 249)),
|
||||
OverworldGroupRequirementData(group_id=15, subgroup_0=None, subgroup_1=None, subgroup_2=78, subgroup_3=None, areas=(42, 186)),
|
||||
OverworldGroupRequirementData(group_id=17, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=76, areas=(106, 250)),
|
||||
OverworldGroupRequirementData(group_id=12, subgroup_0=None, subgroup_1=None, subgroup_2=55, subgroup_3=54, areas=(128, 272)),
|
||||
OverworldGroupRequirementData(group_id=14, subgroup_0=None, subgroup_1=None, subgroup_2=12, subgroup_3=68, areas=(129, 273)),
|
||||
OverworldGroupRequirementData(group_id=26, subgroup_0=15, subgroup_1=None, subgroup_2=None, subgroup_3=None, areas=(146,)),
|
||||
OverworldGroupRequirementData(group_id=23, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=25, areas=(94, 238)),
|
||||
)
|
||||
@@ -1,107 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
class PotDataRecord(NamedTuple):
|
||||
x: int
|
||||
y: int
|
||||
reserved: int
|
||||
|
||||
|
||||
class PotRoomDataRecord(NamedTuple):
|
||||
room_id: int
|
||||
pots: tuple[PotDataRecord, ...]
|
||||
items: tuple[int, ...]
|
||||
|
||||
POT_ROOMS = (
|
||||
PotRoomDataRecord(room_id=4, pots=(PotDataRecord(x=162, y=25, reserved=0), PotDataRecord(x=152, y=25, reserved=0), PotDataRecord(x=152, y=22, reserved=0), PotDataRecord(x=162, y=22, reserved=0), PotDataRecord(x=240, y=19, reserved=0), PotDataRecord(x=204, y=19, reserved=0),), items=(10, 10)),
|
||||
PotRoomDataRecord(room_id=9, pots=(PotDataRecord(x=12, y=4, reserved=0), PotDataRecord(x=48, y=4, reserved=0), PotDataRecord(x=12, y=12, reserved=0),), items=(1, 11, 136)),
|
||||
PotRoomDataRecord(room_id=10, pots=(PotDataRecord(x=204, y=11, reserved=0), PotDataRecord(x=156, y=17, reserved=0), PotDataRecord(x=96, y=8, reserved=0), PotDataRecord(x=100, y=7, reserved=0), PotDataRecord(x=160, y=17, reserved=0), PotDataRecord(x=104, y=8, reserved=0), PotDataRecord(x=100, y=9, reserved=0),), items=(11, 11, 136)),
|
||||
PotRoomDataRecord(room_id=17, pots=(PotDataRecord(x=152, y=19, reserved=0), PotDataRecord(x=152, y=15, reserved=0), PotDataRecord(x=144, y=15, reserved=0), PotDataRecord(x=10, y=15, reserved=0), PotDataRecord(x=144, y=19, reserved=0), PotDataRecord(x=160, y=19, reserved=0),), items=(11, 11, 11, 11)),
|
||||
PotRoomDataRecord(room_id=21, pots=(PotDataRecord(x=96, y=4, reserved=0), PotDataRecord(x=100, y=4, reserved=0), PotDataRecord(x=104, y=4, reserved=0), PotDataRecord(x=108, y=4, reserved=0), PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=12, y=6, reserved=0), PotDataRecord(x=16, y=6, reserved=0), PotDataRecord(x=20, y=6, reserved=0), PotDataRecord(x=70, y=11, reserved=0),), items=(1, 7, 9, 9, 10, 11, 12, 12, 13)),
|
||||
PotRoomDataRecord(room_id=22, pots=(PotDataRecord(x=188, y=3, reserved=0), PotDataRecord(x=192, y=3, reserved=0), PotDataRecord(x=188, y=4, reserved=0), PotDataRecord(x=192, y=4, reserved=0), PotDataRecord(x=188, y=5, reserved=0), PotDataRecord(x=192, y=5, reserved=0), PotDataRecord(x=188, y=6, reserved=0), PotDataRecord(x=192, y=6, reserved=0), PotDataRecord(x=240, y=19, reserved=0),), items=(8, 9, 9, 10, 10, 11, 11, 12, 12)),
|
||||
PotRoomDataRecord(room_id=26, pots=(PotDataRecord(x=232, y=19, reserved=0), PotDataRecord(x=212, y=19, reserved=0), PotDataRecord(x=28, y=5, reserved=0), PotDataRecord(x=32, y=5, reserved=0), PotDataRecord(x=28, y=27, reserved=0), PotDataRecord(x=32, y=27, reserved=0),), items=(10, 10, 10, 10)),
|
||||
PotRoomDataRecord(room_id=33, pots=(PotDataRecord(x=100, y=28, reserved=0), PotDataRecord(x=168, y=24, reserved=0), PotDataRecord(x=48, y=28, reserved=0), PotDataRecord(x=82, y=28, reserved=0), PotDataRecord(x=160, y=20, reserved=0), PotDataRecord(x=104, y=28, reserved=0),), items=(11, 12, 12)),
|
||||
PotRoomDataRecord(room_id=35, pots=(PotDataRecord(x=86, y=26, reserved=0), PotDataRecord(x=90, y=26, reserved=0), PotDataRecord(x=94, y=26, reserved=0), PotDataRecord(x=98, y=26, reserved=0), PotDataRecord(x=102, y=26, reserved=0),), items=(1, 10, 11)),
|
||||
PotRoomDataRecord(room_id=36, pots=(PotDataRecord(x=12, y=4, reserved=0), PotDataRecord(x=48, y=4, reserved=0), PotDataRecord(x=12, y=12, reserved=0), PotDataRecord(x=48, y=12, reserved=0),), items=(1, 11, 12, 7)),
|
||||
PotRoomDataRecord(room_id=38, pots=(PotDataRecord(x=28, y=4, reserved=0), PotDataRecord(x=12, y=8, reserved=0), PotDataRecord(x=150, y=19, reserved=2), PotDataRecord(x=22, y=26, reserved=2), PotDataRecord(x=220, y=26, reserved=0),), items=(7, 9, 10, 12, 136)),
|
||||
PotRoomDataRecord(room_id=39, pots=(PotDataRecord(x=214, y=19, reserved=0), PotDataRecord(x=214, y=20, reserved=0), PotDataRecord(x=166, y=20, reserved=0), PotDataRecord(x=214, y=21, reserved=0), PotDataRecord(x=40, y=28, reserved=0), PotDataRecord(x=44, y=28, reserved=0), PotDataRecord(x=80, y=28, reserved=0), PotDataRecord(x=84, y=28, reserved=0), PotDataRecord(x=102, y=17, reserved=0), PotDataRecord(x=98, y=17, reserved=0), PotDataRecord(x=106, y=17, reserved=0), PotDataRecord(x=166, y=21, reserved=0), PotDataRecord(x=166, y=19, reserved=0), PotDataRecord(x=92, y=12, reserved=0), PotDataRecord(x=160, y=12, reserved=0),), items=(1, 1, 10, 11, 7, 7)),
|
||||
PotRoomDataRecord(room_id=43, pots=(PotDataRecord(x=16, y=5, reserved=2), PotDataRecord(x=44, y=5, reserved=2), PotDataRecord(x=16, y=6, reserved=2), PotDataRecord(x=44, y=6, reserved=2), PotDataRecord(x=16, y=7, reserved=2), PotDataRecord(x=44, y=7, reserved=2), PotDataRecord(x=146, y=21, reserved=0), PotDataRecord(x=170, y=21, reserved=0), PotDataRecord(x=146, y=22, reserved=0), PotDataRecord(x=170, y=22, reserved=0),), items=(9, 9, 10, 10, 10, 10, 11, 11, 11, 136)),
|
||||
PotRoomDataRecord(room_id=47, pots=(PotDataRecord(x=28, y=7, reserved=0), PotDataRecord(x=32, y=7, reserved=0), PotDataRecord(x=28, y=9, reserved=0), PotDataRecord(x=32, y=9, reserved=0), PotDataRecord(x=172, y=19, reserved=0), PotDataRecord(x=180, y=19, reserved=0), PotDataRecord(x=104, y=27, reserved=0), PotDataRecord(x=104, y=28, reserved=0),), items=(7, 7, 7, 7, 11, 11, 11, 11)),
|
||||
PotRoomDataRecord(room_id=53, pots=(PotDataRecord(x=60, y=6, reserved=1), PotDataRecord(x=20, y=8, reserved=0), PotDataRecord(x=24, y=8, reserved=0), PotDataRecord(x=28, y=8, reserved=0), PotDataRecord(x=32, y=8, reserved=0), PotDataRecord(x=36, y=8, reserved=0), PotDataRecord(x=48, y=20, reserved=0), PotDataRecord(x=76, y=23, reserved=1), PotDataRecord(x=88, y=23, reserved=1), PotDataRecord(x=100, y=27, reserved=1), PotDataRecord(x=242, y=28, reserved=1), PotDataRecord(x=240, y=22, reserved=1), PotDataRecord(x=76, y=28, reserved=1),), items=(7, 7, 7, 7, 7, 8, 11)),
|
||||
PotRoomDataRecord(room_id=54, pots=(PotDataRecord(x=108, y=4, reserved=0), PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=10, y=16, reserved=0), PotDataRecord(x=114, y=16, reserved=0),), items=(8, 10, 11)),
|
||||
PotRoomDataRecord(room_id=55, pots=(PotDataRecord(x=48, y=20, reserved=0), PotDataRecord(x=60, y=6, reserved=0),), items=(8,)),
|
||||
PotRoomDataRecord(room_id=56, pots=(PotDataRecord(x=164, y=12, reserved=0), PotDataRecord(x=164, y=13, reserved=0), PotDataRecord(x=164, y=18, reserved=0), PotDataRecord(x=164, y=19, reserved=0),), items=(8, 7, 10, 10)),
|
||||
PotRoomDataRecord(room_id=57, pots=(PotDataRecord(x=12, y=20, reserved=0), PotDataRecord(x=100, y=22, reserved=0), PotDataRecord(x=100, y=26, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(9, 9, 11, 12)),
|
||||
PotRoomDataRecord(room_id=60, pots=(PotDataRecord(x=24, y=8, reserved=0), PotDataRecord(x=64, y=12, reserved=0), PotDataRecord(x=20, y=14, reserved=0), PotDataRecord(x=68, y=18, reserved=0), PotDataRecord(x=96, y=19, reserved=0), PotDataRecord(x=64, y=20, reserved=0), PotDataRecord(x=64, y=26, reserved=0),), items=(1, 7, 7, 7, 7, 11, 12)),
|
||||
PotRoomDataRecord(room_id=61, pots=(PotDataRecord(x=76, y=12, reserved=0), PotDataRecord(x=112, y=12, reserved=0), PotDataRecord(x=24, y=22, reserved=0), PotDataRecord(x=40, y=22, reserved=0), PotDataRecord(x=32, y=24, reserved=0), PotDataRecord(x=20, y=26, reserved=0), PotDataRecord(x=36, y=26, reserved=0),), items=(9, 7, 10, 10, 11, 11, 13)),
|
||||
PotRoomDataRecord(room_id=62, pots=(PotDataRecord(x=96, y=6, reserved=0), PotDataRecord(x=100, y=6, reserved=0), PotDataRecord(x=88, y=10, reserved=0), PotDataRecord(x=92, y=10, reserved=0),), items=(10, 11, 12, 12)),
|
||||
PotRoomDataRecord(room_id=63, pots=(PotDataRecord(x=12, y=25, reserved=0), PotDataRecord(x=20, y=25, reserved=0), PotDataRecord(x=12, y=26, reserved=0), PotDataRecord(x=20, y=26, reserved=0), PotDataRecord(x=12, y=27, reserved=0), PotDataRecord(x=20, y=27, reserved=0), PotDataRecord(x=28, y=23, reserved=0),), items=(1, 1, 8, 10, 10, 11, 136)),
|
||||
PotRoomDataRecord(room_id=65, pots=(PotDataRecord(x=100, y=10, reserved=0), PotDataRecord(x=52, y=15, reserved=0), PotDataRecord(x=52, y=16, reserved=0), PotDataRecord(x=148, y=22, reserved=0),), items=(1, 11, 12, 12)),
|
||||
PotRoomDataRecord(room_id=67, pots=(PotDataRecord(x=112, y=28, reserved=1), PotDataRecord(x=76, y=28, reserved=1), PotDataRecord(x=76, y=20, reserved=1), PotDataRecord(x=66, y=4, reserved=0), PotDataRecord(x=78, y=4, reserved=0), PotDataRecord(x=66, y=9, reserved=0), PotDataRecord(x=78, y=9, reserved=0), PotDataRecord(x=112, y=20, reserved=1),), items=(8, 9, 11, 11, 12)),
|
||||
PotRoomDataRecord(room_id=69, pots=(PotDataRecord(x=12, y=4, reserved=0), PotDataRecord(x=108, y=11, reserved=0), PotDataRecord(x=48, y=12, reserved=0), PotDataRecord(x=220, y=16, reserved=0), PotDataRecord(x=236, y=16, reserved=0),), items=(9, 9, 11, 11, 12)),
|
||||
PotRoomDataRecord(room_id=73, pots=(PotDataRecord(x=156, y=27, reserved=0), PotDataRecord(x=172, y=24, reserved=0), PotDataRecord(x=172, y=23, reserved=0), PotDataRecord(x=144, y=20, reserved=0), PotDataRecord(x=104, y=15, reserved=0), PotDataRecord(x=104, y=16, reserved=0), PotDataRecord(x=144, y=19, reserved=0), PotDataRecord(x=172, y=20, reserved=0), PotDataRecord(x=144, y=27, reserved=0), PotDataRecord(x=172, y=28, reserved=0), PotDataRecord(x=160, y=27, reserved=0),), items=(11, 11, 12, 12, 12, 12)),
|
||||
PotRoomDataRecord(room_id=78, pots=(PotDataRecord(x=48, y=10, reserved=2), PotDataRecord(x=140, y=11, reserved=2), PotDataRecord(x=28, y=12, reserved=2), PotDataRecord(x=112, y=12, reserved=0),), items=(136, 11, 12)),
|
||||
PotRoomDataRecord(room_id=83, pots=(PotDataRecord(x=92, y=11, reserved=0), PotDataRecord(x=96, y=11, reserved=0), PotDataRecord(x=100, y=11, reserved=0), PotDataRecord(x=104, y=11, reserved=0),), items=(8, 11, 11, 12)),
|
||||
PotRoomDataRecord(room_id=84, pots=(PotDataRecord(x=186, y=25, reserved=0), PotDataRecord(x=186, y=26, reserved=0), PotDataRecord(x=186, y=27, reserved=0), PotDataRecord(x=186, y=28, reserved=0),), items=(7, 11, 11, 11)),
|
||||
PotRoomDataRecord(room_id=86, pots=(PotDataRecord(x=100, y=6, reserved=1), PotDataRecord(x=96, y=10, reserved=1), PotDataRecord(x=92, y=10, reserved=1), PotDataRecord(x=48, y=20, reserved=1), PotDataRecord(x=20, y=6, reserved=0), PotDataRecord(x=40, y=6, reserved=0), PotDataRecord(x=24, y=7, reserved=0), PotDataRecord(x=36, y=7, reserved=0), PotDataRecord(x=12, y=8, reserved=0), PotDataRecord(x=48, y=8, reserved=0), PotDataRecord(x=24, y=9, reserved=0), PotDataRecord(x=36, y=9, reserved=0), PotDataRecord(x=20, y=10, reserved=0), PotDataRecord(x=40, y=10, reserved=0), PotDataRecord(x=12, y=20, reserved=1),), items=(7, 7, 11, 11, 8, 12, 12, 12, 12, 12, 12)),
|
||||
PotRoomDataRecord(room_id=87, pots=(PotDataRecord(x=92, y=7, reserved=0), PotDataRecord(x=12, y=20, reserved=2), PotDataRecord(x=92, y=23, reserved=0), PotDataRecord(x=100, y=23, reserved=0), PotDataRecord(x=84, y=25, reserved=0), PotDataRecord(x=76, y=27, reserved=0), PotDataRecord(x=48, y=20, reserved=2), PotDataRecord(x=30, y=22, reserved=2),), items=(7, 10, 11, 12, 12, 12, 13, 136)),
|
||||
PotRoomDataRecord(room_id=88, pots=(PotDataRecord(x=96, y=9, reserved=0), PotDataRecord(x=92, y=8, reserved=0), PotDataRecord(x=108, y=8, reserved=0), PotDataRecord(x=108, y=6, reserved=0), PotDataRecord(x=104, y=5, reserved=0), PotDataRecord(x=92, y=6, reserved=0), PotDataRecord(x=12, y=12, reserved=0), PotDataRecord(x=16, y=7, reserved=0), PotDataRecord(x=96, y=5, reserved=0), PotDataRecord(x=100, y=5, reserved=0), PotDataRecord(x=12, y=7, reserved=0), PotDataRecord(x=92, y=7, reserved=0), PotDataRecord(x=108, y=7, reserved=0), PotDataRecord(x=16, y=8, reserved=0), PotDataRecord(x=100, y=9, reserved=0), PotDataRecord(x=104, y=9, reserved=0),), items=(10, 10, 11, 11, 12, 12, 12, 12)),
|
||||
PotRoomDataRecord(room_id=91, pots=(PotDataRecord(x=218, y=37, reserved=0), PotDataRecord(x=222, y=37, reserved=0), PotDataRecord(x=226, y=37, reserved=0),), items=(136,)),
|
||||
PotRoomDataRecord(room_id=92, pots=(PotDataRecord(x=228, y=25, reserved=0), PotDataRecord(x=104, y=24, reserved=0), PotDataRecord(x=228, y=22, reserved=0), PotDataRecord(x=216, y=25, reserved=0), PotDataRecord(x=84, y=24, reserved=0), PotDataRecord(x=216, y=22, reserved=0), PotDataRecord(x=94, y=22, reserved=0), PotDataRecord(x=94, y=26, reserved=0),), items=(10, 13)),
|
||||
PotRoomDataRecord(room_id=93, pots=(PotDataRecord(x=16, y=5, reserved=0), PotDataRecord(x=44, y=5, reserved=0), PotDataRecord(x=16, y=11, reserved=0), PotDataRecord(x=44, y=11, reserved=0), PotDataRecord(x=12, y=20, reserved=0), PotDataRecord(x=48, y=20, reserved=0), PotDataRecord(x=12, y=28, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(1, 7, 9, 9, 10, 10, 10, 12)),
|
||||
PotRoomDataRecord(room_id=94, pots=(PotDataRecord(x=92, y=4, reserved=0), PotDataRecord(x=96, y=4, reserved=0), PotDataRecord(x=76, y=8, reserved=0), PotDataRecord(x=112, y=8, reserved=0),), items=(11, 11, 12, 12)),
|
||||
PotRoomDataRecord(room_id=99, pots=(PotDataRecord(x=48, y=4, reserved=0), PotDataRecord(x=12, y=4, reserved=0), PotDataRecord(x=12, y=8, reserved=0), PotDataRecord(x=48, y=12, reserved=0), PotDataRecord(x=48, y=8, reserved=0), PotDataRecord(x=12, y=12, reserved=0),), items=(8, 11)),
|
||||
PotRoomDataRecord(room_id=100, pots=(PotDataRecord(x=12, y=22, reserved=0), PotDataRecord(x=16, y=22, reserved=0), PotDataRecord(x=20, y=22, reserved=0), PotDataRecord(x=36, y=28, reserved=0), PotDataRecord(x=40, y=28, reserved=0), PotDataRecord(x=44, y=28, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(10, 10, 10, 10, 12, 12, 136)),
|
||||
PotRoomDataRecord(room_id=102, pots=(PotDataRecord(x=48, y=37, reserved=0), PotDataRecord(x=52, y=37, reserved=0), PotDataRecord(x=56, y=37, reserved=0), PotDataRecord(x=84, y=5, reserved=0), PotDataRecord(x=104, y=5, reserved=0), PotDataRecord(x=48, y=38, reserved=0), PotDataRecord(x=52, y=38, reserved=0), PotDataRecord(x=56, y=38, reserved=0), PotDataRecord(x=84, y=6, reserved=0), PotDataRecord(x=104, y=6, reserved=0),), items=(7, 7, 9, 9, 9, 10, 10, 10, 11, 11)),
|
||||
PotRoomDataRecord(room_id=103, pots=(PotDataRecord(x=22, y=26, reserved=0), PotDataRecord(x=18, y=22, reserved=0), PotDataRecord(x=92, y=9, reserved=0), PotDataRecord(x=84, y=28, reserved=0), PotDataRecord(x=12, y=7, reserved=0), PotDataRecord(x=48, y=7, reserved=0), PotDataRecord(x=96, y=19, reserved=0), PotDataRecord(x=74, y=20, reserved=0), PotDataRecord(x=18, y=23, reserved=0), PotDataRecord(x=18, y=26, reserved=0), PotDataRecord(x=104, y=28, reserved=0),), items=(9, 11, 11, 11, 12, 12, 12)),
|
||||
PotRoomDataRecord(room_id=104, pots=(PotDataRecord(x=84, y=14, reserved=0), PotDataRecord(x=84, y=13, reserved=0), PotDataRecord(x=88, y=12, reserved=0), PotDataRecord(x=88, y=6, reserved=0), PotDataRecord(x=88, y=5, reserved=0), PotDataRecord(x=88, y=4, reserved=0), PotDataRecord(x=64, y=17, reserved=0), PotDataRecord(x=64, y=15, reserved=0), PotDataRecord(x=64, y=7, reserved=0), PotDataRecord(x=88, y=7, reserved=0), PotDataRecord(x=64, y=16, reserved=0), PotDataRecord(x=64, y=24, reserved=0), PotDataRecord(x=64, y=25, reserved=0),), items=(11, 11, 11, 12, 12)),
|
||||
PotRoomDataRecord(room_id=115, pots=(PotDataRecord(x=154, y=21, reserved=0), PotDataRecord(x=158, y=21, reserved=0), PotDataRecord(x=20, y=23, reserved=0), PotDataRecord(x=36, y=23, reserved=0), PotDataRecord(x=144, y=24, reserved=0), PotDataRecord(x=168, y=24, reserved=0), PotDataRecord(x=20, y=26, reserved=0), PotDataRecord(x=36, y=26, reserved=0), PotDataRecord(x=154, y=27, reserved=0), PotDataRecord(x=158, y=27, reserved=0),), items=(1, 1, 11, 11, 7, 7, 9, 9, 12, 136)),
|
||||
PotRoomDataRecord(room_id=116, pots=(PotDataRecord(x=30, y=5, reserved=0), PotDataRecord(x=62, y=5, reserved=0), PotDataRecord(x=94, y=5, reserved=0), PotDataRecord(x=14, y=11, reserved=0), PotDataRecord(x=46, y=11, reserved=0), PotDataRecord(x=78, y=11, reserved=0), PotDataRecord(x=110, y=11, reserved=0),), items=(9, 9, 11, 11, 12, 12, 136)),
|
||||
PotRoomDataRecord(room_id=117, pots=(PotDataRecord(x=148, y=22, reserved=0), PotDataRecord(x=160, y=22, reserved=0), PotDataRecord(x=172, y=22, reserved=0),), items=(9, 11, 12)),
|
||||
PotRoomDataRecord(room_id=123, pots=(PotDataRecord(x=48, y=10, reserved=0), PotDataRecord(x=88, y=10, reserved=0), PotDataRecord(x=76, y=7, reserved=0), PotDataRecord(x=60, y=4, reserved=0), PotDataRecord(x=64, y=4, reserved=0),), items=(11, 8)),
|
||||
PotRoomDataRecord(room_id=124, pots=(PotDataRecord(x=36, y=21, reserved=0), PotDataRecord(x=24, y=11, reserved=0), PotDataRecord(x=28, y=4, reserved=0), PotDataRecord(x=32, y=4, reserved=0),), items=(11, 11)),
|
||||
PotRoomDataRecord(room_id=125, pots=(PotDataRecord(x=44, y=12, reserved=0), PotDataRecord(x=44, y=6, reserved=0), PotDataRecord(x=112, y=6, reserved=0), PotDataRecord(x=108, y=20, reserved=0), PotDataRecord(x=114, y=20, reserved=0), PotDataRecord(x=76, y=28, reserved=0),), items=(9, 10, 10, 11)),
|
||||
PotRoomDataRecord(room_id=126, pots=(PotDataRecord(x=86, y=15, reserved=0), PotDataRecord(x=82, y=26, reserved=0), PotDataRecord(x=100, y=26, reserved=0), PotDataRecord(x=104, y=26, reserved=0),), items=(11, 12, 136)),
|
||||
PotRoomDataRecord(room_id=130, pots=(PotDataRecord(x=50, y=5, reserved=0), PotDataRecord(x=50, y=10, reserved=0), PotDataRecord(x=76, y=50, reserved=0),), items=(11,)),
|
||||
PotRoomDataRecord(room_id=131, pots=(PotDataRecord(x=76, y=4, reserved=0), PotDataRecord(x=80, y=4, reserved=0), PotDataRecord(x=76, y=28, reserved=0), PotDataRecord(x=80, y=28, reserved=0),), items=(1, 7, 9, 9)),
|
||||
PotRoomDataRecord(room_id=132, pots=(PotDataRecord(x=64, y=17, reserved=0), PotDataRecord(x=60, y=17, reserved=0), PotDataRecord(x=80, y=14, reserved=0), PotDataRecord(x=44, y=14, reserved=0), PotDataRecord(x=100, y=6, reserved=0), PotDataRecord(x=24, y=6, reserved=0), PotDataRecord(x=24, y=7, reserved=0), PotDataRecord(x=100, y=7, reserved=0),), items=(9, 9)),
|
||||
PotRoomDataRecord(room_id=135, pots=(PotDataRecord(x=12, y=11, reserved=0), PotDataRecord(x=76, y=20, reserved=0), PotDataRecord(x=112, y=20, reserved=0), PotDataRecord(x=16, y=12, reserved=0), PotDataRecord(x=40, y=12, reserved=0), PotDataRecord(x=32, y=12, reserved=0), PotDataRecord(x=24, y=12, reserved=0), PotDataRecord(x=16, y=11, reserved=0),), items=(12, 13)),
|
||||
PotRoomDataRecord(room_id=139, pots=(PotDataRecord(x=76, y=20, reserved=0), PotDataRecord(x=76, y=12, reserved=1), PotDataRecord(x=32, y=23, reserved=1), PotDataRecord(x=28, y=23, reserved=1), PotDataRecord(x=112, y=12, reserved=1), PotDataRecord(x=32, y=9, reserved=1), PotDataRecord(x=76, y=28, reserved=0),), items=(8, 11, 12)),
|
||||
PotRoomDataRecord(room_id=140, pots=(PotDataRecord(x=76, y=12, reserved=2), PotDataRecord(x=112, y=12, reserved=2), PotDataRecord(x=76, y=20, reserved=0), PotDataRecord(x=92, y=20, reserved=0), PotDataRecord(x=100, y=21, reserved=0), PotDataRecord(x=104, y=26, reserved=0), PotDataRecord(x=88, y=27, reserved=0),), items=(9, 10, 10, 10, 10, 12, 136)),
|
||||
PotRoomDataRecord(room_id=145, pots=(PotDataRecord(x=84, y=4, reserved=0), PotDataRecord(x=104, y=4, reserved=0),), items=(11, 12)),
|
||||
PotRoomDataRecord(room_id=150, pots=(PotDataRecord(x=14, y=18, reserved=0), PotDataRecord(x=32, y=5, reserved=0), PotDataRecord(x=32, y=17, reserved=0), PotDataRecord(x=32, y=24, reserved=0), PotDataRecord(x=76, y=21, reserved=0), PotDataRecord(x=112, y=21, reserved=0), PotDataRecord(x=14, y=24, reserved=0),), items=(11, 12, 12, 13)),
|
||||
PotRoomDataRecord(room_id=155, pots=(PotDataRecord(x=48, y=4, reserved=0), PotDataRecord(x=48, y=12, reserved=0),), items=(8, 12)),
|
||||
PotRoomDataRecord(room_id=157, pots=(PotDataRecord(x=32, y=7, reserved=0), PotDataRecord(x=40, y=9, reserved=0), PotDataRecord(x=76, y=4, reserved=0), PotDataRecord(x=84, y=4, reserved=0),), items=(10, 12)),
|
||||
PotRoomDataRecord(room_id=159, pots=(PotDataRecord(x=138, y=20, reserved=0), PotDataRecord(x=138, y=19, reserved=0), PotDataRecord(x=178, y=19, reserved=0), PotDataRecord(x=40, y=21, reserved=0), PotDataRecord(x=138, y=21, reserved=0), PotDataRecord(x=20, y=27, reserved=0), PotDataRecord(x=138, y=27, reserved=0), PotDataRecord(x=178, y=28, reserved=0), PotDataRecord(x=178, y=21, reserved=0), PotDataRecord(x=178, y=20, reserved=0), PotDataRecord(x=40, y=27, reserved=0), PotDataRecord(x=178, y=27, reserved=0), PotDataRecord(x=178, y=26, reserved=0), PotDataRecord(x=138, y=28, reserved=0), PotDataRecord(x=138, y=26, reserved=0), PotDataRecord(x=20, y=21, reserved=0),), items=(8, 11, 11, 11, 11, 11, 136)),
|
||||
PotRoomDataRecord(room_id=161, pots=(PotDataRecord(x=96, y=27, reserved=0), PotDataRecord(x=92, y=21, reserved=0), PotDataRecord(x=150, y=6, reserved=0), PotDataRecord(x=100, y=11, reserved=0), PotDataRecord(x=104, y=12, reserved=0), PotDataRecord(x=108, y=13, reserved=0), PotDataRecord(x=112, y=14, reserved=0), PotDataRecord(x=96, y=23, reserved=0), PotDataRecord(x=76, y=28, reserved=0), PotDataRecord(x=112, y=28, reserved=0),), items=(8, 11, 11, 11, 12, 12)),
|
||||
PotRoomDataRecord(room_id=168, pots=(PotDataRecord(x=138, y=28, reserved=0), PotDataRecord(x=178, y=28, reserved=0), PotDataRecord(x=178, y=19, reserved=0), PotDataRecord(x=138, y=19, reserved=0), PotDataRecord(x=30, y=24, reserved=0),), items=(1, 11)),
|
||||
PotRoomDataRecord(room_id=169, pots=(PotDataRecord(x=12, y=19, reserved=0), PotDataRecord(x=112, y=19, reserved=0), PotDataRecord(x=144, y=43, reserved=0), PotDataRecord(x=236, y=43, reserved=0), PotDataRecord(x=144, y=44, reserved=0), PotDataRecord(x=236, y=44, reserved=0), PotDataRecord(x=16, y=20, reserved=0), PotDataRecord(x=108, y=20, reserved=0),), items=(11, 11, 11, 9, 9, 9)),
|
||||
PotRoomDataRecord(room_id=170, pots=(PotDataRecord(x=212, y=10, reserved=2), PotDataRecord(x=232, y=10, reserved=2), PotDataRecord(x=232, y=5, reserved=2), PotDataRecord(x=212, y=5, reserved=2), PotDataRecord(x=94, y=8, reserved=2), PotDataRecord(x=108, y=55, reserved=0), PotDataRecord(x=108, y=56, reserved=0), PotDataRecord(x=108, y=57, reserved=0),), items=(11, 11, 11, 11, 136)),
|
||||
PotRoomDataRecord(room_id=176, pots=(PotDataRecord(x=20, y=27, reserved=0), PotDataRecord(x=24, y=24, reserved=0), PotDataRecord(x=44, y=25, reserved=0), PotDataRecord(x=20, y=21, reserved=0), PotDataRecord(x=28, y=21, reserved=0), PotDataRecord(x=32, y=21, reserved=0), PotDataRecord(x=40, y=21, reserved=0), PotDataRecord(x=16, y=23, reserved=0), PotDataRecord(x=44, y=23, reserved=0), PotDataRecord(x=36, y=24, reserved=0), PotDataRecord(x=16, y=25, reserved=0), PotDataRecord(x=28, y=27, reserved=0), PotDataRecord(x=40, y=27, reserved=0), PotDataRecord(x=32, y=27, reserved=0),), items=(1, 1, 7, 7, 9, 9, 10, 10, 11, 11)),
|
||||
PotRoomDataRecord(room_id=179, pots=(PotDataRecord(x=12, y=20, reserved=0), PotDataRecord(x=48, y=20, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(8, 12, 136)),
|
||||
PotRoomDataRecord(room_id=180, pots=(PotDataRecord(x=44, y=28, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(11, 13)),
|
||||
PotRoomDataRecord(room_id=181, pots=(PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=112, y=15, reserved=0), PotDataRecord(x=76, y=16, reserved=0), PotDataRecord(x=112, y=16, reserved=0), PotDataRecord(x=112, y=17, reserved=0), PotDataRecord(x=112, y=28, reserved=0),), items=(7, 10, 11, 11, 13, 136)),
|
||||
PotRoomDataRecord(room_id=184, pots=(PotDataRecord(x=96, y=13, reserved=0), PotDataRecord(x=88, y=16, reserved=0), PotDataRecord(x=104, y=16, reserved=0),), items=(11, 11, 136)),
|
||||
PotRoomDataRecord(room_id=185, pots=(PotDataRecord(x=92, y=18, reserved=0), PotDataRecord(x=96, y=18, reserved=0), PotDataRecord(x=104, y=18, reserved=0), PotDataRecord(x=108, y=18, reserved=0),), items=(1, 1, 7, 7)),
|
||||
PotRoomDataRecord(room_id=186, pots=(PotDataRecord(x=100, y=8, reserved=0), PotDataRecord(x=88, y=8, reserved=0), PotDataRecord(x=94, y=4, reserved=0), PotDataRecord(x=76, y=6, reserved=0), PotDataRecord(x=112, y=6, reserved=0), PotDataRecord(x=76, y=10, reserved=0), PotDataRecord(x=112, y=10, reserved=0), PotDataRecord(x=94, y=12, reserved=0),), items=(1, 1, 8, 11, 11, 12)),
|
||||
PotRoomDataRecord(room_id=188, pots=(PotDataRecord(x=138, y=3, reserved=2), PotDataRecord(x=178, y=3, reserved=2), PotDataRecord(x=86, y=4, reserved=1), PotDataRecord(x=102, y=4, reserved=1), PotDataRecord(x=138, y=12, reserved=2), PotDataRecord(x=178, y=12, reserved=2), PotDataRecord(x=48, y=20, reserved=0), PotDataRecord(x=28, y=21, reserved=0), PotDataRecord(x=32, y=21, reserved=0), PotDataRecord(x=28, y=27, reserved=0), PotDataRecord(x=32, y=27, reserved=0), PotDataRecord(x=12, y=28, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(7, 7, 7, 7, 8, 10, 10, 10, 10, 10, 11, 11, 136)),
|
||||
PotRoomDataRecord(room_id=191, pots=(PotDataRecord(x=40, y=20, reserved=0), PotDataRecord(x=44, y=20, reserved=0), PotDataRecord(x=48, y=20, reserved=0), PotDataRecord(x=40, y=28, reserved=0), PotDataRecord(x=44, y=28, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(9, 10, 11, 12, 12, 12)),
|
||||
PotRoomDataRecord(room_id=192, pots=(PotDataRecord(x=48, y=10, reserved=0), PotDataRecord(x=12, y=14, reserved=0), PotDataRecord(x=12, y=26, reserved=0), PotDataRecord(x=28, y=27, reserved=0),), items=(1, 7, 10, 11)),
|
||||
PotRoomDataRecord(room_id=194, pots=(PotDataRecord(x=180, y=7, reserved=0), PotDataRecord(x=100, y=46, reserved=0), PotDataRecord(x=68, y=48, reserved=0), PotDataRecord(x=64, y=52, reserved=0),), items=(1, 9, 12, 136)),
|
||||
PotRoomDataRecord(room_id=196, pots=(PotDataRecord(x=84, y=9, reserved=0), PotDataRecord(x=24, y=14, reserved=0), PotDataRecord(x=56, y=17, reserved=0), PotDataRecord(x=84, y=17, reserved=0), PotDataRecord(x=12, y=21, reserved=0), PotDataRecord(x=76, y=23, reserved=0), PotDataRecord(x=48, y=25, reserved=0), PotDataRecord(x=12, y=26, reserved=0),), items=(1, 9, 12, 7, 10, 10, 11, 11)),
|
||||
PotRoomDataRecord(room_id=199, pots=(PotDataRecord(x=12, y=10, reserved=0), PotDataRecord(x=12, y=11, reserved=0), PotDataRecord(x=12, y=22, reserved=0), PotDataRecord(x=12, y=28, reserved=0),), items=(9, 12, 11, 13)),
|
||||
PotRoomDataRecord(room_id=201, pots=(PotDataRecord(x=30, y=22, reserved=0), PotDataRecord(x=94, y=22, reserved=0), PotDataRecord(x=60, y=22, reserved=0),), items=(1, 1, 136)),
|
||||
PotRoomDataRecord(room_id=203, pots=(PotDataRecord(x=88, y=16, reserved=0), PotDataRecord(x=88, y=28, reserved=0),), items=(7, 11)),
|
||||
PotRoomDataRecord(room_id=204, pots=(PotDataRecord(x=36, y=4, reserved=0), PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=36, y=28, reserved=0), PotDataRecord(x=112, y=28, reserved=0),), items=(7, 11, 7, 10)),
|
||||
PotRoomDataRecord(room_id=206, pots=(PotDataRecord(x=76, y=8, reserved=0), PotDataRecord(x=80, y=8, reserved=0), PotDataRecord(x=108, y=12, reserved=0), PotDataRecord(x=112, y=12, reserved=0), PotDataRecord(x=204, y=11, reserved=3),), items=(9, 12, 12, 10, 128)),
|
||||
PotRoomDataRecord(room_id=208, pots=(PotDataRecord(x=158, y=5, reserved=0), PotDataRecord(x=140, y=11, reserved=0), PotDataRecord(x=42, y=13, reserved=0), PotDataRecord(x=48, y=16, reserved=0), PotDataRecord(x=176, y=20, reserved=0), PotDataRecord(x=146, y=23, reserved=0), PotDataRecord(x=12, y=28, reserved=0),), items=(1, 1, 7, 11, 11, 12, 12)),
|
||||
PotRoomDataRecord(room_id=209, pots=(PotDataRecord(x=76, y=12, reserved=0), PotDataRecord(x=48, y=4, reserved=0), PotDataRecord(x=76, y=4, reserved=0), PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=168, y=7, reserved=0), PotDataRecord(x=112, y=12, reserved=0),), items=(9, 1, 1, 1, 13)),
|
||||
PotRoomDataRecord(room_id=214, pots=(PotDataRecord(x=92, y=22, reserved=0), PotDataRecord(x=96, y=22, reserved=0),), items=(10, 13)),
|
||||
PotRoomDataRecord(room_id=216, pots=(PotDataRecord(x=202, y=8, reserved=0), PotDataRecord(x=242, y=8, reserved=0), PotDataRecord(x=202, y=10, reserved=0), PotDataRecord(x=242, y=10, reserved=0), PotDataRecord(x=202, y=12, reserved=0), PotDataRecord(x=242, y=12, reserved=0), PotDataRecord(x=92, y=24, reserved=0), PotDataRecord(x=96, y=24, reserved=0),), items=(9, 9, 9, 9, 9, 11, 11, 11)),
|
||||
PotRoomDataRecord(room_id=218, pots=(PotDataRecord(x=24, y=23, reserved=0), PotDataRecord(x=36, y=23, reserved=0), PotDataRecord(x=24, y=25, reserved=0), PotDataRecord(x=36, y=25, reserved=0),), items=(9, 9, 11, 136)),
|
||||
PotRoomDataRecord(room_id=219, pots=(PotDataRecord(x=12, y=4, reserved=0), PotDataRecord(x=62, y=19, reserved=0), PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=88, y=16, reserved=0),), items=(7, 11)),
|
||||
PotRoomDataRecord(room_id=220, pots=(PotDataRecord(x=56, y=4, reserved=0), PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=68, y=16, reserved=0), PotDataRecord(x=12, y=28, reserved=0),), items=(7, 9, 10, 11)),
|
||||
PotRoomDataRecord(room_id=235, pots=(PotDataRecord(x=206, y=8, reserved=0), PotDataRecord(x=210, y=8, reserved=0), PotDataRecord(x=88, y=14, reserved=0), PotDataRecord(x=92, y=14, reserved=0), PotDataRecord(x=96, y=14, reserved=0),), items=(7, 7, 11, 12, 12)),
|
||||
)
|
||||
@@ -1,171 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
ENEMIZER_SYMBOLS = {
|
||||
':pos_1_0': 0x36975E,
|
||||
':pos_1_1': 0x36976D,
|
||||
':pos_1_10': 0x369819,
|
||||
':pos_1_11': 0x369828,
|
||||
':pos_1_12': 0x369837,
|
||||
':pos_1_13': 0x369845,
|
||||
':pos_1_14': 0x36984B,
|
||||
':pos_1_15': 0x369851,
|
||||
':pos_1_16': 0x369873,
|
||||
':pos_1_17': 0x369898,
|
||||
':pos_1_18': 0x36989E,
|
||||
':pos_1_19': 0x3698A4,
|
||||
':pos_1_2': 0x369787,
|
||||
':pos_1_20': 0x3698C6,
|
||||
':pos_1_21': 0x3698EB,
|
||||
':pos_1_22': 0x3698F1,
|
||||
':pos_1_23': 0x3698F7,
|
||||
':pos_1_24': 0x369919,
|
||||
':pos_1_25': 0x36993E,
|
||||
':pos_1_26': 0x369944,
|
||||
':pos_1_27': 0x36994A,
|
||||
':pos_1_28': 0x36996C,
|
||||
':pos_1_29': 0x369AA5,
|
||||
':pos_1_3': 0x369796,
|
||||
':pos_1_30': 0x36B796,
|
||||
':pos_1_4': 0x3697A5,
|
||||
':pos_1_5': 0x3697B4,
|
||||
':pos_1_6': 0x3697D5,
|
||||
':pos_1_7': 0x3697E8,
|
||||
':pos_1_8': 0x3697F7,
|
||||
':pos_1_9': 0x369806,
|
||||
'CheckIfLinkShouldDie': 0x3699B2,
|
||||
'CheckIfLinkShouldDie_dead': 0x3699BB,
|
||||
'CheckIfLinkShouldDie_done': 0x3699BD,
|
||||
'Check_for_Blind_Fight': 0x1DA085,
|
||||
'CopyShield': 0x36B713,
|
||||
'CopyShield_loop_copy': 0x36B729,
|
||||
'CopyShield_shield_positon_gfx': 0x36B74B,
|
||||
'CopySword': 0x36B6D1,
|
||||
'CopySword_loop_copy': 0x36B6E7,
|
||||
'CopySword_sword_positon_gfx': 0x36B709,
|
||||
'DMAKholdstare': 0x3695A5,
|
||||
'DMATrinexx': 0x369617,
|
||||
'Dungeon_ResetSprites': 0x09C114,
|
||||
'EnemizerCodeStart': 0x3694F5,
|
||||
'EnemizerFlags': 0x368100,
|
||||
'EnemizerFlags_agahnim_fun_balls': 0x368104,
|
||||
'EnemizerFlags_close_blind_door': 0x368101,
|
||||
'EnemizerFlags_enable_mimic_override': 0x368105,
|
||||
'EnemizerFlags_enable_terrorpin_ai_fix': 0x368106,
|
||||
'EnemizerFlags_moldorm_eye_count': 0x368102,
|
||||
'EnemizerFlags_randomize_bushes': 0x368100,
|
||||
'EnemizerFlags_randomize_sprites': 0x368103,
|
||||
'EnemizerTablesStart': 0x368000,
|
||||
'Ext_OnBossDeath': 0x29803C,
|
||||
'Ext_OnDungeonCompleted': 0x29804B,
|
||||
'Ext_OnDungeonEnter': 0x298041,
|
||||
'Ext_OnDungeonExit': 0x298046,
|
||||
'Ext_OnFairyRevive': 0x298019,
|
||||
'Ext_OnFileCreate': 0x298000,
|
||||
'Ext_OnFileLoad': 0x298005,
|
||||
'Ext_OnFileSave': 0x29800A,
|
||||
'Ext_OnIemMenuOpen': 0x298023,
|
||||
'Ext_OnItemChange': 0x29802D,
|
||||
'Ext_OnItemMenuClose': 0x298028,
|
||||
'Ext_OnMapUse': 0x298014,
|
||||
'Ext_OnPlayerAttack': 0x298037,
|
||||
'Ext_OnPlayerDamaged': 0x298032,
|
||||
'Ext_OnPlayerDeath': 0x29800F,
|
||||
'Ext_OnYItemUse': 0x29801E,
|
||||
'Ext_OnZeldaRescued': 0x298050,
|
||||
'FixTerrorpin': 0x36971A,
|
||||
'FixTerrorpin_new': 0x369728,
|
||||
'GFX_Kholdstare_Shell': 0x36C79A,
|
||||
'GFX_Trinexx_Shell': 0x36D79A,
|
||||
'GFX_Trinexx_Shell2': 0x36DF9A,
|
||||
'GetRandomInt': 0x0DBA71,
|
||||
'Initialize_Blind_Fight': 0x1DA090,
|
||||
'Kholdstare_Draw': 0x0DD97F,
|
||||
'LoadFile': 0x369A87,
|
||||
'LoadNewSoundFx': 0x369A9E,
|
||||
'LoadOverworldSprites': 0x36B753,
|
||||
'Module_LoadFile_indoors': 0x028118,
|
||||
'Moldorm_UpdateOamPosition': 0x3699E7,
|
||||
'Moldorm_UpdateOamPosition_more_eyes': 0x3699ED,
|
||||
'NMIHookAction': 0x36956C,
|
||||
'NMIHookAction_loadKholdstare': 0x369584,
|
||||
'NMIHookAction_loadTrinexx': 0x369590,
|
||||
'NMIHookAction_return': 0x36959A,
|
||||
'NMIHookReturn': 0x0080D5,
|
||||
'NewLoadSoundBank': 0x369A98,
|
||||
'NewLoadSoundBank_Intro': 0x369A92,
|
||||
'OnInitFileSelect': 0x3696FA,
|
||||
'OnInitFileSelect_continue': 0x36970F,
|
||||
'Player_Main': 0x078000,
|
||||
'Sound_LoadSongBank': 0x008888,
|
||||
'Sound_SetSfx3PanLong': 0x0DBB8A,
|
||||
'Sound_SetSfxPanWithPlayerCoords': 0x0DBB67,
|
||||
'Spawn_Bees': 0x36B75D,
|
||||
'Spawn_Bees_done': 0x36B779,
|
||||
'SpritePrep_Eyegore': 0x1EC6FA,
|
||||
'SpritePrep_EyegoreNew': 0x369A1A,
|
||||
'SpritePrep_EyegoreNew_mimic': 0x369A31,
|
||||
'SpritePrep_EyegoreNew_new': 0x369A25,
|
||||
'Sprite_ResetAll': 0x09C44E,
|
||||
'Sprite_SpawnDynamically': 0x1DF65D,
|
||||
'VitreousKeyReset': 0x36B77A,
|
||||
'boss_move': 0x369743,
|
||||
'boss_move_loop_bottom_left': 0x369935,
|
||||
'boss_move_loop_bottom_left2': 0x369963,
|
||||
'boss_move_loop_bottom_right': 0x3698E2,
|
||||
'boss_move_loop_bottom_right2': 0x369910,
|
||||
'boss_move_loop_middle': 0x36983C,
|
||||
'boss_move_loop_middle2': 0x36986A,
|
||||
'boss_move_loop_top_right': 0x36988F,
|
||||
'boss_move_loop_top_right2': 0x3698BD,
|
||||
'boss_move_move_to_bottom_left': 0x369933,
|
||||
'boss_move_move_to_bottom_right': 0x3698E0,
|
||||
'boss_move_move_to_middle': 0x36983A,
|
||||
'boss_move_move_to_top_right': 0x36988D,
|
||||
'boss_move_no_blind_door': 0x3697D2,
|
||||
'boss_move_no_change': 0x369863,
|
||||
'boss_move_no_change2': 0x3698B6,
|
||||
'boss_move_no_change3': 0x369909,
|
||||
'boss_move_no_change4': 0x36995C,
|
||||
'boss_move_no_change_ov': 0x369885,
|
||||
'boss_move_no_change_ov2': 0x3698D8,
|
||||
'boss_move_no_change_ov3': 0x36992B,
|
||||
'boss_move_no_change_ov4': 0x36997E,
|
||||
'boss_move_return': 0x369986,
|
||||
'change_heartcontainer_position': 0x3699BE,
|
||||
'change_heartcontainer_position_not_moldorm_room': 0x3699E1,
|
||||
'check_blind_boss_room': 0x36B782,
|
||||
'check_special_action': 0x369731,
|
||||
'check_special_action_no_special_action': 0x36973E,
|
||||
'enemizer_info_table': 0x368000,
|
||||
'linkIsDead': 0x0780D5,
|
||||
'linkNotDead': 0x0780F7,
|
||||
'modified_room_object_table': 0x36B79A,
|
||||
'moved_room_header_bank_value_address': 0x368374,
|
||||
'newKodongoCollision': 0x369A02,
|
||||
'newKodongoCollision_continue': 0x369A19,
|
||||
'new_kholdstare_code': 0x369987,
|
||||
'new_kholdstare_code_already_iced': 0x369997,
|
||||
'new_trinexx_code': 0x36999C,
|
||||
'new_trinexx_code_already_rocked': 0x3699AC,
|
||||
'notItemSprite_Mimic': 0x369A66,
|
||||
'notItemSprite_Mimic_changeSpriteId': 0x369A7A,
|
||||
'notItemSprite_Mimic_continue': 0x369A82,
|
||||
'notItemSprite_Mimic_reloadSpriteIdAndSkipMimic': 0x369A7F,
|
||||
'resetSprite_Mimic': 0x369A4E,
|
||||
'resetSprite_Mimic_notMimic': 0x369A60,
|
||||
'room_header_table': 0x368375,
|
||||
'shieldgfx': 0x36AAD1,
|
||||
'sprite_bush_spawn': 0x3694F5,
|
||||
'sprite_bush_spawn_continue': 0x36950F,
|
||||
'sprite_bush_spawn_dontGoPhase2': 0x369565,
|
||||
'sprite_bush_spawn_item_table': 0x369525,
|
||||
'sprite_bush_spawn_newSpriteSpawn': 0x36954C,
|
||||
'sprite_bush_spawn_not_random': 0x36953B,
|
||||
'sprite_bush_spawn_not_random_old': 0x36950B,
|
||||
'sprite_bush_spawn_return': 0x369568,
|
||||
'sprite_bush_spawn_table': 0x368120,
|
||||
'sprite_bush_spawn_table_dungeons': 0x368248,
|
||||
'sprite_bush_spawn_table_overworld': 0x368120,
|
||||
'sprite_bush_spawn_table_random_sprites': 0x368370,
|
||||
'swordgfx': 0x369AD1,
|
||||
}
|
||||
@@ -1,308 +0,0 @@
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
|
||||
from worlds.alttp.EnemizerPatches import (
|
||||
ARROW_REFILL_5_SPRITE_ID,
|
||||
BOSS_GFX_SHEET_INDEXES,
|
||||
BOSS_PATCH_DATA,
|
||||
DAMAGE_GROUP_TABLE_ADDRESS,
|
||||
DUNGEON_BOSS_PATCH_DATA,
|
||||
ENEMY_DAMAGE_TABLE_ADDRESS,
|
||||
ENEMY_HP_TABLE_ADDRESS,
|
||||
EXCLUDED_ENEMY_TABLE_SPRITE_IDS,
|
||||
HIDDEN_ENEMY_CHANCE_POOL_ADDRESS,
|
||||
RANDOMIZED_HIDDEN_ENEMY_CHANCE_POOL,
|
||||
RETRO_ARROW_REPLACEMENT_CHECK_ADDRESS,
|
||||
RETRO_RUPEE_REPLACEMENT_SPRITE_ID,
|
||||
THIEF_DEFAULT_HP,
|
||||
THIEF_SPRITE_ID,
|
||||
TILE_TRAP_FLOOR_TILE_ADDRESS,
|
||||
TRINEXX_ICE_FLOOR_ROUTINE_ADDRESS,
|
||||
TRINEXX_ICE_PROJECTILE_TILE_ADDRESS,
|
||||
VANILLA_HIDDEN_ENEMY_CHANCE_POOL,
|
||||
_apply_killable_thief,
|
||||
_apply_randomized_tile_trap_floor_tile,
|
||||
_get_enemizer_symbol,
|
||||
_make_native_enemizer_rng,
|
||||
_option_key,
|
||||
patch_bosses,
|
||||
_randomize_enemy_damage,
|
||||
_randomize_enemy_health,
|
||||
_set_enemizer_flag,
|
||||
_shuffle_damage_groups,
|
||||
_update_hidden_enemy_item_table_for_retro_mode,
|
||||
apply_enemizer_base_patch,
|
||||
)
|
||||
|
||||
|
||||
class FakeRom:
|
||||
def __init__(self, size: int = 0x400000) -> None:
|
||||
self.buffer = bytearray(size)
|
||||
|
||||
def read_byte(self, address: int) -> int:
|
||||
return self.buffer[address]
|
||||
|
||||
def read_bytes(self, startaddress: int, length: int) -> bytearray:
|
||||
return self.buffer[startaddress:startaddress + length]
|
||||
|
||||
def write_byte(self, address: int, value: int) -> None:
|
||||
self.buffer[address] = value
|
||||
|
||||
def write_bytes(self, startaddress: int, values) -> None:
|
||||
self.buffer[startaddress:startaddress + len(values)] = values
|
||||
|
||||
def write_int16(self, address: int, value: int) -> None:
|
||||
self.write_bytes(address, (value & 0xFF, (value >> 8) & 0xFF))
|
||||
|
||||
|
||||
class TestEnemizerPatches(unittest.TestCase):
|
||||
def test_enemizer_base_patch_applies_mimic_hooks(self) -> None:
|
||||
rom = FakeRom()
|
||||
|
||||
apply_enemizer_base_patch(rom)
|
||||
|
||||
self.assertEqual(tuple(rom.read_bytes(0x307CB, 2)), (0xB6, 0x91))
|
||||
self.assertEqual(tuple(rom.read_bytes(0x311B6, 4)), (0x22, 0x1A, 0x9A, 0x36))
|
||||
self.assertEqual(tuple(rom.read_bytes(0x36C08, 5)), (0x22, 0x4E, 0x9A, 0x36, 0xEA))
|
||||
self.assertEqual(tuple(rom.read_bytes(0x36DA6, 4)), (0x22, 0x66, 0x9A, 0x36))
|
||||
self.assertEqual(tuple(rom.read_bytes(0xF0BB1, 2)), (0x95, 0xC7))
|
||||
self.assertEqual(tuple(rom.read_bytes(TRINEXX_ICE_FLOOR_ROUTINE_ADDRESS, 4)), (0xEA, 0xEA, 0xEA, 0xEA))
|
||||
self.assertEqual(tuple(rom.read_bytes(TRINEXX_ICE_PROJECTILE_TILE_ADDRESS, 2)), (0x00, 0x00))
|
||||
self.assertEqual(rom.read_byte(TILE_TRAP_FLOOR_TILE_ADDRESS), 0x00)
|
||||
|
||||
def test_randomized_tile_trap_floor_tile_patch_is_separate(self) -> None:
|
||||
rom = FakeRom()
|
||||
|
||||
_apply_randomized_tile_trap_floor_tile(rom)
|
||||
|
||||
self.assertEqual(tuple(rom.read_bytes(TRINEXX_ICE_PROJECTILE_TILE_ADDRESS, 2)), (0x88, 0x01))
|
||||
self.assertEqual(rom.read_byte(TILE_TRAP_FLOOR_TILE_ADDRESS), 0x12)
|
||||
|
||||
def test_enemy_shuffle_enables_hidden_enemy_and_mimic_support(self) -> None:
|
||||
rom = FakeRom()
|
||||
world = self._build_world(enemy_shuffle=True, bush_shuffle=False)
|
||||
|
||||
self._apply_native_enemizer_features(world, rom)
|
||||
|
||||
self.assertEqual(
|
||||
tuple(rom.read_bytes(HIDDEN_ENEMY_CHANCE_POOL_ADDRESS, len(VANILLA_HIDDEN_ENEMY_CHANCE_POOL))),
|
||||
VANILLA_HIDDEN_ENEMY_CHANCE_POOL,
|
||||
)
|
||||
self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_randomize_bushes")), 0x01)
|
||||
self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_randomize_sprites")), 0x01)
|
||||
self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_enable_mimic_override")), 0x01)
|
||||
self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_enable_terrorpin_ai_fix")), 0x01)
|
||||
self.assertEqual(tuple(rom.read_bytes(0x1F2D5, 2)), (0x54, 0x9C))
|
||||
self.assertEqual(rom.read_byte(0x1F2E5), 0xB0)
|
||||
self.assertEqual(rom.read_byte(0x1F2EB), 0xD0)
|
||||
|
||||
def test_bush_shuffle_and_remaining_tables_are_patched_natively(self) -> None:
|
||||
rom = FakeRom()
|
||||
item_table_address = _get_enemizer_symbol("sprite_bush_spawn_item_table")
|
||||
not_item_sprite_address = _get_enemizer_symbol("notItemSprite_Mimic")
|
||||
rom.write_byte(RETRO_ARROW_REPLACEMENT_CHECK_ADDRESS, RETRO_RUPEE_REPLACEMENT_SPRITE_ID)
|
||||
rom.write_byte(item_table_address + 5, ARROW_REFILL_5_SPRITE_ID)
|
||||
rom.write_byte(ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID, 0x08)
|
||||
|
||||
included_hp_sprite_id = 0x01
|
||||
included_damage_sprite_id = 0x02
|
||||
excluded_sprite_id = min(EXCLUDED_ENEMY_TABLE_SPRITE_IDS)
|
||||
rom.write_byte(ENEMY_HP_TABLE_ADDRESS + included_hp_sprite_id, 0x06)
|
||||
rom.write_byte(ENEMY_HP_TABLE_ADDRESS + excluded_sprite_id, 0x07)
|
||||
rom.write_byte(ENEMY_DAMAGE_TABLE_ADDRESS + included_damage_sprite_id, 0x06)
|
||||
rom.write_byte(ENEMY_DAMAGE_TABLE_ADDRESS + excluded_sprite_id, 0x05)
|
||||
|
||||
world = self._build_world(
|
||||
bush_shuffle=True,
|
||||
killable_thieves=True,
|
||||
enemy_health="hard",
|
||||
enemy_damage="chaos",
|
||||
)
|
||||
|
||||
self._apply_native_enemizer_features(world, rom)
|
||||
|
||||
self.assertEqual(
|
||||
tuple(rom.read_bytes(HIDDEN_ENEMY_CHANCE_POOL_ADDRESS, len(RANDOMIZED_HIDDEN_ENEMY_CHANCE_POOL))),
|
||||
RANDOMIZED_HIDDEN_ENEMY_CHANCE_POOL,
|
||||
)
|
||||
self.assertEqual(rom.read_byte(item_table_address + 5), RETRO_RUPEE_REPLACEMENT_SPRITE_ID)
|
||||
self.assertEqual(rom.read_byte(not_item_sprite_address + 4), THIEF_SPRITE_ID)
|
||||
self.assertNotEqual(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID), 0x08)
|
||||
self.assertGreaterEqual(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID), 2)
|
||||
self.assertLess(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID), 25)
|
||||
self.assertGreaterEqual(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + included_hp_sprite_id), 2)
|
||||
self.assertLess(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + included_hp_sprite_id), 25)
|
||||
self.assertEqual(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + excluded_sprite_id), 0x07)
|
||||
self.assertIn(rom.read_byte(ENEMY_DAMAGE_TABLE_ADDRESS + included_damage_sprite_id), range(8))
|
||||
self.assertEqual(rom.read_byte(ENEMY_DAMAGE_TABLE_ADDRESS + excluded_sprite_id), 0x05)
|
||||
for group_id in range(10):
|
||||
group_address = DAMAGE_GROUP_TABLE_ADDRESS + (group_id * 3)
|
||||
green_mail, blue_mail, red_mail = rom.read_bytes(group_address, 3)
|
||||
self.assertIn(green_mail, range(64))
|
||||
self.assertIn(blue_mail, range(64))
|
||||
self.assertIn(red_mail, range(64))
|
||||
|
||||
def test_killable_thief_sets_default_hp_without_enemy_health_shuffle(self) -> None:
|
||||
rom = FakeRom()
|
||||
rom.write_byte(ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID, 0x08)
|
||||
|
||||
world = self._build_world(killable_thieves=True)
|
||||
|
||||
self._apply_native_enemizer_features(world, rom)
|
||||
|
||||
self.assertEqual(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID), THIEF_DEFAULT_HP)
|
||||
|
||||
def test_bush_shuffle_without_enemy_shuffle_does_not_enable_sprite_randomization_flags(self) -> None:
|
||||
rom = FakeRom()
|
||||
|
||||
self._apply_native_enemizer_features(self._build_world(bush_shuffle=True), rom)
|
||||
|
||||
self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_randomize_bushes")), 0x01)
|
||||
self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_randomize_sprites")), 0x00)
|
||||
self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_enable_mimic_override")), 0x00)
|
||||
self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_enable_terrorpin_ai_fix")), 0x00)
|
||||
self.assertEqual(tuple(rom.read_bytes(0x1F2D5, 2)), (0x00, 0x00))
|
||||
self.assertEqual(rom.read_byte(0x1F2E5), 0x00)
|
||||
self.assertEqual(rom.read_byte(0x1F2EB), 0x00)
|
||||
|
||||
def test_non_chaos_enemy_damage_uses_expected_mail_scaling(self) -> None:
|
||||
rom = FakeRom()
|
||||
|
||||
self._apply_native_enemizer_features(self._build_world(enemy_damage="hard"), rom)
|
||||
|
||||
for group_id in range(10):
|
||||
group_address = DAMAGE_GROUP_TABLE_ADDRESS + (group_id * 3)
|
||||
green_mail, blue_mail, red_mail = rom.read_bytes(group_address, 3)
|
||||
self.assertEqual(blue_mail, green_mail * 3 // 4)
|
||||
self.assertEqual(red_mail, green_mail * 3 // 8)
|
||||
|
||||
def test_patch_bosses_overwrites_enemy_shuffle_boss_room_graphics(self) -> None:
|
||||
rom = FakeRom()
|
||||
dungeon_header_base = _get_enemizer_symbol("room_header_table")
|
||||
eastern_dungeon_data = DUNGEON_BOSS_PATCH_DATA[("Eastern Palace", None)]
|
||||
rom.write_byte(dungeon_header_base + (eastern_dungeon_data.room_id * 14) + 3, BOSS_PATCH_DATA["Armos"].graphics)
|
||||
|
||||
for table_index in BOSS_GFX_SHEET_INDEXES.values():
|
||||
rom.write_byte(0x4FC0 + table_index, 0xAA)
|
||||
rom.write_byte(0x509F + table_index, 0xBB)
|
||||
rom.write_byte(0x517E + table_index, 0xCC)
|
||||
|
||||
patch_bosses(self._build_boss_world({"Eastern Palace": "Vitreous"}), rom)
|
||||
|
||||
eastern_boss_data = BOSS_PATCH_DATA["Vitreous"]
|
||||
self.assertEqual(
|
||||
tuple(rom.read_bytes(eastern_dungeon_data.sprite_pointer_address, 2)),
|
||||
eastern_boss_data.pointer,
|
||||
)
|
||||
self.assertEqual(
|
||||
rom.read_byte(dungeon_header_base + (eastern_dungeon_data.room_id * 14) + 3),
|
||||
eastern_boss_data.graphics,
|
||||
)
|
||||
|
||||
for table_index in BOSS_GFX_SHEET_INDEXES.values():
|
||||
self.assertEqual(rom.read_byte(0x4FC0 + table_index), 0xAA)
|
||||
self.assertEqual(rom.read_byte(0x509F + table_index), 0xBB)
|
||||
self.assertEqual(rom.read_byte(0x517E + table_index), 0xCC)
|
||||
|
||||
def test_native_enemizer_rng_is_deterministic_for_same_world_settings(self) -> None:
|
||||
world = self._build_world(enemy_health="hard", enemy_damage="chaos", bush_shuffle=True)
|
||||
|
||||
rng_a = _make_native_enemizer_rng(world)
|
||||
rng_b = _make_native_enemizer_rng(world)
|
||||
|
||||
self.assertEqual([rng_a.randrange(256) for _ in range(8)], [rng_b.randrange(256) for _ in range(8)])
|
||||
|
||||
@staticmethod
|
||||
def _apply_native_enemizer_features(world: SimpleNamespace, rom: FakeRom) -> None:
|
||||
enemy_shuffle_enabled = bool(world.options.enemy_shuffle)
|
||||
bush_shuffle_enabled = bool(world.options.bush_shuffle)
|
||||
enemy_health_key = _option_key(world.options.enemy_health)
|
||||
enemy_damage_key = _option_key(world.options.enemy_damage)
|
||||
|
||||
if enemy_shuffle_enabled or bush_shuffle_enabled:
|
||||
_set_enemizer_flag(rom, "EnemizerFlags_randomize_bushes", True)
|
||||
hidden_enemy_chance_pool = (
|
||||
RANDOMIZED_HIDDEN_ENEMY_CHANCE_POOL if bush_shuffle_enabled else VANILLA_HIDDEN_ENEMY_CHANCE_POOL
|
||||
)
|
||||
rom.write_bytes(HIDDEN_ENEMY_CHANCE_POOL_ADDRESS, hidden_enemy_chance_pool)
|
||||
_update_hidden_enemy_item_table_for_retro_mode(rom)
|
||||
|
||||
if enemy_shuffle_enabled:
|
||||
_set_enemizer_flag(rom, "EnemizerFlags_randomize_sprites", True)
|
||||
_set_enemizer_flag(rom, "EnemizerFlags_enable_mimic_override", True)
|
||||
_set_enemizer_flag(rom, "EnemizerFlags_enable_terrorpin_ai_fix", True)
|
||||
rom.write_bytes(0x1F2D5, (0x54, 0x9C))
|
||||
rom.write_byte(0x1F2E5, 0xB0)
|
||||
rom.write_byte(0x1F2EB, 0xD0)
|
||||
|
||||
if world.options.killable_thieves:
|
||||
_apply_killable_thief(rom)
|
||||
|
||||
if enemy_health_key != "default" or enemy_damage_key != "default":
|
||||
rng = _make_native_enemizer_rng(world)
|
||||
else:
|
||||
rng = None
|
||||
|
||||
if enemy_health_key != "default":
|
||||
assert rng is not None
|
||||
_randomize_enemy_health(rom, rng, enemy_health_key)
|
||||
|
||||
if enemy_damage_key != "default":
|
||||
assert rng is not None
|
||||
_randomize_enemy_damage(rom, rng, allow_zero_damage=True)
|
||||
_shuffle_damage_groups(rom, rng, chaos_mode=enemy_damage_key == "chaos", allow_zero_damage=True)
|
||||
|
||||
@staticmethod
|
||||
def _build_world(
|
||||
*,
|
||||
enemy_shuffle: bool = False,
|
||||
bush_shuffle: bool = False,
|
||||
killable_thieves: bool = False,
|
||||
enemy_health: str = "default",
|
||||
enemy_damage: str = "default",
|
||||
) -> SimpleNamespace:
|
||||
return SimpleNamespace(
|
||||
player=1,
|
||||
multiworld=SimpleNamespace(seed=12345, seed_name="native-enemizer-test"),
|
||||
options=SimpleNamespace(
|
||||
enemy_shuffle=enemy_shuffle,
|
||||
bush_shuffle=bush_shuffle,
|
||||
killable_thieves=killable_thieves,
|
||||
enemy_health=SimpleNamespace(current_key=enemy_health),
|
||||
enemy_damage=SimpleNamespace(current_key=enemy_damage),
|
||||
),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _build_boss_world(boss_overrides: dict[str, str] | None = None) -> SimpleNamespace:
|
||||
boss_overrides = boss_overrides or {}
|
||||
|
||||
def boss(name: str) -> SimpleNamespace:
|
||||
return SimpleNamespace(enemizer_name=name)
|
||||
|
||||
return SimpleNamespace(
|
||||
options=SimpleNamespace(mode="open"),
|
||||
dungeons={
|
||||
"Eastern Palace": SimpleNamespace(boss=boss(boss_overrides.get("Eastern Palace", "Armos"))),
|
||||
"Desert Palace": SimpleNamespace(boss=boss(boss_overrides.get("Desert Palace", "Lanmola"))),
|
||||
"Tower of Hera": SimpleNamespace(boss=boss(boss_overrides.get("Tower of Hera", "Moldorm"))),
|
||||
"Palace of Darkness": SimpleNamespace(boss=boss(boss_overrides.get("Palace of Darkness", "Helmasaur"))),
|
||||
"Swamp Palace": SimpleNamespace(boss=boss(boss_overrides.get("Swamp Palace", "Arrghus"))),
|
||||
"Skull Woods": SimpleNamespace(boss=boss(boss_overrides.get("Skull Woods", "Mothula"))),
|
||||
"Thieves Town": SimpleNamespace(boss=boss(boss_overrides.get("Thieves Town", "Blind"))),
|
||||
"Ice Palace": SimpleNamespace(boss=boss(boss_overrides.get("Ice Palace", "Kholdstare"))),
|
||||
"Misery Mire": SimpleNamespace(boss=boss(boss_overrides.get("Misery Mire", "Vitreous"))),
|
||||
"Turtle Rock": SimpleNamespace(boss=boss(boss_overrides.get("Turtle Rock", "Trinexx"))),
|
||||
"Ganons Tower": SimpleNamespace(
|
||||
bosses={
|
||||
"bottom": boss(boss_overrides.get("Ganons Tower Bottom", "Armos")),
|
||||
"middle": boss(boss_overrides.get("Ganons Tower Middle", "Lanmola")),
|
||||
"top": boss(boss_overrides.get("Ganons Tower Top", "Moldorm")),
|
||||
}
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,834 +0,0 @@
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
import random
|
||||
|
||||
from worlds.alttp.EnemyShuffle import (
|
||||
DungeonEnemyRoom,
|
||||
DungeonEnemySprite,
|
||||
DungeonSpriteGroup,
|
||||
EnemyShuffleState,
|
||||
EnemySpriteRequirement,
|
||||
OverworldEnemyArea,
|
||||
OverworldEnemySprite,
|
||||
RandomizedDungeonEnemyRoom,
|
||||
RandomizedDungeonEnemySprite,
|
||||
RandomizedOverworldEnemyArea,
|
||||
RandomizedOverworldEnemySprite,
|
||||
WALLMASTER_SPRITE_ID,
|
||||
_load_dungeon_sprite_metadata,
|
||||
_read_room_sprites,
|
||||
get_possible_dungeon_sprite_groups,
|
||||
_get_requirements_for_usable_dungeon_enemies,
|
||||
_get_requirements_for_usable_overworld_enemies,
|
||||
_get_randomizable_sprites_in_room,
|
||||
_apply_selected_boss_group_requirements,
|
||||
_randomize_overworld_groups,
|
||||
_randomize_room_sprites,
|
||||
_setup_required_overworld_groups,
|
||||
can_spawn_in_room,
|
||||
validate_enemy_shuffle_state,
|
||||
)
|
||||
|
||||
|
||||
class TestEnemyShuffleValidation(unittest.TestCase):
|
||||
def test_curated_room_sprite_addresses_exclude_hera_basement_key_slot(self) -> None:
|
||||
room_id = 135
|
||||
sprite_table_address = 0x4E397
|
||||
rom_bytes = bytearray(0x4E3C0)
|
||||
rom_bytes[sprite_table_address] = 0
|
||||
room_135_sprite_records = (
|
||||
(0x4E398, 0x05, 0x14, 0x18),
|
||||
(0x4E39B, 0x07, 0x1A, 0x18),
|
||||
(0x4E39E, 0x0B, 0x13, 0x18),
|
||||
(0x4E3A1, 0x19, 0x06, 0x18),
|
||||
(0x4E3A4, 0x08, 0xE7, 0x14),
|
||||
(0x4E3A7, 0x04, 0x17, 0x1E),
|
||||
(0x4E3AA, 0x0C, 0x03, 0x1E),
|
||||
(0x4E3AD, 0x15, 0x04, 0x1E),
|
||||
(0x4E3B0, 0x17, 0x0B, 0xA7),
|
||||
(0x4E3B3, 0x18, 0x19, 0xA7),
|
||||
(0x4E3B6, 0x19, 0x04, 0xA7),
|
||||
(0x4E3B9, 0x1A, 0x08, 0xE4),
|
||||
(0x4E3BC, 0x1C, 0x15, 0xA7),
|
||||
)
|
||||
for address, byte_0, byte_1, sprite_id in room_135_sprite_records:
|
||||
rom_bytes[address] = byte_0
|
||||
rom_bytes[address + 1] = byte_1
|
||||
rom_bytes[address + 2] = sprite_id
|
||||
rom_bytes[0x4E3BF] = 0xFF
|
||||
|
||||
sprites = _read_room_sprites(rom_bytes, room_id, sprite_table_address, _load_dungeon_sprite_metadata())
|
||||
sprite_addresses = {sprite.address for sprite in sprites}
|
||||
|
||||
self.assertNotIn(0x4E3B9, sprite_addresses)
|
||||
self.assertIn(0x4E3B6, sprite_addresses)
|
||||
self.assertFalse(any(sprite.has_key for sprite in sprites))
|
||||
|
||||
def test_curated_room_sprite_addresses_deduplicate_duplicate_slots(self) -> None:
|
||||
room_id = 125
|
||||
sprite_table_address = 0x4E2CA
|
||||
metadata = _load_dungeon_sprite_metadata()
|
||||
max_sprite_id_address = max(metadata["room_sprite_id_addresses"][room_id])
|
||||
rom_bytes = bytearray(max_sprite_id_address + 2)
|
||||
rom_bytes[sprite_table_address] = 0
|
||||
for offset, sprite_id_address in enumerate(metadata["room_sprite_id_addresses"][room_id]):
|
||||
address = sprite_id_address - 2
|
||||
sprite_id = 0x80 if offset % 2 == 0 else 0x81
|
||||
rom_bytes[address] = 0
|
||||
rom_bytes[address + 1] = 0
|
||||
rom_bytes[address + 2] = sprite_id
|
||||
|
||||
sprites = _read_room_sprites(rom_bytes, room_id, sprite_table_address, metadata)
|
||||
sprite_addresses = [sprite.address for sprite in sprites]
|
||||
|
||||
self.assertEqual(len(sprite_addresses), len(set(sprite_addresses)))
|
||||
|
||||
def test_rejects_non_killable_shutter_room(self) -> None:
|
||||
room = DungeonEnemyRoom(
|
||||
room_id=1,
|
||||
room_header_address=0,
|
||||
sprite_table_address=0,
|
||||
graphics_block_id=1,
|
||||
tag_1=0,
|
||||
tag_2=0,
|
||||
sort_sprites_value=0,
|
||||
sprites=(
|
||||
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x10, is_overlord=False, has_key=False),
|
||||
),
|
||||
required_group_id=None,
|
||||
required_subgroup_0=tuple(),
|
||||
required_subgroup_1=tuple(),
|
||||
required_subgroup_2=tuple(),
|
||||
required_subgroup_3=tuple(),
|
||||
is_shutter_room=True,
|
||||
is_water_room=False,
|
||||
do_not_randomize=False,
|
||||
no_special_enemies_standard=False,
|
||||
)
|
||||
state = self._build_state(
|
||||
dungeon_rooms={1: room},
|
||||
randomized_dungeon_rooms={
|
||||
1: RandomizedDungeonEnemyRoom(
|
||||
room_id=1,
|
||||
room_header_address=0,
|
||||
sprite_table_address=0,
|
||||
original_graphics_block_id=1,
|
||||
graphics_block_id=1,
|
||||
tag_1=0,
|
||||
tag_2=0,
|
||||
sort_sprites_value=0,
|
||||
sprites=(
|
||||
RandomizedDungeonEnemySprite(
|
||||
address=0x1000,
|
||||
byte_0=0,
|
||||
byte_1=0,
|
||||
original_sprite_id=0x10,
|
||||
sprite_id=0x11,
|
||||
is_overlord=False,
|
||||
has_key=False,
|
||||
),
|
||||
),
|
||||
skipped_randomization=False,
|
||||
)
|
||||
},
|
||||
sprite_requirements=(
|
||||
self._requirement(0x10, killable=True, subgroup_0=(1,)),
|
||||
self._requirement(0x11, killable=False, subgroup_0=(1,)),
|
||||
),
|
||||
)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
validate_enemy_shuffle_state(state, is_standard_mode=False)
|
||||
|
||||
def test_rejects_water_enemy_in_non_water_room(self) -> None:
|
||||
room = DungeonEnemyRoom(
|
||||
room_id=165,
|
||||
room_header_address=0,
|
||||
sprite_table_address=0,
|
||||
graphics_block_id=1,
|
||||
tag_1=0,
|
||||
tag_2=0,
|
||||
sort_sprites_value=0,
|
||||
sprites=(
|
||||
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x20, is_overlord=False, has_key=False),
|
||||
),
|
||||
required_group_id=None,
|
||||
required_subgroup_0=tuple(),
|
||||
required_subgroup_1=tuple(),
|
||||
required_subgroup_2=tuple(),
|
||||
required_subgroup_3=tuple(),
|
||||
is_shutter_room=True,
|
||||
is_water_room=False,
|
||||
do_not_randomize=False,
|
||||
no_special_enemies_standard=False,
|
||||
)
|
||||
state = self._build_state(
|
||||
dungeon_rooms={165: room},
|
||||
randomized_dungeon_rooms={
|
||||
165: RandomizedDungeonEnemyRoom(
|
||||
room_id=165,
|
||||
room_header_address=0,
|
||||
sprite_table_address=0,
|
||||
original_graphics_block_id=1,
|
||||
graphics_block_id=1,
|
||||
tag_1=0,
|
||||
tag_2=0,
|
||||
sort_sprites_value=0,
|
||||
sprites=(
|
||||
RandomizedDungeonEnemySprite(
|
||||
address=0x1000,
|
||||
byte_0=0,
|
||||
byte_1=0,
|
||||
original_sprite_id=0x20,
|
||||
sprite_id=0x81,
|
||||
is_overlord=False,
|
||||
has_key=False,
|
||||
),
|
||||
),
|
||||
skipped_randomization=False,
|
||||
)
|
||||
},
|
||||
sprite_requirements=(
|
||||
self._requirement(0x20, killable=True, subgroup_0=(1,)),
|
||||
self._requirement(0x81, killable=True, subgroup_0=(1,), is_water_sprite=True),
|
||||
),
|
||||
)
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "water enemy"):
|
||||
validate_enemy_shuffle_state(state, is_standard_mode=False)
|
||||
|
||||
def test_rejects_multiple_flopping_fish(self) -> None:
|
||||
area = OverworldEnemyArea(
|
||||
area_id=0x10,
|
||||
sprite_table_address=0,
|
||||
graphics_block_address=0,
|
||||
graphics_block_id=1,
|
||||
bush_sprite_id=0x20,
|
||||
sprites=(
|
||||
OverworldEnemySprite(address=0x2000, y_coord=0, x_coord=0, sprite_id=0x20),
|
||||
OverworldEnemySprite(address=0x2003, y_coord=0, x_coord=0, sprite_id=0x21),
|
||||
),
|
||||
do_not_randomize=False,
|
||||
)
|
||||
state = self._build_state(
|
||||
overworld_areas={0x10: area},
|
||||
randomized_overworld_areas={
|
||||
0x10: RandomizedOverworldEnemyArea(
|
||||
area_id=0x10,
|
||||
sprite_table_address=0,
|
||||
graphics_block_address=0,
|
||||
original_graphics_block_id=1,
|
||||
graphics_block_id=1,
|
||||
original_bush_sprite_id=0x20,
|
||||
bush_sprite_id=0xD2,
|
||||
sprites=(
|
||||
RandomizedOverworldEnemySprite(
|
||||
address=0x2000,
|
||||
y_coord=0,
|
||||
x_coord=0,
|
||||
original_sprite_id=0x20,
|
||||
sprite_id=0xD2,
|
||||
),
|
||||
RandomizedOverworldEnemySprite(
|
||||
address=0x2003,
|
||||
y_coord=0,
|
||||
x_coord=0,
|
||||
original_sprite_id=0x21,
|
||||
sprite_id=0xD2,
|
||||
),
|
||||
),
|
||||
skipped_randomization=False,
|
||||
)
|
||||
},
|
||||
sprite_requirements=(
|
||||
self._requirement(0x20, group_ids=(1,)),
|
||||
self._requirement(0x21, group_ids=(1,)),
|
||||
self._requirement(0x22, group_ids=(1,)),
|
||||
self._requirement(0xD2, group_ids=(1,)),
|
||||
),
|
||||
)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
validate_enemy_shuffle_state(state, is_standard_mode=False)
|
||||
|
||||
def test_allows_multiple_flopping_fish_when_no_other_sprite_is_possible(self) -> None:
|
||||
area = OverworldEnemyArea(
|
||||
area_id=0x10,
|
||||
sprite_table_address=0,
|
||||
graphics_block_address=0,
|
||||
graphics_block_id=1,
|
||||
bush_sprite_id=0x20,
|
||||
sprites=(
|
||||
OverworldEnemySprite(address=0x2000, y_coord=0, x_coord=0, sprite_id=0x20),
|
||||
OverworldEnemySprite(address=0x2003, y_coord=0, x_coord=0, sprite_id=0x21),
|
||||
),
|
||||
do_not_randomize=False,
|
||||
)
|
||||
state = self._build_state(
|
||||
overworld_areas={0x10: area},
|
||||
randomized_overworld_areas={
|
||||
0x10: RandomizedOverworldEnemyArea(
|
||||
area_id=0x10,
|
||||
sprite_table_address=0,
|
||||
graphics_block_address=0,
|
||||
original_graphics_block_id=1,
|
||||
graphics_block_id=1,
|
||||
original_bush_sprite_id=0x20,
|
||||
bush_sprite_id=0xD2,
|
||||
sprites=(
|
||||
RandomizedOverworldEnemySprite(
|
||||
address=0x2000,
|
||||
y_coord=0,
|
||||
x_coord=0,
|
||||
original_sprite_id=0x20,
|
||||
sprite_id=0xD2,
|
||||
),
|
||||
RandomizedOverworldEnemySprite(
|
||||
address=0x2003,
|
||||
y_coord=0,
|
||||
x_coord=0,
|
||||
original_sprite_id=0x21,
|
||||
sprite_id=0xD2,
|
||||
),
|
||||
),
|
||||
skipped_randomization=False,
|
||||
)
|
||||
},
|
||||
sprite_requirements=(
|
||||
self._requirement(0x20, group_ids=(2,)),
|
||||
self._requirement(0x21, group_ids=(2,)),
|
||||
self._requirement(0xD2, group_ids=(1,)),
|
||||
),
|
||||
)
|
||||
|
||||
validate_enemy_shuffle_state(state, is_standard_mode=False)
|
||||
|
||||
def test_excludes_absorbables_from_usable_enemy_pools(self) -> None:
|
||||
state = self._build_state(
|
||||
sprite_requirements=(
|
||||
self._requirement(0x10, subgroup_0=(1,)),
|
||||
self._requirement(0xE3, subgroup_0=(1,), absorbable=True),
|
||||
self._requirement(0x20, subgroup_0=(1,), never_use_dungeon=True),
|
||||
self._requirement(0x21, subgroup_0=(1,), never_use_overworld=True),
|
||||
),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
[requirement.sprite_id for requirement in _get_requirements_for_usable_dungeon_enemies(state)],
|
||||
[0x10, 0x21],
|
||||
)
|
||||
self.assertEqual(
|
||||
[requirement.sprite_id for requirement in _get_requirements_for_usable_overworld_enemies(state)],
|
||||
[0x10, 0x20],
|
||||
)
|
||||
|
||||
def test_key_enemy_replacements_exclude_moblins(self) -> None:
|
||||
room = DungeonEnemyRoom(
|
||||
room_id=1,
|
||||
room_header_address=0,
|
||||
sprite_table_address=0,
|
||||
graphics_block_id=1,
|
||||
tag_1=0,
|
||||
tag_2=0,
|
||||
sort_sprites_value=0,
|
||||
sprites=(
|
||||
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x12, is_overlord=False, has_key=True),
|
||||
),
|
||||
required_group_id=None,
|
||||
required_subgroup_0=tuple(),
|
||||
required_subgroup_1=tuple(),
|
||||
required_subgroup_2=tuple(),
|
||||
required_subgroup_3=tuple(),
|
||||
is_shutter_room=False,
|
||||
is_water_room=False,
|
||||
do_not_randomize=False,
|
||||
no_special_enemies_standard=False,
|
||||
)
|
||||
state = self._build_state(
|
||||
dungeon_rooms={1: room},
|
||||
sprite_requirements=(
|
||||
self._requirement(0x12, killable=True, subgroup_0=(1,), cannot_have_key=True),
|
||||
self._requirement(0x13, killable=True, subgroup_0=(1,)),
|
||||
),
|
||||
)
|
||||
selected_group = state.sprite_groups[0x41]
|
||||
|
||||
randomized_room = _randomize_room_sprites(
|
||||
SimpleNamespace(random=random.Random(0)),
|
||||
state,
|
||||
room,
|
||||
selected_group,
|
||||
False,
|
||||
)
|
||||
|
||||
self.assertEqual(randomized_room.sprites[0].sprite_id, 0x13)
|
||||
|
||||
def test_shutter_water_room_prefers_killable_water_enemy(self) -> None:
|
||||
room = DungeonEnemyRoom(
|
||||
room_id=40,
|
||||
room_header_address=0,
|
||||
sprite_table_address=0,
|
||||
graphics_block_id=1,
|
||||
tag_1=0,
|
||||
tag_2=0,
|
||||
sort_sprites_value=0,
|
||||
sprites=(
|
||||
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x8A, is_overlord=False, has_key=False),
|
||||
),
|
||||
required_group_id=None,
|
||||
required_subgroup_0=tuple(),
|
||||
required_subgroup_1=tuple(),
|
||||
required_subgroup_2=tuple(),
|
||||
required_subgroup_3=tuple(),
|
||||
is_shutter_room=True,
|
||||
is_water_room=True,
|
||||
do_not_randomize=False,
|
||||
no_special_enemies_standard=False,
|
||||
)
|
||||
state = self._build_state(
|
||||
dungeon_rooms={40: room},
|
||||
sprite_requirements=(
|
||||
self._requirement(0x8A, killable=False, subgroup_2=(34,)),
|
||||
self._requirement(0x81, killable=True, subgroup_2=(34,), is_water_sprite=True),
|
||||
self._requirement(0x9A, killable=False, subgroup_2=(34,), is_water_sprite=True),
|
||||
),
|
||||
)
|
||||
selected_group = state.sprite_groups[0x41]
|
||||
selected_group.subgroup_2 = 34
|
||||
|
||||
randomized_room = _randomize_room_sprites(
|
||||
SimpleNamespace(random=random.Random(0)),
|
||||
state,
|
||||
room,
|
||||
selected_group,
|
||||
False,
|
||||
)
|
||||
|
||||
self.assertEqual(randomized_room.sprites[0].sprite_id, 0x81)
|
||||
|
||||
def test_non_water_shutter_room_replacements_exclude_water_enemies(self) -> None:
|
||||
room = DungeonEnemyRoom(
|
||||
room_id=165,
|
||||
room_header_address=0,
|
||||
sprite_table_address=0,
|
||||
graphics_block_id=1,
|
||||
tag_1=0,
|
||||
tag_2=0,
|
||||
sort_sprites_value=0,
|
||||
sprites=(
|
||||
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x20, is_overlord=False, has_key=False),
|
||||
),
|
||||
required_group_id=None,
|
||||
required_subgroup_0=tuple(),
|
||||
required_subgroup_1=tuple(),
|
||||
required_subgroup_2=tuple(),
|
||||
required_subgroup_3=tuple(),
|
||||
is_shutter_room=True,
|
||||
is_water_room=False,
|
||||
do_not_randomize=False,
|
||||
no_special_enemies_standard=False,
|
||||
)
|
||||
state = self._build_state(
|
||||
dungeon_rooms={165: room},
|
||||
sprite_requirements=(
|
||||
self._requirement(0x20, killable=False, subgroup_0=(1,)),
|
||||
self._requirement(0x81, killable=True, subgroup_0=(1,), is_water_sprite=True),
|
||||
self._requirement(0x22, killable=True, subgroup_0=(1,)),
|
||||
),
|
||||
)
|
||||
|
||||
randomized_room = _randomize_room_sprites(
|
||||
SimpleNamespace(random=random.Random(1)),
|
||||
state,
|
||||
room,
|
||||
state.sprite_groups[0x41],
|
||||
False,
|
||||
)
|
||||
|
||||
self.assertEqual(randomized_room.sprites[0].sprite_id, 0x22)
|
||||
|
||||
def test_non_water_shutter_group_selection_requires_non_water_killable_enemy(self) -> None:
|
||||
room = DungeonEnemyRoom(
|
||||
room_id=165,
|
||||
room_header_address=0,
|
||||
sprite_table_address=0,
|
||||
graphics_block_id=1,
|
||||
tag_1=0,
|
||||
tag_2=0,
|
||||
sort_sprites_value=0,
|
||||
sprites=(
|
||||
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x20, is_overlord=False, has_key=False),
|
||||
),
|
||||
required_group_id=None,
|
||||
required_subgroup_0=tuple(),
|
||||
required_subgroup_1=tuple(),
|
||||
required_subgroup_2=tuple(),
|
||||
required_subgroup_3=tuple(),
|
||||
is_shutter_room=True,
|
||||
is_water_room=False,
|
||||
do_not_randomize=False,
|
||||
no_special_enemies_standard=False,
|
||||
)
|
||||
state = self._build_state(
|
||||
dungeon_rooms={165: room},
|
||||
sprite_requirements=(
|
||||
self._requirement(0x20, killable=False, subgroup_0=(1,)),
|
||||
self._requirement(0x81, killable=True, subgroup_0=(1,), is_water_sprite=True),
|
||||
),
|
||||
)
|
||||
|
||||
self.assertEqual(get_possible_dungeon_sprite_groups(state, room), tuple())
|
||||
|
||||
def test_wallmaster_cannot_spawn_in_high_room_ids(self) -> None:
|
||||
room = DungeonEnemyRoom(
|
||||
room_id=0x100,
|
||||
room_header_address=0,
|
||||
sprite_table_address=0,
|
||||
graphics_block_id=1,
|
||||
tag_1=0,
|
||||
tag_2=0,
|
||||
sort_sprites_value=0,
|
||||
sprites=tuple(),
|
||||
required_group_id=None,
|
||||
required_subgroup_0=tuple(),
|
||||
required_subgroup_1=tuple(),
|
||||
required_subgroup_2=tuple(),
|
||||
required_subgroup_3=tuple(),
|
||||
is_shutter_room=False,
|
||||
is_water_room=False,
|
||||
do_not_randomize=False,
|
||||
no_special_enemies_standard=False,
|
||||
)
|
||||
|
||||
self.assertFalse(can_spawn_in_room(self._requirement(WALLMASTER_SPRITE_ID), room))
|
||||
|
||||
def test_room_specific_do_not_randomize_sprites_are_not_updated(self) -> None:
|
||||
room = DungeonEnemyRoom(
|
||||
room_id=7,
|
||||
room_header_address=0,
|
||||
sprite_table_address=0,
|
||||
graphics_block_id=1,
|
||||
tag_1=0,
|
||||
tag_2=0,
|
||||
sort_sprites_value=0,
|
||||
sprites=(
|
||||
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x30, is_overlord=False, has_key=False),
|
||||
DungeonEnemySprite(address=0x1003, byte_0=0, byte_1=0, sprite_id=0x31, is_overlord=False, has_key=False),
|
||||
),
|
||||
required_group_id=None,
|
||||
required_subgroup_0=tuple(),
|
||||
required_subgroup_1=tuple(),
|
||||
required_subgroup_2=tuple(),
|
||||
required_subgroup_3=tuple(),
|
||||
is_shutter_room=False,
|
||||
is_water_room=False,
|
||||
do_not_randomize=False,
|
||||
no_special_enemies_standard=False,
|
||||
)
|
||||
state = self._build_state(
|
||||
dungeon_rooms={7: room},
|
||||
sprite_requirements=(
|
||||
self._requirement(0x30, subgroup_0=(1,), dont_randomize_rooms=(7,)),
|
||||
self._requirement(0x31, subgroup_0=(1,)),
|
||||
),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
[sprite.sprite_id for sprite in _get_randomizable_sprites_in_room(state, room)],
|
||||
[0x31],
|
||||
)
|
||||
|
||||
def test_water_rooms_only_use_water_enemies(self) -> None:
|
||||
room = DungeonEnemyRoom(
|
||||
room_id=1,
|
||||
room_header_address=0,
|
||||
sprite_table_address=0,
|
||||
graphics_block_id=1,
|
||||
tag_1=0,
|
||||
tag_2=0,
|
||||
sort_sprites_value=0,
|
||||
sprites=(
|
||||
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x20, is_overlord=False, has_key=False),
|
||||
),
|
||||
required_group_id=None,
|
||||
required_subgroup_0=tuple(),
|
||||
required_subgroup_1=tuple(),
|
||||
required_subgroup_2=tuple(),
|
||||
required_subgroup_3=tuple(),
|
||||
is_shutter_room=False,
|
||||
is_water_room=True,
|
||||
do_not_randomize=False,
|
||||
no_special_enemies_standard=False,
|
||||
)
|
||||
state = self._build_state(
|
||||
dungeon_rooms={1: room},
|
||||
sprite_requirements=(
|
||||
self._requirement(0x20, subgroup_0=(1,)),
|
||||
self._requirement(0x21, subgroup_0=(1,), is_water_sprite=True),
|
||||
self._requirement(0x22, subgroup_0=(1,), is_water_sprite=True),
|
||||
),
|
||||
)
|
||||
|
||||
randomized_room = _randomize_room_sprites(
|
||||
SimpleNamespace(random=random.Random(0)),
|
||||
state,
|
||||
room,
|
||||
state.sprite_groups[0x41],
|
||||
False,
|
||||
)
|
||||
|
||||
self.assertIn(randomized_room.sprites[0].sprite_id, {0x21, 0x22})
|
||||
|
||||
def test_dungeon_group_selection_excludes_groups_without_enemy_requirements(self) -> None:
|
||||
room = DungeonEnemyRoom(
|
||||
room_id=1,
|
||||
room_header_address=0,
|
||||
sprite_table_address=0,
|
||||
graphics_block_id=1,
|
||||
tag_1=0,
|
||||
tag_2=0,
|
||||
sort_sprites_value=0,
|
||||
sprites=(
|
||||
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x20, is_overlord=False, has_key=False),
|
||||
),
|
||||
required_group_id=None,
|
||||
required_subgroup_0=tuple(),
|
||||
required_subgroup_1=tuple(),
|
||||
required_subgroup_2=tuple(),
|
||||
required_subgroup_3=tuple(),
|
||||
is_shutter_room=False,
|
||||
is_water_room=False,
|
||||
do_not_randomize=False,
|
||||
no_special_enemies_standard=False,
|
||||
)
|
||||
state = self._build_state(
|
||||
dungeon_rooms={1: room},
|
||||
sprite_requirements=(self._requirement(0x20, subgroup_0=(1,)),),
|
||||
)
|
||||
state.sprite_groups[0x42] = DungeonSpriteGroup(
|
||||
group_id=0x42,
|
||||
dungeon_group_id=2,
|
||||
subgroup_0=0,
|
||||
subgroup_1=0,
|
||||
subgroup_2=0,
|
||||
subgroup_3=0,
|
||||
)
|
||||
|
||||
possible_groups = get_possible_dungeon_sprite_groups(state, room)
|
||||
|
||||
self.assertEqual([group.group_id for group in possible_groups], [0x41])
|
||||
|
||||
def test_key_room_group_selection_excludes_groups_without_room_spawnable_key_enemies(self) -> None:
|
||||
room = DungeonEnemyRoom(
|
||||
room_id=61,
|
||||
room_header_address=0,
|
||||
sprite_table_address=0,
|
||||
graphics_block_id=1,
|
||||
tag_1=0,
|
||||
tag_2=0,
|
||||
sort_sprites_value=0,
|
||||
sprites=(
|
||||
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x20, is_overlord=False, has_key=True),
|
||||
),
|
||||
required_group_id=None,
|
||||
required_subgroup_0=tuple(),
|
||||
required_subgroup_1=tuple(),
|
||||
required_subgroup_2=tuple(),
|
||||
required_subgroup_3=tuple(),
|
||||
is_shutter_room=False,
|
||||
is_water_room=False,
|
||||
do_not_randomize=False,
|
||||
no_special_enemies_standard=False,
|
||||
)
|
||||
state = self._build_state(
|
||||
dungeon_rooms={61: room},
|
||||
sprite_requirements=(
|
||||
self._requirement(0x20, subgroup_0=(1,)),
|
||||
self._requirement(0x50, killable=True, subgroup_1=(32,), excluded_rooms=(61,)),
|
||||
self._requirement(0x9C, killable=True, subgroup_1=(32,), cannot_have_key=True),
|
||||
self._requirement(0x51, killable=True, subgroup_1=(33,)),
|
||||
),
|
||||
)
|
||||
state.sprite_groups[0x41] = DungeonSpriteGroup(
|
||||
group_id=0x41,
|
||||
dungeon_group_id=1,
|
||||
subgroup_0=1,
|
||||
subgroup_1=32,
|
||||
subgroup_2=1,
|
||||
subgroup_3=1,
|
||||
)
|
||||
state.sprite_groups[0x42] = DungeonSpriteGroup(
|
||||
group_id=0x42,
|
||||
dungeon_group_id=2,
|
||||
subgroup_0=1,
|
||||
subgroup_1=33,
|
||||
subgroup_2=1,
|
||||
subgroup_3=1,
|
||||
)
|
||||
|
||||
possible_groups = get_possible_dungeon_sprite_groups(state, room)
|
||||
|
||||
self.assertEqual([group.group_id for group in possible_groups], [0x42])
|
||||
|
||||
def test_overworld_group_randomization_preserves_forced_subgroups(self) -> None:
|
||||
sprite_groups = {
|
||||
7: DungeonSpriteGroup(group_id=7, dungeon_group_id=-57, subgroup_0=1, subgroup_1=2, subgroup_2=3, subgroup_3=4),
|
||||
}
|
||||
|
||||
_setup_required_overworld_groups(
|
||||
sprite_groups,
|
||||
(
|
||||
SimpleNamespace(
|
||||
group_id=7,
|
||||
subgroup_0=None,
|
||||
subgroup_1=None,
|
||||
subgroup_2=None,
|
||||
subgroup_3=17,
|
||||
areas=(0x02,),
|
||||
),
|
||||
),
|
||||
)
|
||||
_randomize_overworld_groups(SimpleNamespace(random=random.Random(0)), sprite_groups)
|
||||
|
||||
group = sprite_groups[7]
|
||||
self.assertEqual(group.subgroup_3, 17)
|
||||
self.assertIn(group.subgroup_0, {22, 31, 47, 14})
|
||||
self.assertIn(group.subgroup_1, {44, 30, 32})
|
||||
self.assertIn(group.subgroup_2, {12, 18, 23, 24, 28, 46, 34, 35, 39, 40, 38, 41, 36, 37, 42})
|
||||
|
||||
def test_selected_boss_group_requirements_override_shared_boss_graphics_group(self) -> None:
|
||||
sprite_groups = {
|
||||
0x56: DungeonSpriteGroup(
|
||||
group_id=0x56,
|
||||
dungeon_group_id=22,
|
||||
subgroup_0=1,
|
||||
subgroup_1=1,
|
||||
subgroup_2=60,
|
||||
subgroup_3=49,
|
||||
),
|
||||
}
|
||||
sprite_requirements = (
|
||||
self._requirement(162, subgroup_2=(60,)),
|
||||
self._requirement(189, subgroup_3=(61,)),
|
||||
)
|
||||
|
||||
_apply_selected_boss_group_requirements(
|
||||
self._build_boss_world({"Eastern Palace": "Vitreous"}),
|
||||
sprite_groups,
|
||||
sprite_requirements,
|
||||
)
|
||||
|
||||
group = sprite_groups[0x56]
|
||||
self.assertEqual(group.subgroup_2, 60)
|
||||
self.assertEqual(group.subgroup_3, 61)
|
||||
self.assertTrue(group.preserve_subgroup_2)
|
||||
self.assertTrue(group.preserve_subgroup_3)
|
||||
|
||||
@staticmethod
|
||||
def _requirement(
|
||||
sprite_id: int,
|
||||
*,
|
||||
killable: bool = False,
|
||||
subgroup_0: tuple[int, ...] = tuple(),
|
||||
subgroup_1: tuple[int, ...] = tuple(),
|
||||
subgroup_2: tuple[int, ...] = tuple(),
|
||||
subgroup_3: tuple[int, ...] = tuple(),
|
||||
group_ids: tuple[int, ...] = tuple(),
|
||||
absorbable: bool = False,
|
||||
never_use_dungeon: bool = False,
|
||||
never_use_overworld: bool = False,
|
||||
cannot_have_key: bool = False,
|
||||
is_water_sprite: bool = False,
|
||||
excluded_rooms: tuple[int, ...] = tuple(),
|
||||
dont_randomize_rooms: tuple[int, ...] = tuple(),
|
||||
) -> EnemySpriteRequirement:
|
||||
return EnemySpriteRequirement(
|
||||
sprite_name=f"sprite_{sprite_id:02x}",
|
||||
sprite_id=sprite_id,
|
||||
boss=False,
|
||||
overlord=False,
|
||||
do_not_randomize=False,
|
||||
killable=killable,
|
||||
npc=False,
|
||||
never_use_dungeon=never_use_dungeon,
|
||||
never_use_overworld=never_use_overworld,
|
||||
cannot_have_key=cannot_have_key,
|
||||
is_object=False,
|
||||
absorbable=absorbable,
|
||||
is_water_sprite=is_water_sprite,
|
||||
is_enemy_sprite=True,
|
||||
group_ids=group_ids,
|
||||
subgroup_0=subgroup_0,
|
||||
subgroup_1=subgroup_1,
|
||||
subgroup_2=subgroup_2,
|
||||
subgroup_3=subgroup_3,
|
||||
parameters=None,
|
||||
special_glitched=False,
|
||||
excluded_rooms=excluded_rooms,
|
||||
dont_randomize_rooms=dont_randomize_rooms,
|
||||
spawnable_rooms=tuple(),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _build_state(
|
||||
*,
|
||||
dungeon_rooms=None,
|
||||
overworld_areas=None,
|
||||
randomized_dungeon_rooms=None,
|
||||
randomized_overworld_areas=None,
|
||||
sprite_requirements=tuple(),
|
||||
) -> EnemyShuffleState:
|
||||
sprite_groups = {
|
||||
1: DungeonSpriteGroup(group_id=1, dungeon_group_id=-63, subgroup_0=1, subgroup_1=1, subgroup_2=1, subgroup_3=1),
|
||||
0x41: DungeonSpriteGroup(group_id=0x41, dungeon_group_id=1, subgroup_0=1, subgroup_1=1, subgroup_2=1, subgroup_3=1),
|
||||
}
|
||||
return EnemyShuffleState(
|
||||
dungeon_rooms=dungeon_rooms or {},
|
||||
overworld_areas=overworld_areas or {},
|
||||
sprite_groups=sprite_groups,
|
||||
sprite_requirements=sprite_requirements,
|
||||
room_group_requirements=tuple(),
|
||||
overworld_group_requirements=tuple(),
|
||||
shutter_room_ids=frozenset(),
|
||||
water_room_ids=frozenset(),
|
||||
dont_randomize_room_ids=frozenset(),
|
||||
no_special_enemies_standard_room_ids=frozenset(),
|
||||
boss_room_ids=frozenset(),
|
||||
dont_randomize_overworld_area_ids=frozenset(),
|
||||
randomized_dungeon_rooms=randomized_dungeon_rooms or {},
|
||||
randomized_overworld_areas=randomized_overworld_areas or {},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _build_boss_world(boss_overrides: dict[str, str] | None = None) -> SimpleNamespace:
|
||||
boss_overrides = boss_overrides or {}
|
||||
|
||||
def boss(name: str) -> SimpleNamespace:
|
||||
return SimpleNamespace(enemizer_name=name)
|
||||
|
||||
return SimpleNamespace(
|
||||
options=SimpleNamespace(mode="open"),
|
||||
dungeons={
|
||||
"Eastern Palace": SimpleNamespace(boss=boss(boss_overrides.get("Eastern Palace", "Armos"))),
|
||||
"Desert Palace": SimpleNamespace(boss=boss(boss_overrides.get("Desert Palace", "Lanmola"))),
|
||||
"Tower of Hera": SimpleNamespace(boss=boss(boss_overrides.get("Tower of Hera", "Moldorm"))),
|
||||
"Palace of Darkness": SimpleNamespace(boss=boss(boss_overrides.get("Palace of Darkness", "Helmasaur"))),
|
||||
"Swamp Palace": SimpleNamespace(boss=boss(boss_overrides.get("Swamp Palace", "Arrghus"))),
|
||||
"Skull Woods": SimpleNamespace(boss=boss(boss_overrides.get("Skull Woods", "Mothula"))),
|
||||
"Thieves Town": SimpleNamespace(boss=boss(boss_overrides.get("Thieves Town", "Blind"))),
|
||||
"Ice Palace": SimpleNamespace(boss=boss(boss_overrides.get("Ice Palace", "Kholdstare"))),
|
||||
"Misery Mire": SimpleNamespace(boss=boss(boss_overrides.get("Misery Mire", "Vitreous"))),
|
||||
"Turtle Rock": SimpleNamespace(boss=boss(boss_overrides.get("Turtle Rock", "Trinexx"))),
|
||||
"Ganons Tower": SimpleNamespace(
|
||||
bosses={
|
||||
"bottom": boss(boss_overrides.get("Ganons Tower Bottom", "Armos")),
|
||||
"middle": boss(boss_overrides.get("Ganons Tower Middle", "Lanmola")),
|
||||
"top": boss(boss_overrides.get("Ganons Tower Top", "Moldorm")),
|
||||
}
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,56 +0,0 @@
|
||||
import random
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
|
||||
from worlds.alttp.PotShuffle import (
|
||||
POT_KEY,
|
||||
POT_HOLE,
|
||||
generate_pot_shuffle,
|
||||
get_unique_pot_item_position,
|
||||
)
|
||||
|
||||
|
||||
class TestPotShuffle(unittest.TestCase):
|
||||
def test_reserved_key_rooms_only_place_actual_keys(self) -> None:
|
||||
for seed in range(10):
|
||||
world = SimpleNamespace(
|
||||
random=random.Random(seed),
|
||||
options=SimpleNamespace(retro_bow=False),
|
||||
)
|
||||
shuffled_pots = generate_pot_shuffle(world)
|
||||
conveyor_cross_keys = [
|
||||
pot for pot in shuffled_pots[0x8B]
|
||||
if pot.item == POT_KEY
|
||||
]
|
||||
self.assertEqual(len(conveyor_cross_keys), 1)
|
||||
|
||||
def test_get_unique_pot_item_position_returns_single_match(self) -> None:
|
||||
world = SimpleNamespace(
|
||||
random=random.Random(0),
|
||||
options=SimpleNamespace(retro_bow=False),
|
||||
)
|
||||
shuffled_pots = generate_pot_shuffle(world)
|
||||
|
||||
self.assertEqual(
|
||||
get_unique_pot_item_position(shuffled_pots, 0x36, POT_KEY),
|
||||
(114, 16),
|
||||
)
|
||||
|
||||
def test_reserved_hole_room_keeps_hole_fixed(self) -> None:
|
||||
for seed in range(25):
|
||||
world = SimpleNamespace(
|
||||
random=random.Random(seed),
|
||||
options=SimpleNamespace(retro_bow=False),
|
||||
)
|
||||
shuffled_pots = generate_pot_shuffle(world)
|
||||
hole_positions = [
|
||||
(pot.x, pot.y)
|
||||
for pot in shuffled_pots[206]
|
||||
if pot.item == POT_HOLE
|
||||
]
|
||||
|
||||
self.assertEqual(hole_positions, [(204, 11)])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -739,9 +739,6 @@ group_table: Dict[str, Set[str]] = {
|
||||
"Broken Left Eye of the Traitor"}
|
||||
}
|
||||
|
||||
# Because each item is only in a single group, a reverse lookup table from each item to its group can be created.
|
||||
group_table_reverse: Dict[str, str] = {item: group for group, items in group_table.items() for item in items}
|
||||
|
||||
tears_list: List[str] = [
|
||||
"Tears of Atonement (500)",
|
||||
"Tears of Atonement (625)",
|
||||
|
||||
+262
-377
@@ -1,6 +1,5 @@
|
||||
from typing import Dict, List, Tuple, Any, Callable, TYPE_CHECKING, Mapping
|
||||
from typing import Dict, List, Tuple, Any, Callable, TYPE_CHECKING
|
||||
from BaseClasses import CollectionState
|
||||
from worlds.generic.Rules import CollectionRule
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import BlasphemousWorld
|
||||
@@ -8,145 +7,30 @@ else:
|
||||
BlasphemousWorld = object
|
||||
|
||||
|
||||
# Depending on a player's options, some logic can either always be True, or always be False.
|
||||
# When combining rules together in load_rule(), optimizations can be made by checking whether a rule being combined is
|
||||
# _always or _never.
|
||||
def _always(state: CollectionState):
|
||||
return True
|
||||
|
||||
|
||||
def _never(state: CollectionState):
|
||||
return False
|
||||
|
||||
|
||||
def _bool_rule(b) -> CollectionRule:
|
||||
"""Small helper to return the appropriate rule function for a rule that can be pre-calculated"""
|
||||
if b:
|
||||
return _always
|
||||
else:
|
||||
return _never
|
||||
|
||||
|
||||
# Player strengths required to logically beat bosses.
|
||||
# Mapping is an immutable type, so type hints should warn if attempts are made to modify it.
|
||||
BOSS_STRENGTHS: Mapping[str, float] = {
|
||||
"warden": -0.10,
|
||||
"ten-piedad": 0.05,
|
||||
"charred-visage": 0.20,
|
||||
"tres-angustias": 0.15,
|
||||
"esdras": 0.25,
|
||||
"melquiades": 0.25,
|
||||
"exposito": 0.30,
|
||||
"quirce": 0.35,
|
||||
"crisanta": 0.50,
|
||||
"isidora": 0.70,
|
||||
"sierpes": 0.70,
|
||||
"amanecida": 0.60,
|
||||
"laudes": 0.60,
|
||||
"perpetua": -0.05,
|
||||
"legionary": 0.20
|
||||
}
|
||||
|
||||
|
||||
class BlasRules:
|
||||
player: int
|
||||
world: BlasphemousWorld
|
||||
string_rules: Dict[str, Callable[[CollectionState], bool]]
|
||||
|
||||
upwarp_skips_allowed: bool
|
||||
mourning_skip_allowed: bool
|
||||
enemy_skips_allowed: bool
|
||||
obscure_skips_allowed: bool
|
||||
precise_skips_allowed: bool
|
||||
can_enemy_bounce: bool
|
||||
|
||||
# Player strengths required to logically beat bosses, adjusted by the player's difficulty option.
|
||||
boss_strengths: Mapping[str, float]
|
||||
|
||||
can_enemy_upslash: CollectionRule
|
||||
can_air_stall: CollectionRule
|
||||
can_dawn_jump: CollectionRule
|
||||
can_dive_laser: CollectionRule
|
||||
can_survive_poison_1: CollectionRule
|
||||
can_survive_poison_2: CollectionRule
|
||||
can_survive_poison_3: CollectionRule
|
||||
|
||||
def __init__(self, world: "BlasphemousWorld") -> None:
|
||||
self.player = world.player
|
||||
self.world = world
|
||||
self.multiworld = world.multiworld
|
||||
self.indirect_conditions: List[Tuple[str, str]] = []
|
||||
|
||||
difficulty = world.options.difficulty.value
|
||||
|
||||
# Rules that can be fully or partially pre-calculated based on world.options.
|
||||
|
||||
# Special Skips
|
||||
self.upwarp_skips_allowed = difficulty >= 2
|
||||
self.mourning_skip_allowed = difficulty >= 2
|
||||
self.enemy_skips_allowed = difficulty >= 2 and not world.options.enemy_randomizer.value
|
||||
self.obscure_skips_allowed = difficulty >= 2
|
||||
self.precise_skips_allowed = difficulty >= 2
|
||||
|
||||
if difficulty >= 2:
|
||||
# Beating bosses ends up in logic earlier.
|
||||
self.boss_strengths = {boss: strength - 0.1 for boss, strength in BOSS_STRENGTHS.items()}
|
||||
elif difficulty >= 1:
|
||||
self.boss_strengths = BOSS_STRENGTHS
|
||||
else:
|
||||
# Beating bosses ends up in logic later.
|
||||
self.boss_strengths = {boss: strength + 0.1 for boss, strength in BOSS_STRENGTHS.items()}
|
||||
|
||||
# Enemy tech
|
||||
if self.enemy_skips_allowed:
|
||||
self.can_enemy_bounce = True
|
||||
self.can_enemy_upslash = lambda state: self.combo(state) >= 2
|
||||
else:
|
||||
self.can_enemy_bounce = False
|
||||
self.can_enemy_upslash = _never
|
||||
|
||||
# Movement tech
|
||||
if difficulty >= 1:
|
||||
self.can_air_stall = self.ranged
|
||||
self.can_dawn_jump = lambda state: self.dawn_heart(state) and self.dash(state)
|
||||
else:
|
||||
self.can_air_stall = _never
|
||||
self.can_dawn_jump = _never
|
||||
|
||||
# Breakable tech
|
||||
if difficulty >= 2:
|
||||
self.can_dive_laser = lambda state: self.dive(state) >= 3
|
||||
else:
|
||||
self.can_dive_laser = _never
|
||||
|
||||
# Lung tech
|
||||
if difficulty >= 2:
|
||||
self.can_survive_poison_1 = _always
|
||||
self.can_survive_poison_2 = lambda state: self.lung(state) or self.tiento(state)
|
||||
self.can_survive_poison_3 = lambda state: self.lung(state) or (self.tiento(state)
|
||||
and self.total_fervour(state) >= 120)
|
||||
elif difficulty >= 1:
|
||||
self.can_survive_poison_1 = lambda state: self.lung(state) or self.tiento(state)
|
||||
self.can_survive_poison_2 = lambda state: self.lung(state) or self.tiento(state)
|
||||
self.can_survive_poison_3 = self.lung
|
||||
else:
|
||||
self.can_survive_poison_1 = self.lung
|
||||
self.can_survive_poison_2 = self.lung
|
||||
self.can_survive_poison_3 = self.lung
|
||||
|
||||
|
||||
# BrandenEK/Blasphemous.Randomizer/ItemRando/BlasphemousInventory.cs
|
||||
self.string_rules: dict[str, CollectionRule] = {
|
||||
self.string_rules = {
|
||||
# Visibility flags
|
||||
"DoubleJump": _bool_rule(self.world.options.purified_hand.value),
|
||||
"NormalLogic": _bool_rule(self.world.options.difficulty.value >= 1),
|
||||
"NormalLogicAndDoubleJump": _bool_rule(self.world.options.difficulty.value >= 1
|
||||
and bool(self.world.options.purified_hand.value)),
|
||||
"HardLogic": _bool_rule(self.world.options.difficulty.value >= 2),
|
||||
"HardLogicAndDoubleJump": _bool_rule(self.world.options.difficulty.value >= 2
|
||||
and bool(self.world.options.purified_hand.value)),
|
||||
"EnemySkips": _bool_rule(self.enemy_skips_allowed),
|
||||
"EnemySkipsAndDoubleJump": _bool_rule(self.enemy_skips_allowed and self.world.options.purified_hand.value),
|
||||
"DoubleJump": lambda state: bool(self.world.options.purified_hand),
|
||||
"NormalLogic": lambda state: self.world.options.difficulty >= 1,
|
||||
"NormalLogicAndDoubleJump": lambda state: self.world.options.difficulty >= 1 \
|
||||
and bool(self.world.options.purified_hand),
|
||||
"HardLogic": lambda state: self.world.options.difficulty >= 2,
|
||||
"HardLogicAndDoubleJump": lambda state: self.world.options.difficulty >= 2 \
|
||||
and bool(self.world.options.purified_hand),
|
||||
"EnemySkips": self.enemy_skips_allowed,
|
||||
"EnemySkipsAndDoubleJump": lambda state: self.enemy_skips_allowed(state) \
|
||||
and bool(self.world.options.purified_hand),
|
||||
|
||||
# Relics
|
||||
"blood": self.blood,
|
||||
@@ -168,20 +52,20 @@ class BlasRules:
|
||||
"cherubs20": lambda state: self.cherubs(state) >= 20,
|
||||
"cherubs38": lambda state: self.cherubs(state) >= 38,
|
||||
|
||||
"bones4": lambda state: self.bones(state, 4),
|
||||
"bones8": lambda state: self.bones(state, 8),
|
||||
"bones12": lambda state: self.bones(state, 12),
|
||||
"bones16": lambda state: self.bones(state, 16),
|
||||
"bones20": lambda state: self.bones(state, 20),
|
||||
"bones24": lambda state: self.bones(state, 24),
|
||||
"bones28": lambda state: self.bones(state, 28),
|
||||
"bones30": lambda state: self.bones(state, 30),
|
||||
"bones32": lambda state: self.bones(state, 32),
|
||||
"bones36": lambda state: self.bones(state, 36),
|
||||
"bones40": lambda state: self.bones(state, 40),
|
||||
"bones44": lambda state: self.bones(state, 44),
|
||||
"bones4": lambda state: self.bones(state) >= 4,
|
||||
"bones8": lambda state: self.bones(state) >= 8,
|
||||
"bones12": lambda state: self.bones(state) >= 12,
|
||||
"bones16": lambda state: self.bones(state) >= 16,
|
||||
"bones20": lambda state: self.bones(state) >= 20,
|
||||
"bones24": lambda state: self.bones(state) >= 24,
|
||||
"bones28": lambda state: self.bones(state) >= 28,
|
||||
"bones30": lambda state: self.bones(state) >= 30,
|
||||
"bones32": lambda state: self.bones(state) >= 32,
|
||||
"bones36": lambda state: self.bones(state) >= 36,
|
||||
"bones40": lambda state: self.bones(state) >= 40,
|
||||
"bones44": lambda state: self.bones(state) >= 44,
|
||||
|
||||
"tears0": _always,
|
||||
"tears0": lambda state: True,
|
||||
|
||||
# Special items
|
||||
"dash": self.dash,
|
||||
@@ -234,13 +118,13 @@ class BlasRules:
|
||||
# skip "dive"
|
||||
# skip "lunge"
|
||||
"chargeBeam": self.charge_beam,
|
||||
"rangedAttack": self.ranged,
|
||||
"rangedAttack": lambda state: self.ranged(state) > 0,
|
||||
|
||||
# Main quest
|
||||
"holyWounds3": lambda state: self.holy_wounds(state, 3),
|
||||
"masks1": lambda state: self.masks(state, 1),
|
||||
"masks2": lambda state: self.masks(state, 2),
|
||||
"masks3": lambda state: self.masks(state, 3),
|
||||
"holyWounds3": lambda state: self.holy_wounds(state) >= 3,
|
||||
"masks1": lambda state: self.masks(state) >= 1,
|
||||
"masks2": lambda state: self.masks(state) >= 2,
|
||||
"masks3": lambda state: self.masks(state) >= 3,
|
||||
"guiltBead": self.guilt_bead,
|
||||
|
||||
# LOTL quest
|
||||
@@ -249,17 +133,17 @@ class BlasRules:
|
||||
"hatchedEgg": self.hatched_egg,
|
||||
|
||||
# Tirso quest
|
||||
"herbs1": lambda state: self.herbs(state, 1),
|
||||
"herbs2": lambda state: self.herbs(state, 2),
|
||||
"herbs3": lambda state: self.herbs(state, 3),
|
||||
"herbs4": lambda state: self.herbs(state, 4),
|
||||
"herbs5": lambda state: self.herbs(state, 5),
|
||||
"herbs6": lambda state: self.herbs(state, 6),
|
||||
"herbs1": lambda state: self.herbs(state) >= 1,
|
||||
"herbs2": lambda state: self.herbs(state) >= 2,
|
||||
"herbs3": lambda state: self.herbs(state) >= 3,
|
||||
"herbs4": lambda state: self.herbs(state) >= 4,
|
||||
"herbs5": lambda state: self.herbs(state) >= 5,
|
||||
"herbs6": lambda state: self.herbs(state) >= 6,
|
||||
|
||||
# Tentudia quest
|
||||
"tentudiaRemains1": lambda state: self.tentudia_remains(state, 1),
|
||||
"tentudiaRemains2": lambda state: self.tentudia_remains(state, 2),
|
||||
"tentudiaRemains3": lambda state: self.tentudia_remains(state, 3),
|
||||
"tentudiaRemains1": lambda state: self.tentudia_remains(state) >= 1,
|
||||
"tentudiaRemains2": lambda state: self.tentudia_remains(state) >= 2,
|
||||
"tentudiaRemains3": lambda state: self.tentudia_remains(state) >= 3,
|
||||
|
||||
# Gemino quest
|
||||
"emptyThimble": self.empty_thimble,
|
||||
@@ -267,7 +151,7 @@ class BlasRules:
|
||||
"driedFlowers": self.dried_flowers,
|
||||
|
||||
# Altasgracias quest
|
||||
"ceremonyItems3": lambda state: self.ceremony_items(state, 3),
|
||||
"ceremonyItems3": lambda state: self.ceremony_items(state) >= 3,
|
||||
"egg": self.egg,
|
||||
|
||||
# Redento quest
|
||||
@@ -275,13 +159,13 @@ class BlasRules:
|
||||
# skip "knots", not actually used
|
||||
|
||||
# Cleofas quest
|
||||
"marksOfRefuge3": lambda state: self.marks_of_refuge(state, 3),
|
||||
"marksOfRefuge3": lambda state: self.marks_of_refuge(state) >= 3,
|
||||
"cord": self.cord,
|
||||
|
||||
# Crisanta quest
|
||||
"scapular": self.scapular,
|
||||
"trueHeart": self.true_heart,
|
||||
"traitorEyes2": lambda state: self.traitor_eyes(state, 2),
|
||||
"traitorEyes2": lambda state: self.traitor_eyes(state) >= 2,
|
||||
|
||||
# Jibrael quest
|
||||
"bell": self.bell,
|
||||
@@ -306,32 +190,32 @@ class BlasRules:
|
||||
"canSurvivePoison3": self.can_survive_poison_3,
|
||||
|
||||
# Enemy tech
|
||||
"canEnemyBounce": _bool_rule(self.can_enemy_bounce),
|
||||
"canEnemyBounce": self.can_enemy_bounce,
|
||||
"canEnemyUpslash": self.can_enemy_upslash,
|
||||
|
||||
# Reaching rooms
|
||||
"guiltRooms1": lambda state: self.guilt_rooms(state, 1),
|
||||
"guiltRooms2": lambda state: self.guilt_rooms(state, 2),
|
||||
"guiltRooms3": lambda state: self.guilt_rooms(state, 3),
|
||||
"guiltRooms4": lambda state: self.guilt_rooms(state, 4),
|
||||
"guiltRooms5": lambda state: self.guilt_rooms(state, 5),
|
||||
"guiltRooms6": lambda state: self.guilt_rooms(state, 6),
|
||||
"guiltRooms7": lambda state: self.guilt_rooms(state, 7),
|
||||
"guiltRooms1": lambda state: self.guilt_rooms(state) >= 1,
|
||||
"guiltRooms2": lambda state: self.guilt_rooms(state) >= 2,
|
||||
"guiltRooms3": lambda state: self.guilt_rooms(state) >= 3,
|
||||
"guiltRooms4": lambda state: self.guilt_rooms(state) >= 4,
|
||||
"guiltRooms5": lambda state: self.guilt_rooms(state) >= 5,
|
||||
"guiltRooms6": lambda state: self.guilt_rooms(state) >= 6,
|
||||
"guiltRooms7": lambda state: self.guilt_rooms(state) >= 7,
|
||||
|
||||
"swordRooms1": lambda state: self.sword_rooms(state, 1),
|
||||
"swordRooms2": lambda state: self.sword_rooms(state, 2),
|
||||
"swordRooms3": lambda state: self.sword_rooms(state, 3),
|
||||
"swordRooms4": lambda state: self.sword_rooms(state, 4),
|
||||
"swordRooms5": lambda state: self.sword_rooms(state, 5),
|
||||
"swordRooms6": lambda state: self.sword_rooms(state, 6),
|
||||
"swordRooms7": lambda state: self.sword_rooms(state, 7),
|
||||
"swordRooms1": lambda state: self.sword_rooms(state) >= 1,
|
||||
"swordRooms2": lambda state: self.sword_rooms(state) >= 2,
|
||||
"swordRooms3": lambda state: self.sword_rooms(state) >= 3,
|
||||
"swordRooms4": lambda state: self.sword_rooms(state) >= 4,
|
||||
"swordRooms5": lambda state: self.sword_rooms(state) >= 5,
|
||||
"swordRooms6": lambda state: self.sword_rooms(state) >= 6,
|
||||
"swordRooms7": lambda state: self.sword_rooms(state) >= 7,
|
||||
|
||||
"redentoRooms2": lambda state: self.redento_rooms(state, 2),
|
||||
"redentoRooms3": lambda state: self.redento_rooms(state, 3),
|
||||
"redentoRooms4": lambda state: self.redento_rooms(state, 4),
|
||||
"redentoRooms5": lambda state: self.redento_rooms(state, 5),
|
||||
"redentoRooms2": lambda state: self.redento_rooms(state) >= 2,
|
||||
"redentoRooms3": lambda state: self.redento_rooms(state) >= 3,
|
||||
"redentoRooms4": lambda state: self.redento_rooms(state) >= 4,
|
||||
"redentoRooms5": lambda state: self.redento_rooms(state) >= 5,
|
||||
|
||||
"miriamRooms5": self.all_miriam_rooms,
|
||||
"miriamRooms5": lambda state: self.miriam_rooms(state) >= 5,
|
||||
|
||||
"amanecidaRooms1": lambda state: self.amanecida_rooms(state) >= 1,
|
||||
"amanecidaRooms2": lambda state: self.amanecida_rooms(state) >= 2,
|
||||
@@ -370,11 +254,11 @@ class BlasRules:
|
||||
"openedBotSSLadder": self.opened_botss_ladder,
|
||||
|
||||
# Special skips
|
||||
"upwarpSkipsAllowed": _bool_rule(self.upwarp_skips_allowed),
|
||||
"mourningSkipAllowed": _bool_rule(self.mourning_skip_allowed),
|
||||
"enemySkipsAllowed": _bool_rule(self.enemy_skips_allowed),
|
||||
"obscureSkipsAllowed": _bool_rule(self.obscure_skips_allowed),
|
||||
"preciseSkipsAllowed": _bool_rule(self.precise_skips_allowed),
|
||||
"upwarpSkipsAllowed": self.upwarp_skips_allowed,
|
||||
"mourningSkipAllowed": self.mourning_skip_allowed,
|
||||
"enemySkipsAllowed": self.enemy_skips_allowed,
|
||||
"obscureSkipsAllowed": self.obscure_skips_allowed,
|
||||
"preciseSkipsAllowed": self.precise_skips_allowed,
|
||||
|
||||
# Bosses
|
||||
"canBeatBrotherhoodBoss": self.can_beat_brotherhood_boss,
|
||||
@@ -614,74 +498,30 @@ class BlasRules:
|
||||
|
||||
def load_rule(self, obj_is_region: bool, name: str, obj: Dict[str, Any]) -> Callable[[CollectionState], bool]:
|
||||
clauses = []
|
||||
clauses_are_impossible_if_empty = False
|
||||
rule_indirect_conditions = []
|
||||
for clause in obj["logic"]:
|
||||
reqs = []
|
||||
clause_indirect_conditions = []
|
||||
clause_is_impossible = False
|
||||
for req in clause["item_requirements"]:
|
||||
if self.req_is_region(req):
|
||||
if obj_is_region:
|
||||
# add to indirect conditions if object and requirement are doors
|
||||
clause_indirect_conditions.append((req, f"{name} -> {obj['target']}"))
|
||||
self.indirect_conditions.append((req, f"{name} -> {obj['target']}"))
|
||||
reqs.append(lambda state, req=req: state.can_reach_region(req, self.player))
|
||||
else:
|
||||
string_rule = self.string_rules[req]
|
||||
if string_rule is _never:
|
||||
# This clause is not possible with the options this player has chosen.
|
||||
clause_is_impossible = True
|
||||
break
|
||||
elif string_rule is _always:
|
||||
# Don't need to add a rule that is always True with the options this player has chosen.
|
||||
# Continue to the next requirement.
|
||||
continue
|
||||
if obj_is_region and req in self.indirect_regions:
|
||||
# add to indirect conditions if object is door and requirement has list of regions
|
||||
for region in self.indirect_regions[req]:
|
||||
clause_indirect_conditions.append((region, f"{name} -> {obj['target']}"))
|
||||
self.indirect_conditions.append((region, f"{name} -> {obj['target']}"))
|
||||
reqs.append(self.string_rules[req])
|
||||
if clause_is_impossible:
|
||||
# At least one clause was impossible, so if all clauses were impossible, the entire rule is impossible.
|
||||
clauses_are_impossible_if_empty = True
|
||||
# Continue to the next clause.
|
||||
continue
|
||||
rule_indirect_conditions.extend(clause_indirect_conditions)
|
||||
|
||||
# Combine the requirements if there are multiple.
|
||||
# Requirements are AND-ed together.
|
||||
if len(reqs) == 1:
|
||||
clauses.append(reqs[0])
|
||||
else:
|
||||
def req_func(state, reqs=reqs):
|
||||
for req in reqs:
|
||||
if not req(state):
|
||||
return False
|
||||
return True
|
||||
clauses.append(req_func)
|
||||
|
||||
# Combine the clauses if there are multiple.
|
||||
# Clauses are OR-ed together.
|
||||
clauses.append(lambda state, reqs=reqs: all(req(state) for req in reqs))
|
||||
if not clauses:
|
||||
# There is no need to register the indirect conditions if it turns out the rule is impossible or always
|
||||
# possible.
|
||||
rule_indirect_conditions.clear()
|
||||
if clauses_are_impossible_if_empty:
|
||||
to_return = _never
|
||||
else:
|
||||
to_return = _always
|
||||
return lambda state: True
|
||||
elif len(clauses) == 1:
|
||||
to_return = clauses[0]
|
||||
return clauses[0]
|
||||
else:
|
||||
def clause_func(state, clauses=clauses):
|
||||
for clause in clauses:
|
||||
if clause(state):
|
||||
return True
|
||||
return False
|
||||
to_return = clause_func
|
||||
# Update the list of indirect conditions to add.
|
||||
self.indirect_conditions.extend(rule_indirect_conditions)
|
||||
return to_return
|
||||
return lambda state: any(clause(state) for clause in clauses)
|
||||
|
||||
# Relics
|
||||
def blood(self, state: CollectionState) -> bool:
|
||||
@@ -725,10 +565,8 @@ class BlasRules:
|
||||
def cherubs(self, state: CollectionState) -> int:
|
||||
return state.count("Child of Moonlight", self.player)
|
||||
|
||||
def bones(self, state: CollectionState, count: int) -> bool:
|
||||
# Count of unique items in the "bones" item group that have been collected into state.
|
||||
# BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed.
|
||||
return state.has("bones", self.player, count)
|
||||
def bones(self, state: CollectionState) -> int:
|
||||
return state.count_group_unique("bones", self.player)
|
||||
|
||||
# def tears():
|
||||
|
||||
@@ -756,7 +594,7 @@ class BlasRules:
|
||||
|
||||
# Health boosts
|
||||
def flasks(self, state: CollectionState) -> int:
|
||||
doors = (
|
||||
doors = {
|
||||
"D01Z05S05[SW]",
|
||||
"D02Z02S04[W]",
|
||||
"D03Z02S08[W]",
|
||||
@@ -764,11 +602,10 @@ class BlasRules:
|
||||
"D04Z02S13[W]",
|
||||
"D05Z01S08[NW]",
|
||||
"D20Z01S07[NE]"
|
||||
)
|
||||
for door in doors:
|
||||
if state.can_reach_region(door, self.player):
|
||||
return state.count("Empty Bile Vessel", self.player)
|
||||
return 0
|
||||
}
|
||||
|
||||
return state.count("Empty Bile Vessel", self.player) \
|
||||
if sum(state.can_reach_region(door, self.player) for door in doors) >= 1 else 0
|
||||
|
||||
def quicksilver(self, state: CollectionState) -> int:
|
||||
return state.count("Quicksilver", self.player) if state.can_reach_region("D01Z05S01[W]", self.player) else 0
|
||||
@@ -776,7 +613,7 @@ class BlasRules:
|
||||
# Puzzles
|
||||
def red_wax(self, state: CollectionState) -> int:
|
||||
return state.count("Bead of Red Wax", self.player)
|
||||
|
||||
|
||||
def blue_wax(self, state: CollectionState) -> int:
|
||||
return state.count("Bead of Blue Wax", self.player)
|
||||
|
||||
@@ -833,7 +670,7 @@ class BlasRules:
|
||||
or self.cante(state)
|
||||
or self.cantina(state)
|
||||
or self.tiento(state)
|
||||
or state.has_any((
|
||||
or state.has_any({
|
||||
"Campanillero to the Sons of the Aurora",
|
||||
"Mirabras of the Return to Port",
|
||||
"Romance to the Crimson Mist",
|
||||
@@ -841,7 +678,7 @@ class BlasRules:
|
||||
"Seguiriya to your Eyes like Stars",
|
||||
"Verdiales of the Forsaken Hamlet",
|
||||
"Zambra to the Resplendent Crown"
|
||||
), self.player)
|
||||
}, self.player)
|
||||
)
|
||||
|
||||
def pillar(self, state: CollectionState) -> bool:
|
||||
@@ -873,8 +710,8 @@ class BlasRules:
|
||||
def charged(self, state: CollectionState) -> int:
|
||||
return state.count("Charged Skill", self.player)
|
||||
|
||||
def ranged(self, state: CollectionState) -> bool:
|
||||
return state.has("Ranged Skill", self.player)
|
||||
def ranged(self, state: CollectionState) -> int:
|
||||
return state.count("Ranged Skill", self.player)
|
||||
|
||||
def dive(self, state: CollectionState) -> int:
|
||||
return state.count("Dive Skill", self.player)
|
||||
@@ -886,15 +723,11 @@ class BlasRules:
|
||||
return self.charged(state) >= 3
|
||||
|
||||
# Main quest
|
||||
def holy_wounds(self, state: CollectionState, count: int) -> bool:
|
||||
# Count of unique items in the "wounds" item group that have been collected into state.
|
||||
# BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed.
|
||||
return state.has("wounds", self.player, count)
|
||||
def holy_wounds(self, state: CollectionState) -> int:
|
||||
return state.count_group_unique("wounds", self.player)
|
||||
|
||||
def masks(self, state: CollectionState, count: int) -> bool:
|
||||
# Count of unique items in the "masks" item group that have been collected into state.
|
||||
# BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed.
|
||||
return state.has("masks", self.player, count)
|
||||
def masks(self, state: CollectionState) -> int:
|
||||
return state.count_group_unique("masks", self.player)
|
||||
|
||||
def guilt_bead(self, state: CollectionState) -> bool:
|
||||
return state.has("Weight of True Guilt", self.player)
|
||||
@@ -910,16 +743,12 @@ class BlasRules:
|
||||
return state.has("Hatched Egg of Deformity", self.player)
|
||||
|
||||
# Tirso quest
|
||||
def herbs(self, state: CollectionState, count: int) -> bool:
|
||||
# Count of unique items in the "tirso" item group that have been collected into state.
|
||||
# BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed.
|
||||
return state.has("tirso", self.player, count)
|
||||
def herbs(self, state: CollectionState) -> int:
|
||||
return state.count_group_unique("tirso", self.player)
|
||||
|
||||
# Tentudia quest
|
||||
def tentudia_remains(self, state: CollectionState, count: int) -> bool:
|
||||
# Count of unique items in the "tentudia" item group that have been collected into state.
|
||||
# BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed.
|
||||
return state.has("tentudia", self.player, count)
|
||||
def tentudia_remains(self, state: CollectionState) -> int:
|
||||
return state.count_group_unique("tentudia", self.player)
|
||||
|
||||
# Gemino quest
|
||||
def empty_thimble(self, state: CollectionState) -> bool:
|
||||
@@ -932,29 +761,23 @@ class BlasRules:
|
||||
return state.has("Dried Flowers bathed in Tears", self.player)
|
||||
|
||||
# Altasgracias quest
|
||||
def ceremony_items(self, state: CollectionState, count: int) -> bool:
|
||||
# Count of unique items in the "egg" item group that have been collected into state.
|
||||
# BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed.
|
||||
return state.has("egg", self.player, count)
|
||||
def ceremony_items(self, state: CollectionState) -> int:
|
||||
return state.count_group_unique("egg", self.player)
|
||||
|
||||
def egg(self, state: CollectionState) -> bool:
|
||||
return state.has("Egg of Deformity", self.player)
|
||||
|
||||
# Redento quest
|
||||
def limestones(self, state: CollectionState, count: int) -> bool:
|
||||
# Count of unique items in the "toe" item group that have been collected into state.
|
||||
# BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed.
|
||||
return state.has("toe", self.player, count)
|
||||
def limestones(self, state: CollectionState) -> int:
|
||||
return state.count_group_unique("toe", self.player)
|
||||
|
||||
def knots(self, state: CollectionState) -> int:
|
||||
return state.count("Knot of Rosary Rope", self.player) if state.can_reach_region("D17Z01S07[NW]", self.player)\
|
||||
else 0
|
||||
|
||||
# Cleofas quest
|
||||
def marks_of_refuge(self, state: CollectionState, count: int) -> bool:
|
||||
# Count of unique items in the "marks" item group that have been collected into state.
|
||||
# BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed.
|
||||
return state.has("marks", self.player, count)
|
||||
def marks_of_refuge(self, state: CollectionState) -> int:
|
||||
return state.count_group_unique("marks", self.player)
|
||||
|
||||
def cord(self, state: CollectionState) -> bool:
|
||||
return state.has("Cord of the True Burying", self.player)
|
||||
@@ -966,10 +789,8 @@ class BlasRules:
|
||||
def true_heart(self, state: CollectionState) -> bool:
|
||||
return state.has("Apodictic Heart of Mea Culpa", self.player)
|
||||
|
||||
def traitor_eyes(self, state: CollectionState, count: int) -> bool:
|
||||
# Count of unique items in the "eye" item group that have been collected into state.
|
||||
# BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed.
|
||||
return state.has("eye", self.player, count)
|
||||
def traitor_eyes(self, state: CollectionState) -> int:
|
||||
return state.count_group_unique("eye", self.player)
|
||||
|
||||
# Jibrael quest
|
||||
def bell(self, state: CollectionState) -> bool:
|
||||
@@ -979,6 +800,19 @@ class BlasRules:
|
||||
return state.count("Verses Spun from Gold", self.player)
|
||||
|
||||
# Movement tech
|
||||
def can_air_stall(self, state: CollectionState) -> bool:
|
||||
return (
|
||||
self.ranged(state) > 0
|
||||
and self.world.options.difficulty >= 1
|
||||
)
|
||||
|
||||
def can_dawn_jump(self, state: CollectionState) -> bool:
|
||||
return (
|
||||
self.dawn_heart(state)
|
||||
and self.dash(state)
|
||||
and self.world.options.difficulty >= 1
|
||||
)
|
||||
|
||||
def can_water_jump(self, state: CollectionState) -> bool:
|
||||
return (
|
||||
self.nail(state)
|
||||
@@ -994,6 +828,12 @@ class BlasRules:
|
||||
or self.can_use_any_prayer(state)
|
||||
)
|
||||
|
||||
def can_dive_laser(self, state: CollectionState) -> bool:
|
||||
return (
|
||||
self.dive(state) >= 3
|
||||
and self.world.options.difficulty >= 2
|
||||
)
|
||||
|
||||
# Root tech
|
||||
def can_walk_on_root(self, state: CollectionState) -> bool:
|
||||
return self.root(state)
|
||||
@@ -1004,6 +844,40 @@ class BlasRules:
|
||||
and self.wall_climb(state)
|
||||
)
|
||||
|
||||
# Lung tech
|
||||
def can_survive_poison_1(self, state: CollectionState) -> bool:
|
||||
return (
|
||||
self.lung(state)
|
||||
or self.world.options.difficulty >= 1
|
||||
and self.tiento(state)
|
||||
or self.world.options.difficulty >= 2
|
||||
)
|
||||
|
||||
def can_survive_poison_2(self, state: CollectionState) -> bool:
|
||||
return (
|
||||
self.lung(state)
|
||||
or self.world.options.difficulty >= 1
|
||||
and self.tiento(state)
|
||||
)
|
||||
|
||||
def can_survive_poison_3(self, state: CollectionState) -> bool:
|
||||
return (
|
||||
self.lung(state)
|
||||
or self.world.options.difficulty >= 2
|
||||
and self.tiento(state)
|
||||
and self.total_fervour(state) >= 120
|
||||
)
|
||||
|
||||
# Enemy tech
|
||||
def can_enemy_bounce(self, state: CollectionState) -> bool:
|
||||
return self.enemy_skips_allowed(state)
|
||||
|
||||
def can_enemy_upslash(self, state: CollectionState) -> bool:
|
||||
return (
|
||||
self.combo(state) >= 2
|
||||
and self.enemy_skips_allowed(state)
|
||||
)
|
||||
|
||||
# Crossing gaps
|
||||
def can_cross_gap_1(self, state: CollectionState) -> bool:
|
||||
return (
|
||||
@@ -1147,7 +1021,7 @@ class BlasRules:
|
||||
or state.can_reach_region("D03Z02S03[E]", self.player)
|
||||
and (
|
||||
self.can_cross_gap_5(state)
|
||||
or self.can_enemy_bounce
|
||||
or self.can_enemy_bounce(state)
|
||||
and self.can_cross_gap_3(state)
|
||||
)
|
||||
)
|
||||
@@ -1193,6 +1067,25 @@ class BlasRules:
|
||||
or state.can_reach_region("D17BZ02S01[FrontR]", self.player)
|
||||
)
|
||||
|
||||
# Special skips
|
||||
def upwarp_skips_allowed(self, state: CollectionState) -> bool:
|
||||
return self.world.options.difficulty >= 2
|
||||
|
||||
def mourning_skip_allowed(self, state: CollectionState) -> bool:
|
||||
return self.world.options.difficulty >= 2
|
||||
|
||||
def enemy_skips_allowed(self, state: CollectionState) -> bool:
|
||||
return (
|
||||
self.world.options.difficulty >= 2
|
||||
and not self.world.options.enemy_randomizer
|
||||
)
|
||||
|
||||
def obscure_skips_allowed(self, state: CollectionState) -> bool:
|
||||
return self.world.options.difficulty >= 2
|
||||
|
||||
def precise_skips_allowed(self, state: CollectionState) -> bool:
|
||||
return self.world.options.difficulty >= 2
|
||||
|
||||
# Bosses
|
||||
def can_beat_brotherhood_boss(self, state: CollectionState) -> bool:
|
||||
return (
|
||||
@@ -1290,18 +1183,18 @@ class BlasRules:
|
||||
and state.can_reach_region("D20Z02S07[W]", self.player)
|
||||
)
|
||||
|
||||
def can_beat_graveyard_boss(self, state: CollectionState, player_strength: float | None = None) -> bool:
|
||||
def can_beat_graveyard_boss(self, state: CollectionState) -> bool:
|
||||
return (
|
||||
self.has_boss_strength(state, "amanecida", player_strength)
|
||||
self.has_boss_strength(state, "amanecida")
|
||||
and self.wall_climb(state)
|
||||
and state.can_reach_region("D01Z06S01[Santos]", self.player)
|
||||
and state.can_reach_region("D02Z03S18[NW]", self.player)
|
||||
and state.can_reach_region("D02Z02S03[NE]", self.player)
|
||||
)
|
||||
|
||||
def can_beat_jondo_boss(self, state: CollectionState, player_strength: float | None = None) -> bool:
|
||||
def can_beat_jondo_boss(self, state: CollectionState) -> bool:
|
||||
return (
|
||||
self.has_boss_strength(state, "amanecida", player_strength)
|
||||
self.has_boss_strength(state, "amanecida")
|
||||
and state.can_reach_region("D01Z06S01[Santos]", self.player)
|
||||
and (
|
||||
state.can_reach_region("D20Z01S06[NE]", self.player)
|
||||
@@ -1313,9 +1206,9 @@ class BlasRules:
|
||||
)
|
||||
)
|
||||
|
||||
def can_beat_patio_boss(self, state: CollectionState, player_strength: float | None = None) -> bool:
|
||||
def can_beat_patio_boss(self, state: CollectionState) -> bool:
|
||||
return (
|
||||
self.has_boss_strength(state, "amanecida", player_strength)
|
||||
self.has_boss_strength(state, "amanecida")
|
||||
and state.can_reach_region("D01Z06S01[Santos]", self.player)
|
||||
and state.can_reach_region("D06Z01S02[W]", self.player)
|
||||
and (
|
||||
@@ -1325,9 +1218,9 @@ class BlasRules:
|
||||
)
|
||||
)
|
||||
|
||||
def can_beat_wall_boss(self, state: CollectionState, player_strength: float | None = None) -> bool:
|
||||
def can_beat_wall_boss(self, state: CollectionState) -> bool:
|
||||
return (
|
||||
self.has_boss_strength(state, "amanecida", player_strength)
|
||||
self.has_boss_strength(state, "amanecida")
|
||||
and state.can_reach_region("D01Z06S01[Santos]", self.player)
|
||||
and state.can_reach_region("D09Z01S09[Cell24]", self.player)
|
||||
and (
|
||||
@@ -1351,7 +1244,8 @@ class BlasRules:
|
||||
def can_beat_legionary(self, state: CollectionState) -> bool:
|
||||
return self.has_boss_strength(state, "legionary")
|
||||
|
||||
def get_player_strength(self, state: CollectionState) -> float:
|
||||
|
||||
def has_boss_strength(self, state: CollectionState, boss: str) -> bool:
|
||||
life: int = state.count("Life Upgrade", self.player)
|
||||
sword: int = state.count("Mea Culpa Upgrade", self.player)
|
||||
fervour: int = state.count("Fervour Upgrade", self.player)
|
||||
@@ -1365,16 +1259,30 @@ class BlasRules:
|
||||
+ min(8, flasks) * 0.15 / 8
|
||||
+ min(5, quicksilver) * 0.15 / 5
|
||||
)
|
||||
return player_strength
|
||||
|
||||
def has_boss_strength(self, state: CollectionState, boss: str, player_strength: float | None = None) -> bool:
|
||||
if player_strength is None:
|
||||
return self.get_player_strength(state) >= self.boss_strengths[boss]
|
||||
else:
|
||||
return player_strength >= self.boss_strengths[boss]
|
||||
bosses: Dict[str, float] = {
|
||||
"warden": -0.10,
|
||||
"ten-piedad": 0.05,
|
||||
"charred-visage": 0.20,
|
||||
"tres-angustias": 0.15,
|
||||
"esdras": 0.25,
|
||||
"melquiades": 0.25,
|
||||
"exposito": 0.30,
|
||||
"quirce": 0.35,
|
||||
"crisanta": 0.50,
|
||||
"isidora": 0.70,
|
||||
"sierpes": 0.70,
|
||||
"amanecida": 0.60,
|
||||
"laudes": 0.60,
|
||||
"perpetua": -0.05,
|
||||
"legionary": 0.20
|
||||
}
|
||||
boss_strength: float = bosses[boss]
|
||||
return player_strength >= (boss_strength - 0.10 if self.world.options.difficulty >= 2 else
|
||||
(boss_strength if self.world.options.difficulty >= 1 else boss_strength + 0.10))
|
||||
|
||||
def guilt_rooms(self, state: CollectionState, count: int) -> bool:
|
||||
doors = (
|
||||
def guilt_rooms(self, state: CollectionState) -> int:
|
||||
doors = [
|
||||
"D01Z04S01[NE]",
|
||||
"D02Z02S11[W]",
|
||||
"D03Z03S02[NE]",
|
||||
@@ -1382,25 +1290,20 @@ class BlasRules:
|
||||
"D05Z01S05[NE]",
|
||||
"D09Z01S05[W]",
|
||||
"D17Z01S04[W]",
|
||||
)
|
||||
]
|
||||
|
||||
total: int = 0
|
||||
for door in doors:
|
||||
total += state.can_reach_region(door, self.player)
|
||||
if total >= count:
|
||||
return True
|
||||
return False
|
||||
return sum(state.can_reach_region(door, self.player) for door in doors)
|
||||
|
||||
def sword_rooms(self, state: CollectionState, count: int) -> bool:
|
||||
doors = (
|
||||
("D01Z02S07[E]", "D01Z02S02[SW]"),
|
||||
("D20Z01S04[E]", "D01Z05S23[W]"),
|
||||
("D02Z03S02[NE]",),
|
||||
("D04Z02S21[NE]",),
|
||||
("D05Z01S21[NW]",),
|
||||
("D06Z01S15[NE]",),
|
||||
("D17Z01S07[SW]",)
|
||||
)
|
||||
def sword_rooms(self, state: CollectionState) -> int:
|
||||
doors = [
|
||||
["D01Z02S07[E]", "D01Z02S02[SW]"],
|
||||
["D20Z01S04[E]", "D01Z05S23[W]"],
|
||||
["D02Z03S02[NE]"],
|
||||
["D04Z02S21[NE]"],
|
||||
["D05Z01S21[NW]"],
|
||||
["D06Z01S15[NE]"],
|
||||
["D17Z01S07[SW]"]
|
||||
]
|
||||
|
||||
total: int = 0
|
||||
for subdoors in doors:
|
||||
@@ -1408,90 +1311,72 @@ class BlasRules:
|
||||
if state.can_reach_region(door, self.player):
|
||||
total += 1
|
||||
break
|
||||
if total >= count:
|
||||
return True
|
||||
|
||||
return False
|
||||
return total
|
||||
|
||||
def redento_rooms(self, state: CollectionState, count: int) -> bool:
|
||||
if not (
|
||||
state.can_reach_region("D03Z01S04[E]", self.player)
|
||||
or state.can_reach_region("D03Z02S10[N]", self.player)
|
||||
def redento_rooms(self, state: CollectionState) -> int:
|
||||
if (
|
||||
state.can_reach_region("D03Z01S04[E]", self.player)
|
||||
or state.can_reach_region("D03Z02S10[N]", self.player)
|
||||
):
|
||||
# Realistically, count should never be zero or negative.
|
||||
return count < 1
|
||||
|
||||
if count == 1:
|
||||
return True
|
||||
|
||||
if not (
|
||||
if (
|
||||
state.can_reach_region("D17Z01S05[S]", self.player)
|
||||
or state.can_reach_region("D17BZ02S01[FrontR]", self.player)
|
||||
):
|
||||
return False
|
||||
|
||||
if count == 2:
|
||||
return True
|
||||
|
||||
if not (state.can_reach_region("D01Z03S04[E]", self.player)
|
||||
or state.can_reach_region("D08Z01S01[W]", self.player)):
|
||||
return False
|
||||
|
||||
if count == 3:
|
||||
return True
|
||||
|
||||
if not (state.can_reach_region("D04Z01S03[E]", self.player)
|
||||
or state.can_reach_region("D04Z02S01[W]", self.player)
|
||||
or state.can_reach_region("D06Z01S18[-Cherubs]", self.player)):
|
||||
return False
|
||||
|
||||
if count == 4:
|
||||
return True
|
||||
|
||||
if not (
|
||||
self.knots(state) >= 1
|
||||
and self.limestones(state, 3)
|
||||
and (state.can_reach_region("D04Z02S08[E]", self.player)
|
||||
or state.can_reach_region("D04BZ02S01[Redento]", self.player))
|
||||
):
|
||||
return False
|
||||
|
||||
return count == 5
|
||||
|
||||
def all_miriam_rooms(self, state: CollectionState) -> bool:
|
||||
doors = (
|
||||
):
|
||||
if (
|
||||
state.can_reach_region("D01Z03S04[E]", self.player)
|
||||
or state.can_reach_region("D08Z01S01[W]", self.player)
|
||||
):
|
||||
if (
|
||||
state.can_reach_region("D04Z01S03[E]", self.player)
|
||||
or state.can_reach_region("D04Z02S01[W]", self.player)
|
||||
or state.can_reach_region("D06Z01S18[-Cherubs]", self.player)
|
||||
):
|
||||
if (
|
||||
self.knots(state) >= 1
|
||||
and self.limestones(state) >= 3
|
||||
and (
|
||||
state.can_reach_region("D04Z02S08[E]", self.player)
|
||||
or state.can_reach_region("D04BZ02S01[Redento]", self.player)
|
||||
)
|
||||
):
|
||||
return 5
|
||||
return 4
|
||||
return 3
|
||||
return 2
|
||||
return 1
|
||||
return 0
|
||||
|
||||
def miriam_rooms(self, state: CollectionState) -> int:
|
||||
doors = [
|
||||
"D02Z03S07[NWW]",
|
||||
"D03Z03S07[NW]",
|
||||
"D04Z04S01[E]",
|
||||
"D05Z01S06[W]",
|
||||
"D06Z01S17[E]"
|
||||
)
|
||||
]
|
||||
|
||||
for door in doors:
|
||||
if not state.can_reach_region(door, self.player):
|
||||
return False
|
||||
return True
|
||||
return sum(state.can_reach_region(door, self.player) for door in doors)
|
||||
|
||||
def amanecida_rooms(self, state: CollectionState) -> int:
|
||||
player_strength = self.get_player_strength(state)
|
||||
total: int = 0
|
||||
if self.can_beat_graveyard_boss(state, player_strength):
|
||||
if self.can_beat_graveyard_boss(state):
|
||||
total += 1
|
||||
if self.can_beat_jondo_boss(state, player_strength):
|
||||
if self.can_beat_jondo_boss(state):
|
||||
total += 1
|
||||
if self.can_beat_patio_boss(state, player_strength):
|
||||
if self.can_beat_patio_boss(state):
|
||||
total += 1
|
||||
if self.can_beat_wall_boss(state, player_strength):
|
||||
if self.can_beat_wall_boss(state):
|
||||
total += 1
|
||||
|
||||
return total
|
||||
|
||||
def chalice_rooms(self, state: CollectionState) -> int:
|
||||
doors = (
|
||||
("D03Z01S02[E]", "D01Z05S02[W]", "D20Z01S03[N]"),
|
||||
("D05Z01S11[SE]", "D05Z02S02[NW]"),
|
||||
("D09Z01S09[E]", "D09Z01S10[W]", "D09Z01S08[SE]", "D09Z01S02[SW]")
|
||||
)
|
||||
doors = [
|
||||
["D03Z01S02[E]", "D01Z05S02[W]", "D20Z01S03[N]"],
|
||||
["D05Z01S11[SE]", "D05Z02S02[NW]"],
|
||||
["D09Z01S09[E]", "D09Z01S10[W]", "D09Z01S08[SE]", "D09Z01S02[SW]"]
|
||||
]
|
||||
|
||||
total: int = 0
|
||||
for subdoors in doors:
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from typing import Dict, List, Set, Any
|
||||
from collections import Counter
|
||||
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, CollectionState
|
||||
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification
|
||||
from Options import OptionError
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
from .Items import base_id, item_table, group_table, tears_list, reliquary_set, group_table_reverse
|
||||
from .Items import base_id, item_table, group_table, tears_list, reliquary_set
|
||||
from .Locations import location_names
|
||||
from .Rules import BlasRules
|
||||
from worlds.generic.Rules import set_rule
|
||||
@@ -216,27 +216,6 @@ class BlasphemousWorld(World):
|
||||
for loc, item in option_dict.items():
|
||||
self.get_location(loc).place_locked_item(self.create_item(item))
|
||||
|
||||
def collect(self, state: CollectionState, item: Item) -> bool:
|
||||
changed = super().collect(state, item)
|
||||
if changed:
|
||||
name = item.name
|
||||
if name in group_table_reverse and state.count(name, self.player) == 1:
|
||||
# Count was 0 before super().collect().
|
||||
group_name = group_table_reverse[name]
|
||||
# Increase unique count for items in this group.
|
||||
state.prog_items[self.player][group_name] += 1
|
||||
return changed
|
||||
|
||||
def remove(self, state: CollectionState, item: Item) -> bool:
|
||||
changed = super().remove(state, item)
|
||||
if changed:
|
||||
name = item.name
|
||||
if name in group_table_reverse and state.count(name, self.player) == 0:
|
||||
# Count was 1 before super().remove().
|
||||
group_name = group_table_reverse[name]
|
||||
# Decrease unique count for items in this group.
|
||||
state.prog_items[self.player][group_name] -= 1
|
||||
return changed
|
||||
|
||||
def create_regions(self) -> None:
|
||||
multiworld = self.multiworld
|
||||
|
||||
@@ -296,9 +296,11 @@ class ImpatientMimicsOption(Toggle):
|
||||
class RandomEnemyPresetOption(OptionDict):
|
||||
"""The YAML preset for the static enemy randomizer.
|
||||
|
||||
See the online enemy randomization documentation for all available options.
|
||||
See the static randomizer documentation in `randomizer\\presets\\README.txt` for details.
|
||||
Include this as nested YAML. For example:
|
||||
|
||||
.. code-block:: YAML
|
||||
|
||||
random_enemy_preset:
|
||||
RemoveSource: Ancient Wyvern; Darkeater Midir
|
||||
DontRandomize: Iudex Gundyr
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
# Dark Souls III
|
||||
|
||||
Game Page | [Setup] | [Items] | [Locations] | [Enemy Randomization]
|
||||
Game Page | [Items] | [Locations]
|
||||
|
||||
[Setup]: /tutorial/Dark%20Souls%20III/setup/en
|
||||
[Items]: /tutorial/Dark%20Souls%20III/items/en
|
||||
[Locations]: /tutorial/Dark%20Souls%20III/locations/en
|
||||
[Enemy Randomization]: /tutorial/Dark%20Souls%20III/enemy-randomization/en
|
||||
|
||||
## What do I need to do to randomize DS3?
|
||||
|
||||
@@ -140,14 +138,6 @@ Check out the [item guide], which explains the named groups available for items.
|
||||
|
||||
[item guide]: /tutorial/Dark%20Souls%20III/items/en
|
||||
|
||||
## How can I change what enemies get randomized?
|
||||
|
||||
The [enemy randomization guide] explains how to further customize enemy randomization
|
||||
for challenge runs or convenience. You can target specific enemies or entire
|
||||
categories and even remove annoying enemy types outright.
|
||||
|
||||
[enemy randomization guide]: /tutorial/Dark%20Souls%20III/enemy-randomization/en
|
||||
|
||||
## What's new from 2.x.x?
|
||||
|
||||
Version 3.0.0 of the Dark Souls III Archipelago client has a number of
|
||||
|
||||
@@ -1,411 +0,0 @@
|
||||
# Dark Souls III Enemy Randomization
|
||||
|
||||
[Game Page] | [Setup] | [Items] | [Locations] | Enemy Randomization
|
||||
|
||||
[Game Page]: /games/Dark%20Souls%20III/info/en
|
||||
[Setup]: /tutorial/Dark%20Souls%20III/setup/en
|
||||
[Items]: /tutorial/Dark%20Souls%20III/items/en
|
||||
[Locations]: /tutorial/Dark%20Souls%20III/locations/en
|
||||
|
||||
If `randomize_enemies` in your Dark Souls 3 player config YAML is enabled, bosses, minibosses and basic enemies will
|
||||
be shuffled with themselves respectively.
|
||||
|
||||
To further customize enemy randomization beyond that, there is a section called `random_enemy_preset`.
|
||||
|
||||
This tutorial will show all the ways how to configure that preset.
|
||||
|
||||
## Table of Contents
|
||||
- [The Basics](#the-basics)
|
||||
- [Individual Assignments](#individual-assignments)
|
||||
- [Pools](#pools)
|
||||
* [Pool Groups](#pool-groups)
|
||||
+ [RandomByType](#randombytype)
|
||||
* [Weights](#weights)
|
||||
- [Settings](#settings)
|
||||
* [Boss](#boss)
|
||||
* [Miniboss](#miniboss)
|
||||
* [Basic](#basic)
|
||||
+ [BuffBasicEnemiesAsBosses](#buffbasicenemiesasbosses)
|
||||
* [Enemies](#enemies)
|
||||
* [DontRandomize](#dontrandomize)
|
||||
* [RemoveSource](#removesource)
|
||||
* [OopsAll](#oopsall)
|
||||
- [Enemy Categories](#enemy-categories)
|
||||
|
||||
## The Basics
|
||||
|
||||
There are two main ways to assign an enemy to be randomized: [individual enemy assignments](#individual-assignments)
|
||||
to target a singular enemy placement and setting up [Pools](#pools) to target a category of enemies.
|
||||
|
||||
Custom pools are recommended unless you specifically want to single out one enemy placement.
|
||||
|
||||
All bosses also have their own category, so individual assignment is not necessary in those cases.
|
||||
|
||||
Be aware of correct indentation of your YAML file. Every example in this document will need to be nested under the
|
||||
`random_enemy_preset:` section.
|
||||
|
||||
Disable the preset by leaving just empty brackets `{}`. Like usual with YAML, you can add comments by using `#`.
|
||||
|
||||
For further examples, check out the "presets" folder of the standalone randomizer.
|
||||
|
||||
## Individual Assignments
|
||||
|
||||
Individual enemy assignment allows you to target individual enemies, rather than a category as under pools.
|
||||
|
||||
This overrides pools and any other configuration, will usually ignore progression, and can possibly cause you to have to
|
||||
fight Yhorm the Giant without Storm Ruler.
|
||||
|
||||
You use it in the [`Enemies`](#enemies) section by selecting a specific enemy using its unique
|
||||
ID, or its specific name followed by its ID.
|
||||
|
||||
See the '/randomizer/preset/Template.txt' file of the static randomizer for all available IDs.
|
||||
|
||||
There are also some special target names available for individual assignments:
|
||||
|
||||
- `any`: This is the default and allows any enemy in the pool to appear there.
|
||||
|
||||
- `norandom`: Assigns an enemy to itself. This has the same effect as adding the enemy name to [`DontRandomize`](#dontrandomize).
|
||||
|
||||
## Pools
|
||||
|
||||
A pool is a collection of enemies. A pool can both be a randomization target and an eligible group of random enemies to
|
||||
be drawn from for randomization. See [Enemy Categories](#enemy-categories) for all available pools.
|
||||
|
||||
Pool assignment generally respects progression, like requiring Storm Ruler to be accessible before Yhorm the Giant.
|
||||
|
||||
By default, using a boss as another boss, or a miniboss as another miniboss, takes the source enemy out of the default
|
||||
pool for that category, so each enemy will still be used once if possible. However, the enemy can still appear more
|
||||
than once if used in a custom pool.
|
||||
|
||||
### Pool Groups
|
||||
|
||||
Pools can be joined into a pool group by joining several names, separated by a semicolon.
|
||||
|
||||
```yaml
|
||||
# All basic enemies are just different hollows now
|
||||
Basic:
|
||||
- Weight: 100
|
||||
Pool: Hollow Soldiers; Large Hollow Soldiers
|
||||
```
|
||||
|
||||
#### RandomByType
|
||||
|
||||
By default, selection will be random across all eligible enemies. In our example above it would select from:
|
||||
|
||||
- Hollow Soldier
|
||||
- Road of Sacrifices Hollow Soldier
|
||||
- Cathedral Hollow Soldier
|
||||
- Lothric Castle Hollow Soldier
|
||||
- Grand Archives Hollow Soldier
|
||||
|
||||
and
|
||||
|
||||
- Large Hollow Soldier
|
||||
- Cathedral Large Hollow Soldier
|
||||
- Lothric Castle Large Hollow Soldier
|
||||
|
||||
However, this would make it more likely to select a regular soldier instead of a large one (5 out of 8), just because
|
||||
there are fewer entries in the latter category.
|
||||
|
||||
You can specify `RandomByType: true` to select randomly from the list itself (Hollow Soldiers, Large Hollow Soldiers)
|
||||
and make our previous example a true 50/50 split.
|
||||
|
||||
```yaml
|
||||
# All basic enemies are just different hollows now
|
||||
Basic:
|
||||
- Weight: 100
|
||||
Pool: Hollow Soldiers; Large Hollow Soldiers
|
||||
RandomByType: true # To make it truly 50/50 between the categories
|
||||
```
|
||||
|
||||
### Weights
|
||||
|
||||
Weights can be used to select multiple different outcomes within a pool, weighted to give different probabilities each.
|
||||
|
||||
Weights don't necessarily have to add up to 100, but doing it that way makes estimating probabilities very intuitive.
|
||||
|
||||
```yaml
|
||||
Boss:
|
||||
- Weight: 79 # 79% of bosses will still be bosses
|
||||
Pool: default
|
||||
- Weight: 20 # Replace 20% of all bosses with minibosses
|
||||
Pool: Miniboss
|
||||
- Weight: 1 # Replace 1% of all bosses with regular enemies. It's always funny
|
||||
Pool: Basic
|
||||
```
|
||||
|
||||
Be aware that weights will not work in the [`Enemies`](#enemies) section.
|
||||
|
||||
## Settings
|
||||
|
||||
### Boss
|
||||
|
||||
This setting indicates which enemies can be used as replacements for bosses.
|
||||
By default, this is the pool of all 29 bosses.
|
||||
|
||||
```yaml
|
||||
Boss:
|
||||
- Weight: 80
|
||||
Pool: default
|
||||
- Weight: 20 # Replace 20% of all bosses with minibosses
|
||||
Pool: Miniboss
|
||||
```
|
||||
|
||||
### Miniboss
|
||||
|
||||
This setting indicates which enemies can be used as replacements for minibosses.
|
||||
By default, this is the pool of all 32 minibosses (including duplicates).
|
||||
|
||||
```yaml
|
||||
Miniboss:
|
||||
- Weight: 80
|
||||
Pool: default
|
||||
- Weight: 20 # Replace 20% of all minibosses with bosses
|
||||
Pool: Boss
|
||||
```
|
||||
|
||||
### Basic
|
||||
|
||||
This setting indicates which enemies can be used as replacements for all other enemies, so non-bosses and non-minibosses.
|
||||
By default, this is the pool of all ~2000 basic enemies (including duplicates).
|
||||
|
||||
```yaml
|
||||
Basic:
|
||||
- Weight: 94
|
||||
Pool: default
|
||||
- Weight: 5 # Replace 5% of all basic enemies with minibosses
|
||||
Pool: Miniboss
|
||||
- Weight: 1 # Replace 1% of all basic enemies with bosses
|
||||
Pool: Boss
|
||||
```
|
||||
|
||||
#### BuffBasicEnemiesAsBosses
|
||||
|
||||
If enabled, this causes basic enemies to become a lot stronger when randomized into the slot of a boss.
|
||||
|
||||
```yaml
|
||||
Boss:
|
||||
- Weight: 100 # All bosses are just basic enemies...
|
||||
Pool: Basic
|
||||
|
||||
BuffBasicEnemiesAsBosses: true # ...but they are strong
|
||||
```
|
||||
|
||||
### Enemies
|
||||
|
||||
Under the `Enemies:` setting you can add more nuanced replacements of random enemies.
|
||||
There are two ways you can adjust enemies:
|
||||
- Assign to a group of enemies using their category pool (see [Enemy Categories](#enemy-categories))
|
||||
- Assign to one specifc enemy by using its number (see [Individual Assignments](#individual-assignments))
|
||||
|
||||
```yaml
|
||||
Enemies:
|
||||
# Replace only the very first Ravenous Crystal Lizard with the final boss
|
||||
Ravenous Crystal Lizard 4000380: Lords of Cinder
|
||||
|
||||
# Replace all regular soldiers with skeletons or small crabs
|
||||
Hollow Soldiers: Skeletons; Lesser Crab
|
||||
|
||||
# Knights remain knights, but variants (i.e. weapons) are still shuffled within the category
|
||||
High Wall Lothric Knight: High Wall Lothric Knight
|
||||
```
|
||||
|
||||
### DontRandomize
|
||||
|
||||
A semicolon-separated list of enemies or enemy types to not randomize (assign to themselves).
|
||||
It is taken out of its default pool and also custom pools in this case, but it can still be assigned to
|
||||
[individual enemies](#individual-assignments).
|
||||
|
||||
```yaml
|
||||
DontRandomize: Iudex Gundyr # Iudex Gundyr will be at his vanilla location
|
||||
|
||||
Boss:
|
||||
- Weight: 100
|
||||
Pool: default # Boss slots other than Iudex Gundyr will never become him
|
||||
```
|
||||
|
||||
### RemoveSource
|
||||
|
||||
A semicolon-separated list of enemies or enemy types to remove from all pools.
|
||||
It can still be assigned to individual enemies.
|
||||
This is overridden by [`DontRandomize`](#dontrandomize) directives.
|
||||
|
||||
```yaml
|
||||
# Remove the most annoying enemies from all pools
|
||||
RemoveSource: Bridge Darkeater Midir; Ancient Wyvern Mob; Curse-rotted Greatwood; High Lord Wolnir; Carthus Sandworm
|
||||
```
|
||||
|
||||
### OopsAll
|
||||
|
||||
Assigning an enemy or a pool to `OopsAll` sets all pools to that specific enemy or category of enemy. This can still be
|
||||
overridden using [individual enemy assginments](#individual-assignments), but otherwise every enemy is replaced by
|
||||
this setting.
|
||||
|
||||
```yaml
|
||||
# This run suddenly got very spooky
|
||||
OopsAll: Skeletons
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Enemy Categories
|
||||
|
||||
The following enemy category pools are available:
|
||||
|
||||
- Any
|
||||
- Bosses
|
||||
- Minibosses
|
||||
- Bosses and Minibosses
|
||||
- Basic
|
||||
- Abyss Watchers
|
||||
- Aldrich, Devourer of Gods
|
||||
- Ancient Wyvern
|
||||
- Ancient Wyvern Mob
|
||||
- Angel Pilgrim
|
||||
- Basilisk
|
||||
- Black Knight
|
||||
- Blackflame Friede
|
||||
- Boreal Outrider Knight
|
||||
- Bridge Darkeater Midir
|
||||
- Cage Spider
|
||||
- Carthus Sandworm
|
||||
- Cathedral Evangelist
|
||||
- Cathedral Knight
|
||||
- Cemetery Hollow
|
||||
- Champion Gundyr
|
||||
- Champion's Gravetender and Gravetender Greatwolf
|
||||
- Consumed King Oceiros
|
||||
- Corpse-grub
|
||||
- Corvian
|
||||
- Corvian Knight
|
||||
- Corvian Settler
|
||||
- Crabs
|
||||
- Lesser Crab
|
||||
- Greater Crab
|
||||
- Ariandel Greater Crab
|
||||
- Crystal Lizard
|
||||
- Crystal Sage
|
||||
- Crystal Sage in Archives
|
||||
- Curse-rotted Greatwood
|
||||
- Dancer of the Boreal Valley
|
||||
- Darkeater Midir
|
||||
- Darkwraith
|
||||
- Deacon
|
||||
- Cathedral Deacon
|
||||
- Wide Deacon
|
||||
- Irirthyll Deacon
|
||||
- Irirthyll Tall Deacon
|
||||
- Deacons of the Deep
|
||||
- Deep Accursed
|
||||
- Demon
|
||||
- Demon Cleric
|
||||
- Demon Prince
|
||||
- Demonic Statue
|
||||
- Dragonslayer Armour
|
||||
- Dreg Heap Thrall
|
||||
- Elder Ghru
|
||||
- Father Ariandel
|
||||
- Farron Follower
|
||||
- Fire Witch
|
||||
- Gargoyles
|
||||
- Profaned Capital Gargoyle
|
||||
- Archives Gargoyle
|
||||
- Ghru Grunt
|
||||
- Giant Fly
|
||||
- Giant Slave
|
||||
- Grand Archives Scholar
|
||||
- Grave Warden
|
||||
- Halflight, Spear of the Church
|
||||
- Harald Legion Knight
|
||||
- High Lord Wolnir
|
||||
- Hobbled Cleric
|
||||
- Hollow Manservant
|
||||
- Hollow Soldiers
|
||||
- Hollow Soldier
|
||||
- Road of Sacrifices Hollow Soldier
|
||||
- Cathedral Hollow Soldier
|
||||
- Lothric Castle Hollow Soldier
|
||||
- Grand Archives Hollow Soldier
|
||||
- Hound Rat
|
||||
- Infested Corpse
|
||||
- Irirthyll Dungeon Peasant Hollow
|
||||
- Irithyll Giant Slave
|
||||
- Irithyll Starved Hound
|
||||
- Irithyllian Slave
|
||||
- Iudex Gundyr
|
||||
- Jailer
|
||||
- Judicator
|
||||
- King of the Storm
|
||||
- Large Hollow Soldiers
|
||||
- Large Hollow Soldier
|
||||
- Cathedral Large Hollow Soldier
|
||||
- Lothric Castle Large Hollow Soldier
|
||||
- Large Hound Rat
|
||||
- Large Serpent-Man
|
||||
- Large Starved Hound
|
||||
- Locust Preacher
|
||||
- Lords of Cinder (actually called "Soul of Cinder" ingame)
|
||||
- Lorian, Elder Prince
|
||||
- Lothric Knights
|
||||
- High Wall Lothric Knight
|
||||
- Lothric Castle Lothric Knight
|
||||
- Dreg Heap Lothric Knight
|
||||
- Red-Eyed Lothric Knight
|
||||
- Lothric Priest
|
||||
- Lothric, Younger Prince
|
||||
- Lycanthrope
|
||||
- Lycanthrope Hunter
|
||||
- Maggot Belly Starved Hound
|
||||
- Millwood Knight
|
||||
- Mimic Chest
|
||||
- Monstrosity of Sin
|
||||
- Murkman
|
||||
- Murkman Summoner
|
||||
- Nameless King
|
||||
- Old Demon King
|
||||
- Passive Locust Preacher
|
||||
- Peasant Hollow
|
||||
- Poisonhorn Bug
|
||||
- Pontiff Knight
|
||||
- Pontiff Sulyvahn
|
||||
- Pus of Man
|
||||
- Ravenous Crystal Lizard
|
||||
- Reanimated Corpse
|
||||
- Ringed City Cleric
|
||||
- Ringed Knight
|
||||
- Road of Sacrifices Sorcerer
|
||||
- Rock Lizard
|
||||
- Rotten Slug
|
||||
- Serpent-Man
|
||||
- Serpent-Man Summoner
|
||||
- Sewer Centipede
|
||||
- Silver Knight
|
||||
- Sister Friede
|
||||
- Skeletons
|
||||
- Skeleton
|
||||
- Bonewheel Skeleton
|
||||
- Carthus Curved Sword Skeleton
|
||||
- Carthus Shotel Skeleton
|
||||
- Ringed City Skeleton
|
||||
- Slave Knight Gael
|
||||
- Slave Knight Gael 1
|
||||
- Slave Knight Gael 2
|
||||
- Small Locust Preacher
|
||||
- Smouldering Ghru Grunt
|
||||
- Starved Hound
|
||||
- Stray Demon
|
||||
- Sulyvahn's Beast
|
||||
- Thrall
|
||||
- Tree Woman
|
||||
- Vordt of the Boreal Valley
|
||||
- Winged Knight
|
||||
- Wolves
|
||||
- Smaller Wolf
|
||||
- Larger Wolf
|
||||
- Greatwolf
|
||||
- Wretch
|
||||
- Writhing Flesh
|
||||
- Catacombs Writhing Flesh
|
||||
- Smouldering Writhing Flesh
|
||||
- Anor Londo Writhing Flesh
|
||||
- Yhorm the Giant
|
||||
@@ -1,11 +1,9 @@
|
||||
# Dark Souls III Items
|
||||
|
||||
[Game Page] | [Setup] | Items | [Locations] | [Enemy Randomization]
|
||||
[Game Page] | Items | [Locations]
|
||||
|
||||
[Game Page]: /games/Dark%20Souls%20III/info/en
|
||||
[Setup]: /tutorial/Dark%20Souls%20III/setup/en
|
||||
[Locations]: /tutorial/Dark%20Souls%20III/locations/en
|
||||
[Enemy Randomization]: /tutorial/Dark%20Souls%20III/enemy-randomization/en
|
||||
|
||||
## Item Groups
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
# Dark Souls III Locations
|
||||
|
||||
[Game Page] | [Setup] | [Items] | Locations | [Enemy Randomization]
|
||||
[Game Page] | [Items] | Locations
|
||||
|
||||
[Game Page]: /games/Dark%20Souls%20III/info/en
|
||||
[Setup]: /tutorial/Dark%20Souls%20III/setup/en
|
||||
[Items]: /tutorial/Dark%20Souls%20III/items/en
|
||||
[Enemy Randomization]: /tutorial/Dark%20Souls%20III/enemy-randomization/en
|
||||
|
||||
## Table of Contents
|
||||
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
# Dark Souls III Randomizer Setup Guide
|
||||
|
||||
[Game Page] | Setup | [Items] | [Locations] | [Enemy Randomization]
|
||||
|
||||
[Game Page]: /games/Dark%20Souls%20III/info/en
|
||||
[Items]: /tutorial/Dark%20Souls%20III/items/en
|
||||
[Locations]: /tutorial/Dark%20Souls%20III/locations/en
|
||||
[Enemy Randomization]: /tutorial/Dark%20Souls%20III/enemy-randomization/en
|
||||
|
||||
## Required Software
|
||||
|
||||
- [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/)
|
||||
|
||||
Regular → Executable
+7
-19
@@ -19,7 +19,7 @@ import factorio_rcon
|
||||
from CommonClient import ClientCommandProcessor, CommonContext, logger, server_loop, gui_enabled, get_base_parser
|
||||
from MultiServer import mark_raw
|
||||
from NetUtils import ClientStatus, NetworkItem, JSONtoTextParser, JSONMessagePart
|
||||
from Utils import async_start, get_file_safe_name, is_windows, Version, format_SI_prefix, get_text_between, user_path
|
||||
from Utils import async_start, get_file_safe_name, is_windows, Version, format_SI_prefix, get_text_between
|
||||
from .settings import FactorioSettings
|
||||
from settings import get_settings
|
||||
|
||||
@@ -63,7 +63,7 @@ class FactorioCommandProcessor(ClientCommandProcessor):
|
||||
def _cmd_toggle_chat(self):
|
||||
"""Toggle sending of chat messages from players on the Factorio server to Archipelago."""
|
||||
self.ctx.toggle_bridge_chat_out()
|
||||
|
||||
|
||||
def _cmd_rcon_reconnect(self) -> bool:
|
||||
"""Reconnect the RCON client if its disconnected."""
|
||||
try:
|
||||
@@ -88,7 +88,7 @@ class FactorioContext(CommonContext):
|
||||
|
||||
def __init__(self, server_address, password, filter_connection_changes: bool, filter_item_sends: bool, bridge_chat_out: bool,
|
||||
rcon_port: int, rcon_password: str, server_settings_path: str | None,
|
||||
config_file: str, factorio_server_args: tuple[str, ...] | list[str]):
|
||||
factorio_server_args: tuple[str, ...]):
|
||||
super(FactorioContext, self).__init__(server_address, password)
|
||||
self.send_index: int = 0
|
||||
self.rcon_client = None
|
||||
@@ -105,7 +105,6 @@ class FactorioContext(CommonContext):
|
||||
self.rcon_port: int = rcon_port
|
||||
self.rcon_password: str = rcon_password
|
||||
self.server_settings_path: str = server_settings_path
|
||||
self.config_file: str = config_file
|
||||
self.additional_factorio_server_args = factorio_server_args
|
||||
|
||||
@property
|
||||
@@ -159,11 +158,9 @@ class FactorioContext(CommonContext):
|
||||
"--rcon-port", str(self.rcon_port),
|
||||
"--rcon-password", self.rcon_password,
|
||||
"--server-settings", self.server_settings_path,
|
||||
"--config", self.config_file,
|
||||
*self.additional_factorio_server_args)
|
||||
else:
|
||||
return ("--rcon-port", str(self.rcon_port), "--rcon-password", self.rcon_password,
|
||||
"--config", self.config_file,
|
||||
*self.additional_factorio_server_args)
|
||||
|
||||
@property
|
||||
@@ -367,7 +364,7 @@ async def factorio_server_watcher(ctx: FactorioContext):
|
||||
if not os.path.exists(savegame_name):
|
||||
logger.info(f"Creating savegame {savegame_name}")
|
||||
subprocess.run((
|
||||
executable, "--create", savegame_name, "--preset", "archipelago", "--config", ctx.config_file
|
||||
executable, "--create", savegame_name, "--preset", "archipelago"
|
||||
))
|
||||
factorio_process = subprocess.Popen((executable, "--start-server", savegame_name,
|
||||
*ctx.server_args),
|
||||
@@ -477,11 +474,11 @@ async def get_info(ctx: FactorioContext, rcon_client: factorio_rcon.RCONClient):
|
||||
|
||||
|
||||
async def factorio_spinup_server(ctx: FactorioContext) -> bool:
|
||||
savegame_name = user_path("factorio", "saves", "Archipelago.zip")
|
||||
savegame_name = os.path.abspath("Archipelago.zip")
|
||||
if not os.path.exists(savegame_name):
|
||||
logger.info(f"Creating savegame {savegame_name}")
|
||||
subprocess.run((
|
||||
executable, "--create", savegame_name, "--config", ctx.config_file
|
||||
executable, "--create", savegame_name
|
||||
))
|
||||
factorio_process = subprocess.Popen(
|
||||
(executable, "--start-server", savegame_name, *ctx.server_args),
|
||||
@@ -612,9 +609,6 @@ def launch(*new_args: str):
|
||||
|
||||
if not os.path.exists(os.path.dirname(executable)):
|
||||
raise FileNotFoundError(f"Path {os.path.dirname(executable)} does not exist or could not be accessed.")
|
||||
if os.path.isdir(executable) and os.path.exists(os.path.join(executable, "Contents", "MacOS", "factorio")):
|
||||
# user entered the .App bundle, let's find the executable
|
||||
executable = os.path.join(executable, "Contents", "MacOS", "factorio")
|
||||
if os.path.isdir(executable): # user entered a path to a directory, let's find the executable therein
|
||||
executable = os.path.join(executable, "factorio")
|
||||
if not os.path.isfile(executable):
|
||||
@@ -623,15 +617,9 @@ def launch(*new_args: str):
|
||||
else:
|
||||
raise FileNotFoundError(f"Path {executable} is not an executable file.")
|
||||
|
||||
config_file = user_path('factorio', 'config', 'apconfig.ini')
|
||||
if not os.path.exists(config_file):
|
||||
os.makedirs(os.path.dirname(config_file), exist_ok=True)
|
||||
with open(config_file, 'w') as f:
|
||||
f.write(f"[path]\nread-data=__PATH__system-read-data__\nwrite-data={user_path('factorio')}")
|
||||
|
||||
asyncio.run(main(lambda: FactorioContext(
|
||||
args.connect, args.password,
|
||||
initial_filter_connection_changes, initial_filter_item_sends, initial_bridge_chat_out,
|
||||
rcon_port, rcon_password, server_settings, config_file, rest
|
||||
rcon_port, rcon_password, server_settings, rest
|
||||
)))
|
||||
colorama.deinit()
|
||||
|
||||
@@ -23,6 +23,7 @@ template_env: Optional[jinja2.Environment] = None
|
||||
|
||||
data_template: Optional[jinja2.Template] = None
|
||||
data_final_template: Optional[jinja2.Template] = None
|
||||
locale_template: Optional[jinja2.Template] = None
|
||||
control_template: Optional[jinja2.Template] = None
|
||||
settings_template: Optional[jinja2.Template] = None
|
||||
|
||||
@@ -93,7 +94,7 @@ def generate_mod(world: "Factorio", output_directory: str):
|
||||
multiworld = world.multiworld
|
||||
random = world.random
|
||||
|
||||
global data_final_template, control_template, data_template, settings_template
|
||||
global data_final_template, locale_template, control_template, data_template, settings_template
|
||||
with template_load_lock:
|
||||
if not data_final_template:
|
||||
def load_template(name: str):
|
||||
@@ -106,6 +107,7 @@ def generate_mod(world: "Factorio", output_directory: str):
|
||||
|
||||
data_template = template_env.get_template("data.lua")
|
||||
data_final_template = template_env.get_template("data-final-fixes.lua")
|
||||
locale_template = template_env.get_template(r"locale/en/locale.cfg")
|
||||
control_template = template_env.get_template("control.lua")
|
||||
settings_template = template_env.get_template("settings.lua")
|
||||
# get data for templates
|
||||
@@ -193,7 +195,9 @@ def generate_mod(world: "Factorio", output_directory: str):
|
||||
control_template.render(**template_data)))
|
||||
mod.writing_tasks.append(lambda: (versioned_mod_name + "/settings.lua",
|
||||
settings_template.render(**template_data)))
|
||||
|
||||
mod.writing_tasks.append(lambda: (versioned_mod_name + "/locale/en/locale.cfg",
|
||||
locale_template.render(**template_data)))
|
||||
|
||||
info = base_info.copy()
|
||||
info["name"] = mod_name
|
||||
mod.writing_tasks.append(lambda: (versioned_mod_name + "/info.json",
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
[map-gen-preset-name]
|
||||
archipelago=Archipelago
|
||||
|
||||
[entity-name]
|
||||
ap-energy-bridge=Archipelago EnergyLink Bridge
|
||||
|
||||
[map-gen-preset-description]
|
||||
archipelago=World preset created by the Archipelago Randomizer. World may or may not contain actual archipelagos.
|
||||
|
||||
[technology-name]
|
||||
ap-technology-full=__1__'s __2__ (__3__)
|
||||
ap-technology-hidden=__1__
|
||||
|
||||
[technology-description]
|
||||
ap-technology-full=Researching this technology sends __1__ to __2____3__.
|
||||
ap-technology-item-advancement=, which is considered a logical advancement
|
||||
ap-technology-item-useful=, which is considered useful
|
||||
ap-technology-item-trap=, which is considered fun
|
||||
ap-technology-hidden=Researching this technology sends something to someone__1__.
|
||||
|
||||
[mod-setting-name]
|
||||
archipelago-death-link=Death Link
|
||||
|
||||
[mod-setting-description]
|
||||
archipelago-death-link=Kill other players in the same Archipelago Multiworld that also have Death Link turned on, when you die.
|
||||
|
||||
[archipelago]
|
||||
receive-ap-item=Received __1__ from __2__.
|
||||
receive-ap-catchup=Received __1__ as it is already checked.
|
||||
receive-sample-item=Received __1__x __2__
|
||||
sample-inventory-full=Additional items will be sent when inventory space is available.
|
||||
sample-error=Unable to receive __1__x [item=__2__] as this item does not exist.
|
||||
fail-to-place=Failed to place __1__ in __2__
|
||||
|
||||
[traps]
|
||||
new-evolution-factor=New evolution factor: __1__
|
||||
@@ -420,12 +420,12 @@ function update_player(index)
|
||||
sent = 0
|
||||
end
|
||||
if sent > 0 then
|
||||
player.print({"archipelago.receive-sample-item", sent, "[item=" .. name .. ",quality="..stack.quality.."]"})
|
||||
player.print("Received " .. sent .. "x [item=" .. name .. ",quality={{ free_sample_quality_name }}]")
|
||||
data.suppress_full_inventory_message = false
|
||||
end
|
||||
if sent ~= count then -- Couldn't full send.
|
||||
if not data.suppress_full_inventory_message then
|
||||
player.print({"archipelago.sample-inventory-full"}, {r=1, g=1, b=0.25})
|
||||
player.print("Additional items will be sent when inventory space is available.", {r=1, g=1, b=0.25})
|
||||
end
|
||||
data.suppress_full_inventory_message = true -- Avoid spamming them with repeated full inventory messages.
|
||||
samples[name] = count - sent -- Buffer the remaining items
|
||||
@@ -434,7 +434,7 @@ function update_player(index)
|
||||
samples[name] = nil -- Remove from the list
|
||||
end
|
||||
else
|
||||
player.print({"archipelago.sample-inventory-full", count, name})
|
||||
player.print("Unable to receive " .. count .. "x [item=" .. name .. "] as this item does not exist.")
|
||||
samples[name] = nil
|
||||
end
|
||||
end
|
||||
@@ -665,7 +665,7 @@ function spawn_entity(surface, force, name, x, y, radius, randomize, avoid_ores)
|
||||
end
|
||||
end
|
||||
if new_entity == nil then
|
||||
force.print({"archipelago.fail-to-place", args.name, serpent.line({x = x, y = y, radius = radius})})
|
||||
force.print("Failed to place " .. args.name .. " in " .. serpent.line({x = x, y = y, radius = radius}))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -725,7 +725,7 @@ end,
|
||||
local new_factor = game.forces["enemy"].get_evolution_factor("nauvis") +
|
||||
(TRAP_EVO_FACTOR * (1 - game.forces["enemy"].get_evolution_factor("nauvis")))
|
||||
game.forces["enemy"].set_evolution_factor(new_factor, "nauvis")
|
||||
game.print({"traps.new-evolution-factor", new_factor})
|
||||
game.print({"", "New evolution factor:", new_factor})
|
||||
end,
|
||||
["Teleport Trap"] = function()
|
||||
for _, player in ipairs(game.forces["player"].players) do
|
||||
@@ -777,10 +777,10 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi
|
||||
if index == nil then
|
||||
game.print("ap-get-technology is only to be used by the Archipelago Factorio Client")
|
||||
return
|
||||
elseif index == "-1" then -- for coop sync and restoring from an older savegame
|
||||
elseif index == -1 then -- for coop sync and restoring from an older savegame
|
||||
tech = force.technologies[item_name]
|
||||
if tech.researched ~= true then
|
||||
game.print({"archipelago.receive-ap-catchup", "[technology=" .. tech.name .. "]"})
|
||||
game.print({"", "Received [technology=" .. tech.name .. "] as it is already checked."})
|
||||
game.play_sound({path="utility/research_completed"})
|
||||
tech.researched = true
|
||||
end
|
||||
@@ -792,7 +792,7 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi
|
||||
for _, item_name in ipairs(tech_stack) do
|
||||
tech = force.technologies[item_name]
|
||||
if tech.researched ~= true then
|
||||
game.print({"archipelago.receive-ap-item", "[technology=" .. tech.name .. "]", source})
|
||||
game.print({"", "Received [technology=" .. tech.name .. "] from ", source})
|
||||
game.play_sound({path="utility/research_completed"})
|
||||
tech.researched = true
|
||||
return
|
||||
@@ -804,7 +804,7 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi
|
||||
if tech ~= nil then
|
||||
storage.index_sync[index] = tech
|
||||
if tech.researched ~= true then
|
||||
game.print({"archipelago.receive-ap-item", "[technology=" .. tech.name .. "]", source})
|
||||
game.print({"", "Received [technology=" .. tech.name .. "] from ", source})
|
||||
game.play_sound({path="utility/research_completed"})
|
||||
tech.researched = true
|
||||
end
|
||||
@@ -812,7 +812,7 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi
|
||||
elseif TRAP_TABLE[item_name] ~= nil then
|
||||
if storage.index_sync[index] ~= item_name then -- not yet received trap
|
||||
storage.index_sync[index] = item_name
|
||||
game.print({"archipelago.receive-ap-item", item_name, source})
|
||||
game.print({"", "Received ", item_name, " from ", source})
|
||||
TRAP_TABLE[item_name]()
|
||||
end
|
||||
else
|
||||
|
||||
@@ -154,13 +154,6 @@ technologies["{{ original_tech_name }}"].hidden_in_factoriopedia = true
|
||||
{#- the tech researched by the local player #}
|
||||
new_tree_copy = table.deepcopy(template_tech)
|
||||
new_tree_copy.name = "ap-{{ location.address }}-"{# use AP ID #}
|
||||
{%- if location.revealed %}
|
||||
new_tree_copy.localised_name = {"technology-name.ap-technology-full", "{{ player_names[item.player] }}", "{{ item.name }}", "{{ location.name }}"}
|
||||
new_tree_copy.localised_description = {"technology-description.ap-technology-full", "{{ item.name }}", "{{ player_names[item.player] }}", {% if item.advancement %}{"technology-description.ap-technology-item-advancement"}{% elif item.useful %}{"technology-description.ap-technology-item-useful"}{% elif item.trap %}{"technology-description.ap-technology-item-trap"}{% else %}""{% endif %}}
|
||||
{%- else %}
|
||||
new_tree_copy.localised_name = {"technology-name.ap-technology-hidden", "{{location.name}}"}
|
||||
new_tree_copy.localised_description = {"technology-description.ap-technology-hidden", {% if tech_tree_information == 1 and item.advancement %}{"technology-description.ap-technology-item-advancement"}{% else %}""{% endif %}}
|
||||
{% endif -%}
|
||||
{% if location.crafted_item is not none %}
|
||||
new_tree_copy.research_trigger = {
|
||||
type = "{{ 'craft-fluid' if location.crafted_item in liquids else 'craft-item' }}",
|
||||
|
||||
@@ -13,6 +13,7 @@ end
|
||||
local energy_bridge = table.deepcopy(data.raw["accumulator"]["accumulator"])
|
||||
energy_bridge.name = "ap-energy-bridge"
|
||||
energy_bridge.minable.result = "ap-energy-bridge"
|
||||
energy_bridge.localised_name = "Archipelago EnergyLink Bridge"
|
||||
energy_bridge.energy_source.buffer_capacity = "50MJ"
|
||||
energy_bridge.energy_source.input_flow_limit = "10MW"
|
||||
energy_bridge.energy_source.output_flow_limit = "10MW"
|
||||
@@ -24,6 +25,7 @@ data.raw["accumulator"]["ap-energy-bridge"] = energy_bridge
|
||||
|
||||
local energy_bridge_item = table.deepcopy(data.raw["item"]["accumulator"])
|
||||
energy_bridge_item.name = "ap-energy-bridge"
|
||||
energy_bridge_item.localised_name = "Archipelago EnergyLink Bridge"
|
||||
energy_bridge_item.place_result = energy_bridge.name
|
||||
tint_icon(energy_bridge_item, energy_bridge_tint())
|
||||
data.raw["item"]["ap-energy-bridge"] = energy_bridge_item
|
||||
@@ -33,6 +35,7 @@ energy_bridge_recipe.name = "ap-energy-bridge"
|
||||
energy_bridge_recipe.results = { {type = "item", name = energy_bridge_item.name, amount = 1} }
|
||||
energy_bridge_recipe.energy_required = 1
|
||||
energy_bridge_recipe.enabled = {% if energy_link %}true{% else %}false{% endif %}
|
||||
energy_bridge_recipe.localised_name = "Archipelago EnergyLink Bridge"
|
||||
data.raw["recipe"]["ap-energy-bridge"] = energy_bridge_recipe
|
||||
|
||||
data.raw["map-gen-presets"].default["archipelago"] = {{ dict_to_lua({"default": False, "order": "a", "basic_settings": world_gen["basic"], "advanced_settings": world_gen["advanced"]}) }}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
[map-gen-preset-name]
|
||||
archipelago=Archipelago
|
||||
|
||||
[map-gen-preset-description]
|
||||
archipelago=World preset created by the Archipelago Randomizer. World may or may not contain actual archipelagos.
|
||||
|
||||
[technology-name]
|
||||
{% for location, item in locations %}
|
||||
{%- if location.revealed %}
|
||||
ap-{{ location.address }}-={{ player_names[item.player] }}'s {{ item.name }} ({{ location.name }})
|
||||
{%- else %}
|
||||
ap-{{ location.address }}-= {{location.name}}
|
||||
{%- endif -%}
|
||||
{% endfor %}
|
||||
|
||||
[technology-description]
|
||||
{% for location, item in locations %}
|
||||
{%- if location.revealed %}
|
||||
ap-{{ location.address }}-=Researching this technology sends {{ item.name }} to {{ player_names[item.player] }}{% if item.advancement %}, which is considered a logical advancement{% elif item.useful %}, which is considered useful{% elif item.trap %}, which is considered fun{% endif %}.
|
||||
{%- elif tech_tree_information == 1 and item.advancement %}
|
||||
ap-{{ location.address }}-=Researching this technology sends something to someone, which is considered a logical advancement.
|
||||
{%- else %}
|
||||
ap-{{ location.address }}-=Researching this technology sends something to someone.
|
||||
{%- endif -%}
|
||||
{% endfor %}
|
||||
|
||||
[mod-setting-name]
|
||||
archipelago-death-link-{{ slot_player }}-{{ seed_name }}=Death Link
|
||||
|
||||
[mod-setting-description]
|
||||
archipelago-death-link-{{ slot_player }}-{{ seed_name }}=Kill other players in the same Archipelago Multiworld that also have Death Link turned on, when you die.
|
||||
@@ -21,8 +21,6 @@ data:extend({
|
||||
type = "bool-setting",
|
||||
name = "archipelago-death-link-{{ slot_player }}-{{ seed_name }}",
|
||||
setting_type = "runtime-global",
|
||||
localised_name = {"mod-setting-name.archipelago-death-link"},
|
||||
localised_description = {"mod-setting-description.archipelago-death-link"},
|
||||
{% if death_link %}
|
||||
default_value = true
|
||||
{% else %}
|
||||
|
||||
@@ -60,7 +60,7 @@ adding more randomness and "mystery" to your options. Every configurable setting
|
||||
|
||||
Currently, there are only a few options that are root options. Everything else should be nested within one of these root
|
||||
options or in some cases nested within other nested options. The only options that should exist in root
|
||||
are `description`, `name`, `game`, `quantity`, `requires`, and the name of the games you want options for.
|
||||
are `description`, `name`, `game`, `requires`, and the name of the games you want options for.
|
||||
|
||||
* `description` is ignored by the generator and is simply a good way for you to organize if you have multiple files
|
||||
using this to detail the intention of the file.
|
||||
@@ -78,9 +78,6 @@ are `description`, `name`, `game`, `quantity`, `requires`, and the name of the g
|
||||
* `game` is where either your chosen game goes or, if you would like, can be filled with multiple games each with
|
||||
different weights.
|
||||
|
||||
* `quantity` is the amount of times this yaml should be used when generating. This option is optional, the default value is 1.
|
||||
To ensure that the name is unique with a value of at least two, the keywords from above must be used.
|
||||
|
||||
* `requires` details different requirements from the generator for the YAML to work as you expect it to. Generally this
|
||||
is good for detailing the version of Archipelago this YAML was prepared for. If it is rolled on an older version,
|
||||
options may be missing and as such it will not work as expected. If any plando is used in the file then requiring it
|
||||
|
||||
@@ -99,7 +99,7 @@ case-sensitive. You can also use item groups and location groups that are define
|
||||
|
||||
## Item Plando Examples
|
||||
```yaml
|
||||
plando_items:
|
||||
plando_items:
|
||||
# Example block - Pokémon Red and Blue
|
||||
- items:
|
||||
Potion: 3
|
||||
|
||||
@@ -232,9 +232,6 @@ class JakAndDaxterWorld(World):
|
||||
power_cell_thresholds_minus_one: list[int]
|
||||
trap_weights: tuple[list[str], list[int]]
|
||||
|
||||
# UT Yaml-less flag
|
||||
ut_can_gen_without_yaml = True
|
||||
|
||||
# Store these dictionaries for speed improvements.
|
||||
level_to_regions: dict[str, list[JakAndDaxterRegion]] # Contains all levels and regions.
|
||||
level_to_orb_regions: dict[str, list[JakAndDaxterRegion]] # Contains only regions which contain orbs.
|
||||
@@ -246,15 +243,6 @@ class JakAndDaxterWorld(World):
|
||||
self.level_to_regions = defaultdict(list)
|
||||
self.level_to_orb_regions = defaultdict(list)
|
||||
|
||||
# Implement Universal Tracker support - reset all options to those from UT's gen if applicable.
|
||||
if hasattr(self.multiworld, "re_gen_passthrough"):
|
||||
if jak1_name in self.multiworld.re_gen_passthrough:
|
||||
for key, val in self.multiworld.re_gen_passthrough[jak1_name].items():
|
||||
try:
|
||||
getattr(self.options, key).value = val
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
# Cache the power cell threshold values for quicker reference.
|
||||
self.power_cell_thresholds = [
|
||||
self.options.fire_canyon_cell_count.value,
|
||||
|
||||
@@ -579,6 +579,7 @@ visit_locking_dict = {
|
||||
ItemName.IceCream,
|
||||
ItemName.WaytotheDawn,
|
||||
ItemName.IdentityDisk,
|
||||
ItemName.IceCream,
|
||||
ItemName.NamineSketches
|
||||
],
|
||||
"AllVisitLocking": {
|
||||
|
||||
@@ -184,8 +184,6 @@ class KH2World(World):
|
||||
if self.visitlocking_dict[item] == 0:
|
||||
self.visitlocking_dict.pop(item)
|
||||
self.multiworld.push_precollected(self.create_item(item))
|
||||
# tt is 3 visits so 2nd visit locking unlocks only the third visit
|
||||
self.multiworld.push_precollected(self.create_item(ItemName.IceCream))
|
||||
|
||||
for _ in range(self.options.RandomVisitLockingItem.value):
|
||||
if sum(self.visitlocking_dict.values()) <= 0:
|
||||
|
||||
@@ -14,7 +14,7 @@ Kingdom Hearts II Final Mix from the [Epic Games Store](https://store.epicgames.
|
||||
2. Lua Backend from the OpenKH Mod Manager
|
||||
3. Install the mod `KH2FM-Mods-Num/GoA-ROM-Edition` using OpenKH Mod Manager
|
||||
- Needed for Archipelago
|
||||
1. [Archipelago Launcher](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
1. [ArchipelagoKH2Client.exe](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
2. Install the Archipelago Companion mod from `JaredWeakStrike/APCompanion` using OpenKH Mod Manager
|
||||
3. Install the mod from `TopazTK/KH2-ArchipelagoEnablers` using OpenKH Mod manager
|
||||
1. Do Note that if you have `KH2FM-Mods-equations19/auto-save` OR `KH2FM-Mods-equations19/soft-reset` you should download `TopazTK/KH2-ArchipelagoEnablersLITE` instead
|
||||
@@ -58,7 +58,7 @@ After Installing the seed click "Mod Loader -> Build/Build and Run". Every slot
|
||||
|
||||
## Using the KH2 Client
|
||||
|
||||
Start the game through OpenKH Mod Manager. If starting a new run, enter the Garden of Assemblage from a new save. If returning to a run, load the save and enter the Garden of Assemblage. Then run the [ArchipelagoLauncher.exe](https://github.com/ArchipelagoMW/Archipelago/releases) and select the KH2 Client.<br>
|
||||
Start the game through OpenKH Mod Manager. If starting a new run, enter the Garden of Assemblage from a new save. If returning to a run, load the save and enter the Garden of Assemblage. Then run the [ArchipelagoKH2Client.exe](https://github.com/ArchipelagoMW/Archipelago/releases).<br>
|
||||
When you successfully connect to the server the client will automatically hook into the game to send/receive checks. <br>
|
||||
If the client ever loses connection to the game, it will also disconnect from the server and you will need to reconnect.<br>
|
||||
|
||||
|
||||
@@ -80,10 +80,9 @@ class LinksAwakeningSettings(settings.Group):
|
||||
|
||||
class GfxModFile(settings.FilePath):
|
||||
"""
|
||||
Gfxmod file, select one from the `Archipelago/data/sprites/ladx/` folder,
|
||||
or make your own. You can generate a template here: https://ladx-gfx.3and3.dev/
|
||||
Only .bin or .bdiff files.
|
||||
Extended spritesheets from the upstream randomizer are not supported.
|
||||
Gfxmod file, get it from upstream: https://github.com/daid/LADXR/tree/master/gfx
|
||||
Only .bin or .bdiff files
|
||||
The same directory will be checked for a matching text modification file
|
||||
"""
|
||||
def browse(self, filetypes=None, **kwargs):
|
||||
filetypes = [("Binary / Patch files", [".bin", ".bdiff"])]
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
|
||||
1. Download and install [Archipelago](<https://github.com/ArchipelagoMW/Archipelago/releases/latest>). **The installer
|
||||
file is located in the assets section at the bottom of the version information.**
|
||||
2. The first time you patch your game, you will be asked to locate your base ROM file.
|
||||
This is your Links Awakening DX ROM file. This only needs to be done once.
|
||||
2. The first time you do local generation or patch your game, you will be asked to locate your base ROM file.
|
||||
This is your Links Awakening DX ROM file. This only needs to be done once..
|
||||
|
||||
3. You should assign your emulator as your default program for launching ROM
|
||||
files.
|
||||
|
||||
@@ -48,7 +48,7 @@ class L2ACWorld(World):
|
||||
"""
|
||||
The Ancient Cave is a roguelike dungeon crawling game built into
|
||||
the RGP Lufia II. Face 99 floors of ever harder to beat monsters,
|
||||
random items and find new companions on the way to face the Master
|
||||
random items and find new companions on the way to face the Royal
|
||||
Jelly in the end. Can you beat it?
|
||||
"""
|
||||
game: ClassVar[str] = "Lufia II Ancient Cave"
|
||||
|
||||
@@ -9,8 +9,8 @@ config file.
|
||||
|
||||
As you may or may not know, randomization was already a core feature of the Ancient Cave in Lufia II, basically being a
|
||||
whole game within a game. The Ancient Cave has 99 floors with increasingly hard enemies, red chests and blue chests. At
|
||||
the end of the Ancient Cave you get to fight the Master Jelly... if you make it that far. The Master
|
||||
Jelly gives you three rounds to try and kill it (or manage to vanquish your own party,
|
||||
the end of the Ancient Cave you get to fight the Royal Jelly... if you make it that far. You cannot lose the Royal
|
||||
Jelly fight as it kills itself after giving you three rounds to try and kill it (or manage to vanquish your own party,
|
||||
whichever one you can manage).
|
||||
|
||||
The Randomizer allows you to set different goals and modify the game in several other ways
|
||||
|
||||
@@ -56,6 +56,7 @@ If you would like to validate your config file to make sure it works, you may do
|
||||
4. You will be presented with a server page, from which you can download your patch file.
|
||||
5. Double-click on your patch file, and SNIClient will launch automatically, create your ROM from the patch file, and
|
||||
open your emulator for you.
|
||||
6. Since this is a single-player game, you will no longer need the client, so feel free to close it.
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
|
||||
@@ -56,9 +56,6 @@ class MarioLand2World(World):
|
||||
|
||||
web = MarioLand2WebWorld()
|
||||
|
||||
ut_can_gen_without_yaml = True
|
||||
glitches_item_name = "ut_glitch"
|
||||
|
||||
item_name_groups = {
|
||||
"Level Progression": {
|
||||
item_name for item_name in items if item_name.endswith(("Progression", "Secret", "Secret 1", "Secret 2"))
|
||||
@@ -97,51 +94,42 @@ class MarioLand2World(World):
|
||||
self.max_coin_locations = {}
|
||||
self.sprite_data = {}
|
||||
self.coin_fragments_required = 0
|
||||
self.ut = False
|
||||
|
||||
def generate_early(self):
|
||||
if hasattr(self.multiworld, "re_gen_passthrough") and self.game in self.multiworld.re_gen_passthrough:
|
||||
self.ut = True
|
||||
for key, value in self.multiworld.re_gen_passthrough[self.game].items():
|
||||
if hasattr(self.options, key):
|
||||
getattr(self.options, key).value = value
|
||||
else:
|
||||
setattr(self, key, value)
|
||||
self.sprite_data = deepcopy(level_sprites)
|
||||
if self.options.randomize_enemies:
|
||||
randomize_enemies(self.sprite_data, self.random)
|
||||
if self.options.randomize_platforms:
|
||||
randomize_platforms(self.sprite_data, self.random)
|
||||
|
||||
if self.options.marios_castle_midway_bell:
|
||||
self.sprite_data["Mario's Castle"][35]["sprite"] = "Midway Bell"
|
||||
|
||||
if self.options.auto_scroll_chances == "vanilla":
|
||||
self.auto_scroll_levels = [int(i in [19, 25, 30]) for i in range(32)]
|
||||
else:
|
||||
self.sprite_data = deepcopy(level_sprites)
|
||||
self.auto_scroll_levels = [int(self.random.randint(1, 100) <= self.options.auto_scroll_chances)
|
||||
for _ in range(32)]
|
||||
|
||||
if self.options.marios_castle_midway_bell:
|
||||
self.sprite_data["Mario's Castle"][35]["sprite"] = "Midway Bell"
|
||||
if self.options.randomize_enemies:
|
||||
randomize_enemies(self.sprite_data, self.random)
|
||||
if self.options.randomize_platforms:
|
||||
randomize_platforms(self.sprite_data, self.random)
|
||||
|
||||
if self.options.auto_scroll_chances == "vanilla":
|
||||
self.auto_scroll_levels = [int(i in [19, 25, 30]) for i in range(32)]
|
||||
else:
|
||||
self.auto_scroll_levels = [int(self.random.randint(1, 100) <= self.options.auto_scroll_chances)
|
||||
for _ in range(32)]
|
||||
|
||||
self.auto_scroll_levels[level_name_to_id["Mario's Castle"]] = 0
|
||||
unbeatable_scroll_levels = ["Tree Zone 3", "Macro Zone 2", "Space Zone 1", "Turtle Zone 2", "Pumpkin Zone 2"]
|
||||
if not self.options.shuffle_midway_bells:
|
||||
unbeatable_scroll_levels.append("Pumpkin Zone 1")
|
||||
for level, i in enumerate(self.auto_scroll_levels):
|
||||
if i == 1:
|
||||
if self.options.auto_scroll_mode in ("global_cancel_item", "level_cancel_items"):
|
||||
self.auto_scroll_levels[level_name_to_id["Mario's Castle"]] = 0
|
||||
unbeatable_scroll_levels = ["Tree Zone 3", "Macro Zone 2", "Space Zone 1", "Turtle Zone 2", "Pumpkin Zone 2"]
|
||||
if not self.options.shuffle_midway_bells:
|
||||
unbeatable_scroll_levels.append("Pumpkin Zone 1")
|
||||
for level, i in enumerate(self.auto_scroll_levels):
|
||||
if i == 1:
|
||||
if self.options.auto_scroll_mode in ("global_cancel_item", "level_cancel_items"):
|
||||
self.auto_scroll_levels[level] = 2
|
||||
elif self.options.auto_scroll_mode == "chaos":
|
||||
if (self.options.accessibility == "full"
|
||||
and level_id_to_name[level] in unbeatable_scroll_levels):
|
||||
self.auto_scroll_levels[level] = 2
|
||||
elif self.options.auto_scroll_mode == "chaos":
|
||||
if (self.options.accessibility == "full"
|
||||
and level_id_to_name[level] in unbeatable_scroll_levels):
|
||||
self.auto_scroll_levels[level] = 2
|
||||
else:
|
||||
self.auto_scroll_levels[level] = self.random.randint(1, 3)
|
||||
elif (self.options.accessibility == "full"
|
||||
and level_id_to_name[level] in unbeatable_scroll_levels):
|
||||
self.auto_scroll_levels[level] = 0
|
||||
if self.auto_scroll_levels[level] == 1 and "trap" in self.options.auto_scroll_mode.current_key:
|
||||
self.auto_scroll_levels[level] = 3
|
||||
else:
|
||||
self.auto_scroll_levels[level] = self.random.randint(1, 3)
|
||||
elif (self.options.accessibility == "full"
|
||||
and level_id_to_name[level] in unbeatable_scroll_levels):
|
||||
self.auto_scroll_levels[level] = 0
|
||||
if self.auto_scroll_levels[level] == 1 and "trap" in self.options.auto_scroll_mode.current_key:
|
||||
self.auto_scroll_levels[level] = 3
|
||||
|
||||
def create_regions(self):
|
||||
menu_region = Region("Menu", self.player, self.multiworld)
|
||||
@@ -190,10 +178,7 @@ class MarioLand2World(World):
|
||||
wario.place_locked_item(MarioLand2Item("Wario Defeated", ItemClassification.progression, None, self.player))
|
||||
|
||||
if self.options.coinsanity:
|
||||
if hasattr(self.multiworld, "generation_is_fake"):
|
||||
coinsanity_checks = self.options.coinsanity_checks.range_end
|
||||
else:
|
||||
coinsanity_checks = self.options.coinsanity_checks.value
|
||||
coinsanity_checks = self.options.coinsanity_checks.value
|
||||
self.num_coin_locations = [[region, 1] for region in created_regions if region != "Mario's Castle"]
|
||||
self.max_coin_locations = {region: len(coins_coords[region]) for region in created_regions
|
||||
if region != "Mario's Castle"}
|
||||
@@ -438,40 +423,12 @@ class MarioLand2World(World):
|
||||
self.multiworld.itempool += [self.create_item(item_name) for _ in range(count)]
|
||||
|
||||
def fill_slot_data(self):
|
||||
# Expose settings for accurate tracker logic
|
||||
shark_count: int = [
|
||||
self.multiworld.worlds[self.player].sprite_data["Turtle Zone 1"][i]["sprite"]
|
||||
for i in (27, 28)
|
||||
].count("Shark")
|
||||
crane_count: int = [
|
||||
self.multiworld.worlds[self.player].sprite_data["Mario Zone 3"][i]["sprite"]
|
||||
for i in (17, 18, 25)
|
||||
].count("Claw Grabber")
|
||||
options_dict = self.options.as_dict(
|
||||
"energy_link",
|
||||
"shuffle_golden_coins",
|
||||
"required_golden_coins",
|
||||
"coinsanity",
|
||||
"shuffle_midway_bells",
|
||||
"marios_castle_midway_bell",
|
||||
"shuffle_pipe_traversal",
|
||||
"auto_scroll_mode"
|
||||
)
|
||||
options_dict.update({
|
||||
"auto_scroll_levels": self.auto_scroll_levels,
|
||||
"turtle_zone_1_shark_count": shark_count,
|
||||
"mario_zone_3_crane_count": crane_count,
|
||||
"coin_fragments_required": self.coin_fragments_required
|
||||
})
|
||||
return options_dict
|
||||
|
||||
@staticmethod
|
||||
def interpret_slot_data(slot_data):
|
||||
return slot_data
|
||||
return {
|
||||
"energy_link": self.options.energy_link.value
|
||||
}
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
return MarioLand2Item(name, items[name] if name in items else ItemClassification.progression,
|
||||
self.item_name_to_id[name] if name in self.item_name_to_id else None, self.player)
|
||||
return MarioLand2Item(name, items[name], self.item_name_to_id[name], self.player)
|
||||
|
||||
def get_filler_item_name(self):
|
||||
return "1 Coin"
|
||||
|
||||
@@ -5,8 +5,6 @@ def is_auto_scroll(state, player, level):
|
||||
level_id = level_name_to_id[level]
|
||||
if state.has_any(["Cancel Auto Scroll", f"Cancel Auto Scroll - {level}"], player):
|
||||
return False
|
||||
if state.has("ut_glitch", player) and state.multiworld.worlds[player].auto_scroll_levels[level_id] == 3:
|
||||
return state.has(f"Auto Scroll - {level}", player)
|
||||
return state.multiworld.worlds[player].auto_scroll_levels[level_id] > 0
|
||||
|
||||
|
||||
@@ -289,12 +287,8 @@ def mario_zone_3_coins(state, player, coins):
|
||||
if state.has("Carrot", player):
|
||||
reachable_spike_coins = 15
|
||||
else:
|
||||
if state.multiworld.worlds[player].ut:
|
||||
claw_grabbers = state.multiworld.worlds[player].mario_zone_3_crane_count
|
||||
else:
|
||||
sprites = state.multiworld.worlds[player].sprite_data["Mario Zone 3"]
|
||||
claw_grabbers = len({sprites[i]["sprite"] == "Claw Grabber" for i in (17, 18, 25)})
|
||||
reachable_spike_coins = min(3, claw_grabbers
|
||||
sprites = state.multiworld.worlds[player].sprite_data["Mario Zone 3"]
|
||||
reachable_spike_coins = min(3, len({sprites[i]["sprite"] == "Claw Grabber" for i in (17, 18, 25)})
|
||||
+ state.has("Mushroom", player) + state.has("Fire Flower", player)) * 5
|
||||
reachable_coins += reachable_spike_coins
|
||||
if not auto_scroll:
|
||||
@@ -315,11 +309,8 @@ def mario_zone_4_coins(state, player, coins):
|
||||
|
||||
|
||||
def not_blocked_by_sharks(state, player):
|
||||
if state.multiworld.worlds[player].ut:
|
||||
sharks = state.multiworld.worlds[player].turtle_zone_1_shark_count
|
||||
else:
|
||||
sharks = [state.multiworld.worlds[player].sprite_data["Turtle Zone 1"][i]["sprite"]
|
||||
for i in (27, 28)].count("Shark")
|
||||
sharks = [state.multiworld.worlds[player].sprite_data["Turtle Zone 1"][i]["sprite"]
|
||||
for i in (27, 28)].count("Shark")
|
||||
if state.has("Carrot", player) or not sharks:
|
||||
return True
|
||||
if sharks == 2:
|
||||
|
||||
@@ -316,7 +316,7 @@ CONNECTIONS: dict[str, dict[str, list[str]]] = {
|
||||
"Searing Mega Shard Shop": [
|
||||
"Searing Crags - Falling Rocks Shop",
|
||||
"Searing Crags - Before Final Climb Shop",
|
||||
"Searing Crags - Key of Strength Room",
|
||||
"Searing Crags - Key of Strength Shop",
|
||||
],
|
||||
"Before Final Climb Shop": [
|
||||
"Searing Crags - Raining Rocks Checkpoint",
|
||||
@@ -330,9 +330,6 @@ CONNECTIONS: dict[str, dict[str, list[str]]] = {
|
||||
"Searing Crags - Top",
|
||||
],
|
||||
"Key of Strength Shop": [
|
||||
"Searing Crags - Key of Strength Room",
|
||||
],
|
||||
"Key of Strength Room": [
|
||||
"Searing Crags - Searing Mega Shard Shop",
|
||||
],
|
||||
"Triple Ball Spinner Checkpoint": [
|
||||
|
||||
@@ -7,6 +7,7 @@ from Options import PlandoConnection
|
||||
if TYPE_CHECKING:
|
||||
from . import MessengerWorld
|
||||
|
||||
|
||||
PORTALS: list[str] = [
|
||||
"Autumn Hills",
|
||||
"Riviere Turquoise",
|
||||
@@ -16,6 +17,7 @@ PORTALS: list[str] = [
|
||||
"Glacial Peak",
|
||||
]
|
||||
|
||||
|
||||
SHOP_POINTS: dict[str, list[str]] = {
|
||||
"Autumn Hills": [
|
||||
"Climbing Claws",
|
||||
@@ -110,6 +112,7 @@ SHOP_POINTS: dict[str, list[str]] = {
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
CHECKPOINTS: dict[str, list[str]] = {
|
||||
"Autumn Hills": [
|
||||
"Hope Latch",
|
||||
@@ -182,6 +185,7 @@ CHECKPOINTS: dict[str, list[str]] = {
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
REGION_ORDER: list[str] = [
|
||||
"Autumn Hills",
|
||||
"Forlorn Temple",
|
||||
@@ -302,4 +306,4 @@ def add_closed_portal_reqs(world: "MessengerWorld") -> None:
|
||||
closed_portals = [entrance for entrance in PORTALS if f"{entrance} Portal" not in world.starting_portals]
|
||||
for portal in closed_portals:
|
||||
tower_exit = world.multiworld.get_entrance(f"ToTHQ {portal} Portal", world.player)
|
||||
tower_exit.access_rule = lambda state, portal_item=portal: state.has(f"{portal_item} Portal", world.player)
|
||||
tower_exit.access_rule = lambda state, portal_item=portal: state.has(portal_item, world.player)
|
||||
|
||||
@@ -96,7 +96,7 @@ LOCATIONS: dict[str, list[str]] = {
|
||||
"Searing Crags - Power Thistle",
|
||||
"Searing Crags - Astral Tea Leaves",
|
||||
],
|
||||
"Searing Crags - Key of Strength Room": [
|
||||
"Searing Crags - Key of Strength Shop": [
|
||||
"Searing Crags - Key of Strength",
|
||||
],
|
||||
"Searing Crags - Portal": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from BaseClasses import CollectionState, CollectionRule, Region
|
||||
from BaseClasses import CollectionState, CollectionRule
|
||||
from worlds.generic.Rules import add_rule, allow_self_locking_items
|
||||
from .constants import NOTES, PHOBEKINS
|
||||
from .options import MessengerAccessibility
|
||||
@@ -13,7 +13,6 @@ class MessengerRules:
|
||||
player: int
|
||||
world: "MessengerWorld"
|
||||
connection_rules: dict[str, CollectionRule]
|
||||
indirect_conditions: dict[str, list[Region]]
|
||||
region_rules: dict[str, CollectionRule]
|
||||
location_rules: dict[str, CollectionRule]
|
||||
maximum_price: int
|
||||
@@ -109,18 +108,17 @@ class MessengerRules:
|
||||
"Searing Crags - Right -> Searing Crags - Portal":
|
||||
lambda state: self.has_tabi(state) and self.has_wingsuit(state),
|
||||
"Searing Crags - Colossuses Shop -> Searing Crags - Key of Strength Shop":
|
||||
lambda state: state.has("Power Thistle", self.player),
|
||||
"Searing Crags - Key of Strength Shop -> Searing Crags - Key of Strength Room":
|
||||
lambda state: self.has_dart(state)
|
||||
or (self.has_wingsuit(state)
|
||||
and self.can_destroy_projectiles(state)),
|
||||
lambda state: state.has("Power Thistle", self.player)
|
||||
and (self.has_dart(state)
|
||||
or (self.has_wingsuit(state)
|
||||
and self.can_destroy_projectiles(state))),
|
||||
"Searing Crags - Falling Rocks Shop -> Searing Crags - Searing Mega Shard Shop":
|
||||
self.has_dart,
|
||||
"Searing Crags - Searing Mega Shard Shop -> Searing Crags - Before Final Climb Shop":
|
||||
lambda state: self.has_dart(state) or self.can_destroy_projectiles(state),
|
||||
"Searing Crags - Searing Mega Shard Shop -> Searing Crags - Falling Rocks Shop":
|
||||
self.has_dart,
|
||||
"Searing Crags - Searing Mega Shard Shop -> Searing Crags - Key of Strength Room":
|
||||
"Searing Crags - Searing Mega Shard Shop -> Searing Crags - Key of Strength Shop":
|
||||
self.false,
|
||||
"Searing Crags - Before Final Climb Shop -> Searing Crags - Colossuses Shop":
|
||||
self.has_dart,
|
||||
@@ -221,16 +219,6 @@ class MessengerRules:
|
||||
lambda state: self.can_dboost(state) or self.has_dart(state),
|
||||
}
|
||||
|
||||
# dict of connection names and the regions checked in the requirements to traverse the exit
|
||||
self.indirect_conditions = {
|
||||
"Howling Grotto - Breezy Crushers Checkpoint -> Howling Grotto - Crushing Pits Shop": [
|
||||
self.world.get_region("Howling Grotto - Emerald Golem Shop")
|
||||
],
|
||||
"Glacial Peak - Left -> Elemental Skylands - Air Shmup": [
|
||||
self.world.get_location("Quillshroom Marsh - Queen of Quills").parent_region
|
||||
],
|
||||
}
|
||||
|
||||
self.location_rules = {
|
||||
# hq
|
||||
"Money Wrench": self.can_shop,
|
||||
@@ -376,8 +364,6 @@ class MessengerRules:
|
||||
for entrance_name, rule in self.connection_rules.items():
|
||||
entrance = multiworld.get_entrance(entrance_name, self.player)
|
||||
entrance.access_rule = rule
|
||||
for region in self.indirect_conditions.get(entrance_name, ()):
|
||||
multiworld.register_indirect_condition(region, entrance)
|
||||
for loc in multiworld.get_locations(self.player):
|
||||
if loc.name in self.location_rules:
|
||||
loc.access_rule = self.location_rules[loc.name]
|
||||
@@ -420,7 +406,7 @@ class MessengerHardRules(MessengerRules):
|
||||
lambda state: self.has_dart(state) or
|
||||
(self.can_destroy_projectiles(state) and
|
||||
(self.has_wingsuit(state) or self.can_dboost(state))),
|
||||
"Searing Crags - Searing Mega Shard Shop -> Searing Crags - Key of Strength Room":
|
||||
"Searing Crags - Searing Mega Shard Shop -> Searing Crags - Key of Strength Shop":
|
||||
lambda state: self.can_leash(state) or self.has_windmill(state),
|
||||
"Searing Crags - Before Final Climb Shop -> Searing Crags - Colossuses Shop":
|
||||
self.true,
|
||||
@@ -526,6 +512,7 @@ class MessengerOOBRules(MessengerRules):
|
||||
|
||||
self.location_rules = {
|
||||
"Bamboo Creek - Claustro": self.has_wingsuit,
|
||||
"Searing Crags - Key of Strength": self.has_wingsuit,
|
||||
"Sunken Shrine - Key of Love": lambda state: state.has_all({"Sun Crest", "Moon Crest"}, self.player),
|
||||
"Searing Crags - Pyro": self.has_tabi,
|
||||
"Underworld - Key of Chaos": self.has_tabi,
|
||||
|
||||
@@ -35,29 +35,3 @@ class PortalTestBase(MessengerTestBase):
|
||||
test_state.collect(item)
|
||||
self.assertTrue(entrance.can_reach(test_state), grouping)
|
||||
entrance.access_rule = lambda state: True
|
||||
|
||||
|
||||
class PortalUnlockTest(MessengerTestBase):
|
||||
options = {
|
||||
"available_portals": 3,
|
||||
}
|
||||
|
||||
def test_unlocking_portal(self) -> None:
|
||||
"""Validate that unlocking the portal event actually unlock the portal in HQ"""
|
||||
|
||||
print(self.world.starting_portals)
|
||||
|
||||
for portal in PORTALS:
|
||||
name = f"{portal} Portal"
|
||||
if name in self.world.starting_portals:
|
||||
continue
|
||||
|
||||
entrance_name = f"ToTHQ {name}"
|
||||
with self.subTest(portal=name, entrance_name=entrance_name):
|
||||
hq_portal = self.multiworld.get_entrance(entrance_name, self.player)
|
||||
test_state = CollectionState(self.multiworld)
|
||||
self.assertFalse(hq_portal.can_reach(test_state), "reachable with nothing")
|
||||
|
||||
event = self.multiworld.get_location(name, self.player)
|
||||
test_state.collect(event.item)
|
||||
self.assertTrue(hq_portal.can_reach(test_state))
|
||||
|
||||
+1
-1
@@ -341,7 +341,7 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(base_rom_bytes)
|
||||
if CHECKSUM_BLUE != basemd5.hexdigest():
|
||||
raise Exception('Supplied Base Rom does not match US GBA Blue Version. '
|
||||
raise Exception('Supplied Base Rom does not match US GBA Blue Version.'
|
||||
'Please provide the correct ROM version')
|
||||
|
||||
get_base_rom_bytes.base_rom_bytes = base_rom_bytes
|
||||
|
||||
@@ -712,18 +712,4 @@ SONG_DATA: Dict[str, SongData] = {
|
||||
"Chasing Daylight": SongData(2900836, "96-0", "Wuthering Waves Pioneer Podcast", False, 3, 5, 8),
|
||||
"CATCH ME IF YOU CAN": SongData(2900837, "96-1", "Wuthering Waves Pioneer Podcast", False, 4, 6, 9),
|
||||
"RUNNING FOR YOUR LIFE": SongData(2900838, "96-2", "Wuthering Waves Pioneer Podcast", False, 2, 5, 8),
|
||||
"Denpa-tic Imaginary Girl": SongData(2900839, "43-73", "MD Plus Project", False, 4, 6, 9),
|
||||
"Don't Fight The Music": SongData(2900840, "97-0", "DASH AND SHOOT!", False, 6, 8, 10),
|
||||
"Viyella's Scream": SongData(2900841, "97-1", "DASH AND SHOOT!", False, 7, 9, 11),
|
||||
"Final Flash Flight": SongData(2900842, "97-2", "DASH AND SHOOT!", False, 5, 7, 10),
|
||||
"YURUSHITE": SongData(2900843, "97-3", "DASH AND SHOOT!", False, 6, 8, 11),
|
||||
"girls.exe": SongData(2900844, "97-4", "DASH AND SHOOT!", False, 6, 8, 11),
|
||||
"Baqeela": SongData(2900845, "97-5", "DASH AND SHOOT!", False, 6, 8, 10),
|
||||
"Help me, ERINNNNNN!!": SongData(2900846, "43-74", "MD Plus Project", False, 4, 8, 10),
|
||||
"Utakata, Ai no Mahoroba": SongData(2900847, "98-0", "Touhou Mugakudan -V-", False, 3, 5, 7),
|
||||
"dynamite": SongData(2900848, "98-1", "Touhou Mugakudan -V-", False, 5, 7, 9),
|
||||
"Matsuyoi Nightbug": SongData(2900849, "98-2", "Touhou Mugakudan -V-", False, 5, 7, 10),
|
||||
"Coooonsultant!": SongData(2900850, "98-3", "Touhou Mugakudan -V-", False, 6, 8, 10),
|
||||
"Stop at the affected part and melt quickly - Madness Udine Quarter": SongData(2900851, "98-4", "Touhou Mugakudan -V-", False, 4, 6, 10),
|
||||
"Ultimate taste": SongData(2900852, "98-5", "Touhou Mugakudan -V-", False, 6, 8, 11),
|
||||
}
|
||||
@@ -3,7 +3,6 @@ from BaseClasses import Region, Item, ItemClassification, Tutorial
|
||||
from typing import List, ClassVar, Type, Set
|
||||
from math import floor
|
||||
from Options import PerGameCommonOptions, OptionError
|
||||
from rule_builder.rules import Has
|
||||
|
||||
from .Options import MuseDashOptions, md_option_groups
|
||||
from .Items import MuseDashSongItem, MuseDashFixedItem
|
||||
@@ -34,16 +33,7 @@ class MuseDashWebWorld(WebWorld):
|
||||
["Shiny"]
|
||||
)
|
||||
|
||||
setup_it = Tutorial(
|
||||
setup_en.tutorial_name,
|
||||
setup_en.description,
|
||||
"Italiano",
|
||||
"setup_it.md",
|
||||
"setup/it",
|
||||
["UCSA"]
|
||||
)
|
||||
|
||||
tutorials = [setup_en, setup_es, setup_it]
|
||||
tutorials = [setup_en, setup_es]
|
||||
options_presets = MuseDashPresets
|
||||
option_groups = md_option_groups
|
||||
|
||||
@@ -294,17 +284,17 @@ class MuseDashWorld(World):
|
||||
# Adds 2 item locations per song/album to the menu region.
|
||||
for i in range(0, len(all_selected_locations)):
|
||||
name = all_selected_locations[i]
|
||||
rule = Has(name)
|
||||
loc1 = MuseDashLocation(self.player, name + "-0", self.md_collection.song_locations[name + "-0"], menu_region)
|
||||
self.set_rule(loc1, rule)
|
||||
loc1.access_rule = lambda state, place=name: state.has(place, self.player)
|
||||
menu_region.locations.append(loc1)
|
||||
|
||||
loc2 = MuseDashLocation(self.player, name + "-1", self.md_collection.song_locations[name + "-1"], menu_region)
|
||||
self.set_rule(loc2, rule)
|
||||
loc2.access_rule = lambda state, place=name: state.has(place, self.player)
|
||||
menu_region.locations.append(loc2)
|
||||
|
||||
def set_rules(self) -> None:
|
||||
self.set_completion_rule(Has(self.md_collection.MUSIC_SHEET_NAME, self.get_music_sheet_win_count()))
|
||||
self.multiworld.completion_condition[self.player] = lambda state: \
|
||||
state.has(self.md_collection.MUSIC_SHEET_NAME, self.player, self.get_music_sheet_win_count())
|
||||
|
||||
def get_available_traps(self) -> List[str]:
|
||||
full_trap_list = self.md_collection.trap_items.keys()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"game": "Muse Dash",
|
||||
"authors": ["DeamonHunter"],
|
||||
"world_version": "1.5.33",
|
||||
"world_version": "1.5.30",
|
||||
"minimum_ap_version": "0.6.3"
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
# Guida al Setup di Muse Dash per Archipelago
|
||||
|
||||
## Links
|
||||
- [Pagina Principale](../../../../games/Muse%20Dash/info/en)
|
||||
- [Opzioni](../../../../games/Muse%20Dash/player-options)
|
||||
|
||||
## Software Richiesto
|
||||
|
||||
- Windows 8 o più recente.
|
||||
- Muse Dash: [Disponibile su Steam](https://store.steampowered.com/app/774171/Muse_Dash/)
|
||||
- \[Facoltativo\] DLC [Muse Plus]: [Disponibile su Steam](https://store.steampowered.com/app/2593750/Muse_Dash_Muse_Plus/)
|
||||
- Melon Loader: [GitHub](https://github.com/LavaGang/MelonLoader/releases/latest)
|
||||
- L'installer potrebbe richiedere .Net Framework 4.8: [Download](https://dotnet.microsoft.com/it-it/download/dotnet-framework/net48)
|
||||
- .NET Desktop Runtime 6.0.XX (Se non installato in precedenza): [Download](https://dotnet.microsoft.com/it-it/download/dotnet/6.0)
|
||||
- Muse Dash Archipelago Mod: [GitHub](https://github.com/DeamonHunter/ArchipelagoMuseDash/releases/latest)
|
||||
|
||||
## Installare la mod di Archipelago per Muse Dash
|
||||
|
||||
1. Scarica [MelonLoader.Installer.exe](https://github.com/LavaGang/MelonLoader/releases/latest) ed eseguilo.
|
||||
2. Seleziona la scheda "automated", premi select e naviga fino a `MuseDash.exe`.
|
||||
- Puoi trovare la cartella tramite steam premendo tasto destro sul gioco nella tua libreria e scegliendo *Gestisci→Sfoglia i file locali*
|
||||
- Se clicki sulla barra nella parte superiore della finestra, che ti dice la cartella attuale, questa ti darà un percorso che potrai copiare.
|
||||
Se copi questo percorso nella finestra creata da **MelonLoader** il programma navigherà automaticamente fino a quella cartella.
|
||||
3. Seleziona v0.7.0. e premi "install".
|
||||
4. Esegui il gioco una volta e aspetta fino alla comparsa del menù iniziale di Muse Dash prima di chiuderlo.
|
||||
5. Scarica l'ultima versione della [Muse Dash Archipelago Mod](https://github.com/DeamonHunter/ArchipelagoMuseDash/releases/latest)
|
||||
ed estraila nella cartella `/Mods/` appena creata nella cartella di installazione di Muse Dash.
|
||||
- Tutti i file devono essere nella cartella `/Mods/` e non in una cartella al suo interno.
|
||||
|
||||
Se hai installato tutto correttamente, dovrebbe apparire un bottone in basso a destra che ti permetterà di effettuare il login ad un server di Archipelago.
|
||||
|
||||
## Generare una Sessione MultiWorld
|
||||
1. Visita la pagina [Player Options](/games/Muse%20Dash/player-options) e configura a tuo piacimento le opzioni specifiche per il gioco.
|
||||
2. Esporta il tuo file yaml e usalo per generare una nuova sessione randomizzata
|
||||
- (Per istruizioni su come generare una nuova sessione di Archipelago, fai riferimento alla [Archipelago Web Guide](/tutorial/Archipelago/setup/en))
|
||||
|
||||
## Entrare in una Sessione MultiWorld
|
||||
|
||||
1. Esegui Muse Dash e supera la schermata iniziale. Premi il bottone in basso a destra.
|
||||
2. Inserisci i dettagli della sessione di Archipelago, come l'indirizzo del server con la sua porta (per esempio, archipelago.gg:38381), nome utente e password.
|
||||
3. Se tutto è stato inserito correttamente, la finestra dovrebbe scomparire e dovrebbe apparire il menù principale.
|
||||
Una volta entrato nella schermata di selezione canzoni dovrebbe esserne disponibile un numero ridotto.
|
||||
|
||||
## Risoluzione Problemi
|
||||
|
||||
### No Support Module Loaded
|
||||
|
||||
Questo errore avviene quando Melon Loader non è in grado di trovare i file necessari per eseguire le mod. Generalmente ci sono due cause principali per questo errore:
|
||||
un errore nella generazione dei file quando il gioco è stato avviato con Melon Loader per la prima volta o la rimozione di file dopo la generazione da parte di un antivirus.
|
||||
|
||||
Per risolvere questo problema devi per prima cosa rimuovere Melon Loader da Muse Dash.
|
||||
Puoi fare ciò eliminando la cartella di Melon Loader all'interno della cartella di Muse Dash, dopodichè puoi seguire nuovamente i passaggi per l'installazione.
|
||||
|
||||
Se continui ad avere lo stesso problema e stai usando un antivirus, prova a disattivarlo temporaneamente quando esegui Muse Dash per la prima volta
|
||||
o aggiungi la cartella di Muse Dash alla whitelist.
|
||||
@@ -1,69 +0,0 @@
|
||||
from . import MuseDashTestBase
|
||||
from typing import List
|
||||
|
||||
|
||||
class LocationRules(MuseDashTestBase):
|
||||
CHECK_SONGS: List[str] = [
|
||||
"Magical Wonderland",
|
||||
"Iyaiya",
|
||||
"Wonderful Pain",
|
||||
"Breaking Dawn",
|
||||
"One-Way Subway",
|
||||
"Frost Land",
|
||||
"Heart-Pounding Flight",
|
||||
"Pancake is Love",
|
||||
"Shiguang Tuya",
|
||||
"Evolution",
|
||||
"Dolphin and Broadcast",
|
||||
"Yuki no Shizuku Ame no Oto",
|
||||
"Best One feat.tooko",
|
||||
"Candy-coloured Love Theory",
|
||||
"Night Wander",
|
||||
"Dohna Dohna no Uta",
|
||||
"Spring Carnival",
|
||||
"DISCO NIGHT",
|
||||
"Koi no Moonlight"
|
||||
]
|
||||
|
||||
options = {
|
||||
"starting_song_count": 3,
|
||||
"additional_song_count": 15,
|
||||
"streamer_mode_enabled": True,
|
||||
"include_songs": CHECK_SONGS
|
||||
}
|
||||
|
||||
|
||||
def test_rules(self):
|
||||
"""Due to me typoing the second rule of a location, this test exists to ensure that doesn't happen again"""
|
||||
muse_dash_world = self.get_world()
|
||||
|
||||
for song in self.CHECK_SONGS:
|
||||
if song == muse_dash_world.victory_song_name:
|
||||
continue
|
||||
|
||||
if song in muse_dash_world.starting_songs:
|
||||
self.assertTrue(self.can_reach_location(song + "-0"), f"Starting Location {song}-0 was not beatable.")
|
||||
self.assertTrue(self.can_reach_location(song + "-1"), f"Starting Location {song}-1 was not beatable.")
|
||||
continue
|
||||
|
||||
|
||||
self.assertFalse(self.can_reach_location(song + "-0"), f"Location {song}-0 was unlocked without an item.")
|
||||
self.assertFalse(self.can_reach_location(song + "-1"), f"Location {song}-1 was unlocked without an item.")
|
||||
self.collect_by_name(song)
|
||||
self.assertTrue(self.can_reach_location(song + "-0"), f"Location {song}-0 was not unlocked with its item.")
|
||||
self.assertTrue(self.can_reach_location(song + "-1"), f"Location {song}-1 was not unlocked with its item.")
|
||||
|
||||
|
||||
sheets = self.get_items_by_name("Music Sheet")
|
||||
sheets_to_win = muse_dash_world.get_music_sheet_win_count()
|
||||
|
||||
for sheet in sheets:
|
||||
if sheets_to_win <= 0:
|
||||
break
|
||||
|
||||
self.assertBeatable(False)
|
||||
self.collect(sheet)
|
||||
sheets_to_win -= 1
|
||||
|
||||
self.assertBeatable(True)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user