Merge remote-tracking branch 'remotes/upstream/main'

This commit is contained in:
massimilianodelliubaldini
2025-04-24 22:52:15 -04:00
169 changed files with 2147 additions and 11279 deletions

View File

@@ -223,7 +223,7 @@ class MultiWorld():
AutoWorld.AutoWorldRegister.world_types[self.game[player]].options_dataclass.type_hints}
for option_key in all_keys:
option = Utils.DeprecateDict(f"Getting options from multiworld is now deprecated. "
f"Please use `self.options.{option_key}` instead.")
f"Please use `self.options.{option_key}` instead.", True)
option.update(getattr(args, option_key, {}))
setattr(self, option_key, option)
@@ -1022,9 +1022,6 @@ class Entrance:
connected_region: Optional[Region] = None
randomization_group: int
randomization_type: EntranceType
# LttP specific, TODO: should make a LttPEntrance
addresses = None
target = None
def __init__(self, player: int, name: str = "", parent: Optional[Region] = None,
randomization_group: int = 0, randomization_type: EntranceType = EntranceType.ONE_WAY) -> None:
@@ -1043,10 +1040,8 @@ class Entrance:
return False
def connect(self, region: Region, addresses: Any = None, target: Any = None) -> None:
def connect(self, region: Region) -> None:
self.connected_region = region
self.target = target
self.addresses = addresses
region.entrances.append(self)
def is_valid_source_transition(self, er_state: "ERPlacementState") -> bool:
@@ -1203,6 +1198,48 @@ class Region:
for location, address in locations.items():
self.locations.append(location_type(self.player, location, address, self))
def add_event(
self,
location_name: str,
item_name: str | None = None,
rule: Callable[[CollectionState], bool] | None = None,
location_type: type[Location] | None = None,
item_type: type[Item] | None = None,
show_in_spoiler: bool = True,
) -> Item:
"""
Adds an event location/item pair to the region.
:param location_name: Name for the event location.
:param item_name: Name for the event item. If not provided, defaults to location_name.
:param rule: Callable to determine access for this event location within its region.
:param location_type: Location class to create the event location with. Defaults to BaseClasses.Location.
:param item_type: Item class to create the event item with. Defaults to BaseClasses.Item.
:param show_in_spoiler: Will be passed along to the created event Location's show_in_spoiler attribute.
:return: The created Event Item
"""
if location_type is None:
location_type = Location
if item_name is None:
item_name = location_name
if item_type is None:
item_type = Item
event_location = location_type(self.player, location_name, None, self)
event_location.show_in_spoiler = show_in_spoiler
if rule is not None:
event_location.access_rule = rule
event_item = item_type(item_name, ItemClassification.progression, None, self.player)
event_location.place_locked_item(event_item)
self.locations.append(event_location)
return event_item
def connect(self, connecting_region: Region, name: Optional[str] = None,
rule: Optional[Callable[[CollectionState], bool]] = None) -> Entrance:
"""

View File

@@ -456,6 +456,14 @@ def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str,
def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.bosses):
"""
Roll options from specified weights, usually originating from a .yaml options file.
Important note:
The same weights dict is shared between all slots using the same yaml (e.g. generic weights file for filler slots).
This means it should never be modified without making a deepcopy first.
"""
from worlds import AutoWorldRegister
if "linked_options" in weights:

View File

@@ -1,11 +1,11 @@
"""
Archipelago Launcher
* if run with APBP as argument, launch corresponding client.
* if run with executable as argument, run it passing argv[2:] as arguments
* if run without arguments, open launcher GUI
* If run with a patch file as argument, launch corresponding client with the patch file as an argument.
* If run with component name as argument, run it passing argv[2:] as arguments.
* If run without arguments or unknown arguments, open launcher GUI.
Scroll down to components= to add components to the launcher as well as setup.py
Additional components can be added to worlds.LauncherComponents.components.
"""
import argparse
@@ -230,10 +230,11 @@ def run_gui(path: str, args: Any) -> None:
from kivy.properties import ObjectProperty
from kivy.core.window import Window
from kivy.metrics import dp
from kivymd.uix.button import MDIconButton
from kivymd.uix.button import MDIconButton, MDButton
from kivymd.uix.card import MDCard
from kivymd.uix.menu import MDDropdownMenu
from kivymd.uix.snackbar import MDSnackbar, MDSnackbarText
from kivymd.uix.textfield import MDTextField
from kivy.lang.builder import Builder
@@ -253,6 +254,7 @@ def run_gui(path: str, args: Any) -> None:
navigation: MDGridLayout = ObjectProperty(None)
grid: MDGridLayout = ObjectProperty(None)
button_layout: ScrollBox = ObjectProperty(None)
search_box: MDTextField = ObjectProperty(None)
cards: list[LauncherCard]
current_filter: Sequence[str | Type] | None
@@ -338,14 +340,29 @@ def run_gui(path: str, args: Any) -> None:
scroll_percent = self.button_layout.convert_distance_to_scroll(0, top)
self.button_layout.scroll_y = max(0, min(1, scroll_percent[1]))
def filter_clients(self, caller):
def filter_clients_by_type(self, caller: MDButton):
self._refresh_components(caller.type)
self.search_box.text = ""
def filter_clients_by_name(self, caller: MDTextField, name: str) -> None:
if len(name) == 0:
self._refresh_components(self.current_filter)
return
sub_matches = [
card for card in self.cards
if name.lower() in card.component.display_name.lower() and card.component.type != Type.HIDDEN
]
self.button_layout.layout.clear_widgets()
for card in sub_matches:
self.button_layout.layout.add_widget(card)
def build(self):
self.top_screen = Builder.load_file(Utils.local_path("data/launcher.kv"))
self.grid = self.top_screen.ids.grid
self.navigation = self.top_screen.ids.navigation
self.button_layout = self.top_screen.ids.button_layout
self.search_box = self.top_screen.ids.search_box
self.set_colors()
self.top_screen.md_bg_color = self.theme_cls.backgroundColor
@@ -353,6 +370,7 @@ def run_gui(path: str, args: Any) -> None:
refresh_components = self._refresh_components
Window.bind(on_drop_file=self._on_drop_file)
Window.bind(on_keyboard=self._on_keyboard)
for component in components:
self.cards.append(self.build_card(component))
@@ -389,6 +407,15 @@ def run_gui(path: str, args: Any) -> None:
else:
logging.warning(f"unable to identify component for {file}")
def _on_keyboard(self, window: Window, key: int, scancode: int, codepoint: str, modifier: list[str]):
# Activate search as soon as we start typing, no matter if we are focused on the search box or not.
# Focus first, then capture the first character we type, otherwise it gets swallowed and lost.
# Limit text input to ASCII non-control characters (space bar to tilde).
if not self.search_box.focus:
self.search_box.focus = True
if key in range(32, 126):
self.search_box.text += codepoint
def _stop(self, *largs):
# ran into what appears to be https://groups.google.com/g/kivy-users/c/saWDLoYCSZ4 with PyCharm.
# Closing the window explicitly cleans it up.

20
Main.py
View File

@@ -56,29 +56,15 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
logger.info(f"Found {len(AutoWorld.AutoWorldRegister.world_types)} World Types:")
longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types)
max_item = 0
max_location = 0
for cls in AutoWorld.AutoWorldRegister.world_types.values():
if cls.item_id_to_name:
max_item = max(max_item, max(cls.item_id_to_name))
max_location = max(max_location, max(cls.location_id_to_name))
item_digits = len(str(max_item))
location_digits = len(str(max_location))
item_count = len(str(max(len(cls.item_names) for cls in AutoWorld.AutoWorldRegister.world_types.values())))
location_count = len(str(max(len(cls.location_names) for cls in AutoWorld.AutoWorldRegister.world_types.values())))
del max_item, max_location
for name, cls in AutoWorld.AutoWorldRegister.world_types.items():
if not cls.hidden and len(cls.item_names) > 0:
logger.info(f" {name:{longest_name}}: {len(cls.item_names):{item_count}} "
f"Items (IDs: {min(cls.item_id_to_name):{item_digits}} - "
f"{max(cls.item_id_to_name):{item_digits}}) | "
f"{len(cls.location_names):{location_count}} "
f"Locations (IDs: {min(cls.location_id_to_name):{location_digits}} - "
f"{max(cls.location_id_to_name):{location_digits}})")
logger.info(f" {name:{longest_name}}: Items: {len(cls.item_names):{item_count}} | "
f"Locations: {len(cls.location_names):{location_count}}")
del item_digits, location_digits, item_count, location_count
del item_count, location_count
# This assertion method should not be necessary to run if we are not outputting any multidata.
if not args.skip_output and not args.spoiler_only:

View File

@@ -46,7 +46,8 @@ from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, Networ
SlotType, LocationStore, Hint, HintStatus
from BaseClasses import ItemClassification
min_client_version = Version(0, 1, 6)
min_client_version = Version(0, 5, 0)
colorama.just_fix_windows_console()
@@ -1982,11 +1983,13 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
new_hint = new_hint.re_prioritize(ctx, status)
if hint == new_hint:
return
ctx.replace_hint(client.team, hint.finding_player, hint, new_hint)
ctx.replace_hint(client.team, hint.receiving_player, hint, new_hint)
concerning_slots = ctx.slot_set(hint.receiving_player) | {hint.finding_player}
for slot in concerning_slots:
ctx.replace_hint(client.team, slot, hint, new_hint)
ctx.save()
ctx.on_changed_hints(client.team, hint.finding_player)
ctx.on_changed_hints(client.team, hint.receiving_player)
for slot in concerning_slots:
ctx.on_changed_hints(client.team, slot)
elif cmd == 'StatusUpdate':
update_client_status(ctx, client, args["status"])
@@ -2416,8 +2419,10 @@ async def console(ctx: Context):
def parse_args() -> argparse.Namespace:
from settings import get_settings
parser = argparse.ArgumentParser()
defaults = Utils.get_settings()["server_options"].as_dict()
defaults = get_settings().server_options.as_dict()
parser.add_argument('multidata', nargs="?", default=defaults["multidata"])
parser.add_argument('--host', default=defaults["host"])
parser.add_argument('--port', default=defaults["port"], type=int)

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
import abc
import collections
import functools
import logging
import math
@@ -866,15 +867,49 @@ class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mappin
def __len__(self) -> int:
return self.value.__len__()
# __getitem__ fallback fails for Counters, so we define this explicitly
def __contains__(self, item) -> bool:
return item in self.value
class ItemDict(OptionDict):
class OptionCounter(OptionDict):
min: int | None = None
max: int | None = None
def __init__(self, value: dict[str, int]) -> None:
super(OptionCounter, self).__init__(collections.Counter(value))
def verify(self, world: type[World], player_name: str, plando_options: PlandoOptions) -> None:
super(OptionCounter, self).verify(world, player_name, plando_options)
range_errors = []
if self.max is not None:
range_errors += [
f"\"{key}: {value}\" is higher than maximum allowed value {self.max}."
for key, value in self.value.items() if value > self.max
]
if self.min is not None:
range_errors += [
f"\"{key}: {value}\" is lower than minimum allowed value {self.min}."
for key, value in self.value.items() if value < self.min
]
if range_errors:
range_errors = [f"For option {getattr(self, 'display_name', self)}:"] + range_errors
raise OptionError("\n".join(range_errors))
class ItemDict(OptionCounter):
verify_item_name = True
def __init__(self, value: typing.Dict[str, int]):
if any(item_count is None for item_count in value.values()):
raise Exception("Items must have counts associated with them. Please provide positive integer values in the format \"item\": count .")
if any(item_count < 1 for item_count in value.values()):
raise Exception("Cannot have non-positive item counts.")
min = 0
def __init__(self, value: dict[str, int]) -> None:
# Backwards compatibility: Cull 0s to make "in" checks behave the same as when this wasn't a OptionCounter
value = {item_name: amount for item_name, amount in value.items() if amount != 0}
super(ItemDict, self).__init__(value)

View File

@@ -63,7 +63,6 @@ Currently, the following games are supported:
* TUNIC
* Kirby's Dream Land 3
* Celeste 64
* Zork Grand Inquisitor
* Castlevania 64
* A Short Hike
* Yoshi's Island

View File

@@ -114,6 +114,8 @@ def cache_self1(function: typing.Callable[[S, T], RetType]) -> typing.Callable[[
cache[arg] = res
return res
wrap.__defaults__ = function.__defaults__
return wrap
@@ -427,6 +429,9 @@ class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module: str, name: str) -> type:
if module == "builtins" and name in safe_builtins:
return getattr(builtins, name)
# used by OptionCounter
if module == "collections" and name == "Counter":
return collections.Counter
# used by MultiServer -> savegame/multidata
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint",
"SlotType", "NetworkSlot", "HintStatus"}:

View File

@@ -108,7 +108,7 @@ def option_presets(game: str) -> Response:
f"Expected {option.special_range_names.keys()} or {option.range_start}-{option.range_end}."
presets[preset_name][preset_option_name] = option.value
elif isinstance(option, (Options.Range, Options.OptionSet, Options.OptionList, Options.ItemDict)):
elif isinstance(option, (Options.Range, Options.OptionSet, Options.OptionList, Options.OptionCounter)):
presets[preset_name][preset_option_name] = option.value
elif isinstance(preset_option, str):
# Ensure the option value is valid for Choice and Toggle options
@@ -222,7 +222,7 @@ def generate_yaml(game: str):
for key, val in options.copy().items():
key_parts = key.rsplit("||", 2)
# Detect and build ItemDict options from their name pattern
# Detect and build OptionCounter options from their name pattern
if key_parts[-1] == "qty":
if key_parts[0] not in options:
options[key_parts[0]] = {}

View File

@@ -111,10 +111,19 @@
</div>
{% endmacro %}
{% macro ItemDict(option_name, option) %}
{% macro OptionCounter(option_name, option) %}
{% set relevant_keys = option.valid_keys %}
{% if not relevant_keys %}
{% if option.verify_item_name %}
{% set relevant_keys = world.item_names %}
{% elif option.verify_location_name %}
{% set relevant_keys = world.location_names %}
{% endif %}
{% endif %}
{{ OptionTitle(option_name, option) }}
<div class="option-container">
{% for item_name in (option.valid_keys|sort if (option.valid_keys|length > 0) else world.item_names|sort) %}
{% for item_name in (relevant_keys if relevant_keys is ordered else relevant_keys|sort) %}
<div class="option-entry">
<label for="{{ option_name }}-{{ item_name }}-qty">{{ item_name }}</label>
<input type="number" id="{{ option_name }}-{{ item_name }}-qty" name="{{ option_name }}||{{ item_name }}||qty" value="{{ option.default[item_name]|default("0") }}" data-option-name="{{ option_name }}" data-item-name="{{ item_name }}" />

View File

@@ -93,8 +93,10 @@
{% elif issubclass(option, Options.FreeText) %}
{{ inputs.FreeText(option_name, option) }}
{% elif issubclass(option, Options.ItemDict) and option.verify_item_name %}
{{ inputs.ItemDict(option_name, option) }}
{% elif issubclass(option, Options.OptionCounter) and (
option.valid_keys or option.verify_item_name or option.verify_location_name
) %}
{{ inputs.OptionCounter(option_name, option) }}
{% elif issubclass(option, Options.OptionList) and option.valid_keys %}
{{ inputs.OptionList(option_name, option) }}
@@ -133,8 +135,10 @@
{% elif issubclass(option, Options.FreeText) %}
{{ inputs.FreeText(option_name, option) }}
{% elif issubclass(option, Options.ItemDict) and option.verify_item_name %}
{{ inputs.ItemDict(option_name, option) }}
{% elif issubclass(option, Options.OptionCounter) and (
option.valid_keys or option.verify_item_name or option.verify_location_name
) %}
{{ inputs.OptionCounter(option_name, option) }}
{% elif issubclass(option, Options.OptionList) and option.valid_keys %}
{{ inputs.OptionList(option_name, option) }}

View File

@@ -113,9 +113,18 @@
{{ TextChoice(option_name, option) }}
{% endmacro %}
{% macro ItemDict(option_name, option, world) %}
{% macro OptionCounter(option_name, option, world) %}
{% set relevant_keys = option.valid_keys %}
{% if not relevant_keys %}
{% if option.verify_item_name %}
{% set relevant_keys = world.item_names %}
{% elif option.verify_location_name %}
{% set relevant_keys = world.location_names %}
{% endif %}
{% endif %}
<div class="dict-container">
{% for item_name in (option.valid_keys|sort if (option.valid_keys|length > 0) else world.item_names|sort) %}
{% for item_name in (relevant_keys if relevant_keys is ordered else relevant_keys|sort) %}
<div class="dict-entry">
<label for="{{ option_name }}-{{ item_name }}-qty">{{ item_name }}</label>
<input

View File

@@ -83,8 +83,10 @@
{% elif issubclass(option, Options.FreeText) %}
{{ inputs.FreeText(option_name, option) }}
{% elif issubclass(option, Options.ItemDict) and option.verify_item_name %}
{{ inputs.ItemDict(option_name, option, world) }}
{% elif issubclass(option, Options.OptionCounter) and (
option.valid_keys or option.verify_item_name or option.verify_location_name
) %}
{{ inputs.OptionCounter(option_name, option, world) }}
{% elif issubclass(option, Options.OptionList) and option.valid_keys %}
{{ inputs.OptionList(option_name, option) }}

View File

@@ -80,7 +80,7 @@ MDFloatLayout:
id: all
style: "text"
type: (Type.CLIENT, Type.TOOL, Type.ADJUSTER, Type.MISC)
on_release: app.filter_clients(self)
on_release: app.filter_clients_by_type(self)
MDButtonIcon:
icon: "asterisk"
@@ -90,7 +90,7 @@ MDFloatLayout:
id: client
style: "text"
type: (Type.CLIENT, )
on_release: app.filter_clients(self)
on_release: app.filter_clients_by_type(self)
MDButtonIcon:
icon: "controller"
@@ -100,7 +100,7 @@ MDFloatLayout:
id: Tool
style: "text"
type: (Type.TOOL, )
on_release: app.filter_clients(self)
on_release: app.filter_clients_by_type(self)
MDButtonIcon:
icon: "desktop-classic"
@@ -110,7 +110,7 @@ MDFloatLayout:
id: adjuster
style: "text"
type: (Type.ADJUSTER, )
on_release: app.filter_clients(self)
on_release: app.filter_clients_by_type(self)
MDButtonIcon:
icon: "wrench"
@@ -120,7 +120,7 @@ MDFloatLayout:
id: misc
style: "text"
type: (Type.MISC, )
on_release: app.filter_clients(self)
on_release: app.filter_clients_by_type(self)
MDButtonIcon:
icon: "dots-horizontal-circle-outline"
@@ -131,7 +131,7 @@ MDFloatLayout:
id: favorites
style: "text"
type: ("favorites", )
on_release: app.filter_clients(self)
on_release: app.filter_clients_by_type(self)
MDButtonIcon:
icon: "star"
@@ -141,5 +141,21 @@ MDFloatLayout:
MDNavigationDrawerDivider:
ScrollBox:
id: button_layout
MDGridLayout:
id: main_layout
cols: 1
spacing: "10dp"
MDTextField:
id: search_box
mode: "outlined"
set_text: app.filter_clients_by_name
MDTextFieldLeadingIcon:
icon: "magnify"
MDTextFieldHintText:
text: "Search"
ScrollBox:
id: button_layout

View File

@@ -235,10 +235,6 @@
# Zillion
/worlds/zillion/ @beauxq
# Zork Grand Inquisitor
/worlds/zork_grand_inquisitor/ @nbrochu
## Active Unmaintained Worlds
# The following worlds in this repo are currently unmaintained, but currently still work in core. If any update breaks

View File

@@ -352,8 +352,15 @@ template. If you set a [Schema](https://pypi.org/project/schema/) on the class w
options system will automatically validate the user supplied data against the schema to ensure it's in the correct
format.
### OptionCounter
This is a special case of OptionDict where the dictionary values can only be integers.
It returns a [collections.Counter](https://docs.python.org/3/library/collections.html#collections.Counter).
This means that if you access a key that isn't present, its value will be 0.
The upside of using an OptionCounter (instead of an OptionDict with integer values) is that an OptionCounter can be
displayed on the Options page on WebHost.
### ItemDict
Like OptionDict, except this will verify that every key in the dictionary is a valid name for an item for your world.
An OptionCounter that will verify that every key in the dictionary is a valid name for an item for your world.
### OptionList
This option defines a List, where the user can add any number of strings to said list, allowing duplicate values. You

View File

@@ -561,7 +561,7 @@ from .items import is_progression # this is just a dummy
def create_item(self, item: str) -> MyGameItem:
# this is called when AP wants to create an item by name (for plando) or when you call it from your own code
# this is called when AP wants to create an item by name (for plando, start inventory, item links) or when you call it from your own code
classification = ItemClassification.progression if is_progression(item) else ItemClassification.filler
return MyGameItem(item, classification, self.item_name_to_id[item], self.player)

View File

@@ -45,7 +45,8 @@ MinVersion={#min_windows}
Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}";
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}";
Name: "deletelib"; Description: "Clean existing /lib folder and subfolders including /worlds (leave checked if unsure)"; Check: ShouldShowDeleteLibTask
[Types]
Name: "full"; Description: "Full installation"
@@ -83,18 +84,8 @@ Filename: "{app}\ArchipelagoLauncher"; Description: "{cm:LaunchProgram,{#StringC
Type: dirifempty; Name: "{app}"
[InstallDelete]
Type: files; Name: "{app}\lib\worlds\_bizhawk.apworld"
Type: files; Name: "{app}\ArchipelagoLttPClient.exe"
Type: files; Name: "{app}\ArchipelagoPokemonClient.exe"
Type: files; Name: "{app}\*.exe"
Type: files; Name: "{app}\data\lua\connector_pkmn_rb.lua"
Type: filesandordirs; Name: "{app}\lib\worlds\rogue-legacy"
Type: dirifempty; Name: "{app}\lib\worlds\rogue-legacy"
Type: files; Name: "{app}\lib\worlds\sc2wol.apworld"
Type: filesandordirs; Name: "{app}\lib\worlds\sc2wol"
Type: dirifempty; Name: "{app}\lib\worlds\sc2wol"
Type: filesandordirs; Name: "{app}\lib\worlds\bk_sudoku"
Type: dirifempty; Name: "{app}\lib\worlds\bk_sudoku"
Type: files; Name: "{app}\ArchipelagoLauncher(DEBUG).exe"
Type: filesandordirs; Name: "{app}\SNI\lua*"
Type: filesandordirs; Name: "{app}\EnemizerCLI*"
#include "installdelete.iss"
@@ -261,3 +252,17 @@ begin
Result := True;
end;
end;
function ShouldShowDeleteLibTask: Boolean;
begin
Result := DirExists(ExpandConstant('{app}\lib'));
end;
procedure CurStepChanged(CurStep: TSetupStep);
begin
if CurStep = ssInstall then
begin
if WizardIsTaskSelected('deletelib') then
DelTree(ExpandConstant('{app}\lib'), True, True, True);
end;
end;

View File

@@ -98,7 +98,7 @@ class ThemedApp(MDApp):
class ImageIcon(MDButtonIcon, AsyncImage):
def __init__(self, *args, **kwargs):
super().__init__(args, kwargs)
super().__init__(*args, **kwargs)
self.image = ApAsyncImage(**kwargs)
self.add_widget(self.image)
@@ -183,15 +183,17 @@ class ResizableTextField(MDTextField):
height_rule = subclass.properties.get("height", None)
if height_rule:
height_rule.ignore_prev = True
super().__init__(args, kwargs)
super().__init__(*args, **kwargs)
def on_release(self: MDButton, *args):
super(MDButton, self).on_release(args)
self.on_leave()
MDButton.on_release = on_release
# I was surprised to find this didn't already exist in kivy :(
class HoverBehavior(object):
"""originally from https://stackoverflow.com/a/605348110"""
@@ -904,7 +906,7 @@ class GameManager(ThemedApp):
pos_hint={"center_y": 0.575})
info_button.bind(on_release=self.command_button_action)
bottom_layout.add_widget(info_button)
self.textinput = CommandPromptTextInput(size_hint_y=None, height=dp(30), multiline=False, write_tab=False)
self.textinput = CommandPromptTextInput(size_hint_y=None, multiline=False, write_tab=False)
self.textinput.bind(on_text_validate=self.on_message)
info_button.height = self.textinput.height
self.textinput.text_validate_unfocus = False

View File

@@ -154,7 +154,7 @@ if os.path.exists("X:/pw.txt"):
with open("X:/pw.txt", encoding="utf-8-sig") as f:
pw = f.read()
signtool = r'signtool sign /f X:/_SITS_Zertifikat_.pfx /p "' + pw + \
r'" /fd sha256 /tr http://timestamp.digicert.com/ '
r'" /fd sha256 /td sha256 /tr http://timestamp.digicert.com/ '
else:
signtool = None

View File

@@ -1,3 +1,4 @@
from typing import Callable
import unittest
from enum import IntEnum
@@ -34,7 +35,7 @@ def generate_entrance_pair(region: Region, name_suffix: str, group: int):
def generate_disconnected_region_grid(multiworld: MultiWorld, grid_side_length: int, region_size: int = 0,
region_type: type[Region] = Region):
region_creator: Callable[[str, int, MultiWorld], Region] = Region):
"""
Generates a grid-like region structure for ER testing, where menu is connected to the top-left region, and each
region "in vanilla" has 2 2-way exits going either down or to the right, until reaching the goal region in the
@@ -44,7 +45,7 @@ def generate_disconnected_region_grid(multiworld: MultiWorld, grid_side_length:
for col in range(grid_side_length):
index = row * grid_side_length + col
name = f"region{index}"
region = region_type(name, 1, multiworld)
region = region_creator(name, 1, multiworld)
multiworld.regions.append(region)
generate_locations(region_size, 1, region=region, tag=f"_{name}")
@@ -465,7 +466,7 @@ class TestRandomizeEntrances(unittest.TestCase):
entrance_type = CustomEntrance
multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 5, region_type=CustomRegion)
generate_disconnected_region_grid(multiworld, 5, region_creator=CustomRegion)
self.assertRaises(EntranceRandomizationError, randomize_entrances, multiworld.worlds[1], False,
directionally_matched_group_lookup)

View File

@@ -47,13 +47,39 @@ class TestIDs(unittest.TestCase):
"""Test that a game doesn't have item id overlap within its own datapackage"""
for gamename, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game=gamename):
self.assertEqual(len(world_type.item_id_to_name), len(world_type.item_name_to_id))
len_item_id_to_name = len(world_type.item_id_to_name)
len_item_name_to_id = len(world_type.item_name_to_id)
if len_item_id_to_name != len_item_name_to_id:
self.assertCountEqual(
world_type.item_id_to_name.values(),
world_type.item_name_to_id.keys(),
"\nThese items have overlapping ids with other items in its own world")
self.assertCountEqual(
world_type.item_id_to_name.keys(),
world_type.item_name_to_id.values(),
"\nThese items have overlapping names with other items in its own world")
self.assertEqual(len_item_id_to_name, len_item_name_to_id)
def test_duplicate_location_ids(self):
"""Test that a game doesn't have location id overlap within its own datapackage"""
for gamename, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game=gamename):
self.assertEqual(len(world_type.location_id_to_name), len(world_type.location_name_to_id))
len_location_id_to_name = len(world_type.location_id_to_name)
len_location_name_to_id = len(world_type.location_name_to_id)
if len_location_id_to_name != len_location_name_to_id:
self.assertCountEqual(
world_type.location_id_to_name.values(),
world_type.location_name_to_id.keys(),
"\nThese locations have overlapping ids with other locations in its own world")
self.assertCountEqual(
world_type.location_id_to_name.keys(),
world_type.location_name_to_id.values(),
"\nThese locations have overlapping names with other locations in its own world")
self.assertEqual(len_location_id_to_name, len_location_name_to_id)
def test_postgen_datapackage(self):
"""Generates a solo multiworld and checks that the datapackage is still valid"""

View File

@@ -1,7 +1,11 @@
import unittest
from argparse import Namespace
from typing import Type
from BaseClasses import CollectionState
from worlds.AutoWorld import AutoWorldRegister, call_all
from BaseClasses import CollectionState, MultiWorld
from Fill import distribute_items_restrictive
from Options import ItemLinks
from worlds.AutoWorld import AutoWorldRegister, World, call_all
from . import setup_solo_multiworld
@@ -83,6 +87,47 @@ class TestBase(unittest.TestCase):
multiworld = setup_solo_multiworld(world_type)
for item in multiworld.itempool:
self.assertIn(item.name, world_type.item_name_to_id)
def test_item_links(self) -> None:
"""
Tests item link creation by creating a multiworld of 2 worlds for every game and linking their items together.
"""
def setup_link_multiworld(world: Type[World], link_replace: bool) -> None:
multiworld = MultiWorld(2)
multiworld.game = {1: world.game, 2: world.game}
multiworld.player_name = {1: "Linker 1", 2: "Linker 2"}
multiworld.set_seed()
item_link_group = [{
"name": "ItemLinkTest",
"item_pool": ["Everything"],
"link_replacement": link_replace,
"replacement_item": None,
}]
args = Namespace()
for name, option in world.options_dataclass.type_hints.items():
setattr(args, name, {1: option.from_any(option.default), 2: option.from_any(option.default)})
setattr(args, "item_links",
{1: ItemLinks.from_any(item_link_group), 2: ItemLinks.from_any(item_link_group)})
multiworld.set_options(args)
multiworld.set_item_links()
# groups get added to state during its constructor so this has to be after item links are set
multiworld.state = CollectionState(multiworld)
gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "connect_entrances", "generate_basic")
for step in gen_steps:
call_all(multiworld, step)
# link the items together and attempt to fill
multiworld.link_items()
multiworld._all_state = None
call_all(multiworld, "pre_fill")
distribute_items_restrictive(multiworld)
call_all(multiworld, "post_fill")
self.assertTrue(multiworld.can_beat_game(CollectionState(multiworld)), f"seed = {multiworld.seed}")
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest("Can generate with link replacement", game=game_name):
setup_link_multiworld(world_type, True)
with self.subTest("Can generate without link replacement", game=game_name):
setup_link_multiworld(world_type, False)
def test_itempool_not_modified(self):
"""Test that worlds don't modify the itempool after `create_items`"""

View File

@@ -80,8 +80,8 @@ class Client:
"version": {
"class": "Version",
"major": 0,
"minor": 4,
"build": 6,
"minor": 6,
"build": 0,
},
"items_handling": 0,
"tags": [],

View File

@@ -35,6 +35,19 @@ class TestCacheSelf1(unittest.TestCase):
self.assertFalse(o1.func(1) is o1.func(2))
self.assertFalse(o1.func(1) is o2.func(1))
def test_cache_default(self) -> None:
class Cls:
@cache_self1
def func(self, _: Any = 1) -> object:
return object()
o1 = Cls()
o2 = Cls()
self.assertIs(o1.func(), o1.func())
self.assertIs(o1.func(1), o1.func())
self.assertIsNot(o1.func(2), o1.func())
self.assertIsNot(o1.func(), o2.func())
def test_gc(self) -> None:
# verify that we don't keep a global reference
import gc

View File

@@ -2,7 +2,7 @@ import unittest
from BaseClasses import PlandoOptions
from worlds import AutoWorldRegister
from Options import ItemDict, NamedRange, NumericOption, OptionList, OptionSet
from Options import OptionCounter, NamedRange, NumericOption, OptionList, OptionSet
class TestOptionPresets(unittest.TestCase):
@@ -19,7 +19,7 @@ class TestOptionPresets(unittest.TestCase):
# pass in all plando options in case a preset wants to require certain plando options
# for some reason
option.verify(world_type, "Test Player", PlandoOptions(sum(PlandoOptions)))
supported_types = [NumericOption, OptionSet, OptionList, ItemDict]
supported_types = [NumericOption, OptionSet, OptionList, OptionCounter]
if not any([issubclass(option.__class__, t) for t in supported_types]):
self.fail(f"'{option_name}' in preset '{preset_name}' for game '{game_name}' "
f"is not a supported type for webhost. "

View File

@@ -12,7 +12,7 @@ def load_tests(loader, standard_tests, pattern):
all_tests = [
test_case for folder in folders if os.path.exists(folder)
for test_collection in loader.discover(folder, top_level_dir=file_path)
for test_suite in test_collection
for test_suite in test_collection if isinstance(test_suite, unittest.suite.TestSuite)
for test_case in test_suite
]

View File

@@ -12,6 +12,7 @@ from typing import (Any, Callable, ClassVar, Dict, FrozenSet, Iterable, List, Ma
from Options import item_and_loc_options, ItemsAccessibility, OptionGroup, PerGameCommonOptions
from BaseClasses import CollectionState
from Utils import deprecate
if TYPE_CHECKING:
from BaseClasses import MultiWorld, Item, Location, Tutorial, Region, Entrance
@@ -75,19 +76,20 @@ class AutoWorldRegister(type):
# TODO - remove this once all worlds use options dataclasses
if "options_dataclass" not in dct and "option_definitions" in dct:
# TODO - switch to deprecate after a version
if __debug__:
logging.warning(f"{name} Assigned options through option_definitions which is now deprecated. "
"Please use options_dataclass instead.")
deprecate(f"{name} Assigned options through option_definitions which is now deprecated. "
"Please use options_dataclass instead.")
dct["options_dataclass"] = make_dataclass(f"{name}Options", dct["option_definitions"].items(),
bases=(PerGameCommonOptions,))
# construct class
new_class = super().__new__(mcs, name, bases, dct)
new_class.__file__ = sys.modules[new_class.__module__].__file__
if "game" in dct:
if dct["game"] in AutoWorldRegister.world_types:
raise RuntimeError(f"""Game {dct["game"]} already registered.""")
raise RuntimeError(f"""Game {dct["game"]} already registered in
{AutoWorldRegister.world_types[dct["game"]].__file__} when attempting to register from
{new_class.__file__}.""")
AutoWorldRegister.world_types[dct["game"]] = new_class
new_class.__file__ = sys.modules[new_class.__module__].__file__
if ".apworld" in new_class.__file__:
new_class.zip_path = pathlib.Path(new_class.__file__).parents[1]
if "settings_key" not in dct:

View File

@@ -102,7 +102,7 @@ def KholdstareDefeatRule(state, player: int) -> bool:
state.has('Fire Rod', player) or
(
state.has('Bombos', player) and
(has_sword(state, player) or state.multiworld.swordless[player])
(has_sword(state, player) or state.multiworld.worlds[player].options.swordless)
)
) and
(
@@ -111,7 +111,7 @@ def KholdstareDefeatRule(state, player: int) -> bool:
(
state.has('Fire Rod', player) and
state.has('Bombos', player) and
state.multiworld.swordless[player] and
state.multiworld.worlds[player].options.swordless and
can_extend_magic(state, player, 16)
)
)
@@ -137,7 +137,7 @@ def AgahnimDefeatRule(state, player: int) -> bool:
def GanonDefeatRule(state, player: int) -> bool:
if state.multiworld.swordless[player]:
if state.multiworld.worlds[player].options.swordless:
return state.has('Hammer', player) and \
has_fire_source(state, player) and \
state.has('Silver Bow', player) and \
@@ -146,7 +146,7 @@ def GanonDefeatRule(state, player: int) -> bool:
can_hurt = has_beam_sword(state, player)
common = can_hurt and has_fire_source(state, player)
# silverless ganon may be needed in anything higher than no glitches
if state.multiworld.glitches_required[player] != 'no_glitches':
if state.multiworld.worlds[player].options.glitches_required != 'no_glitches':
# need to light torch a sufficient amount of times
return common and (state.has('Tempered Sword', player) or state.has('Golden Sword', player) or (
state.has('Silver Bow', player) and can_shoot_arrows(state, player)) or
@@ -248,7 +248,7 @@ for location in boss_location_table:
def place_boss(world: "ALTTPWorld", boss: str, location: str, level: Optional[str]) -> None:
player = world.player
if location == 'Ganons Tower' and world.multiworld.mode[player] == 'inverted':
if location == 'Ganons Tower' and world.options.mode == 'inverted':
location = 'Inverted Ganons Tower'
logging.debug('Placing boss %s at %s', boss, location + (' (' + level + ')' if level else ''))
world.dungeons[location].bosses[level] = BossFactory(boss, player)
@@ -260,9 +260,8 @@ def format_boss_location(location_name: str, level: str) -> str:
def place_bosses(world: "ALTTPWorld") -> None:
multiworld = world.multiworld
player = world.player
# will either be an int or a lower case string with ';' between options
boss_shuffle: Union[str, int] = multiworld.boss_shuffle[player].value
boss_shuffle: Union[str, int] = world.options.boss_shuffle.value
already_placed_bosses: List[str] = []
remaining_locations: List[Tuple[str, str]] = []
# handle plando

View File

@@ -66,7 +66,7 @@ def create_dungeons(world: "ALTTPWorld"):
def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dungeon_items):
dungeon = Dungeon(name, dungeon_regions, big_key,
[] if multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal else small_keys,
[] if multiworld.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal else small_keys,
dungeon_items, player)
for item in dungeon.all_items:
item.dungeon = dungeon
@@ -143,7 +143,7 @@ def create_dungeons(world: "ALTTPWorld"):
item_factory(['Small Key (Turtle Rock)'] * 6, world),
item_factory(['Map (Turtle Rock)', 'Compass (Turtle Rock)'], world))
if multiworld.mode[player] != 'inverted':
if multiworld.worlds[player].options.mode != 'inverted':
AT = make_dungeon('Agahnims Tower', 'Agahnim', ['Agahnims Tower', 'Agahnim 1'], None,
item_factory(['Small Key (Agahnims Tower)'] * 4, world), [])
GT = make_dungeon('Ganons Tower', 'Agahnim2',

View File

@@ -23,17 +23,17 @@ def link_entrances(world, player):
connect_simple(world, exitname, regionname, player)
# if we do not shuffle, set default connections
if world.entrance_shuffle[player] == 'vanilla':
if world.worlds[player].options.entrance_shuffle == 'vanilla':
for exitname, regionname in default_connections:
connect_simple(world, exitname, regionname, player)
for exitname, regionname in default_dungeon_connections:
connect_simple(world, exitname, regionname, player)
elif world.entrance_shuffle[player] == 'dungeons_simple':
elif world.worlds[player].options.entrance_shuffle == 'dungeons_simple':
for exitname, regionname in default_connections:
connect_simple(world, exitname, regionname, player)
simple_shuffle_dungeons(world, player)
elif world.entrance_shuffle[player] == 'dungeons_full':
elif world.worlds[player].options.entrance_shuffle == 'dungeons_full':
for exitname, regionname in default_connections:
connect_simple(world, exitname, regionname, player)
@@ -43,7 +43,7 @@ def link_entrances(world, player):
lw_entrances = list(LW_Dungeon_Entrances)
dw_entrances = list(DW_Dungeon_Entrances)
if world.mode[player] == 'standard':
if world.worlds[player].options.mode == 'standard':
# must connect front of hyrule castle to do escape
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
else:
@@ -56,7 +56,7 @@ def link_entrances(world, player):
dw_entrances.append('Ganons Tower')
dungeon_exits.append('Ganons Tower Exit')
if world.mode[player] == 'standard':
if world.worlds[player].options.mode == 'standard':
# rest of hyrule castle must be in light world, so it has to be the one connected to east exit of desert
hyrule_castle_exits = [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')]
connect_mandatory_exits(world, lw_entrances, hyrule_castle_exits, list(LW_Dungeon_Entrances_Must_Exit), player)
@@ -65,9 +65,9 @@ def link_entrances(world, player):
connect_mandatory_exits(world, lw_entrances, dungeon_exits, list(LW_Dungeon_Entrances_Must_Exit), player)
connect_mandatory_exits(world, dw_entrances, dungeon_exits, list(DW_Dungeon_Entrances_Must_Exit), player)
connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player)
elif world.entrance_shuffle[player] == 'dungeons_crossed':
elif world.worlds[player].options.entrance_shuffle == 'dungeons_crossed':
crossed_shuffle_dungeons(world, player)
elif world.entrance_shuffle[player] == 'simple':
elif world.worlds[player].options.entrance_shuffle == 'simple':
simple_shuffle_dungeons(world, player)
old_man_entrances = list(Old_Man_Entrances)
@@ -138,7 +138,7 @@ def link_entrances(world, player):
# place remaining doors
connect_doors(world, single_doors, door_targets, player)
elif world.entrance_shuffle[player] == 'restricted':
elif world.worlds[player].options.entrance_shuffle == 'restricted':
simple_shuffle_dungeons(world, player)
lw_entrances = list(LW_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances)
@@ -210,7 +210,7 @@ def link_entrances(world, player):
# place remaining doors
connect_doors(world, doors, door_targets, player)
elif world.entrance_shuffle[player] == 'full':
elif world.worlds[player].options.entrance_shuffle == 'full':
skull_woods_shuffle(world, player)
lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances)
@@ -227,7 +227,7 @@ def link_entrances(world, player):
# tavern back door cannot be shuffled yet
connect_doors(world, ['Tavern North'], ['Tavern'], player)
if world.mode[player] == 'standard':
if world.worlds[player].options.mode == 'standard':
# must connect front of hyrule castle to do escape
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
else:
@@ -264,7 +264,7 @@ def link_entrances(world, player):
pass
else: #if the cave wasn't placed we get here
connect_caves(world, lw_entrances, [], old_man_house, player)
if world.mode[player] == 'standard':
if world.worlds[player].options.mode == 'standard':
# rest of hyrule castle must be in light world
connect_caves(world, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player)
@@ -316,7 +316,7 @@ def link_entrances(world, player):
# place remaining doors
connect_doors(world, doors, door_targets, player)
elif world.entrance_shuffle[player] == 'crossed':
elif world.worlds[player].options.entrance_shuffle == 'crossed':
skull_woods_shuffle(world, player)
entrances = list(LW_Entrances + LW_Dungeon_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances + DW_Entrances + DW_Dungeon_Entrances + DW_Single_Cave_Doors)
@@ -331,7 +331,7 @@ def link_entrances(world, player):
# tavern back door cannot be shuffled yet
connect_doors(world, ['Tavern North'], ['Tavern'], player)
if world.mode[player] == 'standard':
if world.worlds[player].options.mode == 'standard':
# must connect front of hyrule castle to do escape
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
else:
@@ -348,7 +348,7 @@ def link_entrances(world, player):
#place must-exit caves
connect_mandatory_exits(world, entrances, caves, must_exits, player)
if world.mode[player] == 'standard':
if world.worlds[player].options.mode == 'standard':
# rest of hyrule castle must be dealt with
connect_caves(world, entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player)
@@ -394,7 +394,7 @@ def link_entrances(world, player):
# place remaining doors
connect_doors(world, entrances, door_targets, player)
elif world.entrance_shuffle[player] == 'insanity':
elif world.worlds[player].options.entrance_shuffle == 'insanity':
# beware ye who enter here
entrances = LW_Entrances + LW_Dungeon_Entrances + DW_Entrances + DW_Dungeon_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave']
@@ -431,7 +431,7 @@ def link_entrances(world, player):
# tavern back door cannot be shuffled yet
connect_doors(world, ['Tavern North'], ['Tavern'], player)
if world.mode[player] == 'standard':
if world.worlds[player].options.mode == 'standard':
# cannot move uncle cave
connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player)
connect_exit(world, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player)
@@ -464,7 +464,7 @@ def link_entrances(world, player):
connect_entrance(world, hole, hole_targets.pop(), player)
# hyrule castle handling
if world.mode[player] == 'standard':
if world.worlds[player].options.mode == 'standard':
# must connect front of hyrule castle to do escape
connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
connect_exit(world, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player)
@@ -544,12 +544,12 @@ def link_entrances(world, player):
else:
raise NotImplementedError(
f'{world.entrance_shuffle[player]} Shuffling not supported yet. Player {world.get_player_name(player)}')
f'{world.worlds[player].options.entrance_shuffle} Shuffling not supported yet. Player {world.get_player_name(player)}')
if world.glitches_required[player] in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
if world.worlds[player].options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
overworld_glitch_connections(world, player)
# mandatory hybrid major glitches connections
if world.glitches_required[player] in ['hybrid_major_glitches', 'no_logic']:
if world.worlds[player].options.glitches_required in ['hybrid_major_glitches', 'no_logic']:
underworld_glitch_connections(world, player)
# check for swamp palace fix
@@ -584,17 +584,17 @@ def link_inverted_entrances(world, player):
connect_simple(world, exitname, regionname, player)
# if we do not shuffle, set default connections
if world.entrance_shuffle[player] == 'vanilla':
if world.worlds[player].options.entrance_shuffle == 'vanilla':
for exitname, regionname in inverted_default_connections:
connect_simple(world, exitname, regionname, player)
for exitname, regionname in inverted_default_dungeon_connections:
connect_simple(world, exitname, regionname, player)
elif world.entrance_shuffle[player] == 'dungeons_simple':
elif world.worlds[player].options.entrance_shuffle == 'dungeons_simple':
for exitname, regionname in inverted_default_connections:
connect_simple(world, exitname, regionname, player)
simple_shuffle_dungeons(world, player)
elif world.entrance_shuffle[player] == 'dungeons_full':
elif world.worlds[player].options.entrance_shuffle == 'dungeons_full':
for exitname, regionname in inverted_default_connections:
connect_simple(world, exitname, regionname, player)
@@ -649,9 +649,9 @@ def link_inverted_entrances(world, player):
connect_mandatory_exits(world, lw_entrances, dungeon_exits, lw_dungeon_entrances_must_exit, player)
connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player)
elif world.entrance_shuffle[player] == 'dungeons_crossed':
elif world.worlds[player].options.entrance_shuffle == 'dungeons_crossed':
inverted_crossed_shuffle_dungeons(world, player)
elif world.entrance_shuffle[player] == 'simple':
elif world.worlds[player].options.entrance_shuffle == 'simple':
simple_shuffle_dungeons(world, player)
old_man_entrances = list(Inverted_Old_Man_Entrances)
@@ -748,7 +748,7 @@ def link_inverted_entrances(world, player):
# place remaining doors
connect_doors(world, single_doors, door_targets, player)
elif world.entrance_shuffle[player] == 'restricted':
elif world.worlds[player].options.entrance_shuffle == 'restricted':
simple_shuffle_dungeons(world, player)
lw_entrances = list(Inverted_LW_Entrances + Inverted_LW_Single_Cave_Doors)
@@ -833,7 +833,7 @@ def link_inverted_entrances(world, player):
doors = lw_entrances + dw_entrances
# place remaining doors
connect_doors(world, doors, door_targets, player)
elif world.entrance_shuffle[player] == 'full':
elif world.worlds[player].options.entrance_shuffle == 'full':
skull_woods_shuffle(world, player)
lw_entrances = list(Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_LW_Single_Cave_Doors)
@@ -984,7 +984,7 @@ def link_inverted_entrances(world, player):
# place remaining doors
connect_doors(world, doors, door_targets, player)
elif world.entrance_shuffle[player] == 'crossed':
elif world.worlds[player].options.entrance_shuffle == 'crossed':
skull_woods_shuffle(world, player)
entrances = list(Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_LW_Single_Cave_Doors + Inverted_Old_Man_Entrances + Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + Inverted_DW_Single_Cave_Doors)
@@ -1095,7 +1095,7 @@ def link_inverted_entrances(world, player):
# place remaining doors
connect_doors(world, entrances, door_targets, player)
elif world.entrance_shuffle[player] == 'insanity':
elif world.worlds[player].options.entrance_shuffle == 'insanity':
# beware ye who enter here
entrances = Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + Inverted_Old_Man_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave', 'Hyrule Castle Entrance (South)']
@@ -1254,10 +1254,10 @@ def link_inverted_entrances(world, player):
else:
raise NotImplementedError('Shuffling not supported yet')
if world.glitches_required[player] in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
if world.worlds[player].options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
overworld_glitch_connections(world, player)
# mandatory hybrid major glitches connections
if world.glitches_required[player] in ['hybrid_major_glitches', 'no_logic']:
if world.worlds[player].options.glitches_required in ['hybrid_major_glitches', 'no_logic']:
underworld_glitch_connections(world, player)
# patch swamp drain
@@ -1349,7 +1349,7 @@ def scramble_holes(world, player):
else:
hole_targets.append(('Pyramid Exit', 'Pyramid'))
if world.mode[player] == 'standard':
if world.worlds[player].options.mode == 'standard':
# cannot move uncle cave
connect_two_way(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player)
connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player)
@@ -1358,14 +1358,14 @@ def scramble_holes(world, player):
hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance'))
# do not shuffle sanctuary into pyramid hole unless shuffle is crossed
if world.entrance_shuffle[player] == 'crossed':
if world.worlds[player].options.entrance_shuffle == 'crossed':
hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
if world.shuffle_ganon:
world.random.shuffle(hole_targets)
exit, target = hole_targets.pop()
connect_two_way(world, 'Pyramid Entrance', exit, player)
connect_entrance(world, 'Pyramid Hole', target, player)
if world.entrance_shuffle[player] != 'crossed':
if world.worlds[player].options.entrance_shuffle != 'crossed':
hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
world.random.shuffle(hole_targets)
@@ -1400,14 +1400,14 @@ def scramble_inverted_holes(world, player):
hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance'))
# do not shuffle sanctuary into pyramid hole unless shuffle is crossed
if world.entrance_shuffle[player] == 'crossed':
if world.worlds[player].options.entrance_shuffle == 'crossed':
hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
if world.shuffle_ganon:
world.random.shuffle(hole_targets)
exit, target = hole_targets.pop()
connect_two_way(world, 'Inverted Pyramid Entrance', exit, player)
connect_entrance(world, 'Inverted Pyramid Hole', target, player)
if world.entrance_shuffle[player] != 'crossed':
if world.worlds[player].options.entrance_shuffle != 'crossed':
hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
world.random.shuffle(hole_targets)
@@ -1430,15 +1430,15 @@ def connect_random(world, exitlist, targetlist, player, two_way=False):
def connect_mandatory_exits(world, entrances, caves, must_be_exits, player):
# Keeps track of entrances that cannot be used to access each exit / cave
if world.mode[player] == 'inverted':
if world.worlds[player].options.mode == 'inverted':
invalid_connections = Inverted_Must_Exit_Invalid_Connections.copy()
else:
invalid_connections = Must_Exit_Invalid_Connections.copy()
invalid_cave_connections = defaultdict(set)
if world.glitches_required[player] in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
if world.worlds[player].options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
from . import OverworldGlitchRules
for entrance in OverworldGlitchRules.get_non_mandatory_exits(world.mode[player] == 'inverted'):
for entrance in OverworldGlitchRules.get_non_mandatory_exits(world.worlds[player].options.mode == 'inverted'):
invalid_connections[entrance] = set()
if entrance in must_be_exits:
must_be_exits.remove(entrance)
@@ -1449,7 +1449,7 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player):
world.random.shuffle(caves)
# Handle inverted Aga Tower - if it depends on connections, then so does Hyrule Castle Ledge
if world.mode[player] == 'inverted':
if world.worlds[player].options.mode == 'inverted':
for entrance in invalid_connections:
if world.get_entrance(entrance, player).connected_region == world.get_region('Inverted Agahnims Tower',
player):
@@ -1490,7 +1490,7 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player):
entrance = next(e for e in entrances[::-1] if e not in invalid_connections[exit])
cave_entrances.append(entrance)
entrances.remove(entrance)
connect_two_way(world,entrance,cave_exit, player)
connect_two_way(world, entrance, cave_exit, player)
if entrance not in invalid_connections:
invalid_connections[exit] = set()
if all(entrance in invalid_connections for entrance in cave_entrances):
@@ -1564,7 +1564,7 @@ def simple_shuffle_dungeons(world, player):
dungeon_entrances = ['Eastern Palace', 'Tower of Hera', 'Thieves Town', 'Skull Woods Final Section', 'Palace of Darkness', 'Ice Palace', 'Misery Mire', 'Swamp Palace']
dungeon_exits = ['Eastern Palace Exit', 'Tower of Hera Exit', 'Thieves Town Exit', 'Skull Woods Final Section Exit', 'Palace of Darkness Exit', 'Ice Palace Exit', 'Misery Mire Exit', 'Swamp Palace Exit']
if world.mode[player] != 'inverted':
if world.worlds[player].options.mode != 'inverted':
if not world.shuffle_ganon:
connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player)
else:
@@ -1579,13 +1579,13 @@ def simple_shuffle_dungeons(world, player):
# mix up 4 door dungeons
multi_dungeons = ['Desert', 'Turtle Rock']
if world.mode[player] == 'open' or (world.mode[player] == 'inverted' and world.shuffle_ganon):
if world.worlds[player].options.mode == 'open' or (world.worlds[player].options.mode == 'inverted' and world.shuffle_ganon):
multi_dungeons.append('Hyrule Castle')
world.random.shuffle(multi_dungeons)
dp_target = multi_dungeons[0]
tr_target = multi_dungeons[1]
if world.mode[player] not in ['open', 'inverted'] or (world.mode[player] == 'inverted' and world.shuffle_ganon is False):
if world.worlds[player].options.mode not in ['open', 'inverted'] or (world.worlds[player].options.mode == 'inverted' and world.shuffle_ganon is False):
# place hyrule castle as intended
hc_target = 'Hyrule Castle'
else:
@@ -1593,7 +1593,7 @@ def simple_shuffle_dungeons(world, player):
# ToDo improve this?
if world.mode[player] != 'inverted':
if world.worlds[player].options.mode != 'inverted':
if hc_target == 'Hyrule Castle':
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Hyrule Castle Exit (East)', player)
@@ -1708,7 +1708,7 @@ def crossed_shuffle_dungeons(world, player: int):
dungeon_entrances.append('Ganons Tower')
dungeon_exits.append('Ganons Tower Exit')
if world.mode[player] == 'standard':
if world.worlds[player].options.mode == 'standard':
# must connect front of hyrule castle to do escape
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
else:
@@ -1718,7 +1718,7 @@ def crossed_shuffle_dungeons(world, player: int):
connect_mandatory_exits(world, dungeon_entrances, dungeon_exits,
LW_Dungeon_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit, player)
if world.mode[player] == 'standard':
if world.worlds[player].options.mode == 'standard':
connect_caves(world, dungeon_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player)
connect_caves(world, dungeon_entrances, [], dungeon_exits, player)
@@ -1823,14 +1823,14 @@ lookup = {
def plando_connect(world, player: int):
if world.plando_connections[player]:
for connection in world.plando_connections[player]:
if world.worlds[player].options.plando_connections:
for connection in world.worlds[player].options.plando_connections:
func = lookup[connection.direction]
try:
func(world, connection.entrance, connection.exit, player)
except Exception as e:
raise Exception(f"Could not connect using {connection}") from e
if world.mode[player] != 'inverted':
if world.worlds[player].options.mode != 'inverted':
mark_light_world_regions(world, player)
else:
mark_dark_world_regions(world, player)

View File

@@ -226,25 +226,25 @@ def generate_itempool(world):
player = world.player
multiworld = world.multiworld
if multiworld.item_pool[player].current_key not in difficulties:
raise NotImplementedError(f"Diffulty {multiworld.item_pool[player]}")
if multiworld.goal[player] not in ('ganon', 'pedestal', 'bosses', 'triforce_hunt', 'local_triforce_hunt',
'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'crystals',
'ganon_pedestal'):
raise NotImplementedError(f"Goal {multiworld.goal[player]} for player {player}")
if multiworld.mode[player] not in ('open', 'standard', 'inverted'):
raise NotImplementedError(f"Mode {multiworld.mode[player]} for player {player}")
if multiworld.timer[player] not in (False, 'display', 'timed', 'timed_ohko', 'ohko', 'timed_countdown'):
raise NotImplementedError(f"Timer {multiworld.timer[player]} for player {player}")
if world.options.item_pool.current_key not in difficulties:
raise NotImplementedError(f"Diffulty {world.options.item_pool}")
if world.options.goal not in ('ganon', 'pedestal', 'bosses', 'triforce_hunt', 'local_triforce_hunt',
'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'crystals',
'ganon_pedestal'):
raise NotImplementedError(f"Goal {world.options.goal} for player {player}")
if world.options.mode not in ('open', 'standard', 'inverted'):
raise NotImplementedError(f"Mode {world.options.mode} for player {player}")
if world.options.timer not in (False, 'display', 'timed', 'timed_ohko', 'ohko', 'timed_countdown'):
raise NotImplementedError(f"Timer {world.options.timer} for player {player}")
if multiworld.timer[player] in ['ohko', 'timed_ohko']:
if world.options.timer in ['ohko', 'timed_ohko']:
world.can_take_damage = False
if multiworld.goal[player] in ['pedestal', 'triforce_hunt', 'local_triforce_hunt']:
if world.options.goal in ['pedestal', 'triforce_hunt', 'local_triforce_hunt']:
multiworld.push_item(multiworld.get_location('Ganon', player), item_factory('Nothing', world), False)
else:
multiworld.push_item(multiworld.get_location('Ganon', player), item_factory('Triforce', world), False)
if multiworld.goal[player] in ['triforce_hunt', 'local_triforce_hunt']:
if world.options.goal in ['triforce_hunt', 'local_triforce_hunt']:
region = multiworld.get_region('Light World', player)
loc = ALttPLocation(player, "Murahdahla", parent=region)
@@ -288,7 +288,7 @@ def generate_itempool(world):
for item in precollected_items:
multiworld.push_precollected(item_factory(item, world))
if multiworld.mode[player] == 'standard' and not has_melee_weapon(multiworld.state, player):
if world.options.mode == 'standard' and not has_melee_weapon(multiworld.state, player):
if "Link's Uncle" not in placed_items:
found_sword = False
found_bow = False
@@ -304,10 +304,10 @@ def generate_itempool(world):
elif item in ['Hammer', 'Fire Rod', 'Cane of Somaria', 'Cane of Byrna']:
if item not in possible_weapons:
possible_weapons.append(item)
elif (item == 'Bombs (10)' and (not multiworld.bombless_start[player]) and item not in
elif (item == 'Bombs (10)' and (not world.options.bombless_start) and item not in
possible_weapons):
possible_weapons.append(item)
elif (item in ['Bomb Upgrade (+10)', 'Bomb Upgrade (50)'] and multiworld.bombless_start[player] and item
elif (item in ['Bomb Upgrade (+10)', 'Bomb Upgrade (50)'] and world.options.bombless_start and item
not in possible_weapons):
possible_weapons.append(item)
@@ -315,21 +315,21 @@ def generate_itempool(world):
placed_items["Link's Uncle"] = starting_weapon
pool.remove(starting_weapon)
if (placed_items["Link's Uncle"] in ['Bow', 'Progressive Bow', 'Bombs (10)', 'Bomb Upgrade (+10)',
'Bomb Upgrade (50)', 'Cane of Somaria', 'Cane of Byrna'] and multiworld.enemy_health[player] not in ['default', 'easy']):
if multiworld.bombless_start[player] and "Bomb Upgrade" not in placed_items["Link's Uncle"]:
'Bomb Upgrade (50)', 'Cane of Somaria', 'Cane of Byrna'] and world.options.enemy_health not in ['default', 'easy']):
if world.options.bombless_start and "Bomb Upgrade" not in placed_items["Link's Uncle"]:
if 'Bow' in placed_items["Link's Uncle"]:
multiworld.worlds[player].escape_assist.append('arrows')
world.escape_assist.append('arrows')
elif 'Cane' in placed_items["Link's Uncle"]:
multiworld.worlds[player].escape_assist.append('magic')
world.escape_assist.append('magic')
else:
multiworld.worlds[player].escape_assist.append('bombs')
world.escape_assist.append('bombs')
for (location, item) in placed_items.items():
multiworld.get_location(location, player).place_locked_item(item_factory(item, world))
items = item_factory(pool, world)
# convert one Progressive Bow into Progressive Bow (Alt), in ID only, for ganon silvers hint text
if multiworld.worlds[player].has_progressive_bows:
if world.has_progressive_bows:
for item in items:
if item.code == 0x64: # Progressive Bow
item.code = 0x65 # Progressive Bow (Alt)
@@ -338,21 +338,21 @@ def generate_itempool(world):
if clock_mode:
world.clock_mode = clock_mode
multiworld.worlds[player].treasure_hunt_required = treasure_hunt_required % 999
multiworld.worlds[player].treasure_hunt_total = treasure_hunt_total
world.treasure_hunt_required = treasure_hunt_required % 999
world.treasure_hunt_total = treasure_hunt_total
dungeon_items = [item for item in get_dungeon_item_pool_player(world)
if item.name not in multiworld.worlds[player].dungeon_local_item_names]
if item.name not in world.dungeon_local_item_names]
for key_loc in key_drop_data:
key_data = key_drop_data[key_loc]
drop_item = item_factory(key_data[3], world)
if not multiworld.key_drop_shuffle[player]:
if not world.options.key_drop_shuffle:
if drop_item in dungeon_items:
dungeon_items.remove(drop_item)
else:
dungeon = drop_item.name.split("(")[1].split(")")[0]
if multiworld.mode[player] == 'inverted':
if world.options.mode == 'inverted':
if dungeon == "Agahnims Tower":
dungeon = "Inverted Agahnims Tower"
if dungeon == "Ganons Tower":
@@ -365,7 +365,7 @@ def generate_itempool(world):
loc = multiworld.get_location(key_loc, player)
loc.place_locked_item(drop_item)
loc.address = None
elif "Small" in key_data[3] and multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal:
elif "Small" in key_data[3] and world.options.small_key_shuffle == small_key_shuffle.option_universal:
# key drop shuffle and universal keys are on. Add universal keys in place of key drop keys.
multiworld.itempool.append(item_factory(GetBeemizerItem(multiworld, player, 'Small Key (Universal)'), world))
dungeon_item_replacements = sum(difficulties[world.options.item_pool.current_key].extras, []) * 2
@@ -373,10 +373,10 @@ def generate_itempool(world):
for x in range(len(dungeon_items)-1, -1, -1):
item = dungeon_items[x]
if ((multiworld.small_key_shuffle[player] == small_key_shuffle.option_start_with and item.type == 'SmallKey')
or (multiworld.big_key_shuffle[player] == big_key_shuffle.option_start_with and item.type == 'BigKey')
or (multiworld.compass_shuffle[player] == compass_shuffle.option_start_with and item.type == 'Compass')
or (multiworld.map_shuffle[player] == map_shuffle.option_start_with and item.type == 'Map')):
if ((world.options.small_key_shuffle == small_key_shuffle.option_start_with and item.type == 'SmallKey')
or (world.options.big_key_shuffle == big_key_shuffle.option_start_with and item.type == 'BigKey')
or (world.options.compass_shuffle == compass_shuffle.option_start_with and item.type == 'Compass')
or (world.options.map_shuffle == map_shuffle.option_start_with and item.type == 'Map')):
dungeon_items.pop(x)
multiworld.push_precollected(item)
multiworld.itempool.append(item_factory(dungeon_item_replacements.pop(), world))
@@ -384,7 +384,7 @@ def generate_itempool(world):
set_up_shops(multiworld, player)
if multiworld.retro_bow[player]:
if world.options.retro_bow:
shop_items = 0
shop_locations = [location for shop_locations in (shop.region.locations for shop in multiworld.shops if
shop.type == ShopType.Shop and shop.region.player == player) for location in shop_locations if
@@ -395,12 +395,12 @@ def generate_itempool(world):
else:
shop_items += 1
else:
shop_items = min(multiworld.shop_item_slots[player], 30 if multiworld.include_witch_hut[player] else 27)
shop_items = min(world.options.shop_item_slots, 30 if world.options.include_witch_hut else 27)
if multiworld.shuffle_capacity_upgrades[player]:
if world.options.shuffle_capacity_upgrades:
shop_items += 2
chance_100 = int(multiworld.retro_bow[player]) * 0.25 + int(
multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal) * 0.5
chance_100 = int(world.options.retro_bow) * 0.25 + int(
world.options.small_key_shuffle == small_key_shuffle.option_universal) * 0.5
for _ in range(shop_items):
if multiworld.random.random() < chance_100:
items.append(item_factory(GetBeemizerItem(multiworld, player, "Rupees (100)"), world))
@@ -410,19 +410,19 @@ def generate_itempool(world):
multiworld.random.shuffle(items)
pool_count = len(items)
new_items = ["Triforce Piece" for _ in range(additional_triforce_pieces)]
if multiworld.shuffle_capacity_upgrades[player] or multiworld.bombless_start[player]:
progressive = multiworld.progressive[player]
if world.options.shuffle_capacity_upgrades or world.options.bombless_start:
progressive = world.options.progressive
progressive = multiworld.random.choice([True, False]) if progressive == 'grouped_random' else progressive == 'on'
if multiworld.shuffle_capacity_upgrades[player] == "on_combined":
if world.options.shuffle_capacity_upgrades == "on_combined":
new_items.append("Bomb Upgrade (50)")
elif multiworld.shuffle_capacity_upgrades[player] == "on":
elif world.options.shuffle_capacity_upgrades == "on":
new_items += ["Bomb Upgrade (+5)"] * 6
new_items.append("Bomb Upgrade (+5)" if progressive else "Bomb Upgrade (+10)")
if multiworld.shuffle_capacity_upgrades[player] != "on_combined" and multiworld.bombless_start[player]:
if world.options.shuffle_capacity_upgrades != "on_combined" and world.options.bombless_start:
new_items.append("Bomb Upgrade (+5)" if progressive else "Bomb Upgrade (+10)")
if multiworld.shuffle_capacity_upgrades[player] and not multiworld.retro_bow[player]:
if multiworld.shuffle_capacity_upgrades[player] == "on_combined":
if world.options.shuffle_capacity_upgrades and not world.options.retro_bow:
if world.options.shuffle_capacity_upgrades == "on_combined":
new_items += ["Arrow Upgrade (70)"]
else:
new_items += ["Arrow Upgrade (+5)"] * 6
@@ -481,7 +481,7 @@ def generate_itempool(world):
if len(items) < pool_count:
items += removed_filler[len(items) - pool_count:]
if multiworld.randomize_cost_types[player]:
if world.options.randomize_cost_types:
# Heart and Arrow costs require all Heart Container/Pieces and Arrow Upgrades to be advancement items for logic
for item in items:
if item.name in ("Boss Heart Container", "Sanctuary Heart Container", "Piece of Heart"):
@@ -490,21 +490,25 @@ def generate_itempool(world):
# Otherwise, logic has some branches where having 4 hearts is one possible requirement (of several alternatives)
# rather than making all hearts/heart pieces progression items (which slows down generation considerably)
# We mark one random heart container as an advancement item (or 4 heart pieces in expert mode)
if multiworld.item_pool[player] in ['easy', 'normal', 'hard'] and not (multiworld.custom and multiworld.customitemarray[30] == 0):
next(item for item in items if item.name == 'Boss Heart Container').classification = ItemClassification.progression
elif multiworld.item_pool[player] in ['expert'] and not (multiworld.custom and multiworld.customitemarray[29] < 4):
try:
next(item for item in items if item.name == 'Boss Heart Container').classification \
|= ItemClassification.progression
except StopIteration:
adv_heart_pieces = (item for item in items if item.name == 'Piece of Heart')
for i in range(4):
next(adv_heart_pieces).classification = ItemClassification.progression
try:
next(adv_heart_pieces).classification |= ItemClassification.progression
except StopIteration:
break # logically health tanking is an option, so rules should still resolve to something beatable
world.required_medallions = (multiworld.misery_mire_medallion[player].current_key.title(),
multiworld.turtle_rock_medallion[player].current_key.title())
world.required_medallions = (world.options.misery_mire_medallion.current_key.title(),
world.options.turtle_rock_medallion.current_key.title())
place_bosses(world)
multiworld.itempool += items
if multiworld.retro_caves[player]:
if world.options.retro_caves:
set_up_take_anys(multiworld, world, player) # depends on world.itempool to be set
@@ -527,7 +531,7 @@ take_any_locations.sort()
def set_up_take_anys(multiworld, world, player):
# these are references, do not modify these lists in-place
if multiworld.mode[player] == 'inverted':
if world.options.mode == 'inverted':
take_any_locs = take_any_locations_inverted
else:
take_any_locs = take_any_locations
@@ -578,14 +582,14 @@ def set_up_take_anys(multiworld, world, player):
def get_pool_core(world, player: int):
shuffle = world.entrance_shuffle[player].current_key
difficulty = world.item_pool[player].current_key
timer = world.timer[player].current_key
goal = world.goal[player].current_key
mode = world.mode[player].current_key
swordless = world.swordless[player]
retro_bow = world.retro_bow[player]
logic = world.glitches_required[player]
shuffle = world.worlds[player].options.entrance_shuffle.current_key
difficulty = world.worlds[player].options.item_pool.current_key
timer = world.worlds[player].options.timer.current_key
goal = world.worlds[player].options.goal.current_key
mode = world.worlds[player].options.mode.current_key
swordless = world.worlds[player].options.swordless
retro_bow = world.worlds[player].options.retro_bow
logic = world.worlds[player].options.glitches_required
pool = []
placed_items = {}
@@ -602,11 +606,11 @@ def get_pool_core(world, player: int):
placed_items[loc] = item
# provide boots to major glitch dependent seeds
if logic.current_key in {'overworld_glitches', 'hybrid_major_glitches', 'no_logic'} and world.glitch_boots[player]:
if logic.current_key in {'overworld_glitches', 'hybrid_major_glitches', 'no_logic'} and world.worlds[player].options.glitch_boots:
precollected_items.append('Pegasus Boots')
pool.remove('Pegasus Boots')
pool.append('Rupees (20)')
want_progressives = world.progressive[player].want_progressives
want_progressives = world.worlds[player].options.progressive.want_progressives
if want_progressives(world.random):
pool.extend(diff.progressiveglove)
@@ -680,22 +684,22 @@ def get_pool_core(world, player: int):
additional_pieces_to_place = 0
if 'triforce_hunt' in goal:
if world.triforce_pieces_mode[player].value == TriforcePiecesMode.option_extra:
treasure_hunt_total = (world.triforce_pieces_required[player].value
+ world.triforce_pieces_extra[player].value)
elif world.triforce_pieces_mode[player].value == TriforcePiecesMode.option_percentage:
percentage = float(world.triforce_pieces_percentage[player].value) / 100
treasure_hunt_total = int(round(world.triforce_pieces_required[player].value * percentage, 0))
if world.worlds[player].options.triforce_pieces_mode.value == TriforcePiecesMode.option_extra:
treasure_hunt_total = (world.worlds[player].options.triforce_pieces_required.value
+ world.worlds[player].options.triforce_pieces_extra.value)
elif world.worlds[player].options.triforce_pieces_mode.value == TriforcePiecesMode.option_percentage:
percentage = float(world.worlds[player].options.triforce_pieces_percentage.value) / 100
treasure_hunt_total = int(round(world.worlds[player].options.triforce_pieces_required.value * percentage, 0))
else: # available
treasure_hunt_total = world.triforce_pieces_available[player].value
treasure_hunt_total = world.worlds[player].options.triforce_pieces_available.value
triforce_pieces = min(90, max(treasure_hunt_total, world.triforce_pieces_required[player].value))
triforce_pieces = min(90, max(treasure_hunt_total, world.worlds[player].options.triforce_pieces_required.value))
pieces_in_core = min(extraitems, triforce_pieces)
additional_pieces_to_place = triforce_pieces - pieces_in_core
pool.extend(["Triforce Piece"] * pieces_in_core)
extraitems -= pieces_in_core
treasure_hunt_required = world.triforce_pieces_required[player].value
treasure_hunt_required = world.worlds[player].options.triforce_pieces_required.value
for extra in diff.extras:
if extraitems >= len(extra):
@@ -707,17 +711,24 @@ def get_pool_core(world, player: int):
else:
break
if goal == 'pedestal':
place_item('Master Sword Pedestal', 'Triforce')
pool.remove("Rupees (20)")
if retro_bow:
replace = {'Single Arrow', 'Arrows (10)', 'Arrow Upgrade (+5)', 'Arrow Upgrade (+10)', 'Arrow Upgrade (70)'}
pool = ['Rupees (5)' if item in replace else item for item in pool]
if world.small_key_shuffle[player] == small_key_shuffle.option_universal:
if goal == 'pedestal':
place_item('Master Sword Pedestal', 'Triforce')
for rupee_name in ("Rupees (5)", "Rupees (20)", "Rupees (50)", "Rupees (100)", "Rupees (300)"):
try:
pool.remove(rupee_name)
except ValueError:
pass
else:
break
if world.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal:
pool.extend(diff.universal_keys)
if mode == 'standard':
if world.key_drop_shuffle[player]:
if world.worlds[player].options.key_drop_shuffle:
key_locations = ['Secret Passage', 'Hyrule Castle - Map Guard Key Drop']
key_location = world.random.choice(key_locations)
key_locations.remove(key_location)
@@ -741,11 +752,11 @@ def get_pool_core(world, player: int):
def make_custom_item_pool(world, player):
shuffle = world.entrance_shuffle[player]
difficulty = world.item_pool[player]
timer = world.timer[player]
goal = world.goal[player]
mode = world.mode[player]
shuffle = world.worlds[player].options.entrance_shuffle
difficulty = world.worlds[player].options.item_pool
timer = world.worlds[player].options.timer
goal = world.worlds[player].options.goal
mode = world.worlds[player].options.mode
customitemarray = world.customitemarray
pool = []
@@ -845,10 +856,10 @@ def make_custom_item_pool(world, player):
thisbottle = world.random.choice(diff.bottles)
pool.append(thisbottle)
if "triforce" in world.goal[player]:
pool.extend(["Triforce Piece"] * world.triforce_pieces_available[player])
itemtotal += world.triforce_pieces_available[player]
treasure_hunt_required = world.triforce_pieces_required[player]
if "triforce" in world.worlds[player].options.goal:
pool.extend(["Triforce Piece"] * world.worlds[player].options.triforce_pieces_available)
itemtotal += world.worlds[player].options.triforce_pieces_available
treasure_hunt_required = world.worlds[player].options.triforce_pieces_required
if timer in ['display', 'timed', 'timed_countdown']:
clock_mode = 'countdown' if timer == 'timed_countdown' else 'stopwatch'
@@ -862,7 +873,7 @@ def make_custom_item_pool(world, player):
itemtotal = itemtotal + 1
if mode == 'standard':
if world.small_key_shuffle[player] == small_key_shuffle.option_universal:
if world.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal:
key_location = world.random.choice(
['Secret Passage', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest',
'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross'])
@@ -885,9 +896,9 @@ def make_custom_item_pool(world, player):
pool.extend(['Magic Mirror'] * customitemarray[22])
pool.extend(['Moon Pearl'] * customitemarray[28])
if world.small_key_shuffle[player] == small_key_shuffle.option_universal:
if world.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal:
itemtotal = itemtotal - 28 # Corrects for small keys not being in item pool in universal Mode
if world.key_drop_shuffle[player]:
if world.worlds[player].options.key_drop_shuffle:
itemtotal = itemtotal - (len(key_drop_data) - 1)
if itemtotal < total_items_to_place:
pool.extend(['Nothing'] * (total_items_to_place - itemtotal))

View File

@@ -11,11 +11,11 @@ def GetBeemizerItem(world, player: int, item):
return item
# first roll - replaceable item should be replaced, within beemizer_total_chance
if not world.beemizer_total_chance[player] or world.random.random() > (world.beemizer_total_chance[player] / 100):
if not world.worlds[player].options.beemizer_total_chance or world.random.random() > (world.worlds[player].options.beemizer_total_chance / 100):
return item
# second roll - bee replacement should be trap, within beemizer_trap_chance
if not world.beemizer_trap_chance[player] or world.random.random() > (world.beemizer_trap_chance[player] / 100):
if not world.worlds[player].options.beemizer_trap_chance or world.random.random() > (world.worlds[player].options.beemizer_trap_chance / 100):
return "Bee" if isinstance(item, str) else world.create_item("Bee", player)
else:
return "Bee Trap" if isinstance(item, str) else world.create_item("Bee Trap", player)

View File

@@ -156,10 +156,10 @@ class OpenPyramid(Choice):
def to_bool(self, world: MultiWorld, player: int) -> bool:
if self.value == self.option_goal:
return world.goal[player].current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'}
return world.worlds[player].options.goal.current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'}
elif self.value == self.option_auto:
return world.goal[player].current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'} \
and (world.entrance_shuffle[player].current_key in {'vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed'} or not
return world.worlds[player].options.goal.current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'} \
and (world.worlds[player].options.entrance_shuffle.current_key in {'vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed'} or not
world.shuffle_ganon)
elif self.value == self.option_open:
return True

View File

@@ -2,8 +2,6 @@
Helper functions to deliver entrance/exit/region sets to OWG rules.
"""
from BaseClasses import Entrance
from .StateHelpers import can_lift_heavy_rocks, can_boots_clip_lw, can_boots_clip_dw, can_get_glitched_speed_dw
@@ -222,14 +220,14 @@ def get_invalid_bunny_revival_dungeons():
def overworld_glitch_connections(world, player):
# Boots-accessible locations.
create_owg_connections(player, world, get_boots_clip_exits_lw(world.mode[player] == 'inverted'))
create_owg_connections(player, world, get_boots_clip_exits_dw(world.mode[player] == 'inverted', player))
create_owg_connections(player, world, get_boots_clip_exits_lw(world.worlds[player].options.mode == 'inverted'))
create_owg_connections(player, world, get_boots_clip_exits_dw(world.worlds[player].options.mode == 'inverted', player))
# Glitched speed drops.
create_owg_connections(player, world, get_glitched_speed_drops_dw(world.mode[player] == 'inverted'))
create_owg_connections(player, world, get_glitched_speed_drops_dw(world.worlds[player].options.mode == 'inverted'))
# Mirror clip spots.
if world.mode[player] != 'inverted':
if world.worlds[player].options.mode != 'inverted':
create_owg_connections(player, world, get_mirror_clip_spots_dw())
create_owg_connections(player, world, get_mirror_offset_spots_dw())
else:
@@ -239,24 +237,24 @@ def overworld_glitch_connections(world, player):
def overworld_glitches_rules(world, player):
# Boots-accessible locations.
set_owg_connection_rules(player, world, get_boots_clip_exits_lw(world.mode[player] == 'inverted'), lambda state: can_boots_clip_lw(state, player))
set_owg_connection_rules(player, world, get_boots_clip_exits_dw(world.mode[player] == 'inverted', player), lambda state: can_boots_clip_dw(state, player))
set_owg_connection_rules(player, world, get_boots_clip_exits_lw(world.worlds[player].options.mode == 'inverted'), lambda state: can_boots_clip_lw(state, player))
set_owg_connection_rules(player, world, get_boots_clip_exits_dw(world.worlds[player].options.mode == 'inverted', player), lambda state: can_boots_clip_dw(state, player))
# Glitched speed drops.
set_owg_connection_rules(player, world, get_glitched_speed_drops_dw(world.mode[player] == 'inverted'), lambda state: can_get_glitched_speed_dw(state, player))
set_owg_connection_rules(player, world, get_glitched_speed_drops_dw(world.worlds[player].options.mode == 'inverted'), lambda state: can_get_glitched_speed_dw(state, player))
# Dark Death Mountain Ledge Clip Spot also accessible with mirror.
if world.mode[player] != 'inverted':
if world.worlds[player].options.mode != 'inverted':
add_alternate_rule(world.get_entrance('Dark Death Mountain Ledge Clip Spot', player), lambda state: state.has('Magic Mirror', player))
# Mirror clip spots.
if world.mode[player] != 'inverted':
if world.worlds[player].options.mode != 'inverted':
set_owg_connection_rules(player, world, get_mirror_clip_spots_dw(), lambda state: state.has('Magic Mirror', player))
set_owg_connection_rules(player, world, get_mirror_offset_spots_dw(), lambda state: state.has('Magic Mirror', player) and can_boots_clip_lw(state, player))
else:
set_owg_connection_rules(player, world, get_mirror_offset_spots_lw(player), lambda state: state.has('Magic Mirror', player) and can_boots_clip_dw(state, player))
# Regions that require the boots and some other stuff.
if world.mode[player] != 'inverted':
if world.worlds[player].options.mode != 'inverted':
world.get_entrance('Turtle Rock Teleporter', player).access_rule = lambda state: (can_boots_clip_lw(state, player) or can_lift_heavy_rocks(state, player)) and state.has('Hammer', player)
add_alternate_rule(world.get_entrance('Waterfall of Wishing', player), lambda state: state.has('Moon Pearl', player) or state.has('Pegasus Boots', player))
else:
@@ -279,18 +277,14 @@ def create_no_logic_connections(player, world, connections):
for entrance, parent_region, target_region, *rule_override in connections:
parent = world.get_region(parent_region, player)
target = world.get_region(target_region, player)
connection = Entrance(player, entrance, parent)
parent.exits.append(connection)
connection.connect(target)
parent.connect(target, entrance)
def create_owg_connections(player, world, connections):
for entrance, parent_region, target_region, *rule_override in connections:
parent = world.get_region(parent_region, player)
target = world.get_region(target_region, player)
connection = Entrance(player, entrance, parent)
parent.exits.append(connection)
connection.connect(target)
parent.connect(target, entrance)
def set_owg_connection_rules(player, world, connections, default_rule):

View File

@@ -1,11 +1,11 @@
import collections
import typing
from BaseClasses import Entrance, MultiWorld
from .SubClasses import LTTPRegion, LTTPRegionType
from BaseClasses import MultiWorld
from .SubClasses import LTTPEntrance, LTTPRegion, LTTPRegionType
def is_main_entrance(entrance: Entrance) -> bool:
def is_main_entrance(entrance: LTTPEntrance) -> bool:
return entrance.parent_region.type in {LTTPRegionType.DarkWorld, LTTPRegionType.LightWorld} if entrance.parent_region.type else True
@@ -410,7 +410,7 @@ def _create_region(world: MultiWorld, player: int, name: str, type: LTTPRegionTy
ret = LTTPRegion(name, type, hint, player, world)
if exits:
for exit in exits:
ret.exits.append(Entrance(player, exit, ret))
ret.create_exit(exit)
if locations:
for location in locations:
if location in key_drop_data:

View File

@@ -92,7 +92,7 @@ class LocalRom:
# cause crash to provide traceback
import xxtea
local_random = world.per_slot_randoms[player]
local_random = world.worlds[player].random
key = bytes(local_random.getrandbits(8 * 16).to_bytes(16, 'big'))
self.write_bytes(0x1800B0, bytearray(key))
self.write_int16(0x180087, 1)
@@ -281,7 +281,6 @@ def apply_random_sprite_on_event(rom: LocalRom, sprite, local_random, allow_rand
def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
player = world.player
multiworld = world.multiworld
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'))
@@ -289,18 +288,18 @@ def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
# write options file for enemizer
options = {
'RandomizeEnemies': multiworld.enemy_shuffle[player].value,
'RandomizeEnemies': world.options.enemy_shuffle.value,
'RandomizeEnemiesType': 3,
'RandomizeBushEnemyChance': multiworld.bush_shuffle[player].value,
'RandomizeEnemyHealthRange': multiworld.enemy_health[player] != 'default',
'RandomizeBushEnemyChance': world.options.bush_shuffle.value,
'RandomizeEnemyHealthRange': world.options.enemy_health != 'default',
'RandomizeEnemyHealthType': {'default': 0, 'easy': 0, 'normal': 1, 'hard': 2, 'expert': 3}[
multiworld.enemy_health[player].current_key],
world.options.enemy_health.current_key],
'OHKO': False,
'RandomizeEnemyDamage': multiworld.enemy_damage[player] != 'default',
'RandomizeEnemyDamage': world.options.enemy_damage != 'default',
'AllowEnemyZeroDamage': True,
'ShuffleEnemyDamageGroups': multiworld.enemy_damage[player] != 'default',
'EnemyDamageChaosMode': multiworld.enemy_damage[player] == 'chaos',
'EasyModeEscape': multiworld.mode[player] == "standard",
'ShuffleEnemyDamageGroups': world.options.enemy_damage != 'default',
'EnemyDamageChaosMode': world.options.enemy_damage == 'chaos',
'EasyModeEscape': world.options.mode == "standard",
'EnemiesAbsorbable': False,
'AbsorbableSpawnRate': 10,
'AbsorbableTypes': {
@@ -329,7 +328,7 @@ def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
'GrayscaleMode': False,
'GenerateSpoilers': False,
'RandomizeLinkSpritePalette': False,
'RandomizePots': multiworld.pot_shuffle[player].value,
'RandomizePots': world.options.pot_shuffle.value,
'ShuffleMusic': False,
'BootlegMagic': True,
'CustomBosses': False,
@@ -342,7 +341,7 @@ def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
'BeesLevel': 0,
'RandomizeTileTrapPattern': False,
'RandomizeTileTrapFloorTile': False,
'AllowKillableThief': multiworld.killable_thieves[player].value,
'AllowKillableThief': world.options.killable_thieves.value,
'RandomizeSpriteOnHit': False,
'DebugMode': False,
'DebugForceEnemy': False,
@@ -366,13 +365,13 @@ def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
'MiseryMire': world.dungeons["Misery Mire"].boss.enemizer_name,
'TurtleRock': world.dungeons["Turtle Rock"].boss.enemizer_name,
'GanonsTower1':
world.dungeons["Ganons Tower" if multiworld.mode[player] != 'inverted' else
world.dungeons["Ganons Tower" if world.options.mode != 'inverted' else
"Inverted Ganons Tower"].bosses['bottom'].enemizer_name,
'GanonsTower2':
world.dungeons["Ganons Tower" if multiworld.mode[player] != 'inverted' else
world.dungeons["Ganons Tower" if world.options.mode != 'inverted' else
"Inverted Ganons Tower"].bosses['middle'].enemizer_name,
'GanonsTower3':
world.dungeons["Ganons Tower" if multiworld.mode[player] != 'inverted' else
world.dungeons["Ganons Tower" if world.options.mode != 'inverted' else
"Inverted Ganons Tower"].bosses['top'].enemizer_name,
'GanonsTower4': 'Agahnim2',
'Ganon': 'Ganon',
@@ -386,7 +385,7 @@ def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
max_enemizer_tries = 5
for i in range(max_enemizer_tries):
enemizer_seed = str(multiworld.per_slot_randoms[player].randint(0, 999999999))
enemizer_seed = str(world.random.randint(0, 999999999))
enemizer_command = [os.path.abspath(enemizercli),
'--rom', randopatch_path,
'--seed', enemizer_seed,
@@ -416,7 +415,7 @@ def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
continue
for j in range(i + 1, max_enemizer_tries):
multiworld.per_slot_randoms[player].randint(0, 999999999)
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
@@ -430,7 +429,7 @@ def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
# 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 multiworld.key_drop_shuffle[player]:
if world.options.key_drop_shuffle:
key_drop_enemies = {
0x4DA20, 0x4DA5C, 0x4DB7F, 0x4DD73, 0x4DDC3, 0x4DE07, 0x4E201,
0x4E20A, 0x4E326, 0x4E4F7, 0x4E687, 0x4E70C, 0x4E7C8, 0x4E7FA
@@ -792,8 +791,8 @@ def get_nonnative_item_sprite(code: int) -> int:
def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
local_random = world.worlds[player].random
local_world = world.worlds[player]
local_random = local_world.random
# patch items
@@ -840,20 +839,20 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
# patch music
music_addresses = dungeon_music_addresses[location.name]
if world.map_shuffle[player]:
if local_world.options.map_shuffle:
music = local_random.choice([0x11, 0x16])
else:
music = 0x11 if 'Pendant' in location.item.name else 0x16
for music_address in music_addresses:
rom.write_byte(music_address, music)
if world.map_shuffle[player]:
if local_world.options.map_shuffle:
rom.write_byte(0x155C9, local_random.choice([0x11, 0x16])) # Randomize GT music too with map shuffle
# patch entrance/exits/holes
for region in world.regions:
for region in world.get_regions(player):
for exit in region.exits:
if exit.target is not None and exit.player == player:
if exit.target is not None:
if isinstance(exit.addresses, tuple):
offset = exit.target
room_id, ow_area, vram_loc, scroll_y, scroll_x, link_y, link_x, camera_y, camera_x, unknown_1, unknown_2, door_1, door_2 = exit.addresses
@@ -868,15 +867,15 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
# Thanks to Zarby89 for originally finding these values
# todo fix screen scrolling
if world.entrance_shuffle[player] != 'insanity' and \
if local_world.options.entrance_shuffle != 'insanity' and \
exit.name in {'Eastern Palace Exit', 'Tower of Hera Exit', 'Thieves Town Exit',
'Skull Woods Final Section Exit', 'Ice Palace Exit', 'Misery Mire Exit',
'Palace of Darkness Exit', 'Swamp Palace Exit', 'Ganons Tower Exit',
'Desert Palace Exit (North)', 'Agahnims Tower Exit', 'Spiral Cave Exit (Top)',
'Superbunny Cave Exit (Bottom)', 'Turtle Rock Ledge Exit (East)'} and \
(world.glitches_required[player] not in ['hybrid_major_glitches', 'no_logic'] or
exit.name not in {'Palace of Darkness Exit', 'Tower of Hera Exit', 'Swamp Palace Exit'}):
# For exits that connot be reached from another, no need to apply offset fixes.
(local_world.options.glitches_required not in ['hybrid_major_glitches', 'no_logic'] or
exit.name not in {'Palace of Darkness Exit', 'Tower of Hera Exit', 'Swamp Palace Exit'}):
# For exits that cannot be reached from another, no need to apply offset fixes.
rom.write_int16(0x15DB5 + 2 * offset, link_y) # same as final else
elif room_id == 0x0059 and local_world.fix_skullwoods_exit:
rom.write_int16(0x15DB5 + 2 * offset, 0x00F8)
@@ -903,7 +902,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
else:
# patch door table
rom.write_byte(0xDBB73 + exit.addresses, exit.target)
if world.mode[player] == 'inverted':
if local_world.options.mode == 'inverted':
patch_shuffled_dark_sanc(world, rom, player)
write_custom_shops(rom, world, player)
@@ -914,16 +913,16 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
return 0x53 + int(num), 0x79 + int(num)
credits_total = 216
if world.retro_caves[player]: # Old man cave and Take any caves will count towards collection rate.
if local_world.options.retro_caves: # Old man cave and Take any caves will count towards collection rate.
credits_total += 5
if world.shop_item_slots[player]: # Potion shop only counts towards collection rate if included in the shuffle.
credits_total += 30 if world.include_witch_hut[player] else 27
if world.shuffle_capacity_upgrades[player]:
if local_world.options.shop_item_slots: # Potion shop only counts towards collection rate if included in the shuffle.
credits_total += 30 if local_world.options.include_witch_hut else 27
if local_world.options.shuffle_capacity_upgrades:
credits_total += 2
rom.write_byte(0x187010, credits_total) # dynamic credits
if world.key_drop_shuffle[player]:
if local_world.options.key_drop_shuffle:
rom.write_byte(0x140000, 1) # enable key drop shuffle
credits_total += len(key_drop_data)
# update dungeon counters
@@ -977,11 +976,11 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
rom.write_byte(0x51DE, 0x00)
# set open mode:
if world.mode[player] in ['open', 'inverted']:
if local_world.options.mode in ['open', 'inverted']:
rom.write_byte(0x180032, 0x01) # open mode
if world.mode[player] == 'inverted':
if local_world.options.mode == 'inverted':
set_inverted_mode(world, player, rom)
elif world.mode[player] == 'standard':
elif local_world.options.mode == 'standard':
rom.write_byte(0x180032, 0x00) # standard mode
uncle_location = world.get_location('Link\'s Uncle', player)
@@ -1001,7 +1000,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
rom.write_bytes(0x6D323, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E])
# set light cones
rom.write_byte(0x180038, 0x01 if world.mode[player] == "standard" else 0x00)
rom.write_byte(0x180038, 0x01 if local_world.options.mode == "standard" else 0x00)
rom.write_byte(0x180039, 0x01 if world.light_world_light_cone else 0x00)
rom.write_byte(0x18003A, 0x01 if world.dark_world_light_cone else 0x00)
@@ -1011,7 +1010,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
rom.write_byte(0x18004F, 0x01) # Byrna Invulnerability: on
# handle item_functionality
if world.item_functionality[player] == 'hard':
if local_world.options.item_functionality == 'hard':
rom.write_byte(0x180181, 0x01) # Make silver arrows work only on ganon
rom.write_byte(0x180182, 0x00) # Don't auto equip silvers on pickup
# Powdered Fairies Prize
@@ -1031,7 +1030,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
rom.write_int16(0x180036, world.rupoor_cost)
# Set stun items
rom.write_byte(0x180180, 0x02) # Hookshot only
elif world.item_functionality[player] == 'expert':
elif local_world.options.item_functionality == 'expert':
rom.write_byte(0x180181, 0x01) # Make silver arrows work only on ganon
rom.write_byte(0x180182, 0x00) # Don't auto equip silvers on pickup
# Powdered Fairies Prize
@@ -1071,7 +1070,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
# Set stun items
rom.write_byte(0x180180, 0x03) # All standard items
# Set overflow items for progressive equipment
if world.timer[player] in ['timed', 'timed_countdown', 'timed_ohko']:
if local_world.options.timer in ['timed', 'timed_countdown', 'timed_ohko']:
overflow_replacement = GREEN_CLOCK
else:
overflow_replacement = GREEN_TWENTY_RUPEES
@@ -1083,7 +1082,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
# Set overflow items for progressive equipment
rom.write_bytes(0x180090,
[difficulty.progressive_sword_limit if not world.swordless[player] else 0,
[difficulty.progressive_sword_limit if not local_world.options.swordless else 0,
item_table[difficulty.basicsword[-1]].item_code,
difficulty.progressive_shield_limit, item_table[difficulty.basicshield[-1]].item_code,
difficulty.progressive_armor_limit, item_table[difficulty.basicarmor[-1]].item_code,
@@ -1091,7 +1090,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
difficulty.progressive_bow_limit, item_table[difficulty.basicbow[-1]].item_code])
if difficulty.progressive_bow_limit < 2 and (
world.swordless[player] or world.glitches_required[player] == 'no_glitches'):
local_world.options.swordless or local_world.options.glitches_required == 'no_glitches'):
rom.write_bytes(0x180098, [2, item_table["Silver Bow"].item_code])
rom.write_byte(0x180181, 0x01) # Make silver arrows work only on ganon
rom.write_byte(0x180182, 0x00) # Don't auto equip silvers on pickup
@@ -1099,15 +1098,15 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
# set up game internal RNG seed
rom.write_bytes(0x178000, local_random.getrandbits(8 * 1024).to_bytes(1024, 'big'))
prize_replacements = {}
if world.item_functionality[player] in ['hard', 'expert']:
if local_world.options.item_functionality in ['hard', 'expert']:
prize_replacements[0xE0] = 0xDF # Fairy -> heart
prize_replacements[0xE3] = 0xD8 # Big magic -> small magic
if world.retro_bow[player]:
if local_world.options.retro_bow:
prize_replacements[0xE1] = 0xDA # 5 Arrows -> Blue Rupee
prize_replacements[0xE2] = 0xDB # 10 Arrows -> Red Rupee
if world.shuffle_prizes[player] in ("general", "both"):
if local_world.options.shuffle_prizes in ("general", "both"):
# shuffle prize packs
prizes = [0xD8, 0xD8, 0xD8, 0xD8, 0xD9, 0xD8, 0xD8, 0xD9, 0xDA, 0xD9, 0xDA, 0xDB, 0xDA, 0xD9, 0xDA, 0xDA, 0xE0,
0xDF, 0xDF, 0xDA, 0xE0, 0xDF, 0xD8, 0xDF,
@@ -1169,7 +1168,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
byte = int(rom.read_byte(address))
rom.write_byte(address, prize_replacements.get(byte, byte))
if world.shuffle_prizes[player] in ("bonk", "both"):
if local_world.options.shuffle_prizes in ("bonk", "both"):
# set bonk prizes
bonk_prizes = [0x79, 0xE3, 0x79, 0xAC, 0xAC, 0xE0, 0xDC, 0xAC, 0xE3, 0xE3, 0xDA, 0xE3, 0xDA, 0xD8, 0xAC,
0xAC, 0xE3, 0xD8, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xDC, 0xDB, 0xE3, 0xDA, 0x79, 0x79,
@@ -1196,7 +1195,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
0x12, 0x01, 0x35, 0xFF, # lamp -> 5 rupees
0x51, 0x06, 0x52, 0xFF, # 6 +5 bomb upgrades -> +10 bomb upgrade
0x53, 0x06, 0x54, 0xFF, # 6 +5 arrow upgrades -> +10 arrow upgrade
0x58, 0x01, 0x36 if world.retro_bow[player] else 0x43, 0xFF, # silver arrows -> single arrow (red 20 in retro mode)
0x58, 0x01, 0x36 if local_world.options.retro_bow else 0x43, 0xFF, # silver arrows -> single arrow (red 20 in retro mode)
0x3E, difficulty.boss_heart_container_limit, 0x47, 0xff, # boss heart -> green 20
0x17, difficulty.heart_piece_limit, 0x47, 0xff, # piece of heart -> green 20
0xFF, 0xFF, 0xFF, 0xFF, # end of table sentinel
@@ -1238,13 +1237,13 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
rom.write_byte(0x180029, 0x01) # Smithy quick item give
# set swordless mode settings
rom.write_byte(0x18003F, 0x01 if world.swordless[player] else 0x00) # hammer can harm ganon
rom.write_byte(0x180040, 0x01 if world.swordless[player] else 0x00) # open curtains
rom.write_byte(0x180041, 0x01 if world.swordless[player] else 0x00) # swordless medallions
rom.write_byte(0x180043, 0xFF if world.swordless[player] else 0x00) # starting sword for link
rom.write_byte(0x180044, 0x01 if world.swordless[player] else 0x00) # hammer activates tablets
rom.write_byte(0x18003F, 0x01 if local_world.options.swordless else 0x00) # hammer can harm ganon
rom.write_byte(0x180040, 0x01 if local_world.options.swordless else 0x00) # open curtains
rom.write_byte(0x180041, 0x01 if local_world.options.swordless else 0x00) # swordless medallions
rom.write_byte(0x180043, 0xFF if local_world.options.swordless else 0x00) # starting sword for link
rom.write_byte(0x180044, 0x01 if local_world.options.swordless else 0x00) # hammer activates tablets
if world.item_functionality[player] == 'easy':
if local_world.options.item_functionality == 'easy':
rom.write_byte(0x18003F, 0x01) # hammer can harm ganon
rom.write_byte(0x180041, 0x02) # Allow swordless medallion use EVERYWHERE.
rom.write_byte(0x180044, 0x01) # hammer activates tablets
@@ -1262,11 +1261,11 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
# Set up requested clock settings
if local_world.clock_mode in ['countdown-ohko', 'stopwatch', 'countdown']:
rom.write_int32(0x180200,
world.red_clock_time[player] * 60 * 60) # red clock adjustment time (in frames, sint32)
local_world.options.red_clock_time * 60 * 60) # red clock adjustment time (in frames, sint32)
rom.write_int32(0x180204,
world.blue_clock_time[player] * 60 * 60) # blue clock adjustment time (in frames, sint32)
local_world.options.blue_clock_time * 60 * 60) # blue clock adjustment time (in frames, sint32)
rom.write_int32(0x180208,
world.green_clock_time[player] * 60 * 60) # green clock adjustment time (in frames, sint32)
local_world.options.green_clock_time * 60 * 60) # green clock adjustment time (in frames, sint32)
else:
rom.write_int32(0x180200, 0) # red clock adjustment time (in frames, sint32)
rom.write_int32(0x180204, 0) # blue clock adjustment time (in frames, sint32)
@@ -1274,20 +1273,20 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
# Set up requested start time for countdown modes
if local_world.clock_mode in ['countdown-ohko', 'countdown']:
rom.write_int32(0x18020C, world.countdown_start_time[player] * 60 * 60) # starting time (in frames, sint32)
rom.write_int32(0x18020C, local_world.options.countdown_start_time * 60 * 60) # starting time (in frames, sint32)
else:
rom.write_int32(0x18020C, 0) # starting time (in frames, sint32)
# set up goals for treasure hunt
rom.write_int16(0x180163, max(0, local_world.treasure_hunt_required -
sum(1 for item in world.precollected_items[player] if item.name == "Triforce Piece")))
sum(1 for item in world.precollected_items[player] if item.name == "Triforce Piece")))
rom.write_bytes(0x180165, [0x0E, 0x28]) # Triforce Piece Sprite
rom.write_byte(0x180194, 1) # Must turn in triforced pieces (instant win not enabled)
rom.write_bytes(0x180213, [0x00, 0x01]) # Not a Tournament Seed
gametype = 0x04 # item
if world.entrance_shuffle[player] != 'vanilla':
if local_world.options.entrance_shuffle != 'vanilla':
gametype |= 0x02 # entrance
if enemized:
gametype |= 0x01 # enemizer
@@ -1298,7 +1297,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
rom.write_byte(0x1800A2, 0x01 if local_world.fix_fake_world else 0x00)
# Lock or unlock aga tower door during escape sequence.
rom.write_byte(0x180169, 0x00)
if world.mode[player] == 'inverted':
if local_world.options.mode == 'inverted':
rom.write_byte(0x180169, 0x02) # lock aga/ganon tower door with crystals in inverted
rom.write_byte(0x180171,
0x01 if local_world.ganon_at_pyramid else 0x00) # Enable respawning on pyramid after ganon death
@@ -1309,9 +1308,8 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
rom.write_bytes(0x50563, [0x3F, 0x14]) # disable below ganon chest
rom.write_byte(0x50599, 0x00) # disable below ganon chest
rom.write_bytes(0xE9A5, [0x7E, 0x00, 0x24]) # disable below ganon chest
rom.write_byte(0x18008B, 0x01 if world.open_pyramid[player].to_bool(world, player) else 0x00) # pre-open Pyramid Hole
rom.write_byte(0x18008C, 0x01 if world.crystals_needed_for_gt[
player] == 0 else 0x00) # GT pre-opened if crystal requirement is 0
rom.write_byte(0x18008B, 0x01 if local_world.options.open_pyramid.to_bool(world, player) else 0x00) # pre-open Pyramid Hole
rom.write_byte(0x18008C, 0x01 if local_world.options.crystals_needed_for_gt == 0 else 0x00) # GT pre-opened if crystal requirement is 0
rom.write_byte(0xF5D73, 0xF0) # bees are catchable
rom.write_byte(0xF5F10, 0xF0) # bees are catchable
rom.write_byte(0x180086, 0x00 if world.aga_randomness else 0x01) # set blue ball and ganon warp randomness
@@ -1325,7 +1323,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
equip[0x36C] = 0x18
equip[0x36D] = 0x18
equip[0x379] = 0x68
starting_max_bombs = 0 if world.bombless_start[player] else 10
starting_max_bombs = 0 if local_world.options.bombless_start else 10
starting_max_arrows = 30
startingstate = CollectionState(world)
@@ -1333,12 +1331,12 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
if startingstate.has('Silver Bow', player):
equip[0x340] = 1
equip[0x38E] |= 0x60
if not world.retro_bow[player]:
if not local_world.options.retro_bow:
equip[0x38E] |= 0x80
elif startingstate.has('Bow', player):
equip[0x340] = 1
equip[0x38E] |= 0x20 # progressive flag to get the correct hint in all cases
if not world.retro_bow[player]:
if not local_world.options.retro_bow:
equip[0x38E] |= 0x80
if startingstate.has('Silver Arrows', player):
equip[0x38E] |= 0x40
@@ -1476,7 +1474,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
elif item.name in bombs:
equip[0x343] += bombs[item.name]
elif item.name in arrows:
if world.retro_bow[player]:
if local_world.options.retro_bow:
equip[0x38E] |= 0x80
equip[0x377] = 1
else:
@@ -1502,16 +1500,13 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
rom.write_bytes(0x183000, equip[0x340:])
rom.write_bytes(0x271A6, equip[0x340:0x340 + 60])
rom.write_byte(0x18004A, 0x00 if world.mode[player] != 'inverted' else 0x01) # Inverted mode
rom.write_byte(0x18004A, 0x00 if local_world.options.mode != 'inverted' else 0x01) # Inverted mode
rom.write_byte(0x18005D, 0x00) # Hammer always breaks barrier
rom.write_byte(0x2AF79, 0xD0 if world.mode[
player] != 'inverted' else 0xF0) # vortexes: Normal (D0=light to dark, F0=dark to light, 42 = both)
rom.write_byte(0x3A943, 0xD0 if world.mode[
player] != 'inverted' else 0xF0) # Mirror: Normal (D0=Dark to Light, F0=light to dark, 42 = both)
rom.write_byte(0x3A96D, 0xF0 if world.mode[
player] != 'inverted' else 0xD0) # Residual Portal: Normal (F0= Light Side, D0=Dark Side, 42 = both (Darth Vader))
rom.write_byte(0x2AF79, 0xD0 if local_world.options.mode != 'inverted' else 0xF0) # vortexes: Normal (D0=light to dark, F0=dark to light, 42 = both)
rom.write_byte(0x3A943, 0xD0 if local_world.options.mode != 'inverted' else 0xF0) # Mirror: Normal (D0=Dark to Light, F0=light to dark, 42 = both)
rom.write_byte(0x3A96D, 0xF0 if local_world.options.mode != 'inverted' else 0xD0) # Residual Portal: Normal (F0= Light Side, D0=Dark Side, 42 = both (Darth Vader))
rom.write_byte(0x3A9A7, 0xD0) # Residual Portal: Normal (D0= Light Side, F0=Dark Side, 42 = both (Darth Vader))
if world.shuffle_capacity_upgrades[player]:
if local_world.options.shuffle_capacity_upgrades:
rom.write_bytes(0x180080,
[5, 10, 5, 10]) # values to fill for Capacity Upgrades (Bomb5, Bomb10, Arrow5, Arrow10)
else:
@@ -1522,21 +1517,21 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
(0x02 if 'bombs' in local_world.escape_assist else 0x00) |
(0x04 if 'magic' in local_world.escape_assist else 0x00))) # Escape assist
if world.goal[player] in ['pedestal', 'triforce_hunt', 'local_triforce_hunt']:
if local_world.options.goal in ['pedestal', 'triforce_hunt', 'local_triforce_hunt']:
rom.write_byte(0x18003E, 0x01) # make ganon invincible
elif world.goal[player] in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
elif local_world.options.goal in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
rom.write_byte(0x18003E, 0x05) # make ganon invincible until enough triforce pieces are collected
elif world.goal[player] in ['ganon_pedestal']:
elif local_world.options.goal in ['ganon_pedestal']:
rom.write_byte(0x18003E, 0x06)
elif world.goal[player] in ['bosses']:
elif local_world.options.goal in ['bosses']:
rom.write_byte(0x18003E, 0x02) # make ganon invincible until all bosses are beat
elif world.goal[player] in ['crystals']:
elif local_world.options.goal in ['crystals']:
rom.write_byte(0x18003E, 0x04) # make ganon invincible until all crystals
else:
rom.write_byte(0x18003E, 0x03) # make ganon invincible until all crystals and aga 2 are collected
rom.write_byte(0x18005E, world.crystals_needed_for_gt[player])
rom.write_byte(0x18005F, world.crystals_needed_for_ganon[player])
rom.write_byte(0x18005E, local_world.options.crystals_needed_for_gt)
rom.write_byte(0x18005F, local_world.options.crystals_needed_for_ganon)
# Bitfield - enable text box to show with free roaming items
#
@@ -1547,21 +1542,20 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
# c - enabled for inside compasses
# s - enabled for inside small keys
# block HC upstairs doors in rain state in standard mode
rom.write_byte(0x18008A, 0x01 if world.mode[player] == "standard" and world.entrance_shuffle[player] != 'vanilla' else 0x00)
rom.write_byte(0x18008A, 0x01 if local_world.options.mode == "standard" and local_world.options.entrance_shuffle != 'vanilla' else 0x00)
rom.write_byte(0x18016A, 0x10 | ((0x01 if world.small_key_shuffle[player] else 0x00)
| (0x02 if world.compass_shuffle[player] else 0x00)
| (0x04 if world.map_shuffle[player] else 0x00)
| (0x08 if world.big_key_shuffle[
player] else 0x00))) # free roaming item text boxes
rom.write_byte(0x18003B, 0x01 if world.map_shuffle[player] else 0x00) # maps showing crystals on overworld
rom.write_byte(0x18016A, 0x10 | ((0x01 if local_world.options.small_key_shuffle else 0x00)
| (0x02 if local_world.options.compass_shuffle else 0x00)
| (0x04 if local_world.options.map_shuffle else 0x00)
| (0x08 if local_world.options.big_key_shuffle else 0x00))) # free roaming item text boxes
rom.write_byte(0x18003B, 0x01 if local_world.options.map_shuffle else 0x00) # maps showing crystals on overworld
# compasses showing dungeon count
if local_world.clock_mode or world.dungeon_counters[player] == 'off':
if local_world.clock_mode or local_world.options.dungeon_counters == 'off':
rom.write_byte(0x18003C, 0x00) # Currently must be off if timer is on, because they use same HUD location
elif world.dungeon_counters[player] == 'on':
elif local_world.options.dungeon_counters == 'on':
rom.write_byte(0x18003C, 0x02) # always on
elif world.compass_shuffle[player] or world.dungeon_counters[player] == 'pickup':
elif local_world.options.compass_shuffle or local_world.options.dungeon_counters == 'pickup':
rom.write_byte(0x18003C, 0x01) # show on pickup
else:
rom.write_byte(0x18003C, 0x00)
@@ -1574,11 +1568,11 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
# b - Big Key
# a - Small Key
#
rom.write_byte(0x180045, ((0x00 if (world.small_key_shuffle[player] == small_key_shuffle.option_original_dungeon or
world.small_key_shuffle[player] == small_key_shuffle.option_universal) else 0x01)
| (0x02 if world.big_key_shuffle[player] else 0x00)
| (0x04 if world.map_shuffle[player] else 0x00)
| (0x08 if world.compass_shuffle[player] else 0x00))) # free roaming items in menu
rom.write_byte(0x180045, ((0x00 if (local_world.options.small_key_shuffle == small_key_shuffle.option_original_dungeon or
local_world.options.small_key_shuffle == small_key_shuffle.option_universal) else 0x01)
| (0x02 if local_world.options.big_key_shuffle else 0x00)
| (0x04 if local_world.options.map_shuffle else 0x00)
| (0x08 if local_world.options.compass_shuffle else 0x00))) # free roaming items in menu
# Map reveals
reveal_bytes = {
@@ -1604,31 +1598,25 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
return 0x0000
rom.write_int16(0x18017A,
get_reveal_bytes('Green Pendant') if world.map_shuffle[player] else 0x0000) # Sahasrahla reveal
rom.write_int16(0x18017C, get_reveal_bytes('Crystal 5') | get_reveal_bytes('Crystal 6') if world.map_shuffle[
player] else 0x0000) # Bomb Shop Reveal
get_reveal_bytes('Green Pendant') if local_world.options.map_shuffle else 0x0000) # Sahasrahla reveal
rom.write_int16(0x18017C, get_reveal_bytes('Crystal 5') | get_reveal_bytes('Crystal 6') if local_world.options.map_shuffle else 0x0000) # Bomb Shop Reveal
rom.write_byte(0x180172, 0x01 if world.small_key_shuffle[
player] == small_key_shuffle.option_universal else 0x00) # universal keys
rom.write_byte(0x18637E, 0x01 if world.retro_bow[player] else 0x00) # Skip quiver in item shops once bought
rom.write_byte(0x180175, 0x01 if world.retro_bow[player] else 0x00) # rupee bow
rom.write_byte(0x180176, 0x0A if world.retro_bow[player] else 0x00) # wood arrow cost
rom.write_byte(0x180178, 0x32 if world.retro_bow[player] else 0x00) # silver arrow cost
rom.write_byte(0x301FC, 0xDA if world.retro_bow[player] else 0xE1) # rupees replace arrows under pots
rom.write_byte(0x30052, 0xDB if world.retro_bow[player] else 0xE2) # replace arrows in fish prize from bottle merchant
rom.write_bytes(0xECB4E, [0xA9, 0x00, 0xEA, 0xEA] if world.retro_bow[player] else [0xAF, 0x77, 0xF3,
0x7E]) # Thief steals rupees instead of arrows
rom.write_bytes(0xF0D96, [0xA9, 0x00, 0xEA, 0xEA] if world.retro_bow[player] else [0xAF, 0x77, 0xF3,
0x7E]) # Pikit steals rupees instead of arrows
rom.write_bytes(0xEDA5,
[0x35, 0x41] if world.retro_bow[player] else [0x43, 0x44]) # Chest game gives rupees instead of arrows
rom.write_byte(0x180172, 0x01 if local_world.options.small_key_shuffle == small_key_shuffle.option_universal else 0x00) # universal keys
rom.write_byte(0x18637E, 0x01 if local_world.options.retro_bow else 0x00) # Skip quiver in item shops once bought
rom.write_byte(0x180175, 0x01 if local_world.options.retro_bow else 0x00) # rupee bow
rom.write_byte(0x180176, 0x0A if local_world.options.retro_bow else 0x00) # wood arrow cost
rom.write_byte(0x180178, 0x32 if local_world.options.retro_bow else 0x00) # silver arrow cost
rom.write_byte(0x301FC, 0xDA if local_world.options.retro_bow else 0xE1) # rupees replace arrows under pots
rom.write_byte(0x30052, 0xDB if local_world.options.retro_bow else 0xE2) # replace arrows in fish prize from bottle merchant
rom.write_bytes(0xECB4E, [0xA9, 0x00, 0xEA, 0xEA] if local_world.options.retro_bow else [0xAF, 0x77, 0xF3, 0x7E]) # Thief steals rupees instead of arrows
rom.write_bytes(0xF0D96, [0xA9, 0x00, 0xEA, 0xEA] if local_world.options.retro_bow else [0xAF, 0x77, 0xF3, 0x7E]) # Pikit steals rupees instead of arrows
rom.write_bytes(0xEDA5, [0x35, 0x41] if local_world.options.retro_bow else [0x43, 0x44]) # Chest game gives rupees instead of arrows
digging_game_rng = local_random.randint(1, 30) # set rng for digging game
rom.write_byte(0x180020, digging_game_rng)
rom.write_byte(0xEFD95, digging_game_rng)
rom.write_byte(0x1800A3, 0x01) # enable correct world setting behaviour after agahnim kills
rom.write_byte(0x1800A4, 0x01 if world.glitches_required[player] != 'no_logic' else 0x00) # enable POD EG fix
rom.write_byte(0x186383, 0x01 if world.glitches_required[
player] == 'no_logic' else 0x00) # disable glitching to Triforce from Ganons Room
rom.write_byte(0x1800A4, 0x01 if local_world.options.glitches_required != 'no_logic' else 0x00) # enable POD EG fix
rom.write_byte(0x186383, 0x01 if local_world.options.glitches_required == 'no_logic' else 0x00) # disable glitching to Triforce from Ganons Room
rom.write_byte(0x180042, 0x01 if world.save_and_quit_from_boss else 0x00) # Allow Save and Quit after boss kill
# remove shield from uncle
@@ -1645,7 +1633,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
rom.write_bytes(0x180185, [0, 0, 0]) # Uncle respawn refills (magic, bombs, arrows)
rom.write_bytes(0x180188, [0, 0, 0]) # Zelda respawn refills (magic, bombs, arrows)
rom.write_bytes(0x18018B, [0, 0, 0]) # Mantle respawn refills (magic, bombs, arrows)
if world.mode[player] == 'standard' and uncle_location.item and uncle_location.item.player == player:
if local_world.options.mode == 'standard' and uncle_location.item and uncle_location.item.player == player:
if uncle_location.item.name in {'Bow', 'Progressive Bow'}:
rom.write_byte(0x18004E, 1) # Escape Fill (arrows)
rom.write_int16(0x180183, 300) # Escape fill rupee bow
@@ -1673,8 +1661,8 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
0xAD, 0xBF, 0x0A, 0xF0, 0x4F])
# allow smith into multi-entrance caves in appropriate shuffles
if world.entrance_shuffle[player] in ['restricted', 'full', 'crossed', 'insanity'] or (
world.entrance_shuffle[player] == 'simple' and world.mode[player] == 'inverted'):
if local_world.options.entrance_shuffle in ['restricted', 'full', 'crossed', 'insanity'] or (
local_world.options.entrance_shuffle == 'simple' and local_world.options.mode == 'inverted'):
rom.write_byte(0x18004C, 0x01)
# set correct flag for hera basement item
@@ -1694,8 +1682,8 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
rom.write_byte(0xFED31, 0x2A) # bombable exit
rom.write_byte(0xFEE41, 0x2A) # bombable exit
if world.tile_shuffle[player]:
tile_set = TileSet.get_random_tile_set(world.per_slot_randoms[player])
if local_world.options.tile_shuffle:
tile_set = TileSet.get_random_tile_set(world.worlds[player].random)
rom.write_byte(0x4BA21, tile_set.get_speed())
rom.write_byte(0x4BA1D, tile_set.get_len())
rom.write_bytes(0x4BA2A, tile_set.get_bytes())
@@ -1770,9 +1758,9 @@ def write_custom_shops(rom, world, player):
slot = 0 if shop.type == ShopType.TakeAny else index
if item is None:
break
if world.shop_item_slots[player] or shop.type == ShopType.TakeAny:
count_shop = (shop.region.name != 'Potion Shop' or world.include_witch_hut[player]) and \
(shop.region.name != 'Capacity Upgrade' or world.shuffle_capacity_upgrades[player])
if world.worlds[player].options.shop_item_slots or shop.type == ShopType.TakeAny:
count_shop = (shop.region.name != 'Potion Shop' or world.worlds[player].options.include_witch_hut) and \
(shop.region.name != 'Capacity Upgrade' or world.worlds[player].options.shuffle_capacity_upgrades)
rom.write_byte(0x186560 + shop.sram_offset + slot, 1 if count_shop else 0)
if item['item'] == 'Single Arrow' and item['player'] == 0:
arrow_mask |= 1 << index
@@ -1789,7 +1777,7 @@ def write_custom_shops(rom, world, player):
item_code = get_nonnative_item_sprite(world.worlds[item['player']].item_name_to_id[item['item']])
else:
item_code = item_table[item["item"]].item_code
if item['item'] == 'Single Arrow' and item['player'] == 0 and world.retro_bow[player]:
if item['item'] == 'Single Arrow' and item['player'] == 0 and world.worlds[player].options.retro_bow:
rom.write_byte(0x186500 + shop.sram_offset + slot, arrow_mask)
item_data = [shop_id, item_code] + price_data + \
@@ -1802,7 +1790,7 @@ def write_custom_shops(rom, world, player):
items_data.extend([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])
rom.write_bytes(0x184900, items_data)
if world.retro_bow[player]:
if world.worlds[player].options.retro_bow:
retro_shop_slots.append(0xFF)
rom.write_bytes(0x186540, retro_shop_slots)
@@ -2207,19 +2195,18 @@ def write_string_to_rom(rom, target, string):
def write_strings(rom, world, player):
from . import ALTTPWorld
local_random = world.worlds[player].random
w: ALTTPWorld = world.worlds[player]
local_random = w.random
tt = TextTable()
tt.removeUnwantedText()
# Let's keep this guy's text accurate to the shuffle setting.
if world.entrance_shuffle[player] in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']:
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']:
tt['kakariko_flophouse_man_no_flippers'] = 'I really hate mowing my yard.\n{PAGEBREAK}\nI should move.'
tt['kakariko_flophouse_man'] = 'I really hate mowing my yard.\n{PAGEBREAK}\nI should move.'
if world.mode[player] == 'inverted':
if world.worlds[player].options.mode == 'inverted':
tt['sign_village_of_outcasts'] = 'attention\nferal ducks sighted\nhiding in statues\n\nflute players beware\n'
def hint_text(dest, ped_hint=False):
@@ -2238,21 +2225,21 @@ def write_strings(rom, world, player):
hint += f" for {world.player_name[dest.player]}"
return hint
if world.scams[player].gives_king_zora_hint:
if world.worlds[player].options.scams.gives_king_zora_hint:
# Zora hint
zora_location = world.get_location("King Zora", player)
tt['zora_tells_cost'] = f"You got 500 rupees to buy {hint_text(zora_location.item)}" \
f"\n ≥ Duh\n Oh carp\n{{CHOICE}}"
if world.scams[player].gives_bottle_merchant_hint:
if world.worlds[player].options.scams.gives_bottle_merchant_hint:
# Bottle Vendor hint
vendor_location = world.get_location("Bottle Merchant", player)
tt['bottle_vendor_choice'] = f"I gots {hint_text(vendor_location.item)}\nYous gots 100 rupees?" \
f"\n ≥ I want\n no way!\n{{CHOICE}}"
# First we write hints about entrances, some from the inconvenient list others from all reasonable entrances.
if world.hints[player]:
if world.hints[player].value >= 2:
if world.hints[player] == "full":
if world.worlds[player].options.hints:
if world.worlds[player].options.hints.value >= 2:
if world.worlds[player].options.hints == "full":
tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles have hints!'
else:
tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles can have hints!'
@@ -2265,11 +2252,11 @@ def write_strings(rom, world, player):
entrances_to_hint = {}
entrances_to_hint.update(InconvenientDungeonEntrances)
if world.shuffle_ganon:
if world.mode[player] == 'inverted':
if world.worlds[player].options.mode == 'inverted':
entrances_to_hint.update({'Inverted Ganons Tower': 'The sealed castle door'})
else:
entrances_to_hint.update({'Ganons Tower': 'Ganon\'s Tower'})
if world.entrance_shuffle[player] in ['simple', 'restricted']:
if world.worlds[player].options.entrance_shuffle in ['simple', 'restricted']:
for entrance in all_entrances:
if entrance.name in entrances_to_hint:
this_hint = entrances_to_hint[entrance.name] + ' leads to ' + hint_text(
@@ -2279,9 +2266,9 @@ def write_strings(rom, world, player):
break
# Now we write inconvenient locations for most shuffles and finish taking care of the less chaotic ones.
entrances_to_hint.update(InconvenientOtherEntrances)
if world.entrance_shuffle[player] in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
hint_count = 0
elif world.entrance_shuffle[player] in ['simple', 'restricted']:
elif world.worlds[player].options.entrance_shuffle in ['simple', 'restricted']:
hint_count = 2
else:
hint_count = 4
@@ -2298,31 +2285,31 @@ def write_strings(rom, world, player):
# Next we handle hints for randomly selected other entrances,
# curating the selection intelligently based on shuffle.
if world.entrance_shuffle[player] not in ['simple', 'restricted']:
if world.worlds[player].options.entrance_shuffle not in ['simple', 'restricted']:
entrances_to_hint.update(ConnectorEntrances)
entrances_to_hint.update(DungeonEntrances)
if world.mode[player] == 'inverted':
if world.worlds[player].options.mode == 'inverted':
entrances_to_hint.update({'Inverted Agahnims Tower': 'The dark mountain tower'})
else:
entrances_to_hint.update({'Agahnims Tower': 'The sealed castle door'})
elif world.entrance_shuffle[player] == 'restricted':
elif world.worlds[player].options.entrance_shuffle == 'restricted':
entrances_to_hint.update(ConnectorEntrances)
entrances_to_hint.update(OtherEntrances)
if world.mode[player] == 'inverted':
if world.worlds[player].options.mode == 'inverted':
entrances_to_hint.update({'Inverted Dark Sanctuary': 'The dark sanctuary cave'})
entrances_to_hint.update({'Inverted Big Bomb Shop': 'The old hero\'s dark home'})
entrances_to_hint.update({'Inverted Links House': 'The old hero\'s light home'})
else:
entrances_to_hint.update({'Dark Sanctuary Hint': 'The dark sanctuary cave'})
entrances_to_hint.update({'Big Bomb Shop': 'The old bomb shop'})
if world.entrance_shuffle[player] != 'insanity':
if world.worlds[player].options.entrance_shuffle != 'insanity':
entrances_to_hint.update(InsanityEntrances)
if world.shuffle_ganon:
if world.mode[player] == 'inverted':
if world.worlds[player].options.mode == 'inverted':
entrances_to_hint.update({'Inverted Pyramid Entrance': 'The extra castle passage'})
else:
entrances_to_hint.update({'Pyramid Ledge': 'The pyramid ledge'})
hint_count = 4 if world.entrance_shuffle[player] not in ['vanilla', 'dungeons_simple', 'dungeons_full',
hint_count = 4 if world.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full',
'dungeons_crossed'] else 0
for entrance in all_entrances:
if entrance.name in entrances_to_hint:
@@ -2337,10 +2324,10 @@ def write_strings(rom, world, player):
# Next we write a few hints for specific inconvenient locations. We don't make many because in entrance this is highly unpredictable.
locations_to_hint = InconvenientLocations.copy()
if world.entrance_shuffle[player] in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
locations_to_hint.extend(InconvenientVanillaLocations)
local_random.shuffle(locations_to_hint)
hint_count = 3 if world.entrance_shuffle[player] not in ['vanilla', 'dungeons_simple', 'dungeons_full',
hint_count = 3 if world.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full',
'dungeons_crossed'] else 5
for location in locations_to_hint[:hint_count]:
if location == 'Swamp Left':
@@ -2395,15 +2382,15 @@ def write_strings(rom, world, player):
# Lastly we write hints to show where certain interesting items are.
items_to_hint = RelevantItems.copy()
if world.small_key_shuffle[player].hints_useful:
if world.worlds[player].options.small_key_shuffle.hints_useful:
items_to_hint |= item_name_groups["Small Keys"]
if world.big_key_shuffle[player].hints_useful:
if world.worlds[player].options.big_key_shuffle.hints_useful:
items_to_hint |= item_name_groups["Big Keys"]
if world.hints[player] == "full":
if world.worlds[player].options.hints == "full":
hint_count = len(hint_locations) # fill all remaining hint locations with Item hints.
else:
hint_count = 5 if world.entrance_shuffle[player] not in ['vanilla', 'dungeons_simple', 'dungeons_full',
hint_count = 5 if world.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full',
'dungeons_crossed'] else 8
hint_count = min(hint_count, len(items_to_hint), len(hint_locations))
if hint_count:
@@ -2434,7 +2421,7 @@ def write_strings(rom, world, player):
tt['ganon_phase_3_no_silvers'] = 'Did you find the silver arrows%s' % silverarrow_hint
tt['ganon_phase_3_no_silvers_alt'] = 'Did you find the silver arrows%s' % silverarrow_hint
if world.worlds[player].has_progressive_bows and (w.difficulty_requirements.progressive_bow_limit >= 2 or (
world.swordless[player] or world.glitches_required[player] == 'no_glitches')):
world.worlds[player].options.swordless or world.worlds[player].options.glitches_required == 'no_glitches')):
prog_bow_locs = world.find_item_locations('Progressive Bow', player, True)
local_random.shuffle(prog_bow_locs)
found_bow = False
@@ -2458,26 +2445,26 @@ def write_strings(rom, world, player):
greenpendant = world.find_item('Green Pendant', player)
tt['sahasrahla_bring_courage'] = 'I lost my family heirloom in %s' % greenpendant.hint_text
if world.crystals_needed_for_gt[player] == 1:
if world.worlds[player].options.crystals_needed_for_gt == 1:
tt['sign_ganons_tower'] = 'You need a crystal to enter.'
else:
tt['sign_ganons_tower'] = f'You need {world.crystals_needed_for_gt[player]} crystals to enter.'
tt['sign_ganons_tower'] = f'You need {world.worlds[player].options.crystals_needed_for_gt} crystals to enter.'
if world.goal[player] == 'bosses':
if world.worlds[player].options.goal == 'bosses':
tt['sign_ganon'] = 'You need to kill all bosses, Ganon last.'
elif world.goal[player] == 'ganon_pedestal':
elif world.worlds[player].options.goal == 'ganon_pedestal':
tt['sign_ganon'] = 'You need to pull the pedestal to defeat Ganon.'
elif world.goal[player] == "ganon":
if world.crystals_needed_for_ganon[player] == 1:
elif world.worlds[player].options.goal == "ganon":
if world.worlds[player].options.crystals_needed_for_ganon == 1:
tt['sign_ganon'] = 'You need a crystal to beat Ganon and have beaten Agahnim atop Ganons Tower.'
else:
tt['sign_ganon'] = f'You need {world.crystals_needed_for_ganon[player]} crystals to beat Ganon and ' \
tt['sign_ganon'] = f'You need {world.worlds[player].options.crystals_needed_for_ganon} crystals to beat Ganon and ' \
f'have beaten Agahnim atop Ganons Tower'
else:
if world.crystals_needed_for_ganon[player] == 1:
if world.worlds[player].options.crystals_needed_for_ganon == 1:
tt['sign_ganon'] = 'You need a crystal to beat Ganon.'
else:
tt['sign_ganon'] = f'You need {world.crystals_needed_for_ganon[player]} crystals to beat Ganon.'
tt['sign_ganon'] = f'You need {world.worlds[player].options.crystals_needed_for_ganon} crystals to beat Ganon.'
tt['uncle_leaving_text'] = Uncle_texts[local_random.randint(0, len(Uncle_texts) - 1)]
tt['end_triforce'] = "{NOBORDER}\n" + Triforce_texts[local_random.randint(0, len(Triforce_texts) - 1)]
@@ -2490,10 +2477,10 @@ def write_strings(rom, world, player):
triforce_pieces_required = max(0, w.treasure_hunt_required -
sum(1 for item in world.precollected_items[player] if item.name == "Triforce Piece"))
if world.goal[player] in ['triforce_hunt', 'local_triforce_hunt']:
if world.worlds[player].options.goal in ['triforce_hunt', 'local_triforce_hunt']:
tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Get the Triforce Pieces.'
tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.'
if world.goal[player] == 'triforce_hunt' and world.players > 1:
if world.worlds[player].options.goal == 'triforce_hunt' and world.players > 1:
tt['sign_ganon'] = 'Go find the Triforce pieces with your friends... Ganon is invincible!'
else:
tt['sign_ganon'] = 'Go find the Triforce pieces... Ganon is invincible!'
@@ -2507,7 +2494,7 @@ def write_strings(rom, world, player):
"invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \
"hidden in a hollow tree. If you bring\n%d Triforce piece out of %d, I can reassemble it." % \
(triforce_pieces_required, w.treasure_hunt_total)
elif world.goal[player] in ['pedestal']:
elif world.worlds[player].options.goal in ['pedestal']:
tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Your goal is at the pedestal.'
tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.'
tt['sign_ganon'] = 'You need to get to the pedestal... Ganon is invincible!'
@@ -2516,17 +2503,17 @@ def write_strings(rom, world, player):
tt['ganon_fall_in_alt'] = 'You cannot defeat me until you finish your goal!'
tt['ganon_phase_3_alt'] = 'Got wax in\nyour ears?\nI can not die!'
if triforce_pieces_required > 1:
if world.goal[player] == 'ganon_triforce_hunt' and world.players > 1:
if world.worlds[player].options.goal == 'ganon_triforce_hunt' and world.players > 1:
tt['sign_ganon'] = 'You need to find %d Triforce pieces out of %d with your friends to defeat Ganon.' % \
(triforce_pieces_required, w.treasure_hunt_total)
elif world.goal[player] in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
elif world.worlds[player].options.goal in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
tt['sign_ganon'] = 'You need to find %d Triforce pieces out of %d to defeat Ganon.' % \
(triforce_pieces_required, w.treasure_hunt_total)
else:
if world.goal[player] == 'ganon_triforce_hunt' and world.players > 1:
if world.worlds[player].options.goal == 'ganon_triforce_hunt' and world.players > 1:
tt['sign_ganon'] = 'You need to find %d Triforce piece out of %d with your friends to defeat Ganon.' % \
(triforce_pieces_required, w.treasure_hunt_total)
elif world.goal[player] in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
elif world.worlds[player].options.goal in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
tt['sign_ganon'] = 'You need to find %d Triforce piece out of %d to defeat Ganon.' % \
(triforce_pieces_required, w.treasure_hunt_total)
@@ -2549,11 +2536,11 @@ def write_strings(rom, world, player):
tt['tablet_bombos_book'] = bombos_text
# inverted spawn menu changes
if world.mode[player] == 'inverted':
if world.worlds[player].options.mode == 'inverted':
tt['menu_start_2'] = "{MENU}\n{SPEED0}\n≥@'s house\n Dark Chapel\n{CHOICE3}"
tt['menu_start_3'] = "{MENU}\n{SPEED0}\n≥@'s house\n Dark Chapel\n Mountain Cave\n{CHOICE2}"
for at, text, _ in world.plando_texts[player]:
for at, text, _ in world.worlds[player].options.plando_texts:
if at not in tt:
raise Exception(f"No text target \"{at}\" found.")
@@ -2626,12 +2613,12 @@ def set_inverted_mode(world, player, rom):
rom.write_byte(snes_to_pc(0x08D40C), 0xD0) # morph proof
# the following bytes should only be written in vanilla
# or they'll overwrite the randomizer's shuffles
if world.entrance_shuffle[player] == 'vanilla':
if world.worlds[player].options.entrance_shuffle == 'vanilla':
rom.write_byte(0xDBB73 + 0x23, 0x37) # switch AT and GT
rom.write_byte(0xDBB73 + 0x36, 0x24)
rom.write_int16(0x15AEE + 2 * 0x38, 0x00E0)
rom.write_int16(0x15AEE + 2 * 0x25, 0x000C)
if world.entrance_shuffle[player] in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
rom.write_byte(0x15B8C, 0x6C)
rom.write_byte(0xDBB73 + 0x00, 0x53) # switch bomb shop and links house
rom.write_byte(0xDBB73 + 0x52, 0x01)
@@ -2689,7 +2676,7 @@ def set_inverted_mode(world, player, rom):
rom.write_int16(snes_to_pc(0x02D9A6), 0x005A)
rom.write_byte(snes_to_pc(0x02D9B3), 0x12)
# keep the old man spawn point at old man house unless shuffle is vanilla
if world.entrance_shuffle[player] in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']:
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']:
rom.write_bytes(snes_to_pc(0x308350), [0x00, 0x00, 0x01])
rom.write_int16(snes_to_pc(0x02D8DE), 0x00F1)
rom.write_bytes(snes_to_pc(0x02D910), [0x1F, 0x1E, 0x1F, 0x1F, 0x03, 0x02, 0x03, 0x03])
@@ -2752,7 +2739,7 @@ def set_inverted_mode(world, player, rom):
rom.write_int16s(snes_to_pc(0x1bb836), [0x001B, 0x001B, 0x001B])
rom.write_int16(snes_to_pc(0x308300), 0x0140) # new pyramid hole entrance
rom.write_int16(snes_to_pc(0x308320), 0x001B)
if world.entrance_shuffle[player] in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
rom.write_byte(snes_to_pc(0x308340), 0x7B)
rom.write_int16(snes_to_pc(0x1af504), 0x148B)
rom.write_int16(snes_to_pc(0x1af50c), 0x149B)
@@ -2789,10 +2776,10 @@ def set_inverted_mode(world, player, rom):
rom.write_bytes(snes_to_pc(0x1BC85A), [0x50, 0x0F, 0x82])
rom.write_int16(0xDB96F + 2 * 0x35, 0x001B) # move pyramid exit door
rom.write_int16(0xDBA71 + 2 * 0x35, 0x06A4)
if world.entrance_shuffle[player] in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
rom.write_byte(0xDBB73 + 0x35, 0x36)
rom.write_byte(snes_to_pc(0x09D436), 0xF3) # remove castle gate warp
if world.entrance_shuffle[player] in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
rom.write_int16(0x15AEE + 2 * 0x37, 0x0010) # pyramid exit to new hc area
rom.write_byte(0x15B8C + 0x37, 0x1B)
rom.write_int16(0x15BDB + 2 * 0x37, 0x0418)

View File

@@ -3,7 +3,7 @@ import logging
from typing import Iterator, Set
from Options import ItemsAccessibility
from BaseClasses import Entrance, MultiWorld
from BaseClasses import MultiWorld
from worlds.generic.Rules import (add_item_rule, add_rule, forbid_item,
item_name_in_location_names, location_item_name, set_rule, allow_self_locking_items)
@@ -27,9 +27,9 @@ from .UnderworldGlitchRules import underworld_glitches_rules
def set_rules(world):
player = world.player
world = world.multiworld
if world.glitches_required[player] == 'no_logic':
if world.worlds[player].options.glitches_required == 'no_logic':
if player == next(player_id for player_id in world.get_game_players("A Link to the Past")
if world.glitches_required[player_id] == 'no_logic'): # only warn one time
if world.worlds[player_id].options.glitches_required == 'no_logic'): # only warn one time
logging.info(
'WARNING! Seeds generated under this logic often require major glitches and may be impossible!')
@@ -40,8 +40,8 @@ def set_rules(world):
else:
# Set access rules according to max glitches for multiworld progression.
# Set accessibility to none, and shuffle assuming the no logic players can always win
world.accessibility[player].value = ItemsAccessibility.option_minimal
world.progression_balancing[player].value = 0
world.worlds[player].options.accessibility.value = ItemsAccessibility.option_minimal
world.worlds[player].options.progression_balancing.value = 0
else:
world.completion_condition[player] = lambda state: state.has('Triforce', player)
@@ -49,52 +49,52 @@ def set_rules(world):
dungeon_boss_rules(world, player)
global_rules(world, player)
if world.mode[player] != 'inverted':
if world.worlds[player].options.mode != 'inverted':
default_rules(world, player)
if world.mode[player] == 'open':
if world.worlds[player].options.mode == 'open':
open_rules(world, player)
elif world.mode[player] == 'standard':
elif world.worlds[player].options.mode == 'standard':
standard_rules(world, player)
elif world.mode[player] == 'inverted':
elif world.worlds[player].options.mode == 'inverted':
open_rules(world, player)
inverted_rules(world, player)
else:
raise NotImplementedError(f'World state {world.mode[player]} is not implemented yet')
raise NotImplementedError(f'World state {world.worlds[player].options.mode} is not implemented yet')
if world.glitches_required[player] == 'no_glitches':
if world.worlds[player].options.glitches_required == 'no_glitches':
no_glitches_rules(world, player)
forbid_bomb_jump_requirements(world, player)
elif world.glitches_required[player] == 'overworld_glitches':
elif world.worlds[player].options.glitches_required == 'overworld_glitches':
# Initially setting no_glitches_rules to set the baseline rules for some
# entrances. The overworld_glitches_rules set is primarily additive.
no_glitches_rules(world, player)
fake_flipper_rules(world, player)
overworld_glitches_rules(world, player)
forbid_bomb_jump_requirements(world, player)
elif world.glitches_required[player] in ['hybrid_major_glitches', 'no_logic']:
elif world.worlds[player].options.glitches_required in ['hybrid_major_glitches', 'no_logic']:
no_glitches_rules(world, player)
fake_flipper_rules(world, player)
overworld_glitches_rules(world, player)
underworld_glitches_rules(world, player)
bomb_jump_requirements(world, player)
elif world.glitches_required[player] == 'minor_glitches':
elif world.worlds[player].options.glitches_required == 'minor_glitches':
no_glitches_rules(world, player)
fake_flipper_rules(world, player)
forbid_bomb_jump_requirements(world, player)
else:
raise NotImplementedError(f'Not implemented yet: Logic - {world.glitches_required[player]}')
raise NotImplementedError(f'Not implemented yet: Logic - {world.worlds[player].options.glitches_required}')
if world.goal[player] == 'bosses':
if world.worlds[player].options.goal == 'bosses':
# require all bosses to beat ganon
add_rule(world.get_location('Ganon', player), lambda state: state.can_reach('Master Sword Pedestal', 'Location', player) and state.has('Beat Agahnim 1', player) and state.has('Beat Agahnim 2', player) and has_crystals(state, 7, player))
elif world.goal[player] == 'ganon':
elif world.worlds[player].options.goal == 'ganon':
# require aga2 to beat ganon
add_rule(world.get_location('Ganon', player), lambda state: state.has('Beat Agahnim 2', player))
if world.mode[player] != 'inverted':
if world.worlds[player].options.mode != 'inverted':
set_big_bomb_rules(world, player)
if world.glitches_required[player].current_key in {'overworld_glitches', 'hybrid_major_glitches', 'no_logic'} and world.entrance_shuffle[player].current_key not in {'insanity', 'insanity_legacy', 'madness'}:
if world.worlds[player].options.glitches_required.current_key in {'overworld_glitches', 'hybrid_major_glitches', 'no_logic'} and world.worlds[player].options.entrance_shuffle.current_key not in {'insanity', 'insanity_legacy', 'madness'}:
path_to_courtyard = mirrorless_path_to_castle_courtyard(world, player)
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.multiworld.get_entrance('Dark Death Mountain Offset Mirror', player).can_reach(state) and all(rule(state) for rule in path_to_courtyard), 'or')
else:
@@ -102,21 +102,24 @@ def set_rules(world):
# if swamp and dam have not been moved we require mirror for swamp palace
# however there is mirrorless swamp in hybrid MG, so we don't necessarily want this. HMG handles this requirement itself.
if not world.worlds[player].swamp_patch_required and world.glitches_required[player] not in ['hybrid_major_glitches', 'no_logic']:
if not world.worlds[player].swamp_patch_required and world.worlds[player].options.glitches_required not in ['hybrid_major_glitches', 'no_logic']:
add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player))
# GT Entrance may be required for Turtle Rock for OWG and < 7 required
ganons_tower = world.get_entrance('Inverted Ganons Tower' if world.mode[player] == 'inverted' else 'Ganons Tower', player)
if world.crystals_needed_for_gt[player] == 7 and not (world.glitches_required[player] in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic'] and world.mode[player] != 'inverted'):
ganons_tower = world.get_entrance('Inverted Ganons Tower' if world.worlds[player].options.mode == 'inverted' else 'Ganons Tower', player)
if (world.worlds[player].options.crystals_needed_for_gt == 7
and not (world.worlds[player].options.glitches_required
in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']
and world.worlds[player].options.mode != 'inverted')):
set_rule(ganons_tower, lambda state: False)
set_trock_key_rules(world, player)
set_rule(ganons_tower, lambda state: has_crystals(state, state.multiworld.crystals_needed_for_gt[player], player))
if world.mode[player] != 'inverted' and world.glitches_required[player] in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
set_rule(ganons_tower, lambda state: has_crystals(state, state.multiworld.worlds[player].options.crystals_needed_for_gt, player))
if world.worlds[player].options.mode != 'inverted' and world.worlds[player].options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
add_rule(world.get_entrance('Ganons Tower', player), lambda state: state.multiworld.get_entrance('Ganons Tower Ascent', player).can_reach(state), 'or')
set_bunny_rules(world, player, world.mode[player] == 'inverted')
set_bunny_rules(world, player, world.worlds[player].options.mode == 'inverted')
def mirrorless_path_to_castle_courtyard(world, player):
@@ -150,17 +153,17 @@ def set_always_allow(spot, rule):
def add_lamp_requirement(world: MultiWorld, spot, player: int, has_accessible_torch: bool = False):
if world.dark_room_logic[player] == "lamp":
if world.worlds[player].options.dark_room_logic == "lamp":
add_rule(spot, lambda state: state.has('Lamp', player))
elif world.dark_room_logic[player] == "torches": # implicitly lamp as well
elif world.worlds[player].options.dark_room_logic == "torches": # implicitly lamp as well
if has_accessible_torch:
add_rule(spot, lambda state: state.has('Lamp', player) or state.has('Fire Rod', player))
else:
add_rule(spot, lambda state: state.has('Lamp', player))
elif world.dark_room_logic[player] == "none":
elif world.worlds[player].options.dark_room_logic == "none":
pass
else:
raise ValueError(f"Unknown Dark Room Logic: {world.dark_room_logic[player]}")
raise ValueError(f"Unknown Dark Room Logic: {world.worlds[player].options.dark_room_logic}")
non_crossover_items = (item_name_groups["Small Keys"] | item_name_groups["Big Keys"] | progression_items) - {
@@ -227,12 +230,13 @@ def global_rules(multiworld: MultiWorld, player: int):
set_rule(multiworld.get_location('Sick Kid', player), lambda state: state.has_group("Bottles", player))
set_rule(multiworld.get_location('Library', player), lambda state: state.has('Pegasus Boots', player))
if multiworld.enemy_shuffle[player]:
if world.options.enemy_shuffle:
set_rule(multiworld.get_location('Mimic Cave', player), lambda state: state.has('Hammer', player) and
can_kill_most_things(state, player, 4))
else:
set_rule(multiworld.get_location('Mimic Cave', player), lambda state: state.has('Hammer', player)
and ((state.multiworld.enemy_health[player] in ("easy", "default") and can_use_bombs(state, player, 4))
and ((state.multiworld.worlds[player].options.enemy_health in ("easy", "default")
and can_use_bombs(state, player, 4))
or can_shoot_arrows(state, player) or state.has("Cane of Somaria", player)
or has_beam_sword(state, player)))
@@ -299,8 +303,7 @@ def global_rules(multiworld: MultiWorld, player: int):
set_rule(multiworld.get_entrance('Sewers Door', player),
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4) or (
multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal and multiworld.mode[
player] == 'standard')) # standard universal small keys cannot access the shop
world.options.small_key_shuffle == small_key_shuffle.option_universal and world.options.mode == 'standard')) # standard universal small keys cannot access the shop
set_rule(multiworld.get_entrance('Sewers Back Door', player),
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4))
set_rule(multiworld.get_entrance('Sewers Secret Room', player), lambda state: can_bomb_or_bonk(state, player))
@@ -339,12 +342,12 @@ def global_rules(multiworld: MultiWorld, player: int):
add_rule(ep_prize, lambda state: state.has('Big Key (Eastern Palace)', player) and
state._lttp_has_key('Small Key (Eastern Palace)', player, 2) and
ep_prize.parent_region.dungeon.boss.can_defeat(state))
if not multiworld.enemy_shuffle[player]:
if not world.options.enemy_shuffle:
add_rule(ep_boss, lambda state: can_shoot_arrows(state, player))
add_rule(ep_prize, lambda state: can_shoot_arrows(state, player))
# You can always kill the Stalfos' with the pots on easy/normal
if multiworld.enemy_health[player] in ("hard", "expert") or multiworld.enemy_shuffle[player]:
if world.options.enemy_health in ("hard", "expert") or world.options.enemy_shuffle:
stalfos_rule = lambda state: can_kill_most_things(state, player, 4)
for location in ['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest',
'Eastern Palace - Dark Square Pot Key', 'Eastern Palace - Dark Eyegore Key Drop',
@@ -362,14 +365,14 @@ def global_rules(multiworld: MultiWorld, player: int):
add_rule(multiworld.get_location('Desert Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 4) and state.has('Big Key (Desert Palace)', player) and has_fire_source(state, player) and state.multiworld.get_location('Desert Palace - Boss', player).parent_region.dungeon.boss.can_defeat(state))
# logic patch to prevent placing a crystal in Desert that's required to reach the required keys
if not (multiworld.small_key_shuffle[player] and multiworld.big_key_shuffle[player]):
if not (world.options.small_key_shuffle and world.options.big_key_shuffle):
add_rule(multiworld.get_location('Desert Palace - Prize', player), lambda state: state.multiworld.get_region('Desert Palace Main (Outer)', player).can_reach(state))
set_rule(multiworld.get_location('Tower of Hera - Basement Cage', player), lambda state: can_activate_crystal_switch(state, player))
set_rule(multiworld.get_location('Tower of Hera - Map Chest', player), lambda state: can_activate_crystal_switch(state, player))
set_rule(multiworld.get_entrance('Tower of Hera Small Key Door', player), lambda state: can_activate_crystal_switch(state, player) and (state._lttp_has_key('Small Key (Tower of Hera)', player) or location_item_name(state, 'Tower of Hera - Big Key Chest', player) == ('Small Key (Tower of Hera)', player)))
set_rule(multiworld.get_entrance('Tower of Hera Big Key Door', player), lambda state: can_activate_crystal_switch(state, player) and state.has('Big Key (Tower of Hera)', player))
if multiworld.enemy_shuffle[player]:
if world.options.enemy_shuffle:
add_rule(multiworld.get_entrance('Tower of Hera Big Key Door', player), lambda state: can_kill_most_things(state, player, 3))
else:
add_rule(multiworld.get_entrance('Tower of Hera Big Key Door', player),
@@ -378,7 +381,7 @@ def global_rules(multiworld: MultiWorld, player: int):
or state.has("Cane of Somaria", player)))
set_rule(multiworld.get_location('Tower of Hera - Big Chest', player), lambda state: state.has('Big Key (Tower of Hera)', player))
set_rule(multiworld.get_location('Tower of Hera - Big Key Chest', player), lambda state: has_fire_source(state, player))
if multiworld.accessibility[player] != 'full':
if world.options.accessibility != 'full':
set_always_allow(multiworld.get_location('Tower of Hera - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Tower of Hera)' and item.player == player)
set_rule(multiworld.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Flippers', player) and state.has('Open Floodgate', player))
@@ -387,32 +390,32 @@ def global_rules(multiworld: MultiWorld, player: int):
set_rule(multiworld.get_location('Swamp Palace - Trench 1 Pot Key', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 2))
set_rule(multiworld.get_entrance('Swamp Palace (Center)', player), lambda state: state.has('Hammer', player) and state._lttp_has_key('Small Key (Swamp Palace)', player, 3))
set_rule(multiworld.get_location('Swamp Palace - Hookshot Pot Key', player), lambda state: state.has('Hookshot', player))
if multiworld.pot_shuffle[player]:
if world.options.pot_shuffle:
# it could move the key to the top right platform which can only be reached with bombs
add_rule(multiworld.get_location('Swamp Palace - Hookshot Pot Key', player), lambda state: can_use_bombs(state, player))
set_rule(multiworld.get_entrance('Swamp Palace (West)', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6)
if state.has('Hookshot', player)
else state._lttp_has_key('Small Key (Swamp Palace)', player, 4))
set_rule(multiworld.get_location('Swamp Palace - Big Chest', player), lambda state: state.has('Big Key (Swamp Palace)', player))
if multiworld.accessibility[player] != 'full':
if world.options.accessibility != 'full':
allow_self_locking_items(multiworld.get_location('Swamp Palace - Big Chest', player), 'Big Key (Swamp Palace)')
set_rule(multiworld.get_entrance('Swamp Palace (North)', player), lambda state: state.has('Hookshot', player) and state._lttp_has_key('Small Key (Swamp Palace)', player, 5))
if not multiworld.small_key_shuffle[player] and multiworld.glitches_required[player] not in ['hybrid_major_glitches', 'no_logic']:
if not world.options.small_key_shuffle and world.options.glitches_required not in ['hybrid_major_glitches', 'no_logic']:
forbid_item(multiworld.get_location('Swamp Palace - Entrance', player), 'Big Key (Swamp Palace)', player)
add_rule(multiworld.get_location('Swamp Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6))
add_rule(multiworld.get_location('Swamp Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6))
if multiworld.pot_shuffle[player]:
if world.options.pot_shuffle:
# key can (and probably will) be moved behind bombable wall
set_rule(multiworld.get_location('Swamp Palace - Waterway Pot Key', player), lambda state: can_use_bombs(state, player))
set_rule(multiworld.get_entrance('Thieves Town Big Key Door', player), lambda state: state.has('Big Key (Thieves Town)', player))
if multiworld.worlds[player].dungeons["Thieves Town"].boss.enemizer_name == "Blind":
if world.dungeons["Thieves Town"].boss.enemizer_name == "Blind":
set_rule(multiworld.get_entrance('Blind Fight', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3) and can_use_bombs(state, player))
set_rule(multiworld.get_location('Thieves\' Town - Big Chest', player),
lambda state: ((state._lttp_has_key('Small Key (Thieves Town)', player, 3)) or (location_item_name(state, 'Thieves\' Town - Big Chest', player) == ("Small Key (Thieves Town)", player)) and state._lttp_has_key('Small Key (Thieves Town)', player, 2)) and state.has('Hammer', player))
set_rule(multiworld.get_location('Thieves\' Town - Blind\'s Cell', player),
lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
if multiworld.accessibility[player] != 'full' and not multiworld.key_drop_shuffle[player]:
if world.options.accessibility != 'full' and not world.options.key_drop_shuffle:
set_always_allow(multiworld.get_location('Thieves\' Town - Big Chest', player), lambda state, item: item.name == 'Small Key (Thieves Town)' and item.player == player)
set_rule(multiworld.get_location('Thieves\' Town - Attic', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3))
set_rule(multiworld.get_location('Thieves\' Town - Spike Switch Pot Key', player),
@@ -424,7 +427,7 @@ def global_rules(multiworld: MultiWorld, player: int):
set_rule(multiworld.get_entrance('Skull Woods First Section West Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
set_rule(multiworld.get_entrance('Skull Woods First Section (Left) Door to Exit', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
set_rule(multiworld.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player) and can_use_bombs(state, player))
if multiworld.accessibility[player] != 'full':
if world.options.accessibility != 'full':
allow_self_locking_items(multiworld.get_location('Skull Woods - Big Chest', player), 'Big Key (Skull Woods)')
set_rule(multiworld.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 4) and state.has('Fire Rod', player) and has_sword(state, player)) # sword required for curtain
add_rule(multiworld.get_location('Skull Woods - Prize', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
@@ -501,13 +504,13 @@ def global_rules(multiworld: MultiWorld, player: int):
set_rule(multiworld.get_entrance('Turtle Rock (Trinexx)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6) and state.has('Big Key (Turtle Rock)', player) and state.has('Cane of Somaria', player))
set_rule(multiworld.get_entrance('Turtle Rock Second Section Bomb Wall', player), lambda state: can_kill_most_things(state, player, 10))
if not multiworld.worlds[player].fix_trock_doors:
if not world.fix_trock_doors:
add_rule(multiworld.get_entrance('Turtle Rock Second Section Bomb Wall', player), lambda state: can_use_bombs(state, player))
set_rule(multiworld.get_entrance('Turtle Rock Second Section from Bomb Wall', player), lambda state: can_use_bombs(state, player))
set_rule(multiworld.get_entrance('Turtle Rock Eye Bridge from Bomb Wall', player), lambda state: can_use_bombs(state, player))
set_rule(multiworld.get_entrance('Turtle Rock Eye Bridge Bomb Wall', player), lambda state: can_use_bombs(state, player))
if multiworld.enemy_shuffle[player]:
if world.options.enemy_shuffle:
set_rule(multiworld.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: can_bomb_or_bonk(state, player) and can_kill_most_things(state, player, 3))
else:
set_rule(multiworld.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: can_bomb_or_bonk(state, player) and can_shoot_arrows(state, player))
@@ -517,18 +520,18 @@ def global_rules(multiworld: MultiWorld, player: int):
set_rule(multiworld.get_entrance('Palace of Darkness (North)', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 4))
set_rule(multiworld.get_location('Palace of Darkness - Big Chest', player), lambda state: can_use_bombs(state, player) and state.has('Big Key (Palace of Darkness)', player))
set_rule(multiworld.get_location('Palace of Darkness - The Arena - Ledge', player), lambda state: can_use_bombs(state, player))
if multiworld.pot_shuffle[player]:
if world.options.pot_shuffle:
# chest switch may be up on ledge where bombs are required
set_rule(multiworld.get_location('Palace of Darkness - Stalfos Basement', player), lambda state: can_use_bombs(state, player))
set_rule(multiworld.get_entrance('Palace of Darkness Big Key Chest Staircase', player), lambda state: can_use_bombs(state, player) and (state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or (
location_item_name(state, 'Palace of Darkness - Big Key Chest', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 3))))
if multiworld.accessibility[player] != 'full':
if world.options.accessibility != 'full':
set_always_allow(multiworld.get_location('Palace of Darkness - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5))
set_rule(multiworld.get_entrance('Palace of Darkness Spike Statue Room Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or (
location_item_name(state, 'Palace of Darkness - Harmless Hellway', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 4)))
if multiworld.accessibility[player] != 'full':
if world.options.accessibility != 'full':
set_always_allow(multiworld.get_location('Palace of Darkness - Harmless Hellway', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5))
set_rule(multiworld.get_entrance('Palace of Darkness Maze Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6))
@@ -541,13 +544,13 @@ def global_rules(multiworld: MultiWorld, player: int):
set_rule(multiworld.get_location('Ganons Tower - Bob\'s Torch', player), lambda state: state.has('Pegasus Boots', player))
set_rule(multiworld.get_entrance('Ganons Tower (Tile Room)', player), lambda state: state.has('Cane of Somaria', player))
set_rule(multiworld.get_entrance('Ganons Tower (Hookshot Room)', player), lambda state: state.has('Hammer', player) and (state.has('Hookshot', player) or state.has('Pegasus Boots', player)))
if multiworld.pot_shuffle[player]:
if world.options.pot_shuffle:
set_rule(multiworld.get_location('Ganons Tower - Conveyor Cross Pot Key', player), lambda state: state.has('Hammer', player) and (state.has('Hookshot', player) or state.has('Pegasus Boots', player)))
set_rule(multiworld.get_entrance('Ganons Tower (Map Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 8) or (
location_item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player)] and state._lttp_has_key('Small Key (Ganons Tower)', player, 6)))
# this seemed to be causing generation failure, disable for now
# if world.accessibility[player] != 'full':
# if world.worlds[player].options.accessibility != 'full':
# set_always_allow(world.get_location('Ganons Tower - Map Chest', player), lambda state, item: item.name == 'Small Key (Ganons Tower)' and item.player == player and state._lttp_has_key('Small Key (Ganons Tower)', player, 7) and state.can_reach('Ganons Tower (Hookshot Room)', 'region', player))
# It is possible to need more than 6 keys to get through this entrance if you spend keys elsewhere. We reflect this in the chest requirements.
@@ -582,7 +585,7 @@ def global_rules(multiworld: MultiWorld, player: int):
lambda state: can_use_bombs(state, player) and state.multiworld.get_location('Ganons Tower - Big Key Chest', player).parent_region.dungeon.bosses['bottom'].can_defeat(state))
set_rule(multiworld.get_location('Ganons Tower - Big Key Room - Right', player),
lambda state: can_use_bombs(state, player) and state.multiworld.get_location('Ganons Tower - Big Key Room - Right', player).parent_region.dungeon.bosses['bottom'].can_defeat(state))
if multiworld.enemy_shuffle[player]:
if world.options.enemy_shuffle:
set_rule(multiworld.get_entrance('Ganons Tower Big Key Door', player),
lambda state: state.has('Big Key (Ganons Tower)', player))
else:
@@ -600,12 +603,12 @@ def global_rules(multiworld: MultiWorld, player: int):
set_defeat_dungeon_boss_rule(multiworld.get_location('Agahnim 2', player))
ganon = multiworld.get_location('Ganon', player)
set_rule(ganon, lambda state: GanonDefeatRule(state, player))
if multiworld.goal[player] in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
if world.options.goal in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
add_rule(ganon, lambda state: has_triforce_pieces(state, player))
elif multiworld.goal[player] == 'ganon_pedestal':
elif world.options.goal == 'ganon_pedestal':
add_rule(multiworld.get_location('Ganon', player), lambda state: state.can_reach('Master Sword Pedestal', 'Location', player))
else:
add_rule(ganon, lambda state: has_crystals(state, state.multiworld.crystals_needed_for_ganon[player], player))
add_rule(ganon, lambda state: has_crystals(state, state.multiworld.worlds[player].options.crystals_needed_for_ganon, player))
set_rule(multiworld.get_entrance('Ganon Drop', player), lambda state: has_beam_sword(state, player)) # need to damage ganon to get tiles to drop
set_rule(multiworld.get_location('Flute Activation Spot', player), lambda state: state.has('Flute', player))
@@ -722,9 +725,9 @@ def default_rules(world, player):
set_rule(world.get_entrance('Floating Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has('Moon Pearl', player) and has_sword(state, player) and has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword required to cast magic (!)
set_rule(world.get_entrance('Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.open_pyramid[player].to_bool(world, player))
set_rule(world.get_entrance('Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.worlds[player].options.open_pyramid.to_bool(world, player))
if world.swordless[player]:
if world.worlds[player].options.swordless:
swordless_rules(world, player)
@@ -879,14 +882,14 @@ def inverted_rules(world, player):
set_rule(world.get_entrance('Dark Grassy Lawn Flute', player), lambda state: state.has('Activated Flute', player))
set_rule(world.get_entrance('Hammer Peg Area Flute', player), lambda state: state.has('Activated Flute', player))
set_rule(world.get_entrance('Inverted Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.open_pyramid[player])
set_rule(world.get_entrance('Inverted Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.worlds[player].options.open_pyramid)
if world.swordless[player]:
if world.worlds[player].options.swordless:
swordless_rules(world, player)
def no_glitches_rules(world, player):
""""""
if world.mode[player] == 'inverted':
if world.worlds[player].options.mode == 'inverted':
set_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Moon Pearl', player) and (state.has('Flippers', player) or can_lift_rocks(state, player)))
set_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to
set_rule(world.get_entrance('Lake Hylia Island Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to
@@ -910,7 +913,7 @@ def no_glitches_rules(world, player):
add_conditional_lamps(world, player)
def fake_flipper_rules(world, player):
if world.mode[player] == 'inverted':
if world.worlds[player].options.mode == 'inverted':
set_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Moon Pearl', player))
set_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Moon Pearl', player))
set_rule(world.get_entrance('Lake Hylia Island Pier', player), lambda state: state.has('Moon Pearl', player))
@@ -996,7 +999,7 @@ def add_conditional_lamps(world, player):
'Location', True)
add_conditional_lamp('Palace of Darkness - Dark Basement - Right', 'Palace of Darkness (Entrance)',
'Location', True)
if world.mode[player] != 'inverted':
if world.worlds[player].options.mode != 'inverted':
add_conditional_lamp('Agahnim 1', 'Agahnims Tower', 'Entrance')
add_conditional_lamp('Castle Tower - Dark Maze', 'Agahnims Tower')
add_conditional_lamp('Castle Tower - Dark Archer Key Drop', 'Agahnims Tower')
@@ -1018,7 +1021,7 @@ def add_conditional_lamps(world, player):
add_conditional_lamp('Eastern Palace - Boss', 'Eastern Palace', 'Location', True)
add_conditional_lamp('Eastern Palace - Prize', 'Eastern Palace', 'Location', True)
if not world.mode[player] == "standard":
if not world.worlds[player].options.mode == "standard":
add_lamp_requirement(world, world.get_location('Sewers - Dark Cross', player), player)
add_lamp_requirement(world, world.get_entrance('Sewers Back Door', player), player)
add_lamp_requirement(world, world.get_entrance('Throne Room', player), player)
@@ -1044,7 +1047,7 @@ def open_rules(world, player):
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player),
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4)
and state.has('Big Key (Hyrule Castle)', player)
and (world.enemy_health[player] in ("easy", "default")
and (world.worlds[player].options.enemy_health in ("easy", "default")
or can_kill_most_things(state, player, 1)))
@@ -1058,7 +1061,7 @@ def swordless_rules(world, player):
set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has('Hammer', player)) # need to damage ganon to get tiles to drop
if world.mode[player] != 'inverted':
if world.worlds[player].options.mode != 'inverted':
set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has('Hammer', player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle
set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has('Moon Pearl', player) and has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword not required to use medallion for opening in swordless (!)
set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has('Moon Pearl', player) and has_misery_mire_medallion(state, player)) # sword not required to use medallion for opening in swordless (!)
@@ -1071,9 +1074,8 @@ def swordless_rules(world, player):
def add_connection(parent_name, target_name, entrance_name, world, player):
parent = world.get_region(parent_name, player)
target = world.get_region(target_name, player)
connection = Entrance(player, entrance_name, parent)
parent.exits.append(connection)
connection.connect(target)
parent.connect(target, entrance_name)
def standard_rules(world, player):
@@ -1085,7 +1087,7 @@ def standard_rules(world, player):
set_rule(world.get_entrance('Links House S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player))
set_rule(world.get_entrance('Sanctuary S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player))
if world.small_key_shuffle[player] != small_key_shuffle.option_universal:
if world.worlds[player].options.small_key_shuffle != small_key_shuffle.option_universal:
set_rule(world.get_location('Hyrule Castle - Boomerang Guard Key Drop', player),
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 1)
and can_kill_most_things(state, player, 2))
@@ -1098,7 +1100,7 @@ def standard_rules(world, player):
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player),
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 2)
and state.has('Big Key (Hyrule Castle)', player)
and (world.enemy_health[player] in ("easy", "default")
and (world.worlds[player].options.enemy_health in ("easy", "default")
or can_kill_most_things(state, player, 1)))
set_rule(world.get_location('Sewers - Key Rat Key Drop', player),
@@ -1108,6 +1110,7 @@ def standard_rules(world, player):
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player),
lambda state: state.has('Big Key (Hyrule Castle)', player))
def toss_junk_item(world, player):
items = ['Rupees (20)', 'Bombs (3)', 'Arrows (10)', 'Rupees (5)', 'Rupee (1)', 'Bombs (10)',
'Single Arrow', 'Rupees (50)', 'Rupees (100)', 'Single Bomb', 'Bee', 'Bee Trap',
@@ -1195,15 +1198,15 @@ def set_trock_key_rules(multiworld, player):
return 6
# If TR is only accessible from the middle, the big key must be further restricted to prevent softlock potential
if not can_reach_front and not multiworld.small_key_shuffle[player]:
if not can_reach_front and not multiworld.worlds[player].options.small_key_shuffle:
# Must not go in the Big Key Chest - only 1 other chest available and 2+ keys required for all other chests
forbid_item(multiworld.get_location('Turtle Rock - Big Key Chest', player), 'Big Key (Turtle Rock)', player)
if not can_reach_big_chest:
# Must not go in the Chain Chomps chest - only 2 other chests available and 3+ keys required for all other chests
forbid_item(multiworld.get_location('Turtle Rock - Chain Chomps', player), 'Big Key (Turtle Rock)', player)
forbid_item(multiworld.get_location('Turtle Rock - Pokey 2 Key Drop', player), 'Big Key (Turtle Rock)', player)
if multiworld.accessibility[player] == 'full':
if multiworld.big_key_shuffle[player] and can_reach_big_chest:
if multiworld.worlds[player].options.accessibility == 'full':
if multiworld.worlds[player].options.big_key_shuffle and can_reach_big_chest:
# Must not go in the dungeon - all 3 available chests (Chomps, Big Chest, Crystaroller) must be keys to access laser bridge, and the big key is required first
for location in ['Turtle Rock - Chain Chomps', 'Turtle Rock - Compass Chest',
'Turtle Rock - Pokey 1 Key Drop', 'Turtle Rock - Pokey 2 Key Drop',
@@ -1216,9 +1219,9 @@ def set_trock_key_rules(multiworld, player):
location.place_locked_item(item)
toss_junk_item(multiworld, player)
if multiworld.accessibility[player] != 'full':
if multiworld.worlds[player].options.accessibility != 'full':
set_always_allow(multiworld.get_location('Turtle Rock - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Turtle Rock)' and item.player == player
and state.can_reach(state.multiworld.get_region('Turtle Rock (Second Section)', player)))
and state.can_reach(state.multiworld.get_region('Turtle Rock (Second Section)', player)))
def set_big_bomb_rules(world, player):
@@ -1683,7 +1686,7 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool):
def get_rule_to_add(region, location = None, connecting_entrance = None):
# In OWG, a location can potentially be superbunny-mirror accessible or
# bunny revival accessible.
if world.glitches_required[player] in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
if world.worlds[player].options.glitches_required in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
if region.name == 'Swamp Palace (Entrance)': # Need to 0hp revive - not in logic
return lambda state: state.has('Moon Pearl', player)
if region.name == 'Tower of Hera (Bottom)': # Need to hit the crystal switch
@@ -1723,7 +1726,7 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool):
seen.add(new_region)
if not is_link(new_region):
# For glitch rulesets, establish superbunny and revival rules.
if world.glitches_required[player] in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] and entrance.name not in OverworldGlitchRules.get_invalid_bunny_revival_dungeons():
if world.worlds[player].options.glitches_required in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] and entrance.name not in OverworldGlitchRules.get_invalid_bunny_revival_dungeons():
if region.name in OverworldGlitchRules.get_sword_required_superbunny_mirror_regions():
possible_options.append(lambda state: path_to_access_rule(new_path, entrance) and state.has('Magic Mirror', player) and has_sword(state, player))
elif (region.name in OverworldGlitchRules.get_boots_required_superbunny_mirror_regions()
@@ -1760,7 +1763,7 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool):
# Add requirements for all locations that are actually in the dark world, except those available to the bunny, including dungeon revival
for entrance in world.get_entrances(player):
if is_bunny(entrance.connected_region):
if world.glitches_required[player] in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] :
if world.worlds[player].options.glitches_required in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] :
if entrance.connected_region.type == LTTPRegionType.Dungeon:
if entrance.parent_region.type != LTTPRegionType.Dungeon and entrance.connected_region.name in OverworldGlitchRules.get_invalid_bunny_revival_dungeons():
add_rule(entrance, get_rule_to_add(entrance.connected_region, None, entrance))
@@ -1768,7 +1771,7 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool):
if entrance.connected_region.name == 'Turtle Rock (Entrance)':
add_rule(world.get_entrance('Turtle Rock Entrance Gap', player), get_rule_to_add(entrance.connected_region, None, entrance))
for location in entrance.connected_region.locations:
if world.glitches_required[player] in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] and entrance.name in OverworldGlitchRules.get_invalid_mirror_bunny_entrances():
if world.worlds[player].options.glitches_required in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] and entrance.name in OverworldGlitchRules.get_invalid_mirror_bunny_entrances():
continue
if location.name in bunny_accessible_locations:
continue

View File

@@ -168,7 +168,7 @@ def push_shop_inventories(multiworld):
for location in shop_slots:
item_name = location.item.name
# Retro Bow arrows will already have been pushed
if (not multiworld.retro_bow[location.player]) or ((item_name, location.item.player)
if (not multiworld.worlds[location.player].options.retro_bow) or ((item_name, location.item.player)
!= ("Single Arrow", location.player)):
location.shop.push_inventory(location.shop_slot, item_name,
round(location.shop_price * get_price_modifier(location.item)),
@@ -185,36 +185,36 @@ def push_shop_inventories(multiworld):
def create_shops(multiworld, player: int):
from .Options import RandomizeShopInventories
player_shop_table = shop_table.copy()
if multiworld.include_witch_hut[player]:
if multiworld.worlds[player].options.include_witch_hut:
player_shop_table["Potion Shop"] = player_shop_table["Potion Shop"]._replace(locked=False)
dynamic_shop_slots = total_dynamic_shop_slots + 3
else:
dynamic_shop_slots = total_dynamic_shop_slots
if multiworld.shuffle_capacity_upgrades[player]:
if multiworld.worlds[player].options.shuffle_capacity_upgrades:
player_shop_table["Capacity Upgrade"] = player_shop_table["Capacity Upgrade"]._replace(locked=False)
num_slots = min(dynamic_shop_slots, multiworld.shop_item_slots[player])
num_slots = min(dynamic_shop_slots, multiworld.worlds[player].options.shop_item_slots)
single_purchase_slots: List[bool] = [True] * num_slots + [False] * (dynamic_shop_slots - num_slots)
multiworld.random.shuffle(single_purchase_slots)
if multiworld.randomize_shop_inventories[player]:
if multiworld.worlds[player].options.randomize_shop_inventories:
default_shop_table = [i for l in
[shop_generation_types[x] for x in ['arrows', 'bombs', 'potions', 'shields', 'bottle'] if
not multiworld.retro_bow[player] or x != 'arrows'] for i in l]
not multiworld.worlds[player].options.retro_bow or x != 'arrows'] for i in l]
new_basic_shop = multiworld.random.sample(default_shop_table, k=3)
new_dark_shop = multiworld.random.sample(default_shop_table, k=3)
for name, shop in player_shop_table.items():
typ, shop_id, keeper, custom, locked, items, sram_offset = shop
if not locked:
new_items = multiworld.random.sample(default_shop_table, k=len(items))
if multiworld.randomize_shop_inventories[player] == RandomizeShopInventories.option_randomize_by_shop_type:
if multiworld.worlds[player].options.randomize_shop_inventories == RandomizeShopInventories.option_randomize_by_shop_type:
if items == _basic_shop_defaults:
new_items = new_basic_shop
elif items == _dark_world_shop_defaults:
new_items = new_dark_shop
keeper = multiworld.random.choice([0xA0, 0xC1, 0xFF])
player_shop_table[name] = ShopData(typ, shop_id, keeper, custom, locked, new_items, sram_offset)
if multiworld.mode[player] == "inverted":
if multiworld.worlds[player].options.mode == "inverted":
# make sure that blue potion is available in inverted, special case locked = None; lock when done.
player_shop_table["Dark Lake Hylia Shop"] = \
player_shop_table["Dark Lake Hylia Shop"]._replace(items=_inverted_hylia_shop_defaults, locked=None)
@@ -237,7 +237,7 @@ def create_shops(multiworld, player: int):
add_rule(loc, lambda state, spot=loc: shop_price_rules(state, player, spot))
loc.shop = shop
loc.shop_slot = index
if ((not (multiworld.shuffle_capacity_upgrades[player] and type == ShopType.UpgradeShop))
if ((not (multiworld.worlds[player].options.shuffle_capacity_upgrades and type == ShopType.UpgradeShop))
and not single_purchase_slots.pop()):
loc.shop_slot_disabled = True
loc.locked = True
@@ -309,18 +309,18 @@ def set_up_shops(multiworld, player: int):
from .Options import small_key_shuffle
# TODO: move hard+ mode changes for shields here, utilizing the new shops
if multiworld.retro_bow[player]:
if multiworld.worlds[player].options.retro_bow:
rss = multiworld.get_region('Red Shield Shop', player).shop
replacement_items = [['Red Potion', 150], ['Green Potion', 75], ['Blue Potion', 200], ['Bombs (10)', 50],
['Blue Shield', 50], ['Small Heart',
10]] # Can't just replace the single arrow with 10 arrows as retro doesn't need them.
if multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal:
if multiworld.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal:
replacement_items.append(['Small Key (Universal)', 100])
replacement_item = multiworld.random.choice(replacement_items)
rss.add_inventory(2, 'Single Arrow', 80, 1, replacement_item[0], replacement_item[1])
rss.locked = True
if multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal or multiworld.retro_bow[player]:
if multiworld.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal or multiworld.worlds[player].options.retro_bow:
for shop in multiworld.random.sample([s for s in multiworld.shops if
s.custom and not s.locked and s.type == ShopType.Shop
and s.region.player == player], 5):
@@ -328,19 +328,19 @@ def set_up_shops(multiworld, player: int):
slots = [0, 1, 2]
multiworld.random.shuffle(slots)
slots = iter(slots)
if multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal:
if multiworld.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal:
shop.add_inventory(next(slots), 'Small Key (Universal)', 100)
if multiworld.retro_bow[player]:
if multiworld.worlds[player].options.retro_bow:
shop.push_inventory(next(slots), 'Single Arrow', 80)
if multiworld.shuffle_capacity_upgrades[player]:
if multiworld.worlds[player].options.shuffle_capacity_upgrades:
for shop in multiworld.shops:
if shop.type == ShopType.UpgradeShop and shop.region.player == player and \
shop.region.name == "Capacity Upgrade":
shop.clear_inventory()
if (multiworld.shuffle_shop_inventories[player] or multiworld.randomize_shop_prices[player]
or multiworld.randomize_cost_types[player]):
if (multiworld.worlds[player].options.shuffle_shop_inventories or multiworld.worlds[player].options.randomize_shop_prices
or multiworld.worlds[player].options.randomize_cost_types):
shops = []
total_inventory = []
for shop in multiworld.shops:
@@ -352,7 +352,7 @@ def set_up_shops(multiworld, player: int):
for item in total_inventory:
item["price_type"], item["price"] = get_price(multiworld, item, player)
if multiworld.shuffle_shop_inventories[player]:
if multiworld.worlds[player].options.shuffle_shop_inventories:
multiworld.random.shuffle(total_inventory)
i = 0
@@ -434,39 +434,39 @@ def get_price(multiworld, item, player: int, price_type=None):
price_types = [price_type]
else:
price_types = [ShopPriceType.Rupees] # included as a chance to not change price
if multiworld.randomize_cost_types[player]:
if multiworld.worlds[player].options.randomize_cost_types:
price_types += [
ShopPriceType.Hearts,
ShopPriceType.Bombs,
ShopPriceType.Magic,
]
if multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal:
if multiworld.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal:
if item and item["item"] == "Small Key (Universal)":
price_types = [ShopPriceType.Rupees, ShopPriceType.Magic] # no logical requirements for repeatable keys
else:
price_types.append(ShopPriceType.Keys)
if multiworld.retro_bow[player]:
if multiworld.worlds[player].options.retro_bow:
if item and item["item"] == "Single Arrow":
price_types = [ShopPriceType.Rupees, ShopPriceType.Magic] # no logical requirements for arrows
else:
price_types.append(ShopPriceType.Arrows)
diff = multiworld.item_pool[player].value
diff = multiworld.worlds[player].options.item_pool.value
if item:
# This is for a shop's regular inventory, the item is already determined, and we will decide the price here
price = item["price"]
if multiworld.randomize_shop_prices[player]:
if multiworld.worlds[player].options.randomize_shop_prices:
adjust = 2 if price < 100 else 5
price = int((price / adjust) * (0.5 + multiworld.per_slot_randoms[player].random() * 1.5)) * adjust
multiworld.per_slot_randoms[player].shuffle(price_types)
price = int((price / adjust) * (0.5 + multiworld.worlds[player].random.random() * 1.5)) * adjust
multiworld.worlds[player].random.shuffle(price_types)
for p_type in price_types:
if any(x in item['item'] for x in price_blacklist[p_type]):
continue
return p_type, price_chart[p_type](price, diff)
else:
# This is an AP location and the price will be adjusted after an item is shuffled into it
p_type = multiworld.per_slot_randoms[player].choice(price_types)
return p_type, price_chart[p_type](min(int(multiworld.per_slot_randoms[player].randint(8, 56)
* multiworld.shop_price_modifier[player] / 100) * 5, 9999), diff)
p_type = multiworld.worlds[player].random.choice(price_types)
return p_type, price_chart[p_type](min(int(multiworld.worlds[player].random.randint(8, 56)
* multiworld.worlds[player].options.shop_price_modifier / 100) * 5, 9999), diff)
def shop_price_rules(state: CollectionState, player: int, location: ALttPLocation):

View File

@@ -6,7 +6,7 @@ def is_not_bunny(state: CollectionState, region: LTTPRegion, player: int) -> boo
if state.has('Moon Pearl', player):
return True
return region.is_light_world if state.multiworld.mode[player] != 'inverted' else region.is_dark_world
return region.is_light_world if state.multiworld.worlds[player].options.mode != 'inverted' else region.is_dark_world
def can_bomb_clip(state: CollectionState, region: LTTPRegion, player: int) -> bool:
@@ -24,7 +24,7 @@ def can_buy(state: CollectionState, item: str, player: int) -> bool:
def can_shoot_arrows(state: CollectionState, player: int, count: int = 0) -> bool:
if state.multiworld.retro_bow[player]:
if state.multiworld.worlds[player].options.retro_bow:
return (state.has('Bow', player) or state.has('Silver Bow', player)) and can_buy(state, 'Single Arrow', player)
return (state.has('Bow', player) or state.has('Silver Bow', player)) and can_hold_arrows(state, player, count)
@@ -74,9 +74,9 @@ def can_extend_magic(state: CollectionState, player: int, smallmagic: int = 16,
elif state.has('Magic Upgrade (1/2)', player):
basemagic = 16
if can_buy_unlimited(state, 'Green Potion', player) or can_buy_unlimited(state, 'Blue Potion', player):
if state.multiworld.item_functionality[player] == 'hard' and not fullrefill:
if state.multiworld.worlds[player].options.item_functionality == 'hard' and not fullrefill:
basemagic = basemagic + int(basemagic * 0.5 * bottle_count(state, player))
elif state.multiworld.item_functionality[player] == 'expert' and not fullrefill:
elif state.multiworld.worlds[player].options.item_functionality == 'expert' and not fullrefill:
basemagic = basemagic + int(basemagic * 0.25 * bottle_count(state, player))
else:
basemagic = basemagic + basemagic * bottle_count(state, player)
@@ -99,12 +99,12 @@ def can_hold_arrows(state: CollectionState, player: int, quantity: int):
def can_use_bombs(state: CollectionState, player: int, quantity: int = 1) -> bool:
bombs = 0 if state.multiworld.bombless_start[player] else 10
bombs = 0 if state.multiworld.worlds[player].options.bombless_start else 10
bombs += ((state.count("Bomb Upgrade (+5)", player) * 5) + (state.count("Bomb Upgrade (+10)", player) * 10)
+ (state.count("Bomb Upgrade (50)", player) * 50))
# Bomb Upgrade (+5) beyond the 6th gives +10
bombs += max(0, ((state.count("Bomb Upgrade (+5)", player) - 6) * 10))
if (not state.multiworld.shuffle_capacity_upgrades[player]) and state.has("Capacity Upgrade Shop", player):
if (not state.multiworld.worlds[player].options.shuffle_capacity_upgrades) and state.has("Capacity Upgrade Shop", player):
bombs += 40
return bombs >= min(quantity, 50)
@@ -120,7 +120,7 @@ def can_activate_crystal_switch(state: CollectionState, player: int) -> bool:
def can_kill_most_things(state: CollectionState, player: int, enemies: int = 5) -> bool:
if state.multiworld.enemy_shuffle[player]:
if state.multiworld.worlds[player].options.enemy_shuffle:
# I don't fully understand Enemizer's logic for placing enemies in spots where they need to be killable, if any.
# Just go with maximal requirements for now.
return (has_melee_weapon(state, player)
@@ -135,7 +135,7 @@ def can_kill_most_things(state: CollectionState, player: int, enemies: int = 5)
or (state.has('Cane of Byrna', player) and (enemies < 6 or can_extend_magic(state, player)))
or can_shoot_arrows(state, player)
or state.has('Fire Rod', player)
or (state.multiworld.enemy_health[player] in ("easy", "default")
or (state.multiworld.worlds[player].options.enemy_health in ("easy", "default")
and can_use_bombs(state, player, enemies * 4)))
@@ -152,7 +152,7 @@ def can_get_good_bee(state: CollectionState, player: int) -> bool:
def can_retrieve_tablet(state: CollectionState, player: int) -> bool:
return state.has('Book of Mudora', player) and (has_beam_sword(state, player) or
(state.multiworld.swordless[player] and
(state.multiworld.worlds[player].options.swordless and
state.has("Hammer", player)))
@@ -179,7 +179,7 @@ def has_fire_source(state: CollectionState, player: int) -> bool:
def can_melt_things(state: CollectionState, player: int) -> bool:
return state.has('Fire Rod', player) or \
(state.has('Bombos', player) and
(state.multiworld.swordless[player] or
(state.multiworld.worlds[player].options.swordless or
has_sword(state, player)))
@@ -192,19 +192,19 @@ def has_turtle_rock_medallion(state: CollectionState, player: int) -> bool:
def can_boots_clip_lw(state: CollectionState, player: int) -> bool:
if state.multiworld.mode[player] == 'inverted':
if state.multiworld.worlds[player].options.mode == 'inverted':
return state.has('Pegasus Boots', player) and state.has('Moon Pearl', player)
return state.has('Pegasus Boots', player)
def can_boots_clip_dw(state: CollectionState, player: int) -> bool:
if state.multiworld.mode[player] != 'inverted':
if state.multiworld.worlds[player].options.mode != 'inverted':
return state.has('Pegasus Boots', player) and state.has('Moon Pearl', player)
return state.has('Pegasus Boots', player)
def can_get_glitched_speed_dw(state: CollectionState, player: int) -> bool:
rules = [state.has('Pegasus Boots', player), any([state.has('Hookshot', player), has_sword(state, player)])]
if state.multiworld.mode[player] != 'inverted':
if state.multiworld.worlds[player].options.mode != 'inverted':
rules.append(state.has('Moon Pearl', player))
return all(rules)

View File

@@ -2,11 +2,10 @@
from typing import Optional, TYPE_CHECKING
from enum import IntEnum
from BaseClasses import Location, Item, ItemClassification, Region, MultiWorld
from BaseClasses import Entrance, Location, Item, ItemClassification, Region, MultiWorld
if TYPE_CHECKING:
from .Dungeons import Dungeon
from .Regions import LTTPRegion
class ALttPLocation(Location):
@@ -77,6 +76,19 @@ class ALttPItem(Item):
return self.type
Addresses = int | list[int] | tuple[int, int, int, int, int, int, int, int, int, int, int, int, int]
class LTTPEntrance(Entrance):
addresses: Addresses | None = None
target: int | None = None
def connect(self, region: Region, addresses: Addresses | None = None, target: int | None = None) -> None:
super().connect(region)
self.addresses = addresses
self.target = target
class LTTPRegionType(IntEnum):
LightWorld = 1
DarkWorld = 2
@@ -90,6 +102,7 @@ class LTTPRegionType(IntEnum):
class LTTPRegion(Region):
entrance_type = LTTPEntrance
type: LTTPRegionType
# will be set after making connections.

View File

@@ -1,6 +1,6 @@
from BaseClasses import Entrance
from worlds.generic.Rules import set_rule, add_rule
from .StateHelpers import can_bomb_clip, has_sword, has_beam_sword, has_fire_source, can_melt_things, has_misery_mire_medallion
from .SubClasses import LTTPEntrance
# We actually need the logic to properly "mark" these regions as Light or Dark world.
@@ -9,17 +9,15 @@ def underworld_glitch_connections(world, player):
specrock = world.get_region('Spectacle Rock Cave (Bottom)', player)
mire = world.get_region('Misery Mire (West)', player)
kikiskip = Entrance(player, 'Kiki Skip', specrock)
mire_to_hera = Entrance(player, 'Mire to Hera Clip', mire)
mire_to_swamp = Entrance(player, 'Hera to Swamp Clip', mire)
specrock.exits.append(kikiskip)
mire.exits.extend([mire_to_hera, mire_to_swamp])
kikiskip = specrock.create_exit('Kiki Skip')
mire_to_hera = mire.create_exit('Mire to Hera Clip')
mire_to_swamp = mire.create_exit('Hera to Swamp Clip')
if world.worlds[player].fix_fake_world:
kikiskip.connect(world.get_entrance('Palace of Darkness Exit', player).connected_region)
mire_to_hera.connect(world.get_entrance('Tower of Hera Exit', player).connected_region)
mire_to_swamp.connect(world.get_entrance('Swamp Palace Exit', player).connected_region)
else:
else:
kikiskip.connect(world.get_region('Palace of Darkness (Entrance)', player))
mire_to_hera.connect(world.get_region('Tower of Hera (Bottom)', player))
mire_to_swamp.connect(world.get_region('Swamp Palace (Entrance)', player))
@@ -37,7 +35,7 @@ def fake_pearl_state(state, player):
# Sets the rules on where we can actually go using this clip.
# Behavior differs based on what type of ER shuffle we're playing.
def dungeon_reentry_rules(world, player, clip: Entrance, dungeon_region: str, dungeon_exit: str):
def dungeon_reentry_rules(world, player, clip: LTTPEntrance, dungeon_region: str, dungeon_exit: str):
fix_dungeon_exits = world.worlds[player].fix_palaceofdarkness_exit
fix_fake_worlds = world.worlds[player].fix_fake_world
@@ -61,7 +59,7 @@ def dungeon_reentry_rules(world, player, clip: Entrance, dungeon_region: str, du
# since the clip links directly to the exterior region.
def underworld_glitches_rules(world, player):
def underworld_glitches_rules(world, player):
# Ice Palace Entrance Clip
# This is the easiest one since it's a simple internal clip.
# Need to also add melting to freezor chest since it's otherwise assumed.
@@ -90,12 +88,12 @@ def underworld_glitches_rules(world, player):
# We need to be able to s+q to old man, then go to either Mire or Hera at either Hera or GT.
# First we require a certain type of entrance shuffle, then build the rule from its pieces.
if not world.worlds[player].swamp_patch_required:
if world.entrance_shuffle[player] in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
rule_map = {
'Misery Mire (Entrance)': (lambda state: True),
'Tower of Hera (Bottom)': (lambda state: state.can_reach('Tower of Hera Big Key Door', 'Entrance', player))
}
inverted = world.mode[player] == 'inverted'
inverted = world.worlds[player].options.mode == 'inverted'
hera_rule = lambda state: (state.has('Moon Pearl', player) or not inverted) and \
rule_map.get(world.get_entrance('Tower of Hera', player).connected_region.name, lambda state: False)(state)
gt_rule = lambda state: (state.has('Moon Pearl', player) or inverted) and \

View File

@@ -141,7 +141,7 @@ class ALTTPWorld(World):
item_name_groups = item_name_groups
location_name_groups = {
"Blind's Hideout": {"Blind's Hideout - Top", "Blind's Hideout - Left", "Blind's Hideout - Right",
"Blind's Hideout - Far Left", "Blind's Hideout - Far Right"},
"Blind's Hideout - Far Left", "Blind's Hideout - Far Right"},
"Kakariko Well": {"Kakariko Well - Top", "Kakariko Well - Left", "Kakariko Well - Middle",
"Kakariko Well - Right", "Kakariko Well - Bottom"},
"Mini Moldorm Cave": {"Mini Moldorm Cave - Far Left", "Mini Moldorm Cave - Left", "Mini Moldorm Cave - Right",
@@ -154,15 +154,23 @@ class ALTTPWorld(World):
"Hookshot Cave": {"Hookshot Cave - Top Right", "Hookshot Cave - Top Left", "Hookshot Cave - Bottom Right",
"Hookshot Cave - Bottom Left"},
"Hyrule Castle": {"Hyrule Castle - Boomerang Chest", "Hyrule Castle - Map Chest",
"Hyrule Castle - Zelda's Chest", "Sewers - Dark Cross", "Sewers - Secret Room - Left",
"Sewers - Secret Room - Middle", "Sewers - Secret Room - Right"},
"Hyrule Castle - Zelda's Chest", "Hyrule Castle - Big Key Drop",
"Hyrule Castle - Boomerang Guard Key Drop", "Hyrule Castle - Map Guard Key Drop",
"Sewers - Dark Cross", "Sewers - Secret Room - Left",
"Sewers - Secret Room - Middle", "Sewers - Secret Room - Right",
"Sewers - Key Rat Key Drop"},
"Eastern Palace": {"Eastern Palace - Compass Chest", "Eastern Palace - Big Chest",
"Eastern Palace - Cannonball Chest", "Eastern Palace - Big Key Chest",
"Eastern Palace - Dark Eyegore Key Drop", "Eastern Palace - Dark Square Pot Key",
"Eastern Palace - Map Chest", "Eastern Palace - Boss"},
"Desert Palace": {"Desert Palace - Big Chest", "Desert Palace - Torch", "Desert Palace - Map Chest",
"Desert Palace - Compass Chest", "Desert Palace - Big Key Chest", "Desert Palace - Boss"},
"Desert Palace - Beamos Hall Pot Key", "Desert Palace - Desert Tiles 1 Pot Key",
"Desert Palace - Desert Tiles 2 Pot Key", "Desert Palace - Compass Chest",
"Desert Palace - Big Key Chest", "Desert Palace - Boss"},
"Tower of Hera": {"Tower of Hera - Basement Cage", "Tower of Hera - Map Chest", "Tower of Hera - Big Key Chest",
"Tower of Hera - Compass Chest", "Tower of Hera - Big Chest", "Tower of Hera - Boss"},
"Castle Tower": {"Castle Tower - Room 03", "Castle Tower - Dark Maze",
"Castle Tower - Dark Archer Key Drop", "Castle Tower - Circle of Pots Key Drop"},
"Palace of Darkness": {"Palace of Darkness - Shooter Room", "Palace of Darkness - The Arena - Bridge",
"Palace of Darkness - Stalfos Basement", "Palace of Darkness - Big Key Chest",
"Palace of Darkness - The Arena - Ledge", "Palace of Darkness - Map Chest",
@@ -173,25 +181,33 @@ class ALTTPWorld(World):
"Swamp Palace": {"Swamp Palace - Entrance", "Swamp Palace - Map Chest", "Swamp Palace - Big Chest",
"Swamp Palace - Compass Chest", "Swamp Palace - Big Key Chest", "Swamp Palace - West Chest",
"Swamp Palace - Flooded Room - Left", "Swamp Palace - Flooded Room - Right",
"Swamp Palace - Waterfall Room", "Swamp Palace - Boss"},
"Swamp Palace - Hookshot Pot Key", "Swamp Palace - Pot Row Pot Key",
"Swamp Palace - Trench 1 Pot Key", "Swamp Palace - Trench 2 Pot Key",
"Swamp Palace - Waterway Pot Key", "Swamp Palace - Waterfall Room", "Swamp Palace - Boss"},
"Thieves' Town": {"Thieves' Town - Big Key Chest", "Thieves' Town - Map Chest", "Thieves' Town - Compass Chest",
"Thieves' Town - Ambush Chest", "Thieves' Town - Attic", "Thieves' Town - Big Chest",
"Thieves' Town - Hallway Pot Key", "Thieves' Town - Spike Switch Pot Key",
"Thieves' Town - Blind's Cell", "Thieves' Town - Boss"},
"Skull Woods": {"Skull Woods - Map Chest", "Skull Woods - Pinball Room", "Skull Woods - Compass Chest",
"Skull Woods - Pot Prison", "Skull Woods - Big Chest", "Skull Woods - Big Key Chest",
"Skull Woods - Spike Corner Key Drop", "Skull Woods - West Lobby Pot Key",
"Skull Woods - Bridge Room", "Skull Woods - Boss"},
"Ice Palace": {"Ice Palace - Compass Chest", "Ice Palace - Freezor Chest", "Ice Palace - Big Chest",
"Ice Palace - Freezor Chest", "Ice Palace - Big Chest", "Ice Palace - Iced T Room",
"Ice Palace - Spike Room", "Ice Palace - Big Key Chest", "Ice Palace - Map Chest",
"Ice Palace - Conveyor Key Drop", "Ice Palace - Hammer Block Key Drop",
"Ice Palace - Jelly Key Drop", "Ice Palace - Many Pots Pot Key",
"Ice Palace - Boss"},
"Misery Mire": {"Misery Mire - Big Chest", "Misery Mire - Map Chest", "Misery Mire - Main Lobby",
"Misery Mire - Bridge Chest", "Misery Mire - Spike Chest", "Misery Mire - Compass Chest",
"Misery Mire - Big Key Chest", "Misery Mire - Boss"},
"Misery Mire - Conveyor Crystal Key Drop", "Misery Mire - Fishbone Pot Key",
"Misery Mire - Spikes Pot Key", "Misery Mire - Big Key Chest", "Misery Mire - Boss"},
"Turtle Rock": {"Turtle Rock - Compass Chest", "Turtle Rock - Roller Room - Left",
"Turtle Rock - Roller Room - Right", "Turtle Rock - Chain Chomps", "Turtle Rock - Big Key Chest",
"Turtle Rock - Big Chest", "Turtle Rock - Crystaroller Room",
"Turtle Rock - Eye Bridge - Bottom Left", "Turtle Rock - Eye Bridge - Bottom Right",
"Turtle Rock - Eye Bridge - Top Left", "Turtle Rock - Eye Bridge - Top Right",
"Turtle Rock - Pokey 1 Key Drop", "Turtle Rock - Pokey 2 Key Drop",
"Turtle Rock - Boss"},
"Ganons Tower": {"Ganons Tower - Bob's Torch", "Ganons Tower - Hope Room - Left",
"Ganons Tower - Hope Room - Right", "Ganons Tower - Tile Room",
@@ -204,10 +220,13 @@ class ALTTPWorld(World):
"Ganons Tower - Randomizer Room - Bottom Left", "Ganons Tower - Randomizer Room - Bottom Right",
"Ganons Tower - Bob's Chest", "Ganons Tower - Big Chest", "Ganons Tower - Big Key Room - Left",
"Ganons Tower - Big Key Room - Right", "Ganons Tower - Big Key Chest",
"Ganons Tower - Mini Helmasaur Room - Left", "Ganons Tower - Mini Helmasaur Room - Right",
"Ganons Tower - Pre-Moldorm Chest", "Ganons Tower - Validation Chest"},
"Ganons Tower - Conveyor Cross Pot Key", "Ganons Tower - Conveyor Star Pits Pot Key",
"Ganons Tower - Double Switch Pot Key", "Ganons Tower - Mini Helmasaur Room - Left",
"Ganons Tower - Mini Helmasaur Room - Right", "Ganons Tower - Pre-Moldorm Chest",
"Ganons Tower - Mini Helmasaur Key Drop", "Ganons Tower - Validation Chest"},
"Ganons Tower Climb": {"Ganons Tower - Mini Helmasaur Room - Left", "Ganons Tower - Mini Helmasaur Room - Right",
"Ganons Tower - Pre-Moldorm Chest", "Ganons Tower - Validation Chest"},
"Ganons Tower - Mini Helmasaur Key Drop", "Ganons Tower - Pre-Moldorm Chest",
"Ganons Tower - Validation Chest"},
}
hint_blacklist = {"Triforce"}
@@ -294,74 +313,62 @@ class ALTTPWorld(World):
break
def generate_early(self):
# write old options
import dataclasses
is_first = self.player == min(self.multiworld.get_game_players(self.game))
for field in dataclasses.fields(self.options_dataclass):
if is_first:
setattr(self.multiworld, field.name, {})
getattr(self.multiworld, field.name)[self.player] = getattr(self.options, field.name)
# end of old options re-establisher
player = self.player
multiworld = self.multiworld
self.fix_trock_doors = (multiworld.entrance_shuffle[player] != 'vanilla'
or multiworld.mode[player] == 'inverted')
self.fix_skullwoods_exit = multiworld.entrance_shuffle[player] not in ['vanilla', 'simple', 'restricted',
'dungeons_simple']
self.fix_palaceofdarkness_exit = multiworld.entrance_shuffle[player] not in ['dungeons_simple', 'vanilla',
'simple', 'restricted']
self.fix_trock_exit = multiworld.entrance_shuffle[player] not in ['vanilla', 'simple', 'restricted',
'dungeons_simple']
self.fix_trock_doors = (self.options.entrance_shuffle != 'vanilla' or self.options.mode == 'inverted')
self.fix_skullwoods_exit = self.options.entrance_shuffle not in ['vanilla', 'simple', 'restricted', 'dungeons_simple']
self.fix_palaceofdarkness_exit = self.options.entrance_shuffle not in ['dungeons_simple', 'vanilla', 'simple', 'restricted']
self.fix_trock_exit = self.options.entrance_shuffle not in ['vanilla', 'simple', 'restricted', 'dungeons_simple']
# fairy bottle fills
bottle_options = [
"Bottle (Red Potion)", "Bottle (Green Potion)", "Bottle (Blue Potion)",
"Bottle (Bee)", "Bottle (Good Bee)"
]
if multiworld.item_pool[player] not in ["hard", "expert"]:
if self.options.item_pool not in ["hard", "expert"]:
bottle_options.append("Bottle (Fairy)")
self.waterfall_fairy_bottle_fill = self.random.choice(bottle_options)
self.pyramid_fairy_bottle_fill = self.random.choice(bottle_options)
if multiworld.mode[player] == 'standard':
if multiworld.small_key_shuffle[player]:
if (multiworld.small_key_shuffle[player] not in
(small_key_shuffle.option_universal, small_key_shuffle.option_own_dungeons,
small_key_shuffle.option_start_with)):
if self.options.mode == 'standard':
if self.options.small_key_shuffle:
if (self.options.small_key_shuffle not in
(small_key_shuffle.option_universal, small_key_shuffle.option_own_dungeons,
small_key_shuffle.option_start_with)):
self.multiworld.local_early_items[self.player]["Small Key (Hyrule Castle)"] = 1
self.multiworld.local_items[self.player].value.add("Small Key (Hyrule Castle)")
self.multiworld.non_local_items[self.player].value.discard("Small Key (Hyrule Castle)")
if multiworld.big_key_shuffle[player]:
self.multiworld.local_items[self.player].value.add("Big Key (Hyrule Castle)")
self.multiworld.non_local_items[self.player].value.discard("Big Key (Hyrule Castle)")
self.options.local_items.value.add("Small Key (Hyrule Castle)")
self.options.non_local_items.value.discard("Small Key (Hyrule Castle)")
if self.options.big_key_shuffle:
self.options.local_items.value.add("Big Key (Hyrule Castle)")
self.options.non_local_items.value.discard("Big Key (Hyrule Castle)")
# system for sharing ER layouts
self.er_seed = str(multiworld.random.randint(0, 2 ** 64))
if multiworld.entrance_shuffle[player] != "vanilla" and multiworld.entrance_shuffle_seed[player] != "random":
shuffle = multiworld.entrance_shuffle[player].current_key
if self.options.entrance_shuffle != "vanilla" and self.options.entrance_shuffle_seed != "random":
shuffle = self.options.entrance_shuffle.current_key
if shuffle == "vanilla":
self.er_seed = "vanilla"
elif (not multiworld.entrance_shuffle_seed[player].value.isdigit()) or multiworld.is_race:
elif (not self.options.entrance_shuffle_seed.value.isdigit()) or multiworld.is_race:
self.er_seed = get_same_seed(multiworld, (
shuffle, multiworld.entrance_shuffle_seed[player].value, multiworld.retro_caves[player], multiworld.mode[player],
multiworld.glitches_required[player]))
shuffle, self.options.entrance_shuffle_seed.value,
self.options.retro_caves,
self.options.mode,
self.options.glitches_required
))
else: # not a race or group seed, use set seed as is.
self.er_seed = int(multiworld.entrance_shuffle_seed[player].value)
elif multiworld.entrance_shuffle[player] == "vanilla":
self.er_seed = int(self.options.entrance_shuffle_seed.value)
elif self.options.entrance_shuffle == "vanilla":
self.er_seed = "vanilla"
for dungeon_item in ["small_key_shuffle", "big_key_shuffle", "compass_shuffle", "map_shuffle"]:
option = getattr(multiworld, dungeon_item)[player]
option = getattr(self.options, dungeon_item)
if option == "own_world":
multiworld.local_items[player].value |= self.item_name_groups[option.item_name_group]
self.options.local_items.value |= self.item_name_groups[option.item_name_group]
elif option == "different_world":
multiworld.non_local_items[player].value |= self.item_name_groups[option.item_name_group]
if multiworld.mode[player] == "standard":
multiworld.non_local_items[player].value -= {"Small Key (Hyrule Castle)"}
self.options.non_local_items.value |= self.item_name_groups[option.item_name_group]
if self.options.mode == "standard":
self.options.non_local_items.value -= {"Small Key (Hyrule Castle)"}
elif option.in_dungeon:
self.dungeon_local_item_names |= self.item_name_groups[option.item_name_group]
if option == "original_dungeon":
@@ -369,15 +376,15 @@ class ALTTPWorld(World):
else:
self.options.local_items.value |= self.dungeon_local_item_names
self.difficulty_requirements = difficulties[multiworld.item_pool[player].current_key]
self.difficulty_requirements = difficulties[self.options.item_pool.current_key]
# enforce pre-defined local items.
if multiworld.goal[player] in ["local_triforce_hunt", "local_ganon_triforce_hunt"]:
multiworld.local_items[player].value.add('Triforce Piece')
if self.options.goal in ["local_triforce_hunt", "local_ganon_triforce_hunt"]:
self.options.local_items.value.add('Triforce Piece')
# Not possible to place crystals outside boss prizes yet (might as well make it consistent with pendants too).
multiworld.non_local_items[player].value -= item_name_groups['Pendants']
multiworld.non_local_items[player].value -= item_name_groups['Crystals']
self.options.non_local_items.value -= item_name_groups['Pendants']
self.options.non_local_items.value -= item_name_groups['Crystals']
create_dungeons = create_dungeons
@@ -385,15 +392,15 @@ class ALTTPWorld(World):
player = self.player
multiworld = self.multiworld
if multiworld.mode[player] != 'inverted':
if self.options.mode != 'inverted':
create_regions(multiworld, player)
else:
create_inverted_regions(multiworld, player)
create_shops(multiworld, player)
self.create_dungeons()
if (multiworld.glitches_required[player] not in ["no_glitches", "minor_glitches"] and
multiworld.entrance_shuffle[player] in [
if (self.options.glitches_required not in ["no_glitches", "minor_glitches"] and
self.options.entrance_shuffle in [
"vanilla", "dungeons_simple", "dungeons_full", "simple", "restricted", "full"]):
self.fix_fake_world = False
@@ -401,7 +408,7 @@ class ALTTPWorld(World):
old_random = multiworld.random
multiworld.random = random.Random(self.er_seed)
if multiworld.mode[player] != 'inverted':
if self.options.mode != 'inverted':
link_entrances(multiworld, player)
mark_light_world_regions(multiworld, player)
else:
@@ -486,8 +493,9 @@ class ALTTPWorld(World):
if state.has('Silver Bow', item.player):
return
elif state.has('Bow', item.player) and (self.difficulty_requirements.progressive_bow_limit >= 2
or self.multiworld.glitches_required[self.player] == 'no_glitches'
or self.multiworld.swordless[self.player]): # modes where silver bow is always required for ganon
or self.options.glitches_required == 'no_glitches'
or self.options.swordless):
# modes where silver bow is always required for ganon
return 'Silver Bow'
elif self.difficulty_requirements.progressive_bow_limit >= 1:
return 'Bow'
@@ -530,9 +538,9 @@ class ALTTPWorld(World):
break
else:
raise FillError('Unable to place dungeon prizes')
if world.mode[player] == 'standard' and world.small_key_shuffle[player] \
and world.small_key_shuffle[player] != small_key_shuffle.option_universal and \
world.small_key_shuffle[player] != small_key_shuffle.option_own_dungeons:
if self.options.mode == 'standard' and self.options.small_key_shuffle \
and self.options.small_key_shuffle != small_key_shuffle.option_universal and \
self.options.small_key_shuffle != small_key_shuffle.option_own_dungeons:
world.local_early_items[player]["Small Key (Hyrule Castle)"] = 1
@classmethod
@@ -573,27 +581,27 @@ class ALTTPWorld(World):
multiworld.spoiler.hashes[player] = get_hash_string(rom.hash)
palettes_options = {
'dungeon': multiworld.uw_palettes[player],
'overworld': multiworld.ow_palettes[player],
'hud': multiworld.hud_palettes[player],
'sword': multiworld.sword_palettes[player],
'shield': multiworld.shield_palettes[player],
'dungeon': self.options.uw_palettes,
'overworld': self.options.ow_palettes,
'hud': self.options.hud_palettes,
'sword': self.options.sword_palettes,
'shield': self.options.shield_palettes,
# 'link': world.link_palettes[player]
}
palettes_options = {key: option.current_key for key, option in palettes_options.items()}
apply_rom_settings(rom, multiworld.heartbeep[player].current_key,
multiworld.heartcolor[player].current_key,
multiworld.quickswap[player],
multiworld.menuspeed[player].current_key,
multiworld.music[player],
apply_rom_settings(rom, self.options.heartbeep.current_key,
self.options.heartcolor.current_key,
self.options.quickswap,
self.options.menuspeed.current_key,
self.options.music,
multiworld.sprite[player],
None,
palettes_options, multiworld, player, True,
reduceflashing=multiworld.reduceflashing[player] or multiworld.is_race,
triforcehud=multiworld.triforcehud[player].current_key,
deathlink=multiworld.death_link[player],
allowcollect=multiworld.allow_collect[player])
reduceflashing=self.options.reduceflashing or multiworld.is_race,
triforcehud=self.options.triforcehud.current_key,
deathlink=self.options.death_link,
allowcollect=self.options.allow_collect)
rompath = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.sfc")
rom.write_to_file(rompath)
@@ -610,7 +618,7 @@ class ALTTPWorld(World):
@classmethod
def stage_extend_hint_information(cls, world, hint_data: typing.Dict[int, typing.Dict[int, str]]):
er_hint_data = {player: {} for player in world.get_game_players("A Link to the Past") if
world.entrance_shuffle[player] != "vanilla" or world.retro_caves[player]}
world.worlds[player].options.entrance_shuffle != "vanilla" or world.worlds[player].options.retro_caves}
for region in world.regions:
if region.player in er_hint_data and region.locations:
@@ -726,7 +734,7 @@ class ALTTPWorld(World):
f" {self.pyramid_fairy_bottle_fill}")
spoiler_handle.write(f"\nWaterfall Fairy ({player_name}):"
f" {self.waterfall_fairy_bottle_fill}")
if self.multiworld.boss_shuffle[self.player] != "none":
if self.options.boss_shuffle != "none":
def create_boss_map() -> typing.Dict:
boss_map = {
"Eastern Palace": self.dungeons["Eastern Palace"].boss.name,
@@ -743,7 +751,7 @@ class ALTTPWorld(World):
"Ganons Tower": "Agahnim 2",
"Ganon": "Ganon"
}
if self.multiworld.mode[self.player] != 'inverted':
if self.options.mode != 'inverted':
boss_map.update({
"Ganons Tower Basement":
self.dungeons["Ganons Tower"].bosses["bottom"].name,
@@ -828,7 +836,7 @@ class ALTTPWorld(World):
"triforce_pieces_available", "triforce_pieces_extra",
]
slot_data = {option_name: getattr(self.multiworld, option_name)[self.player].value for option_name in slot_options}
slot_data = {option_name: getattr(self.options, option_name).value for option_name in slot_options}
slot_data.update({
'mm_medalion': self.required_medallions[0],
@@ -849,8 +857,8 @@ def get_same_seed(world, seed_def: tuple) -> str:
class ALttPLogic(LogicMixin):
def _lttp_has_key(self, item, player, count: int = 1):
if self.multiworld.glitches_required[player] == 'no_logic':
if self.multiworld.worlds[player].options.glitches_required == 'no_logic':
return True
if self.multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal:
if self.multiworld.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal:
return can_buy_unlimited(self, 'Small Key (Universal)', player)
return self.prog_items[player][item] >= count

View File

@@ -14,8 +14,8 @@ class TestDungeon(LTTPTestBase):
self.starting_regions = [] # Where to start exploring
self.remove_exits = [] # Block dungeon exits
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
self.multiworld.bombless_start[1].value = True
self.multiworld.shuffle_capacity_upgrades[1].value = 2
self.multiworld.worlds[1].options.bombless_start.value = True
self.multiworld.worlds[1].options.shuffle_capacity_upgrades.value = 2
create_regions(self.multiworld, 1)
self.multiworld.worlds[1].create_dungeons()
create_shops(self.multiworld, 1)

View File

@@ -14,9 +14,9 @@ class TestInverted(TestBase, LTTPTestBase):
def setUp(self):
self.world_setup()
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
self.multiworld.mode[1].value = 2
self.multiworld.bombless_start[1].value = True
self.multiworld.shuffle_capacity_upgrades[1].value = 2
self.multiworld.worlds[1].options.mode.value = 2
self.multiworld.worlds[1].options.bombless_start.value = True
self.multiworld.worlds[1].options.shuffle_capacity_upgrades.value = 2
create_inverted_regions(self.multiworld, 1)
self.world.create_dungeons()
create_shops(self.multiworld, 1)

View File

@@ -12,7 +12,7 @@ class TestInvertedBombRules(LTTPTestBase):
def setUp(self):
self.world_setup()
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
self.multiworld.mode[1].value = 2
self.multiworld.worlds[1].options.mode.value = 2
create_inverted_regions(self.multiworld, 1)
self.multiworld.worlds[1].create_dungeons()

View File

@@ -14,10 +14,10 @@ from worlds.alttp.test import LTTPTestBase
class TestInvertedMinor(TestBase, LTTPTestBase):
def setUp(self):
self.world_setup()
self.multiworld.mode[1].value = 2
self.multiworld.glitches_required[1] = GlitchesRequired.from_any("minor_glitches")
self.multiworld.bombless_start[1].value = True
self.multiworld.shuffle_capacity_upgrades[1].value = 2
self.multiworld.worlds[1].options.mode.value = 2
self.multiworld.worlds[1].options.glitches_required = GlitchesRequired.from_any("minor_glitches")
self.multiworld.worlds[1].options.bombless_start.value = True
self.multiworld.worlds[1].options.shuffle_capacity_upgrades.value = 2
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
create_inverted_regions(self.multiworld, 1)
self.world.create_dungeons()

View File

@@ -14,10 +14,10 @@ from worlds.alttp.test import LTTPTestBase
class TestInvertedOWG(TestBase, LTTPTestBase):
def setUp(self):
self.world_setup()
self.multiworld.glitches_required[1] = GlitchesRequired.from_any("overworld_glitches")
self.multiworld.mode[1].value = 2
self.multiworld.bombless_start[1].value = True
self.multiworld.shuffle_capacity_upgrades[1].value = 2
self.multiworld.worlds[1].options.glitches_required = GlitchesRequired.from_any("overworld_glitches")
self.multiworld.worlds[1].options.mode.value = 2
self.multiworld.worlds[1].options.bombless_start.value = True
self.multiworld.worlds[1].options.shuffle_capacity_upgrades.value = 2
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
create_inverted_regions(self.multiworld, 1)
self.world.create_dungeons()

View File

@@ -11,9 +11,9 @@ from worlds.alttp.test import LTTPTestBase
class TestMinor(TestBase, LTTPTestBase):
def setUp(self):
self.world_setup()
self.multiworld.glitches_required[1] = GlitchesRequired.from_any("minor_glitches")
self.multiworld.bombless_start[1].value = True
self.multiworld.shuffle_capacity_upgrades[1].value = 2
self.multiworld.worlds[1].options.glitches_required = GlitchesRequired.from_any("minor_glitches")
self.multiworld.worlds[1].options.bombless_start.value = True
self.multiworld.worlds[1].options.shuffle_capacity_upgrades.value = 2
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
self.world.er_seed = 0
self.world.create_regions()

View File

@@ -23,7 +23,7 @@ class GoalPyramidTest(PyramidTestBase):
}
def testCrystalsGoalAccess(self):
self.multiworld.goal[1].value = 1 # crystals
self.multiworld.worlds[1].options.goal.value = 1 # crystals
self.assertFalse(self.can_reach_entrance("Pyramid Hole"))
self.collect_by_name(["Hammer", "Progressive Glove", "Moon Pearl"])
self.assertTrue(self.can_reach_entrance("Pyramid Hole"))

View File

@@ -12,9 +12,9 @@ class TestVanillaOWG(TestBase, LTTPTestBase):
def setUp(self):
self.world_setup()
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
self.multiworld.glitches_required[1] = GlitchesRequired.from_any("overworld_glitches")
self.multiworld.bombless_start[1].value = True
self.multiworld.shuffle_capacity_upgrades[1].value = 2
self.multiworld.worlds[1].options.glitches_required = GlitchesRequired.from_any("overworld_glitches")
self.multiworld.worlds[1].options.bombless_start.value = True
self.multiworld.worlds[1].options.shuffle_capacity_upgrades.value = 2
self.multiworld.worlds[1].er_seed = 0
self.multiworld.worlds[1].create_regions()
self.multiworld.worlds[1].create_items()

View File

@@ -10,10 +10,10 @@ from worlds.alttp.test import LTTPTestBase
class TestVanilla(TestBase, LTTPTestBase):
def setUp(self):
self.world_setup()
self.multiworld.glitches_required[1] = GlitchesRequired.from_any("no_glitches")
self.multiworld.worlds[1].options.glitches_required = GlitchesRequired.from_any("no_glitches")
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
self.multiworld.bombless_start[1].value = True
self.multiworld.shuffle_capacity_upgrades[1].value = 2
self.multiworld.worlds[1].options.bombless_start.value = True
self.multiworld.worlds[1].options.shuffle_capacity_upgrades.value = 2
self.multiworld.worlds[1].er_seed = 0
self.multiworld.worlds[1].create_regions()
self.multiworld.worlds[1].create_items()

View File

@@ -16,8 +16,11 @@ Cyberfunk root folder. *Do not use any pre-release versions of BepInEx 6.*
2. Start Bomb Rush Cyberfunk once so that BepInEx can create its required configuration files.
3. Download the zip archive from the [releases](https://github.com/TRPG0/BRC-Archipelago/releases) page, and extract its
contents into `BepInEx\plugins`.
3. Download `ModLocalizer.dll` from its [releases](https://github.com/TRPG0/BRC-ModLocalizer/releases) page, and put it
in `BepInEx\plugins`.
4. Download the zip archive for the Archipelago plugin from its [releases](https://github.com/TRPG0/BRC-Archipelago/releases)
page, and extract the contents into `BepInEx\plugins`.
After installing Archipelago, there are some additional mods that can also be installed for a better experience:

View File

@@ -7,9 +7,9 @@ config file.
## What is considered a location check in ChecksFinder?
Location checks in are completed when the player finds a spot on a board that has the archipelago logo. The bottom of
the screen has a number next to the archipelago logo, that number is how many you can find so far. You can only get as
many checks as you have gained items, plus five to start with being available.
Location checks get cleared when you open all non-bomb cells in a board. The bottom
of the screen has a number next to the Archipelago logo that displays how many location checks are left to be sent with
your current inventory. You can only get as many checks as you have gained items plus five checks to start with.
## When the player receives an item, what happens?

View File

@@ -10,12 +10,20 @@ from worlds._bizhawk.client import BizHawkClient
if TYPE_CHECKING:
from worlds._bizhawk.context import BizHawkClientContext
DEATHLINK_AREA_NUMBERS = [0, 1, 1, 2, 2, 2, 2, 3, 4, 5, 5, 5, 5, 5, 5, 5,
7, 9, 8, 6, 12, 12, 13, 11, 12, 5, 2, 10, 13, 13]
DEATHLINK_AREA_NAMES = ["Forest of Silence", "Castle Wall", "Villa", "Tunnel", "Underground Waterway", "Castle Center",
"Duel Tower", "Tower of Execution", "Tower of Science", "Tower of Sorcery", "Room of Clocks",
"Clock Tower", "Castle Keep", "Level: You Cheated"]
class Castlevania64Client(BizHawkClient):
game = "Castlevania 64"
system = "N64"
patch_suffix = ".apcv64"
self_induced_death = False
time_of_sent_death = None
received_deathlinks = 0
death_causes = []
currently_shopping = False
@@ -62,15 +70,19 @@ class Castlevania64Client(BizHawkClient):
return
if "tags" not in args:
return
if "DeathLink" in args["tags"] and args["data"]["source"] != ctx.slot_info[ctx.slot].name:
if "DeathLink" in args["tags"] and args["data"]["time"] != self.time_of_sent_death:
self.received_deathlinks += 1
if "cause" in args["data"]:
cause = args["data"]["cause"]
# If the other game sent a death with a blank string for the cause, use the default death message.
if cause == "":
cause = f"{args['data']['source']} killed you without a word!"
# Truncate the death cause message at 120 characters.
if len(cause) > 120:
cause = cause[0:120]
else:
cause = f"{args['data']['source']} killed you!"
# If the other game sent a death with no cause at all, use the default death message.
cause = f"{args['data']['source']} killed you without a word!"
self.death_causes.append(cause)
async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
@@ -115,11 +127,30 @@ class Castlevania64Client(BizHawkClient):
if "DeathLink" in ctx.tags and save_struct[0xA4] & 0x80 and not self.self_induced_death and not \
deathlink_induced_death:
self.self_induced_death = True
if save_struct[0xA4] & 0x08:
# Special death message for dying while having the Vamp status.
await ctx.send_death(f"{ctx.player_names[ctx.slot]} became a vampire and drank your blood!")
# If the player died at the Castle Keep exterior map on one of the Room of Clocks boss towers
# (determinable by checking the entrance value as well as the map value), consider Room of Clocks the
# actual area of death.
if save_struct[0xAD] == 0x14 and save_struct[0xAF] in [0, 1]:
area_of_death = DEATHLINK_AREA_NAMES[10]
# Otherwise, determine what area the player perished in from the current map ID.
else:
await ctx.send_death(f"{ctx.player_names[ctx.slot]} perished. Dracula has won!")
area_of_death = DEATHLINK_AREA_NAMES[DEATHLINK_AREA_NUMBERS[save_struct[0xAD]]]
# If we had the Vamp status while dying, use a special message.
if save_struct[0xA4] & 0x08:
death_message = (f"{ctx.player_names[ctx.slot]} became a vampire at {area_of_death} and drank your "
f"blood!")
# Otherwise, use the generic one.
else:
death_message = f"{ctx.player_names[ctx.slot]} perished in {area_of_death}. Dracula has won!"
# Send the death.
await ctx.send_death(death_message)
# Record the time in which the death was sent so when we receive the packet we can tell it wasn't our
# own death. ctx.on_deathlink overwrites it later, so it MUST be grabbed now.
self.time_of_sent_death = ctx.last_death_link
# Write any DeathLinks received along with the corresponding death cause starting with the oldest.
# To minimize Bizhawk Write jank, the DeathLink write will be prioritized over the item received one.
@@ -208,6 +239,7 @@ class Castlevania64Client(BizHawkClient):
# Send game clear if we're in either any ending cutscene or the credits state.
if not ctx.finished_game and (0x26 <= int(cutscene_value) <= 0x2E or game_state == 0x0000000B):
ctx.finished_game = True
await ctx.send_msgs([{
"cmd": "StatusUpdate",
"status": ClientStatus.CLIENT_GOAL

View File

@@ -10,7 +10,7 @@
Kingdom Hearts II Final Mix from the [Epic Games Store](https://store.epicgames.com/en-US/discover/kingdom-hearts) or [Steam](https://store.steampowered.com/app/2552430/KINGDOM_HEARTS_HD_1525_ReMIX/)
- Follow this Guide to set up these requirements [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/)
1. Version 25.01.26.0 or greater OpenKH Mod Manager with Panacea
1. Version 25.03.16.0 or greater OpenKH Mod Manager with Panacea
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
@@ -27,7 +27,7 @@ Kingdom Hearts II Final Mix from the [Epic Games Store](https://store.epicgames.
Load this mod just like the <b>GoA ROM</b> you did during the KH2 Rando setup. `JaredWeakStrike/APCompanion`<br>
Have this mod second-highest priority below the .zip seed.<br>
This mod is based upon Num's Garden of Assemblege Mod and requires it to work. Without Num this could not be possible.
This mod is based upon Num's Garden of Assemblage Mod and requires it to work. Without Num this could not be possible.
<h3 style="text-transform:none";>Required: Auto Save Mod and KH2 Lua Library</h3>
@@ -35,7 +35,7 @@ Load these mods just like you loaded the GoA ROM mod during the KH2 Rando setup.
<h3 style="text-transform:none";>Optional QoL Mods: AP QoL and Bear Skip</h3>
`JaredWeakStrike/AP_QOL` Makes the urns minigames much faster, makes Cavern of Remembrance orbs drop significantly more drive orbs for refilling drive/leveling master form, skips the animation when using the bulky vendor RC, skips carpet escape auto scroller in Agrabah 2, and prevents the wardrobe in the Beasts Castle wardrobe push minigame from waking up while being pushed.
`JaredWeakStrike/AP_QOL` Makes the urns minigames much faster, makes Cavern of Remembrance orbs drop significantly more drive orbs for refilling drive/leveling master form, skips the animation when using the bulky vendor RC, skips carpet escape auto-scroller in Agrabah 2, and prevents the wardrobe in the Beasts Castle wardrobe push minigame from waking up while being pushed.
`shananas/BearSkip` Skips all minigames in 100 Acre Woods except the Spooky Cave minigame since there are chests in Spooky Cave you can only get during the minigame. For Spooky Cave, Pooh is moved to the other side of the invisible wall that prevents you from using his RC to finish the minigame.
@@ -83,6 +83,9 @@ Enter The room's port number into the top box <b> where the x's are</b> and pres
- Loading into Simulated Twilight Town Instead of the GOA.
- To fix this look over the guide at [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/). Specifically the Panacea and Lua Backend Steps.
- Using a seed from the standalone KH2 Randomizer Seed Generator.
- The Archipelago version of the KH2 Randomizer does not use this Seed Generator; refer to the [Archipelago Setup](https://archipelago.gg/tutorial/Archipelago/setup/en) to learn how to generate and play a seed through Archipelago.
<h2 style="text-transform:none"; >Best Practices</h2>
- Make a save at the start of the GoA before opening anything. This will be the file to select when loading an autosave if/when your game crashes.
@@ -139,4 +142,4 @@ This pack will handle logic, received items, checked locations and autotabbing f
- Why should I install the auto save mod at `KH2FM-Mods-equations19/auto-save` and `KH2FM-Mods-equations19/KH2-Lua-Library`?
- Because Kingdom Hearts 2 is prone to crashes and will keep you from losing your progress. Both mods are needed for auto save to work.
- How do I load an auto save?
- To load an auto-save, hold down the Select or your equivalent on your prefered controller while choosing a file. Make sure to hold the button down the whole time.
- To load an auto-save, hold down the Select or your equivalent on your preferred controller while choosing a file. Make sure to hold the button down the whole time.

View File

@@ -3,7 +3,7 @@ Archipelago init file for Lingo
"""
from logging import warning
from BaseClasses import CollectionState, Item, ItemClassification, Tutorial
from BaseClasses import CollectionState, Item, ItemClassification, Tutorial, Location, LocationProgressType
from Options import OptionError
from worlds.AutoWorld import WebWorld, World
from .datatypes import Room, RoomEntrance
@@ -80,10 +80,6 @@ class LingoWorld(World):
for item in self.player_logic.real_items:
state.collect(self.create_item(item), True)
# Exception to the above: a forced good item is not considered a "real item", but needs to be here anyway.
if self.player_logic.forced_good_item != "":
state.collect(self.create_item(self.player_logic.forced_good_item), True)
all_locations = self.multiworld.get_locations(self.player)
state.sweep_for_advancements(locations=all_locations)
@@ -105,11 +101,6 @@ class LingoWorld(World):
def create_items(self):
pool = [self.create_item(name) for name in self.player_logic.real_items]
if self.player_logic.forced_good_item != "":
new_item = self.create_item(self.player_logic.forced_good_item)
location_obj = self.multiworld.get_location("Second Room - Good Luck", self.player)
location_obj.place_locked_item(new_item)
item_difference = len(self.player_logic.real_locations) - len(pool)
if item_difference:
trap_percentage = self.options.trap_percentage
@@ -138,7 +129,7 @@ class LingoWorld(World):
trap_counts = {name: int(weight * traps / total_weight)
for name, weight in self.options.trap_weights.items()}
trap_difference = traps - sum(trap_counts.values())
if trap_difference > 0:
allowed_traps = [name for name in TRAP_ITEMS if self.options.trap_weights[name] > 0]
@@ -169,6 +160,30 @@ class LingoWorld(World):
def set_rules(self):
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
def place_good_item(self, progitempool: list[Item], fill_locations: list[Location]):
if len(self.player_logic.good_item_options) == 0:
return
good_location = self.get_location("Second Room - Good Luck")
if good_location.progress_type == LocationProgressType.EXCLUDED or good_location not in fill_locations:
return
good_items = list(filter(lambda progitem: progitem.player == self.player and
progitem.name in self.player_logic.good_item_options, progitempool))
if len(good_items) == 0:
return
good_item = self.random.choice(good_items)
good_location.place_locked_item(good_item)
progitempool.remove(good_item)
fill_locations.remove(good_location)
def fill_hook(self, progitempool: list[Item], usefulitempool: list[Item], filleritempool: list[Item],
fill_locations: list[Location]):
self.place_good_item(progitempool, fill_locations)
def fill_slot_data(self):
slot_options = [
"death_link", "victory_condition", "shuffle_colors", "shuffle_doors", "shuffle_paintings", "shuffle_panels",

View File

@@ -2,7 +2,7 @@ from dataclasses import dataclass
from schema import And, Schema
from Options import Toggle, Choice, DefaultOnToggle, Range, PerGameCommonOptions, StartInventoryPool, OptionDict, \
from Options import Toggle, Choice, DefaultOnToggle, Range, PerGameCommonOptions, StartInventoryPool, OptionCounter, \
OptionGroup
from .items import TRAP_ITEMS
@@ -222,13 +222,14 @@ class TrapPercentage(Range):
default = 20
class TrapWeights(OptionDict):
class TrapWeights(OptionCounter):
"""Specify the distribution of traps that should be placed into the pool.
If you don't want a specific type of trap, set the weight to zero.
"""
display_name = "Trap Weights"
schema = Schema({trap_name: And(int, lambda n: n >= 0) for trap_name in TRAP_ITEMS})
valid_keys = TRAP_ITEMS
min = 0
default = {trap_name: 1 for trap_name in TRAP_ITEMS}

View File

@@ -95,7 +95,7 @@ class LingoPlayerLogic:
painting_mapping: Dict[str, str]
forced_good_item: str
good_item_options: List[str]
panel_reqs: Dict[str, Dict[str, AccessRequirements]]
door_reqs: Dict[str, Dict[str, AccessRequirements]]
@@ -151,7 +151,7 @@ class LingoPlayerLogic:
self.mastery_location = ""
self.level_2_location = ""
self.painting_mapping = {}
self.forced_good_item = ""
self.good_item_options = []
self.panel_reqs = {}
self.door_reqs = {}
self.mastery_reqs = []
@@ -344,23 +344,23 @@ class LingoPlayerLogic:
# Starting Room - Back Right Door gives access to OPEN and DEAD END.
# Starting Room - Exit Door gives access to OPEN and TRACE.
good_item_options: List[str] = ["Starting Room - Back Right Door", "Second Room - Exit Door"]
self.good_item_options = ["Starting Room - Back Right Door", "Second Room - Exit Door"]
if not color_shuffle:
if not world.options.enable_pilgrimage:
# HOT CRUST and THIS.
good_item_options.append("Pilgrim Room - Sun Painting")
self.good_item_options.append("Pilgrim Room - Sun Painting")
if world.options.group_doors:
# WELCOME BACK, CLOCKWISE, and DRAWL + RUNS.
good_item_options.append("Welcome Back Doors")
self.good_item_options.append("Welcome Back Doors")
else:
# WELCOME BACK and CLOCKWISE.
good_item_options.append("Welcome Back Area - Shortcut to Starting Room")
self.good_item_options.append("Welcome Back Area - Shortcut to Starting Room")
if world.options.group_doors:
# Color hallways access (NOTE: reconsider when sunwarp shuffling exists).
good_item_options.append("Rhyme Room Doors")
self.good_item_options.append("Rhyme Room Doors")
# When painting shuffle is off, most Starting Room paintings give color hallways access. The Wondrous's
# painting does not, but it gives access to SHRINK and WELCOME BACK.
@@ -376,30 +376,7 @@ class LingoPlayerLogic:
continue
pdoor = DOORS_BY_ROOM[painting_obj.required_door.room][painting_obj.required_door.door]
good_item_options.append(pdoor.item_name)
# Copied from The Witness -- remove any plandoed items from the possible good items set.
for v in world.multiworld.plando_items[world.player]:
if v.get("from_pool", True):
for item_key in {"item", "items"}:
if item_key in v:
if type(v[item_key]) is str:
if v[item_key] in good_item_options:
good_item_options.remove(v[item_key])
elif type(v[item_key]) is dict:
for item, weight in v[item_key].items():
if weight and item in good_item_options:
good_item_options.remove(item)
else:
# Other type of iterable
for item in v[item_key]:
if item in good_item_options:
good_item_options.remove(item)
if len(good_item_options) > 0:
self.forced_good_item = world.random.choice(good_item_options)
self.real_items.remove(self.forced_good_item)
self.real_locations.remove("Second Room - Good Luck")
self.good_item_options.append(pdoor.item_name)
def randomize_paintings(self, world: "LingoWorld") -> bool:
self.painting_mapping.clear()

View File

@@ -215,7 +215,7 @@ def set_rules(world: "MM2World") -> None:
continue
highest, wp = max(zip(weapon_weight.values(), weapon_weight.keys()))
uses = weapon_energy[wp] // weapon_costs[wp]
if int(uses * boss_damage[wp]) > boss_health[boss]:
if int(uses * boss_damage[wp]) >= boss_health[boss]:
used = ceil(boss_health[boss] / boss_damage[wp])
weapon_energy[wp] -= weapon_costs[wp] * used
boss_health[boss] = 0
@@ -226,7 +226,7 @@ def set_rules(world: "MM2World") -> None:
# it should be impossible to be out of energy, simply because even if every boss took 1 from
# Quick Boomerang and no other, it would only be 28 off from defeating all 9,
# which Metal Blade should be able to cover
wp, max_uses = max((weapon, weapon_energy[weapon] // weapon_costs[weapon])
max_uses, wp = max((weapon_energy[weapon] // weapon_costs[weapon], weapon)
for weapon in weapon_weight
if weapon != 0 and (weapon != 8 or boss != 12))
# Wily Machine cannot under any circumstances take damage from Time Stopper, prevent this

View File

@@ -2,9 +2,9 @@ from math import ceil
from . import MM2TestBase
from ..options import bosses
from ..rules import minimum_weakness_requirement
# Need to figure out how this test should work
def validate_wily_5(base: MM2TestBase) -> None:
world = base.multiworld.worlds[base.player]
weapon_damage = world.weapon_damage
@@ -67,38 +67,54 @@ def validate_wily_5(base: MM2TestBase) -> None:
weapon_weight.pop(wp)
class StrictWeaknessTests(MM2TestBase):
class WeaknessTests(MM2TestBase):
options = {
"strict_weakness": True,
"yoku_jumps": True,
"enable_lasers": True
"enable_lasers": True,
}
def test_that_every_boss_has_a_weakness(self) -> None:
world = self.multiworld.worlds[self.player]
weapon_damage = world.weapon_damage
for boss in range(14):
if not any(weapon_damage[weapon][boss] for weapon in range(9)):
if not any(weapon_damage[weapon][boss] >= minimum_weakness_requirement[weapon] for weapon in range(9)):
self.fail(f"Boss {boss} generated without weakness! Seed: {self.multiworld.seed}")
def test_wily_5(self) -> None:
validate_wily_5(self)
class RandomStrictWeaknessTests(MM2TestBase):
class StrictWeaknessTests(WeaknessTests):
options = {
"strict_weakness": True,
**WeaknessTests.options
}
class RandomWeaknessTests(WeaknessTests):
options = {
"random_weakness": "randomized",
**WeaknessTests.options
}
class ShuffledWeaknessTests(WeaknessTests):
options = {
"random_weakness": "shuffled",
**WeaknessTests.options
}
class RandomStrictWeaknessTests(WeaknessTests):
options = {
"strict_weakness": True,
"random_weakness": "randomized",
"yoku_jumps": True,
"enable_lasers": True
**WeaknessTests.options
}
def test_that_every_boss_has_a_weakness(self) -> None:
world = self.multiworld.worlds[self.player]
weapon_damage = world.weapon_damage
for boss in range(14):
if not any(weapon_damage[weapon][boss] for weapon in range(9)):
self.fail(f"Boss {boss} generated without weakness! Seed: {self.multiworld.seed}")
def test_wily_5(self) -> None:
validate_wily_5(self)
class ShuffledStrictWeaknessTests(WeaknessTests):
options = {
"strict_weakness": True,
"random_weakness": "shuffled",
**WeaknessTests.options
}

View File

@@ -2640,9 +2640,13 @@ class PokemonRBWarp(Entrance):
self.warp_id = warp_id
self.address = address
self.flags = flags
self.addresses = None
self.target = None
def connect(self, entrance):
super().connect(entrance.parent_region, None, target=entrance.warp_id)
super().connect(entrance.parent_region)
self.addresses = None
self.target = entrance.warp_id
def access_rule(self, state):
if self.connected_region is None:

View File

@@ -1,7 +1,7 @@
import logging
import typing
from random import Random
from typing import Dict, Any, Iterable, Optional, List, TextIO, cast
from typing import Dict, Any, Iterable, Optional, List, TextIO
from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState
from Options import PerGameCommonOptions
@@ -148,8 +148,8 @@ class StardewValleyWorld(World):
self.precollect_building_items()
items_to_exclude = [excluded_items
for excluded_items in self.multiworld.precollected_items[self.player]
if not item_table[excluded_items.name].has_any_group(Group.RESOURCE_PACK,
Group.FRIENDSHIP_PACK)]
if item_table[excluded_items.name].has_any_group(Group.MAXIMUM_ONE)
or not item_table[excluded_items.name].has_any_group(Group.RESOURCE_PACK, Group.FRIENDSHIP_PACK)]
if self.options.season_randomization == SeasonRandomization.option_disabled:
items_to_exclude = [item for item in items_to_exclude
@@ -206,7 +206,8 @@ class StardewValleyWorld(World):
if not building_progression.is_progressive:
return
for building in building_progression.starting_buildings:
# starting_buildings is a set, so sort for deterministic order.
for building in sorted(building_progression.starting_buildings):
item, quantity = building_progression.to_progressive_item(building)
for _ in range(quantity):
self.multiworld.push_precollected(self.create_item(item))

View File

@@ -1,9 +1,10 @@
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Dict, Iterable, Set, Any, Mapping, Type, Tuple, Union
from typing import Iterable, Set, Any, Mapping, Type, Tuple, Union
from .feature import booksanity, cropsanity, fishsanity, friendsanity, skill_progression, building_progression, tool_progression
from ..data.animal import Animal
from ..data.building import Building
from ..data.fish_data import FishItem
from ..data.game_item import GameItem, Source, ItemTag
@@ -18,12 +19,13 @@ class StardewContent:
# regions -> To be used with can reach rule
game_items: Dict[str, GameItem] = field(default_factory=dict)
fishes: Dict[str, FishItem] = field(default_factory=dict)
villagers: Dict[str, Villager] = field(default_factory=dict)
farm_buildings: Dict[str, Building] = field(default_factory=dict)
skills: Dict[str, Skill] = field(default_factory=dict)
quests: Dict[str, Any] = field(default_factory=dict)
game_items: dict[str, GameItem] = field(default_factory=dict)
fishes: dict[str, FishItem] = field(default_factory=dict)
villagers: dict[str, Villager] = field(default_factory=dict)
farm_buildings: dict[str, Building] = field(default_factory=dict)
animals: dict[str, Animal] = field(default_factory=dict)
skills: dict[str, Skill] = field(default_factory=dict)
quests: dict[str, Any] = field(default_factory=dict)
def find_sources_of_type(self, types: Union[Type[Source], Tuple[Type[Source]]]) -> Iterable[Source]:
for item in self.game_items.values():
@@ -109,6 +111,11 @@ class ContentPack:
def farm_building_hook(self, content: StardewContent):
...
animals: Iterable[Animal] = ()
def animal_hook(self, content: StardewContent):
...
skills: Iterable[Skill] = ()
def skill_hook(self, content: StardewContent):

View File

@@ -65,6 +65,10 @@ def register_pack(content: StardewContent, pack: ContentPack):
content.farm_buildings[building.name] = building
pack.farm_building_hook(content)
for animal in pack.animals:
content.animals[animal.name] = animal
pack.animal_hook(content)
for skill in pack.skills:
content.skills[skill.name] = skill
pack.skill_hook(content)

View File

@@ -1,15 +1,19 @@
from .pelican_town import pelican_town as pelican_town_content_pack
from ..game_content import ContentPack, StardewContent
from ...data import villagers_data, fish_data
from ...data.game_item import ItemTag, Tag
from ...data.animal import Animal, AnimalName, OstrichIncubatorSource
from ...data.game_item import ItemTag, Tag, CustomRuleSource
from ...data.harvest import ForagingSource, HarvestFruitTreeSource, HarvestCropSource
from ...data.requirement import WalnutRequirement
from ...data.shop import ShopSource
from ...strings.animal_product_names import AnimalProduct
from ...strings.book_names import Book
from ...strings.building_names import Building
from ...strings.crop_names import Fruit, Vegetable
from ...strings.fish_names import Fish
from ...strings.forageable_names import Forageable, Mushroom
from ...strings.fruit_tree_names import Sapling
from ...strings.generic_names import Generic
from ...strings.metal_names import Fossil, Mineral
from ...strings.region_names import Region, LogicRegion
from ...strings.season_names import Season
@@ -51,6 +55,13 @@ ginger_island_content_pack = GingerIslandContentPack(
Vegetable.taro_root: (HarvestCropSource(seed=Seed.taro, seasons=(Season.summer,)),),
Fruit.pineapple: (HarvestCropSource(seed=Seed.pineapple, seasons=(Season.summer,)),),
# Temporary animal stuff, will be moved once animal products are properly content-packed
AnimalProduct.ostrich_egg_starter: (CustomRuleSource(lambda logic: logic.tool.can_forage(Generic.any, Region.island_north, True)
& logic.has(Forageable.journal_scrap)
& logic.region.can_reach(Region.volcano_floor_5)),),
AnimalProduct.ostrich_egg: (CustomRuleSource(lambda logic: logic.has(AnimalProduct.ostrich_egg_starter)
| logic.animal.has_animal(AnimalName.ostrich)),),
},
shop_sources={
Seed.taro: (ShopSource(items_price=((2, Fossil.bone_fragment),), shop_region=Region.island_trader),),
@@ -81,5 +92,12 @@ ginger_island_content_pack = GingerIslandContentPack(
),
villagers=(
villagers_data.leo,
),
animals=(
Animal(AnimalName.ostrich,
required_building=Building.barn,
sources=(
OstrichIncubatorSource(AnimalProduct.ostrich_egg_starter),
)),
)
)

View File

@@ -1,7 +1,12 @@
from .pelican_town import pelican_town as pelican_town_content_pack
from ..game_content import ContentPack
from ...data.animal import IncubatorSource, Animal, AnimalName
from ...data.harvest import FruitBatsSource, MushroomCaveSource
from ...data.shop import ShopSource
from ...strings.animal_product_names import AnimalProduct
from ...strings.building_names import Building
from ...strings.forageable_names import Forageable, Mushroom
from ...strings.region_names import Region
the_farm = ContentPack(
"The Farm (Vanilla)",
@@ -39,5 +44,64 @@ the_farm = ContentPack(
Mushroom.red: (
MushroomCaveSource(),
),
}
},
animals=(
Animal(AnimalName.chicken,
required_building=Building.coop,
sources=(
ShopSource(shop_region=Region.ranch, money_price=800),
# For now there is no way to obtain the starter item, so this adds additional rules in the system for nothing.
# IncubatorSource(AnimalProduct.egg_starter)
)),
Animal(AnimalName.cow,
required_building=Building.barn,
sources=(
ShopSource(shop_region=Region.ranch, money_price=1500),
)),
Animal(AnimalName.goat,
required_building=Building.big_barn,
sources=(
ShopSource(shop_region=Region.ranch, money_price=4000),
)),
Animal(AnimalName.duck,
required_building=Building.big_coop,
sources=(
ShopSource(shop_region=Region.ranch, money_price=1200),
# For now there is no way to obtain the starter item, so this adds additional rules in the system for nothing.
# IncubatorSource(AnimalProduct.duck_egg_starter)
)),
Animal(AnimalName.sheep,
required_building=Building.deluxe_barn,
sources=(
ShopSource(shop_region=Region.ranch, money_price=8000),
)),
Animal(AnimalName.rabbit,
required_building=Building.deluxe_coop,
sources=(
ShopSource(shop_region=Region.ranch, money_price=8000),
)),
Animal(AnimalName.pig,
required_building=Building.deluxe_barn,
sources=(
ShopSource(shop_region=Region.ranch, money_price=16000),
)),
Animal(AnimalName.void_chicken,
required_building=Building.big_coop,
sources=(
IncubatorSource(AnimalProduct.void_egg_starter),
)),
Animal(AnimalName.golden_chicken,
required_building=Building.big_coop,
sources=(
IncubatorSource(AnimalProduct.golden_egg_starter),
)),
Animal(AnimalName.dinosaur,
required_building=Building.big_coop,
sources=(
# We should use the starter item here, but since the dinosaur egg is also an artifact, it's part of the museum rules
# and I do not want to touch it yet.
# IncubatorSource(AnimalProduct.dinosaur_egg_starter),
IncubatorSource(AnimalProduct.dinosaur_egg),
)),
)
)

View File

@@ -0,0 +1,23 @@
from dataclasses import dataclass, field
from .game_item import Source
from ..strings.animal_names import Animal as AnimalName
assert AnimalName
@dataclass(frozen=True)
class Animal:
name: str
required_building: str = field(kw_only=True)
sources: tuple[Source, ...] = field(kw_only=True)
@dataclass(frozen=True)
class IncubatorSource(Source):
egg_item: str
@dataclass(frozen=True)
class OstrichIncubatorSource(Source):
egg_item: str

View File

@@ -143,7 +143,7 @@ duck_egg = BundleItem(AnimalProduct.duck_egg)
rabbit_foot = BundleItem(AnimalProduct.rabbit_foot)
dinosaur_egg = BundleItem(AnimalProduct.dinosaur_egg)
void_egg = BundleItem(AnimalProduct.void_egg)
ostrich_egg = BundleItem(AnimalProduct.ostrich_egg, source=BundleItem.Sources.island, )
ostrich_egg = BundleItem(AnimalProduct.ostrich_egg, source=BundleItem.Sources.content)
golden_egg = BundleItem(AnimalProduct.golden_egg)
truffle_oil = BundleItem(ArtisanGood.truffle_oil)
@@ -832,7 +832,7 @@ calico_items = [calico_egg.as_amount(200), calico_egg.as_amount(200), calico_egg
magic_rock_candy, mega_bomb.as_amount(10), mystery_box.as_amount(10), mixed_seeds.as_amount(50),
strawberry_seeds.as_amount(20),
spicy_eel.as_amount(5), crab_cakes.as_amount(5), eggplant_parmesan.as_amount(5),
pumpkin_soup.as_amount(5), lucky_lunch.as_amount(5)]
pumpkin_soup.as_amount(5), lucky_lunch.as_amount(5) ]
calico_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.calico, calico_items, 2, 2)
raccoon_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.raccoon, raccoon_foraging_items, 4, 4)

View File

@@ -748,7 +748,7 @@ id,name,classification,groups,mod_name
5208,Chest,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5209,Stone Chest,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5210,Quality Bobber,filler,RESOURCE_PACK,
5211,Mini-Obelisk,filler,"EXACTLY_TWO,RESOURCE_PACK",
5211,Mini-Obelisk,filler,"AT_LEAST_TWO,RESOURCE_PACK",
5212,Monster Musk,filler,RESOURCE_PACK,
5213,Sprinkler,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5214,Quality Sprinkler,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
@@ -760,7 +760,7 @@ id,name,classification,groups,mod_name
5220,Lightning Rod,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5221,Resource Pack: 5000 Money,useful,"BASE_RESOURCE,RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5222,Resource Pack: 10000 Money,useful,"BASE_RESOURCE,RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5223,Junimo Chest,filler,"EXACTLY_TWO,RESOURCE_PACK",
5223,Junimo Chest,filler,"AT_LEAST_TWO,RESOURCE_PACK",
5224,Horse Flute,useful,"MAXIMUM_ONE,RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5225,Pierre's Missing Stocklist,useful,"MAXIMUM_ONE,RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5226,Hopper,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
1 id name classification groups mod_name
748 5208 Chest filler RESOURCE_PACK,RESOURCE_PACK_USEFUL
749 5209 Stone Chest filler RESOURCE_PACK,RESOURCE_PACK_USEFUL
750 5210 Quality Bobber filler RESOURCE_PACK
751 5211 Mini-Obelisk filler EXACTLY_TWO,RESOURCE_PACK AT_LEAST_TWO,RESOURCE_PACK
752 5212 Monster Musk filler RESOURCE_PACK
753 5213 Sprinkler filler RESOURCE_PACK,RESOURCE_PACK_USEFUL
754 5214 Quality Sprinkler filler RESOURCE_PACK,RESOURCE_PACK_USEFUL
760 5220 Lightning Rod filler RESOURCE_PACK,RESOURCE_PACK_USEFUL
761 5221 Resource Pack: 5000 Money useful BASE_RESOURCE,RESOURCE_PACK,RESOURCE_PACK_USEFUL
762 5222 Resource Pack: 10000 Money useful BASE_RESOURCE,RESOURCE_PACK,RESOURCE_PACK_USEFUL
763 5223 Junimo Chest filler EXACTLY_TWO,RESOURCE_PACK AT_LEAST_TWO,RESOURCE_PACK
764 5224 Horse Flute useful MAXIMUM_ONE,RESOURCE_PACK,RESOURCE_PACK_USEFUL
765 5225 Pierre's Missing Stocklist useful MAXIMUM_ONE,RESOURCE_PACK,RESOURCE_PACK_USEFUL
766 5226 Hopper filler RESOURCE_PACK,RESOURCE_PACK_USEFUL

View File

@@ -3,12 +3,13 @@ from __future__ import annotations
from dataclasses import dataclass
from typing import List, Tuple, Union, Optional
from ..strings.monster_names import Monster
from ..strings.animal_product_names import AnimalProduct
from ..strings.fish_names import WaterChest
from ..strings.forageable_names import Forageable
from ..strings.metal_names import Mineral, Artifact, Fossil
from ..strings.region_names import Region
from ..strings.geode_names import Geode
from ..strings.metal_names import Mineral, Artifact, Fossil
from ..strings.monster_names import Monster
from ..strings.region_names import Region
@dataclass(frozen=True)
@@ -105,7 +106,7 @@ class Artifact:
geodes=(Geode.artifact_trove, WaterChest.fishing_chest))
ornamental_fan = create_artifact("Ornamental Fan", 7.4, (Region.beach, Region.forest, Region.town),
geodes=(Geode.artifact_trove, WaterChest.fishing_chest))
dinosaur_egg = create_artifact("Dinosaur Egg", 11.4, (Region.skull_cavern),
dinosaur_egg = create_artifact(AnimalProduct.dinosaur_egg, 11.4, (Region.skull_cavern),
monsters=Monster.pepper_rex)
rare_disc = create_artifact("Rare Disc", 5.6, Region.stardew_valley,
geodes=(Geode.artifact_trove, WaterChest.fishing_chest),

View File

@@ -69,7 +69,7 @@ class Group(enum.Enum):
TRAP = enum.auto()
BONUS = enum.auto()
MAXIMUM_ONE = enum.auto()
EXACTLY_TWO = enum.auto()
AT_LEAST_TWO = enum.auto()
DEPRECATED = enum.auto()
RESOURCE_PACK_USEFUL = enum.auto()
SPECIAL_ORDER_BOARD = enum.auto()
@@ -181,7 +181,7 @@ def create_items(item_factory: StardewItemFactory, locations_count: int, items_t
items += unique_filler_items
logger.debug(f"Created {len(unique_filler_items)} unique filler items")
resource_pack_items = fill_with_resource_packs_and_traps(item_factory, options, random, items, locations_count)
resource_pack_items = fill_with_resource_packs_and_traps(item_factory, options, random, items + items_to_exclude, locations_count - len(items))
items += resource_pack_items
logger.debug(f"Created {len(resource_pack_items)} resource packs")
@@ -711,7 +711,7 @@ def weapons_count(options: StardewValleyOptions):
def fill_with_resource_packs_and_traps(item_factory: StardewItemFactory, options: StardewValleyOptions, random: Random,
items_already_added: List[Item],
number_locations: int) -> List[Item]:
available_item_slots: int) -> List[Item]:
include_traps = options.trap_items != TrapItems.option_no_traps
items_already_added_names = [item.name for item in items_already_added]
useful_resource_packs = [pack for pack in items_by_group[Group.RESOURCE_PACK_USEFUL]
@@ -734,10 +734,9 @@ def fill_with_resource_packs_and_traps(item_factory: StardewItemFactory, options
priority_filler_items = remove_excluded_items(priority_filler_items, options)
number_priority_items = len(priority_filler_items)
required_resource_pack = number_locations - len(items_already_added)
if required_resource_pack < number_priority_items:
if available_item_slots < number_priority_items:
chosen_priority_items = [item_factory(resource_pack) for resource_pack in
random.sample(priority_filler_items, required_resource_pack)]
random.sample(priority_filler_items, available_item_slots)]
return chosen_priority_items
items = []
@@ -745,24 +744,24 @@ def fill_with_resource_packs_and_traps(item_factory: StardewItemFactory, options
ItemClassification.trap if resource_pack.classification == ItemClassification.trap else ItemClassification.useful)
for resource_pack in priority_filler_items]
items.extend(chosen_priority_items)
required_resource_pack -= number_priority_items
available_item_slots -= number_priority_items
all_filler_packs = [filler_pack for filler_pack in all_filler_packs
if Group.MAXIMUM_ONE not in filler_pack.groups or
(filler_pack.name not in [priority_item.name for priority_item in
priority_filler_items] and filler_pack.name not in items_already_added_names)]
while required_resource_pack > 0:
while available_item_slots > 0:
resource_pack = random.choice(all_filler_packs)
exactly_2 = Group.EXACTLY_TWO in resource_pack.groups
while exactly_2 and required_resource_pack == 1:
exactly_2 = Group.AT_LEAST_TWO in resource_pack.groups
while exactly_2 and available_item_slots == 1:
resource_pack = random.choice(all_filler_packs)
exactly_2 = Group.EXACTLY_TWO in resource_pack.groups
exactly_2 = Group.AT_LEAST_TWO in resource_pack.groups
classification = ItemClassification.useful if resource_pack.classification == ItemClassification.progression else resource_pack.classification
items.append(item_factory(resource_pack, classification))
required_resource_pack -= 1
available_item_slots -= 1
if exactly_2:
items.append(item_factory(resource_pack, classification))
required_resource_pack -= 1
available_item_slots -= 1
if exactly_2 or Group.MAXIMUM_ONE in resource_pack.groups:
all_filler_packs.remove(resource_pack)
@@ -803,7 +802,7 @@ def generate_filler_choice_pool(options: StardewValleyOptions) -> list[str]:
def remove_limited_amount_packs(packs):
return [pack for pack in packs if Group.MAXIMUM_ONE not in pack.groups and Group.EXACTLY_TWO not in pack.groups]
return [pack for pack in packs if Group.MAXIMUM_ONE not in pack.groups and Group.AT_LEAST_TWO not in pack.groups]
def get_all_filler_items(include_traps: bool, exclude_ginger_island: bool) -> List[ItemData]:

View File

@@ -1,23 +1,9 @@
import typing
from typing import Union
from .base_logic import BaseLogicMixin, BaseLogic
from .mine_logic import MineLogicMixin
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from .skill_logic import SkillLogicMixin
from .tool_logic import ToolLogicMixin
from ..mods.logic.magic_logic import MagicLogicMixin
from ..stardew_rule import StardewRule
from ..strings.region_names import Region
from ..strings.skill_names import Skill, ModSkill
from ..strings.tool_names import ToolMaterial, Tool
if typing.TYPE_CHECKING:
from ..mods.logic.mod_logic import ModLogicMixin
else:
ModLogicMixin = object
class AbilityLogicMixin(BaseLogicMixin):
def __init__(self, *args, **kwargs):
@@ -25,8 +11,7 @@ class AbilityLogicMixin(BaseLogicMixin):
self.ability = AbilityLogic(*args, **kwargs)
class AbilityLogic(BaseLogic[Union[AbilityLogicMixin, RegionLogicMixin, ReceivedLogicMixin, ToolLogicMixin, SkillLogicMixin, MineLogicMixin, MagicLogicMixin,
ModLogicMixin]]):
class AbilityLogic(BaseLogic):
def can_mine_perfectly(self) -> StardewRule:
return self.logic.mine.can_progress_in_the_mines_from_floor(160)

View File

@@ -1,11 +1,5 @@
from typing import Union
from Utils import cache_self1
from .base_logic import BaseLogic, BaseLogicMixin
from .has_logic import HasLogicMixin
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from .tool_logic import ToolLogicMixin
from ..stardew_rule import StardewRule, True_
from ..strings.generic_names import Generic
from ..strings.geode_names import Geode
@@ -19,7 +13,7 @@ class ActionLogicMixin(BaseLogicMixin):
self.action = ActionLogic(*args, **kwargs)
class ActionLogic(BaseLogic[Union[ActionLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin, ToolLogicMixin]]):
class ActionLogic(BaseLogic):
def can_watch(self, channel: str = None):
tv_rule = True_()

View File

@@ -1,25 +1,10 @@
from typing import Union
import typing
from .base_logic import BaseLogicMixin, BaseLogic
from .building_logic import BuildingLogicMixin
from .has_logic import HasLogicMixin
from .money_logic import MoneyLogicMixin
from ..stardew_rule import StardewRule, true_
from ..strings.animal_names import Animal, coop_animals, barn_animals
from ..stardew_rule import StardewRule
from ..strings.building_names import Building
from ..strings.forageable_names import Forageable
from ..strings.generic_names import Generic
from ..strings.region_names import Region
cost_and_building_by_animal = {
Animal.chicken: (800, Building.coop),
Animal.cow: (1500, Building.barn),
Animal.goat: (4000, Building.big_barn),
Animal.duck: (1200, Building.big_coop),
Animal.sheep: (8000, Building.deluxe_barn),
Animal.rabbit: (8000, Building.deluxe_coop),
Animal.pig: (16000, Building.deluxe_barn)
}
from ..strings.machine_names import Machine
class AnimalLogicMixin(BaseLogicMixin):
@@ -28,32 +13,19 @@ class AnimalLogicMixin(BaseLogicMixin):
self.animal = AnimalLogic(*args, **kwargs)
class AnimalLogic(BaseLogic[Union[HasLogicMixin, MoneyLogicMixin, BuildingLogicMixin]]):
class AnimalLogic(BaseLogic):
def can_buy_animal(self, animal: str) -> StardewRule:
try:
price, building = cost_and_building_by_animal[animal]
except KeyError:
return true_
return self.logic.money.can_spend_at(Region.ranch, price) & self.logic.building.has_building(building)
def can_incubate(self, egg_item: str) -> StardewRule:
return self.logic.building.has_building(Building.coop) & self.logic.has(egg_item)
def has_animal(self, animal: str) -> StardewRule:
if animal == Generic.any:
return self.has_any_animal()
elif animal == Building.coop:
return self.has_any_coop_animal()
elif animal == Building.barn:
return self.has_any_barn_animal()
return self.logic.has(animal)
def can_ostrich_incubate(self, egg_item: str) -> StardewRule:
return self.logic.building.has_building(Building.barn) & self.logic.has(Machine.ostrich_incubator) & self.logic.has(egg_item)
def has_happy_animal(self, animal: str) -> StardewRule:
return self.has_animal(animal) & self.logic.has(Forageable.hay)
def has_animal(self, animal_name: str) -> StardewRule:
animal = self.content.animals.get(animal_name)
assert animal is not None, f"Animal {animal_name} not found."
def has_any_animal(self) -> StardewRule:
return self.has_any_coop_animal() | self.has_any_barn_animal()
return self.logic.source.has_access_to_any(animal.sources) & self.logic.building.has_building(animal.required_building)
def has_any_coop_animal(self) -> StardewRule:
return self.logic.has_any(*coop_animals)
def has_any_barn_animal(self) -> StardewRule:
return self.logic.has_any(*barn_animals)
def has_happy_animal(self, animal_name: str) -> StardewRule:
return self.logic.animal.has_animal(animal_name) & self.logic.has(Forageable.hay)

View File

@@ -1,8 +1,4 @@
from typing import Union
from .base_logic import BaseLogic, BaseLogicMixin
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from .. import options
from ..stardew_rule import StardewRule, True_
from ..strings.region_names import Region
@@ -14,7 +10,7 @@ class ArcadeLogicMixin(BaseLogicMixin):
self.arcade = ArcadeLogic(*args, **kwargs)
class ArcadeLogic(BaseLogic[Union[ArcadeLogicMixin, RegionLogicMixin, ReceivedLogicMixin]]):
class ArcadeLogic(BaseLogic):
def has_jotpk_power_level(self, power_level: int) -> StardewRule:
if self.options.arcade_machine_locations != options.ArcadeMachineLocations.option_full_shuffling:

View File

@@ -1,8 +1,4 @@
from typing import Union
from .base_logic import BaseLogic, BaseLogicMixin
from .has_logic import HasLogicMixin
from .time_logic import TimeLogicMixin
from ..data.artisan import MachineSource
from ..data.game_item import ItemTag
from ..stardew_rule import StardewRule
@@ -20,7 +16,7 @@ class ArtisanLogicMixin(BaseLogicMixin):
self.artisan = ArtisanLogic(*args, **kwargs)
class ArtisanLogic(BaseLogic[Union[ArtisanLogicMixin, TimeLogicMixin, HasLogicMixin]]):
class ArtisanLogic(BaseLogic):
def initialize_rules(self):
# TODO remove this one too once fish are converted to sources
self.registry.artisan_good_rules.update({ArtisanGood.specific_smoked_fish(fish): self.can_smoke(fish) for fish in all_fish})

View File

@@ -1,11 +1,15 @@
from __future__ import annotations
from typing import TypeVar, Generic, Dict, Collection
import typing
from typing import Dict, Collection
from ..content.game_content import StardewContent
from ..options import StardewValleyOptions
from ..stardew_rule import StardewRule
if typing.TYPE_CHECKING:
from .logic import StardewLogic
class LogicRegistry:
@@ -30,18 +34,16 @@ class BaseLogicMixin:
pass
T = TypeVar("T", bound=BaseLogicMixin)
class BaseLogic(BaseLogicMixin, Generic[T]):
class BaseLogic(BaseLogicMixin):
player: int
registry: LogicRegistry
options: StardewValleyOptions
content: StardewContent
regions: Collection[str]
logic: T
logic: StardewLogic
def __init__(self, player: int, registry: LogicRegistry, options: StardewValleyOptions, content: StardewContent, regions: Collection[str], logic: T):
def __init__(self, player: int, registry: LogicRegistry, options: StardewValleyOptions, content: StardewContent, regions: Collection[str],
logic: StardewLogic):
super().__init__(player, registry, options, content, regions, logic)
self.player = player
self.registry = registry

View File

@@ -1,9 +1,5 @@
from typing import Union
from Utils import cache_self1
from .base_logic import BaseLogicMixin, BaseLogic
from .has_logic import HasLogicMixin
from .received_logic import ReceivedLogicMixin
from ..stardew_rule import StardewRule
@@ -13,7 +9,7 @@ class BookLogicMixin(BaseLogicMixin):
self.book = BookLogic(*args, **kwargs)
class BookLogic(BaseLogic[Union[ReceivedLogicMixin, HasLogicMixin]]):
class BookLogic(BaseLogic):
@cache_self1
def has_book_power(self, book: str) -> StardewRule:

View File

@@ -1,21 +1,11 @@
import typing
from functools import cached_property
from typing import Union
from Utils import cache_self1
from .base_logic import BaseLogic, BaseLogicMixin
from .has_logic import HasLogicMixin
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from ..stardew_rule import StardewRule, true_
from ..strings.building_names import Building
from ..strings.region_names import Region
if typing.TYPE_CHECKING:
from .source_logic import SourceLogicMixin
else:
SourceLogicMixin = object
AUTO_BUILDING_BUILDINGS = {Building.shipping_bin, Building.pet_bowl, Building.farm_house}
@@ -25,7 +15,7 @@ class BuildingLogicMixin(BaseLogicMixin):
self.building = BuildingLogic(*args, **kwargs)
class BuildingLogic(BaseLogic[Union[BuildingLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin, SourceLogicMixin]]):
class BuildingLogic(BaseLogic):
@cache_self1
def can_build(self, building_name: str) -> StardewRule:

View File

@@ -1,16 +1,7 @@
from functools import cached_property
from typing import Union, List
from typing import List
from .base_logic import BaseLogicMixin, BaseLogic
from .fishing_logic import FishingLogicMixin
from .has_logic import HasLogicMixin
from .money_logic import MoneyLogicMixin
from .quality_logic import QualityLogicMixin
from .quest_logic import QuestLogicMixin
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from .skill_logic import SkillLogicMixin
from .time_logic import TimeLogicMixin
from ..bundles.bundle import Bundle
from ..stardew_rule import StardewRule, True_
from ..strings.ap_names.community_upgrade_names import CommunityUpgrade
@@ -27,8 +18,7 @@ class BundleLogicMixin(BaseLogicMixin):
self.bundle = BundleLogic(*args, **kwargs)
class BundleLogic(BaseLogic[Union[ReceivedLogicMixin, HasLogicMixin, TimeLogicMixin, RegionLogicMixin, MoneyLogicMixin, QualityLogicMixin, FishingLogicMixin,
SkillLogicMixin, QuestLogicMixin]]):
class BundleLogic(BaseLogic):
# Should be cached
def can_complete_bundle(self, bundle: Bundle) -> StardewRule:
item_rules = []

View File

@@ -1,12 +1,7 @@
from functools import cached_property
from typing import Union
from Utils import cache_self1
from .base_logic import BaseLogicMixin, BaseLogic
from .has_logic import HasLogicMixin
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from ..mods.logic.magic_logic import MagicLogicMixin
from ..stardew_rule import StardewRule, False_
from ..strings.ap_names.ap_weapon_names import APWeapon
from ..strings.performance_names import Performance
@@ -20,7 +15,7 @@ class CombatLogicMixin(BaseLogicMixin):
self.combat = CombatLogic(*args, **kwargs)
class CombatLogic(BaseLogic[Union[HasLogicMixin, CombatLogicMixin, RegionLogicMixin, ReceivedLogicMixin, MagicLogicMixin]]):
class CombatLogic(BaseLogic):
@cache_self1
def can_fight_at_level(self, level: str) -> StardewRule:
if level == Performance.basic:

View File

@@ -1,17 +1,7 @@
from functools import cached_property
from typing import Union
from Utils import cache_self1
from .action_logic import ActionLogicMixin
from .base_logic import BaseLogicMixin, BaseLogic
from .building_logic import BuildingLogicMixin
from .has_logic import HasLogicMixin
from .money_logic import MoneyLogicMixin
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from .relationship_logic import RelationshipLogicMixin
from .season_logic import SeasonLogicMixin
from .skill_logic import SkillLogicMixin
from ..data.recipe_data import RecipeSource, StarterSource, ShopSource, SkillSource, FriendshipSource, \
QueenOfSauceSource, CookingRecipe, ShopFriendshipSource
from ..data.recipe_source import CutsceneSource, ShopTradeSource
@@ -29,8 +19,7 @@ class CookingLogicMixin(BaseLogicMixin):
self.cooking = CookingLogic(*args, **kwargs)
class CookingLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, MoneyLogicMixin, ActionLogicMixin,
BuildingLogicMixin, RelationshipLogicMixin, SkillLogicMixin, CookingLogicMixin]]):
class CookingLogic(BaseLogic):
@cached_property
def can_cook_in_kitchen(self) -> StardewRule:
return self.logic.building.has_building(Building.kitchen) | self.logic.skill.has_level(Skill.foraging, 9)

View File

@@ -1,15 +1,5 @@
from typing import Union
from Utils import cache_self1
from .base_logic import BaseLogicMixin, BaseLogic
from .has_logic import HasLogicMixin
from .money_logic import MoneyLogicMixin
from .quest_logic import QuestLogicMixin
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from .relationship_logic import RelationshipLogicMixin
from .skill_logic import SkillLogicMixin
from .special_order_logic import SpecialOrderLogicMixin
from .. import options
from ..data.craftable_data import CraftingRecipe
from ..data.recipe_source import CutsceneSource, ShopTradeSource, ArchipelagoSource, LogicSource, SpecialOrderSource, \
@@ -25,8 +15,7 @@ class CraftingLogicMixin(BaseLogicMixin):
self.crafting = CraftingLogic(*args, **kwargs)
class CraftingLogic(BaseLogic[Union[ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, MoneyLogicMixin, RelationshipLogicMixin,
SkillLogicMixin, SpecialOrderLogicMixin, CraftingLogicMixin, QuestLogicMixin]]):
class CraftingLogic(BaseLogic):
@cache_self1
def can_craft(self, recipe: CraftingRecipe = None) -> StardewRule:
if recipe is None:

View File

@@ -3,11 +3,6 @@ from typing import Union, Tuple
from Utils import cache_self1
from .base_logic import BaseLogicMixin, BaseLogic
from .has_logic import HasLogicMixin
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from .season_logic import SeasonLogicMixin
from .tool_logic import ToolLogicMixin
from .. import options
from ..stardew_rule import StardewRule, True_, false_
from ..strings.fertilizer_names import Fertilizer
@@ -29,7 +24,7 @@ class FarmingLogicMixin(BaseLogicMixin):
self.farming = FarmingLogic(*args, **kwargs)
class FarmingLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, ToolLogicMixin, FarmingLogicMixin]]):
class FarmingLogic(BaseLogic):
@cached_property
def has_farming_tools(self) -> StardewRule:
@@ -45,6 +40,8 @@ class FarmingLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, RegionLogi
if tier >= 3:
return self.logic.has(Fertilizer.deluxe)
return self.logic.false_
@cache_self1
def can_plant_and_grow_item(self, seasons: Union[str, Tuple[str]]) -> StardewRule:
if seasons == (): # indoor farming

View File

@@ -1,22 +1,7 @@
from typing import Union
from .action_logic import ActionLogicMixin
from .animal_logic import AnimalLogicMixin
from .artisan_logic import ArtisanLogicMixin
from .base_logic import BaseLogicMixin, BaseLogic
from .fishing_logic import FishingLogicMixin
from .gift_logic import GiftLogicMixin
from .has_logic import HasLogicMixin
from .money_logic import MoneyLogicMixin
from .monster_logic import MonsterLogicMixin
from .museum_logic import MuseumLogicMixin
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from .relationship_logic import RelationshipLogicMixin
from .skill_logic import SkillLogicMixin
from .time_logic import TimeLogicMixin
from ..options import FestivalLocations
from ..stardew_rule import StardewRule
from ..strings.animal_product_names import AnimalProduct
from ..strings.book_names import Book
from ..strings.craftable_names import Fishing
from ..strings.crop_names import Fruit, Vegetable
@@ -35,8 +20,7 @@ class FestivalLogicMixin(BaseLogicMixin):
self.festival = FestivalLogic(*args, **kwargs)
class FestivalLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, FestivalLogicMixin, ArtisanLogicMixin, AnimalLogicMixin, MoneyLogicMixin, TimeLogicMixin,
SkillLogicMixin, RegionLogicMixin, ActionLogicMixin, MonsterLogicMixin, RelationshipLogicMixin, FishingLogicMixin, MuseumLogicMixin, GiftLogicMixin]]):
class FestivalLogic(BaseLogic):
def initialize_rules(self):
self.registry.festival_rules.update({
@@ -154,18 +138,37 @@ SkillLogicMixin, RegionLogicMixin, ActionLogicMixin, MonsterLogicMixin, Relation
if self.options.festival_locations != FestivalLocations.option_hard:
return self.logic.true_
animal_rule = self.logic.animal.has_animal(Generic.any)
# Other animal products are not counted in the animal product category
good_animal_products = [
AnimalProduct.duck_egg, AnimalProduct.duck_feather, AnimalProduct.egg, AnimalProduct.goat_milk, AnimalProduct.golden_egg, AnimalProduct.large_egg,
AnimalProduct.large_goat_milk, AnimalProduct.large_milk, AnimalProduct.milk, AnimalProduct.ostrich_egg, AnimalProduct.rabbit_foot,
AnimalProduct.void_egg, AnimalProduct.wool
]
if AnimalProduct.ostrich_egg not in self.content.game_items:
# When ginger island is excluded, ostrich egg is not available
good_animal_products.remove(AnimalProduct.ostrich_egg)
animal_rule = self.logic.has_any(*good_animal_products)
artisan_rule = self.logic.artisan.can_keg(Generic.any) | self.logic.artisan.can_preserves_jar(Generic.any)
cooking_rule = self.logic.money.can_spend_at(Region.saloon, 220) # Salads at the bar are good enough
# Salads at the bar are good enough
cooking_rule = self.logic.money.can_spend_at(Region.saloon, 220)
fish_rule = self.logic.skill.can_fish(difficulty=50)
forage_rule = self.logic.region.can_reach_any((Region.forest, Region.backwoods)) # Hazelnut always available since the grange display is in fall
mineral_rule = self.logic.action.can_open_geode(Generic.any) # More than half the minerals are good enough
# Hazelnut always available since the grange display is in fall
forage_rule = self.logic.region.can_reach_any((Region.forest, Region.backwoods))
# More than half the minerals are good enough
mineral_rule = self.logic.action.can_open_geode(Generic.any)
good_fruits = (fruit
for fruit in
(Fruit.apple, Fruit.banana, Forageable.coconut, Forageable.crystal_fruit, Fruit.mango, Fruit.orange, Fruit.peach, Fruit.pomegranate,
Fruit.strawberry, Fruit.melon, Fruit.rhubarb, Fruit.pineapple, Fruit.ancient_fruit, Fruit.starfruit)
if fruit in self.content.game_items)
fruit_rule = self.logic.has_any(*good_fruits)
good_vegetables = (vegeteable
for vegeteable in
(Vegetable.amaranth, Vegetable.artichoke, Vegetable.beet, Vegetable.cauliflower, Forageable.fiddlehead_fern, Vegetable.kale,
@@ -173,8 +176,7 @@ SkillLogicMixin, RegionLogicMixin, ActionLogicMixin, MonsterLogicMixin, Relation
if vegeteable in self.content.game_items)
vegetable_rule = self.logic.has_any(*good_vegetables)
return animal_rule & artisan_rule & cooking_rule & fish_rule & \
forage_rule & fruit_rule & mineral_rule & vegetable_rule
return animal_rule & artisan_rule & cooking_rule & fish_rule & forage_rule & fruit_rule & mineral_rule & vegetable_rule
def can_win_fishing_competition(self) -> StardewRule:
return self.logic.skill.can_fish(difficulty=60)

View File

@@ -1,17 +1,8 @@
from typing import Union
from Utils import cache_self1
from .base_logic import BaseLogicMixin, BaseLogic
from .has_logic import HasLogicMixin
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from .season_logic import SeasonLogicMixin
from .skill_logic import SkillLogicMixin
from .tool_logic import ToolLogicMixin
from ..data import fish_data
from ..data.fish_data import FishItem
from ..options import ExcludeGingerIsland
from ..options import SpecialOrderLocations
from ..options import ExcludeGingerIsland, SpecialOrderLocations
from ..stardew_rule import StardewRule, True_, False_
from ..strings.ap_names.mods.mod_items import SVEQuestItem
from ..strings.craftable_names import Fishing
@@ -28,8 +19,7 @@ class FishingLogicMixin(BaseLogicMixin):
self.fishing = FishingLogic(*args, **kwargs)
class FishingLogic(BaseLogic[Union[HasLogicMixin, FishingLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, ToolLogicMixin,
SkillLogicMixin]]):
class FishingLogic(BaseLogic):
def can_fish_in_freshwater(self) -> StardewRule:
return self.logic.skill.can_fish() & self.logic.region.can_reach_any((Region.forest, Region.town, Region.mountain))

View File

@@ -1,7 +1,6 @@
from functools import cached_property
from .base_logic import BaseLogic, BaseLogicMixin
from .has_logic import HasLogicMixin
from ..stardew_rule import StardewRule
from ..strings.animal_product_names import AnimalProduct
from ..strings.gift_names import Gift
@@ -13,7 +12,7 @@ class GiftLogicMixin(BaseLogicMixin):
self.gifts = GiftLogic(*args, **kwargs)
class GiftLogic(BaseLogic[HasLogicMixin]):
class GiftLogic(BaseLogic):
@cached_property
def has_any_universal_love(self) -> StardewRule:

View File

@@ -1,5 +1,3 @@
import typing
from .base_logic import BaseLogic, BaseLogicMixin
from ..data.craftable_data import all_crafting_recipes_by_name
from ..data.recipe_data import all_cooking_recipes_by_name
@@ -12,11 +10,6 @@ from ..strings.quest_names import Quest
from ..strings.season_names import Season
from ..strings.wallet_item_names import Wallet
if typing.TYPE_CHECKING:
from .logic import StardewLogic
else:
StardewLogic = object
class GoalLogicMixin(BaseLogicMixin):
def __init__(self, *args, **kwargs):
@@ -24,7 +17,7 @@ class GoalLogicMixin(BaseLogicMixin):
self.goal = GoalLogic(*args, **kwargs)
class GoalLogic(BaseLogic[StardewLogic]):
class GoalLogic(BaseLogic):
def can_complete_community_center(self) -> StardewRule:
return self.logic.bundle.can_complete_community_center

View File

@@ -38,7 +38,7 @@ class GrindLogicMixin(BaseLogicMixin):
self.grind = GrindLogic(*args, **kwargs)
class GrindLogic(BaseLogic[Union[GrindLogicMixin, HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, BookLogicMixin, TimeLogicMixin, ToolLogicMixin]]):
class GrindLogic(BaseLogic):
def can_grind_mystery_boxes(self, quantity: int) -> StardewRule:
opening_rule = self.logic.region.can_reach(Region.blacksmith)

View File

@@ -1,15 +1,7 @@
from functools import cached_property
from typing import Union
from Utils import cache_self1
from .base_logic import BaseLogicMixin, BaseLogic
from .farming_logic import FarmingLogicMixin
from .has_logic import HasLogicMixin
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from .season_logic import SeasonLogicMixin
from .time_logic import TimeLogicMixin
from .tool_logic import ToolLogicMixin
from ..data.harvest import ForagingSource, HarvestFruitTreeSource, HarvestCropSource
from ..stardew_rule import StardewRule
from ..strings.ap_names.community_upgrade_names import CommunityUpgrade
@@ -22,8 +14,7 @@ class HarvestingLogicMixin(BaseLogicMixin):
self.harvesting = HarvestingLogic(*args, **kwargs)
class HarvestingLogic(BaseLogic[Union[HarvestingLogicMixin, HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, ToolLogicMixin,
FarmingLogicMixin, TimeLogicMixin]]):
class HarvestingLogic(BaseLogic):
@cached_property
def can_harvest_from_fruit_bats(self) -> StardewRule:

View File

@@ -2,7 +2,7 @@ from .base_logic import BaseLogic
from ..stardew_rule import StardewRule, And, Or, Has, Count, true_, false_, HasProgressionPercent
class HasLogicMixin(BaseLogic[None]):
class HasLogicMixin(BaseLogic):
true_ = true_
false_ = false_

View File

@@ -149,42 +149,37 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
# self.received("Deluxe Fertilizer Recipe") & self.has(MetalBar.iridium) & self.has(SVItem.sap),
# | (self.ability.can_cook() & self.relationship.has_hearts(NPC.emily, 3) & self.has(Forageable.leek) & self.has(Forageable.dandelion) &
# | (self.ability.can_cook() & self.relationship.has_hearts(NPC.jodi, 7) & self.has(AnimalProduct.cow_milk) & self.has(Ingredient.sugar)),
Animal.chicken: self.animal.can_buy_animal(Animal.chicken),
Animal.cow: self.animal.can_buy_animal(Animal.cow),
Animal.dinosaur: self.building.has_building(Building.big_coop) & self.has(AnimalProduct.dinosaur_egg),
Animal.duck: self.animal.can_buy_animal(Animal.duck),
Animal.goat: self.animal.can_buy_animal(Animal.goat),
Animal.ostrich: self.building.has_building(Building.barn) & self.has(AnimalProduct.ostrich_egg) & self.has(Machine.ostrich_incubator),
Animal.pig: self.animal.can_buy_animal(Animal.pig),
Animal.rabbit: self.animal.can_buy_animal(Animal.rabbit),
Animal.sheep: self.animal.can_buy_animal(Animal.sheep),
AnimalProduct.any_egg: self.has_any(AnimalProduct.chicken_egg, AnimalProduct.duck_egg),
AnimalProduct.brown_egg: self.animal.has_animal(Animal.chicken),
AnimalProduct.chicken_egg: self.has_any(AnimalProduct.egg, AnimalProduct.brown_egg, AnimalProduct.large_egg, AnimalProduct.large_brown_egg),
AnimalProduct.cow_milk: self.has_any(AnimalProduct.milk, AnimalProduct.large_milk),
AnimalProduct.duck_egg: self.animal.has_animal(Animal.duck),
AnimalProduct.duck_egg: self.animal.has_animal(Animal.duck), # Should also check starter
AnimalProduct.duck_feather: self.animal.has_happy_animal(Animal.duck),
AnimalProduct.egg: self.animal.has_animal(Animal.chicken),
AnimalProduct.goat_milk: self.has(Animal.goat),
AnimalProduct.golden_egg: self.received(AnimalProduct.golden_egg) & (self.money.can_spend_at(Region.ranch, 100000) | self.money.can_trade_at(Region.qi_walnut_room, Currency.qi_gem, 100)),
AnimalProduct.egg: self.animal.has_animal(Animal.chicken), # Should also check starter
AnimalProduct.goat_milk: self.animal.has_animal(Animal.goat),
AnimalProduct.golden_egg: self.has(AnimalProduct.golden_egg_starter), # Should also check golden chicken if there was an alternative to obtain it without golden egg
AnimalProduct.large_brown_egg: self.animal.has_happy_animal(Animal.chicken),
AnimalProduct.large_egg: self.animal.has_happy_animal(Animal.chicken),
AnimalProduct.large_goat_milk: self.animal.has_happy_animal(Animal.goat),
AnimalProduct.large_milk: self.animal.has_happy_animal(Animal.cow),
AnimalProduct.milk: self.animal.has_animal(Animal.cow),
AnimalProduct.ostrich_egg: self.tool.can_forage(Generic.any, Region.island_north, True) & self.has(Forageable.journal_scrap) & self.region.can_reach(Region.volcano_floor_5),
AnimalProduct.rabbit_foot: self.animal.has_happy_animal(Animal.rabbit),
AnimalProduct.roe: self.skill.can_fish() & self.building.has_building(Building.fish_pond),
AnimalProduct.squid_ink: self.mine.can_mine_in_the_mines_floor_81_120() | (self.building.has_building(Building.fish_pond) & self.has(Fish.squid)),
AnimalProduct.sturgeon_roe: self.has(Fish.sturgeon) & self.building.has_building(Building.fish_pond),
AnimalProduct.truffle: self.animal.has_animal(Animal.pig) & self.season.has_any_not_winter(),
AnimalProduct.void_egg: self.money.can_spend_at(Region.sewer, 5000) | (self.building.has_building(Building.fish_pond) & self.has(Fish.void_salmon)),
AnimalProduct.void_egg: self.has(AnimalProduct.void_egg_starter), # Should also check void chicken if there was an alternative to obtain it without void egg
AnimalProduct.wool: self.animal.has_animal(Animal.rabbit) | self.animal.has_animal(Animal.sheep),
AnimalProduct.slime_egg_green: self.has(Machine.slime_egg_press) & self.has(Loot.slime),
AnimalProduct.slime_egg_blue: self.has(Machine.slime_egg_press) & self.has(Loot.slime) & self.time.has_lived_months(3),
AnimalProduct.slime_egg_red: self.has(Machine.slime_egg_press) & self.has(Loot.slime) & self.time.has_lived_months(6),
AnimalProduct.slime_egg_purple: self.has(Machine.slime_egg_press) & self.has(Loot.slime) & self.time.has_lived_months(9),
AnimalProduct.slime_egg_tiger: self.has(Fish.lionfish) & self.building.has_building(Building.fish_pond),
AnimalProduct.duck_egg_starter: self.logic.false_, # It could be purchased at the Feast of the Winter Star, but it's random every year, so not considering it yet...
AnimalProduct.dinosaur_egg_starter: self.logic.false_, # Dinosaur eggs are also part of the museum rules, and I don't want to touch them yet.
AnimalProduct.egg_starter: self.logic.false_, # It could be purchased at the Desert Festival, but festival logic is quite a mess, so not considering it yet...
AnimalProduct.golden_egg_starter: self.received(AnimalProduct.golden_egg) & (self.money.can_spend_at(Region.ranch, 100000) | self.money.can_trade_at(Region.qi_walnut_room, Currency.qi_gem, 100)),
AnimalProduct.void_egg_starter: self.money.can_spend_at(Region.sewer, 5000) | (self.building.has_building(Building.fish_pond) & self.has(Fish.void_salmon)),
ArtisanGood.aged_roe: self.artisan.can_preserves_jar(AnimalProduct.roe),
ArtisanGood.battery_pack: (self.has(Machine.lightning_rod) & self.season.has_any_not_winter()) | self.has(Machine.solar_panel),
ArtisanGood.caviar: self.artisan.can_preserves_jar(AnimalProduct.sturgeon_roe),

View File

@@ -12,7 +12,7 @@ class TimeLogicMixin(BaseLogicMixin):
self.time = TimeLogic(*args, **kwargs)
class TimeLogic(BaseLogic[Union[TimeLogicMixin, ReceivedLogicMixin]]):
class TimeLogic(BaseLogic):
def has_lived_months(self, number: int) -> StardewRule:
return self.logic.received(Event.month_end, number)
@@ -72,4 +72,16 @@ of source (Monster drop and fish can have foraging sources).
if easy logic is disabled. For instance, anything that requires money could be accessible as soon as you can sell something to someone (even wood).
Items are classified by their source. An item with a fishing or a crab pot source is considered a fish, an item dropping from a monster is a monster drop. An
item with a foraging source is a forageable. Items can fit in multiple categories.
item with a foraging source is a forageable. Items can fit in multiple categories.
## Prefer rich class to anemic list of sources
For game mechanic that might need more logic/interaction than a simple game item, prefer creating a class than just listing the sources and adding generic
requirements to them. This will simplify the implementation of more complex mechanics and increase cohesion.
For instance, `Building` can be upgraded. Instead of having a simple source for the `Big Coop` being a shop source with an additional requirement being having
the previous building, the `Building` class has knowledge of the upgrade system and know from which building it can be upgraded.
Another example is `Animal`. Instead of a shopping source with a requirement of having a `Coop`, the `Chicken` knows that a building is required. This way, a
potential source of chicken from incubating an egg would not require an additional requirement of having a coop (assuming the incubator could be obtained
without a big coop).

View File

@@ -1,14 +1,5 @@
from typing import Union
from Utils import cache_self1
from .base_logic import BaseLogicMixin, BaseLogic
from .combat_logic import CombatLogicMixin
from .cooking_logic import CookingLogicMixin
from .has_logic import HasLogicMixin
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from .skill_logic import SkillLogicMixin
from .tool_logic import ToolLogicMixin
from .. import options
from ..stardew_rule import StardewRule, True_
from ..strings.performance_names import Performance
@@ -23,8 +14,7 @@ class MineLogicMixin(BaseLogicMixin):
self.mine = MineLogic(*args, **kwargs)
class MineLogic(BaseLogic[Union[HasLogicMixin, MineLogicMixin, RegionLogicMixin, ReceivedLogicMixin, CombatLogicMixin, ToolLogicMixin,
SkillLogicMixin, CookingLogicMixin]]):
class MineLogic(BaseLogic):
# Regions
def can_mine_in_the_mines_floor_1_40(self) -> StardewRule:
return self.logic.region.can_reach(Region.mines_floor_5)

View File

@@ -1,25 +1,11 @@
import typing
from typing import Union
from Utils import cache_self1
from .base_logic import BaseLogicMixin, BaseLogic
from .grind_logic import GrindLogicMixin
from .has_logic import HasLogicMixin
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from .season_logic import SeasonLogicMixin
from .time_logic import TimeLogicMixin
from ..data.shop import ShopSource
from ..options import SpecialOrderLocations
from ..stardew_rule import StardewRule, True_, HasProgressionPercent, False_, true_
from ..strings.currency_names import Currency
from ..strings.region_names import Region, LogicRegion
if typing.TYPE_CHECKING:
from .shipping_logic import ShippingLogicMixin
else:
ShippingLogicMixin = object
qi_gem_rewards = ("100 Qi Gems", "50 Qi Gems", "40 Qi Gems", "35 Qi Gems", "25 Qi Gems",
"20 Qi Gems", "15 Qi Gems", "10 Qi Gems")
@@ -30,13 +16,12 @@ class MoneyLogicMixin(BaseLogicMixin):
self.money = MoneyLogic(*args, **kwargs)
class MoneyLogic(BaseLogic[Union[RegionLogicMixin, MoneyLogicMixin, TimeLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin, SeasonLogicMixin,
GrindLogicMixin, ShippingLogicMixin]]):
class MoneyLogic(BaseLogic):
@cache_self1
def can_have_earned_total(self, amount: int) -> StardewRule:
if amount < 1000:
return True_()
if amount <= 1000:
return self.logic.true_
pierre_rule = self.logic.region.can_reach_all((Region.pierre_store, Region.forest))
willy_rule = self.logic.region.can_reach_all((Region.fish_shop, LogicRegion.fishing))
@@ -44,19 +29,19 @@ GrindLogicMixin, ShippingLogicMixin]]):
robin_rule = self.logic.region.can_reach_all((Region.carpenter, Region.secret_woods))
shipping_rule = self.logic.shipping.can_use_shipping_bin
if amount < 2000:
if amount <= 2500:
selling_any_rule = pierre_rule | willy_rule | clint_rule | robin_rule | shipping_rule
return selling_any_rule
if amount < 5000:
if amount <= 5000:
selling_all_rule = (pierre_rule & willy_rule & clint_rule & robin_rule) | shipping_rule
return selling_all_rule
if amount < 10000:
if amount <= 10000:
return shipping_rule
seed_rules = self.logic.region.can_reach(Region.pierre_store)
if amount < 40000:
if amount <= 40000:
return shipping_rule & seed_rules
percent_progression_items_needed = min(90, amount // 20000)

View File

@@ -20,7 +20,7 @@ class MonsterLogicMixin(BaseLogicMixin):
self.monster = MonsterLogic(*args, **kwargs)
class MonsterLogic(BaseLogic[Union[HasLogicMixin, MonsterLogicMixin, RegionLogicMixin, CombatLogicMixin, TimeLogicMixin]]):
class MonsterLogic(BaseLogic):
@cached_property
def all_monsters_by_name(self):

View File

@@ -22,7 +22,7 @@ class MuseumLogicMixin(BaseLogicMixin):
self.museum = MuseumLogic(*args, **kwargs)
class MuseumLogic(BaseLogic[Union[ReceivedLogicMixin, HasLogicMixin, TimeLogicMixin, RegionLogicMixin, ActionLogicMixin, ToolLogicMixin, MuseumLogicMixin]]):
class MuseumLogic(BaseLogic):
def can_donate_museum_items(self, number: int) -> StardewRule:
return self.logic.region.can_reach(Region.museum) & self.logic.museum.can_find_museum_items(number)

View File

@@ -1,11 +1,6 @@
import math
from typing import Union
from .base_logic import BaseLogicMixin, BaseLogic
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from .time_logic import TimeLogicMixin
from .tool_logic import ToolLogicMixin
from ..content.feature.friendsanity import pet_heart_item_name
from ..stardew_rule import StardewRule, True_
from ..strings.region_names import Region
@@ -17,7 +12,7 @@ class PetLogicMixin(BaseLogicMixin):
self.pet = PetLogic(*args, **kwargs)
class PetLogic(BaseLogic[Union[RegionLogicMixin, ReceivedLogicMixin, TimeLogicMixin, ToolLogicMixin]]):
class PetLogic(BaseLogic):
def has_pet_hearts(self, hearts: int = 1) -> StardewRule:
assert hearts >= 0, "You can't have negative hearts with a pet."
if hearts == 0:

View File

@@ -14,7 +14,7 @@ class QualityLogicMixin(BaseLogicMixin):
self.quality = QualityLogic(*args, **kwargs)
class QualityLogic(BaseLogic[Union[SkillLogicMixin, FarmingLogicMixin]]):
class QualityLogic(BaseLogic):
@cache_self1
def can_grow_crop_quality(self, quality: str) -> StardewRule:

Some files were not shown because too many files have changed in this diff Show More